maeng0830
뇌 채우기 운동
maeng0830
전체 방문자
오늘
어제
  • maeng0830-note (85)
    • 자바 (3)
    • 스프링 (39)
      • Core (21)
      • DB (16)
      • Security (2)
      • Test (0)
    • 자료구조 & 알고리즘 (19)
      • 자료구조 (12)
      • 알고리즘 (7)
    • 다른 개발 도구들 (4)
      • Git&Github (1)
      • Redis (3)
    • 프로젝트 (9)
      • Album (9)
    • CS (10)
      • 운영체제 (5)
      • 데이터베이스 (5)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 트랜잭션
  • JPA
  • spring security
  • 자료구조
  • JPQL

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
maeng0830

뇌 채우기 운동

스프링/DB

데이터 접근 기술_JPA_연관 관계 매핑

2023. 3. 10. 00:57

연관 관계 매핑

데이터베이스에서는 두 테이블 간에 외래키(Foreign Key)를 통해 서로 연관 관계를 맺고 있다.

 

연관 관계 매핑이랑 이러한 테이블 간의 연관 관계를 엔티티들에 매핑하는 것을 의미한다.

 

연관 관계 매핑을 이해하고 정확히 사용하기 위해서는 방향, 연관 관계의 주인, 다중성을 이해해야한다.

 

방향

연관 관계 매핑에서의 방향은 객체 참조의 방향을 의미한다.

 

연관 관계를 가진 두 엔티티 중 하나의 엔티티에서만 다른 엔티티를 참조할 수 있으면 단방향,

두 엔티티 모두 다른 엔티티를 참조할 수 있으면 양방향이라고 한다.

 

여기서 데이터베이스 테이블의 연관 관계와는 약간 차이점이 있다.

테이블의 연관 관계는 단방향, 양방향을 구분할 필요가 없다는 것이다.

두 테이블 간에 외래키 하나만 존재하면, 두 테이블 모두 join을 통해 서로를 조회할 수 있기 때문이다.

 

이렇게 테이블 간의 연관 관계에서는 외래키 하나만 지정해주면 되는데, 엔티티에서는 단방향, 양방향이 왜 구분되어 존재할까?

그리고 엔티티 연관 관계와 테이블 연관 관계를 매핑할 때 단방향, 양방향에 따라 어떤 차이점이 있을까?

 

사실 엔티티 연관 관계와 테이블 연관 관계를 매핑할 때는 양방향 매핑이 전혀 영향을 주지 않는다.

단방향 매핑을 할 때 엔티티의 연관 관계와 테이블의 연관 관계의 매핑은 이미 완료된다.

양방향 매핑이라는 것은 두 객체가 서로 참조하기 위해서는 각각 상대 객체를 필드로 가져야한다는 한계 때문에 생긴 개념이다.   

 

단방향, 양방향 매핑 중 무엇을 사용해야할까?

양방향 매핑을 할 경우, 두 엔티티 모두 다른 엔티티를 참조할 수 있기 때문에 객체를 다루기 쉽다.

그러면 무조건 양방향 매핑을 하는 것이 좋을까?

 

그렇지는 않다. 오히려 무분별하게 양방향 매핑을 할 경우, 엔티티 내부가 상당히 복잡해질 수 있다.

그리고 비즈니스 로직 상으로는 주로 접근하는 테이블(주 테이블)이 정해져 있고, 주 테이블을 통해 다른 테이블에 접근하면 되기 때문에, 양방향 매핑을 불필요한 경우가 많다.

 

따라서 기본적으로는 단방향 매핑을 하는 것이 좋다. 

그리고 비즈니스 로직 상에서 양방향 매핑을 통해 두 엔티티가 서로를 참조하는 것이 큰 메리트가 있을 때 양방향 매핑을 사용하면 된다.

 

연관 관계의 주인

연관 관계의 주인은 양방향 매핑에서 등장하는 개념이다.

연관 관계의 주인은 연관 관계를 가진 두 엔티티 중 외래키(참조 객체)에 대한 제어의 권한을 갖는 엔티티를 말한다.

 

영속성 컨텍스트에서 연관 관계의 주인은 외래키에 대한 조회, 저장, 수정, 삭제를 할 수 있지만(DB에 전달),

다른 엔티티는 외래키에 대해 조회만 가능하다(DB에 전달 X).  

 

연관 관계의 주인이라는 개념도 테이블 연관 관계와 엔티티 연관 관계의 차이 때문에 발생한 개념이다.

테이블 연관 관계에서는 한 테이블에만 외래키가 존재한다. 

