티스토리 뷰

프로그래밍/JPA

양방향 연관관계 매핑

열무룩 2021. 4. 27. 22:40

바로 직전 포스팅에서는 단방향 매핑에 대해서 살펴보았다. 

단방향으로 매핑해주었기 때문에 한쪽에서 반대 방향으로 (예시의 경우 Member -> Team) 호출할 수 있었지만, 그 반대로는 불가능했다. 양방향 매핑으로 바꿔서 양쪽으로 참조가 될 수 있도록 해보겠다.

 

 

Mapping


우선, 사실 테이블의 연관관계에서는 외래 키를 통해서 조인이 되기 때문에 방향성이라는 개념이 존재하지 않는다. 

그렇기 때문에 테이블 연관관계의 형태를 살펴보면 단방향 매핑과 달라진 것이 없는 것을 알 수 있다.

 

참고 이미지 - by 김영한님

반대로 객체의 경우에는 양방향으로 연관관계를 맺어주기 위해서는 별도의 매핑 작업이 필요하다.

 

이번에는 Team의 입장에서 생각해보아야 한다. 

Team의 입장에서는 다수의 Member가 소속될 수 있기 때문에 Member와는 반대로 매핑이 된다.

 

즉! Team이 1 그리고 Member가 다수, 1 대 N 관계가 성립된다.

 

역시 이것을 JPA에서는 앞에 오는 위치가 본인을 가리키도록 하여서, Team 입장에서는 Member와 일 대 다수(N)의 관계를 가지므로, @OneToMany 라는 Annotation을 제공한다.

그래서 Team의 입장에서 Member를 가져오기 위해서는 List의 형태로 가져와야 하는 것이다. 

 

@Entity
public class Team {

    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;

    private String name;

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

    // getter setter
}

이것을 코드로 나타내면 위와 같다. 여기서 중요한 것이 mappedBy 속성을 명시해주어야 하는데, 이것은 일대다 매핑에서 어디와 연결이 되는지 필드명을 명시해주는 것이다. 즉, "이 필드와 매핑되어 있다." 라는 뜻이다.

지금의 경우는 Member에서 team이라는 필드와 매핑되어 있기 때문에 team으로 명시하였다. 

이렇게 매핑을 시키면 아래와 같이 Member에서 Team을 가져올 수 있다.

 

Member findMember = em.find(Member.class, member.getId());

List<Member> members = findMember.getTeam().getMembers();

for (Member m : members) {
    System.out.println("m = " + m.getUsername());
}

 

 

딜레마, 연관관계의 주인


그럼 이 시점에서 이렇게 양방향으로 연관관계를 맺어보니.. 한 가지 딜레마가 발생한다.

 

가령,

 

MemberTeam을 업데이트 했을 때 TEAM_ID가 업데이트 되어야하는지?
Team
members를 업데이트 했을 때 TEAM_ID가 업데이트 되어야하는지?

 

, MEMBER 테이블의 특정 memberTEAM_ID를 바꾸고 싶다면 두 Entity 중 어느 쪽을 수정해야 하는 건지?

 

이에 따라서, 두 Entity 중에서 Table의 외래 키를 관리할 Entity를 정해야 하는 필요성이 생긴다. 

이처럼 연관관계에 있는 두 Entity 중에서 외래 키를 관리하는 Entity가 연관관계의 주인이 된다.

연관관계의 주인만이 외래 키를 관리할 수 있고, 반대쪽은 읽기만 가능하게 된다. 

또한, 주인은 mappedBy 속성을 사용하지 않지만 반대의 경우에는 mappedBy를 사용하여 종속되어 있음을 표기한다.

 

따라서, 어느 쪽이 연관관계의 주인인지 살펴보고자 한다면, 서로 매핑되어 있는 필드에서 mappedBy 속성이 어느 쪽에 잡혀있는지 살펴보면 된다. 

 

// Member Entity

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

// Team Entity

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

 

 

