티스토리 뷰

JPA

[JPA] 양방향 연관관계

Jaime.Lee 2022. 6. 29. 10:30

소스 코드는 여기 있습니다. (commit hash: e336ad7)

> git clone https://github.com/lcalmsky/jpa
> git checkout e336ad7

Overview

양방향 연관관계를 설명합니다.

객체 지향 모델링

양방향 연관관계는 객체가 서로 참조할 수 있는 것을 의미합니다.

따라서 객체간의 관계는 아래 처럼 나타낼 수 있습니다.

테이블 연관관계는 이전과 동일합니다. DB에서는 team_id 만으로도 Member를 찾을 수 있기 때문입니다.

테이의 연관관계에는 방향이라는 개념이 없습니다. FK 하나로 충분히 연관관계를 파악할 수 있기 떄문입니다.

양방향 연관관계 구현

이전 포스팅에서 이미 단방향 연관관계를 구현해놓았기 때문에 양방향 관계를 맺기 위해선 Team 쪽에만 관계를 추가해주면 됩니다.

Member에서 Team@ManyToOne으로 지정했으므로 Team에서 Member의 관계를 지정할 땐 그 반대로 해주시면 됩니다.

/src/main/java/com/tistory/jaimenote/jpa/domain/entity/Team.java`

package com.tistory.jaimenote.jpa.domain.entity;

import java.util.ArrayList;
import java.util.List;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.ToString.Exclude;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@ToString // (2)
public class Team {

  @Id
  @GeneratedValue
  private Long id;

  private String name;

  @OneToMany(mappedBy = "team") // (1)
  @Exclude // (2)
  private List<Member> members = new ArrayList<>(); // (3)

  private Team(String name) {
    this.name = name;
  }

  public static Team withName(String name) {
    return new Team(name);
  }
}

(1) @OneToMany 애너테이션을 사용하고 mappedBy 속성을 team으로 지정합니다. 이는 선언된 membersteam에 의해 매핑된다는 뜻입니다.
(2) @ToString을 사용할 때 서로 순환참조 되어 무한루프에 빠질 수 있는 항목에 @Exclude를 추가해줍니다.
(3) JPA에서 Collection을 사용할 때는 선언과 동시에 초기화해주는 것이 NullPointerException을 방지하는데 도움이 됩니다.

Member.java 보기
package com.tistory.jaimenote.jpa.domain.entity;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@ToString
public class Member {

  @Id
  @GeneratedValue
  private Long id;

  private String name;

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

  private Member(String name) {
    this.name = name;
  }

  public static Member withName(String name) {
    return new Member(name);
  }

  public void join(Team team) {
    this.team = team;
  }
}

테스트

Team을 조회해 Member를 찾을 수 있는지 확인해보겠습니다.

/src/test/java/com/tistory/jaimenote/jpa/infra/repository/MemberRepositoryTest.java`

// 생략
@DataJpaTest
class MemberRepositoryTest {

  @Autowired
  MemberRepository memberRepository;
  @Autowired
  TeamRepository teamRepository;
  @PersistenceContext
  EntityManager entityManager;

  @BeforeEach
  void setup() {
    Team team = Team.withName("토트넘");
    teamRepository.save(team);
    Member member = Member.withName("손흥민");
    member.join(team);
    memberRepository.save(member);
    entityManager.flush();
    entityManager.clear();
  }
  // 생략
  @Test
  void findMemberByTeam() {
    List<Team> teams = teamRepository.findAll();
    for (Team team : teams) {
      System.out.println(team);
      System.out.println(team.getMembers());
    }
  }
}

데이터를 setup하는 부분을 제외하고 team과 member를 조회한 부분을 확인해보면,

Hibernate: 
    select
        team0_.id as id1_1_,
        team0_.name as name2_1_ 
    from
        team team0_
Team(id=1, name=토트넘)
Hibernate: 
    select
        members0_.team_id as team_id3_0_0_,
        members0_.id as id1_0_0_,
        members0_.id as id1_0_1_,
        members0_.name as name2_0_1_,
        members0_.team_id as team_id3_0_1_ 
    from
        member members0_ 
    where
        members0_.team_id=?
2022-06-26 22:18:03.440 TRACE 31087 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
[Member(id=2, name=손흥민, team=Team(id=1, name=토트넘))]

이렇게 team_id로 다시 member를 찾아 출력한 것을 확인할 수 있습니다.

객체와 테이블의 관계 맺는 방식 차이

위에서 확인했듯이 양방향 연관관계의 경우 객체간 연관관계는 2개, 테이블간 연관관계는 1개가 됩니다.

객체입장에서는 양방향 관계는 곧 서로를 향한 단방향 연관관계를 의미합니다.

  • Member -> Team
  • Team -> Member

테이블 입장에서는 FK 하나로 두 테이블의 연관관계를 관리할 수 있습니다.

  • member <-> team

따라서 테이블의 FK와 같은 역할을 할 수 있는 값을 Member 또는 Team 객체에서 관리해주어야 합니다.

이 때 관리의 주체를 연관관계의 주인이라고 부릅니다.

다음 포스팅에서 확인해보겠습니다.

댓글