TIL - 미숙한 첫 DDD 개발 (Ticketing 애그리거트) (10.30)
2025. 10. 31. 00:45

📚 오늘 공부한 내용

1. 팀 회의 진행

항공권 예약 시스템 도메인 모델링에 대한 팀 회의를 진행했다.

아래는 열심히 회의 한 내역입니다.

2. Entity와 VO 구현 실습

Ticketing 애그리거트 역할을 맡아 실제 코드로 구현했습니다:

  • Entity: BoardingPass, Flight, PlaneTicket, Seat
  • Value Object: Airline, Airport, Amount, FlightId, PassengerId 등

🤔 학습 중 궁금했던 점

Q1. @Embeddable과 @Embedded의 역할은?

Q2. JPA를 통해 어떻게 DB에 저장되는가?

Q3. PassengerId를 FK로 참조하지 않는데 어떻게 해결해야 할까?


💡 학습 내용 정리

1️⃣ @Embeddable과 @Embedded의 역할

@Embeddable

  • VO(Value Object) 클래스에 붙이는 어노테이션
  • "이 클래스는 다른 엔티티에 내장될 수 있는 값 타입이다"라고 JPA에게 알려줌
  • 예: Airport, Amount, PassengerId 등

@Embedded

  • Entity 클래스의 필드에 붙이는 어노테이션
  • "이 필드는 Embeddable 타입을 내장하여 사용한다"라고 선언
  • 예: PlaneTicket 엔티티의 from, to 필드

코드 예시

// VO 클래스
@Embeddable
public class Airport {
    private String airportCode;
}

// Entity 클래스
public class PlaneTicket {
    @Embedded
    @AttributeOverride(name = "airportCode", column = @Column(name = "departure_airport"))
    private Airport from;
}

2️⃣ JPA를 통해 DB에 저장되는 방식

핵심: Embedded 타입은 별도 테이블을 만들지 않고, 소유한 엔티티의 테이블에 컬럼으로 펼쳐집니다.

PlaneTicket 엔티티의 DB 저장 예시

CREATE TABLE plane_ticket (
    id BIGINT PRIMARY KEY,
    reservation_id VARCHAR(255),  -- ReservationId의 reservationId 필드
    amount DECIMAL,                -- Amount의 amount 필드
    departure_airport VARCHAR(10), -- Airport(from)의 airportCode 필드
    arrival_airport VARCHAR(10),   -- Airport(to)의 airportCode 필드
    eta TIMESTAMP,
    via VARCHAR(255),
    passenger_name VARCHAR(100),
    fareclass VARCHAR(20),
    baggage_code VARCHAR(50),
    status VARCHAR(20),
    created_at TIMESTAMP
);

@AttributeOverride의 역할

  • 같은 타입(Airport)을 여러 번 사용할 때 컬럼명이 중복되는 것을 방지
  • from과 to 모두 Airport 타입이지만, 각각 departure_airport, arrival_airport로 저장됨

3️⃣ PassengerId를 FK로 참조하지 않는 문제 해결

왜 FK 참조를 하지 않을까?

1. 애그리거트 간 경계 유지

  • Seat는 Ticketing 애그리거트 소속
  • Passenger는 별도의 Passenger 애그리거트 소속
  • 애그리거트 간에는 직접 객체 참조 대신 ID 참조를 권장

2. 느슨한 결합 유지

// ❌ 나쁜 예: 직접 참조 (강한 결합)
@ManyToOne
private Passenger passenger;

// ✅ 좋은 예: ID 참조 (느슨한 결합)
@Embedded
private PassengerId passengerId;

실제 Passenger 정보가 필요할 때 해결 방법

방법 1: 애플리케이션 레이어에서 조합

@Service
public class SeatService {
    private final SeatRepository seatRepository;
    private final PassengerRepository passengerRepository;
    
    public SeatWithPassengerDto getSeatDetail(Long seatId) {
        Seat seat = seatRepository.findById(seatId);
        Passenger passenger = passengerRepository.findById(
            seat.getPassengerId().getId()
        );
        
        return new SeatWithPassengerDto(seat, passenger);
    }
}

방법 2: DB 레벨에서 FK 제약조건 추가 (선택사항)

@Embeddable
public class PassengerId {
    @Column(name = "passenger_id")
    private Long id;
}
-- 실제 DB 스키마
CREATE TABLE seats (
    id BIGINT PRIMARY KEY,
    flight_id VARCHAR(255),
    passenger_id BIGINT,
    seat_number VARCHAR(10),
    fareclass VARCHAR(20),
    seatstatus VARCHAR(20),
    assign_at TIMESTAMP,
    FOREIGN KEY (passenger_id) REFERENCES passengers(id)
);

하지만 JPA 엔티티 레벨에서는 @ManyToOne을 사용하지 않고 ID만 보관하여 애그리거트 독립성을 유지합니다.


🎯 핵심 정리

  1. @Embeddable/@Embedded: VO를 엔티티 테이블의 컬럼으로 펼쳐서 저장
  2. 별도 테이블 생성 안 함: Embedded 타입은 소유 엔티티의 테이블에 통합됨
  3. ID 참조 방식: 애그리거트 경계를 지키기 위해 직접 참조 대신 ID로 참조
  4. 실무적 접근: 필요시 애플리케이션 레이어에서 여러 애그리거트 정보를 조합

📊 장단점 비교

Embedded VO 방식의 장점

  • 도메인 개념을 명확하게 표현 (예: Airport, Amount)
  • 값 객체의 불변성과 유효성 검증 로직을 캡슐화
  • 중복 코드 감소 (Airport를 여러 엔티티에서 재사용)
  • 테이블 구조는 단순하게 유지

주의할 점

  • 같은 Embeddable 타입을 여러 필드에 사용할 때 @AttributeOverride 필수
  • Embedded 타입 내부의 모든 필드는 null이거나 모두 값이 있어야 함
  • 컬렉션 타입의 Embeddable은 @ElementCollection 사용 필요

💭 느낀 점

DDD의 Value Object와 애그리거트 경계 개념을 실제 코드로 구현해보니 이론으로만 배울 때보다 훨씬 명확하게 이해되었다.

그런데 그렇다고 해서 완벽하게 이해한거는 아닌것 같다.

뭔가 지금까지 개발하던 방향과 다르다고 생각한다.

코드를 구현할 때도 우선은 다른 튜터님이 올려주신 예시 코드를 참고하면서 만들었는데 이거는 어떻게 확장될까?

이거는 어떤 기능을 할까?

어떻게 작동할까? 가 궁금증으로 남아있어서 조금 찾아보았다.

특히 ID 참조 방식을 통해 애그리거트 간 독립성을 유지하는 설계 철학이 인상 깊었다.

DDD 굉장히 어려운 작업이라고 생각한다.

내가 나중에 책임자 역할까지 올라가면 굉장히 어려운 작업이 될거라고 생각한다.