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

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

NestJS

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

NangIn 2025. 4. 16. 11:27

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

 

강의 내용 정리   

 

One to One Relationship Create 작업해보기

 

구현 목표 

항목 설명
기능 Movie 생성 시 MovieDetail도 함께 생성
관계 One-to-One (Movie ↔ MovieDetail)
설계 원칙 프론트엔드에서 직관적인 구조로 요청을 받되, 서버 내부적으로 객체 생성 분리
핵심 포인트 프론트에서는 detail: string만 넘기고, 서버에서 이를 MovieDetail 객체로 생성 후 Movie에 연결

 

API 요청 구조 설계

  • // ✅ 프론트엔드에서 보낼 형태 (직관적 구조)
    {
      "title": "반지의 제왕",
      "genre": "판타지",
      "detail": "프로도가 절대반지를 파괴하는 내용입니다"
    }
    
  • 비추천 구조 (내부 구현체에 종속됨)
    • {
        "title": "반지의 제왕",
        "genre": "판타지",
        "detail": {
          "detail": "내용입니다"
        }
      }
      
  • 중요 개념:
    • 백엔드의 Entity 구조를 프론트에 강요 X
    • 상식적으로 납득되는 구조로 데이터를 받고, 내부에서 가공하는 것이 좋은 설계!.

 

DTO 설계

  • CreateMovieDto
    • export class CreateMovieDto {
        @IsNotEmpty()
        title: string;
      
        @IsNotEmpty()
        genre: string;
        
        @IsNotEmpty()
        detail: string;
      }
      

 

Service 로직

  • async createMovie(createMovieDto: CreateMovieDto) {
      // 1. MovieDetail 먼저 생성
      const movieDetail = await this.movieDetailRepository.save({
        detail: createMovieDto.detail,
      });
    
      // 2. Movie 생성 및 MovieDetail 연결
      const movie = await this.movieRepository.save({
        title: createMovieDto.title,
        genre: createMovieDto.genre,
        detail: movieDetail,
      });
    
      return movie;
    }
    
    • 내부 객체 관계 연결 흐름
      • detail 문자열 → MovieDetail 객체로 변환
      • MovieDetail을 저장
      • 해당 MovieDetail을 Movie에 detail로 설정
      • Movie 저장

 

의존성 주입

  • movie.service.ts
    • constructor(
        @InjectRepository(Movie) private movieRepository: Repository<Movie>,
        @InjectRepository(MovieDetail) private movieDetailRepository: Repository<MovieDetail>
      ) {}
      
  • movie.module.ts
    • TypeOrmModule.forFeature([Movie, MovieDetail]) // 둘 다 등록!
      
  • 에러 방지: Nest can't resolve dependency 오류 → 대부분 이거 안 넣어서 발생함

 

DB 결과 확인

  • movie 테이블 
    id  title genre  detail_id
    1 반지의 제왕 판타지 7
  • movie_detail 테이블 
    id detail
    7 프로도가 절대반지를 파괴하는 내용입니다

 

  • ID가 중간에 비는 건 정상 (1, 3, 5, 13)
    • 예: 삭제, 롤백, 트랜잭션 실패 등
    • ID는 항상 auto increment로 비연속 가능

 

정리 

핵심 포인트 요약
DTO 설계 프론트엔드에 직관적인 구조 제공
내부 로직 MovieDetail 따로 생성 후 Movie에 연결
유지보수 Entity 구조가 변경돼도 외부 API 계약은 유지 가능
추후 개선 트랜잭션 처리로 무결성 확보 예정

 

One to One Relationship Read 작업해보기

 

목표 

구분 설명
기능 Movie 단일 조회 시 MovieDetail 같이 가져오기
방법 TypeORM의 relations 옵션 활용
이유 상세 페이지에서만 필요한 데이터를 선별적으로 응답하기 위함

 

기본 구조: findOne({ where, relations })

  • const movie = await this.movieRepository.findOne({
      where: { id },
      relations: ['detail'], // ← 관계 필드명 명시
    });
    
    • relations는 프로퍼티 이름 그대로 문자열로 작성
    • 여러 개 관계도 가능: relations: ['detail', 'director']

 

실무 UX 전략

용도  설명
리스트 조회 필수 정보만 반환 (빠르게 응답)
상세 페이지 relations 포함한 상세 정보 함께 반환
  • 예시: 쿠팡, 배민 등에서 리스트 → 상세 클릭 구조
    • 리스트에선 썸네일, 제목, 평점 정도만 필요
    • 상세에서는 설명, 리뷰 등 추가 정보 필요

 

응용: 전체 목록에서도 relations 사용 가능

  • getManyMovies() {
      return this.movieRepository.find({
        relations: ['detail'],
      });
    }
    
  • 단, 실무에서는 지양
    • 많은 데이터를 불러오면 성능 저하
    • 리스트에서는 최소 정보만 응답하는 것이 일반적

 

정리 

항목 요약
기능 TypeORM의 relations로 관련 테이블 함께 조회
장점 별도 조인 쿼리 없이 TypeScript 코드만으로 관계 데이터 조작 가능
실무 활용 리스트/상세 API를 구분해 필요한 정보만 제공

 

One to One Relationship Update 작업해보기

 

목표 

항목 설명
작업 Movie와 연결된 MovieDetail을 동시에 수정
조건 detail 값이 있을 경우에만 MovieDetail도 업데이트

 

DTO 수정: UpdateMovieDto

  • 기존에는 title, genre만 받았지만 이제 detail도 받을 수 있도록 수정
  • export class UpdateMovieDto {
      @IsOptional()
      @IsNotEmpty()
      title?: string;
    
      @IsOptional()
      @IsNotEmpty()
      genre?: string;
    
      @IsOptional()
      @IsNotEmpty()
      detail?: string;
    }
    

 

Service 로직 작성

  • async updateMovie(id: number, updateMovieDto: UpdateMovieDto) {
      // 1. 영화 존재 여부 확인 (+ detail 관계까지 같이 가져오기)
      const movie = await this.movieRepository.findOne({
        where: { id },
        relations: ['detail'],
      });
    
      if (!movie) throw new NotFoundException('존재하지 않는 영화입니다.');
    
      // 2. 구조 분해
      const { detail, ...movieRest } = updateMovieDto;
    
      // 3. 영화 자체 업데이트
      await this.movieRepository.update(id, movieRest);
    
      // 4. detail이 있을 경우만 MovieDetail 업데이트
      if (detail) {
        await this.movieDetailRepository.update(movie.detail.id, {
          detail,
        });
      }
    
      // 5. 갱신된 영화 정보 반환 (다시 relations 포함해서 fetch)
      const updated = await this.movieRepository.findOne({
        where: { id },
        relations: ['detail'],
      });
    
      return updated;
    }
    

 

핵심 포인트

  • 프론트 요청에 detail이 없으면 무시하고 Movie만 수정
  • TypeORM에서 관계 객체(movie.detail.id)까지 쉽게 접근 가능
  • 복잡한 조인 쿼리 없이도 관계 있는 데이터 수정 가능

 

정리 

항목 요약
기능 Movie + MovieDetail 업데이트
조건 detail이 들어오면 MovieDetail도 수정
장점 구조 분해 + 조건 분기 + relation fetch로 깔끔하게 구현 가능
추천 이유 TypeORM의 관계 기반 설계 구조에 익숙해지기 좋은 실습 예제

 

One to One Relationship Delete 작업해보기

 

목표 

항목 설명
목적 Movie를 삭제할 때, 연결된 MovieDetail도 함께 삭제
방식 Movie 먼저 조회 → 관련 detail까지 포함 → 둘 다 삭제

 

Service 로직 구현

  • async deleteMovie(id: number) {
      // 1. 삭제할 Movie + 연결된 detail 조회
      const movie = await this.movieRepository.findOne({
        where: { id },
        relations: ['detail'],
      });
    
      if (!movie) throw new NotFoundException('존재하지 않는 영화입니다.');
    
      // 2. Movie 먼저 삭제
      await this.movieRepository.delete(id);
    
      // 3. 그다음 관련된 MovieDetail 삭제
      await this.movieDetailRepository.delete(movie.detail.id);
    
      return id;
    }
    

주의 사항

  • 반드시 Movie를 먼저 삭제해야 외래 키 충돌 안 남
  • MovieDetail가 먼저 삭제되면, detail.id 참조가 끊겨 오류 발생 가능
  • TypeORM에서는 명시적 삭제가 가장 안전함 (Soft delete 고려 가능)

 

정리 

항목 요약
목적 Movie + 관계된 MovieDetail 함께 삭제
구현 방식 findOne(..., relations) → delete() 순차 실행
장점 단순하고 직관적, 관계 있는 데이터 안전하게 제거 가능
확장 추후 soft delete, cascading 등 고급 기능도 도입 가능

 

Cascade 옵션 사용해보기

 

