🗺 RoadMap/Jpa

[JPA] #8 연관관계 매핑(2) : 양방향 매핑

an2z 2022. 10. 6. 16:15
본 포스팅은 인프런 - 자바 ORM 표준 JPA 프로그래밍 (기본편) 을 강의를 바탕으로 공부하고 정리한 글입니다.

 

양방향 매핑


@Entity
public class Member {
    @Id
    @GeneratedValue
    @Column(name = "member_id")
    private Long id;

    @Column(name = "user_name")
    private String username;

    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;
}
@Entity
public class Team {
    @Id
    @GeneratedValue
    @Column(name = "team_id")
    private Long id;

    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
}
  • Member 엔티티는 단방향 매핑과 동일하다.
  • Team 엔티티에 List<Member>컬렉션을 추가한다.
  • mappedBy를 통해 연관관계 주인을 지정해준다.
// 조회
Member findMember = em.find(Member.class, member.getId());
Team findTeam = findMember.getTeam();
List<Member> members = findTeam.getMember(); // 역방향 조회
  • Team에서 Member로 반대 방향의 객체 그래프 탐색이 가능하다.

 

 

객체와 테이블이 관계를 맺는 차이


객체의 양방향 연관관계

  • 객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개다.
    • 회원 → 팀 연관관계 1개 (단방향)
    • 팀 → 회원 연관관계 1개 (단방향)
  • 따라서 객체를 양방향으로 참조하려면 단방향 관계 2개를 만들어야 한다.
    • A → B (a.getB())
    • B → A (b.getA())
class A {
    B b; 
}

class B {
    A a;
}

 

테이블의 양방향 연관관계

  • 테이블은 외래 키(FK) 하나로 두 테이블의 연관관계를 관리한다.
    • 회원 ↔ 팀 연관관계 1개 (양방향)
  • 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

 

정리

이렇게 객체와 테이블이 관계를 맺는 차이가 있기 때문에 둘 중 하나로 외래 키를 관리해야 한다.

예를 들어 Member를 바꾸고 싶다거나, 새로운 Team에 들어가고 싶다면?

  • 객체 :  Member의 team 값을 변경해야 할까, Team의 members를 바꿔야 할까?
  • 테이블 : MEMBER.TEAM_ID 외래키 값만 변경되면 됨.
  • 이 둘의 차이가 있기 때문에 둘 중 하나로 주인을 정해 외래키를 관리해야 하는 것이다.

따라서 외래 키를 관리 해줄 연관관계의 주인을 정해야 한다.

 

 

연관관계 주인(Owner)


양방향 매핑 규칙

  • 객체의 두 관계 중 하나를 연관관계의 주인으로 지정한다.
  • 연관관계의 주인만이 외래 키를 관리할 수 있다. (등록 및 수정)
  • 주인이 아닌쪽은 읽기만 가능하다.
  • 주인이 아니면 mappedBy(name = "") 속성으로 주인을 지정해줘야 한다.

 

누구를 주인으로 해야하지? 

  • 외래 키가 있는 곳을 주인으로 정하자.
  • 1:N이라면 N(@ManyToOne)인 곳
  • ex) Member.team이 연관관계의 주인이 된다.

 

 

양방향 매핑 시 주의점


양방향 매핑 시 연관관계의 주인에 값을 입력하지 않아 문제를 겪는 경우가 많다.

어떤 상황인지 예제 코드를 통해 알아보자.

 

Member member = new Member();
member.setUsername("anne");
em.persist(member);

Team team = new Team();
team.setName("팀A");

// 역방향(주인이 아닌 방향)만 연관관계 설정
team.getMembers().add(member); 
em.persist(team);

연관관계 주인이 아닌 Team쪽에서 Member를 추가해줬다.

이 경우 DB를 실제로 확인해보면 Member의 Team의 외래키 값이 null로 저장된다 .

연관관계 주인인 Member쪽에서 Team을 설정하면 어떻게 될까?

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

Member member = new Member();
member.setUsername("anne");

// 연관관계 주인에서 연관관계 설정
member.setTeam(team); 
em.persist(member);

 

null이 아닌 Team의 외래키가 잘 저장된 것을 확인할 수 있다.

 

✅ 정리

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

Member member = new Member();
member.setUsername("member1");

member.setTeam(team); // 연관관계 주인에 값 설정
team.getMembers().add(member); // 연관관계 주인이 아닌 곳에도 값 설정

em.persist(member);
  • 반드시 연관관계의 주인에 값을 입력해야 한다.
  • 순수 객체 상태를 고려해 항상 양쪽 모두 값을 설정해야 한다.
  • 이를 위해 연관관계 편의 메소드 활용하면 좋다.(아래에서 설명)
  • 양방향 매핑 시 무한 루프에 조심해야 한다.
    • toString(), lombok, JSON 생성 라이브러리

 

  

연관관계 편의 메소드


앞서 알아봤듯이 양방향 매핑시 연관관계 주인과 연관관계 주인이 아닌 곳 모두 값을 설정해줘야 한다.

개발을 하다보면 하나를 빼먹는 실수를 할 수 있기 때문에 연관관계 편의 메소드라는 것을 생성해 사용하는 경우가 많다.

 

연관관계 편의 메소드는 1:N에서 1쪽에 지정해도 되고, N쪽에 지정해도 무관하다.

방향은 상관 없지만 연관관계 편의 메소드가 양쪽에 있으면 문제를 일으킬 수 있기 때문에 한쪽에만 만들어야 한다.

 

👉🏻 N(Many)쪽에 지정

@Entity
public class Member {
    ...

    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;

    /**
     * 연관관계 편의 메소드
     */
    public void changeTeam(Team team) {
        this.team = team;
        team.getMember().add(this);
    }
}

👉🏻 1(One)쪽에 지정

@Entity
public class Team {
    ...

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
    
     /**
     * 연관관계 편의 메소드
     */
    public void addMember(Member member) {
    	member.setTeam(this);
        members.add(member);
    }
}