코드 생성 라이브러리를 만들 던 중, 만난 문제와 그 해결 방법 입니다.
제가 만든 라이브러리 관련 사항은 아래에 링크를 남겨 두겠습니다.
[신입 개발자의 세 번째 라이브러리] 컴파일 타임에 클래스의 FQCN 알아내기 - persistence-code-generator
들어가며이전 편에서는 ValuedEnum 처리와 타입 안전성 확보에 대해 다뤘습니다.이번 편에서는 @WithConverter 구현 과정에서 마주친 문제와 그 해결 방법을 공유하고자 합니다.@WithConverter(converter: Kclas
shin-e-dog.tistory.com
KSP(Kotlin Symbol Processing)를 사용해 코드를 생성할 때는, 클래스 리터럴을 통해 메타데이터에 접근이 불가능합니다. 이 글에서는 이 문제의 원인과 해결 방법에 대해 알아보겠습니다.
문제 상황
다음과 같은 코드에서:
@WithConverter(LocalDateTimeConverter::class)
val createdAt: LocalDateTime
KSP는 컴파일 타임에 LocalDateTimeConverter 클래스의 실제 구현에 접근할 수 없습니다.
WithConverter 어노테이션의 파라메터 타입을 String으로 바꾸고 FQCN을 직접 받는 방법도 있지만 String에는 어떠한 문자열도 입력될 수 있기 때문에, 사용법을 별도의 코멘트로 남겨줘야 하고, 제 라이브러리에 익숙하지 않은 개발자의 실수를 야기할 수 있는 등 타입 안정성이 떨어지게 되는 문제가 남습니다.
원인 분석
1. 클래스 리터럴의 동작 원리
- 클래스 리터럴(::class)은 해당 클래스의 KClass 인스턴스를 반환합니다
- KClass는 JVM 클래스 파일이 로드된 후에야 접근할 수 있는 리플렉션 API입니다
- qualifiedName, methods, properties 등의 메타데이터는 클래스가 로드된 런타임에만 접근이 가능합니다
2. KSP의 동작 원리
- KSP는 컴파일 과정 중 소스 코드 파싱 직후, 실제 컴파일 이전에 동작합니다
- 이 시점에는:
- 소스 코드가 AST(Abstract Syntax Tree)로 파싱된 상태입니다
- 아직 클래스 파일이 생성되지 않았습니다
- JVM 클래스 로딩도 일어나지 않은 상태입니다
해결 방법
이 문제를 해결하기 위해 다음과 같은 접근 방식을 사용할 수 있습니다:
Qualified Name 추출
@OptIn(KspExperimental::class)
private val WithConverter.qualifiedName: String
get() = try {
this.converter.qualifiedName!!
} catch (e: ClassNotFoundException) {
e.message ?: throw e
} catch (e: KSTypeNotPresentException) {
val cause = e.cause
if (cause is ClassNotFoundException) {
cause.message ?: throw e
} else {
throw e
}
}
해결 방법의 핵심 포인트
- 문자열로 우회: KSP 처리 시점에는 실제 클래스에 접근할 수 없으므로, qualified name을 문자열로 저장합니다.
- 안전한 참조 생성: KotlinPoet의
%T
포매터를 사용해 타입 안전한 참조를 생성합니다. - 지연된 해결: 실제 클래스 참조는 컴파일 시점까지 지연시킵니다.
- 에러 처리: qualified name을 찾지 못하는 등의 오류 상황에 대한 명확한 에러 메시지를 제공합니다.
마치며
KSP의 처리 시점 제약으로 인해 클래스 리터럴의 메타데이터에 직접 접근할 수는 없지만, qualified name을 활용한 우회 방법을 통해 이 문제를 효과적으로 해결할 수 있습니다. 이러한 접근 방식은 타입 안전성을 유지하면서도 필요한 코드 생성 작업을 가능하게 해줍니다.
코드 생성 도구를 만들 때는 각 도구의 동작 시점과 제약사항을 잘 이해하고, 그에 맞는 적절한 해결 방법을 찾는 것이 중요합니다.