설모의 기록

[Spring] 다대일, 일대다 연관관계 매핑 본문

언어/Spring

[Spring] 다대일, 일대다 연관관계 매핑

HA_Kwon 2018. 8. 17. 18:47

데이터베이스에서의 연관관계와 JPA 에서의 연관관계



 Spring 을 공부하면서 가장 헷갈리는 내용이 연관관계 매핑입니다. 데이터베이스의 외래키와 똑같이 생각하다가 함정에 빠지는 것이 태반입니다.

데이터베이스에서는 외래키라는 하나의 컬럼을 가지고 연관관계를 표현합니다. 그러나 JPA 에서는 객체를 매핑합니다. 따라서 JPA에서의 연관관계 매핑은

  • 관계의 방향
  • 다중성 (다대일? 일대다? 다대다?)
  • 연관관계의 주인
이 세가지가 중요합니다. 아래에 정리한 내용에서 위의 3가지를 알아볼 것입니다.




단방향 연관관계

연관관계 중 다대일(N:1) 을 먼저 알아보겠습니다. 다음에서 설명할 예제의 객체들은 다음의 관계입니다.

  • 사람은 가족이 있습니다.
  • 사람은 하나의 가족에만 소속될 수 있습니다.
  • 사람과 가족은 다대일 관계입니다.
위의 관계를 나타낸 객체 연관관계와 테이블 연관관계는 아래와 같습니다.

(객체 연관관계와 테이블 연관관계)


  • 객체 연관관계 - 사람 객체는 Person.family 로 가족 객체와 연관관계를 맺습니다. - 사람 객체와 가족 객체는 단방향 관계입니다. 사람은 Person.family 를 통해 가족을 알 수 있지만, 가족은 사람을 알 수 없습니다.

  • 테이블 연관관계 - 사람 테이블은 Family_id 외래 키로 가족 테이블과 연관관계를 맺습니다. - 사람 테이블과 가족 테이블은 양방향 관계입니다. 사람 테이블의 Family_id 외래키를 통해 사람과 가족을 조인할 수 있고, 반대로 가족과 사람을 조인할 수도 있습니다. => 예 :     (1) SELECT * FROM PERSON P JOIN FAMILY F ON P.Family_id = F.id     (2) SELECT * FROM FAMILY F JOIN PERSON P ON P.Family_id = F.id


위에서도 정리 해놓은 것처럼 참조를 통한 연관관계인 객체 연관관계는 언제나 단방향입니다. 객체간의 연관관계에서 양방향으로 만들고 싶다면 반대쪽에도 필드를 추가해서 참조를 보관해야 합니다. 결국 양방향이라는 것은 단반향을 2개 만들어 서로 참조하도록 설계해야 하는 것입니다. 

그렇지만 데이터베이스 테이블은 외래 키 하나로 양방향으로 조인할 수 있습니다.

단방향 연관관계인 Person 클래스와 Family 클래스는 아래와 같습니다.

import javax.persistence.*;

@Entity
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column
private String name;
@ManyToOne
@JoinColumn
private Family family;
}


import javax.persistence.*;

@Entity
public class Family {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column
private String address;
}

이 때 사용한 어노테이션의 의미는 다음과 같습니다.

  • ManyToOne : 다대일 관계라는 매핑 정보로 다대일 연관관계를 매핑할 때 필수로 사용해야 합니다.
  • JoinColumn : 외래 키를 매핑할 때 사용합니다. JoinColumn 의 name 속성에는 매핑할 외래키 이름을 지정합니다. 이 어노테이션은 생략해도 됩니다. 만약 생략한다면, 외래키로 매핑되는 컬럼의 이름은 필드명 + '_' + 참조하는_테이블의_컬럼명 입니다. 위를 예로 들면 family_ID 가 외래키로 매핑됩니다.



양방향 연관관계

위의 예제에서는 항상 Person 클래스에서 Family 클래스를 접근해야만 했습니다. 이번에는 Family 클래스에서 Person 클래스를 접근하는 관계를 추가해보도록 하겠습니다. 다시 말하면 서로 양방향으로 점근할 수 있는 양방향 연관관계로 매핑하겠습니다.

양방향 연관관계로 설정된 Person 클래스와 Family 클래스의 연관관계와 데이터베이스 테이블의 연관관계는 다음과 같습니다.

 

(객체 연관관계와 테이블 연관관계)

사람과 가족의 관계는 다대일이지만, 가족과 사람의 관계는 일대다입니다. 따라서 일대다 관계는 여러 객체와 연관관계를 맺을 수 있기 때문에 컬렉션을 사용해야 합니다. 저는 List 컬렉션을 이용했습니다.

이 부분에서 헷갈리지 말아야 하는 부분이 바로 이것입니다. 데이터베이스에서는 외래 키 하나를 이용해 양방향으로 조회할 수 있습니다. 위의 오른쪽 그림만 봐도 양방향 관계이든, 단방향 관계이든 변하는 것은 없습니다. 그러나 객체 연관관계는 아래와 같이 List 가 추가되었습니다.



import javax.persistence.*;

@Entity
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column
private String name;
@ManyToOne
@JoinColumn
private Family family;
}



import javax.persistence.*;
import java.util.List;

@Entity
public class Family {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column
private String address;

@OneToMany(mappedBy = "family")
private List<Person> people;
}


