JPA - 양방향 연관관계 ( mappedBy )

Kim Dae Hyun·2021년 7월 27일
15

JPA

목록 보기
4/4
post-thumbnail
post-custom-banner

🔎 객체 간의 관계를 정말 테이블처럼 하기 위한 ..

보통의 RDB에서는 관계에 있어 방향을 따지지 않습니다. 관계를 맺을 때 사용하는 외래키(Foreign Key) 만으로 두 테이블은 서로 양방향으로 접근이 가능하기 때문이죠.

하지만 객체의 경우 자신이 갖고 있지 않은 참조에 대해 접근할 수 있는 방법이 없기 때문에 @ManyToOne , @OneToMany 등으로 관계를 매핑한다 하더라도 반대쪽 객체에서는 접근이 불가능 합니다.

간단한 시나리오를 볼께요.
MemberTeam 두 개 테이블이 있습니다.
Member는 한 개 Team에 소속될 수 있고 Team은 여러 Member를 포함할 수 있습니다.
이를 JPA를 이용해 엔티티로 구성해보겠습니다.

@ManyToOne 을 이용해 MemberTeam의 관계를 다대일로 설정하고 외래키는 다쪽인 Member에 두었습니다.

테스트를 위해 간단한 데이터를 넣어볼께요.

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

Member member = new Member();
member.setUsername("kim");
member.setTeam(team);

em.persist(member);
tx.commit();

H2 DB를 이용해 결과를 확인해볼께요.

결과 1번은 단순히 MemberTeam을 조회한 결과입니다.
중요한 것은 결과 2번입니다.
결과 2번의 첫 번째 쿼리는 굉장히 자연스럽니다. 객체 입장에서도 Member객체를 통해 Team을 조회할 수 있죠.

하지만, 결과2의 두 번째 쿼리의 결과를 현재 MemberTeam객체가 만들어낼 수 있을까요?
TeamMember객체를 갖고있지 않습니다. 따라서 접근할 수 있는 어떤 방법도 없습니다. 뭐 굳이 하자면 Team의 PK를 찾아와서 해당 PK를 Member에 질의하는 방법이 있지만 전혀 객체지향스럽지 않습니다.

우리는 Team 테이블에서 자기 팀에 소속된 전체 Member를 조회하고 싶습니다. 이것을 위해 양방향 연관관계가 있는 것 입니다.


🔎 양방향 연관관계 mappedBy

Team 객체에서 자기 팀에 포함된 모든 Member를 조회하려면 List<Member> 타입의 필드를 가져야 합니다.
해당 필드를 생성하고 Team은 여러 Member를 포함하니 일대다 매핑을 해주도록 합니다.

@Getter @Setter
@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;
    private String teamName;
    @OneToMany
    private List<Member> members = new ArrayList<>();
}

일단 객체 입장에서 참조 관계가 양방향이 되었으니 1차적으로 목표는 달성했지만 DB 입장을 고려하여 추가적인 설정을 반드시 필요로 합니다.

앞서 말했지만 DB는 외래키만으로 관계를 맺습니다. 바꿔말해 외래키 하나만 잘 관리되면 되는 것 입니다. 두 객체를 다시 보시죠.

현재 구조는 외래키가 양쪽에 모두 있는 것 같은 구조입니다.
외래키를 갖는 쪽 즉, 연관관계의 주인이 되는 쪽을 정해주어야 합니다.

연관관계의 주인은 1:N의 경우 N 쪽으로 해주면 됩니다.

연관관계의 주인을 설정할 때 주인을 따로 설정하는 것이 아니고 자신이 이 연관관계의 주인이 아님을 설정해줘야 합니다.

자신이 연관관계의 주인이 아닌 것을 표시하는 설정이 mappedBy 입니다.

@OneToMany(mappedBy = "team")
위와 같은 포맷이고 mappedBy의 값은 반대쪽에 자신이 매핑되어 있는 필드명을 써주시면 됩니다. 예제의 경우 Team 자신이 Memberteam에 매핑되어 있으므로 team으로 설정해준 것 입니다.

간단한 테스트를 통해 결과를 확인해볼께요.

테스트 성공 여부는 실행시 Teammembers에 저장한 member가 콘솔에 찍혀야 하고 Team 테이블에는 Member에 대한 데이터가 없어야 합니다.

tx.begin();

try{
  Team team = new Team();
  team.setTeamName("teamA");
  em.persist(team);

  Member member = new Member();
  member.setUsername("kim");
  member.setTeam(team);

  team.getMembers().add(member);

  em.persist(member);

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

  List<Member> findMembers = team.getMembers();
  for (Member findMember : findMembers) {
  	System.out.println("findMember.getUsername() = " + findMember.getUsername());
  }

tx.commit();

결과


🔎 연관관계 편의 메서드

위 예제를 해볼 때 살짝 불편한 느낌이 드는 코드가 있었습니다.

team.getMembers().add(member);

이 부분인데요.
조회를 위한 양방향 연관관계 설정으로 직접 생성된 member를 team에 넣어주었습니다.
이렇게 양방향 연관관계에 있는 객체에 데이터를 업데이트 할 때마다 이런 식으로 하나하나 찝어서 넣어줘야 할까요?

아닙니다. 너무 실수의 여지가 많고 보다 편리한 객체 그래프 탐색을 위한 양방향 연관관계가 더 불편한 결과를 초래할 수 있습니다.

이 문제를 해결하기 위해 Member 쪽에 Team이 세팅될 때 TeamList<Member>에 동시에 값을 넣어주는 메서드를 정의합니다.

@Getter @Setter
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

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

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

    public void changeTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}

teamsetter 메서드를 그대로 변경하여 사용해도 되지만 setter의 관례적인 네이밍이 나중에 방해가 될 수 있으므로 되도록 의미있는 이름으로 메서드를 정의합니다.

changeTeamteam이 넘어오면 일단 Memberteam에 저장합니다. 이때 저장되는 것은 DB에 외래키로 저장되는 것 입니다. 그리고 teammembersMember 자체를 넣어줌으로 서로 싱크를 맞추어 줍니다.


간단하게 양방향 연관관계에 대해 알아보았습니다.
양방향 연관관계는 보다 객체지향적으로 객체를 설계하기 위한 기술입니다. 이를 위해 DB에 영향이 없도록 설정을 하는 것 이지요. 바꿔 말하면 양방향 연관관계는 DB에는 영향이 없다는 것 입니다.
따라서 처음 엔티티 구조를 설계할 때는 모두 단방향 연관관계 만으로 설계가 가능합니다. 일단 단방향 연관관계로 모든 관계를 설정하고 필요할 때 객체 레벨의 양방향 연관관계를 설정하는 것이 좋습니다.

감사합니다. 😄

profile
좀 더 천천히 까먹기 위해 기록합니다. 🧐
post-custom-banner

1개의 댓글

comment-user-thumbnail
2022년 7월 27일

감사합니다

답글 달기