코틀린을 사용하는 한국 개발자에게서만 발생할 수 있는 이슈
한국에서 코틀린을 사용하는 개발자들에게 언제든 발생할 수 있는 버그아닌 버그에 대해 소개합니다. 이 이슈는 실제 회사 프로젝트 코드에서 발생했었지만 일반적으로 재현되기 어렵고 특정한 경우에서만 발생할 수 있습니다.
특정한 경우
우리는 테스트 코드를 작성하면 하나의 테스트 케이스에 여러 assertion을 검증하기 위해서 아래와 같이 assertAll을 사용하여 다른 assertion의 동작을 확인할 수 있다.
class Bar {
@Test
fun verifyPerson() {
val person = Person(
id = "aliquid",
name = "Lorem Ipsum",
)
assertAll(
{ assertNotNull(person) },
{ assertEquals(person.id, "wrong") }, // 실패하지만 다음 테스트로 넘어감
{ assertEquals(person.name, "Lorem Ipsum") }, // 테스트함
)
}
}
data class Person(val id: String, val name: String)만약에 실제 비즈니스 로직같은 복잡한 동작을 하고 구체적인 테스트 설명을 추가해야하는 경우가 있다면 다음과 같을 수 있을 것이다.
class GetPrimitiveUserByUniqueIdRequestUserCaseTest {
@Test
fun `특정 아이디로 기본형 유저에 대한 정보를 받아온 후 해당 유저를 팔로워 중인 매니저에 대한 정보를 업데이트하고 해당 받아온 유저의 상태를 온라인 상태로 변환한다`() {
val user = PrimitiveUser(
id = "aliquid",
name = "Lorem Ipsum",
)
val getPrimitiveUserByUniqueIdRequestUserCase = GetPrimitiveUserByUniqueIdRequestUserCase()
val updateManagerInfoRequestUseCase = UpdateManagerInfoRequestUseCase()
val updateUserStatusRequestUseCase = UpdateUserStatusRequestUseCase()
val expectUser = getPrimitiveUserByUniqueIdRequestUserCase("aliquid")
val expectUpdatedManagerInfo = updateManagerInfoRequestUseCase(expectUser.id)
val expectUpdatedStatusStatus = updateUserStatusRequestUseCase(expectUser)
assertAll(
{ assertNotNull(expectUser) },
{ assertEquals(user, expectUser) },
{ assertEquals(expectUpdatedManagerInfo, true) },
{ assertEquals(expectUpdatedStatusStatus, true) },
)
}
}
문제
위 테스트를 빌드한 환경은 MacOS였고 겉으로 보기에는 테스트에서는 문제가 없어 보일 것이다. 하지만 이를 linux기반 환경으로 빌드하면 어떻게 될까? 예를 들어 CI환경이 그러한 경우일 것이다. 그러면 다음과 같은 에러 로그를 확인할 수 있다.
e: error while writing /ci-tool/src/app/build/tmp/kotlin-classes/expDebugUnitTest/in/vvvvvo/manager_status_setting/GetPrimitiveUserByUniqueIdRequestUserCaseTest$특정 아이디로 기본형 유저에 대한 정보를 받아온 후 해당 유저를 팔로워 중인 매니저에 대한 정보를 업데이트하고 해당 받아온 유저의 상태를 온라인 상태로 변환한다$1.class (Permission denied)
원인
Permission denied라는 다소 뜬금없는 원인을 볼 수 있을 것이다. 원인은 org.junit.jupiter.api.Assertion을 사용하면서 람다 표현식을 사용했기 때문이다. 메서드 바디에 람다식이 있다면 gradle task가 내부적으로 파일을 생성한다. 하지만 리눅스 환경에서는 파일이름 길이를 255 bytes로 제한되어 있는데 에러 로그에서 볼 수 있듯 생성된 파일 이름은 다음과 같다.
GetPrimitiveUserByUniqueIdRequestUserCaseTest$특정 아이디로 기본형 유저에 대한 정보를 받아온 후 해당 유저를 팔로워 중인 매니저에 대한 정보를 업데이트하고 해당 받아온 유저의 상태를 온라인 상태로 변환한다$1.class
클래스명 + 메서드명을 합쳐서 파일을 생성했다. 그리고 한글이 3 byte로 계산되었기 때문에 256 bytes를 넘었고 OS에서 제한이 걸렸기 때문에 Permission denied라고 에러 로그가 나타난 것이다. Windows, Mac에서는 파일이름 길이 제한이 255자로 되어 있는데 이를 초과하면 위와 같은 에러를 볼 수 있다.
해결
실제 회사 프로젝트 코드를 CI를 적용하면서 발견했고 예상치 못한 부분에서 문제의 원인을 찾게 되었다. 그래서 CI환경에서 빌드할시 사전에 스크립트로 모든 테스트 케이스 메서드명을 @DisplayName 으로 변경하고 삭제해서 빌드하도록 했다. 스크립트 없이 전부 @DisplayName으로 변경해도 되었지만 좀 더 개발의 편의성을 생각해서 기존과 동일한 방식으로 테스트 코드를 만들 수 있도록 했다.
결론
이는 테스트 코드 뿐만 아니라 코틀린 파일을 사용하는 모든 곳에서 발생할 수 있는 잠재적 버그이다. 람다식이 함수 바디에만 있는 경우만 해당되기 때문에 다음과 같은 케이스를 유의하면 된다.
fun veryLongMethodName.....() {
{}
}람다식이 파라미터에 있거나 그것을 함수에서 사용하는 것에서는 문제는 없었다. 그리고 대부분 영어로 클래스, 메서드명을 작성하기 때문에 255자 이상 혹은 256 bytes를 넘는 경우는 찾기 어려울 것이다.
정리하면 코틀린을 사용하는 프로젝트에서는 언제든 발생할 수는 있지만 대부분의 경우에서 발생하지는 않을 것이다. 다만, CI환경같은 linux와 테스트 코드에서 일부 한글을 허용한다면 발생할 수 있는 문제이니 개인, 팀, 회사의 방향성에 맞게 대책을 충분히 마련할 수 있을 것이다.