클러버 프로젝트에서 마주친 테이블 설계 중 상속 매핑에 관련된 고민이 있어 이를 정리하고자 한다.
[문제상황]
현재 프로젝트에서는 동아리 테이블이 있는데 종류는 크게 중앙동아리(centerclub)과 소모임(smallclub) 2가지가 있다.
DB에는 상속 관계가 없지만 편의를 위해 시각화를 하자면 다음과 같다.
[공통점]
동아리 이름, 소개, 해시태그 등이 있다.
[차이점]
중앙동아리는 소속 분과(학술, 봉사 ..)가 있으며 소모임은 중앙동아리와 다르게 분과는 존재하지 않지만 소속 단과대나 학과는 존재한다.
[중앙 동아리] - 분과 O, 단과대 X, 학과 X
[소모임] - 분과 X, 단과대 O, 학과 O
DB 테이블 설계
위에서 언급한 이유들로 인해 상속 매핑을 생각해보았다.
JPA 상속 매핑을 고려하여 테이블 설계를 함께 진행해보았다.
[테이블 분리]
[구현 방법]
상속 매핑 중 join 전략을 사용한다.
[특징]
-null값이 없어진다. (장점)
-join이 일어난다. (단점)
현재 API들에는 하위 테이블에 있는 요소들 (단과대, 학과, 분과)에 대한 조회를 필요로 하는 경우가 많아 테이블 join이 일어나는 경우가 상당히 많아진다. 해당 테이블에는 그려지지 않았지만 동아리 테이블과 연관관계를 맺고 있는 테이블이 추가적으로 많고 추가적인 join이 더 필요하기에 해당 방법은 선택하지 않도록 하였다.
단일 테이블
[방법]
1. 상속 매핑을 사용하지 않는다. (하나의 엔티티로 설계한다.
2. 상속 매핑 중 single table전략을 사용한다.
[특징]
- null 값이 많아진다.
가장 우려되는 부분이긴했다. 만약 한테이블에 합친다면 서로 없는 속성에 대해서는 null값이 들어갈 수 밖에 없었다.
중앙동아리 같은 경우는 단과대와 학과의 값이 null이며, 소모임 같은 경우는 분과가 null이 될 수 밖에 없다.
이제 단일 테이블을 구현하는 방법에 따른 장단점을 살펴본다.
1. 상속 매핑
@Entity
@Getter
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public abstract class Club {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
private String name;
private String introduction;
private String hashtag;
protected Club(String name, String introduction, String hashtag) {
this.name = name;
this.introduction = introduction;
this.hashtag = hashtag;
}
}
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DiscriminatorValue("center")
public class CenterClub extends Club{
//분과 (중앙동아리)
private String division;
@Builder
private CenterClub(String name, String introduction, String hashtag, String division) {
super(name, introduction, hashtag);
this.division = division;
}
}
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DiscriminatorValue("small")
public class SmallClub extends Club{
private String college; //단과대 (소모임)
private String department; //학과 (소모임)
@Builder
private SmallClub(String name, String introduction, String hashtag, String college, String department) {
super(name, introduction, hashtag);
this.college = college;
this.department = department;
}
}
[장점]
- 만약 개별 자식 클래스 엔티티 (smallclub, centerclub)에 대한 조회만 이뤄질 경우에 해당 repository로 조회가 가능하다.
- @DiscriminatorValue로 이미 자식 클래스 엔티티 간 구별되는 값이 지정되었기 때문에 이를 엔티티의 개별 값으로 만들필요가 없다.
[단점]
각 자식 클래스가 아닌 부모 클래스 엔티티(Club) 를 상속하는 모든 자식 클래스들을 공통으로 조회할 때의 문제점이 생각보다 많다.
[엔티티 조회]
자식 클래스들을 엔티티로 조회하려면 크게 2가지 방법이 있는데,
1. ClubRepository로 조회 후 다운 캐스팅
=> 부모 클래스로 조회 시 자식 클래스에 있는 값이 당연히 조회가 안된다. 어떤 인스턴스인지를 분기하여 이에 따라 처리해야한다.
@BeforeEach
void setUp() {
SmallClub smallClub1 = SmallClub.builder()
.department("SW")
.college("IT")
.hashtag("개발")
.name("소모임1")
.introduction("소모임1 소개")
.build();
CenterClub centerClub1 = CenterClub.builder()
.division("학술")
.hashtag("시사")
.name("중앙동아리1")
.introduction("중앙동아리1 소개")
.build();
clubRepository.save(smallClub1);
clubRepository.save(centerClub1);
em.clear();
}
@Test
void 부모클래스로_모두_조회() {
List<Club> clubs = clubRepository.findAll();
CenterClub centerClub = null;
SmallClub smallClub = null;
for (Club club : clubs) {
if (club instanceof CenterClub) {
centerClub = (CenterClub) club;
} else if (club instanceof SmallClub) {
smallClub = (SmallClub) club;
}
}
assertEquals(centerClub.getDivision(), "학술");
assertEquals(smallClub.getDepartment(), "SW");
}
2. 모든 자식 클래스 엔티티들의 repository(SmallClubRepository, CenterClubRepository)를 조회
=> 하나의 DB 테이블을 자식 클래스의 개수만큼의 조회가 이뤄진다는 얘기다.
이는 모든 자식 클래스들을 한꺼번에 조회가 필요한 API의 확장에 있어서 큰 제약을 가진다고 생각했다.
부모 클래스 엔티티를 상속하는 자식 클래스가 많아질 수록 비즈니스 로직이 많아지거나 불필요한 테이블 조회가 늘 수 밖에 없었다.
@Test
void 자식클래스_레포지토리로_각각_조회() {
SmallClub smallClub = smallClubRepository.findAll().get(0);
CenterClub centerClub = centerClubRepository.findAll().get(0);
assertEquals(centerClub.getDivision(), "학술");
assertEquals(smallClub.getDepartment(), "SW");
}
[DTO 조회]
DTO 조회 방법이 마땅치가 않다는 게 가장 큰 문제라고 생각했다.
앞에서의 엔티티 조회 후 DTO 변환을 하지 않고 바로 DTO 조회를 하려면 native query로 작성하는 수 밖에 없었다.
부모 엔티티와 자식 엔티티 간 join을 통해서 할 수 있는 방법이 있긴 한데 join을 최대한 피하려고 한건데 또 추가적인 join이 생길 수 밖에 없었다.
결론
상속 매핑을 사용하지 않고 하나의 테이블로 만들기로 했다. 구별되는 컬럼 값을 추가하여(clubtype) 구별하기로 했다.
객체 지향에서 생각하는 상속과 다형성의 유연함이 DB와 함께 생각해야하는 JPA와 같은 ORM에서는 오히려 유연하지 않고 확장성에 제약이 걸린다는 생각이 들었다.
자식 클래스 엔티티들과 관련된 서비스 로직이 명확히 구별된다면 이를 적용해도 괜찮겠지만, 만약 모든 자식 클래스들을 함께 조회하여 구현하는 서비스 로직이 있다면 하나의 엔티티로 합치는 편이 낫다고 생각하였다.
참고
상속관계 Entity간 조인 - 인프런
안녕하세요. 고객 테이블을 상속관계를 이용하여 아래와 같이 설계를 하였습니다.(개인고객, 법인고객) @Entity@Getter @Setter@Inheritance(strategy = InheritanceType.SINGLE_TABLE)@DiscriminatorColumn(...
www.inflearn.com
상속관계 매핑을 지양해야 하는가? - 인프런
[질문 템플릿]1. 강의 내용과 관련된 질문인가요? (예)2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (예)3. 질문 잘하기 메뉴얼을 읽어보셨나요? (예)[질문 내용]https://inf.run/P9aM안
www.inflearn.com
https://velog.io/@stay_o2o/JPA-%EC%83%81%EC%86%8D%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91
JPA - 상속관계 매핑
상속관계 매핑 관련 공부
velog.io
[JPA] 고급매핑 - 상속 관계 , 매핑 정보 상속
인프런에서 에서 김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편을 듣고 쓴 정리 글입니다. https://www.inflearn.com/course/ORM-JPA-Basic 자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 JPA를 처음 접하거
jyami.tistory.com
'클러버' 카테고리의 다른 글
[클러버] @Builder을 활용한 Fixture 셋업 개선 (0) | 2025.03.17 |
---|---|
[클러버] Java Enum을 활용한 프로젝트 개선기 (3) (0) | 2025.02.13 |
[클러버] 운영을 위한 초기 인프라 구축 과정 (0) | 2025.02.12 |
[클러버] Java Enum을 활용한 프로젝트 개선기 (2) (0) | 2024.07.07 |
[클러버] Java Enum을 활용한 프로젝트 개선기 (1) (0) | 2024.07.07 |