[Spring Boot] 자바로 작성된 프로젝트를 코틀린으로 리펙토링 하면서 느낀 점
# 서론
스프링 부트를 자바로 작성하다보면 DTO를 만들때 클래스가 매우 많이 생성되서 한 눈에 보기 불편한 경우가 생겼다.
DTO를 코틀린으로 작성하면 얻는 이점 : 하나의 코틀린 파일에 여러개의 DTO 클래스를 data class로 작성할 수 있다.
그래서 코틀린을 도입하여 리펙토링을 진행해보려 하면서 느낀점을 정리하려고 글을 쓴다.
* Ektour 리뉴얼 프로젝트에 적용함으로써 느낀 느낀점을 작성함.
참고한 글
https://d2.naver.com/helloworld/6685007
# 코틀린을 도입하면서 직면한 문제
우선, DTO를 간략화 하기위해서 코틀린을 도입했으므로, DTO 부분 먼저 리펙토링을 진행하였다.
0. 코틀린으로 static 메소드 작성하기
코틀린으로 작성된 객체를 편의 메소드 (엔티티 -> DTO 변환 메소드 등)를 작성하려 할 때, 보통 static 메소드로 구현을 했는데 코틀린에서는 static 문법이 없기 때문에 companion object를 활용해서 구현해야한다.
@Getter @AllArgsConstructor
public class ErrorResponse {
private String code;
private String message;
public static ErrorResponse convertJson(FieldError error) {
return new ErrorResponse(ErrorCode.VALIDATION_ERROR, error.getDefaultMessage());
}
public static ArrayList<ErrorResponse> convertJson(List<FieldError> bindingResults) {
ArrayList<ErrorResponse> result = new ArrayList<>();
for (FieldError e : bindingResults) {
result.add(ErrorResponse.convertJson(e));
}
return result;
}
}
기존 자바로 작성된 ErrorResponse 공통 DTO 클래스이다. BindingResult 결과를 Json으로 변환해서 내리는 역할을 수행하는 편의메소드 convertJson을 코틀린으로 구현하려면 다음과 같이 해야한다.
data class ErrorResponse(
val code: String = "",
val message: String = "",
) {
companion object {
fun convertJson(error: FieldError): ErrorResponse {
return ErrorResponse(VALIDATION_ERROR, error.field + " / " + error.defaultMessage)
}
fun convertJson(bindingResults: List<FieldError?>): ErrorListResponse {
val result = ArrayList<ErrorResponse>()
for (e in bindingResults) {
e?.let { this.convertJson(it) }?.let { result.add(it) }
}
return ErrorListResponse(result)
}
}
}
그런데 이러면 해당 메소드를 호출할 때, ErrorResponse.Companion.convertJson() 형태로 호출해야하서 다소 불편하다.
따라서 함수 앞에 @JvmStatic 어노테이션을 붙혀주면 기존 자바 static 처럼 사용이 가능하다.
data class ErrorResponse(
val code: String = "",
val message: String = "",
) {
companion object {
@JvmStatic fun convertJson(error: FieldError): ErrorResponse {
return ErrorResponse(VALIDATION_ERROR, error.field + " / " + error.defaultMessage)
}
@JvmStatic fun convertJson(bindingResults: List<FieldError?>): ErrorListResponse {
val result = ArrayList<ErrorResponse>()
for (e in bindingResults) {
e?.let { this.convertJson(it) }?.let { result.add(it) }
}
return ErrorListResponse(result)
}
}
}
1. 코틀린에서 롬복 사용 불가능
이는 컴파일 시점에 관련된 문제로, 코틀린 코드를 컴파일러가 컴파일하고, 자바 컴파일러가 자바를 컴파일 한 뒤, Annotation Processing 단계에서 롬복 어노테이션이 적용된다. 따라서 코틀린에서는 자바에서 작성된 롬복 관련 코드를 접근 할 수 없다.
이 문제는 엔티티를 DTO로 변환하는 메소드에서 빌더를 사용해서 만들때 발생한다. (현재 엔티티는 자바로, DTO는 코틀린으로 작성되어있다.)
data class EstimateSimpleResponse(
var id: Long = 0L,
var name: String = "",
var travelType: String = "",
var departPlace: String = "",
var arrivalPlace: String = "",
var vehicleType: String = "",
var createdDate: String = "",
) {
companion object {
@JvmStatic fun createDto(e: Estimate) {
return EstimateSimpleResponse(id = e.id)
}
}
}
이런식으로 자바로 작성된 엔티티인 Estimate의 id에 접근하려면, 롬복의 도움을 받아 @Getter가 생성되어 있어야 하는데, 컴파일 시점의 차이로인해 Estimate의 id에 코틀린 코드에서는 접근할 수 없다.
그래서 다음 두 가지 방법을 생각했다.
- 엔티티도 코틀린으로 리펙토링 한다.
- 엔티티에 Getter를 롬복 도움없이 작성한다.
1번 방법은 다음과 같은 문제를 야기시켰다.
현재, 프론트엔드를 리액트로 작성된 폴더를 스프링 부트에 통합시켜서 빌드, 배포 하고 있다. 이 때 통합빌드 스크립트를 Gradle에 Groovy DSL로 작성되어있는데, 코틀린 환경에서 JPA를 사용하려면 몇 가지 추가적인 세팅을 해줘야한다. (이 때 Kotlin DSL로 작성한다.) 따라서 현재 작성된 Gradle 스크립트를 마이그레이션해야하는데, Kotlin DSL를 사용을 안 해봐서... 공부를 더 해야한다.
2번 방법은 Getter를 너무 많이 생성해서 코틀린을 도입해서 간략화 하는데 의미가 사라지기 때문에 논외다.
3. Service, Controller 단을 코틀린으로 리펙토링 해야할까?
DTO는 하나의 코틀린 파일로 작성하면 한 눈에 보기 쉽워서 간략화 하는데 도움이 되지만, 굳이 잘 작성된 자바 Service나 Controller를 코틀린으로 리펙토링 해야하는지에 대한 의문이 생겼다.
만약 코틀린을 도입하면 롬복을 사용할 수 없으므로, @Slf4J를 사용할 수 없다. 즉, 로거를 직접 만들어서 사용해야하는 번거로움이 존재한다.
그리고 코틀린으로 리펙토링해도 큰 차이가 없음을 발견해서 해당 부분은 그냥 자바 코드로 유지하기로 했다.
# 결론
- DTO나 Custom Exception 처럼 반복되고 간략하며 다른 자바 코드와 결합도가 낮은 클래스들은 코틀린 파일로 하나의 파일로 작성하는게 좋은 것 같다.
- 기존 자바로 작성된 Controller, Service 단은 코틀린을 도입해서 드라마틱한 리펙토링이 이루어지지 않는 이상, 코틀린으로 얻는 이점이 생각보다 크지 않는 것 같다.
- build.gradle을 build.gradle.kts로 마이그레이션 하는 것은 조금 까다롭기 때문에 애초에 처음부터 코틀린으로 프로젝트를 생성하자.