- 초보 개발자의 객체 지향에 대한 실수
- 회원과 주문의 관계에서 사람의 생각으로 회원을 통해서 주문을 생성한다고 생각할 수 있는데 시스템은 이 둘 엔티티를 동급으로 놓고 생각해야하기 때문에 주문을 생성할 때 회원이 필요하다고 생각하는게 맞다.
- 어떤 회원의 주문 기록이 필요하다면 회원에 접근하여 주문을 찾는게 아니라 주문에서 회원의 조건을 걸고 찾는 것이다.
- 연관관계의 주인
- 만약 회원과 주문에 일대다,다대일의 양방향 관계를 가진다면(실제로는 회원에 주문 컬렉션이 필요가 없다.) 양방향 관계의 주인을 정해야하는데 주문이 '다'를 담당하여 복수의 갯수를 가지므로 연관관계에서 주인으로 정하는 것이 좋다. 보통 관계형 데이터베이스에서는 연관관계의 주인인 주문이 외래 키를 가지게 된다.
- 연관관계 주인 쪽에 실제 값을 세팅해야 값이 변경된다. 회원-주문 관계에서는 주문이 실제 회원 id를 가지고 있는 것이다.
- 일대일 관계에서의 주인
- 주문과 배송의 관계는 각 주문당 한번의 배송이 존재하도록 만들면 외래키는 어디든 존재할 수 있다.
- 원칙 - 외래키가 가까운 곳에 있는 것을 연관 관계의 주인으로 잡는다.
- 연관 관계의 주인은 외래키가 어디에 위치하느냐의 문제이다. 비지니스상의 우위 관계를 통해 연관관계의 주인을 정하는 것이 아니다. 개발할 때 사람의 본능적인 직감으로 개발하지 말고 동등한 엔티티로 관계를 보고 설계하자.
- 만약 회원-주문 관계에서 회원이 주인을 가져가게 되면 주문의 데이터가 변경되고 유지보수가 어렵다. 주문의 데이터를 변경하기 위해 업데이트 쿼리가 나가야하므로 성능상의 문제가 일어난다.
- 다대다 관계
- 예로 쇼핑몰에서 회원은 여러 상품을 주문할 수 있다. 한 번 주문할 때 여러 상품을 고를 수 있으므로 주문과 상품은 다대다 관계이다.
- 실무에서 이런 관계에 놓여있을 때 다대다 그 자체로 해결하는 것은 좋지 않다. 주문상품이라는 중간 엔티티를 추가해서 일대다 - 다대일의 관계로 풀어낼 수 있다.
- JPA에서는 Entity 객체의 컬렉션을 통해 다대다 관계를 만들 수 있지만 관계형 데이터베이스에서는 일반적 설계로는 불가능하므로 중간에 '주문상품', '카테고리상품'처럼 중간 매핑 테이블을 만들어야한다.
엔티티 설계시 주의할 점
- Setter를 가급적 사용하지 말자.
- SOLID에 위배됨.
- 변경 포인트가 많아져서 유지보수가 어렵다.
- 모든 연관 관계는 지연로딩으로 설정한다.
- 즉시로딩(`EAGER`)은 예측이 어렵고, 어떤 SQL이 실행될지 추적하기 어렵다.
- ex)
- A -> B -> D -> F
- -> C -> E -> G
- 예시처럼 모두 즉시 조회로 엮여있다면 A를 조회하면서 B ~ G까지 즉시 조회되는 문제가 생기게 된다.
- ex2)
- 회원-주문의 관계에서 JPQL을 통해 주문을 가져오게 되면 EAGER로 묶여있던 탓에 n + 1 문제가 생기게 된다.
- 1(주문 - select o from order : 만약 총 100건이라면)의 조회를 통해 EAGER로 묶인 n 건의 문제(100건의 member를 찾는 문제)가 생기게 된다.
- LAZY로 설정 후 FETCH Join을 통해 필요한 엔티티들을 가져오도록 하자.
- @OneToOne, @ManyToOne은 디폴트가 EAGER이므로 직접 LAZY로 변경하자.
- 한 건을 조회하기 때문에 EAGER로 써도 문제 없는 듯 보이지만 위의 예제 2번처럼 수만, 수십만 건에 대한 n개의 sql이 나갈 수 있다.
- 컬렉션은 필드에서 바로 초기화를 하자.
- ex)
- private List<Order> orders = new ArrayList<>();
- 하이버네이트가 컬렉션을 영속화할 때 변경 추적을 위해서 컬렉션을 하이버네이트 내장 컬렉션으로 바꾸는 작업을 하게 되는데 만약 다른 곳에서 잘못 컬렉션을 생성하게 되면 하이버네이트가 설계한대로 진행되지 않는 문제가 생길 수 있으므로 필드 생성시 바로 초기화를 해주도록 하자. 설령 로직에서 꺼내 사용하더라도 변경하지 말자.
- ex)
Member member = new Member();
System.out.println(member.getOrders().getClass());
em.persist(member);
System.out.println(member.getOrders().getClass());
//출력 결과
class java.util.ArrayList
class org.hibernate.collection.internal.PersistentBag
- 테이블과 컬럼명의 생성 전략
- 스프링 부트에서 하이버네이트 기본 매핑 전략을 변경해서 실제 테이블 필드명은 다르다.
- 하이버네이트 기본 설정
- 엔티티의 필드명을 그대로 테이블의 컬럼명으로 사용 : SpringPhysicalNamingStrategy
- 스프링 부트의 새로운 설정
- 카멜 케이스 -> 언더스코어
- .(점) -> _(언더스코어)
- 대문자 -> 소문자
- 하이버네이트 기본 설정
- 2단계로는 논리명 생성과 물리명 적용이 있다.
- 논리명: 명시적으로 컬럼, 테이블 명을 적지 않을 경우에 대한 전략 설정
-
spring.jpa.hibernate.naming.implicit-strategy
-
- 물리명: 모든 논리명에 적용되는 것 - 전사 표준 테이블 명 앞에 'xx_'을 붙인다. 등..
-
spring.jpa.hibernate.naming.physical-strategy
-
- 논리명: 명시적으로 컬럼, 테이블 명을 적지 않을 경우에 대한 전략 설정
- 스프링 부트에서 하이버네이트 기본 매핑 전략을 변경해서 실제 테이블 필드명은 다르다.
- Cascade 전략에 대한 이야기
- cascade = CascadeType.ALL 옵션은 영속성에 올라간 모든 엔티티에게 cascade를 전파한다.
- 만약 order를 JPA에서 persist해야할 때 orderItem, delivery에 대한 영속성 관리를 위해 cascade 옵션이 없다면 모두 각각 persist를 해주어야 한다 하지만 `CascadeType.ALL` 옵션을 걸어줌으로서 persist(order)라는 한 코드만으로 다른 엔티티에 대한 persist를 하지 않아도 전파가 된다.
- 연관관계 편의 메서드 작성를 작성하자.
- 양방향 연관관계에서 한 곳을 변경하게 되면 다른 객체의 값도 변경해주어야 하기 때문에 2줄의 코드를 작성해야하는데(아래 코드처럼..) 양쪽을 주로 컨트롤하는 객체에 편의 메서드를 작성하여 두 객체의 변경을 원자적으로 관리하도록 메서드를 작성하자
//회원-주문 모두 추가 및 설정을 위한 코드 2줄
member.getOrders().add(order);
order.setMember(member);
//Order에 작성된 메서드
//==연관관계 편의 메서드==//
public void setMember(Member member) {
this.member = member;
member.getOrders().add(this);
}
public void addOrderItems(OrderItem orderItem) {
orderItems.add(orderItem);
orderItem.setOrder(this);
}
public void setDelivery(Delivery delivery) {
this.delivery = delivery;
delivery.setOrder(this);
}
'Web Dev > Spring' 카테고리의 다른 글
[JPA] 정적 쿼리와 동적 쿼리 (0) | 2023.03.23 |
---|---|
[Spring+JPA] 간단 노트 정리 (0) | 2023.03.17 |
[Spring] 스프링 빈 충돌 이슈 관리 (0) | 2023.03.12 |
Spring - Paging 처리 (0) | 2022.08.22 |
Spring Security - Filter의 동작 원리 (0) | 2022.08.02 |