Java -> Kotlin 과연 얼마나 좋을까?

MoonJaeGyeong·2024년 10월 18일
1

개요


배움에 있어 가장 중요하다 생각되는 것은 동기부여라고 생각한다. 무언가를 배움에 있어 좀 더 동기가 있다면 더 열정적으로 잘 배울 수 있지 않을까 싶어 해당 글을 작성해본다.

Kotlin 을 처음 배우는데 Java가 아직 나에겐 너무 익숙하기에 해당 언어를 배우는데 큰 동기가 생기지가 않아 어떤 점이 좋길래, 그리고 앞으로는 어떻게 변화하기에 Kotlin 을 배우는 것이 좋을지 알아보도록 하자


1. 코틀린을 사용하는 기업들


일단 우리는 취업이 목표이기 때문에 이것 또한 굉장한 동기부여의 하나가 될 수 있다고 생각해서 가져와봤다. 코틀린을 사용하는 기업들이 생각보다 굉장히 많다.

안드로이드 스튜디오 때문에 사용하는 곳도 있겠지만 일단 우리가 잘 아는 당근, 토스, 라인, 카카오, 배민 들의 경우에는 실제로 Spring + Kotlin 을 채택하고 사용하고 있다고 한다.


2. 기업들은 왜 코틀린을 선택할까?


이와 관련된 대답은 JetBrains이 코틀린 언어를 개발한 이유와 일맥상통하다.

  • 코드의 생산성을 향상시킨다.
  • 코틀린은 자바의 고질적인 문제였던 NULL 안정성을 해결한다.
  • 개발자들은 평생동안 코드를 쓰는 것 보다 읽는 곳에 많은 시간을 할애한다. 이러한 점을 고려하여 코틀린은 가독성에 초점을 뒀다.

3. null 안전성


코틀린 언어 하면 가장 자바와 비교 되는 부분이 null 안전성 관련이다. null 값에 대한 안전성이 보장되지 않는 자바의 경우는 10억달러 짜리 실수 라며 언급 되기도 한다.

그렇다면 어떤 방식으로 코틀린은 null 안전성을 확보할까요??

3-1. 컴파일 에러

우선 컴파일러 단에서 먼저 검사를 NPE 가 발생할만한 코드를 원천 차단해줍니다.

class User(
  val id: String,
  val name: String,
  val phone: String? // nullable
)

fun main() {
  val user = getUser("id")
  
  if (user.name == "Moon") {
    println("He is Moon")
  }
  
  val number = user.phone.split("-") // 컴파일러 단에서 오류를 뱉음
  println(number)
}

위와 같은 코드에서 원래 자바라면 phone 값이 null 일 경우 user.phone.split("-") 에서 NPE 를 뱉었을 것이다. 하지만 코틀린에서는 애초에 실행조차 못하게 막아준다.

그래서 자바에서는 null 관련 이슈를 전적으로 개발자에게 맡기고 있는데 kotlin 에서는 컴파일러 단에서 막아줘 개발자가 좀 더 null 을 신경 쓸 수 있게 해준다.

3-2 Safe call

val name: String? = "Moon JaeGyeong"
name?.length // name 이 null 일 경우 실행되지 않음

safe call 은 nullable 한 변수가 있을 때 그 변수에 접근할 때 ? 을 사용하여 접근을 시도하는 문법입니다. 만약에 변수가 null 이라면 해당 구문은 실행되지 않습니다.

3-3 Elvis Operation

val name: String? = "Moon JaeGyeong"
val len = name?.length ?: -1 // name 이 null 일 경우 -1

엘비스 연산의 경우 변수가 null 일 경우 우항, 아닐 경우 좌항이 실행된다.

번외

그렇다면 웹 개발자의 경우 NPE 가 발생할만한 곳이 내가 생각하기에는 RequestDto 에서 유효성 검사를 제대로 안해준다면 그 때 가장 많이 날 것이라 생각해 한 번 간단하게 Java 와 Kotlin 코드로 어떤 식으로 차이가 날지 궁금해 짜보았다.

Controller.java

@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class Controller {
    private final Service service;

    @PostMapping
    public ResponseEntity<ResponseDto> exampleController(@RequestBody RequestDto requestDto){
        return ResponseEntity.ok(service.exampleService(requestDto));
    }

}

RequestDto.java

public record RequestDto(
        String name,
        String email
) {
}

Service.java

@Service
public class Service {

    public ResponseDto exampleService(RequestDto requestDto) {
        String[] name = requestDto.name().split(","); // First name, Second name
        return new ResponseDto(name[1], requestDto.email());
    }
}

request

{
    "name" : null,
    "email" : "mjk8087@naver.com"
}

response

{
    "timestamp": "2024-10-18T00:21:49.648+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "path": "/api"
}

request로 name 을 null 값으로 보냈더니 java 의 경우 NPE 를 내뱉는 것을 볼 수 있다. 그렇다면 코틀린은 어떻게 뱉을지가 궁금해졌다.



Controller.kt

@RestController
@RequestMapping("/api")
class Controller(private val service: Service) {

