DDD

리포지터리와 모델 구현

va-la 2022. 12. 1. 00:03

데이터 보관소로 RDBMS를 사용할 때, 객체 기반의 도메인 모델과 관계형 데이터 모델 간의 매핑을 처리하는 기술로 ORM이 쓰이는데, 그 중 자바 ORM 표준인 JPA를 이용해 리포지터리와 애그리거트를 구현하는 방법에 대해 알아보자

 

리포지터리 인터페이스는 애그리거트와 같이 도메인 영역에 속하고, 리포지터리를 구현한 클래스는 인프라스트럭처 영역에 속한다. 

가능하면 리포지터리 구현 클래스를 인프라스트럭처 영역에 위치시켜서 인프라스트럭처에 대한 의존을 낮추는게 좋다.

 

아래와 같은 기능을 제공하는 리포지터리를 살펴보자

  • ID로 애그리거트 조회
  • 애그리거트 저장

두 메서드를 위한 리포지터리 인터페이스는 다음과 같은 형식을 갖는다.

public interface OrderRepository {
    Order findById(OrderNo no);
    void save(Order order);
}

인터페이스는 애그리거트를 중심으로 작성한다. 주문 애그리거트는 Order 루트 엔티티를 비롯해 OrderLine, Orderer 등 다양한 객체를 포함하는데, 이 구성요소 중에서 루트 엔티티는 Order를 기준으로 리포지터리 인터페이스를 작성한다. 

애그리거트를 조회하는 기능의 이름을 지을 때 특별한 규칙은 없지만, 널리 사용되는 규칙은 'findBy프로퍼티이름' 형식을 사용한다. 

findById()는 ID에 해당하는 애그리거트가 존재하면 Order를 리턴하고 존재하지 않으면 null을 리턴한다. null을 사용하고 싶지 않다면 다음과 같이 Optional을 사용해도 된다.

Optional<Order> findById(OrderNo no);

save()는 전달받은 애그리거트를 저장한다. 이 인터페이스를 구현한 클래스는 JPA의 EntityManager를 이용해서 기능을 구현한다.

EntityManager란, 엔티티를 저장하고, 수정하고, 삭제하고 조회하는 등 엔티티와 관련된 모든 일을 처리하는 매니저이다. 또한 영속성 컨텍스트를 통해 데이터의 상태 변화를 감지하고 필요한 쿼리를 자동으로 수행한다. 

영속성 컨텍스트란, 엔티티를 영구 저장하는 환경으로, EntityManager로 엔티티를 저장하거나 조회하면 영속성 컨텍스트에 엔티티를 보관하고 관리한다.

애그리거트를 수정한 결과를 저장소에 반영하는 메서드를 추가할 필요는 없는데, JPA를 사용하면 트랜잭션 범위에서 변경한 데이터를 자동으로 DB에 반영한다.

public class ChangeOrderService {
    @Transactional
    public void changeShippingInfo(OrderNo no, ShippingInfo newShippingInfo) {
        Optional<Order> orderOpt = orderRepository.findById(no);
        Order order = orderOpt.orElseThrow(() -> new OrderNotFoundException());
        order.changeShippingInfo(newShippingInfo);
    }
}

ChangeOrderService 클래스의 changeShippingInfo() 메서드는 스프링 프레임워크의 트랜잭션 관리 기능을 통해 트랜잭션 범위에서 실행된다. 메서드 실행이 끝나면 트랜잭션을 커밋하는데 이때 JPA는 트랜잭션 범위에서 변경된 객체의 데이터를 DB에 반영하기 위해 UPDATE 쿼리를 실행한다. order.changeShippingInfo() 메서드를 실행한 결과로 애그리거트가 변경되면 JPA는 변경 데이터를 DB에 반영하기 위해 UPDATE 쿼리를 실행한다.

 

ID가 아닌 다른 조건으로 애그리거트를 조회할 때는 JPA의 Criteria나 JPQL을 사용해 구현할 수 있다. (생략)

애그리거트를 삭제하는 메서드는 EntityManager의 remove() 메서드를 이용해서 구현할 수 있다. 

public class JpaOrderRepository implements OrderRepository {
    @PersistenceContext
    private EntityManager entityManager;
    
    @Override
    public void delete(Order order) {
        entityManager.remove(order);
    }
}

 

스프링 데이터 JPA는 지정한 규칙에 맞게 리포지터리 인터페이스를 정의하면 리포지터리를 구현한 객체를 알아서 만들어 스프링 빈으로 등록해준다. 리포지터리 인터페이스를 직접 구현하지 않아도 되기 때문에 개발자는 리포지터리를 쉽게 정의할 수 있다. 스프링 데이터 JPA는 다음 규칙에 따라 작성한 인터페이스를 찾아서 인터페이스를 구현한 스프링 빈 객체를 자동으로 할당한다.

  • org.springframework.data.repository.Repository<T,ID> 인터페이스 상속
  • T는 엔티티 타입을 지정하고 ID는 식별자 타입을 지정

예를 들어 Order 엔티티 타입의 식별자가 OrderNo 타입이라고 했을 때,

@Entity
@Table(name = "purchase_order")
@Access(AccessType.FIELD)
public class Order {
    @EmbeddedId
    private OrderNo number; // orderNo가 식별자 타입
}

Order를 위한 OrderRepository는 다음과 같이 작성할 수 있다.

public interface OrderRepository extends Repository<Order, OrderNo> {
    Optional<Order> findById(OrderNo id);
    
    void save(Order order);
}

스프링 데이터 JPA는 OrderRepository를 리포지터리로 인식해서 알맞게 구현한 객체를 스프링 빈으로 등록한다.  사용할 때는 의존성 주입을 통해 사용하면 된다.

 

스프링 데이터 JPA를 사용하려면 지정한 규칙에 맞게 메서드를 작성해야 한다. OrderRepository를 기준으로 엔티티를 저장하는 메서드는 다음 중 하나를 사용한다.

  • Order save(Order entity)
  • void save(Order entity)

엔티티를 조회할 때는 이전에 설명한 것 처럼 Optional을 사용해 조회할 수 있다.

 

애그리거트와 JPA 매핑을 위한 기본 규칙은 애그리거트 루트는 엔티티이므로 @Entity로 매핑 설정한다는 규칙이다. 한 테이블에 엔티티와 벨류 데이터가 같이 있다면 밸류는 @Embeddable로 설정하고, 밸류 타입 프로퍼티는 @Embedded로 매핑 설정한다.

주문 애그리거트를 예로 들어보면 루트 엔티티는 Order이고 Orderer, ShippingInfo는 밸류이다. 또 ShippingInfo에 포함된 Address, Receiver 객체는 한 테이블에 매핑할 수 있다.

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

@Embeddable
public class Orderer {
    // MemberId에 정의된 컬럼 이름을 변경하기 위해
    // @AttributeOverride 애너테이션 사용
    @Embedded
    @AttributeOverrides(
        @AttributeOverride(name = "id", column = @Column(name = "orderer_id"))
    )
    private MemberId id;
    
    @Column(name = "order_name")
    private String name;
}

Orderer의 memberId는 Member 애그리거트를 ID로 참조한다. Member의 ID 타입으로 사용되는 MemberId는 다음과 같이 id 프로퍼티와 매핑되는 테이블 칼럼 이름으로 "member_id"를 지정하고 있다.

@Embeddable
public class MemberId implments Serializble {
    @Column(name = "member_id")
    private String id;
}

@AttributeOverrides 애너테이션을 이용해서 Orderer의 memberId 프로퍼티와 매핑할 컬럼 이름을 변경할 수 있다. 루트 엔티티인 Order 클래스는 @Embedded를 이용해서 밸류 타입 프로퍼티를 설정한다.

