연관관계 매핑 시 고려사항 3가지
- 다중성
- 단방향, 양방향
- 연관관계의 주인
다중성
- 다대일: @ManyToOne
- 일대다: @OneToMany
- 일대일: @OneToOne
- 다대다: @ManyToMany
JPA에서 나오는 어노테이션은 전부 DB랑 매핑하기 위해서 있다.
그래서 데이터베이스 관점에서의 다중성을 기준으로 고민하면 된다.
다중성에서 애매하다고 생각되면 반대쪽으로 생각해 보자.
단방향, 양방향
테이블
- 외래키 하나로 양쪽 조인 가능
- 그래서 방향이라는 개념이 없음
객체
- 참조용 필드가 있는 쪽으로만 참조 가능
- 한쪽만 참조하면 단방향
- 양쪽이 서로 참조하면 양방향
연관관계의 주인
테이블은 외래키 하나로 두 테이블이 연관관계를 맺는다.
반면 객체 양방향 관계는 A->B, B->A 처럼 참조가 2군데이다.
객체 양방향 관계는 참조가 2군데 있어서 둘중 테이블의 외래키를 관리할 곳을 지정해야한다.
- 연관관계의 주인: 외래키를 관리하는 참조
- 주인의 반대편: 외래키에 영향을 주지 않음, 단순 조회만 가능하다.
다대일 [N:1]
- 가장 많이 사용하는 연관관계
- 다대일의 반대는 일대다
다대일 양방향
- 외래 키가 있는 쪽이 연관관계의 주인
- 양쪽을 서로 참조하도록 개발
일대다 [1:N]
여기서는 1이 연관관계 주인이다.
1쪽에서 외래키를 관리하겠다고 보면 된다.
이러한 모델은 권장되지 않는다.
일대다 단방향
객체 연관관계에서는 Team의 members가 연관관계의 주인이 되는 설계 방식이다.
DB입장에서는 N쪽에 외래키가 존재해야 돼서 MEMBER 테이블에 외래키가 존재한다.
그래서 Team의 members의 값이 추가되거나 변경되면 MEMBER 테이블에 있는 FK를 변경해야 한다.
코드 예시
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME", nullable = false)
private String name;
...
}
@Entity
public class Team {
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<>();
...
}
Team쪽에 @JoinColumn을 사용했다.
테스트 코드
Member member = new Member();
member.setName("member1");
em.persist(member);
Team team = new Team();
team.setName("teamA");
team.getMembers().add(member);
em.persist(team);
tx.commit();
실행결과
Hibernate:
/* insert for
jpa_basic.ex1_hello_jpa.hellojpa.Member */insert
into
Member (USERNAME, MEMBER_ID)
values
(?, default)
Hibernate:
select
next value for Team_SEQ
Hibernate:
/* insert for
jpa_basic.ex1_hello_jpa.hellojpa.Team */insert
into
Team (name, TEAM_ID)
values
(?, ?)
Hibernate:
update
Member
set
TEAM_ID=?
where
MEMBER_ID=?
DB에서 외래키가 '다'쪽 테이블에 존재한다.
그래서 객체 쪽 다:일 관계에서는 객체를 생성할 때 값을 넣어주면 insert 쿼리가 2번(team, member) 나가고 끝이다.
하지만 일:다 설계에서는 일이 연관관계 주인이지만 테이블에서는 다쪽에 외래키가 존재해 테이블 다쪽에 외래키에 대한 update 쿼리가 한번 더 나간다. 실행결과를 보면 외래키인 TEAM_ID 값을 수정하는 update 쿼리가 나간 걸 볼 수 있다. team 엔티티 값을 수정했는데 member 테이블의 값이 변경되는 결과를 초래한다.
또한 실무에서는 테이블 수십 개가 엮여 있는 상황이라 위와 같이 설계하면 운영하기 힘들다.
일대다 단방향 정리
- 일대다 단방향은 일대다(1:N)에서 일(1)이 연관관계의 주인
- 테이블 일대다 관계는 항상 다(N) 쪽에 외래키가 있음
- 객체와 테이블의 차이 때문에 반대편 테이블의 외래 키를 관리하는 특이한 구조
- @JoinColumn을 꼭 사용해야 함. 그렇지 않으면 조인 테이블 방식을 사용함(중간에 테이블을 하나 추가함)
일대다 단방향 매핑의 단점
- 엔티티가 관리하는 외래키가 다른 테이블에 있음
- 연관관계 관리를 위해 추가로 UPDATE SQL 실행
일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자
일대다 양방향
- 이런 매핑은 공식적으로 존재X
- @JoinColumn(insertable=false, updatable=false) 삽입과 수정 모두 false로 둔다.
- 그러면 읽기 전용 필드로 만들어서 양방향처럼 사용하는 방법
- 다대일 양방향을 사용하자
코드 예시
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private Long id;
@ManyToOne
@JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
private Team team;
...
}
일대일 [1:1]
- 일대일 관계는 그 반대도 일대일
- 주 테이블이나 대상 테이블 중에 외래 키 선택 가능
- 주 테이블에 외래 키
- 대상 테이블에 외래 키
- 둘 중에 아무 데나 넣어도 상관없다.
- 외래 키에 데이터베이스 유니크(UNI) 제약조건 추가
- DB 입장에서는 외래 키의 데이터베이스 유니크 제약 조건이 추가가 된 게 1:1 관계다.
일대일: 주 테이블에 외래 키 단방향
Member를 주 테이블이라고 가정하자.
이 방식은 다대일(@ManyToOne) 단방향 매핑과 유사하다.
코드 예시
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private Long id;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
...
}
일대일: 주 테이블에 외래 키 양방향 정리
- 다대일 양방향 매핑처럼 외래 키가 있는 곳이 연관관계의 주인
- 반대편은 mappedBy 적용
코드 예시
@Entity
public class Locker {
@Id
@GeneratedValue
private Long id;
@OneToOne(mappedBy = "locker")
private Member member;
}
일대일: 대상 테이블에 외래 키 단방향
- 단방향 관계는 JPA 지원X
- 양방향 관계는 지원
일대일: 대상 테이블에 외래 키 양방향
- 일대일 주 테이블에 외래 키 양방향과 매핑 방법은 같다
일대일 정리
주 테이블에 외래 키
- 주 객체가 대상 객체의 참조를 가지는 것처럼 주 테이블에 외래 키를 두고 대상 테이블을 찾음
- 객체지향 개발자 선호
- JPA 매핑 편리
- 장점: 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능
- 단점: 값이 없으면 외래 키에 null 허용
대상 테이블에 외래 키
- 대상 테이블에 외래 키가 존재
- 전통적인 데이터베이스 개발자 선호
- 장점: 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조 유지
- 단점: 양방향으로 설계해야 한다.
- 단점: 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩됨(프록시는 뒤에서 설명)
다대다[N:M]
- 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없음
- 연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야 함
Member 객체는 Product List를 가질 수 있다.
반대로 Product 객체도 Member List를 가질 수 있다.
그래서 객체는 컬렉션을 사용해서 객체 2개로 다대다 관계가 가능하다.
- @ManyToMany 사용
- @JoinTable로 연결 테이블 지정
- 다대다 매핑: 단방향, 양방향 가능
다대다 매핑의 한계
- 편리해 보이지만 실무에서 사용X
- 연결 테이블이 단순히 연결만 하고 끝나지 않음
- 주문시간, 수량 같은 데이터가 들어올 수 있음
Member_Product 테이블이 단순히 연결할 때만 사용되는 게 아니다.
이 테이블에 주문시간, 수량 같은 데이터가 들어간다.
하지만 매핑 정보만 중간 테이블에 들어갈 수 있고 추가 정보를 넣는 게 불가능하다.
그리고 중간 테이블이 존재하여 멤버에서 제품 조인 시 쿼리가 이상하게 나갈 수 있다.
이러한 이유로 실무에서는 사용하지 않는다.
다대다 한계 극복
- 연결 테이블용 엔티티 추가(연결 테이블을 엔티티로 승격)
- @ManyToMany -> @OneToMany, @ManyToOne
코드 예시
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private Long id;
@OneToMany(mappedBy = "member")
private List<MemberProduct> memberProducts = new ArrayList<>();
...
}
@Entity
public class MemberProduct {
@Id
@GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
@ManyToOne
@JoinColumn(name = "PRODUCT_ID")
private Product product;
}
@Entity
public class Product {
@Id
@GeneratedValue
@Column(name = "PRODUCT_ID")
private Long id;
@OneToMany(mappedBy = "product")
private List<MemberProduct> memberProducts = new ArrayList<>();
...
}
PK 값을 종속되는 식으로 설계하면 시스템을 유연하게 변경하는 것이 어렵다.
그래서 PK 값에는 비즈니스적으로 의미가 없는 값을 넣는게 좋다.
정리
N:M 관계는 1:N, N:1로
- 테이블의 N:M 관계는 중간 테이블을 이용해서 1:N, N:1
- 실전에서는 중간 테이블이 단순하지 않다.
- @ManyToMany는 제약: 필드 추가X, 엔티티 테이블 불일치
- 실전에서는 @ManyToMany 사용X
@JoinColumn
@ManyToOne - 주요 속성
다대일 관계 매핑
@OneToMany - 주요 속성
일대다 관계 매핑
참고
김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본 편 강의를 보고 정리한 내용입니다.
'JPA > JPA 기본' 카테고리의 다른 글
8. 프록시와 연관관계 관리 (0) | 2024.08.11 |
---|---|
7. 상속관계 매핑 (0) | 2024.08.10 |
5. 연관관계 매핑 기초 (0) | 2024.08.10 |
4. 엔티티 매핑 (0) | 2024.08.09 |
3. 영속성 관리 - 내부 동작 방식 (0) | 2024.08.08 |