본문 바로가기
패스트캠퍼스 환급챌린지

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

by 논문보관함 2025. 3. 18.

본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.

https://bit.ly/4hTSJNB

 

커리어 성장을 위한 최고의 실무교육 아카데미 | 패스트캠퍼스

성인 교육 서비스 기업, 패스트캠퍼스는 개인과 조직의 실질적인 '업(業)'의 성장을 돕고자 모든 종류의 교육 콘텐츠 서비스를 제공하는 대한민국 No. 1 교육 서비스 회사입니다.

fastcampus.co.kr

0318 화

학습 내용 정리

QueryBuilder

QueryBuilder 사용하는 방식으로 로직 변경하기

 

    // if(!title){
    //   return [await this.movieRepository.find({
    //     relations:["director",'genres']
    //   }), await this.movieRepository.count()];
    // }
    // return this.movieRepository.findAndCount({
    //   where: {
    //     title: Like(`%${title}%`),
    //   },
    //   relations: ['director','genres']
    // });
    if(title){
      qb.where('movie.title LIKE :title',{title: `%${title}%`})
    }

 

두개의 셀이 같은 기능이다.

그리고 더욱 간편하고, 사람이 읽기 쉽다.

하지만

create endpoint에서는 manytomany 같은 기능도 따로 구현해줘야한다.

즉, 코드가 엄청 길어진다.

    // const movie = await this.movieRepository.save({
    //   title: createMovieDto.title,
    //   detail: {
    //     //Cascade 옵션 사용
    //     detail: createMovieDto.detail,
    //   },
    //   director,
    //   genres,
    // });

위의 코드가 아래 처럼 길어진다.

    const director = await this.directorRepository.findOne({
      where: {
        id: createMovieDto.directorID,
      },
    });

    if (!director) {
      throw new NotFoundException('존재하지않는 감독')
    }

    const genres = await this.genreRepository.find({
      where: {
        id: In(createMovieDto.genreIds),
      },
    });

    if (genres.length !== createMovieDto.genreIds.length) {
      throw new NotFoundException(`존재하지않는 장르가 있습니다! 존재하는 ids ->${genres.map(genre => genre.id).join(',')}`)
    }

    const movieDetail = await this.movieDetailRepository.createQueryBuilder()
      .insert()
      .into(MovieDetail)
      .values({
        detail: createMovieDto.detail,
      })
      .execute();

    const movieDetailId = movieDetail.identifiers[0].id;

    const movie = await this.movieRepository.createQueryBuilder()
      .insert()
      .into(Movie)
      .values({
        title: createMovieDto.title,
        detail: {
          //Cascade 옵션 사용
          id: movieDetailId,
        },
        director,
        //manytomany는 안된다. 따로 해야한다
      })
      .execute();

      const movieId = movie.identifiers[0].id;

      await this.movieRepository.createQueryBuilder()
      .relation(Movie,'genres')
      .of(movieId)
      .add(genres.map(genre => genre.id))

 

 

 

 

 

학습후기: QueryBuilder 사용 방식의 전환과 적용 경험

이번 학습에서는 기존 Repository API 방식에서 QueryBuilder를 활용하는 방식으로 로직을 변경하는 과정에 대해 깊이 있게 다루었다. QueryBuilder는 복잡한 쿼리를 더욱 직관적이고 유연하게 작성할 수 있도록 돕는 도구로, 기존의 find 및 save 메서드와 비교하여 몇 가지 장점과 단점이 있었다.

1. QueryBuilder 사용 방식 전환의 필요성

기존 Repository API를 사용할 경우, findAndCount() 등의 메서드를 활용하여 데이터를 가져오는 로직이 단순하면서도 직관적이었다. 그러나 이 방식은 복잡한 조건을 가진 쿼리를 작성할 때 제약이 많았으며, 성능 최적화가 어렵다는 한계가 있었다. 예를 들어, title 검색을 수행하는 기존 코드에서는 Like 조건을 활용하여 간단한 쿼리를 만들 수 있었지만, 더 정교한 필터링이나 조인을 추가할 경우 가독성이 떨어지고 유지보수가 어려워지는 문제가 있었다.

이러한 문제를 해결하기 위해 QueryBuilder를 활용하면, where, join, orderBy 등의 다양한 SQL 기능을 직관적으로 조합할 수 있었다. 특히 where('movie.title LIKE :title', { title: %${title}% }) 형태의 코드가 Repository API의 find 메서드보다 가독성이 높았고, join을 활용하여 복잡한 데이터 구조를 한 번의 쿼리로 조회할 수 있었다. 결과적으로, 쿼리 성능 최적화 및 유지보수 측면에서 QueryBuilder 방식이 더 효율적이라는 점을 확인할 수 있었다.

2. QueryBuilder 적용 후 개선점

