[DDD] Chap 04 리포지터리와 JPA중심 모델 구현

2021. 8. 21. 18:19DDD

리포지터리는 애그리거트의 저장소를 말하며,

애그리거트를 어떤 DBMS를 사용하여 저장하냐에 따라 리포지터리 구현 방법이 다릅니다.

RDBMS와 객체 기반 도메인 모델을 매핑하는데 사용하는 대표적인 ORM기술로서 JPA가 있습니다.

이번 장에선 JPA 중심으로 리포지터리 구현과 엔티티, 밸류, 애그리거트 매핑 구현 방법에 대해서 알아보겠습니다.

 

모듈 위치

2장에서 살펴보았듯이, Repository 인터페이스는 도메인 영역에 속하고 Repository를 구현하는 구현 클래스는 Infra Structure 영역에 존재합니다.

 

리포지터리의 기본 기능

  1. ID로 애그리거트 조회하기
  2. 애그리거트 저장하기

Repository 인터페이스는 애그리거트 루트를 기준으로 작성합니다.

public interface OrderRepository{
    public Order findById(OrderNo no);
    public void save(Order order);
}
public class JpaOrderRepository implements OrderRepository{
    @PersistenceContext
    private EntityManager em;
    
    @Override
    public Order findById(OrderNo id){
        return em.find(Order.class, id);
    }
    
    @Override
    public void save(Order order){
        em.persist(order);
    }
}

 

JPA는 한 트랜잭션 범위내에서 변경된 내용을 자동으로 DB에 반영합니다.

public class ChangeOrderService{
    @Transactional
    public void changeShippingInfo(OrderNo no, ShippingInfo newShippingInfo){
        Order order = orderRepository.findById(no);
        if(order == null) throw new OrderNotFoundException();
        order.changeShippingInfo(newShippingInfo);
    }
}

 

아이디가 아닌 다른 조건으로 애그리거트를 조회해야하는 경우, findBy 뒤에 조건 대상의 프로퍼티 이름을 붙입니다.

public interface OrderRepository{
    public List<Order> findByOrderId(String orderId, int startRow, int size);
}

 

ID 외의 조건으로 애그리거트를 조회할 때 JPA의 Criteria나 JPQL을 사용합니다.

@Override
public List<Order> findByOrdererId(String ordererId, int startRow, int fetchSize){
    TypedQuery<Order> query = em.createQuery("select o from Order o" +
        "where o.orderer.memberId.id = :ordererId"+
        "order by o.number.number desc", Order.class);
    query.setParameter("ordererId", ordererId);
    query.setFirstResult(startRow);
    query.setMaxResult(fetchSize);
    
    return query.getResultList();
}

 

애그리거트 삭제 기능도 지원하긴 하지만 실무에서 관리자가 삭제한 데이터까지 조회해야하는 경우가 많기 때문에 DB에서 삭제하지않고 일정기간 보관합니다.


엔티티와 밸류 기본 매핑 규현

위 그림은 주문 애그리거트 내부 엔티티와 밸류 객체를 매핑한 그림입니다.

Order는 애그리거트 루트이고 JPA에 의해 애그리거트를 데이터베이스 테이블로 변환하면 PURCHASE_ORDER 테이블과 같습니다.

일반적으로 밸류를 아무리 많이 생성해도 DB 테이블에 영향을 주지 않습니다.

책에서 강조하는 애그리거트의 가장 이상적인 구조는 애그리거트내에 엔티티가 단 하나만 존재하는 형태입니다.

 

엔티티 구현하기

엔티티는 @Entity 어노테이션을 이용해서 생성합니다.

@Entity
@Table(name = "purchase_order")
public class Order{
	...
}

 

밸류 구현하기

밸류는 @Embeddable 어노테이션을 이용해서 생성합니다.

