효습
4장 리포지터리 모델 구현 본문
4.1 JPA 를 이용한 리포지터리 구현
애그리거트를 어떤 저장소에 저장하느냐에 따라 리포지터리를 구현하는 방법이 다르다. 데이터 보관소로 RDBMS를 사용할 때, 객체 기반의 도메인 모델과 관계형 데이터 모델 간의 매핑을 처리하는 기술로 ORM 만한 것이 없다.
4.1.1 모듈 위치
리포지터리 인터페이스는 애그리거트와 같이 도메인 영역에 속하고, 리포지터리를 구현한 클래스는 인프라스트럭처에 속한다.
- 팀 표준에 따라 리포지터리 구현 클래스를 domain.impl과 같은 패키지에 위치시킬 수도 있는데 이것은 리포지터리 인터페이스와 구현체를 분리하기 위한 타협안 같은 것이지 좋은 설계 원칙을 따르는 것은 아니다.
- 가능하면 리포지터리 구현 클래스를 인프라스트럭처 영역에 위치시켜서 인프라스트럭처에 대한 의존을 낮춰야한다.
4.1.2 리포지터리 기본 기능 구현
리포지터리가 제공하는 기본 기능은 다음 두 가지다
- ID로 애그리거트 조회하기
- 애그리거트 저장하기
인터페이스는 애그리거트 루트를 기준으로 작성한다.
애그리거트를 수정한 결과를 저장소에 반영하는 메서드를 추가할 필요는 없다. JPA를 사용하면 트랜잭션 범위에서 변경한 테이터를 자동으로 DB에 반영하기 때문이다.
애그리거트를 삭제하는 기능이 필요할 수도 있다. 삭제기능을 위한 메서드는 삭제할 애그리거트의 객체를 파라미터로 전달받는다.
- 삭제 요구사항이 있다고 해서 실제로 삭제를 하는 경우는 많지 않다.
4.2 스프링데이터 JPA를 이용한 리포지터리 구현
스프링과 JPA를 함께 적용할 때는 스프링 데이터 JPA를 사용한다. 스프링 데이터JPA는 지정한 규칙에 맞게 리포지터리 인터페이스를 정의하면 리포지터리 구현한 객체를 알아서 스프링 빈에 등록해준다. → 개발자는 리포지터리 인터페이스를 직접 구현하지 않아도 됨
4.3 매핑 구현
4.3.1 엔티티와 밸류 기본 매핑 구현
애그리거트와 JPA 매핑을 위한 기본 규칙은 다음과 같다.
- 애그리거트 루트는 엔티티이므로
@Entity
로 매핑 설정한다. - 한 테이블에 엔티티와 밸류 데이터가 같이 있다면
- 밸류는
@Embeddable
로 매핑 설정한다. - 밸류 타입 프로퍼티는
@Embeddable
로 매핑 설정한다.
- 밸류는
- 주문 애그리거트의 루트 엔티티인 Order는
@Entity
로 매핑 - Orderer는 밸류이므로
@Embeddable
로 매핑- 설정한 칼럼 이름과 실제 칼럼 이름이 다르다면
@AttributeOverrides
애너테이션을 이용한다.
- 설정한 칼럼 이름과 실제 칼럼 이름이 다르다면
- 루트 엔티티인 Order 클래스는
@Embedded
를 이용해서 밸류 타입 프로퍼티를 설정한다.
4.3.2 기본 생성자
- 엔티티와 밸류의 생성자는 객체를 생성할 때 필요한 것을 전달받는다.
- 만약 밸류 타입이 불변 타입이라면 set 메서드가 필요하지 않으나 JPA에서
@Entity
나@Embeddable
로 클래스를 매핑하려면 기본 생성자를 제공해야한다.- DB에서 데이터를 읽어와 매필된 객체를 생성할 때 기본 생성자를 사용해서 객체를 생성하기 때문이다.
- 기본 생성자는 JPA 프로바이더가 객체를 생성할 때만 사용한다.
- 기본 생성자를 다른 코드에서 사용하면 값이 불안정하기때문에 protected로 선언한다.
4.3.3 필드 접근 방식 사용
JPA는 필드와 메서드의 두 가지 방식으로 매핑을 처리할 수 있다.
메서드 방식
- 엔티티에 프로퍼티를 위한 공개 get/set 메서드를 추가하면 도메인의 의도가 사라지고 객체가 아닌 데이터 기반으로 엔티티를 구현할 가능성이 높아진다. (특히 set 메서드)
- 엔티티가 객체로서 제 역할을 하려면 외부에 set 메서드 대신 의도가 잘 드러나는 기능을 제공해야한다
- 예) cancel()메서드
- 밸류 타입을 불변으로 구현하면 set메서드 자체가 필요 없는데 JPA의 구현 방식때문에 공개 set 메서드를 추가하는 것은 좋지 않다.
객체가 제공할 기능 중심으로 엔티티를 구현하게끔 유도하려면 JPA 매핑 처리를 프로퍼티 방식이 아닌 필드 방식으로 선택해서 불필요한 get/set 메서드를 구현하지 말아야한다.
4.3.4 AttributeConverter를 이용한 밸류 매핑 처리
밸류 타입의 프로퍼티를 한 개 칼럼에 매핑해야할 때도 있다.
두 개 이상의 프로퍼티를 가진 밸류 타입을 한 개 칼럼에 매핑하려면 @AttributerConverter
이다. 밸류 타입과 칼럼 데이터 간의 변환을 처리할 수 있다.
package jakarta.persistence;
public interface AttributeConverter<X,Y> {
public Y convertToDatabaseColumn (X attribute);
public X convertToEntityAttribute (Y dbData);
}
- 타입 파라미터 X는 밸류 타입이고 Y는 DB 타입이다.
- convertToDatabaseColumn() 메서드는 밸류 타입을 DB 칼럼 값으로 변환하는 기능을 구현
- convertToEntityAttribute() 메서드는 DB 칼럼 값을 밸류로 변환하는 기능을 구현
4.3.5 밸류 컬렉션: 별도 테이블 매핑
- 밸류 컬렉션을 별도 테이블로 매핑할 때는
@ElementCollection
과@CollectionTable
을 함께 사용한다. - List 타입의 컬렉션은 따로 인덱스를 지정할 필요가 없다. List 타입 자체가 인덱스를 갖고 있기 때문이다.
- JPA는
@OrderColumn
애너테이션을 이용해서 지정한 칼럼에 리스트의 인덱스 값을 저장한다.
- JPA는
@CollectionTable
은 밸류를 저장할 테이블을 지정한다.- joinColumns 속성은 외부키로 사용할 칼럼을 지정한다.
- 외부키가 두 개 이상인 경우
@JoinColumn
의 배열을 이용해서 외부키 목록을 지정한다.
4.3.6 밸류 컬렉션 : 한 개 칼럼 매핑
밸류 컬렉션을 별도 테이블이 아닌 한 개 칼럼에 저장해야 할 때가 있다
- AttributeConverter를 사용하면 밸류 컬렉션을 한 개 칼럼에 쉽게 매핑할 수 있다.
- AttributeConverter를 사용하려면 밸류 컬렉션을 표현하는 새로운 밸류 타입을 추가해줘야한다.
4.3.7 밸류를 이용한 ID 매핑
- 식별자라는 의미를 부각시키기 위해 식별자 자체를 밸류 타입으로 만들 수 있다.
- 밸류 타입을 식별자로 매핑하면
@Id
대신@EmbeddedId
애너테이션을 사용해야한다. - JPA에서 식별자 타입은 Serializable 타입이어야 하므로 식별자로 사용할 밸류 타입은 Serializable 인터페이스를 상속받아야한다.
4.3.8 별도 테이블에 저장하는 밸류 매핑
애그리거트에서 루트 엔티티를 뺀 나머지 구성요소는 대부분 밸류이다.
- 루트 엔티티 외에 또 다른 엔티티가 있다면 진짜 엔티티인지 의심해봐야한다.
- 밸류가 아니라 엔티티가 확실하다면 해당 엔티티가 다른 애그리거트가 아닌지 확인해야 한다.
- 자신만의 독자적인 라이프 사이클을 갖는다면 구분되는 애그리거트일 가능성이 높다.
- 애그리거트에 속한 객체가 밸류인지 엔티티인지 구분하는 방법은 고유 식별자를 갖는지를 확인하는 것이다.
- 식별자를 찾을 때 매핑되는 테이블의 식별자는 애그리거트 구성요소의 식별자와 동일한 것으로 착각해서는 안된다.
- 별도의 테이블로 저장하고 테이블에 PK가 있다고 해서 테이블과 매핑되는 애그리거트 구성요소가 항상 고유 식별자는 갖는 것은 아니기 때문이다.
4.3.9 밸류 컬렉션을 @Entity로 매핑하기
JPA는 @Embeddable
타입의 클래스 상속 매핑을 지원하지 않는다.
상속 구조를 갖는 밸류 타입을 사용하려면 @Embeddable
대신 @Entity
를 이용해서 상속 매핑으로 처리해야 한다.
밸류 타입을 @Entity
로 매핑하므로 식별자 매핑을 위한 필드도 추가해야한다.
구현 클래스를 구분하기 위한 타입 식별(discriminator) 칼럼을 추가해야 한다.
4.3.10 ID 참조와 조인 테이블을 이용한 단반향 M-N 매핑
애그리거트 간 집합 연관은 성능 상의 이유로 피해야한다. → 하지만 요구사항을 구현하는 데 집합 연관을 사용하는 것이 유리하다면 ID 참조를 이용한 단방향 집합 연관을 적용해볼 수 있다.
4.4 애그리거트 로딩 전략
JPA 매핑을 설정할 때 항상 기억해야 할 점은 애그리거트에 속한 객체가 모두 모여야 완전한 하나가 된다는 것이다.
조회 시점에서 애그리거트를 완전한 상태가 되도록 하려면 애그리거트 루트에서 연관 매핑의 조회 방식을 즉시 로딩으로 설정하면 된다. ← 항상 좋은 건 아님 , 불필요한 쿼리가 너무 많아진다.
애그리거트는 개념적으로 하나여야 한다. 하지만 루트 엔티티를 로딩하는 시점에 애그리거트에 속한 객체를 모두 로딩해야하는 건 아니다.
애그리거트가 완전해야하는 이유는
- 상태를 변경하는 기능을 실행할 때 애그리거트 상태가 완전해야 한다.
- 표현 영역에서 애그리거트의 상태 정보를 보여줄 때 필요하다.
- 이를 위해 별도의 조회 전용 기능과 모델을 구현하는 방식이 유리함 → 애그리거트의 완전한 로딩은 상태 변경과 관련이 높음
일반적인 애플리케이션에서 상태 변경 기능을 실행하는 횟수보다 조회 기능을 실행하는 횟수가 훨씬 높음
→ 상태 변경을 위해 지연 로딩을 사용할 때 발생하는 추가 쿼리로 인한 실행 속도 저하는 보통 문제 되지 않음
이런 이유로 애그리거트 내의 모든 연관을 즉시 로딩으로 설정할 필요는 없다.
4.5 애그리거트의 영속성 전파
애그리거트가 완전한 상태여야한다는 것은 애그리거트 루트를 조회할 때뿐만 아니라 저장하고 삭제할 때도 하나로 처리해야 함을 의미한다.
- 저장 메서드는 애그리거트 루트만 저장하면 안 되고 애그리거트에 속한 모든 객체를 저장해야한다.
- 삭제 매서드는 애그리거트 루트뿐만 아니라 애그리거트에 속한 모든 객체를 삭제해야한다.
@Embeddable
타입의 경우 함께 저장되고 삭제되므로 cascade 속성을 추가로 설정하지 않아도 됨@Entity
타입의 경우는 cascade 속성을 추가로 설정해야 함
4.6 식별자 생성 기능
식별자는 크게 세 가지 방식 중 하나로 생성된다.
- 사용자가 직접 생성
- 도메인 로직으로 생성
- DB를 이용한 일련번호 사용
4.7 도메인 구현과 DIP
4장에서 구현한 코드들은 구현 기술인 JPA에 특화된 애너테이션을 사용하고 있음 , 뿐만 아니라 리포지터리 인터페이스도 구현 기술인 JPA의 Repository 인터페이스를 상속하며 인프라에 의존하고 있다.
DIP를 적용하는 주된 이유는 저수준 구현이 변경되더라도 고수준이 영향을 받지 않도록 하기 위함이다.
→ 하지만 리포지토리와 도메인 모델의 구현 기술은 거의 바뀌지 않는다. DIP를 완벽하게 지키는 게 좋긴 하겠지만 개발 편의성과 실용성을 가져가면서 구조적인 유연함은 어느 정도 유지하는 게 합리적인 선택이다.
'책 > 도메인 주도 개발 시작하기' 카테고리의 다른 글
6장 응용 서비스와 표현 영역 (0) | 2024.03.25 |
---|---|
5장 스프링 데이터 JPA를 이용한 조회 기능 (1) | 2024.03.25 |
3장 애그리거트 (0) | 2024.03.18 |
2장 아키텍처 개요 (0) | 2024.03.11 |
1장 도메인 모델 시작하기 (0) | 2024.03.11 |