본문 바로가기
Framework and Tool/JPA

JPA - 연관관계 맵핑 - 양방향 연관

by ocwokocw 2021. 7. 1.

- 참조: 자바 ORM 표준 JPA 프로그래밍

- 양방향 연관관계

이전 예제에서는 회원에서 팀으로 접근하는 단방향 연관관계를 알아보았다. 이번에는 팀에서 회원으로 접근할 수 있는 양방향 연관관계를 맵핑해본다.

위 다이어그램에서 회원과 Team은 다대일 연관관계이다. 회원은 아직 팀에 속해있지 않을 수도 있고, 1개팀에 속할수도 있지만 2개 이상팀에는 속할수가 없기 때문에 다중성의 하한과 상한은 0..1이다. 반면 팀은 여러명의 멤버들로 구성될 수 있기 때문에 다중성을 * 이며, Type은 User 이다.

 

DB 입장에서는 달라지는 내용이 전혀 없다. 어차피 외래키 하나로 Join 하면 되기 때문에, 기존의 형상을 유지한다.

User 는 이전소스와 달라지는것이 없다. Team 에서 User 를 참조하기 위한 연관관계 맵핑을 추가해보자.

 

@Entity
public class Team {

	@Id @GeneratedValue
	@Column(name = "TEAM_ID")
	private Long id;
	private String name;
	
	@OneToMany(mappedBy = "team")
	private List<User> members;

	public Team() {
		super();
		members = new ArrayList<>();
	}

