[Spring] JPA Entity Relation Mapping

Gogh·2023년 1월 2일
0

Spring

목록 보기
15/23

🎯 목표 : Spring DATA JPA에서의 연관관계 매핑

📒 엔티티의 연관 관계 매핑


📌 단방향과 양방향 관계

image

  • 멤버와 포인트라는 엔티티가 있고, 멤버는 어떤 서비스를 제공 받음으로 포인트를 쌓을수 있는 경우를 가정했다.
  • 객체에서의 방향성
    • 위 그림에서 멤버와 포인트의 관계는 1:1 관계로 멤버에서 포인트 테이블을 외래 키로 참조하고 있으며, 객체 입장에서 생각했을때, 멤버의 객체에서는 포인트를 조회 할수 있어야 될 것이며, 포인트 객체에서는 맴버의 객체를 확인할 필요가 없을 것 이다. 물론 포인트 객체에서 멤버를 조회하는 기능이 필요하다면 양방향 관계를 맺어주어 객체의 입장에서도 조회할 수 있을 것 이다.
  • 테이블에서의 방향성
    • 객체의 입장에서는 멤버 객체에서 포인트를 조회 할수 있어야 하지만, 포인트 객체에서는 멤버 객체를 조회할 필요가 없을 수도 있다.
    • 하지만, 테이블에서의 입장에서 봤을때, 두 객체의 관계가 서로 단방향, 양방향 관계 없이 멤버 테이블에서의 외래키로 포인트 테이블을 조인하여 데이터를 조회 할 수 있다.
    • 즉, 테이블 에서는 단방향, 양방향의 개념이 존재하지 않는다.
  • 정리하자면, 객체에서의 연관 관계는 필요에 따라 단방향이 될수 있고, 양방향이 될수 있다.
  • A 객체와 B 객체의 관계를 양방향 관계로 설정한 경우, A 객체에서 B 객체를 참조하고 있고 참조하고 있는 B 객체에 대한 정보를 조회 할수 있을 것이며, 반대의 경우도 가능하다. (A.getB()또는 B.getA())
  • 외래키를 사용하는 테이블에서는 객체의 참조 관계와 상관 없이 외래키 만으로 양방향 데이터 조회가 가능하다. (A JOIN B 또는 B JOIN A가 언제나 가능하다.)
  • 객체에서의 관계 설정에 있어 양방향 관계와 단방향 관계의 필요성에 따라 연관 관계를 설정해야 할 것이다.

📌 객체 관계 매핑

[Database] 관계형 데이터베이스 설계

  • DB 에서 정리한 내용중 관계형 데이터베이스에서 각 테이블간의 관계에 대한 내용을 정리한 적이 있다.
  • 실제 JPA에서는 객체간 위 관계를 어떻게 적용하는지 예제를 만들어 학습 했다.

👉 예제의 목표

image

  • 위 객체들을 서로간의 연관관계를 설정 해 줄 것이다. 아래와 같이 관계를 설정해 보았다.

매핑관계 결과

  • 각 객체들의 연관 관계를 설정하여 위 테이블과 같이 만들고자 한다.

📌 1:N & 1:1 관계 매핑

  • 멤버와 오더의 관계는 1:N이다. @OneToMany
  • 하나의 오더가 생성되면, 해당 오더에서 어떤 멤버가 오더를 생성했는지 확인 해야 될 것이며, 해당 멤버가 어떤 오더들을 생성했는지 데이터를 조회 할 수 있어야 된다.
  • 결국, 하나의 멤버는 여러개의 오더를 가질수 있다.
  • 멤버와 포인트의 관계는 1:1이다. @OneToOne
  • 하나의 멤버가 생성되면, 해당 멤버는 하나의 포인트 엔티티를 참조하여 오더를 생성할때마다 적립 포인트가 쌓인다고 가정 하였다.
// Member.java
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Member extends Audit{

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long memberId;
  @Column(nullable = false)
  private String name;

  @Column(nullable = false)
  private String email;

  @Column(nullable = false)
  private String phone;

  @OneToMany(mappedBy = "member")
  private List<Order> orders = new ArrayList<>();

  @OneToOne(cascade = CascadeType.ALL)
  @JoinColumn(name = "point_id")
  private Point point;

}

// Order.java
@Entity(name = "ORDERS")
@Getter
@Setter
@NoArgsConstructor
public class Order extends Audit{

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long orderId;

