낙서장이자 오답 노트이자 컨닝 페이퍼
패스트캠퍼스 환급챌린지 16일차 : 코드팩토리의 백엔드 아카데미 : 한 번에 끝내는 NestJS 패키지 - 기초부터 MSA까지 강의 후기 본문
패스트캠퍼스 환급챌린지 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를 설계하고, 데이터 흐름을 구성하고, 유지보수를 고려한 구조를 선택할 것인가까지 폭넓게 다룬 점에서 매우 인상 깊었고, 실무에서 바로 활용 가능한 감각을 키울 수 있던 좋은 학습 경험이었습니다.
학습 인증샷



