🎯 목표 : Spring DATA JPA에서의 연관관계 매핑
📒 엔티티의 연관 관계 매핑
📌 단방향과 양방향 관계
- 멤버와 포인트라는 엔티티가 있고, 멤버는 어떤 서비스를 제공 받음으로 포인트를 쌓을수 있는 경우를 가정했다.
- 객체에서의 방향성
- 위 그림에서 멤버와 포인트의 관계는 1:1 관계로 멤버에서 포인트 테이블을 외래 키로 참조하고 있으며, 객체 입장에서 생각했을때, 멤버의 객체에서는 포인트를 조회 할수 있어야 될 것이며, 포인트 객체에서는 맴버의 객체를 확인할 필요가 없을 것 이다. 물론 포인트 객체에서 멤버를 조회하는 기능이 필요하다면 양방향 관계를 맺어주어 객체의 입장에서도 조회할 수 있을 것 이다.
- 테이블에서의 방향성
- 객체의 입장에서는 멤버 객체에서 포인트를 조회 할수 있어야 하지만, 포인트 객체에서는 멤버 객체를 조회할 필요가 없을 수도 있다.
- 하지만, 테이블에서의 입장에서 봤을때, 두 객체의 관계가 서로 단방향, 양방향 관계 없이 멤버 테이블에서의 외래키로 포인트 테이블을 조인하여 데이터를 조회 할 수 있다.
- 즉, 테이블 에서는 단방향, 양방향의 개념이 존재하지 않는다.
- 정리하자면, 객체에서의 연관 관계는 필요에 따라 단방향이 될수 있고, 양방향이 될수 있다.
- A 객체와 B 객체의 관계를 양방향 관계로 설정한 경우, A 객체에서 B 객체를 참조하고 있고 참조하고 있는 B 객체에 대한 정보를 조회 할수 있을 것이며, 반대의 경우도 가능하다. (
A.getB()
또는 B.getA()
)
- 외래키를 사용하는 테이블에서는 객체의 참조 관계와 상관 없이 외래키 만으로 양방향 데이터 조회가 가능하다. (
A JOIN B
또는 B JOIN A
가 언제나 가능하다.)
- 객체에서의 관계 설정에 있어 양방향 관계와 단방향 관계의 필요성에 따라 연관 관계를 설정해야 할 것이다.
📌 객체 관계 매핑
[Database] 관계형 데이터베이스 설계
- DB 에서 정리한 내용중 관계형 데이터베이스에서 각 테이블간의 관계에 대한 내용을 정리한 적이 있다.
- 실제 JPA에서는 객체간 위 관계를 어떻게 적용하는지 예제를 만들어 학습 했다.
👉 예제의 목표
- 위 객체들을 서로간의 연관관계를 설정 해 줄 것이다. 아래와 같이 관계를 설정해 보았다.
- 각 객체들의 연관 관계를 설정하여 위 테이블과 같이 만들고자 한다.
📌 1:N & 1:1 관계 매핑
- 멤버와 오더의 관계는 1:N이다.
@OneToMany
- 하나의 오더가 생성되면, 해당 오더에서 어떤 멤버가 오더를 생성했는지 확인 해야 될 것이며, 해당 멤버가 어떤 오더들을 생성했는지 데이터를 조회 할 수 있어야 된다.
- 결국, 하나의 멤버는 여러개의 오더를 가질수 있다.
- 멤버와 포인트의 관계는 1:1이다.
@OneToOne
- 하나의 멤버가 생성되면, 해당 멤버는 하나의 포인트 엔티티를 참조하여 오더를 생성할때마다 적립 포인트가 쌓인다고 가정 하였다.
@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;
}
@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;
}
@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")
을 작성 해 주었다.
- 포인트의 객체에서 멤버를 호출하는 기능은 필요 없다고 생각하여 단방향 매핑으로 작성하였다.
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
@ManyToOne
@JoinColumn(name = "member_id")
private Member member;
- 양방향 매핑
- 위 분석 결과를 보면, 양방향 매핑과 단방향 매핑의 차이점을 볼수 있다.
- 단방향 매핑에서는 단지
@JoinColumn
을 사용하여 참조하고 있는 객체의 테이블 아이디를 에트리뷰트로 설정해 주기만 하였다.
- 하지만, 양방향 매핑에서는
@JoinColumn
과 더불어 관계를 가지고 있는 객체에서 mappedBy 에트리뷰트를 사용하여 외래키를 가지고 있는 테이블의 객체에 필드명을 지정 해 주었다.
- JPA에서 양방향 매핑을 설정 해 주기 위해서는, "외래키를 가지고 있지 않은 테이블의 객체"에서 "외래키를 가지고 있는 테이블의 객체"에 필드명을 mappedBy 에트리뷰트를 사용사용하여 매핑 해 줘야된다.
- 즉 양방향 매핑을 사용하기 위해서는 외래키를 가지고 있는 주인 객체를 설정 해 주어야 한다.
📌 N:N 관계 매핑
- 오더와 푸드의 관계는 N:N의 관계다.
- 하나의 오더에서 여러개의 푸드를 가질수 있어야되고, 하나의 푸드도 여러개의 오더가 있을수도 있다. 단순하게 객체의 입장에서 봤을때 서로 리스트만 가지고 있으면 된다고 생각할 수도 있다.
- 하지만, 테이블의 입장에서 봤을때 하나의 오더에서 여러개의 푸드 외래키가 있고 하나의 푸드에서 여러개의 오더 외래키가 있다고 한다면, 데이터가 많아졌을때 복잡도와 데이터 조회는 아주 어려울 것이다.
- 이것을 방지하기 위해 N:N의 관계는 서브테이블을 만들어 1:N, N:1의 관계로 풀어줘야된다.
@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<>();
}
@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;
}
@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를 조회해 보면 아래와 같은 테이블 컬럼이 생성 된다.
- 의도한 대로 정상적으로 매핑된 것을 확인할 수 있다.
- 단방향 매핑 만으로도 테이블과 객체의 연관관계 매핑은 완료 된 것이다.
- 양방향의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된 것 뿐이며, 서로 다른 단방향이 만나 양방향의 관계를 형성한다.
- 양방향의 관계를 형성 해 주기 위해서는 mappedBy 에트리뷰트로 외래키를 가지고 있는 테이블의 객체에 있는 참조 필드명을 지정 해 주어야 한다.
- 양방향 관계를 형성한 객체는 서로에게 참조값을 지정해 주기 위해 각 객체에서 또는 하나의 객체에서 양쪽 객체를 서로 관리 해 주어야한다.
- 예를 들면, 오더를 생성했을때 해당 오더를 생성한 멤버 객체를 오더에 셋팅 해주어야 하며, 오더를 생성한 멤버 객체에도 생성된 오더를 리스트에 셋팅 해 주어야 한다.
- 이는 관계 매핑시에 편리하게 사용할수 있는 메소드를 따로 지정하여 관리하면 되겠다.