[SpringBoot] Entity 클래스 개발하기
엔티티 클래스
@Entity
@Table(name = "orders")
@Getter @Setter
public class Order {
}
- @Entity는 해당 클래스가 엔티티 클래스임을 Spring에 알려준다.
- @Table은 엔티티 클래스가 매핑될 테이블을 지정하며, 지정하지 않을 시 하이버네이트의 테이블명 생성 전략에 따라 자동으로 생성된다.
- @Getter와 @Setter은 Lombok에서 제공하는 어노테이션.
- @Setter의 경우에는 필요한 부분에서만 부분적으로 사용해야 한다. 변경 포인트가 많아져 예측이 어렵다. 이에 대해서는 후술하려고 한다.
테이블 컬럼 생성
@Entity
@Table(name = "orders") //테이블 이름 변경
@Getter @Setter
public class Order {
@Id
@GeneratedValue
@Column(name = "order_id")
private Long id;
private String name;
private LocalDateTime orderDate;
}
- @Column 어노테이션을 통해 컬럼의 이름과 옵션을 지정할 수 있다.
- @Id 어노테이션을 통해 해당 컬럼이 PK로 사용하는 것을 명시한다.
- @GeneratedValue 어노테이션은 주로 @Id와 함께 사용하며 어떤 방식으로 컬럼의 값을 자동으로 생성할 때 사용한다. 위는 옵션을 지정하지 않았지만 자동 생성에 대한 방법을 옵션으로 작성할 수 있다.
- @Column의 옵션을 지정하지 않으면 Hibernate가 적절하게 자동으로 매핑해준다. 사용자 지정 옵션을 추가할 때, 아래는 컬럼 옵션들의 예시
Enum 타입의 컬럼 생성
@Entity
@Table(name = "orders") //테이블 이름 변경
@Getter @Setter
public class Order {
//...
@Enumerated(EnumType.STRING)
private OrderStatus status; //주문상태 ENUM
}
- ORDER/ CANCEL 두 가지 종류의 Enum 타입을 사용하는 컬럼을 생성하는 방법이다.
- EnumType은 디폴트 값이 EnumType.ORDINAL인데, 타입이 추가되면 기존 테이블들의 값이 밀리는 현상이 발생한다.
- 따라서 EnumType을 STRING으로 지정해주었다.
JPA 내장 타입 사용하기
@Embeddable //JPA 내장타입
@Getter
public class Address {
private String city;
private String street;
private String zipcode;
}
- JPA에서 제공하는 내장 타입을 컬럼으로 사용하기 위해 클래스를 생성하였다. Address와 같이 자주 사용되는 구조의 경우 JPA에서 내장 엔티티 타입으로서 제공한다.
- 내장 엔티티를 사용하기 위해서는 @Embeddable 어노테이션을 붙여준다.
@Entity
@Getter @Setter
public class Delivery {
//...
@Embedded
private Address address;
}
- 다른 엔티티에서 해당 내장 객체 타입의 컬럼을 사용하기 위해서는 @Embedded 어노테이션을 사용한다.
엔티티 클래스의 상속
Item이라는 클래스를 상속하는 Album, Book, Movie라는 엔티티를 만든다고 생각해보자.
Item은 부모 클래스이고, 나머지는 자식 클래스이다.
부모 클래스 작성
@Entity
@Getter @Setter
//상속 관계 테이블의 전략을 지정해주어야함
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) //한 테이블에 다 때려박는 Strategy 사용
@DiscriminatorColumn(name = "dtype") //싱글 테이블 구분방법 지정
public abstract class Item {
@Id
@GeneratedValue
@Column(name = "item_id")
private Long id;
private String name;
private int price;
private int stockQuantity;
}
- Abstract class로 작성된 것을 볼 수 있다. Item 클래스는 추상 클래스이며, Album, Book, Movie 타입이 실제로 엔티티 모델로서 사용될 것이다.
- @Inheritance에 상속 관계 테이블을 실제 모델로 구현할 때의 전략(Strategy)를 설정해준다.
- 위에서 선택한 방식은 SINGLE_TABLE로, 한 테이블에 모든 클래스들을 때려 넣는 방식이다.
- @DiscriminatorColumn는 하위 클래스를 구분하는 용도의 컬럼이다. default = DTYPE
자식 클래스 작성
@Entity
@Getter @Setter
@DiscriminatorValue("A") //싱글 테이블일 때 구분방법
public class Album extends Item{
private String artist;
private String etc;
}
@Entity
@Getter
@Setter
@DiscriminatorValue("B") //싱글 테이블일 때 구분방법
public class Book extends Item{
private String author;
private String isbn;
}
@Entity
@Getter
@Setter
@DiscriminatorValue("M") //싱글 테이블일 때 구분방법
public class Movie extends Item{
private String director;
private String actor;
}
- 자식 클래스에서는 구분방법을 지정했을 때의 DTYPE을 작성해주어야 한다.
- @DiscriminatorValue 어노테이션에 DTYPE을 작성한다.
실제 데이터베이스 매핑된 엔티티 모델 확인
싱글 테이블 전략으로 인해 한 테이블에 자식 테이블의 컬럼들이 모두 합쳐져 보여지는 것을 확인할 수 있다.
여기서 DTYPE을 통해 해당 레코드가 어떤 자식 엔티티의 타입을 구현하고 있는지 확인할 수 있다.
엔티티 관계 설정하기
이제 엔티티 모델간의 관계를 설정해 보려고 한다.
데이터베이스에서의 엔티티 관계에 대해서는 아래의 포스팅에서 기본적인 내용들을 서술해두었으니 참고하면 좋을 것 같다.
https://sjh9708.tistory.com/33
관계를 설정할 때에 아래에서 언급할 유의점은 일단 두 가지가 있다.
1. 연관 관계의 주인은 누구인가?
회원(Member) 엔티티와 팀(Team) 엔티티가 있을 때, 회원은 하나의 팀에 속하며, 팀은 여러 명의 회원을 가질 수 있습니다. 이러한 관계를 매핑하기 위해 JPA에서는 @ManyToOne, @OneToMany, @OneToOne, @ManyToMany와 같은 어노테이션을 제공한다.
이 때, 관계를 매핑하는 두 엔티티 중 하나를 "연관 관계의 주인"으로 지정해야 합니다. 이를 통해 연관 관계가 양방향인 경우, 어느 쪽에서나 연관 관계를 조작할 수 있습니다. 예를 들어, 위의 예시에서 회원과 팀은 양방향 관계이므로, 회원 엔티티와 팀 엔티티 모두에서 서로를 참조할 수 있습니다.
보통 양방향 관계에서는 두 엔티티 중에서 비즈니스적으로 더 중요한 쪽을 주인으로 설정하는 것이 좋습니다. 이유는 양방향 연관 관계를 사용하면 조회 성능이 저하될 가능성이 있기 때문입니다. 예를 들어, 위의 예시에서 회원과 팀이 양방향 연관 관계일 경우, 회원 엔티티에서 팀 엔티티를 참조하고 있기 때문에, 회원을 조회할 때 팀도 함께 조회됩니다. 이는 데이터베이스의 부하를 높일 수 있으므로, 비즈니스적으로 중요한 쪽을 주인으로 설정하여 조회 성능을 최적화하는 것이 좋습니다.
- ChatGPT
- 연관 관계에서의 주인은 Join을 통해 엑세스를 많이 할 것 같은 곳을 주체로 설정한다.
- 연관 관계의 주인 쪽에 다른 엔티티를 엑세스 할 수 있는 FK를 둔다.
- 1:N 관계에서의 주체는 N쪽이 주체이다.
- 1:1 관계나 N:M 관계에서는 비즈니스적으로 중요한 쪽을 주인으로 설정한다.
2. 연관 엔티티를 조회할 때에는 즉시 로딩을 사용하지 않고 지연 로딩을 사용한다.
- 즉시 로딩(EAGER)은 엔티티를 조회할 때 연관되어 있는 다른 엔티티까지 자동으로 로딩한다.
- 지연 로딩(LAZY)는 즉시 로딩과 반대로 실제로 사용하기 전 까지는 연관된 엔티티를 로딩하지 않는다.
- 즉시 로딩을 사용하게 되면 조회 쿼리 성능 등에 영향을 미칠 수 있으며, N+1 문제 등이 발생할 수 있으므로 지연 로딩을 사용하는 쪽을 권장한다.
1 : 1 관계 설정
@Entity
@Table(name = "orders")
@Getter @Setter
public class Order {
//..
//OneToOne도 디폴트가 EAGER이므로 지연 로딩으로 설정
//1:1 연관관계 주인으로 Order, 엑세스를 많이 하는 쪽을 주인으로 두는것이 좋음
@OneToOne(fetch=FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "delivery_id")
private Delivery delivery;
}
@Entity
@Getter @Setter
public class Delivery {
@Id @GeneratedValue
@Column(name = "delivery_id")
private Long id;
@OneToOne(fetch=FetchType.LAZY, mappedBy = "delivery")
private Order order;
//...
}
- 위의 코드들은 Order와 Delivery를 1:1 연관 매핑을 구현하였다.
- 각각의 엔티티들이 1:1 매칭이 되므로 @OneToOne 어노테이션을 사용한다.
- 연관 관계의 주인 쪽에 @JoinColumn을 지정하고, name에 Join시 사용될 키 이름을 지정한다.
- cascade 옵션을 지정할 수 있다.
- fetch = FetchType.LAZY로 지정하여 지연 로딩으로 사용하겠다고 작성한다.
- mappedBy는 연관 관계의 주인이 아닌 엔티티에서 연관 관계의 주인 엔티티의 필드를 지정하는 데에 사용한다.
1 : N 관계 설정
@Entity
@Table(name = "orders")
@Getter @Setter
public class Order {
@Id
@GeneratedValue
@Column(name = "order_id")
private Long id;
//...
@ManyToOne(fetch=FetchType.LAZY) //멤버 : 주문 = 1 : N,
@JoinColumn(name = "member_id")
private Member member;
}
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
@Column(name = "member_id") //PK 이름 지정
private Long id;
//...
@OneToMany(mappedBy = "member") //Order에서 member에 의해서 매핑된 미러임을 명시(연관관계의 주체가 아님)
private List<Order> orders = new ArrayList<>();
}
- 위의 코드들은 Order와 Member를 N:1 연관 매핑을 구현하였다.
- 연관 관계의 주인 쪽에 @JoinColumn을 지정하고, name에 Join시 사용될 키 이름을 지정한다.
- 다수(N)쪽에 @ManyToOne, 단일(1)쪽에 @OneToMany 어노테이션을 사용하여 연관관계를 지정한다.
- 다수쪽이 연관관계의 주인이 되어야 하므로 @JoinColumn을 사용하고, 단일쪽에는 mappedBy를 통해 주인 엔티티의 필드를 지정해주어 미러임을 명시해준다.
- ManyToOne은 디폴트가 즉시 로딩이므로 fetch = FetchType.LAZY로 지정하여 지연 로딩으로 사용하겠다고 작성한다.
- OneToMany는 디폴트가 지연 로딩이므로 지정해주지 않아도 된다.
N : M 관계 설정
@Entity
@Getter @Setter
public class Category {
@Id @GeneratedValue
@Column(name = "category_id")
private Long id;
//...
@ManyToMany
@JoinTable(name = "category_item",
joinColumns = @JoinColumn(name = "category_id"),
inverseJoinColumns = @JoinColumn(name = "item_id")
)
private List<Item> items = new ArrayList<>();
}
@Entity
@Getter @Setter
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "dtype")
public abstract class Item {
@Id
@GeneratedValue
@Column(name = "item_id")
private Long id;
//...
@ManyToMany(mappedBy = "items")
private List<Category> categories = new ArrayList<>();
}
- 위의 코드들은 Item과 Category를 N:M 연관 매핑을 구현하였다.
- 연관 관계의 주인 쪽에 @JoinTable을 지정한다.
- 연관관계의 주인 쪽에만 joinColumns으로 참조할 외래키를 지정하고, inverseJoinColums에는 반대쪽에서 참조할 나의 기본키이자 상대방 입장에서의 외래키를 지정해주면 된다.
- 연관관계의 주인이 아닌 쪽에는 마찬가지로 mappedBy를 통해 미러링할 주인 엔티티 필드를 지정해준다.
- @ManyToMany를 통해 N:M 관계를 형성한다는 것은 Hibernate에서 자동으로 제공하는 중간 테이블을 사용하겠다는 의미인데, 실제로는 두 개로만 연관관계가 이루어지지 않는 경우도 있고, 중간 테이블에 부가적인 컬럼을 추가해야 하는 경우가 있으므로 직접 중간 테이블에 해당하는 엔티티를 만들어 N:M 관계를 두 개의 1:N 관계로 표현하는 것이 좋다.
Self 관계 설정
@Entity
@Getter @Setter
public class Category {
@Id @GeneratedValue
@Column(name = "category_id")
private Long id;
private String name;
//...
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name = "parent_id")
private Category parent;
@OneToMany(mappedBy = "parent")
private List<Category> child = new ArrayList<>();
}
- 자기 자신과의 연관관계를 가지는 경우이다.
- 카테고리는 하위 카테고리를 가질 수도 있으며, 상위 카테고리를 가질 수도 있다. 하나의 상위 카테고리는 여러개의 하위 카테고리를 가진다.
- 다수(Child) 입장에서 단일(Parent)를 조회할 때의 경우가 연관관계의 주축이 되야 하므로 JoinColumn을 parent쪽에 두었다.
엔티티 모델의 생성자 추가
@Entity
@Table(name = "orders")
@Getter
public class Order {
@Id
@GeneratedValue
@Column(name = "order_id")
private Long id;
private String title;
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
@OneToOne(fetch=FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "delivery_id")
private Delivery delivery;
private LocalDateTime orderDate;
@Enumerated(EnumType.STRING)
private OrderStatus status;
}
- 앞에서 언급했지만, @Setter의 경우에는 필요한 부분에서만 Setter를 사용하여야 한다. 따라서 Lombok의 @Setter 대신 생성자, 메서드 등을 추가하여 별도의 로직을 통해서 구현하는 방법을 알아보려고 한다.
@Entity
@Table(name = "orders")
@Getter
public class Order {
//...
protected Order(){
}
//초기에 생성할 때만 값을 설정할 수 있게 하도록 생성자 생성
public Order(String title, LocalDateTime orderDate, OrderStatus status){
this.title = title;
this.orderDate = orderDate;
this.status = status;
}
}
- 기본 생성자와 인자를 받는 생성자를 모두 작성하였다. JPA에서 엔티티를 생성할 때 기본으로 사용하는 생성자가 있으나, 우리가 사용할 일은 없을 것이다. 그래도 JPA에서 사용할 수 있도록 기본 생성자를 명시해주자.
- 인자를 받는 생성자에 우리가 컬럼의 Value를 받아서 넣어주는 내용을 작성하여, Entity 객체 생성 시 값을 넣어줄 수 있다.
생성자에서 연관관계 설정하기
@Entity
@Table(name = "orders")
@Getter
public class Order {
//...
protected Order(){
}
public Order(Member member, String title, LocalDateTime orderDate, OrderStatus status){
//...
this.member = member;
member.getOrders().add(this);
}
}
- 다음과 같이 Order와 N : 1 관계를 가지고 있는 Member와의 연관관계를 생성자에서 설정해주는 방법이 있다.
- Order의 member는 생성자에서 받은 Member 엔티티로 설정해주며, 반대로 member의 Order들에 나 자신, this를 추가시켜주어 관계를 생성해준다.
연관관계 메서드 사용하기
@Entity
@Table(name = "orders")
@Getter
public class Order {
//...
protected Order(){
}
//초기에 생성할 때만 값을 설정할 수 있게 하도록 생성자 생성
public Order(String title, LocalDateTime orderDate, OrderStatus status){
this.title = title;
this.orderDate = orderDate;
this.status = status;
}
// == 연관관계 메서드 ==
public void setMamber(Member member){
this.member = member;
member.getOrders().add(this);
}
public void addOrderItem(OrderItem orderItem){
orderItems.add(orderItem);
orderItem.setOrder(this);
}
public void setDelivery(Delivery delivery){
this.delivery = delivery;
delivery.setOrder(this);
}
}
- 해당 방법은 생성자가 아닌 생성자 밖에 별도로 연관관계를 설정해주는 메서드를 작성하는 것이다.
- 한 쪽에서만의 관계를 설정해주는 것이 아니라 다른 쪽에서의 관계를 설정해 주는 것 까지를 유의하자.
- OrderItem, Delivery에서는 Setter를 부분적으로 사용하였다. 스타일에 따라 다르겠지만 연관관계의 컬럼들은 Setter를 부분적으로 사용하는 사람들도 꽤 있는 것 같다. 만약 Setter를 사용하고 싶지 않다면 별도의 메서드를 작성하는 것도 방법이겠다.
- 중요한 것은 @Setter를 사용하지 말아라가 아니라 필요한 부분에서만 쓰자, 생성자나 관계 메서드, 혹은 다른 의미있는 이름의 메서드로 대체할 수 있는 경우의 방향을 살펴보자
해당 포스팅은 본 강의 수강을 따라가면서 작성합니다.
'Backend > Spring' 카테고리의 다른 글
[Spring Boot/JPA] 영속성 컨텍스트와 준영속 컨텍스트 (0) | 2023.05.09 |
---|---|
[SpringBoot] Repository/Service/Controller 계층 개발 (0) | 2023.05.09 |
[SpringBoot] Repository와 EntityManager 의존성 주입 (0) | 2023.05.02 |
[SpringBoot] H2 데이터베이스 연동해보기 (0) | 2023.03.30 |
[SpringBoot] 프로젝트 환경설정 및 구조 알아보기 (0) | 2023.03.30 |