4.1.1 모듈 위치
4.1.2 리포지터리 기본 기능 구현
public interface OrderRepository {
// 인터페이스는 애그리거트 루트를 기준으로 작성
// 아래의 경우 Order
Order findById(OrderNo no);
// save() 메서드는 전달받은 에그리거트를 저장함.
void save(Order order);
}
findBy프로퍼티이름(프로퍼티 값)
형식package shop.order.infra;
import org.springframework.stereotype.Repository;
import shop.order.domain.Order;
import shop.order.domain.OrderNo;
import shop.order.domain.OrderRepository;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
@Repository
public class JpaOrderRepository implements OrderRepository {
@PersistenceContext
private EntityManager entityManager;
@Override
public Order findById(OrderNo id) {
// EntityManager의 find 메서드를 이용하여 ID로 애그리거트를 검색
return entityManager.find(Order.class, id);
}
@Override
public void save(Order order) {
// EntityManager의 persist 메서드를 이용하여 애그리거트를 저장
entityManager.persist(order);
}
}
➕ 알아두기
다수의 개발자는 스프링과 JPA로 구현할 때 스프링 데이터 JPA를 사용하므로 리포지터리 인터페이스만 정의하면 나머지 리포지터리 구현 객체는 스프링 데이터 JPA가 알아서 만들어줌. 따라서 리포지토리 인터페이스를 구현한 클래스를 직접 작성할 일은 거의 없음.
애그리거트를 수정한 결과를 저장소에 반영하는 메서드를 추가할 필요 X
ex.
public class ChangeOrderService {
@Transactional
// 트랜잭션 관리 기능을 통해 트랜잭션 범위에서 실행됨
public void changeShippingInfo(OrderNo no, ShippingInfo newShippingInfo) {
Optional<Order> orderOpt = orderRepository.findById(no);
Order order = orderOpt.orElseThrow(() -> new OrderNotFoundException());
order.changeShippingInfo(newShippingInfo);
}
...
}
메서드 실행이 끝나면 트랜잭션 커밋 ➡️ JPA; 트랜잭션 범위에서 변경된 객체 데이터를 DB에 반영하기 위해 UPDATE 쿼리 실행
ID가 아닌 다른 조건으로 애그리거트를 조회할 때: findBy(조건 대상이 되는 프로퍼티 이름)
JPQL을 이용한 findByOrdererId 메서드 구현
@Override
public List<Order> findByOrdererId(String ordererId, int startRow, int fetchSize) {
TypedQuery<Order> query = entityManager.createQeury(
"select o from Order o "+
"order by o.number.number desc",
Order.class
);
query.setParameter("ordererId", ordererId);
query.setFirstResult(startRow);
query.setMaxResults(fetchSize);
return query.getResultList();
}
애그리거트를 삭제한 기능 구현
// 삭제할 애그리거트 객체를 파라미터로 전달 받기
public interfate OrderRepository {
...
public void delete(Order order);
}
// EntityManager의 remove() 메서드를 이용한 삭제 기능 구현
public class JpaOrderRepository implements OrderRepository {
@PersistenceContext
private EntityManager entityManager;
...
@Override
public void delete(Order order) {
entityManager.remove(order);
}
}
➕ 삭제 기능
삭제 요구사항이 있더라도 데이터를 실제로 삭제하는 경우는 많지 않음. 사용자가 삭제 기능을 실행할 때 삭제 플래그를 사용하여 데이터를 화면에 보여줄지 여부를 결정하는 방식으로 구현하기.
스프링 데이터 JPA는 지정한 규칙에 맞게 리포지터리 인터페이스를 정의하면 리포지터리를 구현한 객체를 알아서 만들어 Spring Bean으로 등록해줌.
스프링 데이터 JPA를 이용하여 만든 ORderRepository 인터페이스
import org.springframework.data.repository.Repository;
import java.util.Optional;
public interface OrderRepository extends Repository<Order, OrderNo> {
Optional<Order> findById(OrderNo id);
void save(Order order);
}
스프링 빈에 등록된 리포지토리를 주입받아 사용하는 방법
@Service
public class CancelOrderService {
private OrderRepository roderRepository;
...
public CancelOrderService(OrderRepository orderRepository, ...) {
this.orderRepository = orderRepository;
...
}
@Transactional
public void cancel(OrderNo orderNo, Canceller caneller) {
Order order = orderRepository.findById(orderNo)
.orElseThrow(() -> new NoOrderException());
if (!cancelPolicy.hasCancellationPermission(order, canceller)) {
throw new NoCancellablePermission();
}
order.cancel();
}
}
4.3.1 엔티티와 밸류 기본 매핑 구현
애그리거트와 JPA 매핑을 위한 기본 규칙: @Entity
로 매핑
한 테이블에 엔티티와 밸류 데이터가 같이 있을 경우,
@Embeddable
로 매핑 설정@Embedded
로 매핑 설정엔티티와 밸류가 한 테이블로 매핑
@Entity
@Tagble(name = "purchase_order")
public class Order {
// ...
@Embedded
private Orderer orderer;
// ...
}
@Embeddable
public class Orderer {
// MemberId에 정의된 칼럼 이름을 변경하기 위해
// @AttributeOverride 애노테이션 사용
@Embedded
@AttributeOverrides(
@AttributeOverride(name = "id", column = @Column(name = "orderer_id"))
)
private MemberId memberId;
@Column(name = "orderer_name")
private String name;
// ...
}
@Embaddable
public class MemberId implements Serializable {
@Column(name = "member_id")
private String id;
// ...
}
4.3.2 기본 생성자
@Entity
와 @Embeddable
로 클래스 매핑할 시 기본 생성자를 제공해야 함@Embeddable
public class Receiver {
@Column(name = "receiver_name")
private String name;
@Column(name = "receiver_phone")
private String phone;
// 기본 생성자를 다른 코드에서 사용하면 값이 없는 온전하지 못한 객체를 만들어내므로 protected로 선언
protected Receiver() {} // JPA를 적용하기 위한 기본 생성자 추가
public Receiver(String name, String phone) {
this.name = name;
this.phone = phone;
}
}
4.3.3 필드 접근 방식 사용
4.3.4 AttributeConverter를 이용한 밸류 매핑 처리
AttributeConverter
4.3.5 밸류 컬렉션: 별도 테이블 매핑
밸류 컬렉션을 별도 테이블로 매핑할 때는 @ElementCollection
과 @CollectionTable
을 함께 사용
@Entity
@Table(name = "purchase_order")
public class Order {
// ...
@ElementCollection
@CollectionTable(name = "order_line", joinColumns = @JoinColumn(name = "order_number"))
@OrderColumn(name = "line_idx")
private list<OrderLine> orderLines;
}
@Embeddable
public class OrderLine {
// ...
@Embedded
private ProductId productId;
}
4.3.6 밸류 컬렉션: 한 개 칼럼 매핑
밸류 컬렉션을 별도 테이블이 아닌 한 개 칼럼에 저장해야 할 때가 있음
ex. 도메인 모델에는 이메일 주소 목록을 Set으로 보관하고 DB에는 한 개 컬럼에 콤마로 구분해서 저장해야 할 때, AttributeConverter
사용. 단, AttributeConverter를 사용하려면 밸류 컬렉션을 표현하는 새로운 밸류 타입을 추가해야 함.
public class EmailSet {
private Set<Email> emails = new HashSet<>();
private EmailSet() {
}
private EmailSet(Set<Email> emails) {
this.emails.addAll(emails);
}
public Set<Email> getEmails() {
return Collections.unmodifiableSet(emails);
}
}
@Converter
public class EmailSetConverter implements AttributeConveter<EmailSet, String> {
@Override
public String convertToDatabaseColumn(EmailSet attribute) {
if (attribute == null) {
return null;
}
return attribute.getEmails().stream()
.map(Email::toString)
.collect(Collectors.joining(","));
}
@Override
public EmailSet convertToEntityAttribute(String dbData) {
if (dbData == null) {
return null;
}
String[] emails = dbData.split(",");
Set<Email> emailSet = Arrays.stream(emails)
.map(value -> new Email(value))
.collect(toSet());
return new EmailSet(emailSet);
}
}
@Column(name = "emails")
@Convert(converter = EmailSetConverter.class)
private EmailSet emailSet;
4.3.7 밸류를 이용한 ID 매핑
식별자라는 의미를 부각시키기 위해 식별자 자체를 밸류 타입을 만들 수도 있음.
밸류 타입을 식별자로 매핑하면 @Id
대신 @EmbeddedId
애너태이션을 사용.
@Embeddable
public class OrderNo implements Serializable {
@Column(name = "order_number")
private String number;
public boolean is2ndGeneration() {
return number.startsWith("N");
}
// ...
}
밸류 타입으로 식별자를 구현할 경우 식별자에 기능을 추가할 수 있음.
JPA 에서 식별자 타입은 Serializable 타입이어야 함.
내부적으로 엔티티를 비교시 사용하는 equals()
와 hashcode()
메서드도 알맞게 구현해야 함.
4.3.8 별도 테이블에 저장하는 밸류 매핑
애그리거트에서 루트 엔티티를 뺀 나머지 구성요소는 대부분 밸류.
만약 밸류가 아니라 엔티티가 확실하다면 다른 애그리거트는 아닌지 확인해야 함. 자신만의 독자적인 라이프사이클을 갖는다면 다른 애그리거트일 가능성이 높음.
밸류인지 엔티티인지 구분하는 방법: 고유 식별자를 갖는지 확인하기
별도 테이블에 PK가 있다고해서 테이블과 매핑되는 애그리거트가 항상
고유 식별자를 갖는 것은 아님.
밸류를 매핑한 테이블을 지정하기 위해 @SecondaryTable
과 @AttributeOverride
를 사용
@Entity
@Table(name = "article")
// @SecondaryTable 을 이용하면 아래 코드를 실행할 때 두 테이블을 조인해서 데이터를 조회함.
// 그러나 @SecondaryTable 을 사용하면 목록 화면에 보여줄 Article 을 조회할 때 article_content 테이블까지 조인해서 데이터를 읽어오게 됨(원하는 결과 x)
@SecondaryTable(
name = "article_content",
pkJoinColumns = @PrimaryKeyJoinColumn(name = "id")
)
public class Article {
@Id
private Long id;
private String title;
@AttributeOverrides({
@AttributeOverride(name = "content", column = @Column(table = "article_content", name = "content")),
@AttributeOverride(name = "contentType", column = @Column(table = "article_content", name = "content_type"))
})
private ArticleContent content;
}
➡️ ArticleContent 를 엔티티로 매핑하고 지연로딩을 활용할수도 있지만 좋은 방법은 아님.
5장에서 조회 전용 쿼리를 실행하는 방법 알아보기
4.3.9 밸류 컬렉션을 @Entity로 매핑하기
@Entity
를 사용해야 할 때도 있음.@Embeddable
타입의 클래스 상속 매핑을 지원하지 않음.@Entity
를 이용한 상속 매핑으로 처리하기.@Inheritance
애너테이션 적용strategy SINGLE_TABLE
사용@DiscriminatorColumn
사용@DiscriminatorValue
사용clear()
를 사용할 경우 다수의 DELETE 쿼리가 발생하므로 성능상 좋지 않을 수 있음@Embeddable
매핑 단일 클래스를 사용할 경우 다형성을 포기하고 if-else 로 컬럼을 구분해야 함.4.3.10 ID 참조와 조인 테이블을 이용한 단방향 M-N 매핑
@Entity
@Table(name = "product")
public class Product {
// ID 참조를 이용한 애그리거트 간 단방향 M:N 연관은 밸류 컬렉션 매핑과 동일한 방식으로 설정
@EmbeddedId
private ProductId id;
// 차이점: 집합의 값에 밸류 대신 연관을 맺는 식별자가 옴
// @ElementCollection을 이용하기 때문에 Product를 삭제할 때 매핑에 사용한 조인 테이블의 데이터도 함께 삭제됨.
@ElementCollection
@CollectionTable(name = "product_category",
joinColumns = @JoinColumn(name = "product_id"))
private Set<CategoryId> categoryIds;
}
@Transactional
public void revmoeoptions(ProductId id,int optIdxToBeDeleted){
//Product를 로딩. 컬렉션은 지연 로딩으로 설정했다면 Option은 로딩되지 않음
Product product = productRepository.findByid(id);
// 트랜잭션 범위이므로 지연 로딩으로 설정한 연관 로딩 가능
product.removeOption(optIdxToBeDeleted);
}
@Entity
public class Product {
@ElementCollection(fetch = FetchType.LAZY)
@CollectionTable(name = "product_option", joinColumns = @JoinColumn(name = "product_id"))
@OrderColumn(name = "list_idx")
private List<Option> options = new ArrayList<>();
public void removeOption(int optIdx) {
//실제 컬렉션에 접근할 때 로딩
this.options.remove(optIdx);
}
}
@Embeddable
매핑 타입의 경우 함께 저장되고 삭제되므로 cascade
속성을 추가로 설정하지 않아도 됨.@Entity
타입에 대한 매핑은 cascade
속성을 사용해서 저장과 삭제 시에 함께 처리되도록 설정해야 함.@OneToOne
, @OneToMany
는 cascade
속성의 기본값이 없으므로 cascade
속성값으로 CascadeType.PERSIST
, CascadeType.REMOVE
를 설정@GeneratedValue
를 사용하며 DB에 INSERT 쿼리가 실행돼야 식별자가 생성됨.@Entity
, @Table
등 구현 기술에 특화된 애너테이션을 사용하고 있음.