@Entity
public Order {
    ...
    @Embedded
    private Orderer orderer;
    ...
}

 

JPA에서는 @Entity, @Embeddable로 클래스를 매핑하려면 기본 생성자를 제공해야 한다. DB에서 데이터를 읽어와 매핑된 객체를 생성할 때 기본 생성자를 사용해서 객체를 생성하기 때문이다. 이런 기술적인 제약으로 불변 타입은 기본 생성자가 필요없어도 추가해줘야 한다. 기본 생성자를 생성할 때 protected로 생성하면 다른 코드에서 기본 생성자를 사용하지 못하게 할 수 있다.

 

JPA는 필드와 메서드의 두 가지 방식으로 매핑을 처리할 수 있다. 메서드 방식을 이용하려면 프로퍼티를 위한 get/set 메서드를 구현해야 한다.

@Entity
@Access(AccessType.PROPERTY)
public class Order {
    @Column(name = "state")
    @Enumerated(EnumType.STRING)
    public OrderState getState() {
        return state;
    }
    
    public void setState(OrderState state) {
        this.state = state;
    }
}

하지만 get/set 메서드를 추가하면 도메인의 의도가 사라지고 객체가 아닌 데이터 기반으로 엔티티를 구현할 가능성이 높아진다. 엔티티가 객체로서 제 역할을 하려면 외부에 set 메서드 대신 의도가 잘 드러나는 기능을 제공해야 한다. 상태 변경을 위한 setState() 메서드 보다 주문 취소를 위한 cancel() 메서드가 도메인을 더 잘 표현하고, setShippingInfo() 메서드보다 배송지를 변경한다는 의미를 갖는 changeShippingInfo()가 도메인을 더 잘 표현한다. 따라서 객체가 기능 중심으로 엔티티를 구현하게끔 유도하려면 위 방식보다는 필드 방식으로 선택해서 불필요한 get/set을 없애는게 좋다.

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

 

밸류 타입의 프로퍼티를 한 개 컬럼에 매핑해야 할 때도 있는데, 예를들어 Length가 길이값과 단위의 두 프러퍼티를 갖고 있는데 DB에 테이블에는 한 개 컬럼에 '1000mm'와 같은 형식으로 저장할 수 있다. 두 개 이상의 프로퍼티를 가진 밸류 타입을 한 개 컬럼에 매핑하려면 @Embeddable 애너테이션으로는 처리할 수 없다. 이럴 때 사용할 수 있는 것이 AtrributeConverter이다. 이 Converter는 다음과 같이 밸류 타입과 컬럼 데이터 간의 변환을 처리하기 위한 기능을 정의하고 있다.

public interface AttributeConverter<X, Y> {
    public Y convertToDatabaseColumn(X attribute);
    
    public X convertToEntityAttribute(Y dbData);
}

타입 파라미터 X는 밸류 타입, Y는 DB 타입이다. 이 책에서 Money 밸류 타입을 위한 AttributeConverter는 아래와 같이 구현할 수 있다.

@Converter(autoApply = true)
public class MoneyConverter implements AttributeConverter<Money, Integer> {
    @Override
    public Integer convertToDatabaseColumn(Money money) {
        return money == null ? null : money.getValue();
    }
    
    @Override
    public Money convertToEntityAttribute(Integer value) {
        return value == null ? null : new Money(value);
    }
}

AtttributeConverter 인터페이스를 구현할 클래스는 Converter 애너테이션을 적용한다. autoApply 속성값을 true로 주면 모델에 출현하는 모든 Money 타입의 프로퍼티에 대해 MoneyConverter를 자동으로 적용한다. false(기본 값)으로 지정을 하면 직접 지정을 해줘야 한다.

