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 { @ManyToOne(fetch = FetchType.LAZY) //Order N : Member 1 @JoinColumn(name = "member_id") //FK private Member member; }
    • member의 fetchType을 LAZY로 안할 경우 order 조회하는 순간 order가 member를 찾게된다.
      • 즉, 100개의 order 쿼리를 조회할 때 각각의 쿼리가 100개의 member 쿼리를 조회하게 된다.
      • 성능 똥망
  • 컬렉션은 필드에서 초기화하자.
    • List<Order> orderList = new ArrayList<>();
  • 테이블 및 컬럼명 생성 전략.
    • 스프링 부트에서는 @Table 및 @Column을 지정하지 않으면 기본 세팅에 따라 변경된다.(SpringPhysicalNamingStrategy)
      • 카멜 케이스 -> 언더 스코어
      • 점 -> 언더 스코어
      • 대문자 -> 소문자
      private int orderPrice; //주문 가격
      //order_pirce로 변경

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);
        }
        
      }

 

반응형