낙서장이자 오답 노트이자 컨닝 페이퍼
패스트캠퍼스 환급챌린지 12일차 : 코드팩토리의 백엔드 아카데미 : 한 번에 끝내는 NestJS 패키지 - 기초부터 MSA까지 강의 후기 본문
패스트캠퍼스 환급챌린지 12일차 : 코드팩토리의 백엔드 아카데미 : 한 번에 끝내는 NestJS 패키지 - 기초부터 MSA까지 강의 후기
NangIn 2025. 4. 12. 14:35본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.
강의 내용 정리
TypeORM으로 테이블 생성하기
Entity 클래스 정의
- movie.entity.ts
-
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, VersionColumn, } from 'typeorm'; @Entity() export class Movie { @PrimaryGeneratedColumn() id: number; @Column() title: string; @Column() genre: string; @CreateDateColumn() createdAt: Date; @UpdateDateColumn() updatedAt: Date; @VersionColumn() version: number; }- @Entity()는 이 클래스가 테이블로 사용된다는 의미 → 이걸 빼먹으면 테이블이 안 생김!
-
AppModule에 Entity 등록
- app.module.ts 또는 연결한 모듈에서:
-
TypeOrmModule.forRootAsync({ // ...config 설정 useFactory: (configService: ConfigService) => ({ // ... entities: [Movie], // <= 여기에 등록 synchronize: true, // 개발 단계에서만 true }), })
pgAdmin으로 확인
- 테이블 자동 생성됨
- 칼럼: id, title, genre, createdAt, updatedAt, version 전부 생성 완료
- SQL 한 줄 없이 클래스 + 데코레이터만으로 DB 스키마 완성
특수 칼럼 설명
| 데코레이터 | 역할 |
| @CreateDateColumn() | 생성 시 자동 날짜 저장 (createdAt) |
| @UpdateDateColumn() | 수정 시 자동 날짜 갱신 (updatedAt) |
| @VersionColumn() | 수정될 때마다 버전 증가 (version) |
자주 하는 실수들
- @Entity() 빠뜨림 → 테이블 생성 안 됨
- 엔티티를 TypeOrmModule의 entities에 등록 안 함 → 마찬가지로 테이블 안 생김
- synchronize: true 설정 누락 → 자동 스키마 생성 안 됨
Repository CRUD 작업 이론
기본 개념
- Repository 패턴: 특정 엔티티에 대한 DB 작업을 모아놓은 객체
- TypeORM에서 repository는 CRUD 작업을 쉽게 하기 위한 도구
- DataSource.getRepository(Entity) 형태로 가져옴
Create 관련
- create(data)
- 객체만 생성, DB에는 저장 안 됨
- 그냥 TS 객체 생성기라고 생각하면 됨
-
const user = repository.create({ id: 1, firstName: "Timber", lastName: "Saw", })
-
- save(entity)
- DB에 저장됨
- id(PK)가 있으면 업데이트, 없으면 생성
- id를 같이 넘기지 않으면 바로 생성 및 저장
- id를 같이 넘기면 DB에서 그 id를 갖는 데이터가 존재하는지 찾음. 존재하면 없데이트, 존재하지 않으면 저장
- 여러 개 저장도 가능
-
await repository.save(user) await repository.save([ category1, category2, category3, ])
-
- upsert(entities, [ conflictPaths ])
- 존재하면 업데이트, 없으면 생성 (한 트랜잭션 내에서 처리됨)
- conflictPaths: 기준 컬럼 (보통 id)
-
await repository.upsert( [ { externalTd: "abc123", firstName: "Code" }, { externalTd: "bca321", firstName: "Factory" }, ], ["externalId"], )
-
Delete 관련
- delete(condition)
- row 삭제 (주로 id 기준)
- 원한다면 findOptionsWhere 조건으로 여러 값을 삭제할 수도 있음
-
await repository.delete(1) await repository.delete([1, 2, 3]) await repository.delete({ firstName: 'Timber' })
-
- softDelete(condition)
- 비영구적으로 삭제, deletedAt만 설정
- 복구 가능
-
// 삭제 await repository.softDelete(1)
-
- restore(condition)
- soft delete된 row 복구
-
// 복구 await repository.restore(1)
-
- soft delete된 row 복구
Update 관련
- update(condition, changes)
- SQL의 UPDATE ... SET ... WHERE ...과 같음
- 첫번째 파라미터에 검색 조건을 입력
- 두번째 파라미터에 변경 필드를 입력
-
// UPDATE user // SET category = ADULT // WHERE age = 18 await repository.update( { age: 18 }, { category: "ADULT"} ) // UPDATE user // SET firstName = Code Factory // WHERE id = 1 await repository.update(1, { firstName: "Code Factory"} )
-
- increment() / decrement()
- 숫자 컬럼에 대해 증가/감소
조회 관련 (Find)
- find(): 해당되는 row를 모두 반환
- findOne(): 조건에 맞는 첫 번째 한 개 반환. 없을 경우 null
- findAndCount(): [데이터 배열, 총 개수] 반환
-
const rows = await repository.find({ where: { firstName: "Code Factory", }, }) const row = await repository.findOne({ where: { firstName: "Code Factory", }, }) const [rows, count] = await repository.findAndCount({ where: { firstName: "Code Factory", }, })
-
- exists(): 조건 만족하는 row가 있는지 boolean 반환
-
const exists = await repository.exists({ where: { firstName: "Timber", } })
-
- preload(data): → 생각보다 좀 쓴다!
- 데이터베이스에 저장된 값을 PK 기준으로 불러오고 입력된 객체의 값으로 프로퍼티를 덮어씀
- 최종 객체 반환 (저장은 안 됨 → save() 필요)
-
const partialUser = { id: 1, firstName: "Code Factory", profile: { id: 1, } } const user = await repository.preload(partialUser);
-
Find Options 사용법
- 설명
- 모든 find 관련된 API는 FindOptions를 인자로 받음
- FindOptions는 어떤 값들을 불러올지 필터링하는 역할을 함
- FindOptions의 정확한 TS 타입 명칭은 FindOneOptions와 FindManyOptions로 정의됨
- FindManyOptions는 FindOneOptions를 상속받고 skip, take 두가지 프로퍼티가 더 존재
- 공통 속성
속성 설명 select 불러올 컬럼 지정 where 필터링 조건 설정 relations 불러올 관계 테이블 지정 order 정렬 (asc, desc) cache 캐싱 기간 지정 (ms 단위 or true) - findMany 전용
속성 설명 skip 앞에서 몇 개 건너뛸지 take 몇 개 가져올지 - WHERE Property
-
// 기본 사용법 const users = await repository.find({ where: { isActive: true }, }) // 중첩 사용법 -> 관계 테이블과 연계해서 쿼리 가능 const users = await repository.find({ where: { isActive: true, profile: { age: MoreThan(25) }, }, }) // 다중 조건 사용법 // 이 조건은 → (john AND doe) OR (jane AND smith) const users = await repository.find({ where: [ { firstName: 'john', lastName: 'doe' }, { firstName: 'jane', lastName: 'smith' } ] })
-
- ORDER Property
-
// 단일 정렬 사용법 const users = await repository.find({ order: { firstName: 'ASC' } }) // 복수 정렬 사용법 // firstName 먼저 오름차순, 그 후에 lastName 내림차순 정렬 const users = await repository.find({ order: { firstName: 'ASC', lastName: 'DESC' } })
-
- RELATIONS Property
-
// 해당 테이블에 해당되는 값들을 조인해서 가져옴 const users = await repository.find({ relations: ['profile', 'photos'] })
-
- SELECT Property
-
// 선택한 컬럼만 가져옴 const users = await repository.find({ select: ['firstName', 'lastName'] })
-
- CACHE Property
-
// 기본 사용법 const users = await repository.find({ cache: true // 1초 캐시 }) // 기간 직접 정의 const users = await repository.find({ cache: 5000 // 5초 캐시 })
-
- 페이지네이션 (skip / take) Property
-
// 10개 건너뛰고 5개 가져오기 const users = await repository.find({ skip: 10, take: 5 })
-
Movie API에 Repository 적용해보기
MovieModule에 TypeORM 연동
- TypeOrmModule.forFeature([Movie]) 사용하여 사용할 엔티티 등록
-
// movie.module.ts @Module({ imports: [TypeOrmModule.forFeature([Movie])], ... })
-
MovieService에서 Repository 주입
- TypeORM에서 MovieRepository 생성하고 IOC 컨테이너가 자동 주입할 수 있게 설정
-
// movie.service.ts constructor( @InjectRepository(Movie) private readonly movieRepository: Repository<Movie>, ) {}
-
Create (저장)
- save() 메서드 사용
- DTO 전체를 넘겨 저장
-
async createMovie(createMovieDto: CreateMovieDto) { const movie = await this.movieRepository.save(createMovieDto); return movie; }
-
Read (조회)
- 전체 조회
-
getManyMovies() { return this.movieRepository.find(); }
-
- 특정 ID 조회
-
async getMovieById(id: number) { const movie = await this.movieRepository.findOne({ where: { id }, }); if (!movie) throw new NotFoundException('존재하지 않는 영화입니다.'); return movie; }
-
Update (수정)
- 먼저 데이터 존재 확인 → 없으면 에러
- update() 메서드 사용
- 이후 findOne()으로 갱신된 데이터 반환
-
async updateMovie(id: number, updateMovieDto: UpdateMovieDto) { const movie = await this.movieRepository.findOne({ where: { id }, }); if (!movie) throw new NotFoundException('존재하지 않는 영화입니다.'); await this.movieRepository.update(id, updateMovieDto); const newMovie = await this.movieRepository.findOne({ where: { id }, }); return newMovie; }
-
Delete (삭제)
- 데이터 존재 확인 후 delete() 호출
-
async deleteMovie(id: number) { const movie = await this.movieRepository.findOne({ where: { id }, }); if (!movie) throw new NotFoundException('존재하지 않는 영화입니다.'); await this.movieRepository.delete(id); return id; }
-
특수 기능
- @CreateDateColumn, @UpdateDateColumn, @VersionColumn 을 활용해:
- 생성일/수정일/버전 자동 관리
- 서버 재시작해도 DB 유지됨 → 영속성 확인 완료
학습 후기
이번 학습을 통해 TypeORM의 엔티티 기반 설계와 레포지토리 활용법에 대해 실전적으로 익힐 수 있었습니다. 특히 클래스에 데코레이터만 붙여도 SQL 없이 테이블이 자동 생성된다는 점은 개발 생산성을 크게 높여준다는 걸 체감했습니다. 단순한 CRUD 로직도 레포지토리 패턴을 사용하니 코드가 더 깔끔하고 목적에 맞게 분리되어 유지보수에도 도움이 될 것 같습니다. @CreateDateColumn이나 @VersionColumn 등 특수 칼럼을 활용하면 데이터 변경 이력을 자동으로 추적할 수 있어, 이런 기능들이 실무에서 얼마나 유용할지 떠올려보게 되었습니다.
레포지토리 메서드들 중에서 특히 유용하다고 느낀 건 findAndCount, preload, upsert 같은 고급 기능이었습니다. preload를 통해 기존 데이터를 로딩하고 DTO로 수정할 수 있는 흐름은, 유저 편집과 같이 실무에서 자주 쓰이는 시나리오에 바로 적용할 수 있겠다는 인사이트를 얻었습니다. 또한 조건부 조회나 정렬, 관계 테이블 조인까지도 간단한 옵션 객체로 구현 가능한 점은 복잡한 SQL 없이도 유연한 쿼리를 만들 수 있다는 장점을 잘 보여줍니다.
마지막으로, 단순히 기능을 암기하는 것이 아니라 API 개발 전반에 걸친 흐름을 이해할 수 있어 좋았습니다. 모듈에 엔티티를 등록하고, 서비스에서 레포지토리를 주입받아 사용하는 과정, 그리고 실제 API를 통해 DB가 어떻게 반응하는지를 전부 경험하면서 NestJS + TypeORM 조합의 강력함을 다시금 느꼈습니다. 앞으로 더 복잡한 관계 설정이나 트랜잭션 제어도 자연스럽게 이어질 것 같아 기대가 됩니다.
학습 인증샷



