도메인 주도 개발 시작하기(4)
4-1. JPA를 이용한 리포지터리 구현
4-1-1. 모듈 위치
•
리포지터리 인터페이스는 애그리거트와 같이 도메인 영역에 속하고, 리포지터리를 구현한 클래스는 인프라스트럭처 영역에 속한다
◦
가능하면 리포지터리 구현 클래스를 인프라스트럭처 영역에 위치시켜서 인프라스크럭처에 대한 의존을 낮춰야 한다
4-1-2. 리포지터리 기본 기능 구현
삭제 기능
삭제 요구사항이 있더라도 실제로 삭제하는 경우는 많지 않다.
사용자가 삭제 기능을 실행할 때 데이터를 바로 삭제하기보다는 삭제 플래그를 사용해서 데이터를 화면에 보여줄지 여부를 결정하는 방식으로 구현한다
•
리포지터리가 제공하는 기본 기능
1.
ID 로 애그리거트 조회하기
2.
애그리거트 저장하기
인터페이스
•
인터페이스는 애그리거트 루트를 기준으로 작성한다
4-2. 스프링 데이터 JPA를 이용한 리포지터리 구현
•
스프링 데이터 JPA 는 지정한 규칙에 맞게 리포지터리 인터페이스를 정의하면 리포지터리를 구현한 객체를 알아서 만들어 스프링 빈(Bean) 으로 등록해준다
•
스프링 데이터 JPA 규칙
◦
org.springframework.data.repository.Repository<T, ID> 인터페이스 상속
◦
T 는 엔티티 타입을 지정하고 ID 는 식별자 타입을 지정
•
식별자를 이용해서 엔티팉를 조회할 때는 findById 메서드를 사용한다
•
특정 프로퍼티를 이용해서 엔티티를 조회할 때는 findBy{프로퍼티이름} 형식의 메서드를 사용한다
•
엔티티를 삭제하는 메서드는 두 형태를 갖는다
◦
void delete(Order order)
◦
void deleteById(OrderNo id)
4-3. 매핑 구현
4-3-1. 엔티티와 밸류 기본 매핑 구현
•
애그리거트와 JPA 매핑을 위한 기본 규칙
◦
애그리거트 루트는 엔티티이므로 @Entity 로 매핑 설정한다
•
한 테이블에 엔티티와 밸류 데이터가 같이 있으면
◦
밸류는 @Embeddable 로 매핑 설정한다
◦
밸류 타입 프로퍼티는 @Embedded 로 매핑 설정한다
4-3-2. 기본 생성자
•
기본 생성자는 JPA 프로바이더가 객체를 생성할 때만 사용한다
•
기본 생성자를 다른 코드에서 사용하면 값이 없는 온전하지 못한 객체를 만들게 된다
◦
다른 코드에서 기본 생성자를 사용하지 못하도록 protected 로 선언한다
4-3-3. 필드 접근 방식 사용
•
엔티티에 프로퍼티를 위한 공개 get/set 메서드를 추가하면 도메인의 의도가 사라지고 객체가 아닌 데이터 기반으로 엔티티를 구현할 가능성이 높아진다
•
set 메서드 대신 의도가 잘 드러나는 기능을 제공해야 한다
4-3-4. AttributeConverter 를 이용한 밸류 매핑 처리
•
AttributeConverter 는 밸류 타입과 컬럼 데이터 간의 변환을 처리하기 위한 기능을 정의한다
•
AttributeConverter 인터페이스를 구현한 클래스는 @Converter Annotation 을 적용한다
4-3-5. 밸류 컬렉션: 별도 테이블 매핑
•
밸류 컬렉션을 별도 테이블로 매핑할 때는 @ElementCollection 과 @CollectionTable 을 함께 사용한다
4-3-6. 밸류 컬렉션: 한 개 컬럼 매핑
•
애플리케이션에서는 Set 타입으로 지정하고, DB 에서는 컬럼 1개에 콤마로 구분해서 저장해야 할 경우 AttributeConverter 를 이용해서 처리한다
•
단, AttributeConverter 를 사용하려면 밸류 컬렉션을 표현하는 새로운 밸류 타입을 추가해야 한다
4-3-7. 밸류를 이용한 ID 매핑
•
식별자라는 의미를 부각시키기 위해 식별자 자체를 밸류 타입으로 만들수도 있다
•
밸류 타입을 식별자로 매핑하면 @Id 대신 @EmbeddedId Annotation 을 사용한다
•
JPA 에서 식별자 타입은 Serializable 타입이어야 하므로 식별자로 사용할 밸류 타입은 Serializable 인터페이스를 상속받아야 한다
4-3-8. 별도 테이블에 저장하는 밸류 매핑
•
애그리거트에서 루트 엔티티를 뺀 나머지 구성요소는 대부분 밸류이다
◦
루트 엔티티 외 또다른 엔티티가 있다면 진짜 엔티티인지 의심해야 한다.
▪
단지 별도 테이블에 저장한다고 엔티티인것은 아니다
◦
밸류가 아니고 엔티티가 확실하다면 해당 엔티티가 다른 애그리거트는 아닌지 확인해야 한다
▪
독자적인 라이프 사이클을 갖는다면 구분되는 애그리거트일 가능성이 높다
•
애그리거트에 속한 객체라 밸류인지 엔티티인지 구분하는 방법은 고유 식별자를 갖는지를 확인하는 것이다
◦
식별자를 찾을 때 매핑되는 테이블의 식별자를 애그리거트 구성요소의 식별자와 동일한 것으로 착각하면 안된다
4-3-9. 밸류 컬렉션을 @Entity 로 매핑하기
•
개념적으로 밸류인데 구현 기술의 한계나 팀 표준 때문에 @Entity 를 사용해야 할 경우도 있다
◦
@Inheritance Annotataion 적용
◦
Strategy 값으로 SINGLE_TABLE 사용
◦
@DiscriminatorColumn Annotation 을 이용하여 타입 구분용으로 사용할 컬럼 지정
4-3-10. ID 참조와 조인 테이블을 이용한 단방향 M-N 매핑
•
애그리거트 간 집합 연관은 성능 상의 이유로 피해야한다.
•
하지만, 요구사항을 구현하는데 집합 연관을 사용하는 것이 유리하다면 ID 참조를 이용한 단방향 집합 연관을 적용할 수 있다.
4-4. 애그리거트 로딩 전략
•
조회 시점에 애그리거트를 완전한 상태가 되도록 하려면 애그리거트 루트에서 연관 매핑의 조회 방식을 즉시 로딩 (FetchType.EAGER) 로 설정하면 된다
◦
즉시 로딩 방식으로 설정하면 애그리거트 루트를 로딩하는 시점에 애그리거트에 속한 모든 객체를 함께 로딩할 수 있다는 장점이 있지만, 항상 좋은것은 아니다
•
보통 조회 성능 문제 때문에 즉시 로딩 방식을 사용하지만, 조회되는 데이터가 많아지면 즉시 로딩 방식을 사용할 때 성능 (실행 빈도, 트래픽, 지연 로딩 시 실행 속도 등)을 검토해봐야 한다
•
애그리거트는 개념적으로 하나여야 한다.
◦
루트 엔티티를 로딩하는 시점에 애그리거트에 속한 객체를 모둗 로딩해야 하는것은 아니다
◦
애그리거트가 완전해야 하는 이유
1.
상태를 변경하는 기능을 실행할 때 애그리거트 상태가 완전해야 하기 때문이다
2.
표현 영역에서 애그리거트의 상태 정보를 보여줄 때 필요하기 때문이다
•
JPA 는 트랜잭션 범위 내에서 지연 로딩을 허용하기 때문에 상태를 변경하는 시점에 필요한 구성요소만 로딩해도 문제가 되지 않는다
•
지연 로딩은 동작하 방식이 항상 동일하기 때문에 즉시 로딩처럼 경우의 수를 따질 필요가 없다는 장점이 있다
◦
즉시 로딩은 @Entity 나 @Embeddable 에 따라 다르게 동작하고, JPA 프로바이더에 따라 구현 방식이 다를 수 있다
•
무조건 즉시 로딩이나 지연 로딩으로만 설정하기보다는 애그리거트에 맞게 즉시 로딩과 지연 로딩을 설정해야 한다
4-5. 애그리거트의 영속성 전파
•
애그리거트가 완전한 상태여야 한다는 것은 애그리거트 루트를 조회할 때 뿐만 아니라 저장하고 삭제할 때도 하나로 처리해야 함을 의미한다.
◦
저장 메서드는 애그리거트 루트만 저장하면 안되고, 애그리거트에 속한 모든 객체를 저장해야 한다
◦
삭제 메서드는 애그리거트 루트뿐만 아니라 애그리거트에 속한 모든 객체를 삭제해야 한다
•
@Embeddable 매핑 타입은 함께 저장되고 삭제되므로 cascasde 속성이 필요없다
•
@Entity 타입에 대한 매핑은 cascade 를 사용해야 한다
◦
CascadeType.PERSIST, CascadeType.REMOVE 를 설정한다
4-6. 식별자 생성 기능
•
식별자는 크게 3가지 방식 중 하나로 생성한다
1.
사용자가 직접 생성
2.
도메인 로직으로 생성
3.
DB 를 이용한 일련번호 사용
•
식별자 생성 규칙은 도메인 규칙이므로 도메인 영역에 식별자 생성 기능을 위치시켜야 한다
•
식별자 생성 규칙을 구현하기에 적합한 또 다른 장소는 리포지터리다
4-7. 도메인 구현과 DIP
•
DIP 를 적용하는 주된 이유는 저수준 구현이 변경되더라도 구수준이 영향을 받지 않도록 하기 위함이다
◦
하지만, 리포지터리와 도메인 모델의 구현 기술은 거의 바뀌지 않는다
◦
애그리거트, 리포티저리 등 도메인 모델을 구현할 때는 과하게 대응할 필요가 없다
•
DIP 를 완벽하게 지키면 좋겠지만, 개발 편의성과 실용성을 가져가면서 구조적인 유연함은 어느정도 유지해야 한다
•
복잡도를 높이지 않으면서 기술에 따른 구현 제약이 낮다면 합리적인 선택이다