도메인 모델과 테이블 설계
테이블 - N:M 관계 설계
JPA의 다대다 @ManyToMany는 운영에서는 사용하면 안된다. 반드시 1:N, N:1로 풀어내야 하고, 테이블 설계시에는 N:M이 불가하기 때문에 중간에 매핑테이블을 두어야 한다(억지로 만들면 가능은 하지만 정상적인 경우는 불가능하다고 생각하자).
@ManyToMany와 @JoinTable을 통해 매핑 할 경우 매핑 테이블이 자동 생성되는데 문제는 이 테이블을 변경할 수 없어서 실무 활용도가 매우 떨어진다.
엔티티 - 연관 관계의 주인
설계단계에서는 가급적이면 단방향 연관관계로 설계하는게 좋다.
양방향으로 설계했다면 연관관계 설계 시 연관관계의 주인을 정해야 한다. 외래 키가 있는 곳을 연관관계의 주인으로 정한다. 절대로 비즈니스상으로 우위에 있다고 주인으로 정하면 안된다. 외래키에 직접 변경을 가할 수 있는 엔티티 변수가 결국 연관관계의 주인이다.
1:N의 경우 N쪽이 FK를 가지고 있다. 따라서 N이 되는 엔티티의 변수가 연관관계의 주인이 된다. 반대편에 있는 변수는 mappedBy로 상대방이 주인임을 명시해준다. 연관관계의 주인쪽에 값을 세팅해야 값이 변경되며, 거울이 되는쪽은 단순히 읽기전용이 된다.
1:1 양방향 연관관계는 FK가 어디든 들어갈 수 있는데 이 경우 FK를 정의한 엔티티의 변수를 연관관계의 주인으로 본다.
단방향 연관관계는 자연스럽게 본인 변수에 매핑하면 된다.
엔티티 클래스 개발
실무에서는 보통 Entity의 Getter는 열어두고, Setter는 꼭 필요한 경우에만 사용한다. Setter를 막 열어두면 엔티티 변경 추적이 어려워진다. 따라서 엔티티를 변경할 때는 Setter 대신 별도의 비즈니스 메소드를 별도로 제공하는것이 좋다.
내장타입은 내장타입에 @Embeddable을 붙이거나 변수에 @Embedded를 붙이면 둘 중 하나만 있어도 동작은 한다. 하지만 일반적으로 둘 다 명시적으로 붙여준다.
@Embeddable 타입은 일반적으로 Setter를 안만들고 생성자로만 값을 넣는다. @Embeddable 타입의 default constructor는 기본생성자 접근자를 protected나 public으로 설정해야 한다.
기존에는 Date+Annotation을 써서 날짜를 정의했으나, 지금은 LocalDateTime을 쓰면 Hibernate가 자동으로 날짜형으로 변환해준다.
상속관계 매핑은 전략을 지정해주어야 한다. 전략은 부모클래스에 지정한다. @Inheritance(strategy = ...) 어노테이션을 사용하며, Inheritance.Type은 SINGLE_TABLE, JOINED, TABLE_PER_CLASS가 있다. SINGLE_TABLE은 DiscriminatorColumn과 DiscriminatorValue를 부모와 자식 클래스에 각각 지정한다.
EnumType의 Default는 EnumType.ORDINAL인데 문제가 이 값이 숫자라서 중간에 값이 생길 경우 큰 문제가 생긴다. 따라서 반드시 @Enumerated(EnumType.STRING)으로 반드시 문자열로 넣는것을 명시해야 한다.
1:1 매핑의 경우 FK를 주로 먼저 액세스하는 대상에 지정한다. 예를 들어 주문-배송은 1:1 매칭인데 주문을 통해 배송을 조회하므로(일반적으로 배송을 통해 주문을 조회하진 않음) 주문에 FK를 걸고, 연관관계의 주인으로 정의하면 된다.
N:M 관계는 @JoinTable 어노테이션으로 아래와 같이 정의해준다. 반대쪽에는 mappedBy를 쓴다.
@ManyToMany @JoinTable(name = "category_item", joinColumns = @JoinColumn(name = "category_id"), inverseJoinColumns = @JoinColumn(name = "item_id") ) private List<Item> items = new ArrayList<>();
셀프 계층구조는 parent 필드와 children 필드를 추가해서 각각 @JoinColumn, mappedBy를 걸어서 1:N(본인들), 1(본인):N계층구조를 만든다.
주의사항
- 엔티티에는 가급적 Setter를 사용하지 말자
- 모든 연관관계는 지연로딩(LAZY)으로 설정하자.
- 즉시로딩(EAGER)은 예측이 어렵고 어떤 SQL이 실행될지 추적하기 어렵다. 특히 JPQL을 실행할 때 N+1 문제가 자주 발생한다.
- @XToOne의 default FetchType이 EAGER이므로 반드시 FetchType.LAZY로 명시해주어야 한다.
- 컬렉션을 필드에서 초기화하자
기타
Hibernate의 테이블, 컬럼명 생성 전략. implicit은 테이블명을 명시하지 않았을 경우 적용, physical은 테이블명 지정과 무관하게 모두 적용
spring.jpa.hibernate.naming.implicit-strategy:
org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy
spring.jpa.hibernate.naming.physical-strategy:
org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
cascade 속성을 주면 연관관계 데이터도 일괄로 처리한다.
기본적으로 JPA는 Entity에 의해 데이터가 변하지만 로직처리를 위해 별도의 연관관계 편의 메서드를 만드는것이 좋다. Entity 내에 아래와 같은 형태로 만든다. 연관관계 편의 메서드는 핵심적으로 컨트롤 하는쪽 Entity에 만든다.
//==연관관계 메서드==//
public void setMember(Member member) {
this.member = member;
member.getOrders().add(this);
}
public void addOrderItem(OrderItem orderItem) {
orderItems.add(orderItem);
orderItem.setOrder(this);
}
...
//==연관관계 메서드==//
public void addChildCategory(Category child) {
this.children.add(child);
child.setParent(this);
}
도메인 개발
Repository에는 @PersistenceContext를 통해 EntityManager가 주입되는데, Factory를 주입받고 싶다면 @PersistenceUnit으로 주입받는다.
JPQL from의 대상은 Entity이다.
비즈니스 로직은 Entity에 있는게 좋다. 생성메서드도 Entity에 static 메서드로 있는게 좋다. 이처럼 엔티티에 비즈니스 로직을 넣어서 객체지향의 특성을 적극 활용하는 것을 도메인 모델 패턴이라고 한다. 반대로 서비스 계층에서 대부분 비즈니스 로직을 처리하는 것을 트랜잭션 스크립트 패턴이라고 한다.
persist할 경우 CascadeType.ALL이 걸린 타입이 전부 저장되는데 cascade 속성은 다른곳에서 참조하는곳이 없는 참조값에만 적용한다. 수정, 삭제도 마찬가지로 영향을 받는다. cascade는 잘 모르겠으면 안쓰고 추후 리팩토링 하는게 낫다.
이번 강의에서는 OrderService에 테스트를 걸었지만 원래는 도메인의 비즈니스로직에 단위 테스트를 걸어주는것이 좋다.
JPA Criteria로 동적쿼리를 생성하는것이 JPA 표준 스펙이나, 가독성이나 유지보수성이 매우 나쁘기 때문에 잘 쓰지 않는다.
따라서 동적쿼리는 QueryDSL로 작성한다.
'Backend' 카테고리의 다른 글
Effective Java (1) - 아이템1, 2 (0) | 2021.05.06 |
---|---|
JPA (2) - 웹 계층 개발 (0) | 2021.04.11 |
Spring Cloud 일단 기록하기 (0) | 2021.03.01 |
Spring Core (4) AOP (0) | 2020.07.03 |
Spring Core (3) Abstraction (0) | 2020.07.02 |