기록

[자바 ORM 표준 JPA 프로그래밍] JPA 고급 매핑: 상속 관계 매핑(7장-1) 본문

교육/책

[자바 ORM 표준 JPA 프로그래밍] JPA 고급 매핑: 상속 관계 매핑(7장-1)

youngyin 2024. 12. 1. 09:00

시작하면서

이번 포스팅에서는 "자바 ORM 표준 JPA 기술"의 7장 내용을 바탕으로 JPA의 상속 관계 매핑 설계에 대해 알아보겠습니다.

상속 관계 매핑은 객체 지향 언어의 상속 개념을 데이터베이스에서도 효과적으로 구현할 수 있는 유용한 기법입니다. 예를 들어, Item이라는 부모 클래스를 정의하고 이를 상속받는 Album, Book, Movie와 같은 자식 클래스를 생각해 볼 수 있습니다. Item은 이름과 가격 같은 공통된 속성을 가지고, 각 자식 클래스는 자신만의 고유한 속성(예: Album의 아티스트, Book의 저자, Movie의 감독)을 가질 수 있습니다.

이 글에서는 상속 관계를 데이터베이스에서 어떻게 매핑할 수 있는지 세 가지 전략을 통해 살펴보겠습니다: 조인 전략(Joined Strategy), 단일 테이블 전략(Single Table Strategy), 구현 클래스마다 테이블 전략(Table per Class Strategy).

1. 상속 관계 매핑 전략

객체 지향 언어의 상속 개념을 관계형 데이터베이스에서 활용하기 위해 JPA는 "슈퍼타입(부모 클래스)-서브타입(자식 클래스)"이라는 모델링 기법을 사용합니다. 예를 들어, Item이라는 슈퍼타입을 정의하고 이를 상속받는 Album, Book, Movie 같은 서브타입을 생각해볼 수 있습니다. Item은 공통된 속성(예: 이름, 가격)을 가지며, 각 서브타입은 자신만의 특수한 속성(예: Album의 아티스트, Book의 저자, Movie의 감독)을 가집니다.

슈퍼타입 (Item) 서브타입 (Album) 서브타입 (Book) 서브타입 (Movie)
이름 이름 이름 이름
가격 가격 가격 가격
  아티스트 저자 감독

 

이를 테이블로 구현할 때, 다음 세 가지 방법을 사용할 수 있습니다.

1.1 조인 전략 (Joined Strategy)

조인 전략은 부모 클래스와 자식 클래스 각각을 테이블로 만들고, 자식 테이블이 부모 테이블의 기본 키를 기본키 + 외래키로 사용하는 방식입니다. 이를 통해 부모와 자식의 데이터를 연결합니다.

테이블명 컬럼명 설명
Item id (PK) 기본 키
  name 공통 속성: 이름
  price 공통 속성: 가격
Book id (PK, FK) 부모 테이블(Item)과 조인된 기본 키
  author 서브타입 속성: 저자
  isbn 서브타입 속성: ISBN

 

위의 표에서 볼 수 있듯이, Item 테이블은 슈퍼타입의 공통 속성을 담고 있으며, Book 테이블은 Item 테이블의 기본 키를 외래키로 참조하여 연결됩니다. 아래는 이러한 조인 조건을 이용해 Book 엔티티를 조회하는 예시 쿼리입니다:

SELECT b.id, b.author, b.isbn, i.name, i.price
FROM Book b
JOIN Item i ON b.id = i.id;

 

이 쿼리는 Book 테이블과 Item 테이블을 조인하여 Book에 대한 정보를 가져오면서, 부모인 Item의 공통 속성인 nameprice도 함께 조회합니다. 이러한 구조로 인해 부모와 자식 간의 데이터를 조회할 때 조인이 발생합니다.

 

조인 전략의 장점은 테이블을 정규화하여 데이터 중복을 방지할 수 있다는 것입니다. 하지만 조회 시 여러 테이블을 조인해야 하므로 성능이 저하될 수 있습니다.

  • 장점: 데이터 중복을 최소화할 수 있음.
  • 단점: 조회 시 성능이 떨어질 수 있음 (조인이 빈번하게 발생).

