쌍문동 척척박사
Spring / ReactJS / PS

연관관계 매핑

연관관계 매핑

객체-RDB 매핑 (ORM) 두 번째 파트, 연관관계 매핑에 대해 설명하겠다.

관련 포스트 : 엔티티 매핑


연관관계의 등장 배경

자바의 객체지향적 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다. 따라서 우리는 여태까지 객체 주도적인 코딩을 위해 데이터를 객체에 매핑해왔다. 하지만 다음 코드를 보자.

1
2
3
4
@Entity
public class Team {
    // ...
}
1
2
3
4
5
6
7
@Entity
public class User {
    // ...

    @Column(name="team_id")
    private Long teamId;
}
1
2
3
4
5
6
7
8
9
10
11
Team team = Team.build().name("A").builder();
em.persist(team);

User user = User.build()
        .name("Junyoung")
        .teamId(team.getId())   // 외래키 식별자를 직접 다룸
        .builder();
em.persist(user);

User findUser = em.find(User.class, user.getId());
Team findTeam = em.find(Team.class, findUser.getTeamId());

객체를 테이블에 맞춰 데이터 중심으로 모델링하면 결국 다시 객체지향으로부터 멀어져, SQL에 의존적인 코딩을 하게 된다. 이런 현상의 근본적 원인은 객체와 테이블 사이의 패러다임 불일치 때문이다. 따라서 JPA에서도 연관관계에 대한 매핑이 등장하게 됐다.

  • 객체 : 참조를 사용해 연관된 객체를 찾음
  • 테이블 : 외래키조인해 연관 테이블을 찾음

단방향 연관관계 매핑

2.png

User는 team 필드로 Team 객체와 연관관계를 맺어 다음과 같이 코드를 바꿀 수 있다.

1
2
3
4
5
6
7
@Entity
public class User{
    // ...
    @ManyToOne  // N:1 관계 매핑
    @JoinColumn(name = "team_id")  // 매핑할 외래키명
    private Team team;  // 참조
}
1
2
3
4
5
6
7
8
9
10
11
Team team = Team.build().name("A").builder();
em.persist(team);

User user = User.build()
        .name("Junyoung")
        .teamId(team)   // 참조를 사용한 연관관계 설정
        .builder();
em.persist(user);

User findUser = em.find(User.class, user.getId());
Team findTeam = findUser.getTeam();

@JoinColumn

  • name : 매핑할 외래키 이름. 필드명 + “_” + 참조할 테이블의 기본키 컬럼명
  • 단방향 관계에서는 User 데이터를 가져오면 Team에 대한 정보도 같이 조회하게 된다. 조인을 하거나 다른 방법을 사용하는가는 이후 설명할 Proxy 정책에 따라 달라진다.

양방향 연관관계 매핑

User와 Team은 User->Team 방향으로만 조회가 가능하기 때문에 N:1의 단방향 관계다. 하지만 실제 테이블에서는 User->Team 방향 뿐만 아니라 Team->User 방향으로도 조회가 가능한 양방향 관계를 갖는다. 객체 연관관계와 테이블 연관관계의 차이가 여기서 발생한다. 따라서 우리는 단방향 관계를 하나 더 추가해 마치 양방향 관계처럼 동작하도록 매핑을 바꿔야 한다.

1
2
3
4
5
6
7
@Entity
public class Team {

    // 1:N 관계 매핑. User에서의 Team 필드 객체명
    @OneToMany(mappedBy = "team")
    private List<User> users = new ArrayList<>();
}
1
2
// 양방향으로 조회 가능
List<User> userList = findUser.getTeam().getMembers();

mappedBy

  • 참조할 테이블의 외래키 필드명
  • 연관관계의 주인을 지정함

