본문 바로가기
백엔드 개발

#056. 리팩터링: 멀티스레드 환경에서 null 제거 이해하기 (Smart cast to 'xx' is impossible, because 'xxx' is a mutable property that could have been changed by this time)

by iamjoy 2023. 8. 14.

문제 

일반적으로 node 개발을 할 때는 어떤 값이 있는 지 확인하기 위해서 일반적으로 아래와 같이 if 문으로 분기를 나눈다.
file이 없다는 것을 확인하고 나면 그 아래에는 file 값이 존재하는 경우에 대해서만 처리한다.
이게 가능한 이유는 node가 싱글스레드를 지원하기 때문이다. 하나의 스레드가 작업하기 때문에 위에서 아래의 흐름으로 위에서 null을 제거하면 아래에서는 이를 반영해 타입추론을 할 수 있다.

  async function doSomethingWithFile(fileId: number) {
    const file = await this.fileQueryRepository.findOne(fileId);

    if (!file) {
      throw Error({ message: '존재하지 않는 파일입니다' });
    }

    ...
	file 이 존재한다는 가정하에 이후 비즈니스 로직
  }


같은 접근으로 kotlin에서 아래와 같이 작업을 할 수 있는데, 이런 경우 에러가 발생한다.

fun toCommentDto(): Comment {
    requireNotNull(this.author) { "작성자가 존재하지 않습니다" }

    return Comment(
        id = "community-comment/${this.id}",
        _id = this.id,
        author = this.author.toUserDto(), // <-------- 에러 발생: Smart cast to 'User' is impossible, because 'this.author' is a mutable property that could have been changed by this time
        content = this.content,
    )
}

에러의 내용은 아래와 같다

 

 

해결

requireNotNull을 예로 들자면 requireNotNull 은 주어진 파라미터의 타입에 null 제거하는 기능이 있다. ( Throws an IllegalArgumentException with the result of calling lazyMessage if the value is null. Otherwise returns the not null value. - value 가 null 이면 Exception을 발생시키고 아니라면 값을 리턴해준다)
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/require-not-null.html

그래서 아래와 같이 parameter로 받은 값을 처리하면 null인 가능성을 제거해준다.

fun toCommentDto2(author: User?): Comment { //<------- parameter로 받는 경우
    requireNotNull(author) { "작성자가 존재하지 않습니다" }

    return Comment(
        id = "community-comment/${this.id}",
        _id = this.id,
        author = author.toUserDto(), //<------- null 가능성이 사라져 정상 작동한다
        content = this.content,
    )
}



반면 이번 문제와 같이 멀티스레드 환경에서 this로 기존 메모리를 참조하는 경우 같은 요청이 여러개일 때 require를 확인하는 시점과 실제 this.author가 사용되는 시점에 변경될 가능성이 있다. 따라서 this.author가 사용될 때에는 require에서 확인한 값을 보증할 수 없다.

이를 해결하는 방법은 require() check() 등 값이 존재하는 지 확인하는 매서드들이 리턴해주는 값을 사용하는 것이다. 이러한 매서드들은 값이 유효할 경우 검사한 값을 리턴해준다.

requireNotNull 구현 코드

/**
 * Throws an [IllegalArgumentException] with the result of calling [lazyMessage] if the [value] is null. Otherwise
 * returns the not null value. // <---- null 값이 아니면 검사한 값을 리턴한다
 *
 * @sample samples.misc.Preconditions.failRequireNotNullWithLazyMessage
 */
@kotlin.internal.InlineOnly
public inline fun <T : Any> requireNotNull(value: T?, lazyMessage: () -> Any): T {
    contract {
        returns() implies (value != null)
    }

    if (value == null) {
        val message = lazyMessage()
        throw IllegalArgumentException(message.toString()) // <---- 조건에 걸리면 에러를 발생시키고
    } else {
        return value // <---- null 값이 아니면 검사한 값을 리턴한다
    }
}

 

 따라서 검사된 값을 val 로 받아 이후에 사용하면 에러가 해결된다.

 

fun toCommentDto(): Comment {
    val author = requireNotNull(this.author) { "작성자가 존재하지 않습니다" } // <---- 값을 리턴받아 val로 선언

    return Comment(
        id = "community-comment/${this.id}",
        _id = this.id,
        author = author.toUserDto(), // <---- 문제 해결
        content = this.content,
    )
}​

 

마무리

node로 작업할 일이 많다보니, 싱글 스레드 환경과 멀티 스레드 환경에서 nullable을 보증하는 방법이 서로 다르다는 것을 확인해볼 수 있었다. 그리고 이 과정에서 kotlin이 지원하는 매서드들(require, requireNotNull등)이 null을 제거한 값을 반환해주는 것을 이용해 문제를 해결해보았다.