public class Order {
    @Column(name =. total_amounts")
    @Convert(converter = MoneyConverter.class)
    private Money totalAmounts;
}

 

Order 엔티티는 한 개 이상의 OrderLine을 가질 수 있다. OrderLine에 순서가 있다면 다음과 같이 List 타입을 이용해서 컬렉션을 프로퍼티로 지정할 수 있다.

public class Order {
    private List<OrderLine> orderLines;
    ...
}

Order과 OrderLine을 저장하기 위한 테이블은 아래와 같이 매핑 가능하다. 밸류 컬렉션을 저장하는 ORDER_LINE 테이블은 외부키를 이용해서 엔티티에 해당하는 PURCHASE_ORDER 테이블을 참조한다. 이 외부키는 컬렉션이 속할 엔티티를 의미한다. List 타입의 컬렉션은 인덱스 값이 필요하므로 ORDER_LINE 테이블에는 인덱스 값을 저장하기 위한 컬럼도 존재한다.

@Entity
@Table(name = "purchase_order")
public class Order {
    @EmbeddedId
    private OrderNo number;
    ...
    @ElementCollection(fetch = FetchType.EAGER)
    @CollectionTable(name = "order_line",
                     joinColumns = @JoinColumn(name = "order_number"))
    @OrderColumn(name = "line_idx")
    private List<OrderLine> orderLines;
    ...
}


@Embeddable
public class OrderLine {
    @Embedded
    private ProductId productId;
    
    @Column(name = "price")
    private Money price;
    
    @Column(name = "quantity")
    private int quantity;
    
    @Column(name = "amounts")
    private Money amounts;
    ...
}

OrderLine에는 List의 인덱스 값을 저장하기 위한 프로퍼티가 존재하지 않는다. 그 이유는 List 자체가 인덱스를 가지고 있어서, JPA는 @OrderColumn 애너테이션을 이용해 지정한 칼럼에 리스트의 인덱스 값을 저장한다.

 

식별자 자체를 밸류 타입으로 만들 수 있는데, 지금까지 살펴본 예제에서 OrderNo, MemberId 등이 식별자를 표현하기 위해 사용한 밸류 타입이다. 밸류 타입을 식별자로 매핑하면 @Id 대신 @Embedded 애너테이션을 사용한다. JPA에서 식별자 타입은 Serializable 타입이어야 하므로 식별자로 사용할 밸류 타입은 Serializable 인터페이스를 상속받아야 한다.

식별자를 사용할 때 얻을 수 있는 장점은 식별자에 기능을 추가할 수 있다는 점이 있다. JPA는 내부적으로 엔티티를 비교할 목적으로 equals() 메서드와 hashcode() 값을 사용하므로 식별자로 사용할 밸류 타입은 이 두 메서드를 알맞게 구현해야 한다.

 

애그리거트에서 루트 엔티티를 뺀 나머지 구성요소는 대부분 밸류이다. 만약 다른 엔티티가 존재한다면 진짜 엔티티인지 의심해야한다. 단지 별도 테이블에 저장한다고 해서 엔티티인 것은 아니다. 주문 애그리거트도 OrderLine을 별도 테이블에 저장하지만 OrderLine 자체는 밸류이다. 

애그리거트에 속한 객체가 밸류인지 엔티티인지 구분하는 방법은 고유 식별자는 갖는지 확인하는 것이다. 하지만 식별자를 찾을 때 매핑되는 테이블의 식별자를 애그리거트 구성요소의 식별자와 동일한 것으로 착각하면 안된다. 예를들어 게시글 데이터를 ARTICLE 테이블과 ARTICLE_CONTENT 테이블로 나눠서 저장한다고 했을 때, Article과 ArticleContent 클래스를 두 테이블에 매핑할 수 있다.

ARTICLE_CONTENT 테이블의 ID 컬럼이 식별자이므로 ArticleContent를 엔티티로 생각해서 Article과 1-1 연관으로 매핑할 수 있다. 하지만 ArticleContent는 Article의 내용을 담고 있는 밸류로 생각하는게 맞다. ARTICLE_CONTENT의 ID는 식별자이긴 하지만 이 식별자를 사용하는 이유는 ARTICLE 테이블의 데이터와 연결하기 위함이지 ARTICLE_CONTENT를 위한 별도 식별자가 필요하기 때문은 아니다. 즉 이것은 게시글의 특정 프로퍼티를 별도 테이블에 보관한 것으로 접근해야 한다.

ArticleContent는 밸류이므로 @Embeddable로 매핑하고, ArticleContent와 매핑되는 테이블은 Article과 매핑되는 테이블과 다르다. 이때 밸류를 매핑한 테이블을 지정하기 위해 @SecondaryTable과 @AttritubeOverride를 사용한다.

@Entity
@Table(name = "article")
@SecondaryTable(
    name = "article_content"
    pkJoinColumns = @PrimaryKeyJoinColumn(name = "id")
)
public class Article {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String title;
    
