🌱 SPRING/JPA

[JPA] 연관관계 매핑 기초

1HOON 2020. 7. 4. 15:55

연관관계가 필요한 이유


아래와 같은 시나리오를 가정해보자.

 

  • 회원과 팀이 있다.
  • 회원은 하나의 팀에만 소속될 수 있다.
  • 회원과 팀은 다대일(N:1) 관계다.

 

이 시나리오대로 테이블을 모델링하면 아래와 같이 모델링이 된다.

테이블 모델링

그렇다면 이 시나리오를 테이블에 맞춰 Entity객체로 변경하면 어떻게 될까?

테이블에 맞춘 객체 모델링

보이는 것과 같이, Member 객체에 teamId를 추가해 소속 팀을 입력하고있다. 이렇게 객체가 설계된다면 새 사용자를 만들때 코드는 아래와 같이 외래키를 직접 다루게 된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
// 새 팀 생성 및 저장
Team team = new Team();
team.setName("팀이름");
 
entityManager.persist(team);
 
// 새 사용자 생성 및 저장
Member member = new Member();
member.setName("사용자");
member.setTeamId(team.getId());    // 외래키 식별자를 직접 
 
entityManager.persist(member);
cs

 

추가로, 사용자를 조회해서 조회된 사용자의 소속 팀의 이름을 가져오려면 어떻게 해야할까?

 

1
2
3
4
5
Member findMember = entityManager.find(Member.class, member.getId());
 
Team findTeam = entityManager.find(Team.class, findMember.getTeamId());
 
System.out.println(findTeam.getName());
cs

 

전혀 객체지향적이지 않은 코드가 나오게 된다. 왜 이렇게 된걸까?

 

객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다.

 

  • 테이블 : 외래키로 조인을 이용해 연관된 테이블을 찾는다.
  • 객체 : 참조를 사용해서 연관된 객체를 찾는다.

 

때문에, 위에서 설계한 내용은 객체를 객체답게 사용하지 못할 수 밖에 없는 설계인 것이다.

그렇다면 어떻게 설계해야 객체를 객체답게 사용할 수 있을까?

 

 

단방향 연관관계


간단하다. 앞에서 설계한 객체 모델링에서 Member 클래스의 teamId를 Team 클래스를 바라보도록 수정하면 된다.

객체 지향적인 모델링

이 설계에서 Member 클래스의 내용은 어떻게 작성될까?

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Entity
public class Member
{
    @Id
    @GeneratedValue
    private Long id;
 
    private String name;
 
    private int age;
 
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
 
    // getters, setters
}
cs

 

@ManyToOne 어노테이션은 Member-Team 간의 다대일 관계를 의미하며, @JoinColumn은 MEMBER 테이블에서 TEAM 테이블에 조인할 컬럼명을 지정한다.

 

앞서 팀과 사용자를 신규 생성해 저장하는 코드를 변경된 설계에 맞춰 수정해보자.

 

1
2
3
4
5
6
7
8
9
10
Team team = new Team();
team.setName("팀이름");
 
entityManager.persist(team);
 
Member member = new Member();
member.setName("사용자");
member.setTeam(team);    // 참조 
 
entityManager.persist(member);
cs

 

수정된 코드에서는 Member에 Team 객체를 직접 set해주어 참조 저장을 하고있다. 언뜻 보기에는 별 차이 없어보이지만, 사용자의 소속 팀 이름을 조회하는 코드를 수정해보면 차이를 느낄 수 있을것이다.

 

1
2
3
4
5
Member findMember = entityManager.find(Member.class, member.getId());
 
Team findTeam = findMember.getTeam();    // 참조로 연관관계를 조회한다!
 
System.out.println(findTeam.getName());
cs

 

Member에서 팀 아이디를 가져와 다시 조회를 하지 않고, Member에 있는 Team 객체를 통해 객체 그래프 탐색으로 팀 이름을 가져올 수 있다!

 

 

양방향 연관관계


사용자(Member)의 소속팀(Team)을 찾는 것은 해봤고, 반대로 특정 Team 소속의 사용자들을 찾고싶을 때는 어떻게 해야할까?

 

Team 클래스에 List<Member> 속성을 추가해 양방향 매핑을 해주면 된다.

양방향 객체 매핑

 

이렇게 양방향 매핑으로 설계되었을 때, Team 클래스는 아래와 같이 작성된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Entity
public class Team
{
    @Id
    @GeneratedValue
    private Long id;
 
    private String name;
 
    @OneToMany(mappedBy = "team")
    List<Member> members = new ArrayList<Member>();
 
    // getters, setters
}
cs

 

@OneToMany 어노테이션은 Team-Member 간 일대다 관계를 의미하며, mappedBy는 Member 클래스의 team 변수를 통해 관계를 매핑하겠다는 의미이다.

 

양방향 매핑을 했으니, 특정 팀을 조회하고 해당 팀 소속의 사용자 목록을 확인하는 코드를 작성해보자.

 

1
2
3
4
5
6
7
8
Team findTeam = entityManager.find(Team.class, team.getId());
 
List<Member> members = findTeam.getMembers();
 
for (Member member : members)
{
    System.out.println(member.getName());
}
cs

 

양방향 매핑에 있어 아래 내용을 반드시 숙지하고 주의하도록 하자.

  1. 객체의 양방향 관계는 사실 양방향 관계가 아니고, 서로 다른 단방향 관계 2개이다.
  2. 때문에, 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야한다.
  3. 두 객체 중 하나로 외래키를 관리해야하는데, 이 외래키를 관리하는 객체를 연관관계의 주인이라고 한다.

 

 

연관관계의 주인


  • 연관관계의 주인만이 외래키를 관리한다.
  • 주인이 아닌쪽은 읽기만 가능해야한다.
  • 주인은 mappedBy 속성을 사용하지 않는다.
  • 주인이 아니면 mappedBy 속성으로 주인을 지정해야한다.

 

그렇다면 위 시나리오에서는 누구를 주인으로 삼아야할까? 바로 Member이다. Member-Team 간의 조인을 하는 외래키 TEAM_ID가 MEMBER 테이블에 있어 외래키가 MEMBER 테이블에서 관리되기 때문이다.

 

여기서 문제, 아래 코드 중 사용자에게 소속 팀을 지정하는 올바른 방법은?

[1번]

1
2
3
4
5
6
7
8
9
10
11
Team team = new Team();
team.setName("팀A");
 
entityManager.persist(team);
 
Member member = new Member();
member.setName("사용자");
 
member.setTeam(team);
 
entityManager.persist(member);
cs

[2번]

1
2
3
4
5
6
7
8
9
10
11
Team team = new Team();
team.setName("팀A");
 
entityManager.persist(team);
 
Member member = new Member();
member.setName("사용자");
 
team.getMembers().add(member);
 
entityManager.persist(member);
cs

 

정답은 1번이다. 2번 코드에서는 연관관계의 주인인 member에 team이 입력되지 않아 실제 코드 수행 시 TEAM_ID 컬럼이 null이 입력된다.

 

그렇다면, Team 객체를 통해서 사용자를 소속팀에 추가할 수는 없는 것일까?

 

그렇지 않다. 연관관계 편의 메서드를 생성해 처리가 가능하다. 연관관계 편의 메서드는 이후 포스팅에서 다루도록 하자.

 


출처 :: 인프런 강의(자바 ORM 표준 JPA 프로그래밍 - 기본편)

반응형