[JPA] 연관관계 매핑 - 객체지향적으로 테이블 관계를 설정하는 방법

·2024년 2월 21일
0

JPA

목록 보기
2/2
post-thumbnail

JPA 연관관계

데이터베이스의 테이블 간의 관계를 객체지향적으로 표현하는 것으로, JPA의 주요 기능이 객체를 사용하면서도 매핑을 통해 데이터베이스 적용시키는 것과 같이, 객체에 대한 관계를 매핑하여 데이터베이스의 테이블 관계를 설정하는 것이 가능하다.

객체와 테이블의 연관관계 차이

객체는 참조를 통해 관계를 이룰 수 있지만, 테이블을 외래키를 통해 관계를 형성한다.

만약 MemberTeam 에 대한 관계를 정의하는 경우, 객체와 테이블을 형성할 때 다음의 차이를 가지게 된다.

// 테이블의 경우
Member {
	private long id;
	private String userName;
	private long teamId;
}

// 객체의 경우
Member {
	private long id;
	private String userName;
	private Team team;
}

더불어, 테이블의 경우 한 쪽에서만 외래키를 관리하여도, JOIN 을 활용하여 두 테이블에 대한 정보 조회가 가능하다. 즉 외래키를 한 쪽에서 관리하여도 두 테이블 간의 양방향 매핑이 가능하게 된다.

객체의 경우, 이와 다르게 해당 객체를 참조하지 않는 경우, 상대 객체를 인식할 수 없다. 예를 들어, 위의 코드에서 Member 에서는 team 객체를 조회할 수 있으나, Team 객체의 경우는 Member 객체를 조회할 수 없다. (단방향)

연관관계 유형

연관관계는 다중성에 따라 크게 일대일 1:1 , 일대다 1:N , 다대일 N:1 , 다대다 N:M 가 존재하며, 방향성에 따라서는 단방향, 양방향이 존재한다.

방향성에 따른 특징은 다음과 같다.

  • 단방향 : 한 엔티티에서 다른 엔티티를 참조하는 관계이다. 참조하는 엔티티는 상대 엔티티를 파악할 수 있으나, 참조된 엔티티는 상대 엔티티를 파악할 수 없다.
  • 양방향 : 두 엔티티가 서로를 참조하는 관계이다. 한 엔티티에서 다른 엔티티를 참조하는 동시에, 참조된 엔티티에서도 역방향으로 참조가 가능하다.

양방향 설정 시 주의점

순환 참조에 따른 문제

양방향 매핑에서는 순환 참조 문제가 제기 될 수 있다. 대표적으로, 엔티티의 toString() 문제가 있다. 두 엔티티가 서로를 바라보고 있는 상태에서, toString() 메서드를 호출하면, 참조된 객체는 해당 객체의 toString() 을 다시 호출하게 되면서 무한히 toString() 메서드 호출이 발생할 수 있다.

lombok 을 사용한다면 @ToString()exclude 설정으로 연관관계 객체를 결과에서 제외시키는 것으로, toString() 에 따른 순환 참조 문제를 해결할 수 있다.

연관관계 주인 설정

연관관계 주인 설정은 연관관계가 양방향으로 설정되어 있는 경우, 엔티티 간 관계의 주인을 설정하는 것이다. 테이블에서의 외래키는 하나로 정의되지만 객체의 양방향 관계는 두 객체에서 각각 참조가 이루어진다. 즉, 객체에서 JPA 를 통해 테이블 매핑이 발생하는 경우, 두 객체 가운데 외래키를 관리할 곳을 지정할 필요가 있다.

연관관계 주인 설정은 연관관계 설정 시 mappedBy 를 통해 연관관계의 주인을 정할 수 있다.

mappedBy 를 사용하는 클래스가 주인이 아니게 된다.

연관관계의 주인 만이 등록, 수정, 삭제가 가능하며, 연관관계 주인이 아닌 곳은 읽기 전용으로 주인 객체의 정보를 가져온다.

