연관관계 매핑

Jaca·2021년 8월 20일
0

연관관계가 필요한 이유

위 테이블이 있다고 하자.

테이블 연관관계만 보고 클래스를 작성하면 아래와 같이 될 것이다.

@Entity
public class Member {
@Id @GeneratedValue private Long id;
@Column(name = "USERNAME") 
private String name;
@Column(name = "TEAM_ID")
private Long teamId;
...
}

@Entity
public class Team {
@Id @GeneratedValue private Long id;
private String name; 
...
}

참조를 만들지 않고 외래키인 teamId를 그대로 작성한 것이다.
이 클래스에서 어떻게 Member와 Team의 협력을 만들수 있을까?
외래키인 teamId는 그저 Long인 숫자일 뿐이다.
아마 Member의 teamId를 가지고 Team를 전부 뒤져야 할 것이다.

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

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

단방향 연관관계

그래서 연관관계를 매핑해주기 위해 참조에 사용되는 키에 어노테이션을 붙혀준다.

테이블의 연관관계에 따라
@ManyToOne, @OneToMany, @ManyToMany, @OneToOne 의 어노테이션을 알맞게 붙혀준다.

위와 같이 매핑된다.

양방향 연관관계

양방향이라고 거창한 것은 아니다.
위는 Member -> Team 으로 단방향 연관관계만 매핑되어 있다.
Member에서 Team의 참조를 저장하고 있기 때문에 Member에서 Team 으로 조회가 가능하지만, Team에서 Member의 정보는 아직 알 수 없다.

이를 위해 Team -> Member로 단방향 연관관계를 설정해주면 양방향 연관관계가 되는 것이다.

Team과 Member는 일대다 관계로 Many인 관계와 매핑하려면 여러개가 들어올수 있도록 컬렉션으로 받아야 한다.

연관관계의 주인

하지만 이렇게 양방향 연관관계를 설정하면 문제가 발생할 수 있다.

엄밀히 이야기하면 객체에는 양방향 연관관계라는 것이 없다. 서로 다른 단방향 연관관계 2개를 애플리케이션 로직으로 잘 묶어서 양방향인 것처럼 보이게 할 뿐이다. 반면에 데이터베이스 테이블은 외래 키 하나로 양쪽이 서로 조인할 수 있다. 따라서 테이블은 외래 키 하나만으로 양방향 연관관계를 맺는다.

엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나다. 따라서 둘 사이에 차이가 발생한다. 그렇다면 둘 중 어떤 관계를 사용해서 외래 키를 관리하도록 해야한다.

이 것을 연관관계의 주인이라하고, mappedBy 속성을 통해 설정해준다.

이를 통해 설정된 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있다. 반면에 주인이 아닌 쪽은 읽기만 할 수 있다.

누구를 주인으로 선택할 것 인가?

연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야 한다. 여기서는 회원 테이블이 외래 키를 가지고 있으므로 Member.team이 주인이 된다.
주인이 아닌 Team.members에는 mappedBy="team" 속성을 사용해서 주인이 아님을 설정해야 한다.
여기서 mappedBy의 값으로 사용된 team은 연관관계의 주인인 Member 엔티티의 team 필드를 말한다.

데이터베이스 테이블의 다대일, 일대다 관계에서는 항상 다 쪽이 외래 키를 가진다. 다 쪽인 @ManyToOne은 항상 연관관계의 주인이 되므로 mappedBy를 설정할 수 없다. 그래서 @ManyToOne에는 mappedBy 속성이 없다.

양방향 연관관계의 문제점

Member member1 = new Member("member1", "회원1");
em.persist(member1);

Member member2 = new Member("member2", "회원2");
em.persist(member2);

Team team1 = new Team("team1", "팀1");
team1.getMembers().add(member1);
team1.getMembers().add(member2);

em.persist(team1);

회원1,2 를 생성하고 team1에 더해주었다. DB에 저장되었을까?

외래 키 TEAM_ID에 team1이 아닌 null 값이 입력되어 있는데, 연관관계의 주인이 아닌 Team.members에만 값을 저장했기 때문이다.

연관관계의 주인인 Member.team에 아무 값도 입력하지 않았기 때문에, TEAM_ID 외래 키의 값도 null이 저장된다.

객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 안전하다. 양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수 있다.

연관관계 편의 메서드

안전한 관계 설정을 위해서는 아래와 같이 양쪽에 모두 값을 입력해줘야 한다.

public void test(){
  
  Team team1 = new Team("team1", "팀1");
  em.persist(team1);
  
  Member member1 = new Member("member1", "회원1");
  member1.setTeam(team1);
  team1.getMembers().add(member1);
  em.persist(member1);
  
  Member member2 = new Member("member2", "회원2");
  member2.setTeam(team2);
  team1.getMembers().add(member2);
  em.persist(member2);
}

하지만 member.setTeam(team)과 team.getMembers().add(member)를 각각 호출하다 보면 실수로 빼먹을 수도 있기 때문에 김영한 강사님께서 아래와 같은 방법을 추천하셨다.

public class Member{
  
  private Team team;
  
  public void setTeam(Team team) {
    this.team = team;
    team.getMembers().add(this);
  }
  // ...
}

setTeam() 메서드를 리팩토링해 한쪽을 설정해줄때 자동으로 같이 되게 하는 것이다.

다양한 연관관계

다대일 [N:1]

다대일 단방향의 경우 가장 흔하게 사용되는 연관관계이다.
다대일의 반대는 일대다로 양방향 매핑 시 반대 테이블에 일대다 매핑을 해준다.
위에서 보았듯 외래 키가 있는쪽이 연관관계의 주인이 된다.
양쪽을 서로 참조가 필요할 경우 다대일 양방향 매핑을 해준다.

일대다 [1:N]

일대다 단방향 매핑을 하게되면, 일(1)의 테이블이 연관관계의 주인이 된다.
하지만 외래키는 다(N) 테이블이 가지고 있다.
이러한 구조때문에 @JoinColumn 을 사용하지 않으면, 조인 테이블을 생성하게 된다.
그리고 Update 쿼리를 실행하게 되기때문에, 차라리 다대일 양방향 매핑이 효율적이다.

일대다 양방향은 다대일 양방향 매핑의 반대 개념으로 공식적으로 있는 개념은 아니다.
@JoinColumn(insertable = false, updatable = false) 속성을 이용해 읽기 전용 필드를 양방향 처럼 사용할 수 있다.

일대일 [1:1]

일대일의 반대는 일대일 일 것이다.
주 테이블이나 대상 테이블 중에 어디서 외래키를 보관하게 할 것인지 선택해야한다.
외래 키에 유니크 제약조건을 추가시켜 주는 것이 좋다.

일대일 단반향은 다대일의 단방향 매핑과 유사하다.

양방향의 경우 외래 키를 가지고있는 테이블을 연관관계의 주인으로 설정해준다.

다대다 [N:N]

관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.
그래서 연결 테이블을 생성해 일대다 + 다대일 관계로 풀어서 표현하게 된다.

하지만 연결 테이블이 단순히 연결만 하고 끝나지 않는다. 조인 테이블 자체에 주문시간, 수량 같은 추가 데이터가 많이 들어갈 수 있다.
하지만, 매핑 정보만 넣는 것이 가능하고, 추가 정보를 넣는 것 자체가 불가능하다.
그리고 중간 테이블이 숨겨져 있기 때문에 예상하지 못하는 쿼리들이 나간다.

profile
I am me

0개의 댓글