양방향 매핑 규칙

  • 객체의 두 관계 중 하나는 연관관계의 주인 (Owner)이 됨
  • 외래키가 있는 곳 (N)을 주인으로 정하면 됨
    • ex) User가 주인
  • 연관관계의 주인만이 외래키를 관리함 (등록, 수정)
    • 이후 추가 설명 확인
    • ex) User 객체 쪽에서 Team과의 연관관계를 관리
  • 주인이 아닌 쪽은 mappedBy 속성으로 주인을 지정하고, 읽기만 가능함
    • ex) Team은 User를 읽기만 가능

주의 1

1
2
3
4
5
6
7
Team team = Team.builder().id(10L).name("A").build();
em.persist(team);
User user = User.builder().id(100L).name("Junyoung").build();

user.changeTeam(team);  // 주인 쪽에서만 연관관계 설정

em.persist(user);

매핑 규칙에 따라 주인 쪽에서 연관관계를 설정했다. DB에서는 애초에 방향성이란 개념이 없고, 외래키로 연관관계를 지정하기 때문에 1:N 관계 시 N 쪽에서 외래키를 갱신하면 연관관계가 성립된다. 그렇다면 엔티티 입장에서는? 당연히 다음 번 트랜잭션에서는 DB에서 가져온 외래키로 연관관계를 구축하니 문제가 없다.

하지만 문제는 현재 트랜잭션이 아직 끝나지 않았을 때 발생한다. 만약 현 상태에서 team.getUsers() 를 하면 마지막에 추가한 user는 리스트에 존재하지 않는다. 그 이유는 현재 영속성 컨텍스트에서 user->team 참조는 형성됐지만, team->user 참조는 형성되지 않았기 때문이다.

따라서 현재 트랜잭션에서도 올바른 참조 상태를 유지하기 위해서는 양방향 모두에서 연관관계를 설정 (외래키 관리)해야 한다.

1
2
user.changeTeam(team);
team.getUsers().add(user);  // 양쪽 모두에서 연관관계 설정

이런 방식으로 구현해도 된다.

1
2
3
4
5
6
7
8
public class User {
    // ...
    public void changeTeam(Team team){
        this.team = team;
        // 엔티티 내부에서 연관관계 설정
        team.getUsers().add(this);
    }
}
1
user.changeTeam(team);

주의 2

엔티티 내에서 toString() / lombok / JSON 생성 라이브러리를 사용하면 양방향 매핑 시 Team->User->Team->User->… 와 같이 참조 무한 루프에 빠지게 된다.

따라서 lombok에서는 @ToString을 사용하지 말거나 사용하더라도 exclude 속성을 잘 설정해야 하고, JSON 생성 라이브러리 사용 시에는 절대로 컨트롤러에서 Entity를 바로 반환하면 안 되고 DTO로 변환해서 반환해야 한다.

이렇게 제약사항이 많은데 굳이 반대 방향에서도 참조할 필요가 있나 싶기도 하지만 실제 서비스를 운영하다보면 양방향으로 참조하는 경우가 많이 발생한다. 따라서 단방향 매핑을 기본으로 설정하되, 양방향 매핑이 필요한 경우에는 그때 가서 반대쪽 방향으로도 매핑을 확장하도록 하자.

연관관계 매핑의 종류

연관 관계는 N:1 / 1:N / 1:1 / N:M 총 4가지 종류가 있다. 결론부터 말하면 그 중 우리가 사용해야 하는 관계는 N:1 / 1:1 밖에 없다.

  • 1:1 매핑 : 외래키 매핑 시 unique 조건을 걸고 (@Table(unique=””)), 기본으로 양방향 매핑을 하자.
  • 1:N 매핑 : DB에서 공식적으로 존재하지 않는 매핑이다. N:1 매핑 상태에서 양방향 매핑이 필요한 경우에만 추가해서 사용하고, 단독으로는 사용하면 안 된다.
  • N:M 매핑 : 절대 사용하지 말고 설계부터 다시 하자. 중간에 관계 테이블을 생성해 (N:1 ~ 1:1 ~ N:1) 관계로 만들면 된다.

참고 자료

자바 ORM 표준 JPA 프로그래밍

comments powered by Disqus