inflearn 김영한님의 강의 실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 중 도메인 분석 설계 챕터를 듣고 배운것을 정리한 내용입니다.
JPA 와 함께 Entity 를 잘 만드는 방법에 대한 이야기
DB 테이블 간의 관계를 나타내기 위해선 FK 를 사용한다. 만약, Entity 클래스도 똑같은 방식으로 설계하게 된다면 어떻게 될까?
public class Member {
/*...*/
private Long teamId;
}
public class Team {
private Long id;
/*...*/
}
회원의 Team을 가져오기 위해선 매번 teamId 를 빼내서 Team 쪽에 쿼리를 날리는 코드를 넣어줘야 할 것이다. 이것은 객체지향적이라 보긴 어려운 모습이다.
객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 객체간 협력 관계를 만들 수 없다.
객체간 연관관계를 나타내기엔 다음과 같은 방법이 더 좋을 것이다.
public class Member {
/*...*/
private Team team;
}
이렇게 Entity 클래스 간의 관계를 대상 Entity 의 참조를 가지는 형태로 만들어 줄 수 있고,
양 Entity 가 서로의 참조를 가지는 경우 양방향 연관관계
한 Entity 만 다른 Entity의 참조를 가지는 경우 단방향 연관관계라고 한다.
"회원, 주문 Entity 가 있고 한 회원은 여러 주문을 할 수 있어야 한다" 라는 요구사항이 존재하는 경우
회원 Entity 입장에서는 주문에 대해 일대다 관계가, 주문 Entity 입장에서는 회원에 대해 다대일의 양방향 연관관계를 만들 수 있다.
일대다 관계에서는 '다' 부분에 FK 가 존재해야 한다. 즉 주문이 회원의 FK 를 가진다.
양방향 연관관계에서는 연관관계의 주인
을 정하는 것이 중요하다.
객체에는 양방향 연관관계라는 것은 사실 없고, 서로 다른 단방향 연관관계 2개를 잘 묶어서 양방향으로 보이게 하는 것 뿐이다. 반면, DB 는 FK 하나로 연관관계가 성립되므로 객체와 DB 둘 간에 차이점이 생긴다. 이 때문에 JPA 에서는 연관관계 중 한 객체를 정해서 FK 를 관리하게 해야하고 이를 연관관계의 주인이라고 한다.
FK 가 있는 곳을 연관관계의 주인으로 정한다 (비지니스상 우위에 있다고 주인이 아니다)
자동차와 바퀴로 예를들면 바퀴가 '다' -> FK존재 -> 연관관계의 주인이 된다.
연관관계의 주인을 이런 규칙으로 정하는 이유는 쉬운 유지보수와 업데이트 쿼리의 성능을 목적으로 한다.
연관관계의 주인이 FK 를 관리 (등록, 수정, 삭제) 할 수 있고 연관관계의 거울(주인이 아닌 곳) 에서는 mappedBy 속성을 통해 읽기만 가능하다.
// 연관관계의 주인 (Member의 FK를 가짐)
public class Order {
/*...*/
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "member_id")
private Member member;
}
// 연관관계의 거울
public class Member {
/*...*/
@OneToMany(mappedBy = "member") // Order의 member 필드와 매핑
private List<Order> orders = new ArrayList<>();
}
// 실제 member.getOrders().add(order) 를 하고 persist() 해도 null이 저장된다.
// (연관관계의 주인인 Order.member 에 들어가지 않았기 때문)
1:1 관계는 둘 중 어디든 FK 를 둘 수 있다. 더 많이 조회하는 Entity 에 두자.
실무에서는 @ManyToMany 를 사용하지 않는다.
M2M 은 중간에 mapping table 이 생길 수 밖에 없는데 이 테이블에는 등록일, 수정일 같은 필드가 없이 mapping key 만 존재해야 해서 운영상으로 어려움이 크다.
Address 같은 애들을 값을 표현하기 위한 타입으로 만들 수 있다.
값 타입은 변경하면 안된다 생성시에만 값을 주고 변경 불가능하도록 만들어야 한다.
@Embeddable
@Getter
public class Address {
private String city;
private String street;
private String zipcode;
protected Address() {}
public Address(String city, String street, String zipcode) { /*...*/}
}
public class Member {
/*...*/
@Embedded
private Address address;
}
값 타입에는 @Embeddable
을 사용하는 필드에는 @Embedded
를 준다.
변경 불가능하게 설계하자
변경 포인트가 많아지고 유지보수가 어려워진다. 스프링을 하며 많이 듣는 이야기 중 하나.
@Enumberated(EnumType.ORDINAL) vs @Enumberated(EnumType.STRING)
무조건 STRING 을 사용한다.
ORDINAL 사용 시 0,1,2 이런식으로 내부적으로 숫자로 타입을 나누는데, 이 경우 중간에 새로운 type을 추가할 경우 기존에 만들어놨던 0,1,2 의 순서와 꼬이게 된다면 난리가 난다. 즉 위험하다.
EAGER(즉시로딩) 은 예측이 어렵고 어떤 SQL 이 실행될지 추적이 어렵다.
EAGER 사용 시 조회 하나 때리면 관련된 애들을 모두 join 해서 가져온다. em.find 를 통해 하나만 조회하는 경우는 큰 문제가 없겠지만, JPQL(JPA 가 지원하는 쿼리) 로 가져올 경우 예를들어 select * from order limit 100
를 수행할 때 EAGER 로 fetchType이 지정된 경우 order 가 member를 가져오기 위해 쿼리 100개가 같이 날아감. 즉 101개가 날아가는 것. 이걸 N+1 문제
라고 한다.
실무상의 모든 연관관계는 지연로딩(LAZY) 로 설정해야 한다.
@XToOne(OneToOne, ManyToOne) 의 경우 EAGER 가 default 이므로 직접 LAZY 로 설정해줘야 한다
(@XToMany 는 기본이 LAZY 라서 그냥 둬도 된다)
만일 내가 연관관계에 필요한 entity 를 한번에 가져오고 싶은경우?
=> FETCH JOIN 을 사용하자
EAGER 를 고려하지 말자.
Collection 은 생성자나 setter 등이 아닌 필드에서 초기화 하는 것이 hibernate best practice 이다.
public class Member {
/*...*/
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
그 이유는 다음과 같다
new ArrayList<>();
를 통해 초기화를 시켜버릴 경우 Hibernate 가 원하는 메커니즘으로 동작하지 않을 수 있다.Member member = new Member();
System.out.println(member.getOrders().getClass());
// class java.util.ArrayList
em.persist(member);
System.out.println(member.getOrders().getClass());
// class org.hibernate.collection.internal.PersistentBag
필드에서 초기화 한 후 읽기만 하자. 건들지 말자.
모든 엔티티는 저장하고 싶으면 각각 persist를 해줘야 한다.
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
와 같이 써주면 collection에 있는 orderItem 들을 각각 persist 할 필요 없이 persist(order) 로 한방에 insert 또는 delete 할 수 있다.
양방향 연관관계에서 setting을 할 경우 양쪽 다 해줘야 하는 불편함이 있는데 이를 원자적으로 처리할 수 있게 편의 메서드를 만들어 둘 수 있다.
public class Order {
/*...*/
public void setMember(Member member) {
this.member = member;
member.getOrders().add(this);
// member에 대한 order setting 도 원자적으로 일어나게 함
}
}
연관관계 편의 메서드는 핵심적으로 컨트롤 하는 쪽이 들고있는 쪽이 좋다.
예약어
주문을 나타내는 Order 의 경우 예약어를 피하기 위해 객체는 Order로 table은 Orders 로 나타내는게 관례이다.
Table 네이밍
소문자의 underscore (snake_case) 가 일반적이다.
Entity의 id
객체의 필드는 id 라고 만들고 @Column(name = "member_id")
이런 식으로 만드는 것이 좋다.
객체는 명확히 어떤것의 id 인지 구분할 수 있는데 테이블은 id라고만 적어놓으면 구분이 쉽지 않다. join 시에도 알아보기가 힘들어서 관례적으로 이런 스타일로 많이 적는다.
객체와 Table의 매핑
Hibernate 의 기존 구현에서는 Entity의 필드명을 그대로 테이블의 필드명으로 적지만, springboot 사용 시 SpringPhysicalNamingStrategy 를 사용해서 필드명의 기본 전략이 camelCase -> snake_case 이고, 대문자를 소문자로, '.' 을 '_' 로 변경한다. (커스텀한 테이블 네이밍을 하고 싶은 경우에도 변경해서 활용 가능하다)
시스템마다 다르다. 실시간 트레픽이 엄청나고 정합성보다 성능이 중요한 경우 FK 를 빼고 idx 만 잘 잡기도 한다.
늘 궁금했던 내용이다. 반정규화나 FK를 빼는 것을 고려할 정도로 많은 트레픽은 어느정도일지 감이 안오긴 한다.
새로 알게된 intellij 단축키