  @ManyToOne
  @JoinColumn(name = "member_id")
  private Member member;

}
// Point.java
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Point extends Audit{

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long pointId;

  private int pointCount;
}
  • 멤버와 오더의 관계 매핑을 먼저 분석해 보자.
    • (멤버 : 오더xN)의 관계로 멤버에서는 @OneToMany 어노테이션을 사용하여 오더객체의 리스트를 가지도록 하였다. "하나의 멤버가 여러개의 오더를 가질수 있다" 라는 말을 멤버의 객체에서는 리스트로 표현을 한 것이다.
    • (오더xN : 멤버)의 관계에서 오더는 멤버 객체를 참조하고 있다. 하나의 오더는 하나의 멤버밖에 가지지 않는다 하지만, 오더는 여러개가 될수 있고 여러개의 오더가 하나의 맴버를 참조할수도 있는 것 이다.
      • 그것을 표현한 것이 @ManyToOne 어노테이션이다. 오더 객체 입장에서 멤버는 N:1에 해당하는 객체로 생각하면 되겠다.
  • 멤버와 오더 관계 매핑에서 추가적인 어노테이션과 에트리뷰트를 살펴 보자.
    • @JoinClumn은 외래키를 갖고있는 테이블의 객체(엔티티)에 설정해준다.
      • 오더 객체에서 멤버를 참조하고 있고 @JoinColumn(name = "member_id")을 사용하여 참조하고 있는 객체의 테이블 ID 컬럼 명을 에트리뷰트로 설정해 주었다.
      • 테이블에서 확인 해 보면, MEMBER 테이블은 외래키로 ORDERS 테이블을 참조하지 않고 있고, ORDERS 테이블 에서만 MEMBER 테이블을 외래키로 참조하고 있다.
      • 하지만, 멤버 객체에 오더 객체의 리스트를 필드멤버로 가지고 있다.
      • 이 관계를 완벽히 설정 해 주려면 멤버의 객체에서 @OneToMany(mappedBy = "member") mappedBy 에트리뷰트를 설정 해 주어 해당 리스트는 오더 객체에 있는 멤버를 매핑해 주고 있다고 알려 주어야 한다.
        • 여기서, mappedBy의 대상은 외래키를 가지고 있는 테이블의 객체에 존재하는 필드명을 대상(Order 클래스의 member 필드)으로 작성해야한다. 연관 관계에서의 외래키를 가지고 있는 주인을 설정해준다고 생각하면 되겠다.
      • 이 관계는 양방향 1:N의 관계다.
      • 만약, 멤버의 객체에서 오더 리스트를 필드로 가지고 있지 않더라도 DB의 테이블에서의 연관 관계는 정상적으로 맺어진 것을 볼수 있다. 이 경우 단방향 1:N의 경우로 볼수 있겠다.
      • 예제에서는 하나의 멤버를 조회해서 그 멤버가 가지고있는 주문 이력들을 볼수 있다는 가정을 해 두었기 때문에 양방향으로 매핑 하였다. Member.getOrders()의 기능을 사용하기 위해 양방향으로 매핑했다고 생각하면 되겠다.
  • 멤버와 포인트의 관계 매핑을 분석해 보자.
    • 멤버와 포인트의 관계는 1:1의 관계로 @OneToOne 어노테이션을 멤버에서만 포인트 객체를 참조하도록 매핑해 주었다.
    • 참조하고 있는것을 설정하기 위해 @JoinColumn(name = "point_id")을 작성 해 주었다.
    • 포인트의 객체에서 멤버를 호출하는 기능은 필요 없다고 생각하여 단방향 매핑으로 작성하였다.
//Member.java
  @OneToMany(mappedBy = "member")
  private List<Order> orders = new ArrayList<>();

//Order.java
  @ManyToOne
  @JoinColumn(name = "member_id")
  private Member member;
  • 양방향 매핑
    • 위 분석 결과를 보면, 양방향 매핑과 단방향 매핑의 차이점을 볼수 있다.
    • 단방향 매핑에서는 단지 @JoinColumn을 사용하여 참조하고 있는 객체의 테이블 아이디를 에트리뷰트로 설정해 주기만 하였다.
    • 하지만, 양방향 매핑에서는 @JoinColumn과 더불어 관계를 가지고 있는 객체에서 mappedBy 에트리뷰트를 사용하여 외래키를 가지고 있는 테이블의 객체에 필드명을 지정 해 주었다.
    • JPA에서 양방향 매핑을 설정 해 주기 위해서는, "외래키를 가지고 있지 않은 테이블의 객체"에서 "외래키를 가지고 있는 테이블의 객체"에 필드명을 mappedBy 에트리뷰트를 사용사용하여 매핑 해 줘야된다.
    • 즉 양방향 매핑을 사용하기 위해서는 외래키를 가지고 있는 주인 객체를 설정 해 주어야 한다.

