DEV ℧ Developer Diary

[JPA] 양방향 연관관계 (2) - 양방향 매핑 주의사항

해당 포스트는 인프런 김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 을 듣고 정리한 글입니다.

양방향 매핑 주의사항

연관관계의 주인을 미입력시

/* Team class의 양방향 매핑 관계 */
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
Member member = new Member();
member.setUsername("member1");
entityManager.persist(member);

Team team = new Team();
team.setName("TeamA");
team.getMembers().add(member);
entityManager.persist(team);

entityTransaction.commit();
/* insert com.dhaudgkr.jpa02.domain.Member */
insert
into
    Member
    (TEAM_ID, USERNAME, id)
values
    (?, ?, ?)

/* insert com.dhaudgkr.jpa02.domain.Team */
insert
into
    Team
    (name, id)
values
    (?, ?)

주의1

위의 예시를 실행했을때 insert SQL 쿼리가 날라갔음에도 데이터가 누락이 되어있음

mappedBy로 지정된 경우 읽기전용이라 엔티티데이터를 수정해도 update가 날아가지 않는다.

member에 team의 값을 넣어주지않아 생김

해당 이슈를 지양하기 위해서 연관관계인 두 Entity에 모두 데이터를 넣어주는것이 낫다.

한쪽만 데이터를 넣을 경우 한 트랜잭션 내에 인서트 쿼리가 닐라가지 않아 1차 캐시 내부에서 데이터를 조회해 오는데 해당 컬렉션의 데이터가 null이 들어가있을 수 있다.

그렇다면 양쪽다 데이터를 넣어주기 위해서는 어떻게 해야할까?

양방향 매핑시 연관관계의 주인에 값을 입력해 주면된다.

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

Team team = new Team();
team.setName("TeamA");
/* member객체에 team데이터를 넣어주는 메서드 생성 */
team.addMember(member);
entityManager.persist(team);

entityTransaction.commit();
@Entity
@Getter @Setter
public class Team {
  ...
  @OneToMany(mappedBy = "team")
  private List<Member> members = new ArrayList<>();

  /* member Entity의 team객체에 this를 통해 변경된 Team Entity를 넣어준다. */
  public void addMember(Member member) {
    members.add(member);
    member.setTeam(this);
  }
}

주의2

데이터가 제대로 들어간 것을 확인 할 수 있다.

양방향 연관관계 주의

  • 순수 객체상태를 고려해서 항상 양쪽에 값을 설정하자
  • 연관관계 편의 메소드를 생성하자
  • 양방매핑시 무한 루프를 조심하자
    • toString, lombok, JSON 라이브러리
    • lombok 에서 tostring 은 제외할것
    • entity를 controller에서 response로 반환하면 안된다 dto로 가공해서 반환할것
  • 값을 셋팅할 주체는 둘중 한군데로 정해주면 된다 한군데에 둘다 값을 셋팅

잘못된 예시를 들어보자.

/* Member Entity와 Team Entity에 각각 toString Override */
@Entity
@Getter @Setter
public class Team {

  @Id @GeneratedValue
  private Long id;

  private String name;

  @OneToMany(mappedBy = "team")
  private List<Member> members = new ArrayList<>();

  public void addMember(Member member) {
    members.add(member);
    member.setTeam(this);
  }

  @Override
  public String toString() {
    return "Team{" +
            "id=" + id +
            ", name='" + name + '\'' +
            ", members=" + members +
            '}';
  }
}

@Entity
@Getter @Setter
public class Team {

  @Id @GeneratedValue
  private Long id;

  private String name;

  @OneToMany(mappedBy = "team")
  private List<Member> members = new ArrayList<>();

  public void addMember(Member member) {
    members.add(member);
    member.setTeam(this);
  }

  @Override
  public String toString() {
    return "Team{" +
            "id=" + id +
            ", name='" + name + '\'' +
            ", members=" + members +
            '}';
  }
}
/* find해서 찾아온 Team Entity를 호출 */
Team findTeam = entityManager.find(Team.class, team.getId());
System.out.println(findTeam);
Exception in thread "main" java.lang.StackOverflowError
	at java.base/java.lang.Long.toString(Long.java:1385)
	at java.base/java.lang.String.valueOf(String.java:2951)
	at com.dhaudgkr.jpa02.domain.Team.toString(Team.java:29)
	at java.base/java.lang.String.valueOf(String.java:2951)
	at com.dhaudgkr.jpa02.domain.Member.toString(Member.java:28)
	at java.base/java.lang.String.valueOf(String.java:2951)
	at java.base/java.lang.StringBuilder.append(StringBuilder.java:168)
	at java.base/java.util.AbstractCollection.toString(AbstractCollection.java:473)
	at org.hibernate.collection.internal.PersistentBag.toString(PersistentBag.java:622)
	at java.base/java.lang.String.valueOf(String.java:2951)
	at com.dhaudgkr.jpa02.domain.Team.toString(Team.java:29)
	at java.base/java.lang.String.valueOf(String.java:2951)
	at com.dhaudgkr.jpa02.domain.Member.toString(Member.java:28)
	at java.base/java.lang.String.valueOf(String.java:2951)
	at java.base/java.lang.StringBuilder.append(StringBuilder.java:168)
	at java.base/java.util.AbstractCollection.toString(AbstractCollection.java:473)
	at org.hibernate.collection.internal.PersistentBag.toString(PersistentBag.java:622)
	...

findTeam을 통해 찾아온 Team Entity의 toString을 호출할시 Team의 toString에 있는 member를 호출하고, member의 toString에서 다시 team을 호출하는 등의 반복적인 행위가 일어나 StackOverflowError가 일어난다.

정리

  • 단방향 매핑만으로도 이미 연관관계 매핑은 완료
  • 양방향 매핑은 반대 방향으로 조회 (객체 그래프 탐색) 기능이 추가된 것 뿐
  • JPQL에서 역방향으로 탐색할 일이 많다
  • 양방향 매핑은 무조건 하는것이 아니라 단방향매핑을 달하고 꼭 필요할때 양방향매핑을 넣을것
  • (테이블에 연관을 주지 않는다.)

연관관계의 주인을 정하는 기준

  • 비지니스 로직을 기준으로 연관관계의 주인을 선택하면 안된다
  • 연관관계 주인은 외래키의 위치를 기준으로 선택해야 한다.

정리

단방향을 잘이용해서 매핑하는것이 중요하다.