@Embeddable
public class Orderer{  //Value
    @Embedded
    @AttributeOverrides(
        @AttributeOverride(name = "id", column = @Column(name = "orderer_id")
    )
    private MemberId memberId;  //Member 애그리거트를 ID 참조
    
    @Column(name = "orderer_name")
    private String name;
}

@Embeddable
public class MemberId implements Serializable{  //Value
    @Column(name = "member_id")
    private String id;
}

 

JPA 2부터 @Embeddable은 중첩을 허용하므로 밸류안에 다른 밸류를 포함시킬 수 있고,

하나의 클래스안에서 같은 밸류를 여러번 사용가능합니다.

단, 이때 @AttributeOverride 어노테이션을 사용해서 명시해줘야 합니다.

위의 코드는 밸류를 식별자 타입으로 사용했는데 JPA에서 식별자 타입은 Serializable이어야하므로 Serializable 인터페이스를 구현하도록 했습니다.

식별자를 밸류로 구현시, 식별자에 기능을 추가할 수 있다는 장점이 있습니다.

 

기본 생성자

JPA에서 @Entity, @Embeddable로 엔티티와 밸류를 매핑하려면 기본 생성자가 필요합니다.

밸류는 immutable이므로 기본 생성자가 필요없음에도 불구하고,

지연 로딩시 JPA 프로바이더는 기본 생성자를 이용하여 proxy 객체(빈 객체)를 생성하기 때문에 기본 생성자가 꼭 필요합니다.

그러니 기본 생성자를 꼭 만드는 습관을 들이도록 합시다.

단, 외부에서 사용하지 못하도록 protected 접근 제어자를 사용해야 합니다.

 

필드 접근 방식 사용

JPA는 필드와 메서드 두가지 방식으로 매핑 처리가 가능합니다.

메서드 접근 방식은 getter/setter를 남발하기 때문에 엔티티안에 도메인 핵심 로직만 존재해야한다는 설계 원칙을 위반하여 객체 지향적 코딩이 아닌 데이터 기반 코딩을 유발합니다.

또한, setter는 외부에서 객체 상태를 변경하므로 캡슐화를 깨는 원인이 됩니다.

 

따라서 JPA에선 필드 접근 방식을 사용합니다.

@Entity
@Access(AccessType.FIELD)
public class Order{
    @EmbeddedId
    private OrderNo number;
    
    @Column(name ="state")
    @Enumerated(EnumType.STRING)
    private OrderState state;
    
    public void cacel() {...}
    
    public void changeShippingInfo() {...}
}

메서드명은 도메인 핵심 기능이 잘 드러나는 이름을 사용해야 합니다.

ex) setState() -> cancel()

      setShippingInfo() -> changeShippingInfo()

 

@Access

@Access 어노테이션은 JPA에게 어떤 접근 방식(필드 접근 방식, 메서드 접근 방식)을 사용할 것인지 알려주는 역할을 합니다.

@Access를 명시하지 않으면 @Id나 @EmbeddedId가 어디 위치하냐에 따라 접근 방식을 판단합니다.

고정적으로 필드 접근 방식을 사용한다고 생각해도 무방합니다.


엔티티간 연관관계 매핑

DB에 MEMBER와 TEAM 두 테이블이 존재하고, '한 팀은 여러 명의 멤버를 가질 수 있다'는 요구사항에 의해 N:1 연관관계를 가집니다.

해당 연관 관계를 객체지향적으로 구현하면 아래의 3가지 방법으로 나타낼 수 있습니다.

1번 방법은 '한 팀은 여러 명의 멤버를 가진다'는 요구사항을 논리적으로 잘 설명하는 것처럼 보입니다.

하지만 이 방법은 어떤 Member의 정보를 수정하고자할 때 Member가 아닌 Team 엔티티를 수정해야하는 논리적 모순이 발생합니다.

이를 보완하기 위해 Member안에 참조 변수를 두는 2번 방법을 사용하여 개발 편의성을 높입니다.

