02 — Relations (관계)
질문: TypeORM에서 두 엔티티 사이의 관계를 정확히 표현하려면 어디까지 결정해야 하는가? 한 줄 답: 관계는 데코레이터 쌍(예:
@OneToMany/@ManyToOne) +@JoinColumn/@JoinTable위치(외래키가 어느 테이블에 사느냐) + 로딩 전략(eager/lazy/명시 relations) — 이 세 축이 동시에 맞아야 한 번에 동작한다.
이전 챕터(01 — Entity & 데코레이터)가 한 테이블을 클래스로 박는 법이었다면, 이번 챕터는 두 테이블 사이의 외래키를 데코레이터로 박는 법이다. 한 줄로 끝나는 이야기처럼 보이지만, TypeORM 사용자의 가장 흔한 버그가 모두 여기서 나온다 — 방향, 외래키 위치, 로딩 시점. 세 축을 분리해서 본다.
챕터 지도
읽는 순서
| 순서 | 문서 | 누구에게 |
|---|---|---|
| 1 | 01-one-to-one | 가장 단순한 관계부터 — 비대칭성의 출발 |
| 2 | 02-one-to-many-and-many-to-one | 가장 흔한 관계 — 90%의 실전 케이스 |
| 3 | 03-many-to-many | 조인 테이블이 추가 컬럼을 가져야 하는 순간 |
| 4 | 04-eager-vs-lazy | 관계는 언제 로드할 것인가 — 함정 챕터 |
| 5 | 05-cascade-and-orphan | TypeORM의 cascade는 애플리케이션 레벨이다 |
| 6 | 06-self-referencing-and-polymorphic | 트리 구조와, TypeORM이 지원하지 않는 polymorphic |
추천 동선: 처음 TypeORM을 만지면 1·2·3 → 4·5는 반드시 → 6은 트리/STI를 다루게 될 때 돌아오면 된다.
6개 문서 한 줄 요약
| # | 문서 | 한 줄 답 |
|---|---|---|
| 01 | OneToOne | @JoinColumn은 한 쪽 엔티티에만 붙는다 — 그 쪽 테이블이 외래키의 주인이다. |
| 02 | OneToMany & ManyToOne | 두 데코레이터는 항상 쌍으로 정의되고, FK는 Many 쪽 테이블에 산다. |
| 03 | ManyToMany | @JoinTable은 한 쪽만에 붙는다 — 추가 컬럼이 필요하면 중간 엔티티 패턴으로 분해한다. |
| 04 | Eager vs Lazy | eager: true는 모든 find가 join을 끌고 다니게 만든다 — N+1로 가는 가장 빠른 길. |
| 05 | Cascade & Orphan | TypeORM cascade는 save() 시점에 동작하는 애플리케이션 레벨 전파다 — DB의 ON DELETE CASCADE와 다르다. |
| 06 | Self-ref & Polymorphic | TypeORM은 트리는 4가지 패턴으로 지원하지만, polymorphic은 1급 시민이 아니다. |
왜 이 챕터가 중요한가
관계 한 줄을 잘못 적으면 세 가지 방식으로 실패한다.
| 흔한 실수 | 결과 | 어느 문서가 답하는가 |
|---|---|---|
@JoinColumn을 양쪽에 다 붙임 | 마이그레이션 시 FK가 두 개 생성됨 | 01 |
@ManyToOne만 정의하고 반대편 @OneToMany를 빠뜨림 | 반대 방향 탐색 불가 — user.posts 못 씀 | 02 |
@JoinTable을 양쪽에 붙임 | TypeORM이 조인 테이블을 두 개 생성 시도 → 에러 | 03 |
eager: true를 깊은 관계에 줌 | 모든 find가 User → Posts → Comments → Author join | 04 |
cascade: true만 믿고 DB ON DELETE를 안 검 | 애플리케이션 레벨에서만 동작 — raw SQL/migration 우회 시 깨짐 | 05 |
트리 구조에 그냥 @ManyToOne(() => Self)만 씀 | 깊은 트리 재귀 쿼리 못 함 | 06 |
이 챕터는 *세 축(데코레이터 쌍 · JoinColumn 위치 · 로딩 전략)*을 분리하면 이 실수들이 모두 체계적으로 사라진다는 것을 보여준다.
세 축을 한 표로
| 관계 유형 | 데코레이터 (소유 쪽) | 데코레이터 (반대) | @JoinColumn/@JoinTable 위치 | FK가 사는 테이블 |
|---|---|---|---|---|
| OneToOne | @OneToOne + @JoinColumn | @OneToOne (옵션) | 소유 쪽에만 | 소유 쪽 |
| OneToMany / ManyToOne | @ManyToOne | @OneToMany | (자동, @JoinColumn은 옵션) | Many 쪽 (항상) |
| ManyToMany | @ManyToMany + @JoinTable | @ManyToMany | 소유 쪽에만 | 제3의 조인 테이블 |
기억할 한 줄:
@JoinColumn이 붙은 쪽이 FK의 주인이고, FK는 항상 one을 가리키는 many 쪽에 산다.
다음 챕터로
이 챕터에서 관계를 어떻게 선언하는가를 끝내면, 다음은 그 관계를 어떻게 SQL로 가져오는가다. find({ relations: { posts: true } }), queryBuilder.leftJoinAndSelect, Repository.save(cascadeOptions) — 모두 03 — Repository & DataMapper에서.
그리고 관계의 로딩 전략 한 줄이 어떻게 프로덕션 N+1 사고가 되는지는 07 — 성능 & N+1에서 끝장을 본다.
다음: 01 — OneToOne — 가장 단순한 관계에서 비대칭성을 본다.