[Spring Data Jdbc] 코틀린에서 wither를 인식하지 못하는 문제

2020. 12. 6. 22:58Java

자바에서 Spring Data Jdbc 사용할 때 setter 없이 id를 채워주려면(populate) 다음과 같이 Wither를 만들어주면 된다.

@Getter
@RequriedArgsConstructor
public class Menu {
    @Id
    private final Long id;
    private final String name;
    private final double price;

    public Menu(String name, double price) {
        this(null, name, price);
    }

    public Menu withId(Long id) {
        return new Menu(id, name, price);
    }
}

코틀린에서도 다음과 같이 wither를 만들어주면 작동하겠거니 했다.

class Menu(
        val name: String,
        val price: Double,
        @Id val id: Long? = null
) {
    fun withId(id: Long): Menu = Menu(name, price, id)
}

하지만 다음과 같이 id를 set할 수 없다는 에러가 난다.
java.lang.UnsupportedOperationException: No accessor to set property @org.springframework.data.annotation.Id()

디버깅을 하다보니 아래와 같은 코드를 만났다.

    @Override
    public void setProperty(PersistentProperty<?> property, @Nullable Object value) {

        if (!property.isImmutable() || property.getWither() != null || KotlinDetector.isKotlinType(owner.getType())) {

            delegate.setProperty(property, value);
            this.bean = delegate.getBean();

            return;
        }

주목해야할 부분은 property.getWither()의 결과가 null이 나온다는 점이었다. Wither를 인식하지 못하는 것 같아서 프로퍼티에 wither를 찾아서 넣어주는 부분을 찾아봤다.

    private static Optional<Method> findWither(TypeInformation<?> owner, String propertyName, Class<?> rawType) {

        AtomicReference<Method> resultHolder = new AtomicReference<>();
        String methodName = String.format("with%s", StringUtils.capitalize(propertyName));

        ReflectionUtils.doWithMethods(owner.getType(), it -> {

            if (owner.isAssignableFrom(owner.getReturnType(it))) {
                resultHolder.set(it);
            }
        }, it -> isMethodWithSingleParameterOfType(it, methodName, rawType));

        Method method = resultHolder.get();
        return method != null ? Optional.of(method) : Optional.empty();
    }

    private static boolean isMethodWithSingleParameterOfType(Method method, String name, Class<?> type) {

        return method.getParameterCount() == 1 //
                && method.getName().equals(name) //
                && method.getParameterTypes()[0].equals(type);
    }

가장 아랫줄에서 method.getParameterTypes()[0].equals(type)의 결과가 false로 나오는 것을 확인했다.

코틀린의 Long은 non-nullable하기 때문에 자바에서는 long으로 번역되고, 코틀린의 Long?은 nullable하기 떄문에 자바에서는 java.lang.Long으로 번역된다.
이 때 long.equals(Long)의 결과가 false기 때문에 같은 타입으로 인식하지 못해서 Wither를 인식하지 못하는 문제였다.
따라서, 아래와 같이 필드 타입과 동일하게 파라미터 타입을 맞춰주니 해결되었다(돌이켜보면 당연한 건데...)

class Menu(
        val name: String,
        val price: Double,
        @Id val id: Long? = null
) {
    fun withId(id: Long?): Menu = Menu(name, price, id)
}