    @AttributeOverrides({
        @AttributeOverride(
            name = "content"
            column = @Column(table = "article_content", name = "content")),
        @AttributeOverride(
            name = "contentType",
            column = @Column(table = "article_content", name = "content_type"))
    })
    @Embedded
    private ArticleContent content;
}

@AttributeOverride 애너테이션을 사용해 해당 밸류 데이터가 저장된 테이블 이름을 지정한다.  @SecondaryTable을 이용하면 아래 코드를 실행할 때 두 테이블을 조인해서 데이터를 조회한다.

Article article = entityManager.find(Article.class, 1L);

 

개념적으로는 밸류인데 구현 기술의 한계나 팀 표준 때문에 @Entity를 사용해야 할 때도 있다.예를들어 제품의 이미지 업로드 방식에 따라 이미지 경로와 섬네일 이미지 제공 여부가 달라진다고 하면, 이를 위해 Image를 다음과 같이 계층 구조로 설계할 수 있다.

JPA는 @Embeddable 타입의 클래스 상속 매핑을 지원하지 않는다. 상속 구조를 갖는 밸류 타입을 사용하려면 @Entity를 이용해서 상속 매핑으로 처리해야 한다.

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "image_type")
@Table(name = "image")
@Getter
public abstract class Image {
    @Id
    @GenerateValue(strategy =. enerationType.IDENTY)
    private Long id;
    
    @Column(name = "image_path")
    private String path;
    
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "upload_time")
    private Date uploadTime;
    
    protected Image() {}
    
    public abstract String getURL();
    public abstract boolean hasThumbnail();
    public abstract String getThumbnailURL();
}

Image를 상속받은 클래스를 @Entity와 @Discriminator를 사용해서 매핑을 설정한다.

@Entity
@DiscriminatorValue("II")
public class InternalImage extends Image {
    ...
}

@Entity
@DiscriminatorValue("EI")
public class ExternalImage extends Image {
    ...
}

Image는 @Entity이므로 목록을 담고 있는 Product는 @OneToMany를 이용해서 매핑을 처리한다. Image는 밸류이므로 독자적인 라이프 사이클을 갖지 않고 Product에 완전히 의존한다. 따라서 Product를 저장할 때 함께 저장되고 Product를 삭제할 때 함께 삭제되도록 cascade 속성을 지정한다. 또 리스트에서 Image 객체를 제거하면 DB에서 함께 삭제되도록 orphanRemoval도 true로 지정한다.

@Entity
@Table(name = "product")
public class Product {
    @EmbeddedId
    private ProductId id;
    private String name;
    
    @Convert(converter = MoneyConverter.class)
    private Money price;
    private String detai;
    
    
    @OneToMany(
        cascade = {CascadeType.PERSIST, CascadeType.REMOVE},
        orphanRemoval = true, fetch = FetchType.EAGER)
    @JoinColumn(name = "product_id")
    @Ordercolumn(name = "list_idx")
    private List<Image> images = new ArrayList<>();
    
    public void changeImages(List<Image> newImages) {
        images.clear();
        images.addAll(newImages);
    }
}