@OneToOne 핵심 개념 요약

  • movie와 movieDetail은 1:1 관계로 묶여 있음.
  • 관계만 설정하면 연결은 되지 않음.
  • 관계 데이터를 자동 생성하려면 cascade: true가 필요함.

 

문제 상황 요약

  • 원래는 다음과 같은 방식으로 직접 엔티티 2개를 따로 생성
    • // 1. MovieDetail 먼저 생성
      const movieDetail = await this.movieDetailRepository.save({
        detail: createMovieDto.detail,
      });
      
      // 2. Movie 생성 및 MovieDetail 연결
      const movie = await this.movieRepository.save({
        title: createMovieDto.title,
        genre: createMovieDto.genre,
        detail: movieDetail,
      });
      
  • 이걸 다음과 같이 한번에 해결하고 싶음
    • const movie = await this.movieRepository.save({
        title: createMovieDto.title,
        genre: createMovieDto.genre,
        detail: {
      	  detail: createMovieDto.detail,
        },
      });
      
  • 그런데 이렇게 했더니 동작을 안 함.
    • movieDetail.id가 null이고, DB에서도 연결이 안 되어 있음.

 

원인

  • @OneToOne 관계 설정만으로는 TypeORM이 자동으로 관련된 데이터를 저장해주지 않음.
  • @OneToOne(() => MovieDetail)
    @JoinColumn()
    movieDetail: MovieDetail;
    
  • 이 상태에선 movieDetail을 자동으로 저장하지 않음.
  • 기본값은 cascade: false이기 때문.

 

해결 방법

  • cascade: true 옵션 추가
  • @OneToOne(() => MovieDetail, (movieDetail) => movieDetail.id, {
      cascade: true,
    })
    @JoinColumn()
    movieDetail: MovieDetail;
    
  • 이렇게 설정하면,
  • movie를 저장할 때 movie.movieDetail도 자동으로 생성/저장됨.

 

정리 

항목 설명
문제 movieDetail이 자동 저장되지 않음
원인 @OneToOne의 기본 cascade: false
해결 { cascade: true } 설정 추가
장점 create 시 movie + movieDetail 한 줄로 가능

 

학습 후기         

 

이번 강의를 통해 One-to-One 관계를 실제로 구현하면서, 단순히 엔티티 간의 연결을 넘어서 현실적인 서비스 설계에 어떤 고민이 들어가야 하는지를 배울 수 있었습니다. 특히 인상 깊었던 부분은 프론트엔드와의 계약을 지킬 수 있도록 API 구조를 설계해야 한다는 원칙이었습니다.

 

백엔드에서 어떻게 엔티티를 분리해서 구성했든 간에, 프론트엔드 입장에서는 단순하고 직관적인 JSON 형태로 데이터를 주고받는 것이 가장 중요하다는 점은 실무에 가까운 사고방식이었습니다. 예를 들어, detail: string으로 받는 간단한 구조를 서버 내부에서 MovieDetail 객체로 변환해 처리하는 흐름은 기술보다는 설계 철학에 가까운 가치였습니다.

 

또한 relations 옵션을 사용해 Movie와 MovieDetail을 동시에 가져오거나 수정, 삭제할 수 있는 기능을 구축하면서, TypeORM의 장점을 실감할 수 있었습니다. 특히 복잡한 SQL 없이도 객체지향적으로 관계 데이터를 다룰 수 있다는 점에서 ORM이 제공하는 개발 생산성이 확실히 체감되었습니다.

 

추가로 cascade: true 설정을 통해 관계된 엔티티를 별도로 저장하지 않고도 자동으로 함께 persist할 수 있었는데, 이는 프로젝트 규모가 커졌을 때 트랜잭션 처리나 무결성 보장 측면에서 매우 유용하게 작용할 것으로 보입니다. 단, cascade 설정이 모든 상황에서 좋은 것은 아니기 때문에 언제 쓰는 것이 적절할지에 대한 기준도 앞으로 함께 고민해야 할 주제라 느꼈습니다.

 

이번 실습은 단순히 One-to-One 관계만 구현한 것이 아니라, 어떻게 API를 설계하고, 데이터 흐름을 구성하고, 유지보수를 고려한 구조를 선택할 것인가까지 폭넓게 다룬 점에서 매우 인상 깊었고, 실무에서 바로 활용 가능한 감각을 키울 수 있던 좋은 학습 경험이었습니다.

 

학습 인증샷       

                       

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

https://abit.ly/lisbva