문제
일반적으로 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을 제거한 값을 반환해주는 것을 이용해 문제를 해결해보았다.
'백엔드 개발' 카테고리의 다른 글
#058. 인프라: cloudflare WAF 설정하기 (0) | 2023.08.26 |
---|---|
#057. 쿼리개선: 피드 페이지 로직 개선 (0) | 2023.08.25 |
#055. 리팩터링: Promise 객체 다루기 (0) | 2023.08.13 |
#054. 코프링: spring-event 로 pub-sub 알림 시스템 만들기 (0) | 2023.07.29 |
#052. 쿼리 개선: subquery가 원하는 인덱스를 타지 않는 경우 🚗 (1) | 2023.07.20 |