ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 4장 - 리포지터리와 모델 구현
    📕 book/도메인 주도 개발 시작하기 2023. 1. 2. 23:05

    📌 JPA를 이용한 리포지터리 구현


    모듈 위치

    리포지터리 인터페이스는 애그리거트와 같이 도메인 영역에 속한다.

    리포지터리를 구현한 클래스는 인프라스트럭처 영역에 속한다.

    • 가능한 리포지터리 구현 클래스를 인프라스트럭처 영역에 위치시켜서 인프라스트럭처에 대한 의존을 낮춰야 한다.

     

    리포지터리 기본 기능 구현

    리포지터리가 제공하는 기본 기능

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

    리포지터리 인터페이스 - OrderRepository

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

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

    애그리거트 조회하는 기능의 이름을 지을때 널리 사용하는 규칙 - ‘findBy프로퍼티이름(프로퍼티 값)’

     

    스프링 프레임워크에 기반한 리포지터리 구현 클래스

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

    애그리거트를 수정한 결과를 저장소에 반영하는 메서드를 추가할 필요 ❌

    ⇒ JPA를 사용하면 트랜잭션 범위에서 변경한 데이터를 자동으로 DB에 반영하기 때문이다.

     

    📌 스프링 데이터 JPA를 이용한 리포지터리 구현


    스프링과 JPA를 함께 적용할 때는 스프링 데이터 JPA를 사용한다.

    • 스프링 데이터 JPA는 지정한 규칙 에 맞게 리포지터리 인터페이스를 정의하면 리포지터리를 구현한 객체를 알아서 만들어 스프링 빈으로 등록해준다.

    스프링 데이터 JPA는 아래 규칙에 따라 작성한 인터페이스를 찾고 인터페이스를 구현한 스프링 빈 객체를 자동으로 등록해준다.

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

    스프링 데이터 JPA를 이용한 OrderRepository 인터페이스

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

     

    📌 매핑 구현


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

    애그리거트와 JPA 매핑을 위한 기본규칙

    • 애그리거트 루트는 엔티티이므로 @Entity 로 매핑 설정을 한다.

    한 테이블에 엔티티와 밸류 데이터가 같이 있다면

    • 밸류는 @Embeddable 로 매핑 설정
    • 밸류 타입 프로퍼티는 @Embedded 로 매핑 설정한다.

     

    기본 생성자

    JPA에서 @Entity@Embeddable로 클래스를 매핑하려면 기본 생성자를 제공해야 한다.

     

    ❗DB에서 데이터를 읽어와 매핑된 객체를 생성할 때 기본 생성자를 이용해 객체를 생성하기 때문

    ⇒ 불변 타입은 기본생성자가 필요없음에도 불구하고 기본생성자를 추가해야 한다.

     

    기본 생성자는 JPA 프로바이더가 객체를 생성할 때만 사용한다.

    • 다른 코드에서 사용하면 값이 없는 온전하지 못한 객체를 생성하게 된다.

    따라서, 다른 코드에서 기본 생성자를 사용하지 못하도록 protected로 선언!!

     

    @Embbeddable
    public class Receiver {
      ...
    
      protected Receiver(){}
    }

     

    필드 접근 방식 사용

    JPA는 필드와 메서드 두 가지 방식으로 매핑을 처리할 수 있다.

     

    메서드 방식을 이용하면 프로퍼티를 위한 get/set 메서드를 구현해야 한다.

    → 도메인의 의도가 사라지고 객체가 아닌 데이터 기반으로 엔티티를 구현할 가능성이 높아진다.

    → 특히 set 메서드는 내부 데이터를 외부에서 변경할 수 있는 수단 ⇒ 캡슐화를 깨는 원인

     

    객체가 제공할 기능을 중심으로 엔티티를 구현하고자 하면 JPA 매핑 처리를 필드 방식으로 선택해 불필요한 get/set 메서드를 구현하지 말아야 한다.

    @Entity
    @Access(AccessType.FIELD)
    public class Order {
    
      @EmbbeddedId
      private OrderNo number;
      ...
    }
    • JPA 구현체인 하이버네이트는 @Access를 이용해 명시적으로 접근 방식을 지정하지 않는다면 @Id, @EmbeddedId 가 어디에 위치했느냐에 따라 접근 방식을 결정한다.
      • 필드에 위치하면 필드 접근 방식
      • get 메서드에 위치하면 메서드 접근 방식

     

    AttributeConverter를 이용항 밸류 매핑 처리

    밸류 타입의 프로퍼티를 한 개 컬럼에 매핑해야할 경우!!

     

    두 개 이상의 프로퍼티를 가진 밸류 타입을 한 개 컬럼에 매핑하려면 @Embeddable 애너테이션으로 할 수 없다.

    AttributeConverter

    • 밸류 타입과 컬럼 데이터간의 변환을 처리하기 위한 기능을 정의
    public interface AttributeConverter<X, Y> {
    
      public Y convertToDatabaseColumn (X attribute); // 밸류 타입을 DB 컬럼 값으로 변환
    	
      public X convertToEntityAttribute (Y dbData); // DB 컬럼 값을 밸류로 변환
    	
    }
    • X : 밸류 타입
    • Y : DB 타입

     

    Money를 위한 AttributeConverter 구현

    @Converter(autoApply = true)
    public class MoneyConverter implements AttributeConveter <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);
      }
    
    }

    AttributeConverter 인터페이스를 구현한 클래스는 @Converter 애너테이션을 적용한다.

    • autoApply = true
      • 지정하는 모델에 출현하는 모든 Money 타입의 프로퍼티에 대해 자동으로 MoneyConverter를 적용
      • ex) Order의 totalAmounts 프로퍼티가 Money 타입이라면 이 프로퍼티를 DB에서 total_amounts 컬럼에 매핑시 MoneyConverter를 사용한다.
    • autoApply = false
      • 프로퍼티 값을 변환할 때 사용할 컨버터를 직접 지정해야 한다.

     

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

     

    밸류 컬렉션 : 별도 테이블 매핑

    밸류 컬렉션을 저장하는 테이블은 외부키를 이용해 엔티티에 해당하는 테이블을 참조한다.

    밸류 컬렉션을 별도 테이블로 매핑할 때는 @ElementCollection@CollectionTable을 함께 사용한다.

    @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 {
      @EmbeddedId
      private ProductId productId;
    	
      @Column(name = "price")
      private Money price;
    	
      @Column(name = "quantity")
      private int quantity;
    
      ...
    }
    • @OrderColumn
      • JPA는 이 애너테이션을 이용해 지정한 컬럼에 리스트의 인덱스 값을 저장한다.
    • @CollectionTable
      • 밸류를 저장할 테이블을 지정한다
      • name - 테이블 이름 지정
      • joinColumns - 외부키로 사용할 컬럼을 지정

     

    밸류 컬렉션 : 한개 컬럼 매핑

    밸류 컬렉션을 별도 테이블이 아닌 한 개 컬럼에 저장해야 할 때가 있다.

     

    AttributeConverter를 사용하면 밸류 컬렉션을 한 개 컬럼에 쉽게 매핑할 수 있다.

    • 단, AttritubeConverter를 사용하기 위해선 밸류 컬렉션을 표현하는 새로운 밸류 타입을 추가해야 한다.

    이메일을 위한 새로운 밸류 타입을 추가로 작성 = EmailSet

     

    public class EmailSet {
      private Set<Email> emails = new HashSet<>();
    
      ...
    }
    • 도메인 영역에서 이메일 목록을 Set을 이용해 보관

     

    EmailSetConverter

    public class EmailSetConverter implements AttributeConverter<EmailSet, String> {
        @Override
        public String convertToDatabaseColumn(EmailSet attribute) {
            if (attribute == null) return null;
            return attribute.getEmails().stream()
                    .map(email -> email.getAddress())
                    .collect(Collectors.joining(","));
        }
    
        @Override
        public EmailSet convertToEntityAttribute(String dbData) {
            if (dbData == null) return null;
            String[] emails = dbData.split(",");
            Set<Email> emailSet = Arrays.stream(emails)
                    .map(value -> new Email(value))
                    .collect(toSet());
            return new EmailSet(emailSet);
        }
    }

     

    EmailSet 타입 프로퍼티가 Converter사용

    @Column(name = "emails")
    @Convert(converter = EmailSetConverter.class)
    private EmailSet emailSet;

     

    밸류를 이용한 ID 매핑

    식별자 의미를 강조하기 위해 식별자 자체를 밸류 타입으로 만들 수 있다.

    → 밸류 타입을 식별자로 매핑하면 @Id 대신 @EmbeddedId 애너테이션을 사용한다.

     

    JPA에서 식별자 타입은 Serializable 타입이어야 한다.

    따라서 식별자로 사용할 밸류타입은 Serializable 인터페이스를 상속받아야 한다.

     

    @Entity
    @Table(name = "purchase_order")
    @Access(AccessType.FIELD)
    public class Order {
        @EmbeddedId
        private OrderNo number;
    	...
    }
    
    @Embeddable
    public class OrderNo implements Serializable {
        @Column(name = "order_number")
        private String number;
    	...
    }

     

    별도 테이블에 저장하는 밸류 매핑

    애그리거트에서 루트 엔티티를 뺀 나머지 구성요소는 대부분 밸류이다.

    루트 엔티티 외에 또 다른 엔티티가 있다면 진짜 엔티티인지 의심해봐야 한다.

     

    엔티티가 확실하다면 해당 엔티티가 다른 애그리거트는 아닌지 확인해야 한다.

    애그리거트가 속한 객체가 밸류인지 엔티티인지 구분하는 방법

    • 고유 식별자를 갖는지 확인

    식별자를 찾을 때 매핑되는 테이블의 식별자를 애그리거트 구성요소의 식별자와 동일한것으로 착각하면 안된다.

     

    예시) 게시글 데이터를 Article 테이블과 Article_content 테이블로 나눠서 저장하는 경우

    • Article과 ArticleContent 클래스를 두 테이블에 매핑할 수 있다.
    • ArticleContent 테이블의 ID컬럼이 식별자 이므로 엔티티로 생각할 수 있지만 Article의 내용을 담고 있는 밸류로 생각하는 것이 맞다.

    밸류를 매핑 한 테이블을 지정하기 위해 @SecondaryTable과 @AttributeOverride를 사용한다.

     

    Article 엔티티

    @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;
    • @SecondaryTable
      • name - 밸류를 저장할 테이블 지정
      • pkJoinColumns - 밸류 테이블에서 엔티티 테이블로 조인할 때 사용할 컬럼을 지정
    • @AttributeOverride
      • 해당 밸류 데이터가 저장된 테이블 이름을 지정한다.

     

    @SecondaryTable을 이용하면 두 테이블을 조인해 데이터를 조회한다.

     

    밸류 컬렉션을 @Entity로 매핑하기

    개념적으로 밸류지만 구현 기술의 한계나 팀 표준 때문에 @Entity를 사용할 때도 있다.

     

    @Entity로 매핑했지만 밸류 이므로 상태를 변경하는 기능은 추가하지 않는다.

     

    ID 참조와 조인 테이블을 이용한 단방향 M-N 매핑

    애그리거트 간 집합 연관은 성능상의 이유로 피해야 하지만 집합 연관을 사용하는 것이 유리하다면 ID 참조를 이용한 단방향 집합 연관을 적용해 볼 수 있다.

    @Entity
    @Table(name = "product")
    public class Product {
        @EmbeddedId
        private ProductId id;
    
        @ElementCollection(fetch = FetchType.LAZY)
        @CollectionTable(name = "product_category",
                joinColumns = @JoinColumn(name = "product_id"))
        private Set<CategoryId> categoryIds;
    	...
    }
    • Product에서 Category로의 단반향 M-N 연관을 ID 참조 방식으로 구현한 것
    • ID 참조를 이용한 애그리거트 간 단반향 M-N 연관은 밸류 컬렉션 매핑과 동일한 방식으로 설정한다.

    @ElementCollection을 이용하기 때문에 Product를 삭제할 때 매핑에 사용한 조인 테이블의 데이터도 함께 삭제된다.

     

    📌 애그리거트 로딩 전략


    JPA 매핑 설정시 기억해야 할 점은 애그리거트에 속한 객체가 모두 모여야 완전한 하나가 된다는 것이다.

    → 애그리거트 루트를 로딩하면 루트에 속한 모든 객체가 완전한 상태여야 함을 의미

     

    이를 위해선 애그리거트 루트에서 연관 매핑의 조회 방식을 즉시 로딩으로 설정하면 된다.

    • 애그리거트 루트를 로딩하는 시점에 애그리거트에 속한 모든 객체를 함께 로딩할 수 있는 장점
    • 항상 좋은 것은 아니다.

     

    컬렉션에 대해 로딩 전략을 즉시로딩으로 설정하면 문제가 될 수 있다.

    → 카타시안 조인을 사용하고 쿼리 결과에 중복을 발생 시킨다.

     

    애그리거트는 개념적으로 하나여야 한다

    하지만, 루트 엔티티를 로딩하는 시점에 애그리거트에 속한 객체를 모두 로딩해야 하는 것은 아니다 ❌

    이유

    • 상태를 변경하는 기능을 실행할 때 애그리거트 상태가 완전해야 하기 때문
    • 표현 영역에서 애그리거트의 상태 정보를 보여줄 때 필요하기 때문

     

    상태 변경 기능을 실행하기 위해 조회시점에서 즉시 로딩을 이용해 완전한 상태로 로딩할 필요는 없다.

    JPA는 트랜잭션 범위 내에서 지연로딩을 허용하기 때문에 실제로 상태를 변경하는 시점에 필요한 구성요소만 로딩해도 문제가 되지 않는다.

    📌 애그리거트의 영속성 전파


    애그리거트가 완전한 상태여야 하는 것은 애그리거트 루트를 조회할 때 뿐아니라 저장하고 삭제할 때도 하나로 처리해야 함을 의미

    @Embeddable 매핑 타입은 함께 저장되고 삭제되므로 cascade 속성을 추가로 설정하지 않아도 된다.

     

    📌 식별자 생성 기능


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

    1. 사용자가 직접 생성
    2. 도메인 로직으로 생성
    3. DB을 이용한 일련번호 사용

    DB 자동 증가 컬럼을 식별자로 사용하면 식별자 매핑에서 @GeneratedValue 을 사용한다.

    • DB의 Insert 쿼리를 실행해야 식별자가 생성되므로 도메인 객체를 리포지터리에 저장할 때 생성된다.

     

    참고자료

    http://www.yes24.com/Product/Goods/108431347

    댓글

Designed by Tistory.