[SpringBoot] Entity 클래스 개발하기

2023. 4. 6. 20:20
반응형

엔티티 클래스

 

@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

 

[NestJS] - 8. 데이터베이스 관계와 TypeORM 데이터베이스 관계 매핑

이전 포스팅에서 ORM을 통해서 데이터베이스의 테이블을 생성해 보았는데, 이번에는 TypeORM을 통해 테이블 간의 관계 설정하는 방법을 포스팅하려고 한다. 데이터베이스 관계 데이터베이스의 관

sjh9708.tistory.com

 

 

 

 

관계를 설정할 때에 아래에서 언급할 유의점은 일단 두 가지가 있다.

 

 

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를 사용하지 말아라가 아니라 필요한 부분에서만 쓰자, 생성자나 관계 메서드, 혹은 다른 의미있는 이름의 메서드로 대체할 수 있는 경우의 방향을 살펴보자

 

 

 


https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-JPA-%ED%99%9C%EC%9A%A9-1

 

실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 인프런 | 강의

실무에 가까운 예제로, 스프링 부트와 JPA를 활용해서 웹 애플리케이션을 설계하고 개발합니다. 이 과정을 통해 스프링 부트와 JPA를 실무에서 어떻게 활용해야 하는지 이해할 수 있습니다., - 강

www.inflearn.com

 

해당 포스팅은 본 강의 수강을 따라가면서 작성합니다.

반응형

BELATED ARTICLES

more