- 레포지터리 인터페이스는 애그리거트와 같이 도메인 영역에 속하고, 레포지터리를 구현한 클래스는 인프라스트럭처 영역에 속한다.
레포지터리 구현 클래스를 domain.impl
과 같은 패키지에 위치시킬 수 있지만 인터페이스와 구현체를 분리하기 위한 타협안 같은 것일 뿐 좋은 설계 원칙은 아니다. 가능하면 레포지터리 구현 클래스를 인프라스트럭처 영역에 위치 시켜 인프라스트럭처에 대한 의존을 낮춰야 된다.
interface OrderRepository {
fun findById(no: OrderNo): Order
fun save(order: Order)
}
Value
와 Root Entity
를 포함하고 있기 때문이다.findBy프로퍼티이름(프로퍼티 값)
class ChangeOrderService(private val orderRepository: OrderRepository) {
@Transactional
fun changeShippingInfo(no: OrderNo, newShippingInfo: ShippingInfo) {
val order = orderRepository.findById(no).orElseThrow {throw NotFoundException()}
order.changeShippingInfo(newShippingInfo)
}
}
위 코드는 스프링 프레임워크의 트랜잭션 관리 기능을 통해 트랝개션 범위에서 실행이 된다. 메서드 실행이 끝나면 트랜잭션을 커밋하는데 이때 JPA는 트랜잭션 범위에서 변경된 객체의 데이터를 DB에 반영하기 위헤 UPDATE 쿼리를 실행한다.
스트링 데이터 JPA는 지정한 규칙에 맞게 레포지터리 인터페이스를 정의하면 레포지터리를 구현한 객체를 알아서 만들어 스프링 빈으로 등록해준다.
@Entity
@Table(name = "purchase_order")
@Access(AccessType.FIELD)
class Order(
private val number: OrderNo,
) {
// order의 식별자
var number: OrderNo = number
private set
}
위와 같이 구현이 되면 레포지터리에서 아래와 같이 구현이 가능하다.
@Repository
interface OrderRepository : JpaRepository<Order, String> {
fun findByOrderNumber(id: OrderNo): Order?
}
fun save(entity: Order): Order
// or
fun save(entity: Order)
fun findById(id: OrderNo): Order
// or
fun findById(id: OrderNo): Order?
fun findByOrderer(orderer: Orderer): List<Order>
fun findByOrdererMemberId(memberId: MemberId): List<Order>
fun delete(order: Order)
// or
fun deleteById(id: OrderNo)
@Entity
로 매핑 설정한다.Entity
와 Value
가 같이 있다면 Value
는 @Embeddable
로 Value
타입 프로퍼티는 @Embedded
로 매핑 설정한다.@AttributeOverrides
애너테이션을 이용한다.@Entity
와 @Embeddable
로 클래스를 매핑하려면 실제로는 아무런 파라미터를 받지 않는 기본 생성자를 추가해야된다.Entity
를 구현할 가능성이 높아진다. set 메서드는 내부 데이터를 외부에서 변경할 수 있는 수단이 되기 때문에 캡슐화를 깨는 원인이 될 수 있다.Entity
가 객체로서 역할을 하기 위해선 외부에서 set 메서드 대신 의도가 잘 드러나는 기능을 제공해야 된다.Value
타입을 불변으로 구현하려면 set 메서드 자체가 필요 없으며, JPA의 구현 방식 때문에 공개 set 메서드를 추가하는 것도 좋지 않다.객체가 제공할 기능 중심으로 Entity
를 구현하게끔 유도하려면 JPA 매핑 처리를 프로퍼티 방식이 아닌 필드 방식으로 선택해서 불필요한 get/set 메서드를 구현하지 말아야한다.
Value
타입의 프로퍼티를 한 개 컬럼에 매핑해야될 때 사용한다.- 두 개 이상의 프로퍼티를 가진
Value
타입을 한 개 컬럼에 매핑하려면@Embeddable
애너테이션으로는 처리가 불가하다.AttributeConverter
는Value
타입과Column
데이터 간의 변환을 처리하기 위한 기능을 정의한다.
AttributeConverter
인터페이스를 구현한 클래스는 @Converter
애너테이션을 적용한다.
autoApply
속성값을 true
로 지정하면 모델에 출현하는 모든 타입의 프로퍼티에 대해 converter를 자동으로 적용한다. DB에 매핑할 때도 converter를 지정하여 사용한다.
autoApply
를 false
로 지정하면 프로퍼티 값을 변환할 때 사용할 컨버터를 직접 지정해야 한다.
Root Entity
는 하나의Value Collection
과 매핑이 가능하다.
Value Collection
을 저장하는 테이블은 외부키를 이용해서 Entity
에 해당하는 테이블을 참조한다. 이 외부키는 컬렉션이 속할 Entity
를 의미한다. List 타입의 컬렉션은 인덱스 값이 필요하므로 Value Collection
테이블에는 인덱스 값을 저장하기 위한 컬럼도 존재한다.
Value Collection
을 별도 테이블로 매핑할 때는 @ElementCollection
과 @CollectionTable
을 함께 사용한다.
class Order (
) {
@EmbeddedId
var number: OrderNo = number
private set
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "order_line", joinColumns = @JoinColumn(name = "order_number"))
@OrderColumn(name = "line_idx")
var orderLines: List<OrderLine>
private set
}
OrderColumn
애너테이션을 이용해서 지정한 컬럼에 리스트의 인덱스 값을 저장하기 때문에 OrderLine
에는 List의 인덱스 값을 저장하기 위한 프로퍼티가 존재하지 않는다.
@CollectionTable
은 Value
를 저장할 테이블을 지정한다. name
속성은 테이블 이름을 지정하고 joinColumns
속성은 외부키로 사용할 칼럼을 지정한다.
Value Collection
을 별도 테이블이 아닌 한 개 컬럼에 저장해야 할 떄 사용한다.AttributeConverter
를 사용하면Value Collection
에 쉽게 매핑이 되지만,AttributeConverter
를 사용하려면Value Collection
을 표현하는 새로운Value
타입을 추가해야된다.
- 식별자라는 의미를 부각시키기 위해 식별자 자체를
Value
타입으로 만들 수 있다.Value
타입을 식별자로 매핑하면@EmbeddedId
애너테이션을 사용한다.
Serializable
타입이어야 하므로 식별자로 사용할 Value
타입은 Serializable
인터페이스를 상속 받아야 한다.@Embeddable
class OrderNo(private val orderNumber: String): Serializable {
@Column(name = "order_number")
var number: String = orderNumber
private set
}
- 애그리거트에서
Root Entity
를 제외한 나머지 구성요소는 대부분Value
이다.Root Entity
외에 또 다른Entity
가 있다면 진짜Entity
인지 의심해야 된다.
Root Entity
가 아닌 다른 Entity
가 있다면 다른 애그리거트는 아닌지 확인해야된다. 독자적인 라이프 사이클을 갖는다면 구분되는 애그리거트일 가능성이 높다.Value
인지 Entity
인지 구분하는 방법은 고유 식별자를 갖는지 확인하는 것이다. 별도 테이블로 저장하고 테이블에 PK가 있다고 해서 테이블과 매핑되는 애그리거트 구성요소가 항상 고유 식별자를 갖는 것은 아니기 때문이다.
- 개념적으로는
Value
이지만 구현 기술의 한계나 팀 표준 때문에@Entity
를 사용해야 할 때 사용한다.
@Embeddable
타입의 클래스 상속 매핑을 지원하지 않으므로 상속 구조를 갖는 Value
타입을 사용하려면 @Embeddable
대신 @Entity
를 이용해서 상속 매핑으로 처리해야 한다.@Inheritance
애너테이션 적용strategy
값으로 SINGLE_TABLE
사용@DiscriminatorColumn
애너테이션을 이용하여 타입 구분용으로 사용할 컬럼 지정
- 집합 연관을 피해야되지만 사용하는 것이 유리하다면 ID 참조를 이용한 단방향 집합 연관을 적용해 볼 수 있다.
@ElementCollection
을 이용하기 때문에 Root Entity
를 삭제할 때 매핑에 사용한 조인 테이블의 데이터도 함께 삭제된다.
- JPA 매핑을 설정할 때 기억해야 할 점은 애그리거트에 속한 객체가 모두 모여야 완전한 하나가 된다.
- 애그리거트 루트를 로딩하면 루트에 속한 모든 객체가 완전한 상태여야된다.
FetchType.EAGER
)을 즉시 로딩으로 설정하면 된다.FetchType.EAGER
로 설정하면 오히려 즉시 로딩 방식이 문제가 된다.
- 저장 메서드는 애그리거트 루트만 저장하면 안되고 애그리거트에 속한 모든 객체를 저장해야된다.
- 삭제 메서드는 애그리거트 루트뿐만 아니라 래그리거트에 속한 모든 객체를 삭제해야 한다.
@Embeddable
매핑 타입은 함께 저장되고 삭제되므로 cascade 속성을 추가로 설정하지 않아도 된다.
- 사용자가 직접 생성
- 도메인 로직으로 생성
- DB를 이용한 일련번호 사용
Entity
를 생성할 때 식별자를 Entity
가 별도 서비스로 식별자 생성 기능을 분리해야 한다.Entity
를 생성한다.@GeneratedValue
를 사용해서 DB 자동 증가 컬럼을 식별자로 사용한다.
- JPA에 특화된 애너테이션을 사용함으로써 DIP 원칙을 어기고 있다.