반응형
목표
- 객체와 테이블 연관관계의 차이를 이해
- 객체의 참조와 테이블의 외래 키를 매핑
- 용어 이해
- 방향 : 단방향, 양방향
- 다중성 : 다대일, 일대다, 일대일, 다대다
- 연관관계의 주인 : 객체 양방향 연관관계는 관리 주인이 필요
연관관계가 필요한 이유
객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다.
예제 시나리오
- 회원과 팀 존재할 때, 회원은 하나의 팀에만 소속 = 하나의 팀에는 여러 회원 소속
- 회원과 팀은 다대일 관계를 갖는다.
테이블 중심 설계시 문제점
다음과 같이 설계했을 때 발생하는 문제점에 대해 알아보자.
회원 테이블
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@Column(name = "TEAM_ID")
private Long teamId;
팀 테이블
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
}
JpaMain
package hellojpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
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 findMember = em.find(Member.class, member.getId());
Long findTeamId = findMember.getTeamId();
Team findTeam = em.find(Team.class, findTeamId);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
- 테이블에 맞추어 설계할 경우 객체 지향스럽지가 않다.
- 조회하려면 Member의 Id를 찾고 그 값으로 TeamId를 찾고 등..
단방향 연관관계
- 현재는 Team에서 Member로 갈 수 없는 상황으로 보자. (단방향)
Member 엔티티 수정
package hellojpa;
import javax.persistence.*;
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
/* @Column(name = "TEAM_ID")
private Long teamId;*/
@ManyToOne // 해당 클래스의 관점에서 지정
@JoinColumn(name = "TEAM_ID")
private Team team;
}
@ManyToOne
- 멤버 입장에선 여러 멤버가 하나의 팀에 가입 가능 = 다대일@JoinColume(name = "TEAM\_ID")
- 조인이 필요한 대상의 컬럼 네임 지정
package hellojpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
member.setTeam(team);
em.persist(member);
Member findMember = em.find(Member.class, member.getId());
Team findTeam = findMember.getTeam();
System.out.println("findTeam = " + findTeam.getName());
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
- 단방향 연관관계를 매핑해서 객체지향적으로 테스트 해볼 수 있다.
- 현재는
em.find
에서 1차 캐시의 값을 바로 가져오기 때문에 별도의 쿼리문이 출력되지 않지만, 해당 부분을 보고 싶다면em.flush();
,em.clear();
를 적용시켜줘서 영속성 컨텍스트를 초기화 시켜주면된다.
현재는 Member에서 Team으로는 갈 수 있지만, Team에서 Member로는 갈 수 없는 상황이다. 이어서 양방향 연관관계에 대해 알아보자.
양방향 연관관계 , 연관관계 주인
- 테이블 관계는 FK 하나로 조인을 하기 때문에 FK가 들어간 순간 자동으로 양방향 관계다.
- 객체 관계는 추가로 Team에다가 List members를 넣어줘야만 양방향 관계를 만들 수 있다.
양방향 관계를 위한 Team 수정
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team") //Member의 Team team 부분 매핑 설정 (team이 연관관계 주인이다.)
private List<Member> members = new ArrayList<>();
}
- Team 입장에선 OneToMany - 일대다
- mappedBy로 Member의 Team team 설정한 부분의 이름을 매핑 - 하인 입장 (읽기만 가능)
- 객체는 사실 양방향 설정을 하려고 하면, 단방향을 2개 설정해야 하는 것이다. (테이블 연관관계는 양방향1개로 끝)
JpaMain
package hellojpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.util.List;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
Team findTeam = findMember.getTeam();
List<Member> members = findTeam.getMembers();
for (Member m : members) {
System.out.println("m = " + m.getName());
}
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
- 양방향 연관관계 설정으로 이제 Team에서도 멤버 객체를 가져올 수 있다.
그렇다면 언제 mappedBy를 사용해야할까? 이 부분을 정확하게 이해해야한다.
객체와 테이블이 관계를 맺는 차이
- 객체 연관관계 : 단방향 2개 (회원 -> 팀, 팀 -> 회원)
- 테이블 연관관계 : 양방향 1개 (조인 연산)
- 테이블에 있는 FK를 변경해야 되는데 양방향이다 보니 Member에있는 Team을 변경해야할지, Team에 있는 List members를 변경해야 할지 딜레마가 온다. 연관 관계 주인으로 해결하자.
연관 관계 주인 - 양방향 매핑 규칙
- 객체의 두 관계 중 하나를 연관관계의 주인으로 지정
- 연관관계의 주인만이 외래키를 관리 (등록, 수정)하고, 주인이 아닌 쪽은 읽기만 가능
- 주인은 mappedBy 속성 사용 X , 주인이 아니면 mappedBy 속성으로 주인 지정
- 외래키가 있는 곳을 주인으로 정하자. (Member.team 이 연관관계의 주인) - 多 쪽이 주인 (외래키가 있는 곳)
- 반대의 경우라면, Team의 값을 변경했는데 Member 업데이트 쿼리가 나간다. (성능면에서도 부적절)
잘못된 매핑 예시
- TEAM_ID 가 null 이된다. 주인에 값을 안넣고 주인이 아닌 곳에 값을 넣으려고 함 - 주의
- 주인에만 값을 넣어도 되지만, 양쪽에다 값을 모두 넣어주는게 맞다. (객체지향적 관점)
올바른 매핑 예시
package hellojpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.util.List;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
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.getMembers().add(member);
em.flush();
em.clear();
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
- 연관관계 주인에만 값을 넣어줘도 되지만, 객체지향 관점에서 읽기 전용인 반대편에도 넣어주는 것이 좋다. 즉, 양 쪽에 모두 값을 넣어주는 것이 바람직하다. 그 이유는 주인 쪽에만 값을 넣어줘도 지연로딩을 통해 커밋 시점에서 동작하지만, 트랙잭션 안에서 값을 꺼내는 경우에 하인 쪽의 값은 조회가 되지 않기 때문에 별도의
flush
가 필요하기 때문이다.
연관관계 편의 메서드
// 주인 엔티티에서 양방향 엔티티를 설정하는 방식
public void changeTeam(Team team) { //setTeam을 비즈니스로직이 들어간 다른 메서드로 변경
this.team = team;
team.getMembers().add(this); //this 나 자신 (Member)
}
- 주인이나 하인 쪽 엔티티에서 양방향으로 값을 넣어주어서 단방향 매핑만으로 연관관계 매핑을 할 수 있다.
- 무한루프 주의(toString, lombok, JSON 생성 라이브러리 - 컨트롤러에서 엔티티 반환 X, DTO 사용 (스펙이 바껴버림))
- Member에서도 Team정보를 참조, Team에서도 Member정보를 참조를 무한 반복하는 현상
양방향 매핑 정리
- 연관관계의 주인은 외래 키의 위치를 기준으로 정해야한다.
- 단방향 매핑만으로도 이미 연관관계 매핑은 완료된다. 즉, 단방향으로 설계를 마치자.
- 양방향 매핑은 객체 그래프 탐색 기능(반대 방향으로 조회)이 추가된 것이다.
- JPQL에서 역방향 탐색할일이 많으므로 단방향 매핑을 잘 해두고, 추후 필요시 양방향 매핑을 하자.
예제
테이블 구조는 이전 포스팅과 동일하다. 게터와 세터는 모두 들어가있지만 코드에는 생략했다. 단방향 연관관계 매핑으로 리팩토링해보자.
객체 구조
- 참조를 사용하도록 변경한다.
주문 테이블
package jpabook.jpashop.domain;
import javax.persistence.*;
import java.time.LocalDateTime;
@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;
}
주문 상품 테이블
package jpabook.jpashop.domain;
import javax.persistence.*;
@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;
private int count;
}
본 포스팅은 인프런 김영한님 강의(자바 ORM 표준 JPA 프로그래밍 - 기본편)를 토대로 정리한 내용입니다.
반응형