JPA/JPA 기본

17. 양방향 연관관계 주의점

sdafdq 2023. 10. 23. 13:37

JPA입장에서는, 연관관계의 주인에게만 값을 넣어주면 되지만,

사실 객체지향 입장에서는 둘 다 넣어줘야 한다.

 

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

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

em.persist(member);

tx.commit();

여기서 보면,

DB상으로는 문제가 없다.

Team 생성한 다음에, persist 해서 id까지 완벽히 넣고 영속성 컨텍스트에 까지 넣고,

 

Member를 만들어 그 만든 team을 넣어주고, Member도 마찬가지로 영속성 컨텍스트에 넣어준다.

DB상으로는 제대로 반영이 되었다.

근데, 객체관점에서는 

team의 members에 들어가 있지 않은 상태이다.

 

만약 여기서 em.find(Member.class,member.getId()).getTeam();

해서 가져온다고 생각해 보자.

 

이거는 한 트랜잭션이고, 우리가 따로 em.flush() + em.clrear()를 하지 않았기 때문에, 

이미 영속성 컨텍스트에 1차 캐시로 가지고 있어서,

member에 넣은 team에 members를 넣어주지 않았기 때문에 그냥 그 빈 members를 가진 team을 가지고 온다.

 

그래서 문제다.

em.flush() + em.clrear() 하고 가져오면 영속성 컨텍스트에 없어서 select 해서 제대로 모두 조회해서 가지고 오겠지만.

 

 

여기서 재밌는 게 지연 뭐라고,

저렇게 연관관계 있을 경우 

그 연관관계 있는 것을 호출하기 전 까지 DB에 값을 달라고 쿼리를 날리지 않는다.

member같은 경우는 team이 연관관계니까 team을 호출하기 전 까지 DB에 team을 가져오라고 쿼리를 날리지 않음.

 

왜냐하면, 복잡한 애플리케이션 경우 연관관계가 복잡할 수 있다.

근데 그 경우 연관관계를 다 가져오면 비효율적이다.

 

그래서, JPA는 호출을 해야지 비로소 그 때서야 select 해서 그 연관관계의 데이터들을 DB에서 가져온다.

 

호출을 해야만 가져온다.

 

 

여튼, 양방향 연관관계는 항상 둘 다 값을 set 해줘야 한다.

 

이럴 경우, 하면 좋을 게,

뭐 연관관계 편의 메소드 라고도 부르는데,

 

Member가 주인이니(더 명시적이기 때문에), Member에다가

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

이렇게 자기 자신을 추가해 주는 거다.

뭐 이것도 주인에다 할 지 하위에다가 할 지 상황마다 다르긴 하고, 자신이 선택하면 된다고 했다.

 

만약 team에 필요하다고 하면,

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

 

 

기존의 get, set같은 경우는 JPA나 스프링 등에서 활용할 수도 있으니,

아예 새 메소드를 만들어 내가 Member에 team을 넣는 것은 저걸로 쓰는 것이다.

 

원래 저렇게 하면 좀 더 복잡하기는 하다. 

근데 그렇게 깊이 있게 쓸 일은 없다고 한다.

실무에서도 여기까지만 대부분 해도 괜찮을 거라고 한다.

 

 

근데 여기서 주의할 점.

무한루프

예를 들어, 먼저 toString을 보겠다.

다음은, 인텔리제이의 기능으로 자동으로 만들어 주는 toString() 이다.

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

이 기능을 쓰면 이렇게 만들어 주는데,

team의 toString을 호출한다.

 

다음은, Team의 toString을 해 보겠다.

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

저것도, members의 toString을 호출한다.

저거는 member 하나하나 toString을 다 호출한다.

 

그럼 또 team을 toString 했다가..

또 members의 member들을 toString 했다가..

또 member안에 있는 team을 toString 했다가..

또 team안의 members의 member들을..

끝이 없다. 무한루프다.

이거 말고도 lombok, JSON 등.

JSON도 team을 JSON으로 한다고 치면 

team의 id, name 하고,

members도 각각의 member를 json 형태로 만드는데 또 member안에 team이 있으므로 team을 json 형태로 만드는데 team 안에는 또 members가 있으므로...

 

조심해야 한다.

toString(), lombok, JSON 등

 

근데 보통 요새는 API로 많이 쓰는데, 그럼 컨트롤러에서 @Entity 반환하면 안되는 거냐?

네, 반환하지 마세요.

이유는, 우선 이렇게 무한루프에 빠질 수도 있고,

나중에 @Entity 바꿀 요지가 있다.

근데 @Entity 그대로 반환 시 약속해 뒀던 API 스펙이 바뀌게 된다.

 

그래서, Dto로 만들어 반환하는 게 좋다.

 

 

여튼, 근데

양방향 매핑은 필요할 때만 mapped by해서 넣는 걸 추천한다. 위와 같이 저거 말고도 여러가지 문제를 초래할 수 있다.

 

사실, 단방향 매핑만으로도 이미 연관관계의 매핑은 완료이다.

단방향

 

양방향

테이블만 보면 이미 테이블은 단방향 이기 때문에.

 

양방향 매핑은, 반대쪽에서 주인쪽으로 조회기능이 추가된 것 뿐이다.

 

근데 또 JPQL 하다보면 역방향으로 탐색해야 할 때도 많다.

그럴 때만 추가해 주면 된다. 필요할 때만.

 

위 보면, 양방향 매핑 추가가

Team에 List members 이거 하나 추가한 것이다.

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

 

그러니까, 단방향 매핑으로 일단 클래스 설계를 마친 다음에,

필요할 때만 양방향 추가. 위에 저거 넣는 것 처럼.

 

객체지향 관점에서 볼 때 양방향이 이득이 많이 없다고 함.

일대다 일 때 다 쪽에 연관관계 넣고 필요할 때만 양방향.