티스토리 뷰
JPA를 이용한 리포지터리 구현
애그리게이트를 어떤 저장소에 저장하느냐에 따라 리포지터리를 구현하는 방법이 다릅니다. 해당 파트에서는 자바의 ORM 표준인 JPA를 이용해서 리포지터리와 애그리게이트를 구현합니다.
💡 모듈 위치
리포지터리 인터페이스는 애그리게이트와 함께 도메인 영역에 속하며 리포지터리를 구현한 구현 클래스는 인프라 영역에 속합니다. 팀 표준에 따라 리포지터리 구현 클래스를 domain.impl과 같은 패키지에 위치시킬 수 잇지만 이것은 인터페이스와 구현체를 분리하기 위한 타협안이지 좋은 설계가 아니라고 합니다.
💡 리포지터리의 기본 기능 구현
인터페이스는 애그리게이트 루트를 기준으로 작성하며 애그리게이트를 조회하는 기능의 이름을 지을 때 특별한 규칙은 없지만 보통 findBy(프로퍼티 값) 형식을 사용합니다. 또한 애그리게이트를 수정한 결과를 DB에 반영하는 로직을 추가할 필요가 없습니다. JPA를 사용하면 트랜잭션 범위에서 변경한 데이터를 자동으로 DB에 반영해줍니다.
아래는 리포지터리가 기본으로 제공하는 2가지 기능입니다.
- ID로 애그리게이트 조회
- 애그리게이트 저장
interface OrderRepository {
public Order findById(OrderNo no);
public List<Order> findByOrdererId(String ordererId);
public void save(Order order);
}
매핑 구현
애그리게이트와 JPA 매핑을 위한 기본 규칙은 다음과 같습니다.
- 애그리게이트는 루트 엔티티이므로 @Entity 어노테이션을 사용하여 매핑 설정을 합니다.
- 한 테이블에 엔티티와 밸류 데이터가 존재한다면 밸류는 @Embeddable 어노테이션을 사용하고, 밸류 타입 프로퍼티는 @Embedded 어노테이션을 사용합니다.
@Entity
@Table(name = "purchase_order")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Embedded
private Orderer orderer;
}
@Embeddable
public class Orderer {
@Column(name = "orderer_name")
private String name;
}
💡 @NoArgsConstructor(access = AccessLevel.PROTECTED)의 의미
JPA에서 @Entity와 @Embeddable로 클래스를 매핑하기 위해서는 기본 생성자를 제공해야 합니다. DB에서 데이터를 읽어와 매핑된
객체를 생성할 때 기본 생성자를 사용해서 객체를 생성하기 때문입니다.
또한 기본 생성자는 JPA 프로바이더가 객체를 생성할 때만 사용합니다. 기본 생성자를 다른 코드에서 사용하면 온전하지 못한 객체를 만들게되므로 접근 제한자를 protected로 설정합니다.
💡 밸류 컬렉션 : @ElementCollection
Order 엔티티는 한 개 이상의 OderLine을 가질 수 있습니다.
@ElementCollection 어노테이션은 마치 @OneToMany 어노테이션을 연상케 합니다. 아래는 @ElementCollection 어노테이션의 특징입니다.
- 부모 엔티티에 의해 관리됩니다. 그렇기 때문에 독립적으로 사용할 수 없습니다.
- 항상 부모 엔티티와 함께 저장되고 삭제되므로 cascade 옵션은 제공되지 않습니다. 항상 cascade = ALL 입니다.
- 기본적으로 식별자 개념이 없으므로 컬렉션 값 변경시 전체 삭제 후 새로 추가합니다.
@Entity
@Table(name = "purchase_order")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "order_line", joinColumns = @JoinColumn(name = "order_id"))
@OrderColumn(name = "line_idx")
private List<OrderLine> orderLines = new ArrayList<>();
}
@Embeddable
public class OrderLine {
@Column(name = "price")
private double price;
@Column(name = "quantity")
private int quantity;
@Column(name = "amounts")
private int amounts;
}
💡 밸류 컬렉션 : 한 개 컬럼 매핑
밸류 컬렉션을 별도의 테이블이 아닌 한 개 컬럼에 저장해야할 때가 있다고 합니다. 예를들어 도메인 모델에는 이메일 주소 목록을 Set으로 보관하고 DB에는 한 컬럼에 콤마(,)로 구분해서 저장해야하는 경우입니다. 이때 AttributeConverter를 사용하면 쉽게 구현할 수 있다고 합니다. EmailsSet 구현시 일급 객체를 적용할 수 있을거 같습니다.
@Entity
@Table(name = "purchase_order")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "emails")
@Convert(converter = EmailSetConverter.class)
private EmailSet emailSet;
}
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(this.emails);
}
}
@Getter
@AllArgsConstructor
public class Email {
private String email;
}
public class EmailSetConverter implements AttributeConverter<EmailSet, String> {
@Override
public String convertToDatabaseColumn(EmailSet attribute) {
if (attribute == null) return null;
return attribute.getEmails().stream()
.map(Email::getEmail)
.collect(Collectors.joining(", "));
}
@Override
public EmailSet convertToEntityAttribute(String dbData) {
if (dbData == null) return null;
String[] emails = dbData.split(",");
Set<Email> emailSets = Arrays.stream(emails)
.map(Email::new)
.collect(Collectors.toSet());
return new EmailSet(emailSets);
}
}
💡 밸류를 이용한 ID 매핑
식별자라는 의미를 부각시키기 위해 식별자 자체를 밸류 타입으로 만들 수 있다고 합니다. 밸류 타입을 식별자로 매핑하기 위해서는 @Id가 아닌 @EmbeddedId 어노테이션을 사용해야 합니다.
또한 JPA에서 식별자 타입은 Serializable 타입이어야 하므로 식별자로 사용할 밸류 타입은 Serializable 인터페이스를 구현해야 합니다.
@Entity
@Table(name = "purchase_order")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {
@EmbeddedId
private OrderNo orderNo;
}
@Embeddable
public class OrderNo implements Serializable {
@Column(name = "order_number")
private String number;
}
애그리게이트 로딩 전략
JPA 매핑을 설정할 때 애그리게이트에 속한 객체가 모두 모여야 완전한 하나가 됩니다. 즉 아래처럼 루트 엔티티를 조회하면 루트 엔티티에 속한 모든 객체가 완전한 상태가 되어야 합니다. 이때 Fetch 전략을 EAGER을 사용하면 즉시 로딩을 할 수 있지만 항상 좋은 것은 아닙니다. 이때 N + 1과 같은 문제가 발생할 수 있습니다.
public void find(Long id) {
Product product = productRepository.findByid(id);
}
💡 항상 루트 엔티티를 조회하는 시점에 관련된 객체가 완전한 상태를 이루어야할까?
항상 완전해야할 필요는 없다고 합니다. 다만 완전해야할 이유는 두 가지 있습니다.
이중 두번째에서 표현영역에 데이터를 보여줘야한다면 조회 전용 기능을 만들면되기 때문에 큰 영향은 없습니다. 다만 첫번째의 경우 JPA는 트랜잭션 범위 내에서 지연로딩을 허용하기 때문에 문제가 되지 않습니다. 그렇기 때문에 Fetch 전략을 LAZY해도 괜찮다고 생각합니다.
- 첫째, 상태를 변경하는 기능을 실행할 때 애그리게이트 상태가 완전해야합니다.
- 둘째, 표현영역에서 애그리게이트의 상태 정보를 보여줄 때 필요하기 때문입니다.
애그리게이트의 영속성 전파
애그리게이트가 완전한 상태여야한다는 것은 루트 엔티티를 조회할 때뿐만 아니라 저장하고 삭제할 때도 하나로 처리해야함을 의미합니다.
@Embeddable 매핑 타입의 경우 함께 저장되고 삭제되므로 cascade 옵션을 따로 설정하지 않아도 됩니다. 하지만 애그리게이트에 속한 @OneToMany 등 다양한 연관관계 어노테이션은 함께 저장되고 삭제되도록 설정해야 합니다.
- 저장 메서드는 루트 엔티티만 저장하면 안되고 애그리게이트에 속한 모든 객체를 저장해야 합니다.
- 삭제 메서드는 루트 엔티티뿐만 아니라 애그리게이트에 속한 모든 객체를 삭제해야 합니다.
'스터디 > 도메인 주도 개발 시작하기' 카테고리의 다른 글
바운디드 컨텍스트 (0) | 2023.02.18 |
---|---|
응용 서비스와 표현 영역 (0) | 2023.01.26 |
애그리게이트 (0) | 2022.12.30 |
아키텍처 개요 (0) | 2022.12.25 |
도메인 모델 시작하기 (0) | 2022.12.14 |
- Total
- Today
- Yesterday
- spring boot poi excel download
- redis sorted set
- java ThreadLocal
- spring boot redisson 분산락 구현
- spring boot redisson sorted set
- spring boot redis 대기열 구현
- 레이어드 아키텍처란
- redis 대기열 구현
- transactional outbox pattern
- java userThread와 DaemonThread
- 공간 기반 아키텍처
- transactional outbox pattern spring boot
- redis sorted set으로 대기열 구현
- 트랜잭셔널 아웃박스 패턴 스프링 부트 예제
- spring boot redisson destributed lock
- 서비스 기반 아키텍처
- microkernel architecture
- pipe and filter architecture
- 자바 백엔드 개발자 추천 도서
- service based architecture
- JDK Dynamic Proxy와 CGLIB의 차이
- polling publisher spring boot
- @ControllerAdvice
- spring boot 엑셀 다운로드
- pipeline architecture
- 트랜잭셔널 아웃박스 패턴 스프링부트
- space based architecture
- spring boot excel download oom
- spring boot excel download paging
- 람다 표현식
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |