엔티티들은 대부분 다른 엔티티와 연관관계가 있다.
객체의 참조와 테이블의 외래 키를 매핑하는 것이 목표이다.
- 방향 : 단방향, 양방향
- 다중성 : 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M) 이 있다.
- 연관관계의 주인(owner) : 객체를 양방향 연관관계로 만들면 연관관계의 주인을 만들어야 한다.
단방향 연관관계
요구사항
- 회원과 팀이 있다.
- 회원은 하나의 팀에만 소속 될 수 있다.
- 회원과 팀은 다대일(N:1)관계이다.
객체 연관관계
- 회원 객체는 Member.team 필드로 팀 객체와 연관관계를 맺는다.
- 회원 객체와 팀 객체는 단방향 관계
- 회원은 Member.team 필드를 통해 팀을 알 수 있다.
- 팀은 회원의 존재를 모른다.
- member.getTeam()은 가능
- team은 접근 필드가 없다.
테이블 연관관계
- 회원 테이블은 TEAM_ID 외래 키로 팀 테이블과 연관 관계를 맺는다.
- 회원과 테이블과 팀 테이블은 양방향 관계
- 회원 테이블의 TEAM_ID를 통해 팀과 JOIN 가능
- 팀 테이블의 TEAM_ID를 통해 JOIN 가능
조인 코드
# MEMBER의 TEAM_ID로 조인
SELECT *
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID;
#TEAM의 TEAM_ID로 조인
SELECT *
FROM TEAM T
JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID;
객체 연관관계와 테이블 연관관계의 차이점
- 참조를 통한 연관관계는 언제나 단방향이다.
- 객체간에 연관관계를 양방향으로 만드려면 반대쪽에도 필드를 추가해서 참조를 보관해야함
- 결국 객체는 양방향 관계가 아니라 서로 다른 단방향 관계가 2개인 셈이다.
객체 양방향의 예시
# 단방향, B는 A의 존재를 모름
class A {
B b;
}
class B {
}
#-----------------------------------
# 양방향, A와 B는 서로를 참조
class A {
B b;
}
class B{
A a;
}
- 객체는 참조(주소)로 연관관계를 맺는다.
- 테이블은 외래 키로 연관관계를 맺는다.
JPA를 사용해서 매핑
@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID")
private String id;
private String username;
//연관관계 매핑
@ManyToOne
@JoinColumn(name="TEAM_ID")
private Team team;
}
@Entity
public class Team{
@Id
@Column(name = "TEAM_ID")
private String id;
private String name;
}
@ManyToOne | 다대일(N:1) 매핑 |
@JoinColumn(name = "TEAM_ID") | 외래 키를 매핑, name 속성은 매핑할 외래 키 이름을 지정, 어노테이션 생략 가능 |
@JoinColumn
속성 | 기능 | 기본값 |
name | 매핑할 외래 키 이름 | 필드명 + _ + 참조하는 테이블의 기본 키 컬럼명 |
referencedColumnName | 외래 키가 참조하는 대상 테이블의 컬럼명 | 참조하는 테이블의 기본 키 컬럼명 |
foreginKey(DDL) | 외래 키 제약 조건을 직접 지정, 이 속성은 테이블을 생성할 때만 사용 |
@ManyToOne
속성 | 기능 | 기본값 |
optional | false로 설정하면 연관된 엔티티가 항상 있어야 한다. | true |
fetch | 글로벌 페치 전략을 설정 | @ManyToOne = FetchType.EAGER @ONeToMay = FetchType.LAZY |
cascade | 영속성 전이 기능 사용 | |
targetEntity | 연관된 엔티티의 타입 정보를 설정 거의 사용되지 않음 컬렉션을 사용해도 제네릭의 타입 정보를 알 수 있다. |
targetEntity 사용 예시
@OneToMany
private List<Member> members; // 제네릭으로 타입 정보를 알 수 있다.
@OneToMany(targetEntity = Member.class)
Private List members; //제네릭이 없으면 타입 정보를 알 수 없다.
양방향 연관관계
- 회원 -> 팀
- 팀 -> 회원
양방향 연관관계 매핑
@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID")
private String id;
private String username;
//연관관계 매핑
@ManyToOne
@JoinColumn(name="TEAM_ID")
private Team team;
//연관관계 설정
public void setTeam(Team team) {
this.team = team;
}
}
@Entity
public class Team{
@Id
@Column(name = "TEAM_ID")
private String id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<Member>();
}
팀과 회원은 일대다(1:N) 관계이기 때문에 팀 엔티티 컬렉션인 List<Member> members를 추가했다.
일대다 관계를 매핑하기 위해 @OneToMany 매핑정보를 사용했다.
mappedBy 속성은 양방향 매핑일 때 사용하는데 반대쪽 매핑의 필드 이름을 값을 주면 된다.
연관관계의 주인
mappedBy는 왜 필요할까?
엄밀히 말하자면 객체에는 양방향 연관관계라는 것이 없다.
단방향 연관관계가 2개 있을뿐...
앱 로직을 잘 묶어서 양방향인 것처럼 보이게 하는 것 뿐이다.
- 회원 -> 팀연관관계 1개(단방향)
- 팀 -> 회원 연관관계 1개(단방향)
테이블의 연관관계는
- 회원 <-> 팀의 연관관계 1개(양방향)
테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다.
이런 차이로 인해 JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계의 주인이라한다.
양방향 매핑의 규칙: 연관관계의 주인
- 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록,수정,삭제)할 수 있다.
- 주인이 아닌 쪽은 읽기만 할 수 있다.
- 주인은 mappedBy 속성을 사용하지 않는다.
- 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.
연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것이다.
- 연관관계의 주인은 외래 키가 있는곳
- 연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야 한다.
- 연관관계의 주인만 데이터베이스 연관관계와 매핑되고 외래 키를 관리할 수 있다.
- 주인이 아닌 반대편은 읽기만 가능하고 외래키를 변경하지는 못한다.
참고
DB 테이블은 N:1, 1:N 관계에서는 항상 N 쪽이 외래 키를 가진다.
N 쪽인 @ManyToOne은 항상 연관관계의 주인이 되므로 mappedBy를 설정할 수 없다. 따라서 @ManyToOne에는 mappedBy 속성이 없다.
양방향 연관관계 저장
양방향 연관관계를 사용해서 저장해보기
public void testSave()
// 팀1 저장
Team team1 = new Team("Team1", "팀1");
em.persist(team1);
//회원1 저장
Member member1 = new Member("member1", "회원1");
member1.setTeam(team1); //연관관계 설정 member1 -> team1
em.persist(member1);
//회원2 저장
Member member1 = new Member("member1", "회원2");
member1.setTeam(team1); //연관관계 설정 member2 -> team1
em.persist(member1);
}
SELECT * FROM MEMBER;
조회해보면
MEMBER_ID | USERNAME | TEAM_ID |
member1 | 회원1 | team1 |
member2 | 회원2 | team1 |
양방향 연관관계는 연관관계의 주인이 외래키를 관리한다.
주인이 아닌 방향은 값을 설정하지 않아도 데이터베이스에 외래 키 값이 정상 입력된다.
team1.getMembers().add(member1); //무시(연관관계의 주인이 아님)
team1.getMembers().add(member2); //무시(연관관계의 주인이 아님)
Team이 연관관계의 주인이 아니다. 주인이 아닌 곳에 입력된 값은 외래 키에 영향을 주지 않는다.
member1.setTeam(team1); //연관관계 설정(연관관계의 주인)
member2.setTeam(team1); //연관관계 설정(연관관계의 주인)
Member.team은 연관관계의 주인이다. 엔티티 매니저는 이곳에 입력된 값을 사용해서 외래 키를 관리한다.
양방향 연관관계 주의점
양방향 연관관계를 설정하고 가장 흔히 하는 실수는 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것이다.
예를 들면 연관관계의 주인이 아닌 TEAM에만 연관관계를 설정하고, MEMBER에 아무 값도 입력하지 않으면 MEMBER를 조회했을떄 TEAM_ID는 null값이 출력된다.
따라서 연관관계의 주인만이 외래 키 값을 변경할 수 있으므로 연관관계의 주인에는 값을 꼭 입력해야한다.
순수한 객체까지 고려한 양방향 연관관계
연관관계의 주인에만 값을 저장하고 주인이 아닌 곳에는 값을 저장않아도 될까? 사실은 객체 관점에서 양쪽 방향 모두에게 값을 입력해주는 것이 안전한다. JPA를 사용하지 않는 순수 객체 상태에서는 심각한 문제가 발생할 수 있다.
정리
단방향 매핑과 비교해서 양방향 매핑은 복잡하다.
연관관계의 주인도 정해야하고, 두 개의 단방향 연관관계를 양방향으로 만들기 위해 로직도 잘 관리해야 한다.
중요한 사실은 연관관계가 하나인 단방향 매핑은 언제나 연관관계의 주인이라는 점이다.
양방향은 주인이 아닌 연관관계를 추가했을 뿐이다.
단방향과 비교해서 양방향의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된 것뿐이다.
주인의 반대편은 mappedBy로 주인을 지정해야한다.
그리고 주인의 반대편은 단순히 보여주는 일만 할 수 있다.
- 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료되었다.
- 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가된다.
- 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 한다.
주의
양방향 매핑시 무한 루프에 빠지지 않게 조심해야 한다.
Member.toStirng()에서 getTeam()을 호출
Team.toString에서 getMember()를 호출하면 무한 루프에 빠질 수 있다.
이런 문제는 JSON으로 변환할 때 자주 발생하는데 JSON라이브러리들은 보통 루한루프에 빠지지 않도록 하는 어노테이션이나 기능을 제공한다.
그리고 Lombk이라는 라이브러리를 사요할 때도 자주 발생한다.
참고
일대다를 연관관계의 주인으로 선택하는 것이 불가능한 것만은 아니다.
Team.members를 연관관계의 주인으로 선택할 수 있는데
성능과 관리 측면에서 권장하지 않는다고 한다.
될 수 있으면 외래 키가 있는 곳을 연관관계의 주인으로 선택하자.
'JPA' 카테고리의 다른 글
JPA 엔티티 매핑 (0) | 2024.06.23 |
---|---|
JPA 영속성 관리 (0) | 2024.06.20 |
JPA 시작 (2) | 2024.06.08 |
JPA(Java Persistent API) 소개 (0) | 2024.05.11 |