JPA 연관관계 & 상속관계 매핑 & 영속성 전이와 고아 객체 정리

Minjae An·2024년 2월 25일
0

Spring data JPA

목록 보기
4/5

연관관계의 필요성

객체지향 설계는 자율적인 객체들의 협력으로 형상화된다. 객체들간의 협력을 표현하기 위해 그들간 관계를 표현해주는 수단이 바로 연관관계이다.

연관관계 매핑시 고려사항 3가지

  • 다중성
  • 단방향, 양방향
  • 연관관계의 주인

단방향 연관관계

객체의 참조와 테이블의 외래키를 매핑

참조할 객체를 멤버로 설정하여 연관관계를 형성하고 이는 DB상에서 외래키로 매핑된다.

User

@Entity
@Getter
public class User {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "member_id")
  private Long id;

	private String username;

	@ManyToOne
	@JoinColumn(name="team_id")
	private Team team;
}

Team

@Entity
@Getter
public class Team {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "team_id")
  private Long id;
  
  private String name;
}

양방향 매핑


앞선 단방향 관계에서 양방향 관계로 전환할 경우 TeamUser컬렉션 필드가 추가된다.

@Entity
@Getter
public class Team {

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

  private String name;

  @OneToMany(mappedBy = "team")
  private List<User> users = new ArrayList<>();
}

연관관계의 주인과 mappedBy

객체의 양방향 관계

  • 객체의 양방향 관계는 서로 다른 단방향 관계 2개로 구성된다.
  • 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 구성해주면 된다.

테이블의 양방향 관계

  • 테이블에선 외래 키 하나로 두 테이블의 연관관계를 관리한다.
  • user.team_id 외래 키 하나로 양방향 연관관계를 가진다. (양쪽으로 조인 가능)
SELECT *
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID

SELECT *
FROM TEAM T
JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID

따라서 두 테이블중 한 곳에서 외래 키를 관리해야 한다.

양방향 매핑 규칙

  • 연관관계를 이루는 객체 중 하나를 연관관계의 주인으로 지정
  • 연관관계의 주인만이 외래 키를 관리(등록, 수정)
  • 주인이 아닌 쪽은 조회(읽기)만 가능
  • 주인은 mappedBy 속성을 사용하지 않음
  • 주인이 아니면 mappedBy 속성으로 주인 지정
  • 보통 외래 키가 있는 곳(일대다 관계에서 ‘다’ 입장의 객체)을 주인으로 정한다.
  • 앞선 예시에선 User 가 연관관계의 주인(User.team )이다.
  • 연관관계의 주인은 비즈니스 로직에서의 중요도와 상관이 없다.

양방향 매핑시 많이 하는 실수

연관관계의 주인에 값을 입력하지 않는 경우이다.

Team team = new Team();
team.setName("teamA");

User user = new User();
user.setName("user1");

// 주인이 아닌 쪽만 연관관계 설정
team.getUsers().add(user);

순수한 객체 관계를 고려하면 아래와 같이 항상 양쪽 다 값을 설정해주어야 한다.

Team team = new Team();
team.setName("teamA");

User user = new User();
user.setName("user1");

team.getUsers().add(user);
user.setTeam(team); // 연관관계의 주인에 값 설정

DB에 진입했다가 나올 경우 JPA 상에서 자동으로 처리되는 경우가 있으나 그래도 설정하는 것이 좋다.

양방향 연관관계 주의

  • 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하는 것이 좋다.
  • 연관관계 편의 메서드를 생성하자. 두 객체중 한 곳에만 설정함에 유의하자
    • 관례적인 자바의 setter 보다 별도의 명명된 메서드를 설정해주는 것이 의미를 분명히 하기에 바람직하다.
public class User {
	// ...
	public void changeTeam(Team team) {
		this.team = team;
		team.getUsers().add(team);
	}
}

// 혹은

public class Team {
	// ...
	public void addUser(User user) {
		user.setTeam(this);
		usere.add(user);
	}
}
  • 양방향 매핑시 무한루프를 조심해야 한다.
    • lombok의 toString 의 경우 사용하지 않는 것을 권장한다.(서로 무한 호출)
    • JSON lib → 컨트롤러에서 기본적으로 엔티티가 아닌 DTO를 반환하는 것이 바람직하다.