1.2 단일 테이블 전략 (Single Table Strategy)

단일 테이블 전략은 이름 그대로 부모와 자식 클래스의 모든 데이터를 하나의 테이블에 저장하는 방식입니다. "구분 컬럼"을 추가하여 해당 레코드가 어떤 자식 클래스에 속하는지 구분합니다.

아래는 단일 테이블 전략을 적용했을 때의 테이블 시각화 예시입니다:

테이블명 컬럼명 설명
Item id (PK) 기본 키
  name 공통 속성: 이름
  price 공통 속성: 가격
  DTYPE 구분 컬럼: 자식 클래스 구분 (예: 'Book', 'Album', 'Movie')
  author Book 속성: 저자 (Book 타입일 때 사용)
  isbn Book 속성: ISBN (Book 타입일 때 사용)
  artist Album 속성: 아티스트 (Album 타입일 때 사용)
  director Movie 속성: 감독 (Movie 타입일 때 사용)

 

위의 표는 단일 테이블 전략을 사용하여 모든 속성을 하나의 테이블에 저장하는 구조를 나타냅니다. 각 자식 클래스의 속성은 하나의 테이블에 포함되며, 구분 컬럼(DTYPE)을 통해 어떤 서브타입인지 판별할 수 있습니다.

조인이 필요 없기 때문에 성능 면에서는 가장 빠릅니다.

SELECT i.id, i.name, i.price, i.DTYPE, i.author, i.isbn
FROM Item i
WHERE i.DTYPE = 'Book';

이 쿼리는 단일 테이블인 Item 테이블에서 Book에 대한 데이터를 조회하는 방식입니다. DTYPE 컬럼을 통해 Book 타입의 데이터만 필터링하여 가져옵니다.

  • 장점: 조회 성능이 우수함 (조인이 필요 없음).
  • 단점: 테이블이 커지고, 특정 자식 클래스와 관련 없는 컬럼에 NULL 값이 많이 들어갈 수 있음.

1.3 구현 클래스마다 테이블 전략 (Table per Class Strategy)

이 전략은 각 자식 엔티티마다 별도의 테이블을 만드는 방식입니다. 부모 클래스에 대한 테이블은 없으며, 자식 클래스들이 각각 독립적인 테이블을 가집니다. 일반적으로 권장되지 않는 전략입니다.

아래는 이 전략을 적용했을 때의 테이블 시각화 예시입니다:

테이블명 컬럼명 설명
Book id (PK) 기본 키
  name 공통 속성: 이름
  price 공통 속성: 가격
  author 서브타입 속성: 저자
Album id (PK) 기본 키
  name 공통 속성: 이름
  price 공통 속성: 가격
  artist 서브타입 속성: 아티스트
Movie id (PK) 기본 키
  name 공통 속성: 이름
  price 공통 속성: 가격
  director 서브타입 속성: 감독

 

이 전략을 채택했을 때는 부모 클래스에 대한 테이블이 없고, 각 자식 클래스가 독립적인 테이블을 가집니다. 따라서 통합 조회 시에는 UNION 쿼리가 발생할 수 있으며, 이는 성능에 영향을 미칠 수 있습니다. 아래는 이러한 통합 조회를 수행하기 위한 UNION 쿼리 예시입니다:

SELECT id, name, price, author
FROM Book
UNION
SELECT id, name, price, artist
FROM Album
UNION
SELECT id, name, price, director
FROM Movie;

 

이 쿼리는 각 자식 테이블(Book, Album, Movie)의 데이터를 모두 통합하여 조회하는 방식으로, UNION을 사용하여 모든 자식 엔티티를 하나의 결과로 결합합니다.

  • 장점: 각 자식 테이블이 독립적이므로 구조가 명확함.
  • 단점: 자식 클래스들을 통합 조회할 때 UNION이 발생해 성능 저하 가능성이 큼.

