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

반응형

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

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

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

 

양방향과 단방향 매핑

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

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

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

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

etc-image-0
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

객체에서 양방향과 단방향

객체는 참조용 필드가 있는 객체만 다른 객체를 참조하는 것이 가능하다.

아래의 단방향 관계와 양방향 관계를 보자.

 

단방향 관계

etc-image-1
etc-image-2

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

즉, Member 엔티티는 Team 엔티티의 모든 필드를 조회할 수 있지만 Team 엔티티는 Member 엔티티의 필드를 조회할 수 없다.

 

 

양방향 관계

etc-image-3
etc-image-4

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에게 알리는 것이다.

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

 

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

etc-image-5
N : 1 관계

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

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

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

etc-image-6

 


양방향 매핑시 주의 할점

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

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

etc-image-7
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);

etc-image-8
역방향만 연관관계 설정 시 null 발생

 

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

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

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

etc-image-9

 

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

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

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

@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)