양방향 매핑 정리

  • 단방향 매핑을 통해 양방향 매핑을 형성
  • 양방향 매핑은 단방향 매핑에 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것
  • JPQL에서 역방향으로 탐색하는 경우가 많다.
  • 단방향 매핑을 잘 설정하고 양방향 매핑의 경우 필요할 때 추가한다.(테이블에 영향 x)

연관관계 주인을 정하는 기준

  • 비즈니스 로직을 기준으로 주인을 선택하지 않는다.
  • 외래 키의 위치를 기준으로 정해야 한다.

다대일(N:1)

단방향

  • 가장 많이 사용하는 연관관계
  • 다대일의 반대는 일대다

양방향

  • 양쪽을 서로 참조하도록 개발

일대다(1:N)

단방향

  • 일대다 단방향은 일대다에서 ‘일’이 주인
  • 테이블 일대다 관계는 항상 ‘다’쪽에 외래 키가 있다.
  • 객체와 테이블의 차이 때문에 반대편 테이블의 외래 키를 관리하는 특이한 구조
  • @JoinColumn 을 꼭 사용해야 한다. 그렇지 않으면 중간(조인) 테이블 방식을 사용

일대다 단방향 매핑의 단점

  • 엔티티가 관리하는 외래 키가 다른 테이블에 있다
  • 연관관계 관리를 위해 추가로 UPDATE SQL 실행

일대다 단방향 매핑보다 다대일 양방향 매핑을 사용하는 것을 권장한다.

양방향

  • 이런 매핑은 공식적으로 존재하지 않는다.
  • @JoinColumn(insertable = false, updatable = false)
  • 읽기 전용 필드를 사용해서 양방향처럼 사용하는 방법

복잡도, 성능 면에서 더 나은 다대일 양방향을 사용하는 것을 권장한다.

일대일(1:1)

  • 주 테이블이나 대상 테이블 중에 외래 키 선택 가능
  • 외래 키에 DB UNIQUE 제약조건 추가

주 테이블에 외래 키 단방향


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

주 테이블에 외래 키 양방향

  • 다대일 양방향 매핑처럼 외래 키가 있는 곳이 연관관계의 주인
  • 반대면은 mappedBy 적용

대상 테이블에 외래 키 단방향

  • JPA가 지원하지 않는 형태

대상 테이블에 외래 키 양방향


주 테이블에 외래 키 양방향을 설정하는 경우와 매핑 방법이 같다.

정리

주 테이블에 외래 키

  • 주 객체가 대상 객체의 참조를 가진듯이 주 테이블에 외래 키를 두고 대상 테이블 조회
  • 객체지향적인 형태
  • JPA 매핑이 편리하다.
  • 장점은 주 테이블만 조회해도 대상 테이블에 데이터 존재 여부 확인 가능
  • 단점은 값이 없으면 외래 키에 null 적용

대상 테이블에 외래 키

  • 대상 테이블에 외래 키가 존재
  • 전통적인 DB 형태
  • 장점은 주 테이블, 대상 테이블에서 일대일 → 일대다 관계로 변경시 테이블 구조 유지
  • 단점은 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩된다.

다대다(N:M)

RDB는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다. 연결 테이블을 추가하여 일대다, 다대일 관계롤 풀어내야 한다.

객체는 컬렉션을 사용하여 다대다 관계의 표현이 가능하다.

  • @ManyToMany 사용
  • @JoinTable 로 연결 테이블 지정
  • 단방향, 양방향 가능

다대다 매핑의 한계

  • 실무에서 사용하지 않는다.
  • 연결 테이블이 단순히 연결 역할만 수행하지 않는 경우가 많다.
  • 연관 데이터가 추가될 수 있다.

다대다 한계 극복

  • 연결 테이블용 엔티티 추가(연결 테이블을 엔티티로 승격)
  • @ManyToMany@OneToMany , @ManyToOne

상속관계 매핑

  • RDB는 상속 관계를 가지지 않는다.
  • 슈퍼타입-서브타입 관계 모델링 기법이 객체 상속과 유사하다.
  • 상속관계 매핑은 객체의 상속 구조와 DB의 슈퍼타입/서브타입을 매핑

슈퍼타입, 서브타입 논리 모델을 실제 물리 모델로 구현하는 방법은 다음과 같다.

  • 각각 테이블로 변환 → 조인 전략
  • 통합 테이블로 변환 → 단일 테이블 전략
  • 서브타입 테이블로 변환 → 구현 클래스마다 테이블 생성 전략