질문: 실무에서 서브타입을 사용하지 않고 각 테이블에 값을 분리하여 관리하는 이유는 무엇일까?

저는 지금까지 데이터베이스 설계 시 서브타입의 활용을 실무에서 보지 못했습니다. 실무에서는 같은 컬럼이라도 여러 테이블에 분산하여 관리하는 경우가 많았습니다. 비즈니스의 변화에 유연하게 대응하기 위해 대체 키를 사용하는 것이 일반적으로 권장되며, 이러한 이유로 비즈니스 변화에 따른 유연성을 확보하기 위해 각 테이블에 값을 독립적으로 분리하여 관리하는 것도 좋은 전략이라고 생각합니다. 다른 개발자들은 실무에서 서브타입을 어떻게 활용하고 있는지 궁금합니다.

2. @MappedSuperclass - 공통 속성 상속받기

일반적으로 여러 엔티티에 공통으로 사용되는 속성들이 있을 때 이를 상속받아 사용하는 경우가 많습니다. 예를 들어, createdBy, createdAt, updatedBy, updatedAt과 같은 공통 필드들은 모든 엔티티에서 사용될 수 있습니다. 이를 위해 JPA에서는 @MappedSuperclass를 제공합니다.

@MappedSuperclass는 매핑 정보를 제공하는 역할을 하고, 이를 상속받는 자식 클래스는 해당 속성을 테이블에 매핑할 수 있습니다.

@MappedSuperclass
public abstract class BaseEntity {
    private String createdBy;
    private LocalDateTime createdAt;
    // getter, setter 등
}

 

이렇게 정의된 BaseEntity 클래스는 테이블과 매핑되지 않으며, 이를 상속받는 자식 클래스들이 실제 테이블과 매핑됩니다. 이를 통해 코드의 재사용성과 유지보수성을 높일 수 있습니다.

 

질문: 공통 속성을 테이블과 연동할지 여부는 어떤 기준으로 결정해야 할까?

사이드 프로젝트에서 createdBy, createdAt, updatedBy, updatedAt, deleteYn 같은 공통 컬럼들을 여러 테이블에서 상속받도록 설계한 경험이 있습니다. 이러한 공통 속성을 테이블과 연동할지 여부를 결정할 때는 명확한 기준이 필요합니다. 일반적으로 이런 공통 속성들은 모든 엔티티에 공통적으로 적용되고, 데이터 추적과 같은 관리 목적이기 때문에 @MappedSuperclass로 정의하는 것이 좋습니다. 하지만, 특정 속성이 비즈니스 로직에 깊이 연관되어 독립적인 속성으로 관리되어야 한다면, 해당 속성을 별도의 테이블로 매핑하는 것이 더 유리합니다. 예를 들어, 주문 처리와 관련된 orderStatus, deliveryDate와 같은 속성들은 비즈니스 로직의 핵심적인 부분으로, 별도의 테이블에 매핑하여 독립적으로 관리하는 것이 더 나은 설계일 수 있습니다.

마무리

JPA의 상속 관계 매핑은 객체 지향 언어의 상속 개념을 데이터베이스에서도 효과적으로 구현할 수 있는 유용한 기법입니다. 각 전략에는 장단점이 있으므로, 비즈니스 요구사항에 따라 적합한 매핑 전략을 선택하는 것이 중요합니다.

  • 조인 전략: 데이터 중복을 최소화할 수 있지만 성능상 불리할 수 있습니다.
  • 단일 테이블 전략: 조회 성능이 우수하지만, 테이블이 커지고 많은 NULL 값이 들어갈 수 있습니다.
  • 구현 클래스마다 테이블 전략: 구조가 명확하지만 통합 조회 시 성능 저하의 문제가 있습니다.

비즈니스 요구사항과 데이터 구조의 복잡성에 따라 상속 매핑 전략을 유연하게 선택해야 합니다.

 
Comments