    @PostMapping("")
    fun exampleController(@RequestBody requestDto: RequestDto) : ResponseEntity<ResponseDto> {
        return ResponseEntity.ok(service.exampleService(requestDto))
    }
}

RequestDto.kt

data class RequestDto (
    val name: String,
    val email: String){
}

Service.kt

@Service
class Service {
    fun exampleService(requestDto : RequestDto) : ResponseDto {
        val name = requestDto.name.split(","); // First name, Second name
        return ResponseDto(name[1], requestDto.email)
    }
}

request

{
    "name" : null,
    "email" : "mjk8087@naver.com"
}

response

{
    "timestamp": "2024-10-18T00:44:32.231+00:00",
    "status": 400,
    "error": "Bad Request",
    "trace": "org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Instantiation of [simple type, class com.kotlinexample.dto.RequestDto] value failed for JSON property name due to missing (therefore NULL) value for creator parameter name which is a non-nullable type\r\n\tat org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:406)\r\n\tat org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.read(AbstractJackson2HttpMessageConverter.java:354)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(AbstractMessageConverterMethodArgumentResolver.java:184)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.readWithMessageConverters(RequestResponseBodyMethodProcessor.java:161)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:135)\r\n\tat org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:122)\r\n\tat org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:224)\r\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:178)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:926)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:831)\r\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979)\r\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)\r\n\tat org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914)\r\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590)\r\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)\r\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\r\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\r\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\r\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\r\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\r\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)\r\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)\r\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483)\r\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)\r\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)\r\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)\r\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344)\r\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:384)\r\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)\r\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905)\r\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741)\r\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)\r\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190)\r\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)\r\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)\r\n\tat java.base/java.lang.Thread.run(Thread.java:842)\r\nCaused by: com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException: Instantiation of [simple type, class com.kotlinexample.dto.RequestDto] value failed for JSON property name due to missing (therefore NULL) value for creator parameter name which is a non-nullable type\n at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 4, column: 1] (through reference chain: com.kotlinexample.dto.RequestDto[\"name\"])\r\n\tat com.fasterxml.jackson.module.kotlin.KotlinValueInstantiator.createFromObjectWith(KotlinValueInstantiator.kt:97)\r\n\tat com.fasterxml.jackson.databind.deser.impl.PropertyBasedCreator.build(PropertyBasedCreator.java:202)\r\n\tat com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:451)\r\n\tat com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1493)\r\n\tat com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:348)\r\n\tat com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:185)\r\n\tat com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:342)\r\n\tat com.fasterxml.jackson.databind.ObjectReader._bindAndClose(ObjectReader.java:2125)\r\n\tat com.fasterxml.jackson.databind.ObjectReader.readValue(ObjectReader.java:1501)\r\n\tat org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:395)\r\n\t... 51 more\r\n",
    "message": "JSON parse error: Instantiation of [simple type, class com.kotlinexample.dto.RequestDto] value failed for JSON property name due to missing (therefore NULL) value for creator parameter name which is a non-nullable type",
    "path": "/api"

같은 코드에 같은 request를 보냈더니 NPE 를 뱉지 않고 400으로 안전하게 처리하는 것을 볼 수 있다. message 또한 정확히 어떤 부분이 잘못됐는지는 알 수 없지만 JSON parse error 에서 구문이 뭔가 잘못 됐다는 거 정도는 알 수 있을 것이다.

이런 식으로 kotlin 은 null 값에 대한 처리가 다르다는 것을 확실히 알 수 있었다.

번외 2

하지만 요즘 자바에 편의성이 많은 어노테이션들이 많아 Dto로 들어오는 값들은 보통 @NotNULL 어노테이션으로 모두 거르는 편이다. 또한 어떤 부분이 비었는지, 어떤 부분이 잘못되었는지를 확실하게 알려주는 것 또한 비용 절약을 위해서는 필요하다고 생각하므로 Kotlin 에서도 동일한 방법으로 가능한지 한 번 시도해봤다.

Controller.java

@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class Controller {
    private final Service service;

    @PostMapping
    public ResponseEntity<ResponseDto> exampleController(@RequestBody @Valid RequestDto requestDto){
        return ResponseEntity.ok(service.exampleService(requestDto));
    }

}

RequestDto.java


public record RequestDto(
        @NotNull(message = "name 이 비었습니다.")
        String name,
        @NotNull(message = "email 이 비었습니다.")
        String email
) {
}

GlobalExceptionHandler.java

@ControllerAdvice
public class GlobalExceptionHandler {

    // 유효성 검사 실패 시 처리
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach(error -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
    }

}



request

{
    "name" : null,
    "email" : "mjk8087@naver.com"
}

response

{
    "name": "name 이 비었습니다."
}

항상 하던 대로 에러를 처리하도록 진행했다. 깔끔하게 어떤 부분에서 에러가 났는지를 클라이언트에게 잘 알려주는 모습이다. 코틀린의 경우에도 동일하게 진행 될까?



Controller.kt

@RestController
@RequestMapping("/api")
class Controller(private val service: Service) {

    @PostMapping("")
    fun exampleController(@RequestBody @Valid requestDto: RequestDto) : ResponseEntity<ResponseDto> {
        return ResponseEntity.ok(service.exampleService(requestDto))
    }
}

RequestDto.kt

data class RequestDto (
    @field:NotNull(message = "name 이 비어있습니다.")
    val name: String,
    @field:NotNull(message = "email 이 비어있습니다.")
    val email: String){
}

Service.kt

@Service
class Service {
    fun exampleService(requestDto : RequestDto) : ResponseDto {
        val name = requestDto.name.split(","); // First name, Second name
        return ResponseDto(name[1], requestDto.email)
    }
}

request

{
    "name" : null,
    "email" : "mjk8087@naver.com"
}

response

{
    "timestamp": "2024-10-18T00:59:03.094+00:00",
    "status": 400,
    "error": "Bad Request",
    "trace": "org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Instantiation of [simple type, class com.kotlinexample.dto.RequestDto] value failed for JSON property name due to missing (therefore NULL) value for creator parameter name which is a non-nullable type\r\n\tat org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:406)\r\n\tat org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.read(AbstractJackson2HttpMessageConverter.java:354)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(AbstractMessageConverterMethodArgumentResolver.java:184)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.readWithMessageConverters(RequestResponseBodyMethodProcessor.java:161)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:135)\r\n\tat org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:122)\r\n\tat org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:224)\r\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:178)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:926)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:831)\r\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979)\r\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)\r\n\tat org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914)\r\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590)\r\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)\r\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\r\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\r\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\r\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\r\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\r\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)\r\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)\r\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483)\r\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)\r\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)\r\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)\r\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344)\r\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:384)\r\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)\r\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905)\r\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741)\r\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)\r\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190)\r\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)\r\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)\r\n\tat java.base/java.lang.Thread.run(Thread.java:842)\r\nCaused by: com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException: Instantiation of [simple type, class com.kotlinexample.dto.RequestDto] value failed for JSON property name due to missing (therefore NULL) value for creator parameter name which is a non-nullable type\n at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 4, column: 1] (through reference chain: com.kotlinexample.dto.RequestDto[\"name\"])\r\n\tat com.fasterxml.jackson.module.kotlin.KotlinValueInstantiator.createFromObjectWith(KotlinValueInstantiator.kt:97)\r\n\tat com.fasterxml.jackson.databind.deser.impl.PropertyBasedCreator.build(PropertyBasedCreator.java:202)\r\n\tat com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:451)\r\n\tat com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1493)\r\n\tat com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:348)\r\n\tat com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:185)\r\n\tat com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:342)\r\n\tat com.fasterxml.jackson.databind.ObjectReader._bindAndClose(ObjectReader.java:2125)\r\n\tat com.fasterxml.jackson.databind.ObjectReader.readValue(ObjectReader.java:1501)\r\n\tat org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:395)\r\n\t... 51 more\r\n",
    "message": "JSON parse error: Instantiation of [simple type, class com.kotlinexample.dto.RequestDto] value failed for JSON property name due to missing (therefore NULL) value for creator parameter name which is a non-nullable type",
    "path": "/api"
}

하지만 기대와는 다르게 아까와 같은 에러를 뱉는 것을 볼 수 있다.. 이거에 대해서 간단하게 알아보니 @NotNull 어노테이션을 통해서 원하는 에러를 던지게 하고 싶으면
kotlin 이 먼저 검증을 하기 때문에 requestDto 에서 해당 field 들을 nullable 하게 선언을 해줘야 한다고 한다.

그래서 코드를 아래와 같이 바꾼다면

requestDto.kt

data class RequestDto (
    @field:NotNull(message = "name 이 비어있습니다.")
    val name: String?,
    @field:NotNull(message = "email 이 비어있습니다.")
    val email: String?){
}

response

{
    "name": "name 이 비어있습니다."
}

위와 같이 원하는 방향으로 바뀌는 것을 볼 수 있다.



4. 결론


사실 기업이 코틀린을 쓰는 이유는 해당 null 안전성 말고도 많은 이유가 존재한다. 타입 추론, 싱글톤 Object 등등의 많은 기능들이 존재한다.
하지만 단점도 존재한다. 컴파일 시간이 Java 보다 조금 더 오래 걸리고, 런타임 라이브러리의 크기가 더 커질 수 있다. 하지만 요즘 좋아지고 있는 하드웨어에 따라 해당 단점들은 적어도 우리가 개발하는 웹 개발쪽에서는 크게 신경쓰지 않는 듯 싶다.

되게 좋은 언어이고 자바에 비해 장점도 많은 언어이다. 앞으로도 계속 발전해나갈 것이고 쓰는 곳도 많아질 것으로 예상되는데 아직까지는 반드시 배워야 하는 언어는 아닌 거 같고 배워 놓으면 좋은 언어 정도의 위치이지 아닐까 싶다.

그래도 프로젝트 하는 만큼 한 번 공부를 잘 해봐야겠다.

profile
내 맘대로 끄적이는 개발 블로그

0개의 댓글

관련 채용 정보