학습 목표
- 객체와 테이블 연관관계의 차이 이해
- 객체의 참조와 테이블의 외래 키 매핑
- 용어 이해
- 방향 (Direction) : 단방향, 양방향
- 다중성 (Multiplicity) : 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M) 이해
- 연관관계의 주인 (Owner) : 객체 양방향 연관관계는 관리 주인이 필요하다.
단방향 연관관계
- 여기서 한 명의 멤버는 한 개의 팀만 가질 수 있다.
- 멤버 : 팀 = N : 1
📌 Member
package hellojpa.domain;
import javax.persistence.*;
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Team getTeam() {
return team;
}
public void setTeam(Team team) {
this.team = team;
}
}
- 한 개의 팀에는 여러 멤버가 있을 수 있으니 @ManyToOne 애노테이션을 사용했다. (N:1)
- @JoinColumn : 외래 키 매핑시 사용한다.
- 속성
- name : 매핑할 외래 키 이름
- 속성
📌 Team
package hellojpa.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
📌JpaMain
연관관계 저장
package hellojpa;
import hellojpa.domain.Member;
import hellojpa.domain.Team;
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.setUsername("member1");
member.setTeam(team); // 단방향 연관관계 설정
em.persist(member);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
연관관계 조화 - 객체 그래프 탐색
package hellojpa;
import hellojpa.domain.Member;
import hellojpa.domain.Team;
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.setUsername("member1");
member.setTeam(team);
em.persist(member);
// 조회
Member findMember = em.find(Member.class, member.getId());
// 참조를 사용해서 연관관계 조회
Team findTeam = findMember.getTeam();
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
연관관계 수정
package hellojpa;
import hellojpa.domain.Member;
import hellojpa.domain.Team;
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.setUsername("member1");
member.setTeam(team);
em.persist(member);
// 새로운 B팀
Team teamB = new Team();
teamB.setName("teamB");
// 회원1에 새로운 팀B 설정
member.setTeam(teamB);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
양방향 연관관계와 연관관계의 주인
- 양방향의 의미는 양측에서 서로 참조할 수 있다는 것이다.
- 객체 참조는 양방향 연관관계를 만들기 위해 단방향 연관관계를 2개 만들어야 한다.
- 참고로 객체는 가급적 단방향이 좋다.
- 양방향이면 신경 쓸게 많아진다.
- 테이블 연관관계는 외래키 하나로 1개의 양방향 연관관계를 만들 수 있다.
1. 객체와 테이블이 괜계를 맺는 차이
🔍 객체 연관관계
- Member → Team
- 연관관계 1개 (단방향)
- Team → Member
- 연관관계 1개 (단방향)
- 객체에서 양방향은 사실상 양방향 관계가 아니라 서로 다른 단방향 관계가 2개 존재하는 것이다.
- 그러므로 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.
🔍 테이블 연관관계
- 회원 <-> 팀
- 연관관계 1개 (양방향)
- 테이블은 외래 키 하나로 두 테이블을 관리하고 양방향 연관관계를 가진다.
- 외래 키 하나로 양쪽으로 조인이 가능하다.
2. 둘 중 하나를 외래 키로 관리해야 한다.
- 두 객체(Member, Team)에서 서로가 서로를 참조하는 값을 만들어 놨다.
- 여기서 외래 키를 관리하는 연관관계의 주인(Owner)을 정해야 한다.
3. 연관관계의 주인(Owner)
🔍 양방향 매핑 규칙
- 객체의 두 관계 중 하나를 연관관계의 주인으로 지정한다.
- 연관관계의 주인만이 외래 키를 관리한다. (등록, 수정이 가능하다.)
- 주인이 아닌 쪽은 읽기만 가능하다.
- 연관관계 주인은 mappedBy 속성을 사용하지 않는다.
- 주인이 아닌 쪽에서 mappedBy 속성으로 주인을 지정한다.
🔍 누구를 주인으로?
- 외래 키가 있는 곳을 주인으로 정한다.
- 외래 키가 있는 곳을 주인으로 해야 성능 이슈도 없고 깔끔하게 작성할 수 있다.
- 보통 '일대다' 구조에서 '다' 인 쪽을 연관관계 주인으로 설정하는 것이 좋다.
- 비즈니스 로직 기준으로 연관관계의 주인을 정하면 안된다.
- 무조건 외래 키 위치를 기준으로 정해야 한다.
- 여기서는 Member.team이 연관관계의 주인이다.
- Team에서 외래키를 관리하는 것은 불가능한가?
- 가능은 하다. 하지만 Team에서 members를 수정하면 Team이 아닌 Member 업데이트 쿼리가 날라가는 불일치 현상이 발생한다.
4. 양방향 매핑시 많이 하는 실수
🔍연관관계 주인에 값을 입력하지 않는 실수
package hellojpa;
import hellojpa.domain.Member;
import hellojpa.domain.Team;
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.setUsername("member1");
// 역방향만 연관관계 설정하는 실수
team.getMemberList().add(member);
em.persist(member);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
연관관계 주인이 아닌 Team에서 값을 입력했다.
- 실행결과를 보면 MEMBER 테이블에 TEAM_ID 값이 들어가지 않았다.
💡 해결책 v1 : 양방향 매핑시 연관관계 주인에 값을 입력해야 한다.
package hellojpa;
import hellojpa.domain.Member;
import hellojpa.domain.Team;
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.setUsername("member1");
// 연관관계의 주인에 값 설정
member.setTeam(team);
em.persist(member);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
- 연관관계 주인에 값을 입력하니깐 DB에 제대로 값 세팅이 이뤄졌다.
💡 해결책 v2 : 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자
해결책 v1에서는 team.getMemberList().add(member); 대신 연관관계 주인에게만 값을 넣었다. 이럴 때 생기는 문제는 없을까?
DB에 반영하는데 문제는 없다. 하지만 영속성 컨텍스트의 1차 캐시에 저장된 team에서는 memberList에 들어있는 해당 member가 추가되지 않는 상태다. 이런 상황에서 team.memberList()를 사용하게 된다면 DB에서 조회하는 것이 아닌 1차 캐시에서 꺼내기 때문에 해당 member가 추가되지 않는 결과가 반환된다.
테스트 케이스 경우 순수 자바코드로 작성하기 때문에 이런 점이 문제가 생길 수 있다. 그러므로 연관관계 주인에 상관없이 항상 양쪽 객체에 값을 세팅하도록 하는것이 좋다.
✔ 연관관계 편의 메서드 생성
- 양쪽 객체에 값을 세팅할 때 연관관계 편의 메서드를 생성해서 이용하는 것이 좋다.
- 연관관계 편의 메서드란, 양쪽 객체의 값을 세팅해주는 메서드다.
- 연관관계 편의 메서드는 어느쪽에 생성할지 상황에 따라 다르다.
- 참고로 연관관계 편의 메서드는 양 쪽에 존재해서는 안된다. 반드시 한 쪽에만 작성해야 한다.
➰ Member 쪽에 연관관계 편의 메서드 생성하기
// Member 객체에 연관관계 편의 메서드 추가
public void changeTeam(Team team) {
this.team = team;
team.getMemberList().add(this);
}
- setTeam() 메서드를 연관관계 편의 메서드로 작성할 수 있지만, setter 보다는 'changeXXX' 이런식으로 메서드명을 따로 작성하여 단순히 값만 바꾸는 행동이 아니라 중요한 행동이라는 것을 명시하는 것이 좋다.
- setter는 가급적 작성을 안하는 것이 좋다.
- 왜냐하면 다른 개발자들이 아무 생각없이 setter로 값을 바꿀 수 있는 상황이 있기 때문이다.
- 그러므로 값을 바꿔주는건 다른 메서드 명으로 사용해서 중요한 행동임을 명시하는것이 좋다.
package hellojpa;
import hellojpa.domain.Member;
import hellojpa.domain.Team;
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.setUsername("member1");
// 연관관계 편의 메서드 사용
member.changeTeam(team);
em.persist(member);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
➰ Team 쪽에 연관관계 편의 메서드 생성하기
// Team 객체에 연관관계 편의 메서드 추가
public void addMember(Member member) {
member.setTeam(this);
memberList.add(member);
}
연관관계 편의메서드 사용
package hellojpa;
import hellojpa.domain.Member;
import hellojpa.domain.Team;
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.setUsername("member1");
// 연관관계 편의 메서드 사용
team.addMember(member);
em.persist(member);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
🔍 양방향 매핑시 무한루프 (순환참조)에 빠지는 실수
- 양방향 엔티티 객체에 toString() 메서드 / 롬복의 @toString / JSON 생성 라이브러리 등으로 무한루프에 빠질 수 있다.
- 엔티티 객체에 가급적 toString() / @toString을 쓰지 않는 것이 좋다.
- 컨트롤러에서 엔티티 객체를 반환하면 절대 안된다.
- DTO로 변환해서 반환할 것
- 엔티티를 수정할 경우 api 스펙이 바뀔 수도 있기에 엔티티 객체는 반환해서는 안된다.
- 엔티티를 반환할 경우 순환 참조가 일어날 수 있다.
5. 양방향 매핑 정리
- 처음 DB 설계를 할 때 무조건 단방향 매핑만으로 설계할 것.
- 양방향 매핑은 그저 반대 방향으로 조회 기능만 추가 된 것이다.
- 양방향 설계는 객체지향 설계에 별 이득이 없다.
- 실무에서는 JPQL에서 역방향으로 탐색할 일이 많다.
- 단방향 매핑으로 설계하고 애플리케이션 개발 단계에서 필요할 때만 양방향을 추가할 것.
- 양방향은 테이블에 영향을 주지 않는다.
👀 참고 자료
https://www.inflearn.com/course/ORM-JPA-Basic/dashboard
https://catsbi.oopy.io/ed9236a0-6521-471d-8a0d-b852147b5980
'[JPA] > JPA 프로그래밍 - 기본편' 카테고리의 다른 글
[JPA] 고급 매핑 (0) | 2022.03.29 |
---|---|
[JPA] 다양한 연관관계 매핑 (0) | 2022.03.29 |
[JPA] 엔티티 매핑 (0) | 2022.03.27 |
[JPA] 영속성 관리 - 내부 동작 방식 (0) | 2022.03.26 |
[JPA] JPA 시작하기 (0) | 2022.03.26 |