그러나 엔티티에 양방향 매핑을 할 경우, 두 엔티티에 모두 외래키(참조 객체)가 존재하게 되는 것이다.

때문에 엔티티에서도 외래키에 대한 제어 권한을 한 엔티티에만 주기 위해 연관 관계의 주인이라는 개념이 생긴 것이다.

 

연관 관계를 가진 두 테이블 중 외래키를 가진 테이블에 매핑된 엔티티를 연관 관계의 주인으로 설정해야한다.

그것이 논리적으로 타당하며, 추후 작업 시 혼란을 예방할 수 있기 때문이다.

 

영속성 컨텍스트에서 양방향 매핑된 두 엔티티의 외래키(참조 객체)에 대해 작업 할 때,

연관 관계의 주인 뿐만 아니라, 상대 엔티티에도 외래키의 변경 사항을 적용해주는 것이 좋다.

외래키에 대한 변경 사항이 1차 캐시에 곧 바로 적용되지 않아 작업에 문제가 발생할 수 있기 때문이다.

 

다중성

다중성을 반영하기 위해 JPA는 @ManyToOne(다대일), @OneToMany(일대다), @OneToOne(일대일), @ManyToMany(다대다) 어노테이션을 제공한다.

 

그리고 엔티티에서는 양방향 연관 관계일 경우, 다중성이 대칭성을 가지며 어노테이션 또한 그러하다.

  • @ManyToOne(다대일) <-> @OneToMany(일대다)
  • @OneToOne(일대일) <-> @OneToOne(일대일)
  • @ManyToMany(다대다) <-> @ManyToMany(다대다)

 

예시 코드를 통해 각각의 다중성이 어노테이션으로 어떻게 매핑되는지 알아보자.

각 예시 코드는 다중성 중 앞 쪽이 연관 관계의 주인임을 가정하여 설명한다.

  • 다대일(N:1): N이 연관 관계의 주인임

 

다대일(N:1)

데이터베이스에 Member 테이블, Team 테이블이 존재한다.

Member(N)는 하나의 Team을 가질 수 있다. Team(1)은 다수의 Member를 가질 수 있다.

Member 테이블은 Team 테이블의 ID를 외래키로 갖고 있다.

 

이 상황에서 다대일 단방향 매핑은 아래 코드처럼 가능하다.

연관 관계의 주인인 Member가 N이므로 Team타입의 참조 객체에 @ManyToOne을 적용해준다.

@JoinColumn(name = "team_id")을 통해 Member 테이블의 team_id 컬럼을 외래키로 사용하여 조인하겠다고 지정한다.

 

이렇게 하면 다대일 단방향 매핑이 완료된다.

@Entity
public class Member {

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	@Column(name = "member_id")
	private Long id;

	@Column(name = "username")
	private String name;


	@ManyToOne // N이 연관관계의 주인인 경우
	@JoinColumn(name = "team_id") // DB에서 조인에 활용되는 컬럼
	private Team team;

	...
}

 

양방향 매핑을 할 때는 Team에도 작업이 필요하다.

 

Team이 1이기 때문에 N인 Member를 참조해야한다. 그러므로 List<Member> 타입으로 참조 객체를 선언한다.

양방향 매핑에서 다중성은 대칭성을 갖기 때문에 @OneToMany(mappedBy = "team")를 적용해준다.

 

mappedBy = "team"은 두 엔티티 간의 연관 관계가 team(@ManyToOne의 변수명)에 의해 매핑된다는 것을 의미한다.

즉 team을 가진 Member가 외래키를 제어한다는 의미이다. 

@Entity
public class Team {

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private Long id;

	private String name;

	// 양방향 연관관계
	// 1(Team) : N(Member)
	// mappedBy = N의 @ManyToOne의 변수명(주인)
	@OneToMany(mappedBy = "team")
	private List<Member> members = new ArrayList<>(); // 관례대로 ArrayList로 초기화 해둔다.

	...
}

 

일대다(1:N)

동일한 테이블 연관 관계 상황에서 이번에는 1쪽인 Team이 연관 관계의 주인인 일대다 연관 관계 매핑을 해보자.

참고로 일대다 단방향 매핑은 JPA에서 정식으로 지원하지만, 양방향 매핑은 정식으로 지원하지 않고 약식으로만 가능하기 때문에 단방향 매핑만 다루겠다.

 

아래 코드는 일대다 단방향 매핑 예시 코드이다.

간단하다. 참조해야할 객체가 N이므로 @OneToMany를 연관 관계의 주인인 Team의 List<Member>타입 참조 객체에 적용해준다.

