4.1 JPA를 이용한 리포지터리 구현
4.1.1 모듈 위치
- 리포지터리 인터페이스는 애그리거트와 같이 도메인 영역에 위치
- 기술로 리포지터리를 구현한 클래스는 인프라스트럭처 영역에 속한다.
4.1.2 리포지터리 기본 기능 구현
- E findById(ID id);
- E save(E entity);
4.2 스프링 데이터 JPA를 이용한 리포지터리 구현
@Entity public class Order { @Id @GeneratedValue private Long id; } public class OrderRepository extends JpaRepository<Order, Long>{ }
스프링 데이터 JPA는 OrderRepository를 리포지터리로 인식해서 알맞게 구현한 객체를 스프링 빈으로 등록한다.
4.3 매핑 구현
4.3.1 엔티티와 밸류 기본 매핑 구현
- 애그리거트 루트
-
@Entity
- 밸류
-
@Embeddable
- 다른 엔티티 혹은 밸류에서 프로퍼티로 사용될 때는
@Embedded
@Embedded
사용시 대상 밸류의 내부 프로퍼티와 다른 column name을 가지게 하고 싶다면@AttributeOverride
애너테이션을 사용한다.
4.3.2 기본 생성자
- JPA에서 @Entity와 @Embeddable로 클래스를 매핑하려면
protected
/public
기본 생성자를 제공해야 한다.
4.3.3 필드 접근 방식 사용
- JPA 프로바이더가 객체 생성을 위해 기본 생성자를 호출 한뒤 매핑을 처리하는 방식에는 두가지 방식이 있다.
@Access(AccessType.FIELD)
필드 방식@Access(AccessType.PROPERTY)
메서드 방식 (getter/setter)- 게터 세터 필요
- 필드 접근 방식을 사용하자
- 세터를 요구한다는 점이 설계에 치명적이다.
- 기본적으로
@Id
/@EmbeddedId
의 위치에 따라 매핑 처리 방식을 자동으로 결정한다.
4.3.4 AttributeConberter를 이용한 밸류 매핑 처리
@Converter(autoApply = true) public class OpenTypeConverter implements AttributeConverter<OpenType, Byte> { @Override public Byte convertToDatabaseColumn(final OpenType attribute) { return attribute.getCode(); } @Override public OpenType convertToEntityAttribute(final Byte dbData) { return Stream.of(OpenType.values()) .filter(ot -> ot.getCode() == dbData) .findFirst() .orElseThrow(IllegalStateException::new); } }
4.3.5 밸류 컬렉션: 별도 테이블 매핑
@ElementCollection
+@CollectionTable
을 함께 사용한다.- 대상 컬렉션을 List로 선언하면 하이버네이트는 순서를 표현하기 위한 index column을 자동으로 생성해준다.
4.3.6 밸류 컬렉션: 한개 칼럼 매핑
AttributeConvert를 이용해 한개의 칼럼에 매핑할 수 도 있다.
4.3.7 밸류를 이용한 ID 매핑
- 식별자라는 의미를 부각
- 식별자에 로직을 넣을 수 있다.
4.3.8 별도 테이블에 저장하는 밸류 매핑
- 애그리거트에서 루트 엔티티를 뺀 나머지 요소는 대부분 밸류이다.
4.3.9 밸류 컬렉션을 @Entity로 매핑하기
- @Embedabble에는 상속을 적용할 수 없다.
- @Entity와 @Embeddable은 컬렉션의 clear() 메서드 호출시 발생하는 쿼리가 다르다.
- 코드 유지 보수와 성능 두 가지 측면을 잘 고려해야한다.
4.3.10 ID 참조와 조인 테이블을 이용한 단방향 M-N 매핑
3장에서 애그리거트 간 집합 연관 (toMany)은 성능 상의 이유로 피해야 한다고 했다. toMany를 정말 양방향으로 설정하고 싶다면 Id 집합을 조인 테이블 거는것을 고려하자
이때 사용할 수 있는 JPA의 구현 방식이
@ElementCollection
과 @CollectionTable
이다.@Entity public class Bookmark extends BaseIdEntity { private String link; @Embedded private TagIds tagIds; } @Embeddable public class TagIds{ @ElementCollection @CollectionTable( name = "bookmark_tag", joinColumns = @JoinColumn(name = "bookmark_id"), uniqueConstraints = @UniqueConstraint(columnNames = {"bookmark_id", "tag_id"}) ) @Column(name = "tag_id") private Set<Long> values = new HashSet<>(); }
생성 테이블 구조
id | link |
1 | www.naver.com |
bookmark_id | tag_id |
1 | 2 |
1 | 3 |
id 참조를 이용해 북마크 도메인에서는 태그 도메인에 대한 참조를 끊을 수 있다.
북마크 조회시 태그를 함께 조회해야 하는 경우 tag_id 까지만 조회하고 tag_id를 tag로 변환하는 책임은 태그 도메인에 위임 할 수 있다.
4.4 애그리거트 로딩 전략
애그리거트에 속한 객체가 모두 모여야 완전한 애그리거트 루트 하나를 이룰 수 있다. 즉 findById를 이용해 조회한 애그리거트 루트는 완전한 상태여야 한다.
Bookmark bookmark = bookmarkRepository.findById(id);
개발자는 조회한 북마크로 내부의 태그 id 집합 과 같은 모든 객체를 참조할 수 있을 것을 기대한다.
이를 위해 조회 방식을
EAGER
로 설정 할 수 있다. EAGER 로딩을 통해 루트 엔티티를 조회하면 하이버네이트는 연관된 데이터 조회를 위해 join 쿼리를 발생시킨다. 한 쿼리에서 join 문이 여러개 걸쳐지는 경우 애플리케이션에 로딩되는 메모리가 필요 이상으로 과다해 질 수 있다.조회 방식을
LAZY
로 설정하는 경우 한 트랜잭션 (요청)에 대해서 쿼리 실행 횟수가 많아질 가능성이 더 높다. 일장일단이 있으므로 애그리거트 및 발생 쿼리를 고려하여 로딩 전략을 선택하여야 한다.
4.5 애그리거트의 영속성 전파
위에서 언급한 애그리거트 루트의 조회 뿐만 아니라 저장/삭제에 따른 영속성 처리 역시 애그리거트의 내부에서는 한 덩어리로 이루어 져야 한다.
@Embeddable
매핑은 함께 저장되고 삭제되므로 cascade 속성을 추가로 설정하지 않아도 된다. 반면 애그리거트에 속한 @Entity
타입에 대한 매핑은 cascade 속성을 사용해 저장,삭제 시에 함께 처리되도록 설정해야한다.4.6 식별자 생성 기능
- 사용자가 직접 생성 (e.g. 이메일)
- 도메인 로직으로 생성 (e.g. timestamp + uuid)
- 도메인서비스/리포지터리에 규칙을 위치 시킴
- db seqence (e.g. oracle sequence/ mysql auto increment)
4.7 도메인 구현과 DIP
- 도메인 구현의 편의를 위해 DIP를 위반할 수 있다.
- jpa에 대한 의존, spring에 대한 의존을 도메인에 남기면 개발 속도를 크게 높일 수 있다.
- 저울질을 잘 해서 의식적으로 선택하자