Spring/Spring Boot JPA
[Spring Boot JPA] 테이블 설계 및 Entity 개발 시 주의사항
메성
2021. 8. 14. 14:55
반응형
실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 인프런 | 강의
실무에 가까운 예제로, 스프링 부트와 JPA를 활용해서 웹 애플리케이션을 설계하고 개발합니다. 이 과정을 통해 스프링 부트와 JPA를 실무에서 어떻게 활용해야 하는지 이해할 수 있습니다., 본
www.inflearn.com
테이블 설계
- 1:N 관계에서는 N 쪽이 무조건 외래키가 존재한다.
- 일대다, 다대일 양방향 관계에서는 연관관계의 주인을 정해야하는데, 외래키가 있는 쪽을 연관관계의 주인으로 정하는 것이 좋다.
- Ex. 주문과 회원일 경우 주문쪽이 연관관계의 주인
- 연관관계의 주인쪽에 값을 세팅해야지만 값이 변경된다.
- 연관관계의 주인을 FK로 정해야하는데 그 이유는, 연관관계를 맺고 있는 두 테이블(회원과 주문)의 수정작업이 있을 때, FK(주문 테이블쪽의 회원ID)를 수정해야지만 유지보수 측면에서 우수하다. 즉, 주문 테이블의 회원번호가 변경되었으니 회원의 회원번호도 변경된다라는 것을 자연스럽게 인식할 수 있다.
- 회원과 주문사이의 연관관계가 맺어있을 경우 연관관계의 주인은 주문의 회원번호가 되고, 회원의 주문관련컬럼은 단순히 매핑만 되는 것으로 회원쪽 Entity에 매핑 여부를 기입한다.
public class Member {
//Member 1 : Order N | 이쪽 orderList는 단순히 Order에게 매핑만 된다. orderList가 변경되어도 Order의 FK는 변경되지 않는다.
@OneToMany(mappedBy = "member")
private List<Order> orderList = new ArrayList<>();
}
public class Order {
//Order N : Member 1 | 해당 값의 변경이 일어날 시 member의 id값도 변경이 일어난다.
@ManyToOne
@JoinColumn(name = "member_id") //FK
private Member member;
}
- 일대일 관계인 경우 FK는 보통 access가 많은 쪽으로 정하면 된다.
- 그렇다면 연관관계의 주인은 FK가 있는 곳으로 잡으면 된다.
- Ex. 주문과 배달의 경우 주문쪽이 access가 많고 해당에 FK를 줄 것이므로 연관관계의 주인은 주문이 된다.
Entity 개발
주요 어노테이션
JPA 내장 타입 세팅
- @Embeddable : JPA에 내장되어 있는 타입일 경우 / 본 클래스에 기입
- @Embedded : 본 클래스를 의존하는 곳에 기입
- @Embeddable이나 @Embedded 둘 중에 하나만 기입되어져 있으면 JPA 내장 타입으로 정의할 수 있다.
-
@Embeddable @Getter public class Address { private String city; private String street; private String zipcode; protected Address() {} public Address(String city, String street, String zipcode) { this.city = city; this.street = street; this.zipcode = zipcode; } } @Entity @Getter @Setter public class Delivery { @Id @GeneratedValue @Column(name = "delivery_id") private Long id; @OneToOne(mappedBy = "delivery", fetch = FetchType.LAZY) private Order order; @Embedded private Address address; @Enumerated(EnumType.STRING) private DeliveryStatus status; //READY, COMP }
상속 관계 전략
- @Inheritance(strategy = InheritanceType.SINGLE_TABLE)
- 한 테이블에 모든 item을 때려 박음
-
@Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "dtype") @Getter @Setter public abstract class Item { @Id @GeneratedValue @Column(name = "item_id") private Long id; ... }
- @Inheritance(strategy = InheritanceType.JOINED)
- 정규화된 스타일
- @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
- 상속하고 있는 item 값들의 테이블들이 세팅되는 방식
- Ex. Album, Book, Movie의 테이블이 생성
Enum 사용시 주의사항
- Status를 사용할 때 보통 Enum을 사용하는데, Entity에서 EnumStatus를 세팅할 때 주의해야할 점이 있다.
- @Enumerated는 Enum의 타입을 정하는데,
-
public enum DeliveryStatus { READY, COMP } @Enumerated(EnumType.STRING) //입력한 그대로 문자타입 private DeliveryStatus status; //READY, COMP @Enumerated(EnumType.ORDINAL) //숫자 타입으로 저장됨(default) private DeliveryStatus status; //READY : 1, COMP : 2
- ORDINAL이 디폴트인데 해당 형태로 등록이 되어있을 때 위 처럼 READY와 COMP 사이에 다른 상태가 들어오면 COMP는 2가 아닌 3으로 변경되어 원하는 로직을 구성 못할 수 있다.
Getter와 Setter
- 실무에서 엔티티의 데이터는 조회할 일이 매우 많으므로 Getter는 열어두는 것이 일반적으로 좋다.
- 하지만 Setter의 경우는 다르다.
- Setter를 호출하게 되면 데이터가 변하므로, Setter를 열어두면 가까운 미래에 엔티티가 변경되는 지점을 찾기가 어려워잔다.
- 그러므로 Setter 대신 변경 지점이 명확하도록 메소드를 별도로 지정하는 것이 좋다. 아래 코드처럼 Setter 대신 addStock과 removeStock 메소드를 Entity내에 만들어 놓는다.
-
@Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "dtype") @Getter public abstract class Item { ... /** * stock 증가 */ public void addStock(int quantity) { this.stockQuantity += quantity; } /** * stock 감소 */ public void removeStock(int quantity) { int realStock = this.stockQuantity - quantity; if (realStock < 0) { throw new NotEnoughStockException("need more stock"); } this.stockQuantity = realStock; } }
Entity의 식별자
- 보통 Entity의 식별자는 Long id;를 사용하는데 객체의 경우 타입이 존재하므로 어떤 객체의 변수인지 쉽게 구분할 수 있다.
- 하지만 Table의 경우 타입이 없으므로 @Column을 지정할 때는 객체이름_id로 작성하는 것이 유지보수에 편하다.
-
@Id @GeneratedValue @Column(name = "order_id") private Long Id;
값 타입
@Embeddable
@Getter
public class Address {
private String city;
private String street;
private String zipcode;
protected Address() {}
public Address(String city, String street, String zipcode) {
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
}
- 위 처럼 값 타입의 경우 Setter를 지정하지 않고, 생성자에 값을 모두 초기화해서 변경 불가능한 클래스를 만드는 것이 좋다.
- JPA 스펙상 Entity나 임베디드(@Embeddable) 타입은 기본 생성자를 public이나 protected로 두는 것이 좋다.
- JPA가 이런 제약(기본 생성자를 만드는 제약)을 두는 이유는 JPA 구현 라이브러리가 객체를 생성할 때 리플렉션 같은 기술을 사용할 수 있도록 지원해야 하기 때문이다.
엔티티 설계 시 주의점
- 가급적이면 Setter를 사용하지 말자.
- 변경 포인트가 너무 많아서 유지보수가 힘들다.
- 모든 연관관계는 지연로딩으로 설정해야한다.
- @XToOne인 경우에는 모두 지연 로딩으로 변경해줘야 한다. 기본이 즉시 로딩이므로,
- 즉시 로딩으로 하게 되면 실무에서 매우 위험할 수 있다.
- //@OneToOne(fetch = FetchType.LAZY) //@ManyToOne(fetch = FetchType.LAZY) public class Order { (fetch = FetchType.LAZY) //Order N : Member 1 (name = "member_id") //FK private Member member; }
- member의 fetchType을 LAZY로 안할 경우 order 조회하는 순간 order가 member를 찾게된다.
- 즉, 100개의 order 쿼리를 조회할 때 각각의 쿼리가 100개의 member 쿼리를 조회하게 된다.
- 성능 똥망
- @XToOne인 경우에는 모두 지연 로딩으로 변경해줘야 한다. 기본이 즉시 로딩이므로,
- 컬렉션은 필드에서 초기화하자.
-
List<Order> orderList = new ArrayList<>();
-
- 테이블 및 컬럼명 생성 전략.
- 스프링 부트에서는 @Table 및 @Column을 지정하지 않으면 기본 세팅에 따라 변경된다.(SpringPhysicalNamingStrategy)
- 카멜 케이스 -> 언더 스코어
- 점 -> 언더 스코어
- 대문자 -> 소문자
private int orderPrice; //주문 가격 //order_pirce로 변경
- 스프링 부트에서는 @Table 및 @Column을 지정하지 않으면 기본 세팅에 따라 변경된다.(SpringPhysicalNamingStrategy)
Cascade
public class Order {
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItemList = new ArrayList<>();
}
- 원래는 OrderItem에 먼저 세팅을 하고 Order를 저장해야하는데, Order에 값들을 저장하게되면 Cascade에 의해서 OrderItem에도 값들이 저장되게 된다.
public class Order {
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "delivery_id")
private Delivery delivery;
}
- Order 저장 시 Delivery도 자동으로 값 저장이 된다.
연관관계 메소드
- 양방향인 객체들을 저장하려고 할 때 각각의 객체를 생성해서 각각 넣어줘야 한다.
-
public static void main(String [] args) { Member member = new Member(); Order order = new Order(); member.getOrders().add(order); order.setMember(member); }
-
- 하지만 연관관계 메소드를 작성함으로써 좀 더 간단하게 작성이 가능하다.
-
public class Order { ... // ====연관관계 메소드==== public void setMember(Member member) { this.member = member; member.getOrders().add(this); } public void addOrderItem(OrderItem orderItem) { orderItemList.add(orderItem); orderItem.setOrder(this ); } public void setDelivery(Delivery delivery) { this.delivery = delivery; delivery.setOrder(this); } }
-
반응형