대부분의 프로젝트는 1:N 단방향 연관 관계만으로도 구현 가능하지만, 2번 방법만으로는 불완전함이 존재합니다.

드물지만 Team의 참조 변수를 통해 Member에 접근해야하는 요구사항도 발생할 수도 있습니다.

따라서 양쪽 객체에서 참조가 가능한 완전한 상태로 만들어주기 위해, 3번 방법을 사용합니다.(너무 남발하면 코드가 복잡해질 수 있으므로 필요할 때만 사용하는 것이 좋습니다.)

 

연관 관계의 주인

위 그림에서 3번 방법을 사용한다면 또 다른 문제를 야기합니다.

바로 'Member나 Team중에서 누가 FK인 team_id를 수정할 것인가' 입니다.

양방향 연관 관계는 두 엔티티간 서로 참조가 가능하므로, 어떤 엔티티를 수정했을 때 FK가 바뀔지 기준이 모호합니다.

그래서 JPA는 mappedBy property를 이용해서 연관 관계의 주인을 명시하여, 한 엔티티에게 FK를 바꿀 권리를 전부 위임합니다.

위 예시에선 N쪽인 Member가 연관 관계가 주인이 되고 Team의 members를 통해서 Member의 정보를 읽어올 수는 있지만 엔티티의 상태를 변경할 수는 없는 read only 속성을 가지게 됩니다.

결론적으로 추천하는 방법은 두 엔티티간 연관 관계에서 N쪽에 참조 변수를 두고 N쪽 객체를 연관 관계의 주인으로 명시하면 됩니다.

 

애그리거트간 ID 참조

가장 이상적인 애그리거트는 단 하나의 엔티티만 가집니다.

따라서 Member와 Team도 사실은 두 애그리거트간 연관 관계일 수 있습니다.

3장에서 언급한 바와 같이 애그리거트간 참조는 ID참조를 통해 이루어져야 애그리거트간 의존성을 낮추고 코드 집중도를 높일 수 있습니다.

 

애그리거트 로딩 전략

두개 이상의 애그리거트를 한꺼번에 로딩해야할 때, 즉시 로딩이나 지연 로딩 전략을 사용할 수 있습니다.

즉시 로딩은 Join을 통해 한꺼번에 데이터를 로딩해서 가져오지만, 카타시안 조인을 발생시켜서 개발자의 의도와 다르게 과도하게 많은 데이터를 가져올 수 있습니다.

지연 로딩은 연관된 객체를 프록시 객체(빈 객체)로 받아놓고 해당 객체의 프로퍼티를 호출할 때까지 조회를 미루고 미루기 때문에 대규모 프로젝트에서 조회 성능이 유리합니다.

다만, N + 1문제를 야기하므로 조회 요구가 자주 발생하는 상황에선 JPQL을 사용하거나 조회 전용 저장소를 따로 두어 조회 성능을 높입니다.

 

 

정리

- 리포지터리의 기본 기능

  1. 애그리거트 조회
  2. 애그리거트 상태 변경

- 가장 이상적인 애그리거트는 단 하나의 엔티티로 애그리거트 루트를 가진다.

- 엔티티간 연관 관계 매핑시 N:1 단방향 연관 관계를 이용하자.

또한, N쪽 엔티티에 참조 변수를 두고 N쪽 엔티티를 '연관 관계의 주인'으로 설정하자

 

 

Ref.

- 'DDD START! 도메인 주도 설계 구현과 핵심 개념 익히기' - 최범균

- https://www.inflearn.com/course/ORM-JPA-Basic 

'DDD' 카테고리의 다른 글

[DDD] Chap 06 응용 영역과 표현 영역  (0) 2021.09.21
[DDD] Chap 03 Aggregate  (0) 2021.08.15
[DDD] Chap 02 아키텍처 개요  (0) 2021.08.14
[DDD] Chap 01 도메인 모델 시작  (0) 2021.08.14