[JPA] 다양한 연관관계 매핑 (1)
28 Jun 2022해당 포스트는 인프런 김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 을 듣고 정리한 글입니다.
다양한 연관관계 매핑 (1)
연관관계는 매핑시 3가지를 고려해서 설계해야한다.
- 다중성
- 단방향, 양방향
- 연관관계의 주인
종류
JPA의 연관관계의 대표적인 다중성 매핑방법은 아래와 같다.
- 다대일 : @ManyToOne
- 일대다 : @OneToMany
- 일대일 : @OneToOne
- 다대다 : @ManyToMany
보통 설계에서는 다대일을 주로 사용하고, 다대일을 통해 대부분의 문제를 해결할 수 있다. 하지만 실무에서는 다대다의 관계는 쓰면 안된다.
단방향, 양방향
- 테이블
- 외래 키 하나로 양쪽 조인 가능
- 사실 방향이라는 개념이 없음
- 객체
- 참조용 필드가 있는 쪽으로만 참조 가능
- 한쪽만 참조하면 단방향
- 양쪽이 서로 참조하면 양방향
연관관계의 주인
- 테이블은 외래 키 하나로 두 테이블이 연관관계를 맺음
- 객체 양방향 관계는 A->B, B->A 처럼 참조가 2군데 하지만 테이블은 참조가 하나이다.
- 객체 양방향 관계는 참조가 2군데 있음. 둘중 테이블의 외래 키를 관리할 곳을 지정해야함
- 연관관계의 주인(mappedBy) : 외래 키를 관리하는 참조
- 주인의 반대편 : 외래 키에 영향을 주지 않음, 단순 조회만 가능
다대일 (N:1)
무조건 외래키는 다의 관계를 가진 곳에 외래키나 참조를 관리한다.
기존에 작성한 예시의 @ManyToOne이 다대일에 해당한다.
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
private long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
@Entity
@Getter @Setter
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
public void addMember(Member member) {
members.add(member);
member.setTeam(this);
}
}
다대일 정리
- 가장 많이 사용하는 연관관계
- 다대일의 반대는 일대다
- 다수쪽에 보통 외래키의 참조를 넣는다.
일대다 (1:N)
1인 Entity애서 외래키를 관리하겠다는 뜻이다.
먼저 사족을 붙이자면 실무에서는 해당 모델은 추천하지 않는다.
일대다 단방향
해당 그림에 대해 객체의 입장에서는 일대다의 구조가 자주 나올 수 있다.
하지만 DB의 입장에서는 다의 관계에 무조건 외래키가 들어가므로, 해당 구조가 나올 수가 없다.
해당 구조는 Team에 있는 Member의 정보를 변경하면, Member의 Entity를 변경해야하는 요상한 설계가 들어간다.
/* Member */
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
private long id;
@Column(name = "USERNAME")
private String username;
}
/* Team */
@Entity
@Getter @Setter
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<>();
}
private List<Member> members = new ArrayList<>();
에 @OneToMany
을 선언하고 Member Entity와 일대다 구조로 매핑하였다.
해당 구조를 간단한 예시를 통해 들어보자.
Member member = new Member();
member.setUsername("member1");
entityManager.persist(member);
Team team = new Team();
team.setName("TeamA");
team.getMembers().add(member);
entityManager.persist(team);
entityTransaction.commit();
해당 소스를 실행하면 이상한 일이 일어난다.
/* Member Insert */
insert
into
Member
(USERNAME, id)
values
(?, ?)
/* Team Insert */
insert
into
Team
(name, id)
values
(?, ?)
/* Member update */
update
Member
set
TEAM_ID=?
where
id=?
각각 Member와 Team에 데이터를 Insert를 해준뒤 @OneToMany를 선언한 테이블은 Team Entity 지만 Member 테이블에 데이터를 update하는 것을 볼 수 있다.
그리고 결과를 살펴보자.
해당 관계는 객체에서는 올바르지만 DB에서는 다의 관계에서 외래키를 관리하므로 Member에 들어갈 수 밖에 없다. 그래서 Team Entity에 연관관계를 선언 해도 Member에 update를 날려서 해당 데이터를 관리할 수 밖에 없다.
해당 구조는 연관관계의 주인인 Team이 아닌 연관관계를 매핑하고 있는 Member에서 엉뚱하게 update를 주기때문에 혼란을 줄 수가 있다. 실제 테이블이 수십개가 되는 운영에서 해당 구조를 쓸 경우 혼란을 야기할 수 있다.
일대다 단방향 정리
- 일대다 단방향은 일대다(1:N)에서 일(1)이 연관관계의 주인
- 테이블 일대다 관계는 항상 다(N) 쪽에 외래 키가 있음
- 객체와 테이블의 차이 때문에 반대편 테이블의 외래 키를 관리하는 특이한 구조
- @JoinColumn을 꼭 사용해야 함. 그렇지 않으면 조인 테이블 방식을 사용함(중간에 테이블을 하나 추가한다.)
일대다 단방향 매핑의 단점
- 엔티티가 관리하는 외래 키가 다른 테이블에 있다. DB의 입장에서는 다의 관계에서 외래키를 관리하기 때문이다.
- 연관관계 관리를 위해 추가로 UPDATE SQL 실행
일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자.
일대다 양방향 정리
- 이런 매핑은 공식적으로는 존재하지 않는다.
- @JoinColumn(insertable=false, updateble=false)
- 읽기 전용 필드를 사용해서 양방향 처럼 사용하는 방법
- 차라리 다대일 양방향을 사용하자.
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
private long id;
@Column(name = "USERNAME")
private String username;
/* insert와 update를 막아서 읽기전용 필드를 생성한다. */
@ManyToOne
@JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
private Team team;
}
결론
정리하자면 해당 구조는 객체지향에서는 맞지만 DB에서는 어긋나는 설계이고, 직접 건드리는 테이블외에 다른 테이블에 영향이 갈 수가있다.
최대한 일대다를 지양하고 다대일 양방향을 사용하도록 하자.