위의 경우에서는, Member Team은 TEAM_ID와 완전히 매핑을 한 형태이고, TeamMembersmappedByteam에 의해 관리된다라고 명시되어 있는 형태. , 주인은 MemberTeam이 된다.

 

* 누구를 주인으로 해야 하는가?

참고 이미지 - by 김영한님

사실 어느 쪽을 주인으로 해야 한다 라는 정답은 없다. 

하지만, 쉽게 외래 키를 가지는 쪽을 주인으로 정하면 개발에 편리하다.

 

테이블에서 잘 생각해보면, 외래 키가 있는 곳은 항상 N(다수) 이다. 그리고 그 반대, 외래 키가 없는 곳은 항상 1이다.

그 말은 즉, N쪽이 연관관계의 주인이 되고, @ManyToOne이 된다.

 

 

주의할 점


1. 연관관계에 있는 값을 설정할 때, 연관관계 주인에 값을 입력하지 않은 경우, 실제 Data가 insert가 되지 않는다.

 

Member가 연관관계의 주인일 때, 

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

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

위처럼 Member(주인) 쪽에 값을 입력하지 않으면, 주인이 아닌 쪽은 읽기 전용이기 때문에 실제 Data가 insert 되지 않는다. 아래와 같이 주인 쪽에 값을 입력해주어야 한다.

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


Member member = new Member();
member.setUsername("member1");
member.setTeam(team);
em.persist(member);

 

2. 순수한 객체 관계를 고려한다면 양쪽 모두 값을 입력해주어야 한다.

 

우선, 아래의 Case를 살펴보자.

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

Member member = new Member();
member.setUsername("member1");
member.setTeam(team);
em.persist(member);

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

Team findTeam = em.find(Team.class, team.getId());
List<Member> members = findTeam.getMembers();
for (Member m : members) {
    System.out.println("m = " + m.getUsername());
}

tx.commit();

Member에 있는 Team을 설정해주기만 했는데도 위 코드에서 findTeam은 이상 없이 잘 출력된다.

이것은 JPA의 동작 방식에 의한 것으로 members를 사용하는 시점에 select 쿼리를 날려 값을 넣기 때문이다.

 

이 부분을 잘 생각해보면, 위 코드에서 flush와 clear를 생략하면? DB에 싱크로나이징이 안된 상태라면?

이 경우에는 members가 조회되지 않는다. 그 이유는 team1차 캐시에만 올라가 있기 때문으로 개발을 하다 보면 이러한 예상치 못한 버그가 발생할 수 있다.

따라서 양방향 매핑할 때는 사실 양쪽 모두에 값을 세팅해주는 것이 맞다.

 

그러나 매번 

member.setTeam(team);
team.getMembers().add(member);

개발을 하면서 이렇게 의식적으로 양방향 모두 세팅해주는 것은 사실상 어렵기 때문에,

 

별도의 메소드 안에 심어주면 편리하다. 이 메소드의 이름은 set이라는 키워드를 사용하기보다는 change / add 등과 같이 관례상 사용하는 setter가 아님을 명시해주는 것이 바람직하다.

 

그래서 양방향 관계에 있는 둘 중 어느 쪽에 심어줄지는 더 자주 사용하는 쪽을 정해서 사용하면 된다. 

다만, 둘 다 사용하는 것은 버그 발생의 가능성이 있기 때문에 하나만 사용하도록 하자.

 

// 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);
}

 

 

@ 출처 : inflearn 자바 ORM 표준 JPA 프로그래밍 - 기본편 (by 김영한님)

'프로그래밍 > JPA' 카테고리의 다른 글

단방향 연관관계 매핑  (0) 2021.04.27
Entity 매핑하기  (0) 2021.04.26
영속성 컨텍스트  (0) 2021.04.24
Hello JPA  (0) 2021.04.21
왜 JPA인가?  (0) 2021.04.21
댓글
링크
최근에 올라온 글
Total
Today
Yesterday