이번 포스팅에서는 Spring Data JPA를 사용해 엔티티와 클래스를 매핑하는 과정에 대해 설명한다.
지난 포스팅에도 업로드했지만, 클래스 구조를 다시 한 번 살펴보자.
보다시피 엔티티들은 단방향 참조, 양방향 참조, 1:N, N:1 관계로 이루어져 있다. 크게 Value Type과 Entity로 구현 방법이 크게 달라지기 때문에 차례로 살펴보려 한다.
+) 기본적인 JPA 지식은 JPA 마스터하기 시리즈에서 참고할 수 있다.
Value Type은 @Embedded
, @Embeddable
어노테이션을 사용하여 적용할 수 있다.
물론 DB 테이블에 있는 것처럼 그대로 모든 필드를 클래스 멤버 변수로 선언해도 가능은 하다. 하지만 클래스로 추출할 수 있는 것은 최대한 추출하여 보기 좋게 정리하고 클래스 크기를 최소화하는 것이 좋은 코드를 만드는 중요한 요소라는 것을 클린 코드를 읽으며 배웠다. 그렇기에, Value Type으로 선언할 수 있는 것은 해주고 넘어가도록 하였다.
생성 방식은 모두 동일하기 때문에 Value Type 중 하나의 예시만 서술했다.
Position
Value Type이 될 클래스를 생성하고 @Embeddable
어노테이션을 적용한다. 그러면 JPA가 엔티티 매핑을 하면서 해당 객체를 생성하고 필드에 값을 할당하는 것까지 알아서 해준다.
매핑할 컬럼은 @Column
어노테이션을 적용하여 원하는 컬럼과 필드가 매핑될 수 있도록 한다.
@Embeddable
@NoArgsConstructor
@ToString
public class Position {
@Column(name = "latitude", nullable = false)
private Double latitude;
@Column(name = "longitude", nullable = false)
private Double longitude;
}
Store
엔티티에서는 멤버 변수 Position
에 @Embedded
어노테이션만 적용하면 된다.
@Entity
@Table(name = "stores")
@Getter
@NoArgsConstructor
public class Store {
@Id
@Column(name = "id", nullable = false)
private Integer storeId;
@Embedded
private Position position;
//...
}
DB 테이블과 매핑하는 클래스를 엔티티
라고 한다.
엔티티는 기본적으로 @Entity
어노테이션을 적어 스프링 부트가 엔티티 빈으로 등록할 수 있도록 하고, @Table
어노테이션에 테이블 이름을 명시하여 그 이름의 테이블과 매핑될 수 있도록 한다.
테이블의 컬럼은 @Column
어노테이션을 통해 매핑할 수 있다. 만약 name
을 별도로 지정하지 않으면 디폴트값인 필드명으로 컬럼을 검색하게 된다. 또한, @Id
어노테이션을 필수로 하나의 필드에 적용해서 PK임을 명시해야 한다.
여기까지는 테이블만 보고도 큰 어려움 없이 작성할 수 있다. 하지만 엔티티 간 연관관계를 설정하는 것이 엔티티 매핑의 진정한 존재 이유라고 할 수 있다. 헤이동동에 구현한 몇 가지 엔티티들을 통해 연관관계 매핑에 대해 설명하도록 하겠다.
Menu
menus
테이블을 보면, 각 메뉴는 category_id
와 store_id
를 통해 각각 categories
와 stores
테이블을 참조하고 있음을 알 수 있다. 따라서 Menu
엔티티가 N, 그리고 상대 엔티티가 1인 N:1 관계를 설정해야 했다.
@Entity
@Table(name = "menus")
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Menu {
@Id
@Column(name = "id")
private Integer menuId;
@Column(name = "name", nullable = false)
private String menuName;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id", nullable = false)
private Category category;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "store_id", nullable = false)
private Store store;
@Embedded
private Price price;
@Column(name = "img_url", nullable = false)
private String imgUrl;
}
우선 @ManyToOne
어노테이션으로 LazyLoading N:1 관계라는 것을 명시하고, @JoinColumn
어노테이션을 통해 어떤 컬럼으로 조인 관계를 맺는지 JPA에게 전달한다. name
에는 현재 엔티티 테이블의 실제 조인 컬럼명을 적으면 된다.
이를 바탕으로 JPA는 자동으로 그 컬럼과 해당 필드 엔티티(여기서는 Category
와 Store
)의 PK를 조인하여 쿼리를 날리고 데이터를 불러 온다. 이로써 서버에서는 menu.getCategory()
, menu.getStore()
등의 객체 탐색이 가능해진다.
MenuInOrder
menus_in_orers
테이블도 FK를 통해 menus
테이블과 orders
테이블을 참조하고 있다. 이러한 연관관계는 아래와 같은 코드를 통해 매핑될 수 있다.
@Entity
@Table(name = "menus_in_orders")
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MenuInOrder {
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "count")
private Integer count;
@Column(name = "price")
private Integer price;
@Embedded
private Option option;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "menu_id")
private Menu menu;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;
}
하지만 menus_in_orders
→order
객체 탐색보다는 order
→menus_in_orders
방향으로 조회하는 시나리오가 더 많다. 이러한 기능을 위해서는 order
테이블에서도 menus_in_orders
필드 객체를 조회할 수 있도록 양방향 연관관계 매핑을 해주어야 한다.
+) Option
매핑은 다음 포스팅에서 자세하게 설명하였다.
Order
@Entity
@Table(name = "orders")
@Getter
@NoArgsConstructor
public class Order {
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long orderId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "store_id")
private Store store;
@Column(name = "order_at")
private Timestamp orderAt;
@Setter
@Column(name = "progress")
@Enumerated(EnumType.STRING)
private Progress progress;
//...
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<MenuInOrder> menus = new ArrayList<>();
@Builder
public Order(Long orderId, User user, Store store, Timestamp orderAt, Progress progress, Integer totalCount, Integer totalPrice, Boolean isNoShow) {
Assert.notNull(user, "User must not be null");
Assert.notNull(store, "Store must not be null");
Assert.notNull(orderAt, "OrderAt must not be null");
//...
}
}
위 코드의 @OneToMany
어노테이션이 양방향 연관관계 매핑을 위해 필요한 부분이다. 이로써 order.getMenus().getId()
등의 객체 탐색이 가능해졌다.
JPA 책을 통해 익혔던 지식을 실제 프로젝트에서 엔티티 매핑을 하면서 적용할 수 있어서 아주 재밌었다. 처음이라 어색하고 책을 계속 참고하면서 코드를 썼는데 그러면서 책도 다시 한 번 가볍게 훑고 이해하고 넘어갈 수 있었던 것 같다.
또한, 양방향 참조 매핑을 할 때는 항상 무한 참조로 인한 StackOverflow 에러를 고려해야 하며, DTO를 사용하여 실제 서비스를 운영함으로써 문제를 해결할 수 있다는 점을 새로 배울 수 있었다. 책에는 이런 부분까지 나와 있지는 않았던 것으로 기억하는데, 직접 코드를 짜면서 좋은 경험을 또 한 번 쌓은 것 같다.