📌 N:N 관계 매핑

  • 오더와 푸드의 관계는 N:N의 관계다.
  • 하나의 오더에서 여러개의 푸드를 가질수 있어야되고, 하나의 푸드도 여러개의 오더가 있을수도 있다. 단순하게 객체의 입장에서 봤을때 서로 리스트만 가지고 있으면 된다고 생각할 수도 있다.
  • 하지만, 테이블의 입장에서 봤을때 하나의 오더에서 여러개의 푸드 외래키가 있고 하나의 푸드에서 여러개의 오더 외래키가 있다고 한다면, 데이터가 많아졌을때 복잡도와 데이터 조회는 아주 어려울 것이다.
  • 이것을 방지하기 위해 N:N의 관계는 서브테이블을 만들어 1:N, N:1의 관계로 풀어줘야된다.
// Order.java
@Entity(name = "ORDERS")
@Getter
@Setter
@NoArgsConstructor
public class Order extends Audit{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long orderId;

    @ManyToOne
    @JoinColumn(name = "member_id")
    private Member member;

    @OneToMany(mappedBy = "order")
    private List<OrderFood> orderFoods = new ArrayList<>();
}

// OrderFood.java
@Entity
@Getter
@Setter
@NoArgsConstructor
public class OrderFood extends Audit{

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long orderFoodId;

  private int quantity;

  @ManyToOne
  @JoinColumn(name = "order_id")
  private Order order;

  @ManyToOne
  @JoinColumn(name = "food_id")
  private Food food;

}

// Food.java
@Entity
@Getter
@Setter
@NoArgsConstructor
public abstract class Food extends Audit{

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long foodId;

  @Column(nullable = false)
  private String name;

  @Column(nullable = false)
  private int price;

}
  • 오더와 푸드의 연관 관계 매핑을 분석 해 보자.
    • 1:N과 N:1의 관계로 풀어주기 위해 OrderFood.java를 추가 했다.
    • 오더와 오더푸드의 관계를 보면 위 예제에서 다룬 1:N의 관계 즉, 멤버와 오더의 관계와 동일하다.
    • 결국 오더푸드와 푸드의 관계를 봐도 멤버와 오더의 관계와 동일할 것이다.
    • 확인해야할 점은, 오더푸드라는 새로운 객체를 만들어 서브테이블로 지정해 주었고, 오더푸드 객체에서는 오더 객체와, 푸드 객체를 @JoinColumn으로 참조 하고 있으며,
    • 오더푸드와 푸드의 관계는 단방향으로 매핑을 하였다. 푸드 객체에 오더푸드를 참조하고 있는 리스트가 없다는 말과 동일하다.
    • 푸드를 통해 오더를 조회하는건 의미 없다는 가정하에 단방향 매핑을 하였다.
    • 위 예제에서는 외래키를 가지고 있는 외래키 주인 테이블은 ORDER_FOOD가 될 것이다.

📌 @MappedSuperClass

  • 예제의 클래스들을 살펴보면, 하나같이 동일한 클래스를 상속 받고 있다.
  • 제일 위 테이블 다이어그램을 확인해 보면 컬럼으로 created_at, last_modified_at을 가지고 있다.
  • 중복 코드를 방지하기 위해 @MappedSuperClass를 사용하였다.
  • 아래와 같이 클래스를 만들어 해당 필드를 필요로하는 클래스에 상속을 해주면, 테이블에서 @MappedSuperClass 클래스의 필드를 가지고 있는 것과 동일한 효과를 가진다.
@Getter
@MappedSuperclass
public abstract class Audit {
    @CreatedDate
    @Column(name = "created_at", updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    @Column(name = "LAST_MODIFIED_AT")
    private LocalDateTime modifiedAt;
}

📌 정리

  • 위 예제의 DB를 조회해 보면 아래와 같은 테이블 컬럼이 생성 된다.

image

  • 의도한 대로 정상적으로 매핑된 것을 확인할 수 있다.
  • 단방향 매핑 만으로도 테이블과 객체의 연관관계 매핑은 완료 된 것이다.
  • 양방향의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된 것 뿐이며, 서로 다른 단방향이 만나 양방향의 관계를 형성한다.
  • 양방향의 관계를 형성 해 주기 위해서는 mappedBy 에트리뷰트로 외래키를 가지고 있는 테이블의 객체에 있는 참조 필드명을 지정 해 주어야 한다.
  • 양방향 관계를 형성한 객체는 서로에게 참조값을 지정해 주기 위해 각 객체에서 또는 하나의 객체에서 양쪽 객체를 서로 관리 해 주어야한다.
    • 예를 들면, 오더를 생성했을때 해당 오더를 생성한 멤버 객체를 오더에 셋팅 해주어야 하며, 오더를 생성한 멤버 객체에도 생성된 오더를 리스트에 셋팅 해 주어야 한다.
    • 이는 관계 매핑시에 편리하게 사용할수 있는 메소드를 따로 지정하여 관리하면 되겠다.
profile
컴퓨터가 할일은 컴퓨터가

0개의 댓글