1. 참조 관계 (Association)
"서로 알고는 있지만, 각자 독립적으로 존재"
class Student(
val id: Long,
val name: String
)
class Course(
val id: Long,
val title: String,
val students: List<Student> // 학생을 "참조"
)
- 학생은 수업 없이도 존재할 수 있음
- 수업은 학생 없이도 존재할 수 있음
- 수업이 폐강돼도 학생은 사라지지 않음
다이어그램 표현
┌─────────────┐ ┌─────────────┐
│ Student │ │ Course │
├─────────────┤ ├─────────────┤
│ - id │<──────│ - id │ ← 참조 (실선)
│ - name │ │ - title │
└─────────────┘ └─────────────┘
실제 예시
상품 ↔ 재고
class Product(
val id: Long,
val name: String,
val price: BigDecimal
)
class Stock(
val id: Long,
val productId: Long, // 상품을 참조
val quantity: Int,
val warehouse: String
)
- 상품을 먼저 등록하고, 재고는 나중에 입고될 수 있음
- 상품이 단종돼도 재고 기록은 남겨둘 수 있음
- 재고가 0이어도 상품은 존재함
- 서로 생명주기가 완전히 독립적
신발 상품 등록 (7월 1일)
⬇️
재고 입고 (7월 15일) ← 2주 뒤에 입고
⬇️
상품 단종 (12월 1일)
⬇️
재고 기록은 여전히 존재 (정산, 감사 목적)
회원 ↔ 쿠폰
class Member(
val id: Long,
val email: String,
val name: String
)
class Coupon(
val id: Long,
val code: String,
val discountAmount: BigDecimal,
val memberId: Long? // nullable - 아직 미발급 상태일 수 있음
)
- 쿠폰은 회원 없이도 미리 만들어 둘 수 있음 (선착순 쿠폰)
- 회원이 탈퇴해도 쿠폰 사용 내역은 보관할 수 있음
- 쿠폰을 다른 회원에게 양도할 수도 있음
쿠폰 100장 생성 (회원 없이)
⬇️
이벤트 시작 -> 회원들이 쿠폰 획득
⬇️
회원 A 탈퇴
⬇️
쿠폰 사용 이력은 남아있음
2. 합성 관계 (Composition)
"부모가 죽으면 자식도 죽는다"
class Order(
val id: Long,
val items: List<OrderItem> // Order가 생성될 때 함께 생성
)
class OrderItem(
val productId: Long,
val quantity: Int,
val price: BigDecimal
// Order 없이는 의미가 없음
)
- OrderItem은 Order 없이 존재 자체가 불가능
- Order가 삭제되면 OrderItem도 함께 삭제
- 생명주기가 완전히 동일
다이어그램 표현
┌─────────────┐ ┌─────────────┐
│ Order │◆──────│ OrderItem │ ← 합성 (채워진 다이아몬드)
├─────────────┤ ├─────────────┤
│ - id │ │ - productId │
│ - items │ │ - quantity │
└─────────────┘ └─────────────┘
실제 예시
주문 → 주문항목
class Order(
val id: Long,
val memberId: Long,
val orderDate: LocalDateTime,
val items: List<OrderItem> // 주문 생성 시 함께 생성
) {
init {
require(items.isNotEmpty()) { "주문항목 없이 주문 불가" }
}
}
class OrderItem(
val id: Long,
val orderId: Long, // 반드시 Order에 속해야 함
val productId: Long,
val quantity: Int,
val price: BigDecimal
)
- 주문 항목은 주문 없이 존재할 수 없음
- 주문이 취소/삭제되면 주문항목도 함께 삭제
- 주문항목을 다른 주문으로 옮기는 건 말이 안됨
- 항상 같이 생성되고 같이 삭제됨
게시글 → 댓글
class Post(
val id: Long,
val title: String,
val content: String,
val comments: List<Comment>
)
class Comment(
val id: Long,
val postId: Long, // 반드시 특정 게시글에 속함
val memberId: Long,
val content: String,
val createdAt: LocalDateTime
)
- 댓글은 해당 게시글 맥락에서만 의미가 있음
- 게시글 삭제 시 댓글도 함께 삭제
- 댓글을 다른 게시글로 옮기는 건 말이 안됨
단, 비즈니스에 따라 달라질 수 있음
| 서비스 정책 | 관계 해석 | |
| 케이스 1 | 게시글 삭제 -> 댓글도 삭제 | 합성 |
| 케이스 2 | 게시글 삭제 -> "삭제된 게시글입니다" + 댓글 유지 | 참조 |
케이스 2는 게시글을 삭제해도 댓글은 DB에 존재하며, 화면에 "삭제된 게시글의 댓글입니다" 표시된다.
댓글이 부모 없이도 살아남을 수 있어서 참조에 가깝다.
3. 조합 관계 (Aggregation)
"부품들이 모여서 하나를 이루지만, 부품은 독립적"
class Computer(
val cpu: CPU,
val mainboard: Mainboard,
val memory: Memory
)
class CPU(
val model: String,
val cores: Int
)
- CPU는 컴퓨터에서 빼서 다른 컴퓨터에 장착 가능
- 컴퓨터가 폐기돼도 CPU는 재활용 가능
- 전체-부분 관계이지만 부분이 독립적 생명주기를 가짐
다이어그램 표현
┌─────────────┐ ┌─────────────┐
│ Computer │◇──────│ CPU │ ← 조합 (빈 다이아몬드)
├─────────────┤ ├─────────────┤
│ - cpu │ │ - model │
│ - memory │ │ - cores │
└─────────────┘ └─────────────┘
실제 예시
팀 ↔ 팀원
class Team(
val id: Long,
val name: String,
val members: List<Employee>
)
class Employee(
val id: Long,
val name: String,
val email: String,
val teamId: Long? // 팀 없이도 존재 가능 (신입, 부서이동 중)
)
- 팀원은 팀의 구성원이지만 독립적 존재
- 팀이 해체돼도 직원은 퇴사하지 않음 -> 다른 팀으로 이동
- 직원이 팀 없이 존재할 수 있음 (신입 온보딩 기간)
개발팀 해체
⬇️
팀원 A → 플랫폼팀으로 이동
팀원 B → 인프라팀으로 이동
⬇️
팀원들은 여전히 회사에 존재
재생목록 ↔ 노래
class Playlist(
val id: Long,
val name: String,
val ownerId: Long,
val songs: List<Song>
)
class Song(
val id: Long,
val title: String,
val artist: String,
val duration: Int
)
- 노래는 재생목록의 구성요소지만 독립적
- 같은 노래가 여러 재생목록에 포함될 수 있음
- 재생목록 삭제해도 노래 자체는 삭제 안 됨
재생목록 "운동할 때" 삭제
⬇️
노래들은 그대로 존재
⬇️
다른 재생목록 "드라이브"에 같은 노래 여전히 있음
한눈에 비교
| 구분 | 참조 | 합성 | 조합 |
| 생명주기 | 완전 독립 | 완전 동일 | 부분적 독립 |
| 부모 삭제 시 | 자식 유지 | 자식도 삭제 | 자식 유지 |
| 관계 강도 | 약함 | 가장 강함 | 중간 |
| DB 설계 | FK (nullable) | FK + CASCADE DELETE | FK (nullable) |
다이어그램 표현 암기 팁
| 모양 | 의미 | 기억법 |
| ◆ (채워진) | 합성 | 꽉 채워진 = 강한 결합 = 같이 죽음 |
| ◇ (빈) | 조합 | 비어있음 = 느슨함 = 분리 가능 |
| → (화살표) | 참조 | 그냥 가리킴 = 독립적 |
참조와 조합 구분
참조와 조합 둘다 독립적으로 존재 가능해서 헷갈린다.
핵심 차이는 "부분인가 아닌가" 이다.
| 구분 | 참조 | 조합 |
| 관계 | 그냥 알고 있음 | 부분/구성요소임 |
| 질문 | B를 알고 있나? | B는 A의 부품인가? |
판단 질문1
"A를 분해하면 B가 나오는가?"
| 예시 | 분해하면? | 관계 |
| 컴퓨터 → CPU | CPU 나옴 ✅ | 조합 |
| 팀 → 팀원 | 팀원 나옴 ✅ | 조합 |
| 재생목록 → 노래 | 노래 나옴 ✅ | 조합 |
| 회원 → 쿠폰 | 쿠폰 안 나옴 ❌ | 참조 |
| 상품 → 재고 | 재고 안 나옴 ❌ | 참조 |
| 학생 → 수업 | 수업 안 나옴 ❌ | 참조 |
판단 질문2
B가 A의 구성요소로서 여러 A에 포함될 수 있는가?
조합: 노래가 여러 재생목록에 "구성요소로" 포함 ✅
참조: 학생이 여러 수업을 "수강' (구성요소가 아님) ❌
한 줄 정리
| 질문 | 참조 | 조합 |
| B는 A의 부품인가? | ❌ | ✅ |
| A를 분해하면 B가 나오나? | ❌ | ✅ |
| 전체-부분 관계인가? | ❌ | ✅ |
참조 = "알고 있다" 조합 = "구성하고 있다"
도메인 모델링에 정답은 없다
같은 개념도 비즈니스 맥락에 따라 다르게 해석될 수 있다.
- 게시글 -> 댓글: 서비스 정책에 따라 합성 또는 참조
- 상품 -> 옵션: 옵션을 독립 엔티티로 볼지, 상품에 종속된 것으로 볼지
설계할 때 "이 개념의 본질이 뭔가?"를 깊이 고민하는 습관이 중요하다.