[헤이동동 #03] JPA 엔티티 클래스 매핑

Jiwoo Kim·2020년 12월 2일
0
post-thumbnail

☕헤이동동 : 생협 음료 원격 주문 서비스

이번 포스팅에서는 Spring Data JPA를 사용해 엔티티와 클래스를 매핑하는 과정에 대해 설명한다.


Class Diagram

지난 포스팅에도 업로드했지만, 클래스 구조를 다시 한 번 살펴보자.

보다시피 엔티티들은 단방향 참조, 양방향 참조, 1:N, N:1 관계로 이루어져 있다. 크게 Value Type과 Entity로 구현 방법이 크게 달라지기 때문에 차례로 살펴보려 한다.

+) 기본적인 JPA 지식은 JPA 마스터하기 시리즈에서 참고할 수 있다.


Value Type

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;
	
    //...
}

Entity

DB 테이블과 매핑하는 클래스를 엔티티라고 한다.

엔티티는 기본적으로 @Entity 어노테이션을 적어 스프링 부트가 엔티티 빈으로 등록할 수 있도록 하고, @Table 어노테이션에 테이블 이름을 명시하여 그 이름의 테이블과 매핑될 수 있도록 한다.

테이블의 컬럼은 @Column 어노테이션을 통해 매핑할 수 있다. 만약 name을 별도로 지정하지 않으면 디폴트값인 필드명으로 컬럼을 검색하게 된다. 또한, @Id 어노테이션을 필수로 하나의 필드에 적용해서 PK임을 명시해야 한다.

여기까지는 테이블만 보고도 큰 어려움 없이 작성할 수 있다. 하지만 엔티티 간 연관관계를 설정하는 것이 엔티티 매핑의 진정한 존재 이유라고 할 수 있다. 헤이동동에 구현한 몇 가지 엔티티들을 통해 연관관계 매핑에 대해 설명하도록 하겠다.

menus 테이블을 보면, 각 메뉴는 category_idstore_id를 통해 각각 categoriesstores 테이블을 참조하고 있음을 알 수 있다. 따라서 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는 자동으로 그 컬럼과 해당 필드 엔티티(여기서는 CategoryStore)의 PK를 조인하여 쿼리를 날리고 데이터를 불러 온다. 이로써 서버에서는 menu.getCategory(), menu.getStore() 등의 객체 탐색이 가능해진다.

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_ordersorder 객체 탐색보다는 ordermenus_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를 사용하여 실제 서비스를 운영함으로써 문제를 해결할 수 있다는 점을 새로 배울 수 있었다. 책에는 이런 부분까지 나와 있지는 않았던 것으로 기억하는데, 직접 코드를 짜면서 좋은 경험을 또 한 번 쌓은 것 같다.


전체 코드는 Github에서 확인하실 수 있습니다.

0개의 댓글