본문 바로가기
Spring/JPA

#6 다양한 연관관계 매핑

by 히포파타마스 2021. 8. 28.

다양한 연관관계 매핑

 

엔티티의 연관관계를 매핑할 때는 다음 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

댓글