주요 애노테이션

  • @Inheritance(strategy = InheritanceType.XXX)
    • JOINED : 조인 전략
    • SINGLE_TABLE : 단일 테이블 전략, default
    • TABLE_PER_CLASS : 구현 클래스마다 테이블 전략
  • @DiscriminatorColumn(name=”DTYPE”)
  • @DiscriminatorValue(”XXX”) - default , Entity 이름

조인 전략


장점

  • 테이블 정규화
  • 외래 키 참조 무결성 제약조건 활용 가능
  • 저장공간 효율화

단점

  • 조회시 조인을 많이 사용, 성능 저하
  • 조회 쿼리가 복잡
  • 데이터 저장시 INSERT 2번 발생

조인 전략 활용시 @DiscriminatorColumn 을 사용하는 것을 권장한다.

단일 테이블 전략


장점

  • 조인이 없으므로 일반적으로 조회 성능이 좋다.
  • 조회 쿼리가 단순하다.

단점

  • 자식 엔티티가 매핑한 컬럼은 모두 null 허용
  • 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다. 상황에 따라 되려 조회 성능 저하

구현 클래스마다 테이블 생성 전략


권장되지 않는 전략이다.

@MappedSuperClass

  • 공통 속성이 필요할 때 사용한다.
  • 부모 클래스를 상속 받는 자식 클래스에 공통 데이터만 제공한다.
  • 조회, 검색이 불가하다. (em.find(BaseEntity) 불가)
  • 직접 생성해서 사용할 일이 없으므로 추상 클래스를 권장한다.
  • 테이블과 관계 없고, 단순히 엔티티가 공통으로 사용하는 데이터를 모으는 역할
  • 주로 등록일, 수정일 같은 전체 엔티티에 공통으로 적용되는 데이터를 모을 때 사용
  • 엔티티나 @MappedSuperClass 로 지정한 클래스만 상속 가능

예시

BaseEntity

package hellojpa;

import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@MappedSuperclass
public abstract class BaseEntity {
    private String createdBy;
    private LocalDateTime created;
    private String lastModifiedBy;
    private LocalDateTime lastModified;
		//getter & setter
}

User

@Entity
public class User extends BaseEntity { ... }

테이블 생성

create table user (
	id bigint not null,
  created datetime,
  createdBy varchar(255),
  lastModified datetime,
  lastModifiedBy varchar(255),
  primary key (id)
);

영속성 전이 - CASCADE

  • 특정 엔티티를 영속 상태로 만들 때 연관 엔티티도 영속 상태로 만들고자 할 때 사용
  • 예시: 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장

영속성 전이 - 저장

@OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST)

주의점

  • 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없다.
  • 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐이다.
  • 단일 엔티티에 종속되며 두 엔티티의 라이프사이클이 같을 시 사용함에 유의하자.

CASCADE의 종류

  • ALL : 모두 적용
  • PERSIST : 영속
  • REMOVE : 삭제
  • MERGE : 병합
  • REFRESH : REFRESH
  • DETACH : DETACH

고아 객체

  • 고아 객체 제거 : 부모 엔티티와 연관관계가 끊어진 자식 엔티티 자동 삭제
  • orphanRemoval = true
  • 자식 엔티티를 컬렉션에서 제거
  • DELETE FROM CHILD WHERE ID = ?

주의점

  • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
  • 참조하는 곳이 하나일 때 사용해야함
  • 특정 엔티티가 개인 소유할 때 사용
  • @OneToOne , @OneToMany 만 가능

참고 - 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화 하면

부모를 제거할 때 자식도 함께 제거된다. 이것은 CascadeType.REMOVE 처럼 동작한다.

영속성 전이 + 고아 객체, 생명주기

  • CascadeType.ALL + orphanRemoval=true
  • 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거
  • 두 옵션을 모두 활성화하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있음
  • 도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용

참고 - Aggregate root

aggregate라는 단어는 ‘모으다’, ‘집계하다’라는 의미를 가지며 도메인 주도 설계(domain driven desgin, DDD)에서는 관련된 객체들을 한데 묶거나 모아서 하나의 논리적인 단위로 취급한다는 의미로 사용된다. aggreegate는 관련된 객체들의 그룹을 형성하며, 이 그룹 안에서는 aggregate root를 중심으로 일관성을 유지하고 도메인 규칙을 적용한다. 정리하면, 한 단위로 묶여 관리되는 도메인 객체들의 집합을 aggregate라 지칭한다고 볼 수 있다.

참고

profile
도전을 성과로

0개의 댓글