그러나 연관관계 주인이 아닌 객체가 읽기 전용으로 정보를 가져온다고 하여, 변경사항을 적용할 필요가 없는 것은 아니다. 예를들어, 1차 캐시를 통해 정보를 가져오는 경우, 영속성 컨텍스트가 관리하는 객체 내부에서 연관관계 객체를 확인하게 되는데, 이 때 객체에 변경사항이 적용되지 않아, 빈 결과를 가져오는 문제가 발생할 수 있다.

어차피 JPA 의 경우, 연관관계 주인에서 일어난 변화를 기반으로 DB 에 반영하기 때문에, 이러한 문제를 줄이기 위해서라도, JPA 양방향 연관관계에서는 순수한 객체 관계를 유지하도록 양쪽 모두 변경사항을 적용시키는 것이 적합하다.

연관관계 편의 메서드 : 양방향 연관관계 시, 두 객체 관계를 맺어주기 위한 메서드를 의미한다. setter 를 통한 관계설정은 추후 무분별한 객체의 변경을 야기할 수 있고, 객체별로 각각 설정을 진행해야 한다면, 연관관계 편의 메서드를 구현하는 것으로, 두 객체의 관계를 한번에 설정하는 것이 가능하며, setter 메서드를 구현하지 않아도 된다는 장점이 있다.

DB 테이블 관계에 대해 외래키는 일반적으로 N 쪽을 향하는 것이 옳다. 왜냐하면 1 쪽이 외래키를 관리하는 것이 사실상 불가능하기 때문이다. 1차 정규화에 근거하여, 테이블 한 속성 내에 여러 외래키 값이 들어가는 것이 안되며, 이를 지키고자, 모두 동일한 값에 외래키만 다른 row 를 연속적으로 만드는 것도 매우 비효율적인 작업이라 할 수 있다.

N:1 연관관계 설정

주 테이블이 N , 대상 테이블이 1 의 형태가 되는 연관관계 설정이다. 주 테이블이 N 인 곳에서 외래키를 관리하기 때문에, 테이블 매핑에서 이상현상 없이 저장이 가능하다.

주 테이블은 현재 중심이 되는 객체를 의미하며, 대상 테이블은 참조를 통해 바라보고 있는 객체를 의미한다. 예를 들어, Team 객체와 연관관계 설정이 된 Member 객체가 존재하는 경우, Member 객체 입장에서 주 테이블은 Member 이며 대상 테이블은 Team 이 된다. 반대로 Team 객체 입장에서 주 테이블은 Team 이며 대상 테이블은 Member 가 된다. 만약 MemberTeamN:1 의 관계를 가진다면, Team 입장에서 보았을 때 Member 와의 관계는 1:N 이 되는 것이다.

다중성을 통해 연관관계를 표현하는 경우, 주 테이블(앞)쪽에 연관관계의 주인을 표현하는 듯 하다.

1:N 연관관계 설정

주 테이블이 1 , 대상 테이블이 N 의 형태가 되는 연관관계 설정이다. 테이블 상에서는 N 인 곳이 외래키를 관리하고 있지만 연관관계 주인은 1 이 되는 아리송한 형태를 가진다.

객체와 DB 테이블의 구조가 틀어지는 현상 때문에, 대상 테이블에 대한 추가적인 업데이트가 발생할 수 있다. 이러한 이유로 실무에서는 1:N 보다는 N:1 의 연관관계를 더 많이 사용한다.

1:N 단방향

1:N 단방향의 경우 반드시 @JoinColumn 을 활용하여 외래키 설정을 적용해줘야 한다. 그렇지 않은 경우, 1 에 외래키가 없다는 것으로 파악하고, 두 테이블의 관계를 형성하는 매핑 테이블 JoinTable 을 형성하게 된다. (꼭 필요한 테이블이 아니므로, 자원을 낭비하는 행위이며 외래키 기반 조회 시, 한 테이블 더 거쳐서 가므로, 성능상에도 좋지 않다.)

