
JPA에서는 상속관계 맵핑을 이용해서 객체지향에서 지원하는 다형성을 DB와 호환할 수 있도록 지원한다.
토이 프로젝트를 하고 있는 과정에서 아래와 같은 문제가 생겼다.
1.상속 관계 맵핑 중 싱글 테이블 전략을 사용
2.부모 Entity를 이용해서 자식 Entity를 조회한다.
여기에서 고민이 생겼다.
상속 관계 맵핑을 사용하면서 그 전략이 싱글 테이블 전략이라면, Repository도 부모 Entity의 Repository를 사용할텐데 그러면 자식 Entity를 어떻게 구분해서 조회할까?
이 문제를 해결하기 위한 가장 쉬운 방법은 Controller에 조회를 요청할 때 마다 자식 Entity를 구분할 수 있는 Type을 함께 전달하는 방법이다.
생각만 하면 복잡하니까 방법을 하나씩 정리해보자.
JPA 상속 관계 맵핑 전략 중
SingleTable전략을 사용해서Entity를 구현한 경우, Controller, Service, Repository에서 부모Entity를 이용해 자식Entity를 깔끔하게 조회할 수 있는 방법은 무엇인가?

부모 Entity : Card
자식 Entity : SubwayArrivalCard, BusArrivalCard
가장 간단한 방법은 내가 조회할 Entity가 어떤 타입인지 파라미터로 전달해서 구현하는 방법이 있다.
즉, Controller에서 조회시, 어떤 자식 Entity를 조회할건지 Type을 함께 전달해서 구현한다.
Type은 전달하는 이유는 상속관계 맵핑시, 데이베이스 테이블에는 dtype과 같이 자식 Entity를 구분하기 위한 값이 존재하지만 객체에는 구분 값이 없기 때문이다.
create table tb_card
(
card_group_id bigint
constraint fkb3o624yacnuicqd33d86mgq0t
references tb_card_group,
card_id bigint not null
primary key,
created_date timestamp(6),
last_modified_date timestamp(6),
card_type varchar(31) not null,
bus_stop_id varchar(255),
bus_stop_name varchar(255),
city_code varchar(255),
name varchar(255),
station_name varchar(255)
);
tb_card 테이블에는 자식 Entity를 구분하기 위한 card_type 컬럼이 존재한다. 하지만 객체에는 구분값이 존재하지 않는다.
@GetMapping("/v1")
public ResponseEntity<Response<CardDto.CardDetail>> getCard(@RequestParam("id") Long id, String type) {
if (type == BUS) {
} else if (type == SUBWAY) {
}
..
}
따라서, 상속관계 맵핑에서 SingleTable 전략을 사용하고 있다면 부모 Entity를 이용해서 자식 Entity를 조회하기 때문에(더 정확히는 1개의 Respotiry를 이용해서 여러 모든 자식Entity를 조회) Controller에서 자식 Entity를 구분해서 전달하고 싶다면 Type을 전달받아 자식 Entity로 조회해서 리턴할 수 있겠다.
Type을 전달 받은 방법은 구현이 매우 쉽지만, 코드가 매우 지저분해진다.
뭔가 다형성을 이용해서 깔끔하게 구현할 수 있을것 같은 생각이 든다.(나만 이런 고민을 했을리 없기 때문에..)
구글링을 조금 해본 결과 코드를 깔끔하게 유지하면서 다형성도 살릴 수 있는 방법이 있었다.
참고)
인프런 유사 질문
핵심 포인트는 아래의 2가지다.
Visitor 패턴은 SOLID 원칙 중 개방 폐쇄 원칙을 따르기 위한 방법 중 하나로 방문자와 방문 공간을 분리해서 실제 행위를 방문자에게 위임하는 방법이다.
그리고 이번에 알게 된 내용 중 Jackson 라이브러리는 다형성을 지원하기 위해서 런타임 객체의 타입을 구체적으로 분석한다고 한다. 따라서 컴파일 타임에 결정된 구조가 아닌 런타임에 결정된 객체를 직렬화/역직렬화하게 된다.
위 2가지 핵심 포인트를 이용하면 if문을 제거하고 다형성을 활용할 수 있다.
개선된 코드는 아래와 같다.
public interface CardVisitor {
CardDto.BusArrivalCardDetail visitor(BusArrivalCard busArrivalCard);
CardDto.SubwayArrivalCardDetail visitor(SubwayArrivalCard subwayArrivalCard);
}
public class CardToCardDetailDtoVisitor implements CardVisitor {
@Override
public CardDto.BusArrivalCardDetail visitor(BusArrivalCard busArrivalCard) {
return new CardDto.BusArrivalCardDetail(
busArrivalCard.getId(),
busArrivalCard.getName(),
busArrivalCard.getBusStopName(),
busArrivalCard.getCityCode(),
busArrivalCard.getBusStopId()
);
}
@Override
public CardDto.SubwayArrivalCardDetail visitor(SubwayArrivalCard subwayArrivalCard) {
return new CardDto.SubwayArrivalCardDetail(
subwayArrivalCard.getId(),
subwayArrivalCard.getName(),
subwayArrivalCard.getStationName()
);
}
}
@Entity
@Getter
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "CardType",
discriminatorType = DiscriminatorType.STRING
)
@Table(name = "tb_card")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public abstract class Card extends BaseEntity {
@Id
@Column(name = "card_id")
@GeneratedValue
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "card_group_id")
private CardGroup cardGroup;
public Card(String name) {
this.name = name;
}
/**
* 연관 관계 편의 메서드
*/
public void setCardGroup(CardGroup cardGroup) {
if (this.cardGroup != null) {
this.cardGroup.getCardList().remove(this);
}
this.cardGroup = cardGroup;
cardGroup.getCardList().add(this);
}
/**
* visitor 위임 함수
*/
public abstract CardDto.CardDetail accept(CardVisitor visitor);
}
@Entity
@DiscriminatorValue(value = CardType.Values.BUS)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class BusArrivalCard extends Card{
private String busStopName;
private String cityCode;
private String busStopId;
public BusArrivalCard(String cardName, String busStopName, String cityCode, String busStopId) {
super(cardName);
this.busStopName = busStopName;
this.cityCode = cityCode;
this.busStopId = busStopId;
}
@Override
public CardDto.CardDetail accept(CardVisitor visitor) {
return visitor.visitor(this);
}
}
결과적으로 CardVisitor에게 주요 행위를 위임함으로써 개방 폐쇄 원칙을 따를 수 있으며, 다형성을 활용할 수 있게 되었다.
전체 소스는 아래의 github 주소에서 확인할 수 있습니다.
전체코드