F-Lab
🚀
깊이있는 개발자 커뮤니티, 데브클럽을 한 달 동안 무료로 시작해보세요
🚀
깊이있는 개발자 커뮤니티,
데브클럽을 한 달 동안
무료로 시작해보세요

코틀린 타입 시스템: 왜 Long 타입으로 변환했는데 null이 됐을까?

writer_thumbnail

F-Lab : 상위 1% 개발자들의 멘토링

안녕하세요! 저는 F-Lab 에서 백엔드 개발을 맡고 있는 Luca입니다.

 

개요


 

저희는 멘티들이 자신의 멘토링 프로젝트를 F-Lab GitHub 조직의 레포지토리에서 진행하도록 권장합니다.

 

아래와 같은 이유인데요:

  1. 멘티가 멘토링을 진행하며 Git과 Github에 익숙해지는 것
  2. 멘토가 프로젝트의 상황을 효율적으로 추적할 수 있음

그런데 최근, 멘티의 GitHub 계정을 연동하는 과정에서 GitHub ID가 null로 저장되는 문제가 발생했습니다. 이 문제를 파악하고 해결한 과정을 공유하고자 합니다.

 

 

 

웹훅 데이터


 

앞선 이유 외에 F-Lab 조직의 레포지토리에서 멘토링 프로젝트를 진행하면 저희에게도 좋은 점이 있습니다. Github에서는 조직 단위의 이벤트와 웹훅을 제공하는데요. 저희는 멘티와 멘토분들께 나은 서비스를 제공하기 위해 데이터를 수집하는데 활용하고 있습니다.

 

다음은 저희가 수신한 웹훅 데이터의 예입니다:

{
 "action": "member_added",
 "membership": {
   "url": "https://api.github.com/orgs/f-lab-edu/memberships/f-lab-luca",
   "state": "active",
   "role": "member",
   "organization_url": "https://api.github.com/orgs/f-lab-edu",
   "user": {
     "login": "f-lab-luca",
     "id": 147676366,
     "type": "User",
     "user_view_type": "public",
     "site_admin": false
   }
 }
 
 ...
 
}

멘티가 조직 초대 이메일을 수락하면 위와 같은 데이터가 미리 작성한 웹훅 핸들러로 전송됩니다.

 

 

 

초기 접근 방식


 

저는 action, login, id 값을 추출하여 로그에 출력하기 위해 다음과 같은 코드를 작성했습니다:

val membership = body["membership"] as Map<*, *>
val user = membership["user"] as Map<*, *>
val login = user["login"] as? String
val id = user["id"] as? Long

log.info { "webhook action: ${body["action"]} github name: $login github id: $id" }

 

제가 기대했던 출력값은 다음과 같았습니다:

webhook action: member_added github name: f-lab-luca github id: 147676366

 

그리고 실제 출력된 값은 다음과 같았습니다:

webhook action: member_added github name: f-lab-luca github id: null

 

 

 

문제 분석


 

웹훅의 로그를 살펴보면 데이터는 정상적으로 전송된 것을 확인할 수 있습니다. 때문에 저는 변환에 문제가 있는 것 같다고 판단을 하고 차근차근 분석해나갔습니다.

 

 

테스트를 작성해보자

우선 다음과 같은 간단한 테스트를 작성했습니다:

class Test {
    @Test
    fun `id 값을 Long으로 변환`() {
        val id = 14676366

        val actual = id as? Long

        actual shouldBe 14676366L
    }
}

위 테스트는 성공할까요?

 


앞서 살펴본 문제와 같이 기대한 값이 null로 표시되고 있습니다. IDEA 상에서도 이를 표시해줍니다.


아래 문구를 살펴보면 변환이 절대 성공하지 않는다라고 되어있는데요. 왜 그런걸까요?

 

 

코틀린 타입 시스템

Kotlin In Action의 6장 “코틀린 타입 시스템”에서는 이런 내용이 나옵니다. 

 

“as? 연산자는 어떤 값을 지정한 타입으로 캐스트한다. 값을 대상 타입으로 변환할 수 없으면 null로 변환한다”. 

 