QueryBuilder를 적용하면서 얻은 주요 개선점은 다음과 같았다.

  1. 가독성 향상
    • 기존 Repository API 방식에서는 find()와 findAndCount() 등의 메서드 안에서 where 조건을 작성하는 방식이었는데, QueryBuilder를 사용하면 SQL과 유사한 형태로 작성할 수 있어 직관적이었다.
    • 특히, 조건문(if(title))을 활용하여 쿼리를 동적으로 조정하는 것이 쉬워졌고, andWhere, orWhere 등을 조합하여 더욱 세밀한 쿼리 작성을 할 수 있었다.
  2. 유연한 조인 및 관계 처리
    • 기존 방식에서는 relations 옵션을 통해 director, genres 등을 가져왔지만, QueryBuilder를 사용하면 leftJoinAndSelect() 등을 활용하여 더 정교한 관계 설정이 가능했다.
    • 예를 들어, 특정 감독이 연출한 영화 목록을 조회하면서, 해당 영화의 장르 정보를 함께 가져오는 복합적인 쿼리를 더욱 쉽게 작성할 수 있었다.
  3. 쿼리 최적화 및 성능 개선
    • QueryBuilder는 limit, offset, groupBy 등의 기능을 더욱 직관적으로 사용할 수 있어, 성능 최적화가 용이했다.
    • 필요에 따라 getQuery()를 활용하여 실제 생성된 SQL을 확인할 수도 있어, 성능 테스트 및 디버깅이 용이했다.

3. QueryBuilder의 단점 및 해결 방안

QueryBuilder가 가진 강점에도 불구하고 몇 가지 단점도 존재했다.

  1. 코드가 길어짐
    • 기존 save() 방식에서는 cascade 옵션을 활용하여 MovieDetail을 자동으로 저장할 수 있었지만, QueryBuilder 방식에서는 insert()와 execute()를 사용하여 각각 처리해야 했다.
    • 특히, ManyToMany 관계에서는 relation().of().add() 등의 추가 작업이 필요하여 코드가 상당히 길어지는 단점이 있었다.
    • 해결 방안으로는 반복되는 QueryBuilder 로직을 service 계층에서 별도의 메서드로 분리하여 코드의 재사용성을 높이는 방법을 고려할 수 있었다.
  2. 트랜잭션 처리의 필요성 증가
    • QueryBuilder를 사용할 경우 여러 개의 insert 문이 실행되므로, 데이터 정합성을 유지하기 위해 트랜잭션 처리가 필요했다.
    • NestJS의 Transactional 데코레이터를 활용하여 여러 쿼리를 하나의 트랜잭션으로 묶는 방식을 적용하는 것이 필요했다.

4. 학습 후 얻은 인사이트

이번 학습을 통해 QueryBuilder를 활용한 데이터 조회 및 삽입 방식에 대해 깊이 이해할 수 있었으며, 다음과 같은 중요한 인사이트를 얻을 수 있었다.

  1. 단순한 CRUD 작업에는 Repository API가 더 적합
    • 기본적인 데이터 조회, 생성, 수정, 삭제 작업의 경우 Repository API가 더 직관적이며 코드가 간결하다.
    • 따라서 QueryBuilder를 적용할 필요가 없는 경우에는 기존 방식을 유지하는 것이 유지보수성 측면에서 더 좋다.
  2. 복잡한 관계 및 동적 필터링이 필요한 경우 QueryBuilder가 유리
    • QueryBuilder를 활용하면 join을 효율적으로 사용할 수 있고, 조건을 동적으로 추가할 수 있어 유연성이 높다.
    • 특히, 데이터가 많아지고 복잡한 비즈니스 로직이 추가될 경우, 성능 최적화 측면에서도 유리하다.
  3. ManyToMany 관계 처리 시 QueryBuilder의 추가 작업 필요
    • ManyToMany 관계에서는 relation().of().add()를 활용하여 관계 데이터를 별도로 추가해야 한다는 점을 유념해야 한다.
    • 이를 위해 Service 계층에서 helper function을 만들어 반복되는 작업을 줄이는 것이 효과적이다.

5. 향후 적용 계획

이번 학습을 바탕으로 향후 프로젝트에서 다음과 같은 방식으로 적용할 계획이다.

  1. 단순 조회 및 저장은 Repository API 유지
    • 불필요하게 QueryBuilder를 남용하지 않고, 단순한 CRUD 작업에는 기존 방식을 유지한다.
  2. 복잡한 조건이 필요한 경우 QueryBuilder 적극 활용
    • 여러 조건이 조합된 검색 기능을 구현할 때 QueryBuilder를 적극 활용하여 가독성과 성능을 개선한다.
  3. ManyToMany 및 관계 삽입 처리 로직 모듈화
    • ManyToMany 관계를 처리하는 별도의 헬퍼 메서드를 만들어 반복되는 코드를 줄인다.
  4. 트랜잭션을 활용한 데이터 정합성 유지
    • QueryBuilder로 여러 개의 insert 및 update를 수행할 때, NestJS의 트랜잭션 기능을 활용하여 데이터 정합성을 유지한다.

 

 

 

 

 

 

 

사진

종료
수강클립
결과물