그리고 @JoinColum(name = "team_id)를 통해 Member 테이블의 team_id 컬럼을 외래키로 사용하여 조인하겠다고 지정한다.

@Entity
public class Team {

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private Long id;

	private String name;

	@OneToMany // 1이 연관 관계의 주인인 경우
	@JoinColumn(name = "team_id") // DB에서 조인에 활용되는 컬럼
	private List<Member> members = new ArrayList<>(); // 관례대로 ArrayList로 초기화 해둔다.

	...
}

 

일대일(1:1)

엔티티의 일대일 연관 관계는 외래키에 유니크 제약 조건이 걸린 테이블 연관 관계와 매핑된다. 

이번에는 테이블 예시를 변경하겠다.

 

데이터베이스에 Member 테이블, Locker 테이블이 존재한다.

Member(1)는 하나의 Locker를 가질 수 있다. Locker(1)는 하나의 Member를 가질 수 있다.

 

Member 테이블과 Locker 테이블은 일대일 관계이기 때문에 어느 쪽에도 외래키를 둘 수 있다.

이런 경우, 주로 사용하는 주 테이블에 외래키를 두는 것이 개발자 입장에서는 편하다.

현재 예시에서는 Member 테이블을 주로 접근하는 주 테이블이라고 가정하고, 외래키를 Member 테이블에 지정하겠다.

 

아래는 일대일 단방향 매핑(주테이블 외래키)의 예시 코드이다.

다대일 단방향 매핑과 매우 유사하다.

연관 관계의 주인인 Member의 Locker타입 참조 객체에 @ManyToOne 대신 @OneToOne을 사용하면 된다.

@Entity
public class Member {

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private Long id;

	private String username;

	@OneToOne
	@JoinColumn(name = "locker_id") // fk로 사용할 컬럼명
	private Locker locker;

	...
}

그리고 아래는 일대일 양방향 매핑(주테이블 외래키)의 예시 코드이다.

이 또한 다대일 양방향 매핑과 매우 유사하다.

연관 관계의 주인이 아닌 Locker의 Member타입 참조 객체에 @OneToOne(mappedBy ="locker")를 적용해준다.  

@Entity
public class Locker {

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private Long id;

	private String name;

	@OneToOne(mappedBy = "locker")
	private Member member;

	...
}

 

다대다(N:M)

마지막으로 @ManyToMany를 사용하는 다대다 연관 관계 매핑이다.

 

사실 관계형 데이터베이스에서는 두 테이블을 통해 다대다 관계를 표현할 수가 없다.

따라서 두 엔티티에 대해 다대다 연관 관계 매핑하는 경우, 데이터베이스에서는 연결 테이블이 추가로 필요하다.

 

그리고 다대다 연관 관계 매핑에는 많은 단점이 있다.

  • 다대다 매핑을 사용할 경우, 연결 테이블이 자동으로 생성되는데 이 연결 테이블로 인해 개발자 자신도 모르는 복잡한 조인 쿼리가 발생할 수 있다.
  • 그리고 자동 생성된 연결 테이블에는 다대다 매핑된 두 엔티티에 대응하는 테이블의 외래키만 저장된다. 사실 실무를 해보면 연결 테이블에 외래키 외에 다양한 정보를 사용하는 경우가 많다. 

이러한 단점들 때문에 실무에서는 다대다 연관 관계 매핑을 사용하지 않는다. 사용하지 말자!

대신에 연결 테이블에 대한 엔티티를 만들고, 다대다를 일대다, 다대일로 풀어서 사용하는 것이 현명한 방법이다.

 

결론

연관 관계 매핑을 사용할 때 권장되는 방법은 다음과 같다.

  1. 외래키를 가진 테이블에 대응하는 엔티티가 연관 관계의 주인이 되도록 하자. 즉 다대일 매핑을 사용하자.
  2. 단방향 매핑을 기본으로 사용하자. 양방향 매핑은 추후 비즈니스 로직상 이점이 클 때 적용하자.
  3. 다대다 매핑은 사용하지 말자. 

출처: 자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의 (inflearn.com)

    '스프링/DB' 카테고리의 다른 글
    • 데이터 접근 기술_JPA_프록시와 지연 로딩
    • 데이터 접근 기술_JPA_상속 관계 매핑
    • 데이터 접근 기술_JPA_영속성 컨텍스트
    • 데이터 접근 기술_JPA_기본 개념
    maeng0830
    maeng0830

    티스토리툴바