자바 ORM 표준 JPA 프로그래밍 섹션5. 연관관계 매핑 기초
기본 용어
1. 방향 (Direction) : 단방향, 양방향
2. 다중성 (Multiplicity) : 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M)
3. 연관관계의 주인 (Owner) : 객체 양방향 연관관계는 관리 주인 필요
테이블에 맞춘 모델링
예제 시나리오
- 회원과 팀이 있다.
- 회원은 하나의 팀에만 소속될 수 있다.
- 회원과 팀은 다대일 관계다.
객체를 테이블에 맞추어 모델링할 경우
테이블에 맞춘 모델링의 경우, 객체와 테이블 연관관계는 아래와 같다.
객체를 테이블에 맞추어 모델링할 경우 참조 대신에 외래 키를 그대로 사용해야 하므로 Entity 관련 코드는 아래와 같다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@Column(name = "TEAM_ID")
private Long teamId;
…
}
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
…
}
Team과 Member 저장은 아래와 같이 할 수 있다.
//팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
//회원 저장
Member member = new Member();
member.setName("member1");
member.setTeamId(team.getId());
em.persist(member);
Member가 속한 Team을 찾고자 할 때에는 아래와 같이 찾아야 한다.
Member findMember = em.find(Member.class, member.getId());
Long findTeamId = findMember.getTeamId();
Team findTeam = em.find(Team.class, findTeamId);
참조 대신 외래키를 그대로 사용해야 하므로 외래키 식별자를 직접 다루게 된다. 따라서 이는 객체 지향적인 방법이라고 할 수 없다.
테이블은 외래 키로 조인을 사용하여 연관된 테이블을 찾아야 한다. 그러나 객체는 참조를 사용하여 연관된 객체를 찾는다. 따라서 테이블과 객체 사이에는 이런 큰 간격이 존재한다. 따라서 객체를 테이블에 맞추어 데이터 중심으로 모델링 하면, 협력 관계를 만들 수 없다.
단방향 연관관계
단방향 연관관계의 객체와 테이블 연관관계는 아래와 같다.
객체 연관관계를 사용하여 객체 지향 모델링을 하게 된다면 아래와 같이 Entity 코드를 작성할 수 있다. 관계와 조인하는 컬럼 값만 어노테이션으로 입력해주면 된다.
@Entity
public class Member {
@Id @GeneratedValue
@Column(name="MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne
@JoinColumn(name="TEAM_ID")
private Team team;
...
}
Team과 Member 저장은 아래와 같이 할 수 있다.
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
member.setTeam(team);
em.persist(member);
이후 Team 조회는 아래와 같이 할 수 있다. Member.getTeamId() 없이 바로 Team 조회가 가능하다.
Member findMember = em.find(Member.class, member.getId());
Team findTeam = findMember.getTeam();
위가 단방향 연관관계인 이유는 Member로 Team 조회는 가능하지만, Team으로 Member 조회는 불가능하기 때문이다.
양방향 연관관계
위와 다르게 양쪽으로 참조할 수 있는 것을 양방향 연관관계라고 한다.
양방향 연관관계의 객체와 테이블 연관관계는 아래와 같다.
테이블의 연관관계에서는 Member의 입장에서 자신이 소속되어 있는 Team을 알고 싶으면 MEMBER의 TEAM_ID와 TEAM의 TEAM_ID를 조인하여 알 수 있다. 반대로 Team의 입장에서 우리 팀에 어떤 Member들이 소속되어 있는지 알고 싶으면 TEAM의 TEAM_ID와 MEMBER의 TEAM_ID와 조인하여 알 수 있다. 여기서 포인트는 테이블의 연관관계가 외래키 하나로 양방향이 모두 있다는 것이다.
하지만 객체의 연관관계에서는 Member가 Team을 가지고 있기에 Member 입장에서 Team 조회가 가능하다. 또한 Team도 Member 리스트를 가지고 있기에 Team 입장에서 Member 조회가 가능하다.
아래와 같이 코드를 작성할 수 있다.
@Entity
public class Team {
@Id @GeneratedValue
@Column(name="TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team") //Member의 team과 연결, Team의 반대편에 걸려 있는 것을 표기
private List<Member> members = new ArrayList<>();
...
}
그럼 참조를 사용하여 연관관계를 조회하여 Team의 Member들을 조회할 수 있다.
Member findMember = em.find(Member.class, member.getId());
findMember.getTeam().getMembers();
이 반대방향으로도 객체 그래프를 탐색할 수 있다.
양방향 매핑 시 가장 많이 하는 실수
연관관계 주인의 값을 입력하지 않는 것이다.
아래와 같이 코드를 작성하면 연관관계의 주인은 Team이고 members는 mappedBy 되어 읽기 전용이기 때문에 TeamA의 members에 member1이 추가되지 않는다.
Member member = new Member();
member.setName("member1");
em.persist(member);
Team team = new Team();
team.setName("TeamA");
team.getMembers().add(member);
em.persist(team);
tx.commit();
연관관계의 주인과 mappedBy
객체와 테이블이 관계를 맺는 차이
- 객체의 연관관계 : 단방향이 2개
- 회원 → 팀 연관관계 1개 (단방향)
- 팀 → 회원 연관관계 1개 (단방향)
- 참조가 각각에 있어야 하기 때문에 단방향 연관관계 2개이다.
- 테이블의 연관관계 : 양방향이 1개
- 회원 ↔ 팀 연관관계 1개 (양방향)
- FK와 PK의 조인으로 서로 조회 가능하기 때문에 양방향 연관관계 1개이다.
객체의 양방향 연관관계
즉 객체의 양방향 연관관계는 실제 양방향 관계가 아니라 서로 다른 단방향 관계 2개이다. 따라서 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.
class A {
B b;
}
class B {
A a;
}
테이블의 양방향 연관관계
반면에 테이블의 양방향 연관관계는 외래키 하나로 두 테이블의 완관관계를 관리할 수 있다. MEMBER.TEAM_ID 외래키 하나로 양방향 연관관계를 가질 수 있다. 양쪽으로 조인할 수 있다는 뜻이다.
SELECT *
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
SELECT *
FROM TEAM T
JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID
연관관계의 주인(Owner)
Member의 team으로 외래키를 관리할지, Team의 members로 외래키를 관리할지 정해야 하기 때문에 이 과정에서 연관관계의 주인이 생긴다.
양방향 매핑 규칙
객체의 두 관계 중 하나를 연관관곌의 주인으로 지정해야 한다. 예를 들면 Member의 team이 주인이 될지, Team의 members가 주인이 될지 정해야 한다. 연관관계의 주인만이 외래키를 관리(등록/수정)할 수 있다. 주인이 아닌 쪽은 읽기만 가능하다. 주인은 mappedBy 속성을 사용하지 않는다. 주인이 아니면 mappedBy 속성으로 주인을 지정해야 한다.
연관관계의 주인을 정하는 방법
연관관계의 주인은 비즈니스적으로 중요한 것이 주인이 되는 것이 아니다. 외래키가 있는 곳을 주인으로 정하면 된다. DB의 n(다) 쪽이 연관관계의 주인이 된다. 즉 위의 예제에서는 Member.team이 연관관계의 주인이 된다. 이래야 엔티티와 테이블이 매핑 되어 있는 곳에서 직관적으로 연관관계가 관리가 된다.
만약 Team의 members를 주인으로 정하게 되면 members의 값을 바꿀 때 Team이 아닌 MEMBER에 업데이트 쿼리문이 나가기 때문에 설계상 부적절하다. 또한 성능적인 이슈도 있다.
양방향 연관관계와 연관관계의 주인
양방향 매핑 시 가장 많이 하는 실수
연관관계 주인에 값을 입력하지 않는 것이다.
아래와 같이 코드를 작성하면 연관관계의 주인은 Team이고 members는 mappedBy 되어 읽기 전용이기 때문에 TeamA의 members에 member1이 추가되지 않는다.
Member member = new Member();
member.setName("member1");
em.persist(member);
Team team = new Team();
team.setName("TeamA");
team.getMembers().add(member);
em.persist(team);
tx.commit();
따라서 아래와 같이 해야 TeamA에 member1을 추가할 수 있다.
Team team = new Team();
team.setName("TeamA");
//team.getMembers().add(member);
em.persist(team);
Member member = new Member();
member.setName("member1");
member.setTeam(team);
em.persist(member);
하지만 순수한 객체 관계를 고려한다면 아래와 같이 항상 양쪽 모두 값을 입력해주어야 한다.
Team team = new Team();
team.setName("TeamA");
//team.getMembers().add(member);
em.persist(team);
Member member = new Member();
member.setName("member1");
team.getMembers().add(member);
member.setTeam(team);
em.persist(member);
양쪽에 값을 설정하는 것을 놓칠 수 있기 때문에 연관 관계 편의 메소드를 만들면 아래 코드를 일일히 작성하지 않아도 된다.
team.getMembers().add(member);
연관 관계 편의 메소드는 Member 쪽에 아래와 같이 만들 수 있다.
public void setTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
혹은 Member 쪽에 만드려면 아래와 같이 만들 수 있다.
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
em.persist(member);
team.addMember(member);
tx.commit();
public void addMember(Member member) {
member.setTeam(this);
members.add(member);
}
하지만 이런 연관관계 편의 메소드는 양방향 매핑 시에 toString(), lombok, JSON 생성 라이브러리에서 무한 루프를 생성할 수 있다. 이렇게 되면 stackoverflow나 장애가 날 수 있기 때문에 한 쪽에만 메소드를 설정해주는 것이 좋다.
양방향 매핑 정리
단방향 매핑만으로도 이미 연관관계 매핑은 완료된 것이다. 양방향 매핑은 반대 방향으로 조회 기능이 추가된 것뿐이기 때문이다. 따라서 양방향 매핑은 최대한 피하는 것이 좋다. 개발 시에는 처음에 엔티티 설계 단계에서는 단방향 매핑을 잘 해둔 후 양방향은 필요할 때만 추가해도 된다. 테이블에 영향을 주지 않기 때문에 가능하다.
예제
테이블 구조
테이블 구조를 외래키가 아닌 참조를 사용하도록 변경하였다.
매핑 코드
Order 쪽 매핑은 아래와 같이 할 수 있다.
@Entity
@Table(name="ORDERS")
public class Order {
@Id
@GeneratedValue
@Column(name="ORDER_ID")
private Long id;
// @Column(name="MEMBER_ID")
// private Long memberId;
@ManyToOne
@JoinColumn(name="MEMBER_ID")
private Member member;
private LocalDateTime orderDate;
@Enumerated(EnumType.STRING)
private OrderStatus status;
...
}
OrderItem 쪽 매핑은 아래와 같이 할 수 있다.
@Entity
public class OrderItem {
@Id @GeneratedValue
@Column(name="ORDER_ITEM_ID")
private Long id;
// @Column(name="ORDER_ID")
// private Long orderId;
@ManyToOne
@JoinColumn(name="ORDER_ID")
private Order order;
// @Column(name="ITEM_ID")
// private Long itemId;
@ManyToOne
@JoinColumn(name="ITEM_ID")
private Item item;
private int orderPrice;
...
}
객체 구조
객체 구조도 참조를 사용하도록 변경하였다. Member와 Order 사이, Order와 OrderItem, OrderItem과 Item 사이를 모두 양방향 연관관계로 설정하도록 하겠다.
만약 Order 리스트를 양방향 관계로 가지고 가고 싶을 때의 예시이다. 그렇다면 Member 쪽 연관관계는 아래와 같이 작성할 수 있다.
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name="MEMBER_ID")
private Long id;
private String name;
private String city;
private String street;
private String zipcode;
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
...
}
OrderItem 리스트를 양방향 관계로 가지고 가고 싶을 때에는 Order 쪽 연관관계를 아래와 같이 작성할 수 있다.
@Entity
@Table(name="ORDERS")
public class Order {
@Id
@GeneratedValue
@Column(name="ORDER_ID")
private Long id;
@ManyToOne
@JoinColumn(name="MEMBER_ID")
private Member member;
@OneToMany(mappedBy = "order")
private List<OrderItem> orderItems = new ArrayList<>();
private LocalDateTime orderDate;
@Enumerated(EnumType.STRING)
private OrderStatus status;
...
}
하지만.. 위와 같은 양방향 관계는 개발 시에 지양되어야 한다.
'Spring > SpringBoot&JPA' 카테고리의 다른 글
[JPA] 상속관계 매핑 (0) | 2024.03.29 |
---|---|
[JPA] 다양한 연관관계 (다대일, 일대다, 일대일, 다대다) (0) | 2024.03.29 |
[JPA] 엔티티 매핑 (매핑 어노테이션, DDL, 기본키 전략) (0) | 2024.03.22 |
[자바 ORM 표준 JPA 프로그래밍] 섹션3. 영속성 관리 - 내부 동작 방식 (0) | 2024.03.18 |
[자바 ORM 표준 JPA 프로그래밍] 섹션2. JPA 시작하기 (0) | 2024.03.15 |