changeImages() 메서드를 보면 이미지 교체를 위해 clear() 메서드를 사용하고 있는데, @Entity에 대한 @OneToMany 매핑에서 컬렉션의 clear() 메서드를 호출하면 삭제 과정이 효율적이지는 않다. 하이버네이트의 경우에는 select 쿼리로 대상 엔티티를 로딩하고, 각 개별 엔티티에 대해 delete 쿼리를 실행하게 된다. 하지만 @Embeddable 타입에 대한 컬렉션의 clear() 메서드는 컬렉션에 속한 객체를 로딩하지 않고 한 번의 delete 쿼리로 삭제 처리를 한다. 따라서 애그리거트의 특성을 유지하면서 이 문제를 해결하려면 결국 상속을 포기하고 @Embeddable로 매핑된 단일 클래스로 구현해야 한다.

@Embeddable
public class Image {
    @Column(name = "image_type")
    private String imageType;
    @Column(name = "image_path")
    private String path;
    
    @Emporal(TemporalType.TIMESTAMP)
    private Date uploadTime;
    ...
    
    public boolean hasThumbnail() {
        if(imageType.equals("II") {
            return true;
        } else {
            return false;
        }
    }
}

코드 유지 보수와 성능의 두 가지 측면을 고려해서 구현 방식을 선택해야 한다.

 

JPA 매핑을 설정할 때는 애그리거트에 속한 객체가 모두 모여야 완전한 하나가 된다는 것이다. 조회 시점에서 애그리거트를 완전한 상태가 되도록 하려면 애그리거트 루트에서 연관 매핑의 조회 방식을 즉시 로딩(FetchType.EAGER)로 설정하면 된다. (위 코드에 적용) 그러면 애그리거트 루트를 구할 때 연관된 구성요소를 DB에서 함께 읽어온다.

즉시 로딩 방식으로 설정하면 애그리거트 루트를 로딩하는 시점에 애그리거트에 속한 모든 객체를 함께 로딩할 수 있다는 장점이 있지만, 문제가 될 수 있는데 카타시안 조인을 사용할 경우가 있다. Product의 image가 2개, option이 2개면 쿼리 결과로 구해지는 행 개수는 4개가 된다. 물론 하이버네이트가 중복된 데이터를 알맞게 제거해서 실제 메로리에는 1개의 Product, 2개이 Image, 2개의 Option 객체로 변환해 주지만 애그리거트가 커지면 문제가 될 수 있으므로 성능을 잘 검토해야 한다.

 

애그리거트는 개념적으로 하나여야 하는데, 애그리거트가 완전해야 하는 이유는 상태를 변경하는 기능을 실행할 때 애그리거트 상태가 완전해야 하기 때문이고, 표현 영역에서 애그리거트의 상태 정보를 보여줄 때 필요하기 때문이다. 일반적인 애플리케이션은 상태 변경 기능을 실행하는 빈도보다 조회 기능을 실행하는 빈도가 훨씬 높으므로 상태 변경을 위해 지연 로딩을 사용할 때 발생하는 추가 쿼리로 인한 실행 속도 저하는 보통 문제가 되지 않는다. 이런 이유로 애그리거트 내의 모든 연관을 즉시 로딩으로 설정할 필요는 없다. 지연 로딩은 동작 방식이 항상 동일하기 때문에 즉시 로딩처럼 경우의 수를 따질 필요가 없는 장점이 있다. 물론 지연 로딩은 즉시 로딩보다 쿼리 실행 횟수가 많아질 가능성이 있지만 애그리거트에 맞게 즉시 로딩과 지연 로딩을 선택해야 한다.

 

애그리거트가 완전한 상태여야 한다는 것은 애그리거트 루트를 조회할 때뿐만 아니라 저장하고 삭제할 때도 하나로 처리해야 함을 의미한다. @Embeddable 매핑 타입은 함께 저장되고 삭제되므로 cascade 속성을 추가로 설정하지 않아도 되지만, @Entity 타입에 대한 매핑은 cascade 속성을 사용해서 저장과 삭제 시에 함께 처리되도록 설정해야 한다.

 

식별자는 크게 세 가지 방식 중 하나로 생성한다.

