본문 바로가기
JPA

[JPA] 연관관계 매핑 - 양방향 연관관계

by snow_white 2022. 8. 8.

※ 인프런 강의 김영한님의 「자바 ORM 표준 JPA 프로그래밍」을 학습한 내용을 정리한 글입니다. 

양방향 연관관계와 연관관계의 주인

이전 글에서 회원에서 팀을 조회할 수 있는 다대일 단방향 매핑을 정리하였습니다.

반대로 팀에서 회원에 접근하려면 어떻게 해야 할까요?

이번에는 양방향 연관관계 패밍을 통해 알아보도록 하겠습니다.

✅ 양방향 객체 연관관계에서 Team 객체에 List 형 members가 추가되었다!

회원과 팀은 다대일 관계, 반대로 팀에서 회원은 일대다 관계로 여러 데이터를 한꺼번에 연관관계를 맺으려면 List와 같은 컬렉션을 사용해야 함

 

❗❓ 테이블 연관관계는 단방향 연관관계일 때와 똑같지 않은가?!

테이블 연관관계는 외래키 하나로 양방향으로 조회 가능

( = DB 테이블은 JOIN으로 모두 조회 가능하기 때문)

 

@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;
    
    // getter/setter 생략
}
@Entity
public class Team {

    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    private String name;
    // getter/setter 생략
    
    @OneToMany(mappedBy = "team") // team => Member 클래스의 Team 변수명과 동일
    private List<Member> members = new ArrayList<Member>();
}

Main.java

Member findMember = em.find(Member.class, member.getId());
List<Member> members = findTeam.getTeam().getMembers(); // 팀 -> 회원 탐색

// Team에서 Member 접근 후 출력
for(Member m : members){
	System.out.println("m = "+m.getName());
}

 


연관관계의 주인

양방향 매핑 규칙

  • 객체의 두 관계 중 하나를 연관관계의 주인으로 지정
  • 연관관계의 주인만이 외래 키를 관리(등록, 수정)
  • 주인이 아닌 쪽은 읽기만 가능
  • 주인은 mappedBy 속성 사용 X
  • 주인이 아니면 mappedBy 속성으로 주인 지정

 

mappedBy 속성은 왜 쓰일까

말그대로 누군가에 의해 map 되었다! 양방향 관계에서 내가 주인이 아니라는 의미!

사실상 양방향 연관관계는 서로 다른 단방향 연관관계 2개의 로직으로 묶어서 양방향인 것처럼 보이게 하는 것인데,

객체 연관관계 = 단방향 연관관계 2개로 이루어짐

  • 회원 → 팀 : 연관관계 1개(단방향)
  • 팀 → 회원 : 연관관계 1개(단방향)

테이블 연관관계 = 1개

  • 회원 ↔️ 팀 : 연관관계 1개(양방향)

테이블은 외래 키 하나만으로 양방향 연관관계를 맺는다.

엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나이다.

따라서 둘 사이에 차이가 발생하게 된다. 그렇다면 둘 중 어떤 관계를 사용해서 외래 키를 관리해야 할까?
이런 차이로 인해 JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계의 주인이라 한다.

 

❓ 그렇다면, 주인 결정하는 기준은 무엇인가

외래 키가 있는 곳을 주인으로 정함!

아래의 경우 Member.team이 연관관계의 주인이 된다.

반대로 Team.members는 주인의 반대편 역할로 mappedBy="team" 과 같이 "team"필드로 주인에게 map되어 있음을 명시해주어야 한다. 

(1:N 관계에서 N인 곳이 주인, 1인 곳이 주인의 반대편 권장)

 

Member가 주인일 때 Team이 바뀌었다면 Member에서 Team 정보를 수정하면 되지만, 

만약 Team을 주인으로 설정한다면 Team에 어떤 행위(수정, 삽입)했을 때 매핑되어 있는 Member  객체 모두에게 수정(Update)행위가 따르기 마련이다.

 


양방향 매핑시 주의점

❌ 연관관계의 주인에 값을 입력하지 않음

Member member = new Member();
member.setName("member1");
em.persist(member);

Team team = new Team();
team.setName("TeamA");
team.getMembers().add(member);
em.persist(team);

연관관계의 주인이 Member에 있는 team이다. 

Team의 members는 mappedBy로 연관되어 읽기 전용이기 때문에 JPA가 update나 insert할 때는 고려하지 않는다. (mappedBy는 가짜매핑)

연관관계의 주인만이 외래 키의 값을 변경할 수 있기 때문에 주인인 Member.team에는 아무 값도 입력되지 않은 것과 같다.

 

아래와 같이 양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수 있기 때문이다.

Team team = new Team();
team.setName("TeamA");
em.persist(team);

Member member = new Member();
member.setName("member1");
member.setTeam(team);
team.getMembers().add(member);

em.persist(member);

em.flush();
em.clear();

Team findTeam = em.find(Team.class, team.getId());
List<Memer> members = findTeam.getMembers();

for(Member m : members)
	System.out.println("m = " + m.getUsername());
    
/* 출력결과
m = member1
*/

 

🔅 여기서 주의해야 할 점!

(1)을 주석처리 한 것과 같이 team의 members 컬렉션에 아무것도 저장하지 않으면 1차 캐시에는 순수한 객체 그대로 저장될 것임. 따라서 Members를 출력했을 때 team과 연결되어 있는 객체를 찾을 수 없으므로 아무것도 출력되지 않는다.

Team team = new Team();
team.setName("TeamA");
em.persist(team);

Member member = new Member();
member.setName("member1");
member.setTeam(team);
// (1) team.getMembers().add(member);

em.persist(member);

// (2)em.flush();
// (3)em.clear();

Team findTeam = em.find(Team.class, team.getId()); // 위의 (1)(2)(3) 주석처리 하면 DB가 아닌 1차 캐시에 저장되어 있음
List<Memer> members = findTeam.getMembers(); // 따라서 findTeam()으로 정보 얻어올 수 없음

for(Member m : members)
	System.out.println("m = " + m.getUsername());

 

✅ 연관관계 편의 메소드를 생성해서 실수를 방지하자!

member.setTeam()과 team.getMembers().add()를 한 번에 처리하자는 의미이다.

@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 void setTeam(Team team){
    	this.team = team;
        team.getMembers().add(this); // 나 자신의 instance를 넣어줌
    }
}

이로써 메소드를 원자적으로 활용하여 실수도 방지하고, member와 team을 한 번에 세팅 가능하다.

'JPA' 카테고리의 다른 글

[JPA] 다양한 연관관계 매핑  (0) 2022.11.28
[JPA] 연관관계 매핑 - 단방향 연관관계  (0) 2022.08.07
[JPA] 엔티티 매핑  (0) 2022.07.28
[JPA] 준영속 상태  (0) 2022.07.18
[JPA] 플러시(Flush)  (0) 2022.07.18

댓글