본문 바로가기

책리뷰/도메인 주도 개발 시작하기(DDD핵심 개념 정리부터 구현까지)

도메인 주도 개발(DDD) 리포지터리와 모델 구현

728x90
반응형

목차

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

    - 도메인 모델과 리포지터리를 구현할 때 선호하는 기술은 JPA이다.

    - 데이터 보관소로 RDBMS를 사용할 때, 객체 기반 도메인 모델과 관계 데이터 모델간의 매핑 처리 기술로 ORM만한 것이 없음 

     

    모듈위치

    • 리포지터리 인터페이스는 애그리거트와 같이 도메인 영역에 속함
    • 리포지터리 구현 클래스는 인프라스트럭처 영역
    • 팀 표준에 따라 다르지만 리포지터리 구현 클래스틑 인프라스트럭처에 둬서 의존을 낮춰야 함

     

    리포지터리 기본 기능 구현

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

    • ID로 애그리거트 조회하기
    • 애그리거트 저장하기 
    public interface OrderRepository {
    	Order findById(OrderNo no);
        void save(Order order);
    }

    ex) 주문 애그리거트는 Order 루트 엔티티, OrderLine(주문목록), Orderer(주문자), ShippingInfo(배송지) 등 객체를 포함

    위의 코드에서는 루트 엔티티인 Order 기준으로 인터페이스 작성

     

    애그리거트 조회 기능의 이름은 'findBy프로퍼티이름' 형식이 널리 사용됨

     

     

    • null리턴을 원하지 않을 경우 Optional 사용
    Optional<Order> findById(OrderNo no);

     

    • OrderRepository 구현 클래스 
    @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);
        }
    }

     

    • 애그리거트 트랜잭션 
    public class ChangeOrderService {
    	@Transactional
        public void changeShippingInfo(OrderNo no, ShippingInfo newShppingInfo) {
        	Optional<Order> orderOpt = orderRepository.findById(no);
            Order order = orderOpt.orElseThrow(() -> new OrderNotFoundException());
            order.changeShippingInfo(newShippingInfo);
        }
        ...
    }
    • changeShippingInfo() 메서드는 Spring Transaction 관리기능을 통해 트랜잭션 범위에서 실행됨 
    • JPA는 이때 DB에 변경된 데이터 반영을 위해 UPDATE실행 
    • changeShippingInfo()메서드 실행 결과로 애그리거트가 변경되면 JPA는 DB에 Update 쿼리 실행 

     

    • ID가 아닌 조건으로 애그리거트 조회 findBy프로퍼티
    public interface OrderRepository {
    	List<Order> findByOrdererId(String ordererId, int startRow, int size);
    }
    • findByOrderId 메서드는 한 개 이상의 Order객체를 리턴할 수 있음 (List타입으로 리턴)
    • JPQL을 이용해서도 구현할 수 있음 
    • 애그리거트 삭제 > EntityManager의 remove()로 삭제 가능
      • 요구사항이 있더라도 실제 DB삭제하는 경우는 많지 않음(데이터를 화면에 보여줄지 여부 결정하는 방식)

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

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

    - 인터페이스를 직접 구현하지 않아도 되기 때문에 쉽게 정의할 수 있음 

     

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

     

    • Spring Data JPA 빈 등록 
    @Service
    public class CancelOrderService{
    	private OrderRepository orderRepository;
        
        public cancelOrderService(OrderRepository orderRepository, ...) {
        	this.orderRepository = orderRepository;
            ..
        }
        
        @Transactional
        public void cancel(OrderNo orderNo, Canceller canceller) {
        	Order order = orderRepository.findById(orderNo)
            	.orElseThrow(() -> new NoOrderException());
                
           if(!cancelPolicy.hasCancellationPermission(order, canceller)) {
           		throw new NoCancellablePermission();
           }
           order.cancel();
        }
    }
    • OrderRepository가 필요하면 코드를 주입받아 사용하면 된다.

     

    • OrderRepository 엔티티 저장하는 메서드
    Order save(Order entity)
    void save(Order entity)

     

    • 식별자 조회
    Order findById(OrderNo id)
    Optional<Order> findById(OrderNo id)

     

    • Order목록 조회
    List<Order> findByOrderer(Orderer orderer)
    
    // Order객체의 memberId 프로퍼티가 목록 조회(중첩파라미터)
    List<Order> findByOrderMemberId(MemberId memberId)

     

    • Entity 삭제
    void delete(Order order)
    void deleteById(OrderNo id)

     


    매핑 구현

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

    • 애거리루트는 @Entity로 매핑 설정 
    • 한 테이블에 엔티티와 밸류데이터가 같이 있을 경우 
      • 밸류는 @Embeddable 
      • 밸류 타입프로퍼티는 @Embedded

    • 주문 애그리거트 루트 엔티티는 Order
    • Orderer와 ShippingInfo는 밸류
    • ShppingInfo에 포함된 Address, Receiver는 한 테이블에 매핑할 수 있음 

     

    • 루트 Entity Order는 JPA @Entity로 매핑 
    @Entity
    @Table(name="purchase_order")
    public class Order {
    	...
    }

     

     

    • Order에 속하는 Orderer는 밸류 @Embeddable로 매핑 
    @Embeddable
    public class Orderer {
    	
        // MemberId에 정의된 컬럼 이름을 변경하기 위해
        // @AttributeOverride 애너테이션 
        @Embedded
        @AttributeOverrides(
        	@AttributeOverride(name = "id", Column = @Column(name="orderer_id"))
        )
    	private MemberId memberId;
        
        @Column(name = "orderer_name")
        private String name;
    }
    • Orderer의 memberId와 매핑되는 컬럼은 orderer_id
    • @Embeddable타입에 설정한 컬럼이름과 실제 컬럼이름이 다르므로 @AttributeOverrids 이용해서 Orderer의 memberId프로퍼티와 매핑 컬럼 이름 변경 

     

    • ShippingInfo 밸류도 Address와 Receiver를 포함
    @Embeddable
    public class ShippingInfo {
    	@Embedded
        @AttributeOverrids({
        	@AttributeOverride(name = "zipCode", column= @Column(name="shipping_zipcode")),
            @AttributeOverride(name = "address1", column= @Column(name="shipping_addr1")),
            @AttributeOverride(name = "address2", column= @Column(name="shipping_addr2"))
        })
        private Address address;
        
        @Column(name = "shipping_message")
        private String message;
        
        @Embedded
        private Receiver receiver;
    }
    • @Embedded를 이용해서 밸류 타입 프로퍼티를 설정 

    기본 생성자

    • 엔티티와 밸류 생성자는 객체 생성시 필요한 것을 전달받음 
    public class Receiver {
    	private String name;
        private String phone;
        
        public Receiver(String name, String phone) {
        	this.name = name;
            this.phone = phone;
        }
    }
    • Receiver가 불변타입이면 생성시 모두 값을 전달하므로 set메서드 제공하지 않음 
    • JPA에서 @Entity와 @Embeddable 클래스 매핑시 기본 생성자를 제공해야함
      DB에서 데이터를 읽어와서 매핑된 객체 생성시 기본 생성자를 사용해서 객체를 생성하기 때문 

    @Embeddable
    public class Receiver {
    	@Column(name = "receiver_name")
        private String name;
        @Column(name = "receiver_phone")
        private String phone;
        
        protected Receiver() {}	// JPA 적용을 위한 기본 생성자
        
        public Receiver(String name, String phone) {
        	this.name = name;
            this.phone = phone;
        }
    }
    • 기본생성자는 JPA 프로바이더가 객체 생성시에만 사용 
    • 다른 코드에서 사용할 경우 값이 온전하지 못한 객체가 되기 때문에 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은 외부에서 변경할 수 있는 수단이므로 캡슐화가 깨지는 원인이 됨
    • set메서드 대신 의도가 잘 드러나는 기능 제공 
    • 객체가 제공할 기능 중심 엔티티 구현 방법
    @Entity
    @Access(AccessType.FIELD)
    public class Order {
    
        @EmbeddedId
        private OrderNo number;
    
        @Column(name = "state")
        @Enumerated(EnumType.STRING)
        private OrderState state;
    
        ... // cancel(), changeShippingInfo() 등 도메인 기능 구현
        ... // 필요한 get메서드 제공
    }
    
    • JPA매핑 처리를 필드 방식으로 선택
    • 불필요한 get/set 구현 X

    AttributeConverter를 이용한 밸류 매핑처리

    • int, long, String, LocalDate 같은 타입은 DB테이블 한 개 컬럼에 매핑
    • 밸류타입의 프로퍼티를 한 개 컬럼에 매핑할 때가 있음 
    • @Embeddable 애너테이션으로 처리 X

    • AttributeConverter
    public interface AttributeConverter<X, Y>{
        public Y convertToDatabaseColumn(X attribute);
        public X convertToEntityAttribute(Y dbData);
    }
    @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);
        }
    }
    • AttributeConverter인터페이스 구현 클래스는 @Converter 애너테이션 적용
    • autoApply = true 일 때 모델에 출현하는 모든 Money타입 프로퍼티에 대해 MoneyConverter 자동 적용

    @Entity
    @Table(name = "purchase_order")
    public class Order {
    
        @Column(name = "total_amounts")
        private Money totalAmounts; // MoneyConverter적용해서 값 변환
    }

     

    • autoApply속성 fal이면 프로퍼티 변환시 컨버터 직접 지정
    public class Order {
    
        @Column(name = "total_amounts")
        @Convert(converter = MoneyConverter.class)
        private Money totalAmounts; 
    }

     

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

    • Order 엔티티는 한 개 이상의 OrderLine을 가질 수 있음

    • ORDER_LINE테이블은 order_number외부키를 이용해 PURCHASE_ORDER테이블 참조 
    • 인덱스값을 저장하기 위한 컬럼도 존재(line_idx)

    @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<OrderList> orderLines;
        
    }
    
    @Embeddable
    public class OrderLine {
        @Embedded
        private ProductId productId;
        
        @Column(name = "quantity")
        private int quantity;
        
        @Column(name = "amounts")
        private Money amounts;
    }
    • JPA는 @OrderColumn을 통해 지정한 컬럼에 리스트의 인덱스 값을 저장 
    • @CollectionTable은 밸류를 저장할 테이블 지정 (name = 테이블이름, joinColumn = 외부키로 사용할 컬럼)

     

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

    • 밸류컬렉션을 별도 테이블이 아닌 한 개 컬럼에 저장해야할 때 
    • 도메인 모델에는 이메일 주소 목록을 Set으로 보관, DB에는 한 개 컬럼에 콤마 구분할 경우 

     

    • AttributeConverter
    public class EmailSet {
        private Set<Email> emails = new HashSet<>();
        
        public EmailSet(Set<Email> emails) {
            this.emails.addAll(emails);
        }
        
        public Set<Email> getEmails() {
            return Collections.unmodifiableSet(emails);
        }
    }
    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);
        }
    }
    @Column(name = "emails")
    @Convert(converter = EmailSetConverter.class)
    private EmailSet emailSet;

     

     

    밸류를 이용한 ID매핑

    • 식별자 자체를 밸류 타입으로 만들 수 있음 
    @Entity
    @Table(name = "purchase_order")
    public class Order {
        @EmbeddedId
        private OrderNo number;
    }
    
    @Embeddable
    public class OrderNo implements Serializable {
        @Column(name = "order_number")
        private String number;
    }
    • JPA에서 식별자 타입은 Serializable타입이어야 함 
    • 밸류 타입으로 식별자 구현시 장점 
      • 식별자에 기능을 추가할 수 있음 
      • 1세대 주문번호, 2세대 주문 번호 구분시 첫 글자 이용할 경우 OrderNo클래스에서 구분 기능 구현 가능
    @Embeddable
    public class OrderNo implements Serializable {
        @Column(name = "order_number")
        private String number;
        
        public boolean is2ndGeneration() {
        	return number.startsWith("N");
        }
    }
    • JPA는 내부적으로 엔티티 비교시 equals(), hashCode값 사용 
    • 식별자로 사용할 밸류타입은 이 두 메서드를 알맞게 구현해야함 

     

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

    • 애그리거트에서 루트 엔티티 뺀 부분은 대부분 밸류
    • 또다른 엔티티가 있을 경우 애그리거트인지 체크 필요 
    • ex) Product와 Review는 서로 영향을 주지 않기 때문에 다른 애그리거트다. 
    • 애그리거트에 속한 객체가 밸류인지 엔티티인지 구별하는 방법
      • 고유 식별자를 갖는지 확인 할 것
      • 별도의 PK가 있다고 해서 고유식별자를 갖는 것은 아니다. 

    • ARTICLE_CONTENT 테이블의 ID컬럼은 식별자이기 때문에 엔티티로 생각할 수 있지만
      Article의 내용을 담고 있으므로 밸류다.
    • ARTICLE_CONTENT의 ID는 ARTICLE테이블과 연결하기 위함, ARTICLE_CONTENT를 위한 별도 식별자가 필요한 것이 아님 

    • ArticleContent는 밸류이므로 @Embeddable로 매핑해야함 
    • ArticleContent와 매핑되는 테이블은 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;
        
        @AttributeOverride({
                @AttributeOverride(
                        name="content",
                        column = @Column(table = "article_content", name = "content")),
            @AttributeOverride(
                    name = "content",
                    column = @Column(table = "article_content", name = "content_type"))
        })
        @Embedded
        private ArticleContent content;
    }
    • 밸류를 매핑한 테이블 지정을 위해 @SecondaryTable, @AttributeOverride사용
    • @SecondaryTable의 name 속성은 밸류 테이블 , pkJoinColumns 속성 밸류 테이블에서 조인할 컬럼 

    • 위 코드를 통해 두 테이블 조인해서 데이터 조회 
    • 게시글 목록에서는 Article_content 데이터가 필요 없음 
    • 이 문제를 해소하기 위해 ArticleContent를 엔티티 매핑하고 지연 로딩 방식을 쓸 수 있음
    • 좋은 방법은 아님(밸류를 엔티티로 만드는 것이기 때문에)

     

    밸류컬렉션을 @Entity로 매핑

    • 밸류이지만 구현기술 한계나 팀 표준때문에 @Entity를 사용해야할 때가 있음 

    • 이미지 업로드 방식에 따라 이미지 경로와 썸네일 이미지 제공여부가 달라지는 경우 
    • JPA는 @Embeddable타입의 클래스 상속 매핑 지원 X
    • 상속 구조를 갖는 밸류 타입 사용하려면 @Entity사용이 필요 

    • @Entity매핑시 타입식별을 위해 식별 컬럼을 추가해야함 
    • 한 테이블에 Image와 하위 클래스를 매핑하므로 @Inheritance 애너테이션 적용 
    • strategy 값으로 SINGLE_TABLE사용
    • @DiscriminatorColumn 을 이용해서 타입 구분용으로 사용할 컬럼 지정 
    @Entity
    @Inheritance(strategy = InheritanceType.SINGLE_TABLE)
    @DiscriminatorColumn(name = "image_type")
    @Table(name = "image")
    public abstract class Image {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "image_id")
        private Long id;
        
        @Column(name = "image_path")
        private String path;
        
        @Temporal(TemporalType.TIMESTAMP)
        @Column(name = "upload_time")
        private Date uploadTime;
        
        protected Image() {}
        
        public Image(String path) {
            this.path = path;
            this.uploadTime = new Date();
        }
        
        protected String getPath() {
            return path;
        }
        
        public Date getUploadTime() {
            return uploadTime;
        }
        
        public abstract String getURL();
        public abstract boolean hasThumbnail();
        public abstract String getThumbnailURL();
    }
    @Entity
    @DiscriminatorValue("II")
    public class InternalImage extends Image{
        
    }
    @Entity
    @DiscriminatorValue("EI")
    public class ExternalImage extends Image{
    
    }
    • Image를 상속받는 클래스는 @Entity와 @Discriminator를 사용해서 매핑 설정 

     

    @Entity
    @Table(name = "product")
    public class Product {
        @EmbeddedId
        private ProductId id;
        private String name;
    
        @Convert(converter = MoneyConverter.class)
        private Money price;
        private String detail;
    
        @OneToMany(
                cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, orphanRemoval = true)
        @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);
        }
    }
    • Image를 목록으로 담고 있는 Product는 @OneToMany를 통해 매핑
    • Image는 완전히 Product에 의존하기 때문에 cascade속성으로 삭제시 함께 삭제되도록 설정 
    • clear()는 삭제 과정에 성능이 좋지 않음 (image_id를 하나씩 조회해서 삭제함)
    • delete쿼리로 삭제가 가능한데 이걸 사용하려면 상속을 포기하고 @Embeddable로 매핑된 단일 클래스로 구현해야함 
    @Embeddable
    public  class Image {
    
        @Column(name = "image_type")
        private String imageType;
        @Column(name = "image_path")
        private String path;
    
        @Temporal(TemporalType.TIMESTAMP)
        @Column(name = "upload_time")
        private Date uploadTime;
    
        public boolean hasThumbnail() {
            // 성능을 위해 다형 포기하고 if-else로 구현
            if(imageType.equals("II")) {
                return true;
            } else {
                return false;
            }
        }
    }
    • 코드 유지보수와 성능 두 가지 측면 고려해서 구현방식을 선택해야함 

     

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

    • 애그리거트 집합 연관은 성능상 이유로 피해야함
    • 집합 연관이 유리할 경우 ID참조를 이용한 단방향 집합 연관 적용 
    @Entity
    @Table(name = "product")
    public class Product {
        @EmbeddedId
        private ProductId id;
    
        @ElementCollection
        @CollectionTable(name = "product_category",
            joinColumns = @JoinColumn(name = "product_id"))
        private Set<CategoryId> categoryIds;
    
    • Product에서 Category로의 단방향 M-N 연관을 ID참조 방식으로 구현함 
    • 집합의 값에 밸류 대신 연관 맺는 식별자가 옴 
    • @ElementCollection을 이용하기 때문에 Product삭제시 매핑에 사용한 조인테이블 데이터도 삭제됨 

    애그리거트 로딩 전략

    • JPA 매핑 설정시 애그리거트에 속한 객체가 모두 모여야 하나가 된다는 것이 중요 
    • 조회시점에 애그리거트가 완전한 상태가 되려면 조회방식을 즉시로딩(FetchType.EAGER)으로 설정 
    • 연관된 구성요소를 DB에서 함께 읽어옴 
    //@Entity컬렉션에 대한 즉시 로딩 설정
    @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<>();
    
    //@Embeddable컬렉션에 대한 즉시 로딩 설정
    @ElementCollection(fetch = FetchType.EAGER)
    @CollectionTable(name = "order_line", joinColumns = @JoinColumn(name="order_number"))
    @OrderColumn(name = "line_idx")
    private List<OrderLine> orderLines;
    • 로딩시점에 애그리거트에 속한 모든 객체를 함께 로딩 
    • 항상 좋은 것은 아님 
    @Entity
    @Table(name = "product")
    public class Product {
    
    
        @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<>();
    
        @ElementCollection(fetch = FetchType.EAGER)
        @CollectionTable(name = "product_option", joinColumns = @JoinColumn(name = "product_id"))
        @OrderColumn(name = "line_idx")
        private List<Option> options = new ArrayList<>();
    
    • 위의 경우 Product 테이블 조회 시 Image, Option테이블을 조인한 쿼리를 실행함
    • 카타시안 조인을 사용 (Product 이미지가 2개고 Option이 2개면 행 개수는 4개가 됨 -> 중복 )
    • 애그리거트는 개념적으로 하나여야 하지만 로딩시점에 모두 로딩해야하는 것은 아님  
    • 실제로 상태 변경시점에 구성요소만 로딩해도 문제가 되지 않음 
      • FetchType.LAZY
    • 일반적인 애플리케이션은 상태변경보다 조회기능 빈도가 훨씬 높음 

    애그리거트 영속성 전파

    • 애그리거트 루트 조회 뿐 아니라 저장, 삭제시에도 하나로 처리해야함 
    • @Embeddable 매핑 타입은 함께 저장, 삭제 되므로 cascade속성 설정 필요X
    • @Entity타입에 대한 매핑은 cascade 사용해서 처리하도록 설정해야함 
    • @OneToOne, @OneToMany는 cascade속성의 기본값이 없음 
    @OneToMany(cascade = {CascadeType.PERSIST, CascadeType.REMOVE},
            orphanRemoval = true)
    @JoinColumn(name = "product_id")
    @OrderColumn(name = "list_idx")
    private List<Image> images = new ArrayList<>();

    식별자 생성 기능

    • 식별자 생성 방식
      • 사용자가 직접 생성
      • 도메인 로직으로 생성
      • DB를 이용한 일련번호 사용 
    • 식별자 생성 규칙이 있으면 별도 서비스로 식별자 생성 기능 분리해야함 
    public class ProductIdService {
    	public ProductId nextId() {
        	// 정해진 규칙으로 식별자 생성
        }
    }
    • 식별자 생성 기능은 도메인 영역에 위치해야함

     

    public class CreateProductService {
    	@Autowired private ProductIdService idService;
        @Autowired private ProductRepository productRepository;
        
        @Transactional
        public ProductId createProduct(ProductCreationCommand cmd) {
        	// 응용 서비스는 도메인 서비스를 이용해서 식별자 생성
            ProductId id = productIdService.nextId();
            Product product = new Product(id, cmd.getDetail(), cmd.getPrice(), ...);
            productRepository.save(product);
            return id;
        }
    }
    • 응용서비스는 도메인 서비스를 이용해서 식별자 구하고 엔티티 생성 

     

    public class OrderIdService {
    	public OrderId createId(UserId userId) {
        	if(userId == null)
            	throw new IllegalArgumentException("invalid userId:" + userId);
            return new OrderId(userId.toString() + "-" + timestamp());
        }
        
        private String timestamp() {
        	return Long.toString(System.currentTimeMillis());
        }
    }
    • 특졍 값의 조합으로 식별자 생성하는 것 역시 도메인 서비스를 이용해서 생성할 수 있음 

     

    public interface ProductRepository {
    	...// save() ...
        // 식별자 생성 메서드
        ProductId nextId();
    }
    • 리포지터리에 구현할 수도 있다.

    • @GeneratedValue 를 이용해서 DB자동 증가 컬럼을 식별자 매핑으로 사용할 수도 있음 
    • 자동증가 컬럼은 insert뒤에 식별자가 생성되므로 리포지터리에 저장할 때 식별자가 생성된다.
    • 객체 생성시점에는 식별자를 알 수 없음(저장 후에 알 수 있음)

    도메인 구현과 DIP

    • DIP에 따르면 @Entity, @Table은 구현기술이기 때문에 Article과 같은 도메인 모델은 구현기술인 JPA에 의존하지 말아야하는데 의존하고 있음
    • 리포지터리 인터페이스도 JPA Repository인터페이스를 상속하므로 도메인이 인프라에 의존함
    • 구현 기술에 대한 의존 없이 도메인을 순수하게 유지하려면 Spring Date JPA Repository 인터페이스 상속받지 않고 ArticleRepository 인터페이스를 구현 클래스인프라에 위치시켜야함 

    • DIP 적용 이유는 저수준 구현이 변경되더라고 고수준 영향을 받지 않도록 하기 위함 
    • 리포지터리와 도메인 모델 구현기술은 거의 바뀌지 않음 
    • DIP를 완벽하게 지키면 좋지만 개발 편의성과 실용성을 가져가면서 구조적인 유연함은 어느정도 필요함 
    • 복잡도를 높이지 않으면서 기술에 따른 구현제약이 낮으면 합리적인 선택인듯

     

     

    728x90
    반응형