  • 사용자가 직접 생성
  • 도메인 로직으로 생성
  • DB를 이용한 일련번호 사용

식별자 생성 규칙이 있다면 엔티티를 생성할 때 식별자를 엔티티가 별도 서비스로 생성기능을 분리해야 한다. 식별자 생성 규칙은 도메인 규칙이므로 도메인 영역에 식별자 생성 기능을 위치시켜야 한다.

public class ProductIdService {
    public ProductId nextId() {
        ... // 식별자 생성
    }
}

public class CreateProductService {
    private ProductIdService idservice;
    private ProductRepository producyRepository;
    
    @Transactional
    public ProductId createProduct(ProductCreationCommand cmd) {
        ProductId id = productIdService.nextId();
        Product product = new Product(id, cmd.getDetail(), ...);
        productRepository.save(product);
        return id;
    }
}

식별자 생성 규칙을 구현하기에 적합한 또 다른 장소는 리포지터리다. 리포지터리 인터페이스에 식별자를 생성하는 메서드를 추가하고 구현 클래스에서 알맞게 구현하면 된다.

 

지금까지 리포지터리를 구현하면서 DIP 원칙을 지키지 않고 있었는데, 엔티티는 JPA에 특화된 @Entity, @Table 등의 애너테이션을 사용하고 있다. DIP에 따르면 해당 애너테이션은 구현 기술에 속하므로 Article과 같은 도메인 모델은 구현 기술인 JPA에 의존하지 말아야 하는데 이 코드는 도메인 모델인 Article이 영속성 구현 기술인 JPA에 의존하고 있다.

리포지터리 인터페이스도 마찬가지다. 레포지터리 인터페이스는 도메인 패키지에 위치하는데 구현 기술인 스프링 데이터 JPA의 Repository 인터페이스를 상속하고 있다. 즉, 도메인이 인프라에 의존하는 것이다. DIP를 적용하려면 스프링 데이터 JPA의 Repository 인터페이스를 상속받지 않고 직접 구현을 해야한다. 

DIP를 적용하는 주된 이유는 저수준 구현이 변경되더라도 고수준이 영향을 받지않도록 하기 위함이다. 하지만 리포지터리와 도메인 모델의 구현 기술은 거의 바뀌지 않는다. 따라서 개별 편의성과 실용성을 가져가면서 구조적인 유연함은 어느정도 유연하게 유지하는게 좋다.

 

 

 

 

 

 

참고

https://incheol-jung.gitbook.io/docs/study/jpa/16

 

16장 트랜잭션과 락, 2차 캐시 - Incheol's TECH BLOG

트랜잭션은 ACID라 하는 원자성(Atomicty), 일관성(Consistency), 격리성(Isolation), 지속성(Durability)을 보장해야 한다.

incheol-jung.gitbook.io

https://junseokdev.tistory.com/39

 

[JPA] 영속성 컨텍스트란?

영속성 컨텍스트(Persistence Context)란 엔티티를 영구 저장하는 환경으로, 애플리케이션과 데이터베이스 사이에서 객체를 보관하는 논리적인 개념이다. EntityManager를 통해서 영속성 컨텍스트에 접

junseokdev.tistory.com

도메인 주도 개발 시작하기(DDD 핵심 개념 정리부터 구현까지) - 최범균

'DDD' 카테고리의 다른 글

도메인 서비스, 애그리거트 트랜잭션 관리  (0) 2022.12.20
응용 서비스와 표현 영역  (0) 2022.12.13
애그리거트  (0) 2022.11.13
아키텍처 개요  (0) 2022.11.08
도메인 모델 시작하기  (0) 2022.10.31