[JPA] 연관관계 매핑 주인에 대해서 (mappedBy)

이번 포스팅에서 JPA의 연관관계 매핑의 주인이란 무엇인지? 또 연관관계 매핑의 주인이 왜 필요한지에 대해 알아보려한다.

연관관계 주인이 필요한 이유에 대해 먼저 말하자면, 객치 지향의 패러다임과 데이터베이스의 패러다임에 차이가 있기 때문이다.

먼저 각 패러다임의 차이를 이해할 수 있도록 양방향과 단방향 매핑에 대해 설명하고자 한다.

 

양방향과 단방향 매핑

  • 양방향 : 두 객체 모두가 각각 참조용 필드를 갖고 참조
  • 단방향 : 두 객체 사이에 하나의 객체만 참조용 필드를 갖고 참조

데이터베이스에서 양방향과 단방향

데이터베이스에는 양방향과 단방향이라는 개념이 없다.

외래키(FK) 하나면 양쪽의 연관관계를 알 수 있다. (양방향 관계의 특성을 갖는다.)

N : 1 관계

데이터 베이스 테이블 외래키(FK) 하나로 양쪽 테이블을 조인하여 두 테이블의 연관관계를 관리한다.

위의 예시 ERD 관계도에서는 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 엔티티는 Member 엔티티의 필드를 조회할 수 없다.

 

 

양방향 관계

Member 엔티티는 Team 엔티티의 클래스를 필드로 갖고 있다.

Team 엔티티는 여러 멤버는 하나의 팀에 속할 수 있는 다 : 1 관계이므로 Member타입을 갖는 List를 필드로 갖고 있다.

이렇게 두 엔티티가 서로 참조용 필드를 갖고 있기 때문에 양쪽에서 필요한 모든 필드를 조회할 수 있다.

(여기서 연관관계 주인을 지정하는 mappedBy 옵션이 사용되었는데 밑에서 설명한다.)


패러다임의 차이 발생

패러다임의 차이는 단방향에서는 문제가 되지 않지만 양방향에서 문제가 발생한다.

데이터베이스는 외래 키 하나로 두 테이블이 연관관계를 맺지만, 객체의 양방향 관계는 A -> B, B -> A 로 참조가 2군데가 있다.

 

예를 들어 Member, Team 엔티티를 양방향 연관 관계를 갖도록 매핑했을 때 Member가 새로운 Team에 들어가도록 변경한다고 가정해보자.

Member에서 Team을 수정할지, Team에서 List<Member>를 바꿔야할 지 혼란이 온다.

하지만, 데이터베이스에서는 외래키 값 TEAM_ID을 사용하면 된다. 

 

객체 패러다임에서는 두 방식 다 옳지만, 데이터베이스의 패러다임을 적용하는 JPA 입장에서는 혼란스럽다.

 

이러한 패러다임의 차이를 해결하기 위해 양방향 매핑에서 객체의 두 관계 중 하나를 연관관계의 주인으로 지정해야 한다.

연관관계의 주인이 있어야 객체 패러다임에서의 양방향 관계가 데이터베이스 패러다임에서 연관관계가 하나임을 보장할 수 있게 된다.

 

그냥 다 양방향 관계만 사용하면 안돼?

객체 입장에서 양방향 매핑을 했을 때 양쪽으로 신경을 써야하기 때문에 오히려 복잡해질 수 있다.

그래서 기본적으로 단방향 매핑으로 하고 나중에 역방향으로 객체 탐색이 꼭 필요하다고 느낄 때 추가하는 것을 권장한다.


연관관계의 주인(mappedBy)

연관관계의 주인을 지정한다는 것은, 객체의 두 관계 중 제어의 권한(데이터 조회, 저장, 수정, 삭제)를 갖는 실질적인 관계가 무엇인지 JPA에게 알리는 것이다.

따라서 연관관계의 주인은 연관 관계를 갖는 두 객체 사이에서 조회, 저장, 수정, 삭제를 할 수 있지만, 주인이 아니면 조회만 가능하다.

 

누구를 주인으로 지정해줘야 하는가?

N : 1 관계

간단하게 주인은 외래 키가 있는 곳을 주인으로 지정하면 된다.

이 예시에서는 Member 엔티티가 왜래 키(FK)를 갖고 있으므로 연관관계의 주인이 된다.

연관관계의 주인이 아닌 객체는 mappedBy 속성을 사용해 주인 필드의 변수명을 지정해주면 된다.

 


양방향 매핑시 주의 할점

두 관계 양쪽에 값을 넣어주어야 한다.

순수한 객체 관계를 고려하면 항상 양쪽다 값을 세팅해야 한다.

N : 1 관계

만약 역방향(주인이 아닌 방향)만 연관관계 설정 하면 Member 테이블의 TEAM_ID는 null이 들어간다.

Member가 연관관계의 주인이고, Team의 List<Member>는 mappedBy로 읽기 전용이기 때문이다.

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

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

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

em.persist(member);

역방향만 연관관계 설정 시 null 발생

 

다음과 같이 양쪽에 값을 세팅해줘야 값이 정상적으로 들어간다.

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

Member member = new Member();
member.setUsername("member1");
member.addTeam(team); // 연관관계 편의 메서드 사용
em.persist(member);

 

양쪽 다 값을 세팅하기 위해 연관관계 편의 메소드를 생성하는 것을 권장

매번 양방향의 코드를 추가하기 힘들다.

다음과 같이 편의 메소드를 만들어 사용하는 것을 권장한다.

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "MEMBER_ID")
    private Long id;

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

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

	// 연관관계 편의 메서드
    public void addTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
    
    ... 생략
}
양방향 매핑시 무한 루프 주의
  • toString() 메서드나 Lombok을 한 쪽에서만 사용 하면 문제가 되진 않지만 양쪽에서 사용할 경우 무한 루프가 발생하게 된다.
  • JSON은 컨트롤러에서 엔티티를 반환하지 않고 Dto 객체를 따로 만들어서 반환하도록 하자.

 

정리

단방향 매핑만으로도 이미 연관관계 매핑은 충분하다.
처음 설계시에는 단방향 매핑으로 설계를 하고 반대 방향으로 조회가 필요 할 때 추가해도 된다. (테이블에 영향 X)