자바에서는 잠재적으로 값 손실이 발생할 수 있는 암시적 형 변환을 허용하지만 코틀린의 타입 시스템은 엄격하며 암시적인 형 변환을 허용하지 않습니다.  코틀린에서 IntLong은 모두 정수를 다루지만, 별개의 타입이며 as 연산자는 숫자 유형 변환이 아닌 호환되는 유형의 타입 간에만 작동합니다. 

 

따라서 Intas? Long으로 변환하려는 시도는 항상 null을 반환합니다.  만약 as를 사용했다면, ClassCastException이 발생했을 것입니다. 

 

테스트를 수정해서 Int로 변환하니 성공하는 모습입니다.

 

 

왜 id가 Int 타입으로 처리되었을까?

Map<String, Any?>를 사용할 때 JSON 데이터는 Jackson에 의해 파싱됩니다. Jackson은 숫자 값을 가능한 가장 작은 적합한 타입으로 매핑합니다. 

기본 설정으로 32비트 정수 범위(-2,147,483,648 ~ 2,147,483,647)에 해당하는 숫자는 Int로 변환되고, 더 큰 숫자는 Long으로 변환됩니다. 147676366은 32비트 범위 내에 있으므로 Jackson은 이를 Int로 역직렬화했습니다. 따라서 as? Long 변환이 실패한 것입니다.

 

 

 

해결책


 

Map 대신 DTO를 다음과 같이 작성해 문제를 해결했습니다:

@JsonIgnoreProperties(ignoreUnknown = true)
data class OrganizationEvent(
    @JsonProperty("action")
    val action: String,
    @JsonProperty("membership")
    val membership: Membership,
) {
    val login: String
        get() = membership.login

    val id: Long
        get() = membership.id

    @JsonIgnoreProperties(ignoreUnknown = true)
    data class Membership(
        @JsonProperty("user") val user: User,
    ) {
        val login: String
            get() = user.login

        val id: Long
            get() = user.id
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    data class User(
        @JsonProperty("login") val login: String,
        @JsonProperty("id") val id: Long,
    )
}

 

DTO에서는 id 타입을 명시적으로 Long으로 정의했습니다. 또한 필요한 데이터만 받기 위해 Jackson에서 제공하는 @JsonIgnoreProperties를 사용했습니다.

 

앞서 Jackson에서 역직렬화 방식 설정을 다루는 DeserializationFeature를 살펴보면

Jackson은 기본적으로 Json에 담긴 모든 데이터들의 역직렬화를 시도하기 때문에 매핑할 프로퍼티가 없으면 JsonMappingException이 발생하게 됩니다. @JsonIgnoreProperties 어노테이션은 특정 필드를 인식할 수 없을 때 이를 무시하며 예외를 회피할 수 있게 됩니다.

테스트를 작성하니 성공하는 모습입니다. 진작에 DTO로 정의했으면 어땠을까란 생각이 드네요.

 

 

왜 DTO에서는 잘 동작할까?

 평소에는 전혀 의심을 하지 않았던 부분인데요. 제가 하나 의문점을 가진 것은 Map<String, Any?>로 받을 때도 Json 데이터를 Jackson이 변환해줬을 터인데, 이번에는 Userid 프로퍼티를 Long으로 작성했음에도 변환이 잘 이루어진 부분입니다. 

 

아까도 보셨을 위 이미지의 문구를 다시 한 번 살펴보겠습니다. Jackson은 DTO에 타입이 명시되어 있다면 이를 우선합니다. JSON 데이터를 DTO의 Long 필드로 역직렬화할 때, Jackson은 작은 숫자 타입(Int)도 자동으로 Long으로 변환합니다. 때문에 문제가 발생하지 않고 정상적으로 변환이 됩니다.

 

 

 

배운 점


 

이번 문제를 통해 다음과 같은 중요한 점을 다시 확인할 수 있었습니다.

 

명시적인 타입 정의의 중요성: DTO를 사용하면 데이터 타입이 명시적으로 정의되어 예기치 못한 역직렬화 문제를 방지할 수 있습니다.

Kotlin 타입 시스템 이해: IntLong은 Kotlin에서 별개의 타입이며, 두 타입 간의 변환은 명시적으로 처리해야 합니다.

Jackson의 기본 동작: Jackson은 명시적으로 설정하지 않는 한, JSON 데이터를 가장 작은 적합한 숫자 타입으로 변환합니다.

DTO를 우선적으로 사용: Map을 사용하는 것이 간단해 보일 수 있지만, DTO는 더 안전하고 신뢰할 수 있습니다.

 

 

 

맺으며


 

평소에는 DTO를 작성하여 매핑하는 것을 선호합니다. 이번 케이스에서는 단순하게 해보자란 생각으로 Map으로 역직렬화를 시도했다가 이런 문제를 만났는데요. 덕분에 Kotlin의 타입 변환과 Jackson을 더 공부하게 되는 시간을 가지게 된 것 같습니다.


읽어주셔서 감사합니다. 😊

ⓒ F-Lab & Company

이 컨텐츠는 F-Lab의 고유 자산으로 상업적인 목적의 복사 및 배포를 금합니다.

조회수

멘토링 코스 선택하기

  • 코스 이미지
    Java Backend

    아키텍처 설계와 대용량 트래픽 처리 능력을 깊이 있게 기르는 백앤드 개발자 성장 과정

  • 코스 이미지
    Node.js Backend

    아키텍처 설계와 대용량 트래픽 처리 능력을 깊이 있게 기르는 백앤드 개발자 성장 과정

  • 코스 이미지
    Python Backend

    대규모 서비스를 지탱할 수 있는 대체 불가능한 백엔드, 데이터 엔지니어, ML엔지니어의 길을 탐구하는 성장 과정

  • 코스 이미지
    Frontend

    기술과 브라우저를 Deep-Dive 하며 성능과 아키텍처, UX에 능한 개발자로 성장하는 과정

  • 코스 이미지
    iOS

    언어와 프레임워크, 모바일 환경에 대한 탄탄한 이해도를 갖추는 iOS 개발자 성장 과정

  • 코스 이미지
    Android

    아키텍처 설계 능력과 성능 튜닝 능력을 향상시키는 안드로이드 Deep-Dive 과정

  • 코스 이미지
    Flutter

    네이티브와 의존성 관리까지 깊이 있는 크로스 플랫폼 개발자로 성장하는 과정

  • 코스 이미지
    React Native

    네이티브와 의존성 관리까지 깊이 있는 크로스 플랫폼 개발자로 성장하는 과정

  • 코스 이미지
    Devops

    대규모 서비스를 지탱할 수 있는 데브옵스 엔지니어로 성장하는 과정

  • 코스 이미지
    ML Engineering

    머신러닝과 엔지니어링 자체에 대한 탄탄한 이해도를 갖추는 머신러닝 엔지니어 성장 과정

  • 코스 이미지
    Data Engineering

    확장성 있는 데이터 처리 및 수급이 가능하도록 시스템을 설계 하고 운영할 수 있는 능력을 갖추는 데이터 엔지니어 성장 과정

  • 코스 이미지
    Game Server

    대규모 라이브 게임을 운영할 수 있는 처리 능력과 아키텍처 설계 능력을 갖추는 게임 서버 개발자 성장 과정

  • 코스 이미지
    Game Client

    대규모 라이브 게임 그래픽 처리 성능과 게임 자체 성능을 높힐 수 있는 능력을 갖추는 게임 클라이언트 개발자 성장 과정

F-Lab
소개채용멘토 지원
facebook
linkedIn
youtube
instagram
logo
(주)에프랩앤컴퍼니 | 사업자등록번호 : 534-85-01979 | 대표자명 : 박중수 | 전화번호 : 1600-8776 | 제휴 문의 : info@f-lab.kr | 주소 : 서울특별시 강남구 테헤란로63길 12, 438호 | copyright © F-Lab & Company 2025