보통의 RDB에서는 관계에 있어 방향을 따지지 않습니다. 관계를 맺을 때 사용하는 외래키(Foreign Key)
만으로 두 테이블은 서로 양방향으로 접근이 가능하기 때문이죠.
하지만 객체
의 경우 자신이 갖고 있지 않은 참조에 대해 접근할 수 있는 방법이 없기 때문에 @ManyToOne
, @OneToMany
등으로 관계를 매핑한다 하더라도 반대쪽 객체에서는 접근이 불가능 합니다.
간단한 시나리오를 볼께요.
Member
와 Team
두 개 테이블이 있습니다.
Member
는 한 개 Team
에 소속될 수 있고 Team
은 여러 Member
를 포함할 수 있습니다.
이를 JPA
를 이용해 엔티티로 구성해보겠습니다.
@ManyToOne
을 이용해 Member
와 Team
의 관계를 다대일로 설정하고 외래키는 다쪽인 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번은 단순히 Member
와 Team
을 조회한 결과입니다.
중요한 것은 결과 2번입니다.
결과 2번의 첫 번째 쿼리는 굉장히 자연스럽니다. 객체 입장에서도 Member
객체를 통해 Team
을 조회할 수 있죠.
하지만, 결과2의 두 번째 쿼리의 결과를 현재 Member
와 Team
객체가 만들어낼 수 있을까요?
Team
은 Member
객체를 갖고있지 않습니다. 따라서 접근할 수 있는 어떤 방법도 없습니다. 뭐 굳이 하자면 Team
의 PK를 찾아와서 해당 PK를 Member
에 질의하는 방법이 있지만 전혀 객체지향스럽지 않습니다.
우리는 Team
테이블에서 자기 팀에 소속된 전체 Member
를 조회하고 싶습니다. 이것을 위해 양방향 연관관계가 있는 것 입니다.
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
자신이 Member
의 team
에 매핑되어 있으므로 team
으로 설정해준 것 입니다.
간단한 테스트를 통해 결과를 확인해볼께요.
테스트 성공 여부는 실행시 Team
의 members
에 저장한 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
이 세팅될 때 Team
의 List<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);
}
}
team
의 setter
메서드를 그대로 변경하여 사용해도 되지만 setter
의 관례적인 네이밍이 나중에 방해가 될 수 있으므로 되도록 의미있는 이름으로 메서드를 정의합니다.
changeTeam
에 team
이 넘어오면 일단 Member
의 team
에 저장합니다. 이때 저장되는 것은 DB에 외래키로 저장되는 것 입니다. 그리고 team
의 members
에 Member
자체를 넣어줌으로 서로 싱크를 맞추어 줍니다.
간단하게 양방향 연관관계에 대해 알아보았습니다.
양방향 연관관계는 보다 객체지향적으로 객체를 설계하기 위한 기술입니다. 이를 위해 DB에 영향이 없도록 설정을 하는 것 이지요. 바꿔 말하면 양방향 연관관계는 DB에는 영향이 없다는 것 입니다.
따라서 처음 엔티티 구조를 설계할 때는 모두 단방향 연관관계 만으로 설계가 가능합니다. 일단 단방향 연관관계로 모든 관계를 설정하고 필요할 때 객체 레벨의 양방향 연관관계를 설정하는 것이 좋습니다.
감사합니다. 😄
감사합니다