다양한 연관관계 매핑
엔티티의 연관관계를 매핑할 때는 다음 3가지를 고려해야 한다.
■ 다중성
연관관계에는 다음과 같은 다중성이 있다.
● 다대일(@ManyToOne)
● 일대다(@OneToMany)
● 일대일(@OneToOne)
● 다대다(@ManyToMany)
■ 단방향, 양방향
테이블은 외래 키 하나로 조인을 사용해서 양방향으로 쿼리가 가능하므로 방향이라는 개념이 없다.
반면에 객체는 참조용 필드를 가지고 있는 객체만 연관 객체를 조회할 수 있다.
객체 관계에서 한쪽만 참조하는 것을 단방향 관계라 하고, 양쪽이 서로 참조하는 것을 양방향 관계라 한다.
■ 연관관계의 주인
객체를 양방향 관계로 구성하면 각 객체는 연관관계를 서로 참조할 수 있다.
따라서 객체의 연관관계를 관리하는 포인트는 2곳이 된다.
JPA는 두 개체 연관관계 중 하나를 정해서 데이터베이스 외래 키를 관리하는데 이것을 연관관계의 주인이라 한다.
보통 외래 키를 가진 테이블과 매핑한 엔티티가 외래 키를 관리하는 게 효율적이다.
1. 다대일
■ 다대일 단방향[N:1]
Member 엔티티와 Team 엔티티가 다음과 같은 관계를 갖고있다고하자
[다대일 단방향]
Member는 Team을 참조할 수 있지만 Team은 Member를 참조할 수 있는 필드가 없다.
따라서 Member와 Team은 다대일 단방향 연관관계다.
[다대일 단방향 매핑]
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
@ManyToOne 으로 연관관계 정보를 명시하고, @JoinColumn 으로 "TEAM_ID" 의 외래 키와 매핑하였다.
■ 다대일 양방향[N:1, 1:N]
[다대일 양방향]
위 예제처럼 Member도 Team을 조회할 수 있고 Team도 Member를 필드로 갖고 있기 때문에 Team도 Member를 조회할 수 있다.
따라서 Member와 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<Member>();
}
member를 참조할 수 있는 members를 필드로 넣었고, @OneToMay 로 매핑하였다.
연관관계의 주인은 Member이기 때문에 mappedBy를 사용해서 연관관계의 주인(Member의 team)을 명시했다.
연관관계의 주인이 아니기 때문에 members는 조회만 할 수 있다.
■ 다대일 양방향 주의점
● 다대일 양방향 매핑은 항상 서로를 참조해야 한다.
◎ 어느 한쪽만 참조하면 양방향 연관관계가 성립하지 않는다.
◎ 어떤 상황에서든 서로를 참조할 수 있도록 한쪽 값이 세팅되면 반대 값도 세팅되도록 설계해야 한다.
◎ 연관관계에 있는 값이 삭제될 때 반대 값이 삭제되는 것도 고려해줘야 한다.
● 무한루프
◎ toString(), Lombook, JSON 변환 등 필드에 메서드를 호출하는 경우 무한 루프가 발생할 수 있음을 고려해야 한다.
2. 일대다
일대다 관계는 다대일 관계의 반대 방향이다.
일대다 관계는 엔티티를 하나 이상 참조할 수 있으므로 자바 컬렉션 중에 하나를 사용해야 한다.
■ 일대다 단방향
[일대다 단방향]
Team은 members로 여러 Member를 조회할 수 있다.
하지만 Member는 Team과 관련된 필드가 없기 때문에 Team을 조회할 수 없다.
따라서 Team과 Member는 일대다 단방향 관계라고 볼 수 있다.
위 예제에서 알 수 있듯이 Team의 members는 Member 테이블의 외래 키(TEAM_ID)와 매핑된다.
즉, 반대쪽 테이블에 있는 외래 키를 관리하는 꼴이 된다.
[일대다 단방향 매핑]
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "TEAM_ID") // MEMBER 테이블의 TEAM_ID(FK)
private List<Member> members = new ArrayList<Member>();
}
members에 @JoinColumn 으로 MEMBER 테이블의 TEAM_ID(FK)와 매핑하였다.
일대다 단방향 관계를 매핑할 때는 @JoinColumn을 명시해야 한다.
그렇지 않으면 JPA는 연결 테이블을 중간에 두고 연관관계를 관리하는 조인 테이블 전략을 기본으로 사용해서 매핑한다.
□ 일대다 단방향 매핑의 단점
일대다 단방향 매핑의 단점은 매핑한 객체가 관리하는 외래 키가 다른 테이블에 있다는 점이다.
본인 테이블에 외래 키가 있으면 엔티티의 저장과 연관관계 처리를 INSERT SQL 한 번으로 끝낼 수 있지만 다른 테이블에 외래 키가 있으면 연관관계 처리를 위한 UPDATE SQL을 추가로 실행해야 한다.
[일대다 단방향 매핑의 단점]
Member member1 = new Member("member1");
Member member2 = new Member("member2");
Team team1 = new Team("team1");
team1.getMembers().add(member1);
team1.getMembers().add(member2);
em.persist(member1); //INSERT-member1
em.persist(member2); //INSERT-member2
em.persist(team1); //INSERT-team1, UPDATE-member1.fk, UPDATE-member2.fk
위 예제의 코드를 실행시키면 다음과 같은 SQL이 실행된다.
[실제 적용되는 SQL]
insert into Member (MEMBER_ID, username) values (null, ?)
insert into Member (MEMBER_ID, username) values (null, ?)
insert into Team (TEAM_ID, username) values (null, ?)
update Member set TEAM_ID=? where MEBER_ID=?
update Member set TEAM_ID=? where MEBER_ID=?
member를 persist()하면 Insert문이 나간다.
하지만 member 엔티티는 Team 엔티티를 모르기 때문에 TEAM_ID의 값을 처리할 수 없다.
따라서 member 엔티티를 저장할 때는 TEAM_ID 외래 키에 아무 값도 저장되지 않는다.
대신, Team 엔티티를 저장할 때 Team.members의 참조 값을 확인해서 회원 테이블에 있는 TEAM_ID 외래 키를 업데이트한다.
위와 같이 일대다 매핑을 하면 다른 테이블의 외래 키를 관리해야 하기 때문에 관리도 부담스럽지만, 성능 상 문제도 발생한다.
따라서 일대다 단방향 매핑보다는 차라리 다대일 양방향 매핑을 사용하는 것이 권장된다.
■ 일대다 양방향
일대다 양방향 매핑은 존재하지 않는다(JPA에서 지원 안 함).
3. 일대일
일대일 관계는 그 반대도 일대일이다.
따라서 다대일 - 일대다 관계처럼 역방향을 신경 쓸 필요는 없다.
다만, 주 테이블과 대상 테이블 둘 중에서 어느 곳이 외래 키를 관리하는지를 고려해주어야 한다.
■ 일대일 : 주 테이블에 외래 키 단방향
[일대일 : 주 테이블에 외래 키 단방향]
주 테이블인 Member에 외래 키가 있고 Member 객체는 필드 멤버인 locker 로 외래 키와 매핑한다.
Member 테이블이 외래 키로 Locker를 조회할 수 있는 것처럼 Member 객체도 locker를 통해 Locker를 자유롭게 조회할 수 있다.
※ 일대일 관계이기 때문에 외래 키는 유일해야 한다(unique 제약 조건)
[일대일 : 주 테이블에 외래 키 단방향 매핑]
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
}
@OneToOne으로 연관관계를 명시하고 @JoinColumn으로 외래 키를 매핑한다.
주 테이블에 외래 키를 두고, 이에 맞춰 객체도 매핑하면 두 객체의 연관관계상 참조가 쉽다는 장점이 있다.
ex) Member가 주 테이블이기 때문에 Member -> Locker 의 참조가 자연스럽다.
이때, 주 테이블에 외래 키를 두고 매핑하면 member.locker 처럼 member만 있어도 Member -> Locker 조회를 쉽게 할 수 있다
다만, 위 예시의 구조에서 Member에 Locker를 세팅하지 않으면 Member 테이블의 LOCKER_ID(FK)에 null 값이 들어가게 된다.
즉, 주 테이블에 외래 키를 두고 매핑하는 방식은 외래 키에 null 값을 허용할 수 있다는 단점이 있다.
■ 일대일 : 주 테이블에 외래 키 양방향
[일대일 : 주 테이블에 외래 키 양방향]
앞의 단방향 관계에서 Locker가 member를 필드로 갖기 때문에 이제 Locker도 member를 조회할 수 있다.
[일대일 : 주 테이블에 외래 키 양방향 매핑]
@Entity
public class Locker {
@Id @GeneratedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
@OneToOne(mappedBy = "locker")
private Member member;
}
연관관계의 주인은 Member이기 때문에 mappedBy로 Member의 locker를 지정하였다.
당연히 Locker의 member는 조회 기능만 제공된다.
■ 일대일 : 대상 테이블에 외래 키 단방향
[대상 테이블에 외래 키 단방향]
대상 테이블에 외래 키를 두고 단방향 관계를 맺으려면 위 예시처럼 대상 테이블인 LOCKER에 외래 키를 두고 Member 엔티티의 locker와 외래 키를 매핑해야 한다.
하지만 JPA는 위와 같은 매핑을 지원하지 않는다.
■ 일대일 : 대상 테이블에 외래 키 양방향
[대상 테이블에 외래 키 양방향]
대상 테이블에 외래 키를 두고, 양방향 관계를 맺는 것은 주 테이블에 외래 키를 두고 양방향 관계를 맺는 것과 같다.
단지 외래 키가 대상 테이블에 있다는 것이 차이점이다.
위 예제처럼 대상 테이블에 외래 키를 두고, 양방향 관계를 맺으면 관계상 외래 키에 null 값이 들어갈 걱정은 하지 않아도 된다는 장점이 있다.
ex) Member가 주 테이블이고 Locker가 대상 테이블이므로 관계상 Member가 있어야 Locker가 있을 수 있기 때문
또한, 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조를 그대로 유지할 수 있다.
ex) 만약 Member가 여러 개의 Locker를 가질 수 있게 돼서 일대다 관계가 된다 해도 테이블은 변하지 않는다.
단점으로는 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩된다는 것이다.
ex) Member를 조회하면 Locker까지 전부 조회해야 됨
4. 다대다
관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.
때문에 연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야 한다.
Member와 Product라는 엔티티가 서로 다대다 관계라면 객체와 테이블은 아래와 같이 구성될 것이다.
[다대다]
객체는 컬렉션을 사용해서 객체 2개로 다대다 관계가 가능하다.
하지만 테이블은 연결 테이블을 추가해서 일대다, 다대일 관계로 각 테이블을 연결할 수밖에 없다.
다대다 매핑도 @ManyToMany 어노테이션을 사용하면 된다.
다만, @JoinTable 을 사용해서 두 테이블을 연결할 table에 대해 설정해줘야 한다.
[다대다 매핑]
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@ManyToMany
@JoinTable(name = "MEMBER_PRODUCT",
joinColumns = @JoinColumn(name = "MEMBER_ID"),
inverseJoinColumns = @JoinColumn(name = "PRODUCT_ID"))
private List<Product> products = new ArrayLisst<Product>();
}
@JoinTable을 사용해서, 두 테이블을 연결할 테이블의 이름과 외래 키로 사용할 컬럼(현재방향, 양쪽 방향 모두)을 지정해준다.
양방향인 경우 반대 관계에 mappedBy로 연관관계 주인을 지정한다.
■ 다대다 매핑 한계 극복
다대다 매핑은 복잡한 다대다 관계를 편리하게 풀어주는 것 같지만 실무에서 잘 사용되지 않는다.
실무에서는 연결 테이블이 단순 연결만 하고 끝나지 않기 때문이다.
실제로 위의 Member와 Product의 관계에서 연결 테이블은 주문 시간, 수량 같은 데이터가 필요할 수도 있다.
[연결 엔티티 추가 정보]
상황에 따라 Member와 Product가 연결됐을 때를 주문이 발생했다고 하다면, 연결 테이블에 주문과 관련된 값들이 추가적으로 필요할 수 있다.
때문에 @ManyToMany를 사용하기보다는 연결 테이블을 엔티티로 만들고 @OneToMany, @ManyToOne으로 연결하는 게 권장된다.
[연결 테이블을 엔티티 승격]
MemberProduct 라는 엔티티를 만들어 Member와 Product를 연결한다.
이러면 연결 테이블도 자체적인 PK를 사용할 수 있기 때문에 추가적인 관리도 더 쉽게 할 수 있다.
'Spring > JPA' 카테고리의 다른 글
#8 프록시와 연관관계 관리 (0) | 2021.08.29 |
---|---|
#7 고급 매핑 (0) | 2021.08.28 |
#5 연관관계 매핑 기본 (0) | 2021.08.27 |
#4 엔티티 매핑 (0) | 2021.08.27 |
#3 영속성 관리 (0) | 2021.08.25 |
댓글