낙서장이자 오답 노트이자 컨닝 페이퍼

패스트캠퍼스 환급챌린지 12일차 : 코드팩토리의 백엔드 아카데미 : 한 번에 끝내는 NestJS 패키지 - 기초부터 MSA까지 강의 후기 본문

NestJS

패스트캠퍼스 환급챌린지 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)
        

 

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 조합의 강력함을 다시금 느꼈습니다. 앞으로 더 복잡한 관계 설정이나 트랜잭션 제어도 자연스럽게 이어질 것 같아 기대가 됩니다.

 

학습 인증샷       

                       

수강 인증 사진
학습 인증샷
공부 시작 시간

             

공부 종료 시간

https://abit.ly/lisbva