Entity 안에서 String 으로 관리되고 있는 값을 다른 DTO 클래스에서 받을 때 BigDecimal 과 같은 데이터 타입으로 받고 싶다면 어떻게 할까?

물론, Entity 안에 있는 필드 데이터 타입과 똑같이 관리되면 좋겠지만 뭔가 크롤러 같은 프로그램에서 어딘가의 가격 데이터를 가져올 때 아래와 같은 데이터들이 포함된다고 해보자.

 

'판매중이지 않음'

'1200000'

'없음'

 

만약, 위 데이터 중에서 숫자만 이루어진 것을 뽑아내려면 어떻게 해야 할까? 충분히 있을 수 있는 상황이다.

 

우선 아래와 같은 클래스가 있다고 하고, renewalPrice 라는 필드 값에 위와 같은 데이터들이 포함된다고 생각해보자

@Builder
@Getter
@NoArgsConstructor @AllArgsConstructor
public class Info {
    private BigDecimal usdPrice;
    private BigDecimal renewalPrice;
}
@Entity
@Builder
@Getter
@NoArgsConstructor @AllArgsConstructor
public class PremiumPrice {
    @Id
    private Long id;

    @Column
    private BigDecimal usdPrice;

    @Column(name = "RENEWAL_PRICE")
    private String renewalPrice;
}

 

자, 여기서 PremiumPrice 라는 Entity 에서 데이터를 불러올 때 Info DTO 클래스로 가격을 넣으려면 어떻게 해야 할까?

사실, Projections.constructor 로 하고 DTO 생성자에서 문자열로 받아 BigDecimal 로 해도 되겠지만, 이 글에서는 순수히 QueryDsl 형변환 하는 법을 다룬다.

 

QueryDsl 로 작성하면 아래와 같이 작성할지도 모른다.

@Override
public List<Info> getList(Request param) {
    BooleanBuilder searchCondition = getListCondition(param);

    // 정규형에 일치하면 숫자로 판단하기 위함
    NumberExpression<Integer> regIns = Expressions.numberTemplate(Integer.class, "REGEXP_INSTR({0}, '^[+-]?\\d*(\\.?\\d*)$')", premiumPrice.renewalPrice);

    JPAQuery<Info> query = queryFactory
            .select(Projections.fields(Info.class,
                    premiumPrice.usdPrice,
                    new CaseBuilder()
                            .when(regIns.eq(1)).then(new BigDecimal(String.valueOf(premiumPrice.renewalPrice))) // 이 부분
                            .otherwise(new BigDecimal(""))
                            .as("renewalPrice")
            ))
            .from(premiumPrice)
            .where(searchCondition);

    return query.fetch();
}

위의 방법은 잘못된 방식이다.

QueryDsl 은 QClass 로 쿼리문을 통해 내부적으로 JPQL 로 변환해서 사용된다. QClass 의 필드는 StringPath 와 같은 데이터 타입으로 관리되는데 위에서처럼 단순히 외부에서 BigDecimal 로 QClass 의 필드를 감싸서 넣는다고 해서 내부적으로 BigDecimal 로 변환해서 동작되지는 않는다. 생각을 해보면 당연한 것이다.

즉, 위 코드는 컴파일에서는 문제가 없지만 런타임에서는 문제가 발생한다.

 

QueryDsl 에서는 위와 같은 경우 캐스팅해서 사용할 수 있도록 castToNum 이라는 메서드를 지원하고 있다.

 

이 castToNum 메서드을 사용하기 위해서는 Number 클래스를 상속하면서 Comparable 인터페이스를 구현하고 있는 클래스를 인자로 받아야 한다.

우리가 자주 쓰는 Integer, Double 같은 래퍼 클래스는 Number 클래스를 상속받아 사용하고 있고 Comparable 인터페이스를 구현하고 있다. 이는 BigDecimal 도 마찬가지다. 숫자를 다루는 일반적인 래퍼 클래스들이라면 보통 위의 조건에 부합한다.

즉, 아까 전의 코드는 아래와 같이 castToNum 을 사용하면 해결할 수 있다.

@Override
public List<Info> getList(Request param) {
    BooleanBuilder searchCondition = getListCondition(param);

    NumberExpression<Integer> regIns = Expressions.numberTemplate(Integer.class, "REGEXP_INSTR({0}, '^[+-]?\\d*(\\.?\\d*)$')", premiumPrice.renewalPrice);

    JPAQuery<Info> query = queryFactory
            .select(Projections.fields(Info.class,
                    premiumPrice.usdPrice,
                    new CaseBuilder()
                            .when(regIns.eq(1)).then(premiumPrice.renewalPrice.castToNum(BigDecimal.class))
                            .otherwise(Expressions.ZERO.castToNum(BigDecimal.class))
                            .as("renewalPrice")
            ))
            .from(premiumPrice)
            .where(searchCondition);

    return query.fetch();
}

참고로 빈 값 또는 0 을 변환해서 사용하려면 Expressions.ZERO 를 사용해서 변환하면 된다.