	public Long getId() {
		return id;
	}
	public void setId(Long id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public List<User> getMembers() {
		return members;
	}
	public void setMembers(List<User> members) {
		this.members = members;
	}

	@Override
	public String toString() {
		return "Team [id=" + id + ", name=" + name + "]";
	}
}

 

List<User> 형을 추가한 후 @OneToMany 어노테이션을 달아주었다. @JoinColumn 을 사용해야지라고 생각했는데 갑자기 mappedBy 속성이 나온다. mappedBy 는 양방향 연관관계일 때 사용하는데, 다음 구절인 연관관계 주인에서 설명한다. 지금은 mappedBy 의 값은 반대편 엔티티의 필드 이름을 설정한다고만 생각하자.

 

DB 에 Team1 와 User1, User2 가 있고, User1,2 에 TEAM_ID 가 맵핑되어있다고 가정하고 아래 코드를 확인해보자. TeamId 로 검색한 후, findTeam1.getMembers() 메소드로 회원들 참조를 불러온다.

 

실행된 SQL을 보면 TEAM_ID 로 해당 Team 을 조회한 후, User 와 조인하여 불러오는것을 알 수 있다.

 

EntityManager em1 = emf.createEntityManager();
EntityTransaction tx1 = em1.getTransaction();
tx1.begin();

Team findTeam1 = em1.find(Team.class, team1Id);
List<User> team1Memebers = findTeam1.getMembers();

team1Memebers.forEach(System.out::println);

tx1.commit();
em1.close();


====실행결과
Hibernate: 
    select
        team0_.team_id as team_id1_0_0_,
        team0_.name as name2_0_0_ 
    from
        team team0_ 
    where
        team0_.team_id=?
Hibernate: 
    select
        members0_.team_id as team_id3_1_0_,
        members0_.user_id as user_id1_1_0_,
        members0_.user_id as user_id1_1_1_,
        members0_.team_id as team_id3_1_1_,
        members0_.user_name as user_nam2_1_1_ 
    from
        user members0_ 
    where
        members0_.team_id=?
User [id=1, userName=User1, team=Team [id=3, name=Team1]]
User [id=2, userName=User2, team=Team [id=3, name=Team1]]

- 연관관계의 주인

양방향 연관관계 맵핑시 @JoinColumn 이 아니라 뜬금없이 mappedBy 속성이 나왔었다. 이전 글에서 엔티티간 양방향 연관관계는 엄밀히 말하면 서로에 대한 단방향 연관관계이며, DB 에서는 연관이 있는 테이블 양쪽 중 1곳에만 외래키를 두면 어떤 순서로든 조인이 가능하다고 했었다.

 

객체는 서로를 참조하지만 DB 는 1곳에서만 외래키를 사용한다. 이 차이를 해결하기 위해서 mappedBy 속성을 이용하여 객체중 어느 객체에서 외래키를 관리해야할지를 설정해야 한다. mappedBy 는 한마디로 말하면 외래키를 관리하는 주체이다. JPA 에서는 이를 "연관관계의 주인" 이라고 한다.

 

양방향 연관관계에서는 규칙이 있는데, 두 연관관계 중 하나를 주인으로 정해야 한다. 연관관계의 주인만이 외래키를 관리할 수 있다. 주인은 mappedBy를 사용하지 않으며, 주인이 아닌 객체에서 mappedBy 로 주인의 필드를 참조한다.

 

그렇다면 왜 연관관계의 주인은 Team 이 아니라 User 인가? 연관관계의 주인은 외래키를 관리한다고 했다. 이 말을 그대로 대입시켜볼 때 Team 을 주인으로 정하면 외래키가 User 에게 있는데 Team 이 외래키를 관리해야한다는 말이 된다. 

 

DB 테이블에서는 1 : N 관계에서 항상 N 쪽이 외래키를 갖는다. 따라서 @ManyToOne 은 mappedBy를 가질 수 없다. 실제로도 자동완성을 해보면 mappedBy 속성을 찾을 수 없을것이다.


- 연관관계의 저장

예제코드에서 연관관계를 설정할 때 아래와 같이 User 에만 설정하였다.

 

user1.setTeam(team1);
user2.setTeam(team1);

 

Team 에서 참조를 하려면 team1.getMembers.add(user1); 과 같은 코드가 있어야하지 않을까 라고 생각할수도 있다. 하지만 외래키는 User 에서 관리하고 있기 때문에 외래키의 주인인 User 가 Team 을 참조하는 코드만 있으면 연관관계를 저장할 수 있다.


- 양방향 연관관계 주의점

연관관계의 주인이 정말 중요할까? 연관관계의 주인이 아닌쪽에서 저장을 하면 동작을 하지 않을까? 아래와 같이 User1, User에 연관관계가 설정되어있지 않다고 가정하자.

그리고 User1, Uesr2 를 Team 의 members 참조에 추가한다.

 

tx1.begin();

User user1 = new User();
user1.setUserName("User1");
em1.persist(user1);

User user2 = new User();
user2.setUserName("User2");
em1.persist(user2);

Team team1 = new Team();
team1.setName("Team 1");
em1.persist(team1);

team1.getMembers().add(user1);
team1.getMembers().add(user2);

tx1.commit();

 

H2 DB를 확인해보지 않고 Log만 보아도 알 수 있듯이 update 문 자체가 없다. 물론 H2 DB 를 조회해도 USER 의 TEAM_ID 컬럼은 null 값이다.


- 연관관계 메소드

만약 JPA 가 아니라면 user1.setTeam(team1) 로 연관관계 설정이 끝나지 않고, team1 에서도 user1 을 참조하는 코드를 작성해야 한다. JPA 를 배우고 있는데 순수객체까지 굳이 신경쓸필요가 있을까?

 

만약 Test 코드를 잘 짜고 싶은 욕심이있다면 신경을 써야한다. 정말 부지런하고 수준높은 코드를 작성한다면 비즈니스 로직을 순수코드로 Test 할 수 있다. JPA 를 사용함에 불편함이 없으면서 Test 까지 고려하여 코드를 작성할 수 있을까? User의 setTeam 을 아래와 같이 바꾸어 보자.

 

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

 

Team 의 member 에도 User 자신을 참조로 넣어주었다. setter/getter 란 필드를 private 로 선언하고 자동완성하는것이라는 인식을 넘어서 setter 도 하나의 메소드임을 명심하도록 하자.

 

"이제 완벽하다." 나 역시도 그렇게 생각했지만 사실 고려해야할 부분이 하나 더 있다. 아래 코드를 보자. user1 의 Team 을 team1 으로 지정했다가 team2 로 변경한다. 그리고 team1 의 멤버를 출력한다. 

 

tx1.begin();

User user1 = new User();
user1.setUserName("User1");
em1.persist(user1);

User user2 = new User();
user2.setUserName("User2");
em1.persist(user2);

Team team1 = new Team();
team1.setName("Team 1");
em1.persist(team1);

Team team2 = new Team();
team2.setName("Team 2");
em1.persist(team2);

user1.setTeam(team1);
user1.setTeam(team2);

System.out.println("Print team1 members");
team1.getMembers().forEach(System.out::println);

tx1.commit();
Print team1 members
User [id=1, userName=User1, team=Team [id=4, name=Team 2]]

 

team1 의 멤버를 출력한 결과는 없어야 한다. 아래 User 테이블을 기준으로 Left outer join 을 해보면 DB 상으로도 Team1 에 속하는 User 는 없다.

결과가 왜 위와 같이 나오는지는 쉽게 예상할 수 있다. 위와 같은 상황이 있을수도 있으니 Uesr의 setTeam 메소드를 더 발전시켜야 한다는 생각을 못했을뿐이다. 배우는 과정이니 괜찮다. User의 setTeam 메소드를 고쳐보자.

 

public void setTeam(Team team) {
	
	if(this.team != null) {
		this.team.getMembers().remove(this);
	}
	
	this.team = team;
	team.getMembers().add(this);
}

 

User 가 현재 속한 Team 이 null 이 아니면 자신을 해당 팀에서 제거한다. 책에는 위의 코드에서 끝났지만 내 생각에는 하나의 경우를 더 처리해야 한다. User 가 Team 을 탈퇴하여 어느 팀에도 속하지 않을 수 있기 때문이다.

 

user1.setTeam(team1);
user1.setTeam(team2);
user1.setTeam(null);

.....

public void setTeam(Team team) {
	
	if(this.team != null) {
		this.team.getMembers().remove(this);
	}
	
	this.team = team;
	
	if(team != null) {
		team.getMembers().add(this);
	}
}

댓글