연관관계 매핑시 고려사항 3가지
다중성
- 다대일: @ManyToOne
- 일대다: @OneToMany
- 일대일: @OneToOne
- 다대다: @ManyToMany
→ 다대다는 실무에서 절대 사용하면 안된다.
단방향, 양방향
- 테이블
- 외래 키 하나로 양쪽 조인 가능
- 방향이라는 개념이 없음.
- 객체
- 참조용 필드가 있는 쪽으로만 참조 가능
- 한 쪽만 참조하면 단방향
- 양쪽이 서로 참조하면 양방향
객체 입장에선 참조는 방향이 각각 하나일 뿐이다. 즉, 단방향이 2개가 있는 것이다.
이렇게 이해해야 연관 관계 주인에 대한 이해가 수월하다.
연관관계의 주인
- 테이블은 외래 키 하나로 두 테이블의 연관관계를 찾음
- 객체 양방향 관계는 A→B, B→A처럼 참조가 2군데
- 객체 양방향 관계는 참조가 2군데 있음. 둘중 테이블의 외래 키 를 관리할 곳을 지정해야함
- 연관관계의 주인 : 외래 키를 관리하는 참조
- 주인의 반대편 : 외래 키에 영향을 주지않고 단순 조회(참조)만 가능
다대일(N:1)
다대일(N:1) 단방향
- 가장 많이 사용하는 연관관계
- 외래키가 있는 테이블의 엔티티에 참조 값을 만들고 연관 관계 매핑을 하면 된다.
- 다대일의 반대는 일대다
다대일(N:1) 양방향
- 외래 키가 있는 쪽이 연관관계의 주인
- 양쪽을 서로 참조하도록 개발
- 연관관계가 주인이 아닌 쪽은 단순 조회만 가능하기에 필드만 추가해주면 된다.
- 주의할 점은 객체간 양방향 연관관계를 설정한다고 해서 테이블에 영향을 주는 것은 아니다.
// === Member Entity ===
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
// === Team Entity ===
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
일대다(1:N)
일(One)이 연관관계의 주인이다
→ 권장하는 방법은 아니다 실무에서도 거의 사용되지 않음.
일대다(1:N) 단방향
- 일대다 단방향은 일대다(1:N)에서 일(1)이 연관관계의 주인
- 테이블 일대다 관계는 항상 다(N) 쪽에 외래 키가 있다
- 객체와 테이블의 차이 때문에 반대편 테이블의 외래키를 관리하는 특이한 구조
- Ex) Team에 있는 members의 값을 변경하면 Member 테이블로 쿼리가 실행되는 특이한 구조
Member member = new Member();
member.setName("member1");
em.persist(member);
Team team = new Team();
team.setName("teamA");
// team 테이블이 아니라 member 테이블의 외래키가 바껴야함...어색
team.getMembers().add(member);
em.persist(team);
tx.commit();
권장하지 않는 이유
- 테이블에서는 항상 다(N) 쪽에 외래키가 있기 때문에 패러다임 충돌이 있다.
- @JoinColumn을 꼭 사용해야 한다. 그렇지 않으면 조인 테이블 방식을 사용한다(중간에 테이블을 하나 추가함)
- 엔티티가 관리하는 외래 키가 다른 테이블에 있음
- 연관관계 관리를 위해 추가로 UPDATE SQL 실행
- 일대다(1:N)에서 저장(save)이 될 때 양 쪽 객체를 저장한 뒤 update query를 통해 외래키 설정( 3번이나 수행)
- 실무에선 테이블이 수십개 이상 운영이 되는데, 관리 및 트레이싱이 어렵다.
위 코드만 살펴보면 개발자는 team 엔티티를 중심으로 개발을 했는데 쿼리를 확인해보니 member에 대해 Update가 수행됨. 이러한 쿼리 추적 및 관리가 힘든다.
결론
기본은 다대일(N:1)로 구현하자.
필요에 의해 객체지향적으로 참조를 추가하여 설계가 조금 덜 깔끔해지더라도 다대일(N:1) 양방향 관계를 수립하도록 하자.
일대다(1:N) 양방향
- 이런 매핑은 공식적으로는 존재하지 않는다.
- @JoinColumn(insertable=false, updatable=false)
- Member Entity의 team field가 읽기전용 field가 됐다.
/* 팀(Team) */
public class Team{
...
@OneToMany
@JoinColumn(name="TEAM_ID")
private List<Member> members = new ArrayList<>();
...
}
/* 멤버(Member) */
public class Member{
...
@ManyToOne
@JoinColumn(name="TEAM_ID", insertable=false, updatable=false)
private Team team;
...
}
- 읽기 전용 필드를 사용해서 양방향 처럼 사용하는 방법
- 다대일 양방향을 사용하자
일대일(1:1)
일대일 관계
- 일대일 관계는 그 반대도 일대일
- 주 테이블이나 대상 테이블 중에 외래 키 선택 가능
- 주 테이블에 외래 키
- 대상 테이블에 외래 키
- 외래 키에 데이터베이스 유니크(UNI) 제약조건 추가
주 테이블에 외래 키 단방향
- 다대일(@ManyToOne) 단방향 매핑과 유사
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
주 테이블에 외래 키 양방향
- 다대일 양방향 매핑 처럼 외래 키가 있는 곳이 연관관계의 주인
- 반대편은 mappedBy 적용
@OneToOne(mappedBy = "locker")
private Member member;
대상 테이블에 외래 키 단방향
- 단방향 관계는 JPA 자체가 지원X
- 양방향 관계는 지원
대상 테이블에 외래 키 양방향
- 사실 일대일 주 테이블에 외래 키 양방향과 매핑 방법은 같음
그렇다면 DB입장에서 외래키를 Member에 놓는 거랑 Locker에 놓는 것 중에 어떤게 더 좋은 설계일까?
사실 둘 중 어떠한 방법을 사용하여라도 객체 입장에서는 일대일(1:1) 관계가 유효하게 성립한다.
하지만 DBA 입장에서 설계했을 때 나중에 비지니스 룰이 바껴 하나의 Member가 여러 Locker를 가질 수 있다고 한다면 대상 테이블에 외래 키 양방향처럼 설계하는 것이 Unique 제약 조건만 빼주면 되기 때문에 더 수월할 것이다.
그런데 만약 주 테이블 외래키 단방향처럼 기존에 Member에 외래키가 있었는데 하나의 회원이 여러 Locker를 사용할 수 있다는 시나리오가 된다면 Member 테이블의 외래키는 의미가 없어지므로 지우고 Locker 테이블에 컬럼을 추가하고 기능 변경이 수반될 것이다.
하지만 ORM을 직접 사용하는 개발자의 입장에서는 Member에 외래키가 있는 것이 성능 면에서도 그렇고 여러가지가 유리하다. 일반적으로 Member 엔티티를 많이 조회하므로 이미 조회가 됐을 테니 이미 Locker 값이 있으므로 Join없이 쿼리 하나로 성능상 이점이 있을 것이다.
보통은 주 테이블 외래키 단방향처럼 설계를 가져가면 개발상 이점이 있으므로 이 부분은 DBA랑 항상 협의가 필요하다.
일대일 정리
- 주 테이블이나 대상 테이블 중에 외래 키 선택 가능
- 외래키에 데이터베이스 유니크 제약조건 추가
- 다대일 연관관계와 동일하게 외래키가 있는곳이 연관관계의 주인
- 연관관계의 주인이 아닌 곳에 mappedBy를 넣어준다.
주 테이블에 외래 키
- 주 객체가 대상 객체의 참조를 가지는 것처럼 주 테이블에 외래 키를 두고 대상 테이블을 찾음
- 객체지향 개발자 선호
- JPA 매핑 편리
- 장점: 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능
- 단점: 값이 없으면 외래 키에 null 허용
대상 테이블의 외래 키
- 대상 테이블에 외래 키가 존재
- 전통적인 데이터베이스 개발자 선호
- 장점: 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조 유지
- 단점: 주 테이블에는 외래키가 없기 때문에 대상 테이블이 있는지 없는지 알 수 없음. 그러므로 값의 유무를 확인하기 위해 필연적으로 대상 테이블을 조회할 수 밖에 없음. 이러한 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩됨
다대다(N:M)
- 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없음
- 연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야함
- 객체는 컬렉션을 사용해서 객체 2개로 다대다 관계 가능
- @ManyToMany 사용
- @JoinTable로 연결 테이블 지정 ex. @JoinTable(name = "MEMBER_PRODUCT")
- 다대다 매핑: 단방향, 양방향 가능
=== Member Entity ===
@ManyToMany
@JoinTable(name = "MEMBER_PRODUCT")
private List<Product> products = new ArrayList<>();
=== Product Entity ===
@ManyToMany(mappedBy = "products")
private List<Member> members = new ArrayList<>();
다대다 매핑의 한계
- 편리해 보이지만 실무에서 사용X
- 연결 테이블이 단순히 연결만 하고 끝나지 않음.
- 주문시간, 수량 같은 데이터가 들어올 수 있음.그런데 못들어옴
- 중간테이블에 추가적인 데이터를 넣을 수 없다는 한계점 존재.
- 중간 테이블이 숨겨져 있기 때문에 의도치 않은 쿼리가 생성 될 수 있음.
다대다 한계 극복
- 연결 테이블용 엔티티 추가(연결 테이블을 엔티티로 승격)
- Ex: Order 와 Item 사이에 OrderItem 연결 테이블을 엔티티로 추가
- @ManyToMany → @OneToMany, @ManyToOne
- 아래의 Order 테이블과 다르게 MEMBER_ID, PRODUCT_ID를 복합키로 PK 선언도 가능하지만, 새로운 프라이머리 키를 선언해서 사용하는게 조금 더 선호됨.
@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;
private int count;
private int price;
private LocalDateTime orderDateTime;
}
=== Member ===
@OneToMany(mappedBy = "member")
private List<MemberProduct> memberProducts = new ArrayList<>();
=== Product ===
@OneToMany(mappedBy = "product")
private List<MemberProduct> memberProducts = new ArrayList<>();
실전 예제
엔티티
ERD
엔티티 상세
N:M 관계는 1:N, N:1로
- 테이블의 N:M 관계는 중간 테이블을 이용해서 1:N, N:1
- 실전에서는 중간 테이블이 단순하지 않다.
- @ManyToMany는 제약: 필드 추가X, 엔티티 테이블 불일치
- 실전에서는 @ManyToMany 사용X
@JoinColumn - 외래 키를 매핑할 때 사용
속성 | 설명 | 기본값 |
name | 매핑할 외래 키 이름 | 필드명 + _ + 참조하는 테 이블의 기본 키 컬럼명 |
referencedColumnName | 외래 키가 참조하는 대상 테이블의 컬럼명 | 참조하는 테이블의 기본 키 컬럼명 |
foreignKey(DDL) | 외래 키 제약조건을 직접 지정할 수 있다. 이 속성은 테이블을 생성할 때만 사용한다. |
|
unique nullable insertable updatable columnDefinition table |
@Column의 속성과 같다. |
@ManyToOne
속성 | 설명 | 기본값 |
optional | false로 설정하면 연관된 엔티티가 항상 있어야 한다. | TRUE |
fetch | 글로벌 페치 전략을 설정한다. | - @ManyToOne=FetchType.EAGER - @OneToMany=FetchType.LAZY |
cascade | 영속성 전이 기능을 사용한다. | |
targetEntity | 연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거 의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타 입 정보를 알 수 있다 |
@OneToMany
속성 | 설명 | 기본값 |
mappedBy | 연관관계의 주인 필드를 선택한다. | |
fetch | 글로벌 페치 전략을 설정한다. | - @ManyToOne=FetchType.EAGER - @OneToMany=FetchType.LAZY |
cascade | 영속성 전이 기능을 사용한다. | |
targetEntity | 연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거 의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타 입 정보를 알 수 있다. |