위와 같이 Family 클래스에 people 이라는 List 를 추가했습니다. 그리고 일대다 관계를 매핑하기 위해 @OneToMany 매핑 정보를 사용했습니다. mappedBy 속성은 양방향 매핑일 때 사용하게 되는데, 반대쪽 매핑의 필드 이름을 값으로 주면 됩니다. 위에서는 Person 클래스에 family 와 매핑할 것이기 때문에 family 를 적으면 됩니다.

이 때 간과하지 말아야 할 개념은 연관관계의 주인입니다.




연관관계의 주인?

위의 예제에서 @OneToMany 만 있어도 될 것 같은데, mappedBy 속성까지 있어야하는 이유는 무엇일까요?

엄밀하게 말하자면 객체에는 양방향 연관관계라는 것은 없습니다. 서로 다른 단방향 연관관계 2개를 잘 묶어서 양방향인 것처럼 보이게 할 뿐입니다. 반면에 데이터베이스 테이블은 외래 키 하나로 양쪽이 서로 조인을 해서 양방향 연관관계를 맺습니다.


Entity 를 단방향으로 매핑하면 참조를 하나만 사용하기 때문에 (위에서는 family) 이 참조로 외래 키를 관리하면 됩니다. 그러나 Entity 를 양방향으로 매핑하면 두 곳에서 서로를 참조하게 되기 때문에 객체의 연관관계를 관리하는 포인트는 2곳으로 늘어나게 됩니다.

엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래키는 하나다?

이 문제를 해결하기 위해 JPA 에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계의 주인 이라 합니다.


양방향 연관관계 매핑에서는 두 연관관계 중 하나를 연관관계의 주인으로 정해야 합니다.

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

이 때, 주인은 mappedBy 속성을 사용하지 않습니다.
또한 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 합니다.


그렇다면 위의 예제에서 Person 이 주인이 되어야 할까요? 아니면 Family 가 주인이 되어야 할까요?

이 과정에서 생각해봐야 할 것은 연관관계의 주인을 정한다는 것은 외래 키 관리자를 선택하는 것입니다. 따라서 위의 예제에서는 Person 이 주인이 되어야 본인 테이블에 있는 외래키를 관리할 수 있습니다. 만약에 Family Entity 에 있는 people 이 주인이 된다면 물리적으로 다른 테이블의 외래 키를 관리해야 합니다. 따라서 주인이 아닌 people 에는 mappedBy="family" 속성을 사용해 주인이 아님을 설정해야 합니다.




양방향 연관관계에서의 주의사항

양방향 연관관계에서 조심해야 할 실수는 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 넣는 경우입니다. 다음 예제를 보겠습니다.

Person hyeona = new Person("kwonhyeona");
entityManager.persist(hyeona);

Person father = new Person("father");
entityManager.persist(father);

Family family = new Family();
family.getPeople().add(hyeona);
family.getPeople().add(father);

entityManager.persist(family);

위의 코드에서는 hyeona, father 객체의 family 멤버변수를 설정하는 것이 아니라 family 객체의 people 리스트를 수정했습니다. 이 후 데이터베이스를 조회해보면 Person 테이블의 Family_id 에는 null 값이 있을 것입니다. 주인인 Person 에 값을 입력하지 않고, 주인이 아닌 Family 에 값을 넣었기 때문입니다.


그렇다면 주인인 Entity 에만 값을 넣는 것이 바람직한 것일까요? 

객체 관점에서 가장 안전한 것은 주인/주인이 아닌 Entity 모두 값을 입력해주는 것이 안전합니다. 


사실 이런 경우에는 편의 메소드를 구현하는 것이 좋습니다. 편의 메소드란 한 번에 양방향 관계를 설정하는 메소드를 말합니다. 아래의 함수를 보겠습니다.

import javax.persistence.*;

@Entity
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column
private String name;
@ManyToOne
@JoinColumn
private Family family;

public Person(String name) {
this.name = name;
}

public void setFamily(Family family) {
if (this.family != null) {
this.family.removePerson(this);
}
this.family = family;
family.addPerson(this);
}
}


import lombok.Data;

import javax.persistence.*;
import java.util.List;

@Data
@Entity
public class Family {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column
private String address;

@OneToMany(mappedBy = "family")
private List<Person> people;

public void addPerson(Person person) {
this.people.add(person);
}

public void removePerson(Person person) {
this.people.remove(person);
}
}

위의 예제에는 Person 클래스에 setFamily() 를, Family 클래스에 addPerson(), removePerson() 메소드를 구현했습니다. 따라서 Person.setFamily() 메소드를 호출하면 Person 과 Family 객체 보두 양방향 관계를 설정하게 됩니다. 이런 메소드를 편의 메소드라 합니다.

따라서 객체의 양방향 연관관계를 사용하기 위해서는 로직을 견고하게 작성해야 합니다.


위의 내용을 정리하면 다음과 같습니다.

  • 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료되었습니다.
  • 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가됩니다.
  • 앙방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 합니다.

마지막으로 연관관계의 주인을 정하는 기준은 외래 키의 위치와 관련해서 정해야 하며 비즈니스 중요도로 접근하면 안된다는 점을 강조드립니다.




'언어 > Spring' 카테고리의 다른 글

[Spring] 상속 관계 매핑  (4) 2018.10.05
[Spring] WebSocket 구현하기  (5) 2018.08.26
[Spring] 영속성이란 (persistence)  (11) 2018.08.12
[Spring] Ehcache 캐시 사용  (1) 2018.08.08
[JUnit] 단위테스트를 위한 JUnit Framework  (0) 2018.07.04
Comments