연관관계를 매핑할 때는 다음의 3가지를 고려해서 매핑하여야 합니다.
- 다중성 (일대 다, 다대 일, 다대 다, 일대 일)
- 방향 (양방향, 단방향)
- 연관관계의 주인
다중성
연관관계에는 다음과 같은 다중성이 있습니다.
다대일(N : 1)[ManyToOne]
일대다(1 : N)[OneToMany]
일대일(1 : 1)[OneToOne]
다대다(N : N)[ManyToMany]
방향
방향에는 단방향, 양방향이 있습니다.
DB 테이블은 외래 키(Foreign key)하나로 조인(Join)을 사용해서 양방향으로 쿼리가 가능합니다.
따라서 DB에는 방향의 개념이 없습니다.
그러나 객체의 경우, 참조용 필드를 가지고 있는 객체만 연관된 객체를 조회할 수 있으므로 방향의 개념이 존재합니다.
객체 관계에서 한 쪽만 반대쪽을 참조하는 관계를 단방향, 양 쪽 모두 서로를 참조하는 관계를 양방향이라고 합니다.
연관관계의 주인
데이터베이스는 외래 키(FK) 하나로 두 테이블이 연관관계를 맺습니다.
즉 연관관계를 관리하는 포인트는 외래 키 하나입니다.
반면 객체에서는 양방향 관계로 매핑하면 A -> B, B -> A, 두 곳에서 서로를 참조하므로 연관관계를 관리하는 포인트는 두 곳이 됩니다.
따라서 JPA는 두 객체 중 하나를 정해서 외래 키를 관리하게 만들어야 하는데, 여기서 외래 키를 관리하는 객체를 연관관계의 주인이라고 합니다.
보통 외래 키를 가진 테이블과 매핑되는 엔티티가 외래 키를 관리하는 것이 효율적이므로, 보통 이곳을 연관관계의 주인으로 선택합니다.
주의할 점은 외래 키를 관리하는 연관관계의 주인만이 외래 키를 변경할 수 있으며, 주인이 아닌 곳은 읽기만 가능하다는 것입니다.
논리적 FK에 대하여
이번 글은 물리적으로 FK가 존재하는 경우에 대한 매핑에 대해 작성한 글입니다.
논리적 FK만 사용하는 경우는 논리적 FK를 자바 클래스의 필드로 설정해주면 됩니다.
간단하게 적용할 수 있으므로 넘어가겠지만, 실제 실무에서는 논리적 FK를 사용하는 경우가 많다고 합니다.
실제 Github에서도 FK를 사용하지 않는다고 하며, 다음을 참조하시면 좋을 것 같습니다.
https://github.com/github/gh-ost/issues/331
아무튼 저희는 학습을 위해 배우는 것이기에, 계속해서 연관관계를 매핑하는 법에 대하여 공부하도록 하겠습니다.
다중성, 방향, 연관관계의 주인을 고려한 모든 연관관계
- 다대일 : 단방향, 양방향
- 일대다 : 단방향, 양방향
- 일대일 : 주 테이블 단방향, 양방향
- 일대일 : 대상 테이블 단방향, 양방향
- 다대다 : 단방향, 양방향
연관관계의 주인을 푸른색으로 표현하였습니다.
다대일 [N : 1] -@ManyToOne
다대일 단방향
객체와 테이블 연관관계
코드로 표현하면 다음과 같습니다.
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
...
}
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
...
}
회원은 Member.team으로 팀 엔티티를 참조할 수 있지만, 반대로 팀에는 회원을 참조하는 필드가 없습니다.
따라서 회원과 팀은 다대일 단방향 연관관계입니다.
@ManyToOne
@JoinColumn(name = "TEAM_ID")
다대일 연관관계이므로 @ManyToOne을 사용하였습니다.
@JoinColumn을 사용하여 Member.team 필드를 TEAM_ID 외래 키와 매핑하였습니다.
외래 키로 매핑할 컬럼 지정하는 방법
referencedColumnName을 통해 대상 테이블의 어떠한 컬럼을 FK로 사용할지 지정할 수 있습니다.
기본값은 참조하는 테이블의 기본키(PK) 컬럼명이므로 자동으로 기본 키가 외래 키로 매핑됩니다.
name 속성은 연관관계의 주인의 테이블에 FK를 저장할 컬럼명을 지정하는 것입니다.
예를 들어 referencedColumnName = "name"이며, name = "fk_name"인 경우, 대상 테이블의 name 컬럼의 값을 FK로 사용하며, 이는 fk_name 컬럼에 저장됩니다.
코틀린의 예시 코드
생성된 DB
다대일 양방향
양방향 객체와 테이블 연관관계
여기서 중요한 것은 단뱡향이 양방향이 되었다고 해서 테이블에 영향을 주는것이 아닙니다.
테이블은 그대로이고, 객체에서만 반대방향으로의 연관관계가 하나 늘어난 것입니다.
코드는 다음과 같이 Team에만 참조가 하나 늘어납니다.
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
...
}
@OneToMany(mappedBy = "team")
위의 어노테이션을 통해, 연관관계의 주인, 즉 외래키를 관리하는 필드(Member의 team 필드)를 명시해 준 것입니다.
일대다 [1 : N] : @OneToMany
일대다 단방향
Team에서 Member를 참조하는 단방향 연관관계
일대다 단방향 관계는 특이하게도 Team 엔티티의 Team.members로 MEMBER 테이블의 TEAM_ID 외래 키를 관리합니다.
보통은 자신이 매핑한 테이블의 외래 키를 관리합니다.
그러나 일대다(연관관계의 주인이 1) 단방향 연관관계에서는 외래키가 항상 다쪽 테이블에 존재하기 때문에,
반대편 테이블의 외래 키를 관리하는 특이한 모습이 나타납니다.
어떻게 매핑하나?
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
...
}
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<>();
...
}
@OneToMany
mappedBy 속성이 없어졌다는 것에 주목해야 합니다.
Team이 연관관계의 주인이므로 mappedBy 속성을 지워주었습니다.
또한 @JoinColumn을 명시해 주었습니다.
@JoinColumn을 사용하지 않으면 JPA는 연결 테이블을 중간에 두고 연관관계를 관리하는 조인 테이블 전략을 기본으로 사용하여 매핑하기 때문에, 꼭 @JoinColumn을 붙여주어야 합니다.
문제점
일대다 단방향 매핑의 문제는, 매핑한 객체가 관리하는 외래 키가 다른 테이블에 있다는 점입니다.
자신의 테이블의 외래 키가 있다면, 엔티티의 저장과 연관관계 처리는 INSERT SQL 한 번으로 끝낼 수 있지만, 다른 테이블에 외래 키가 있으면, 연관관계 처리를 위해 UPDATE SQL을 추가로 발생시킵니다.
또한 자신의 테이블이 아닌 다른 테이블의 외래 키를 관리하게 되기 때문에, 관리가 굉장히 어려워집니다.
일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하는 것을 권장합니다.
일대다 단방향 매핑을 사용하면, 자신이 매핑된 테이블이 아닌 다른 테이블의 외래 키를 관리하게 되므로 성능 문제를 발생시킬 뿐더러 관리도 어렵게 만듭니다.
따라서 일대다 단방향 매핑보다는, 다대일 양방향 관계를 사용하도록 하는것이 좋습니다.
일대다 양방향
사실 일대다 양방향 매핑은 존재하지 않습니다. (일대다 양방향과 다대일 양방향은 사실 똑같은 말입니다.)
정확히는 일대다 양방향 매핑에서 @OneToMany는 데이터베이스의 특성상 연관관계의 주인이 될 수 없습니다.
왜냐하면 일대다 관계에서는 항상 다 쪽에 외래 키가 존재하기 때문입니다.
이런 이유로 @ManyToOne에는 mappedBy 속성이 없습니다.
그러나 정말 일대다 양방향 매핑을 하고 싶다면, 방법이 아예 없는것은 아닙니다.
일대다 단방향 매핑 반대편에, 같은 외래키를 사용하는 다대일 단방향 매핑을 읽기 전용으로 하나 추가해주면 됩니다.
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID", insertable = false, updatable = false) //읽기 전용
private Team team;
...
}
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<>();
...
}
위처럼 각 엔티티에 @JoinColumn을 명시하여 두 엔티티가 같은 외래키를 관리하도록 만들어준 후, 다(N)쪽 엔티티를 읽기 전용으로 하나 만들어주었습니다.
같은 외래키를 사용해야만 가능한 방법입니다.
이 방법도 역시 일대다 단방향 매핑의 단점을 그대로 가지므로, 될 수 있으면 다대일 양방향 매핑을 사용하도록 하는것이 좋습니다.
일대일 [1 : 1] : @OneToOne
일대일 관계는 양쪽이 서로 하나의 관계만을 가집니다.
일대일 관계의 반대도 일대일 관계이며, 일대일 관계는 두 테이블 중 어느곳에서든 외래 키를 가질 수 있습니다.
일대일 관계일 경우 고려해야 할 요소가 하나 더 늘어납니다.
주 테이블에 외래 키
주 객체가 대상 객체를 참조하는 것처럼, 주 테이블에 외래 키를 두고 대상 테이블을 참조하는 방법입니다.
장점은 주 테이블만 확인해도 대상 테이블과 연관관계가 있는지 알 수 있다는 점이며, 객체 참조와 외래 키를 비슷하게 사용할 수 있습니다.
해당 방법을 사용하기 위해서는 외래 키에 데이터베이스 유니크 제약조건을 추가해주어야 합니다.
(이외에는 일대다, 다대일과 굉장히 비슷합니다)
아래 예시에서 주 테이블은 MEMBER이고, 대상 테이블은 LOCKER 입니다.
주 테이블에 외래 키 - 단방향
1대1 관계
코드로 표현하면 다음과 같습니다.
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
}
@Entity
public class Locker {
@Id @GeneratedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
}
일대일 관계이므로 @OneToOne을 사용해 매핑하였고, 데이터베이스에는 LOCKER_ID(FK)에 유니크 제약조건을 추가하였습니다.
다대일, 일대다와 굉장히 비슷하고, 어노테이션에만 차이가 있다는 것을 알 수 있습니다.
주 테이블에 외래 키 - 양방향
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@OneToOne
@JoinColumn(name = "locker_ID")
private Locker locker;
}
@Entity
public class Locker {
@Id @GeneratedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
@OneToOne(mappedBy = "locker")
private Member memeber;
}
양방향 연관관계이므로, mappedBy를 사용하여 연관관계의 주인을 정해주었습니다.
마찬가지로 다대일, 일대다와 굉장히 비슷하고, 어노테이션에만 차이가 있다는 것을 알 수 있습니다.
대상 테이블에 외래 키
데이터베이스 개발자들이 선호하는 방식입니다.
테이블 관계를 일대일에서 일대다 관계로 변경할 때, 테이블의 구조를 그대로 유지할 수 있다는 장점이 있습니다.
(일대다에서는 다 쪽이 항상 외래키를 가지므로, 대상 테이블에 외래 키가 있다면, 테이블의 구조가 유지됩니다.)
대상 테이블에 외래 키 - 단방향
일대일 관계 중 대상 테이블의 외래 키가 있는 단방향 관계는 지원하지 않습니다.
대상 테이블에 외래 키 - 양방향
사실 이 관계는 주 테이블에 외래 키 - 양방향과 동일합니다.
단지 주 테이블만 바뀌었을 뿐입니다.
@Entity
public class Locker {
@Id @GeneratedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
@OneToOne
@JoinColumn(name = "MEMBER_ID")
private Member memeber;
}
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@OneToOne(mappedBy = "member")
private Locker locker;
}
참고 - 일대일 관계의 주의사항
프록시를 사용할 때 외래 키를 직접 관리하지 않는 일대일 관계는 지연 로딩으로 설정하더라도 즉시 로딩됩니다.
예를 들어 위의 예제에서 Locker.member는 지연 로딩할 수 있지만, Member.locker는 지연 로딩으로 설정하더라도 즉시 로딩됩니다.
이것은 프록시의 한계 때문에 발생하는 문제입니다.
위의 예시를 들어 간단히 설명해보겠습니다.
JPA에서는 Member 객체의 Locker 필드의 프록시를 만들기 위해서는, DB에 그 값이 있는지 없는지 확인해야 합니다.
(있으면 프록시를 넣고, 없으면 null을 넣습니다)
그러나 위의 관계에서는 LOCKER 테이블에 FK가 있으므로, 값을 확인하기 위해서는 LOCKER 테이블을 조회해야 하는것입니다.
즉 어차피 쿼리가 발생하기 때문에, 굳이 프록시로 만들 이유가 없는 것입니다.
따라서 지연 로딩으로 설정하더라도 즉시 로딩됩니다.
이 문제를 해결하기 위해서는 프록시 대신 bytecode instrumentation을 사용하는 방법이 있으나,, 굉장히 복잡하다고 합니다.
궁금하다면 다음을 참고해주세요
https://developer.jboss.org/docs/DOC-13960
일대일 연관관계 정리
주 테이블에 외래 키
- 주 객체가 대상 객체의 참조를 가지는 것 처럼, 주 테이블에 외래 키를 두고 대상 테이블을 찾음
- 객체지향 개발자 선호
- JPA 매핑이 편리합니다.
- 장점 : 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능(성능상으로 좋다)
- 단점 : 값이 없으면 외래 키에 null이 허용됩니다.
대상 테이블에 외래 키
- DB 개발자 선호
- 장점 : 주 테이블과 대상 테이블의 관계를 1대1에서 1대다 관계로 변경할 때 테이블 구조가 유지
- 단점 : 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩됩니다
다대다[N : N] : @ManyToMany
안 쓰는게 좋습니다.
아니 그냥 쓰지마세요.
다대다 관계 대신 연결 엔티티를 사용합니다
두 테이블을 연결하는 연결 테이블을 만들고, 그것을 연결 엔티티로 승격시켜줍니다.
즉 @ManyToMany -> @OneToMany와 @ManyToOne으로 나눠지는 것입니다.
Member와 Product의 다대다 관계를 Order 엔티티를 추가하며 1대다, 다대1 관계로 풀어냄
양방향 매핑 TIP
- 단방향 매핑만으로도 이미 연관관계 매핑은 완료된 상태입니다.
- 단방향 매핑으로 먼저 설계를 끝낸 후 필요한 경우에만 양방향을 추가해도 됩니다. (이는 DB 테이블에 영향을 주지 않습니다.)
Reference
[자바 ORM 표준 JPA 프로그래밍 - 김영한]
아래 블로그는 Spring과 Java, 그리고 다양한 알고리즘에 대한 정보가 많은 블로그이니 참고하시면 좋을 것 같습니다.
출처 : https://ttl-blog.tistory.com/129
'Back-End > Spring' 카테고리의 다른 글
[Spring Boot] Spring Boot 배포하기(스프링부트 프로젝트 jar / war 파일로 빌드) (0) | 2023.02.10 |
---|---|
[JPA] save , saveAll 비교 (0) | 2023.02.10 |
[Spring] 드래그 앤 드롭 파일 업로드 스프링 예제 (0) | 2023.02.10 |
[Spring Security] 로그인시 발생되는 오류 문구를 내가 설정해보자 (0) | 2023.02.10 |
[Spring Boot] 로그인 및 사용자 인증 정보 참조 (0) | 2023.02.10 |