[JPA] 싱글테이블 전략에서 자식 Entity 조회하기

hoyong.eom·2024년 12월 24일

JPA

목록 보기
4/8
post-thumbnail

JPA

JPA에서는 상속관계 맵핑을 이용해서 객체지향에서 지원하는 다형성을 DB와 호환할 수 있도록 지원한다.

토이 프로젝트를 하고 있는 과정에서 아래와 같은 문제가 생겼다.

1.상속 관계 맵핑 중 싱글 테이블 전략을 사용
2.부모 Entity를 이용해서 자식 Entity를 조회한다.

여기에서 고민이 생겼다.

상속 관계 맵핑을 사용하면서 그 전략이 싱글 테이블 전략이라면, Repository도 부모 EntityRepository를 사용할텐데 그러면 자식 Entity를 어떻게 구분해서 조회할까?

이 문제를 해결하기 위한 가장 쉬운 방법은 Controller에 조회를 요청할 때 마다 자식 Entity를 구분할 수 있는 Type을 함께 전달하는 방법이다.

생각만 하면 복잡하니까 방법을 하나씩 정리해보자.

문제

JPA 상속 관계 맵핑 전략 중 SingleTable 전략을 사용해서 Entity를 구현한 경우, Controller, Service, Repository에서 부모 Entity를 이용해 자식 Entity를 깔끔하게 조회할 수 있는 방법은 무엇인가?

Entity 구조

부모 Entity : Card
자식 Entity : SubwayArrivalCard, BusArrivalCard

구현 방법

Type 전달

가장 간단한 방법은 내가 조회할 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로 조회해서 리턴할 수 있겠다.

Visitor 패턴

Type을 전달 받은 방법은 구현이 매우 쉽지만, 코드가 매우 지저분해진다.
뭔가 다형성을 이용해서 깔끔하게 구현할 수 있을것 같은 생각이 든다.(나만 이런 고민을 했을리 없기 때문에..)

구글링을 조금 해본 결과 코드를 깔끔하게 유지하면서 다형성도 살릴 수 있는 방법이 있었다.

참고)
인프런 유사 질문

핵심 포인트는 아래의 2가지다.

  • Visitor 패턴 사용
  • Jackson 라이브러리

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 주소에서 확인할 수 있습니다.
전체코드

0개의 댓글