03 — Repository & DataMapper
질문: TypeORM은 왜 Repository라는 한 겹을 두고, 그 Repository의
save()는 왜 Hibernate와 다르게 동작하는가? 한 줄 답: Repository는 엔티티가 자기 영속성을 모르게 만드는 경계이고, 그래서save()는 dirty checking 없이 매번 명시적으로 호출되는 upsert다.
이전 챕터(02 — Relations)가 엔티티들이 어떻게 묶이는가를 다뤘다면, 이번 챕터는 그 엔티티가 어떻게 DB와 만나는가다. 한 줄의 userRepository.save(user)가 SQL 두세 개로 풀려나가는 길을 6단계로 추적한다.
챕터 지도
읽는 순서
| 순서 | 문서 | 누구에게 |
|---|---|---|
| 1 | 01-datamapper-vs-activerecord | ”왜 두 가지 스타일이 있지?” 묻는 사람 — Fowler의 두 패턴부터 |
| 2 | 02-repository-api | 매일 쓰는 find/save의 정확한 의미를 다지고 싶은 사람 |
| 3 | 03-find-options | where/relations/In/Like 등 옵션을 한 번에 정리하고 싶은 사람 |
| 4 | 04-save-semantics | ”save가 왜 이렇게 느리지?”를 한 번이라도 의심해본 사람 |
| 5 | 05-custom-repository | NestJS·DDD 진영에서 도메인 메서드를 어디 둘지 고민하는 사람 |
| 6 | 06-active-record-mode | BaseEntity를 본 적 있고 “이거 써도 되나?” 망설이는 사람 |
추천 동선: 1·4를 먼저 읽으면 멘탈 모델이 잡힌다. 2·3은 레퍼런스로, 5·6은 실전 단계에서.
6개 문서 한 줄 요약
| # | 문서 | 한 줄 답 |
|---|---|---|
| 01 | DataMapper vs ActiveRecord | DataMapper는 엔티티가 영속성을 모르게 분리하고, ActiveRecord는 엔티티가 영속성을 안다. TypeORM은 둘 다 지원한다 — 그래서 함정이 많다. |
| 02 | Repository API | find/findOne은 SELECT, save는 upsert, insert는 순수 INSERT, delete는 hard delete, softDelete는 컬럼 한 칸으로 죽이기. |
| 03 | Find 옵션 | where·relations·order·take·skip·select·cache·withDeleted 여덟 키, In·Not·Like·Between·IsNull 다섯 operator. |
| 04 | save 의미 | save()는 upsert이고 항상 SELECT를 먼저 한다 — 그래서 insert()보다 느리다. dirty checking은 없다. |
| 05 | Custom Repository | Repository.extend() 또는 클래스 상속으로 도메인 메서드를 한곳에. NestJS는 @InjectRepository로 주입. |
| 06 | ActiveRecord 모드 | BaseEntity 상속 시 user.save()가 가능 — 짧지만 테스트·의존성·DataSource 다중화에 약하다. |
왜 이 챕터가 중요한가
TypeORM의 거의 모든 예상치 못한 동작은 Repository 레이어에서 발생한다.
| 흔한 의문 | 어느 문서가 답하는가 |
|---|---|
| ”save가 왜 SELECT부터 하지?“ | 04 — save는 upsert, 존재 여부를 알아야 한다 |
| ”방금 update했는데 다른 컬럼이 안 바뀌네?“ | 02·04 — update는 dirty가 아니라 주어진 컬럼만 쓴다 |
| ”관계를 같이 저장하고 싶은데 안 되네?“ | 02 — cascade 옵션이 필요 |
| ”soft delete한 레코드를 다시 찾고 싶은데?“ | 03 — withDeleted: true |
| ”리포지토리에 도메인 메서드 어디 둬?“ | 05 — Custom Repository 패턴 |
| ”user.save() 되던데 그럼 그게 표준 아니야?“ | 06 — ActiveRecord 모드의 함정 |
이 답들이 추측이 아니라 TypeORM 소스에서 나온다는 것을 보여주는 게 이 챕터의 목표다.
Why — 왜 Repository라는 한 겹을 두는가
답은 Martin Fowler의 1996년 책 Analysis Patterns까지 거슬러 올라간다.
- ActiveRecord: 엔티티 클래스가 자기 save·delete를 안다. 짧고 빠르다. Rails·Django·Eloquent.
- DataMapper: 엔티티는 POJO, 영속성은 Repository가 책임. 길지만 분리된다. Hibernate·TypeORM·MikroORM.
TypeORM은 둘 다 지원하지만 — 공식 권장은 DataMapper다. 이 챕터는 그 권장의 이유와 대가를 묻는다.
다음 챕터로
Repository의 find 옵션은 대부분의 쿼리를 표현한다. 하지만 서브쿼리·복잡한 join·raw SQL이 필요한 순간이 온다 — 그때가 04장의 출발점이다.
다음: 04 — QueryBuilder — find가 풀 수 없을 때 SQL의 손잡이를 다시 손에 쥐는 길.