1. 단방향 연관관계 복습
2024.06.13 - [인프런 김영한 강의 정리/자바 ORM 표준 JPA 프로그래밍 기본편] - JPA 기본 | 연관관계 매핑 기초
JPA 기본 | 연관관계 매핑 기초
1. 객체를 테이블에 맞춰 데이터 중심으로 모델링 했을 때 이 방식으로 모델링을 하면 객체지향과 거리가 멀어진다. 왜 객체의 참조와 외래키를 매핑해야되는지 예시를 보며 이해해보자. Member
ururuwave.tistory.com
member와 team이 있다.
member는 하나의 team에 소속될 수 있다.
단방향 연관관계 Member 엔티티에 @ManyToOne, @JoinColumn으로 Team을 매핑해줬다.
@Entity
@Getter
@Setter
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne //Member와 Team은 N:1
@JoinColumn(name = "TEAM_ID")
private Team team;
}
이렇게 매핑하면 Member에서 Team를 찾을 수 있다.
//조회
Member findMember = em.find(Member.class, member.getId());
Team findTeam = findMember.getTeam();
2. 양방향 매핑
양방향 매핑을 하면 객체를 참조하여 Member에서 Team을 찾는 것은 물론이고 Team에서 Member도 찾을 수 있다.
* 테이블 연관관계에서는 외래키 하나로 양방향 연관관계를 가진다.
TEAM_ID(외래키)로 조인을 하면 Member에서 Team을 찾고 Team에서 Member를 찾을 수 있다.
*양방향 객체 연관관계에서는 Team에서 Member를 찾기 위해 member를 List로 담아야한다.
(단방향 관계 2개를 만드는 셈이다.)
Member ➡️Team(member.getTeam())
Team ➡️ Member(team.())
✅Team 엔티티에 추가할 코드
@OneToMany(mappedBy = "team") //team과 member는 1:n
private List<Member> members = new ArrayList<>();
//ArrayList<>()로 초기화한다. add할때 NullPointException이 뜨지 않게!
⭐mappedBy 속성
- 위 코드에서는 Member엔티티의 team 필드에 의해 매핑됨을 의미함.
- 이 속성이 있는 필드는 '읽기 전용'이다.
- 주인의 필드를 지정한다.
3. 연관관계의 주인
- JPA에서 양방향 연관관계 매핑 시 외래키를 관리하는 엔티티.
- 외래키가 있는 곳을 주인으로 정하자. (DB에서 외래키가 있는 곳이 N이 된다.)
- DB에 외래키를 업데이트할 책임이 있다.
- 주인 엔티티 필드에 @JoinColumn을 사용하여 외래키 컬럼을 지정한다.
4. 양방향 매핑 시 주의사항
1) 연관관계 주인에 값을 입력하자.
위 예시를 보면 Member 엔티티가 연관관계 주인이다.
💡Team 엔티티는 연관관계 주인이 아니다. 때문에, List members를 바꿔도 DB에 값이 안들어간다.
✅ 연관관계 주인이 아닌 것에 값을 입력했을 때 (Team으로 member세팅)
package hellojpa;
import jakarta.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{
//저장(Member)
Member member = new Member();
member.setUsername("member1");
em.persist(member);
//저장(Team)
Team team = new Team();
team.setName("TeamA");
team.getMembers().add(member);
em.persist(team);
//영속성컨텍스트 변경사항 DB에 반영
em.flush();
//1차 캐시 초기화
em.clear();
tx.commit();
}catch (Exception e){
tx.rollback();
}finally {
em.close();
}
}
}
team.getMembers().add(member);
Team에 있는 Members를 가져와서 member를 추가해준다.
그리고 DB를 확인해보자.
TEAM_ID(외래키)가 null이다.
연관관계 주인이 아닌 것에 값을 입력했기 때문에 DB에 반영이 안된 것이다.
✅연관관계 주인에 값을 입력(Member로 team 세팅)
package hellojpa;
import jakarta.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 team = new Team();
team.setName("TeamA");
em.persist(team);
//저장(Member)
Member member = new Member();
member.setUsername("member1");
member.setTeam(team);
em.persist(member);
//영속성컨텍스트 변경사항 DB에 반영
em.flush();
//1차 캐시 초기화
em.clear();
tx.commit();
}catch (Exception e){
tx.rollback();
}finally {
em.close();
}
}
}
team을 먼저 저장하고 member.setTeam(team)으로 team을 세팅해준다.
이제 DB를 확인해보자.
TEAM_ID(외래키)의 값이 잘 반영됐다.
2) 항상 양쪽 다 값을 입력해야한다.
Why? 연관관계 주인의 변경이 비주인에 자동 반영되지 않기 때문에 양쪽 다 값을 입력하여 일관성을 유지해주어야한다.
2-1) JPA 동작 방식과 영속성 컨텍스트 동작을 이해하기 위해서 세 가지 경우를 살펴보자.
1️⃣연관관계 주인에만 값 세팅 (em.flush(), em.clear() 안함)
2️⃣em.flush(), em.clear() 코드 추가 (영속성 컨텍스트 초기화, 1차 캐시 비우기)
3️⃣양방향 연관관계 동기화
✅코드 예시 - 연관관계 주인에만 값 세팅 (em.flush(), em.clear() 안함)
// Team 엔티티와 Member 엔티티 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setUsername("member1");
member.setTeam(team);
em.persist(member);
// Team 엔티티의 members 리스트에서 Member 조회
Team findTeam = em.find(Team.class, team.getId());
List<Member> members = findTeam.getMembers();
System.out.println("==========================");
for (Member m : members) {
System.out.println("m = " + m.getUsername());
}
System.out.println("==========================");
tx.commit();
연관관계 주인인 member로 team을 세팅한다.
그리고 team에서 members를 꺼내서 members의 이름을 출력한다.
➡️결과 (username 출력X)
- Member 엔티티의 변경이 Team 엔티티의 members 리스트에 반영되지 않기 때문이다.
✔️ 코드 해석
member.setTeam(team)
- Member 엔티티의 team 필드가 Team 엔티티를 참조한다.
Team 참조를 통해 team_id(외래키)값을 설정한다.
em.persist(team)
- Team 엔티티가 영속성 컨텍스트에 저장된다. (DB에 반영X)
em.persist(member)
- Member 엔티티가 영속성 컨텍스트에 저장된다. (DB 반영X)
em.find(Team.class, team.getId())
- Team 엔티티를 1차 캐시(영속성 컨텍스트)에서 찾는다.
- Team의 members 리스트가 비어있다.
✅코드 예시 - em.flush(), em.clear() 코드 추가 (영속성 컨텍스트 초기화, 1차 캐시 비우기)
//저장(Team)
Team team = new Team();
team.setName("TeamA");
em.persist(team);
//저장(Member)
Member member = new Member();
member.setUsername("member1");
member.setTeam(team);
em.persist(member);
//영속성컨텍스트 변경사항 DB에 반영
em.flush();
//1차 캐시 초기화
em.clear();
Team findTeam = em.find(Team.class, team.getId());
List<Member> members = findTeam.getMembers();
System.out.println("==========================");
for (Member m: members) {
System.out.println("m = " + m.getUsername());
}
System.out.println("==========================");
tx.commit();
➡️결과 (username 출력O)
- 데이터베이스를 조회하여 Team 엔티티와 관련된 Member 엔티티를 함께 조회한다.
✔️코드 해석
em.flush()
- 영속성 컨텍스트의 변경 사항이 DB에 강제로 반영된다.
Member 엔티티의 team_id 값이 DB에 저장된다.
em.clear()
- 영속성 컨텍스트를초기화하고 1차 캐시를 비운다.
em.find(Team.class, team.getId())
- 1차 캐시가 비워져 있으니, DB에서 Team 엔티티를 새로 조회한다.
- 이때, Team 엔티티의 members 필드는 프록시 객체로 초기화된다. (지연로딩)
findTeam.getMembers()
- 일반적으로 JPA에서 일대다(@OneToMany)연관관계는 지연 로딩으로 설정된다.
- 이 코드를 호출하면, 지연 로딩 설정에 의해 DB에서 연관된 Member 엔티티들이 자동 조회되어 members 리스트가 채워진다.
- members 필드에 접근하면 JPA는 프록시 객체를 통해 DB에서 Team엔티티와 연관된 Member엔티티를 조회한다.
- team_id 외래키를 기준으로 DB에서 해당 팀이 속한 멤버들을 가져온다.
✅Best 코드 예시 - 양방향 연관관계 동기화(team.getMembers().add(member) 추가)
//저장(Team)
Team team = new Team();
team.setName("TeamA");
em.persist(team);
//저장(Member)
Member member = new Member();
member.setUsername("member1");
member.setTeam(team);
em.persist(member);
team.getMembers().add(member);
Team findTeam = em.find(Team.class, team.getId());
List<Member> members = findTeam.getMembers();
System.out.println("==========================");
for (Member m: members) {
System.out.println("m = " + m.getUsername());
}
System.out.println("==========================");
tx.commit();
➡️결과 (username 출력O)
- 메모리상에서 Team 엔티티의 members 리스트에 member가 추가된다.
✔️코드 해석
team.getMembers().add(member)
- 영속성 컨텍스트 내에서 Team엔티티의 members 리스트에 member가 추가된다.
- em.flush() 나 em.clear()가 없어도 username이 올바르게 출력된다.
- DB에 접근하지 않고 1차 캐시에서 일관된 상태를 유지한다.
⭐정리
객체 지향 관점에서 객체 상태 일관성과 무결성 유지를 위해 양방향 연관관계 매핑 시 항상 양쪽 다 값을 입력해야 한다.
2-2) 연관관계 편의 메소드를 생성하자.
위 코드를 보면 양쪽 다 값을 입력하기 위해서 2가지 코드를 매번 넣어야한다.
member.setTeam(team);
team.getMembers().add(member);
실수를 줄이고 코드를 간편하게 작성하기 위해, 편의 메소드를 생성한다.
public void setTeam(Team team){
this.team = team;
team.getMembers().add(this); //추가
}
Member엔티티에 setTeam 메서드에 team.getMembers().add(this)를 추가해준다.
그럼 member.setTeam(team)만 써도 된다.
💡다만, 메서드 이름을 setTeam보다는 changeTeam같이 다른 이름으로 설정하는게 좋다.
왜냐하면, setter는 관례상 단순히 필드의 값만 설정하는 역할을 해야하는데 team.getMembers().add(this) 코드를 추가하면서 관례가 깨지기 때문이다.
2-3) 무한루프를 조심하자.
✅무한루프를 유발하는 코드
@Entity
@Getter
@Setter
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 void changeTeam(Team team){
this.team = team;
team.getMembers().add(this); //무한루프 가능성 유발
}
}
@Entity
@Getter
@Setter
public class Team {
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
public void addMember(Member member){
members.add(member);
member.changeTeam(this); //무한루프 가능성 유발
}
}
✅Team 객체에 member 추가
Team team = new Team();
Member member = new Member();
team.addMember(member);
team.addMember(member) ➡️ member.setTeam(this) ➡️ team.getMembers().add(this) ➡️ team.addMember(member)
이 과정이 무한히 반복되어 StackOverflowError가 발생한다.
⭐해결법
1. 연관관계 편의 메소드는 한 쪽에만 넣어둔다.
2. 이미 포함되어 있는지 확인하는 코드를 넣어서 무한 루프를 방지한다.
public void addMember(Member member) {
if (!members.contains(member)) { // 이미 포함되어 있는지 확인
members.add(member);
member.setTeam(this);
}
}
public void setTeam(Team team) {
this.team = team;
if (team != null && !team.getMembers().contains(this)) { // 이미 포함되어 있는지 확인
team.getMembers().add(this);
}
}
✔️이 외에도 toString()메서드 호출, 컨트롤러에서 엔티티 반환 시 무한 루프를 발생시킬 수 있다.
toString() 메서드에서 연관관계 필드를 제외하거나, 컨트롤러에서 엔티티를 반환하지 않도록 해야한다.
5. 총정리
- 처음에는 단방향 매핑으로 설계를 끝내야한다.
- 양방향매핑은 필요할 때 추가하면 된다.(테이블에 영향을 주지 않기때문)
- 역방향으로 조회가 필요해지면 그때, 양방향 매핑을 해주면 된다.
ex) 처음에는 Member가 어느 Team에 속해 있는지만 알면 됐다.
개발을 하다보니 특정 Team에 속한 모든 Member를 조회해야되는 상황이 생겼다. 이때 양방향 매핑을 해주면 된다.
- 연관관계 주인은 외래키 위치를 기준으로 정한다.
'인프런 김영한 강의 정리 > 자바 ORM 표준 JPA 프로그래밍 기본편' 카테고리의 다른 글
JPA 기본 | 상속관계 매핑 (0) | 2024.07.10 |
---|---|
JPA 기본 | 다양한 연관관계 매핑 (0) | 2024.07.05 |
JPA 기본 | 연관관계 매핑 기초 (0) | 2024.06.13 |
H2 새로운 데이터베이스로 접속 오류 해결하기 (0) | 2024.06.10 |
JPA 기본 | 기본키 매핑 | @id, @GeneratedValue (0) | 2024.06.10 |