1:N 양방향

스펙상 공식적으로 지원하는 방식은 아니지만, 양방향 매핑 시 대상 테이블 쪽 대해, @JoinColumn(insertable=false, updatable=false) 을 설정시켜 기존의 JPA 양방향에서의 주인이 아닌 엔티티가 읽기전용 형태로 정보를 가져오도록 설계하는 것이 가능하다. (물론 의도적으로 이러한 형태로 구현하는 것 보다는 N:1 의 양방향을 사용하는 것이 더욱 권장되는 방법이긴 하다.)

1:1 연관관계 설정

1:1 관계는 대칭적인 관계이기 때문에, 주 테이블 혹은 대상 테이블 중 어떤 테이블이 외래키를 관리해도 상관없다. 필수적이지는 않지만, 항시 1:1 관계의 매커니즘을 유지하도록 외래키에 UNIQUE 제약 조건을 걸어두자.

1:1 주 테이블이 외래키를 관리하는 단방향, 양방향

기존의 JPA 가 제공하는 기능 그대로, 특별한 설정이나 추가적인 쿼리 요청 없이 단방향 및 양방향이 가능하다.

1:1 대상 테이블이 외래키를 관리하는 단방향

DB 의 외래키를 관리하는 테이블과, 연관관계의 주인이 다른 경우를 의미한다. 이 경우는 JPA 로 동작하지 않는다.

1:1 대상 테이블이 외래키를 관리하는 양방향

양방향 매핑의 경우는 JPA 에서 동작한다. 주 테이블이 연관관계의 주인을 가지며, 외래키를 가진 엔티티가 mappedBy 를 적용시키는 것으로 기존의 1:1 주 테이블이 외래키를 관리하는 양방향 형태와 동일해지기 때문이다.

N:M 연관관계 설정

일반적으로 관계형 데이터베이스의 경우, 테이블 2개를 통해 N:M 의 관계를 표현하는 것은 불가능하다.

이는 데이터의 정규화 원칙과 무결성을 침해하기 때문이다. 직접적인 N:M 관계는 중복된 데이터를 유발하며, 각 행이 다른 여러 행과 연결되어야 하는 상황에서 직접적으로 표현하기 위해서는 복합키나 리스트와 같은 구조가 필요하다.

허나 다대다가 반드시 필요한 경우라면, 연결 테이블을 추가하는 것으로 구현하는 것이 가능하다. @JoinTable

@JoinTable

별도의 테이블을 생성하여 각 테이블의 외래키를 관리하도록 한다. 단 권장하지는 않는 방법인데, 테이블이 하나 더 생성되는 만큼, 관리해야 하는 테이블이 늘어나고, 조회 시에도 3가지 테이블을 연산하여 쿼리를 짜기 때문에 복잡해질 수 있다. 그리고 매핑 테이블은 반드시 외래키의 매핑으로서만 기능을 가져야하기 때문에 추가적인 컬럼을 생성하는 것도 불가능하다.

Hibernate 공식문서의 경우 N:M 의 경우, @JoinTable 을 사용하기 보다는 N:M 사이의 중간 엔티티를 생성하여, 1:N , N:1 의 형태로 반영시키는 것을 권장한다. 이러한 경우 비즈니스에 직접적으로 의존하지 않아, 변경사하에 대한 유연성이 생긴다는 장점이 있다.

참고
[JPA] 연관관계 매핑기초3: 양방향 사용시 주의점
[Java] Lombok 사용법
[Spring] 양방향 매핑시 주의점 : toString
JPA 연관 관계 한방에 정리 (단방향/양방향, 연관 관계의 주인, 일대일, 다대일, 일대다, 다대다)
JPA 연관관계 - 김수빈 | 백엔드 데브코스 2기 | 백둥이Deview 220608

profile
새로운 것에 관심이 많고, 프로젝트 설계 및 최적화를 좋아합니다.

0개의 댓글