pile·
백엔드·카카오 스타일 (지그재그)카카오 스타일 (지그재그)·

내가 만든 non-null 변수에서 NullPointerException이 발생할 리가 없어!

카카오스타일 전시 UX팀이 Kotlin에서 non-null로 선언한 변수에 null이 들어와 NPE가 난 사례를 파고든 글이다. 지그재그 특가의 알림 유도 UI가 일부 사용자에게 노출되지 않아 추적해보니 non-null 필드에서 예외가 나고 있었다. 원인이 Kotlin 버전, JSON 라이브러리, 타입 정의 세 갈래로 갈린다.

핵심 포인트
  • Kotlin에는 null과 non-null 구분이 있지만 기반인 JVM에는 없어 언어의 제약과 무관하게 내부 값이 null일 수 있다.
  • Gson은 JSON에 필드가 없어도 조용히 null을 넣어 객체를 만들고 그 값을 쓰는 시점에 NPE가 난다.
  • 1.4.20에서는 when 안에서 이 값을 쓸 때 예외가 났고 1.5로 올리자 재현되지 않았다.
  • 같은 상황에서 if는 통과하는데 when은 예외가 나 when이 if의 문법 설탕만은 아니라는 것이 드러났다.
  • Jackson과 kotlinx는 파싱 단계에서 필수 필드 누락을 예외로 잡아준다.
  • 근본 원인은 GraphQL 스키마에서 nullable인 필드를 Kotlin 클래스에서 non-null로 선언한 것이었다.
상세 정리
  • 증상: 알림 동의 상태 값이 특정 값일 때 알림 유도 UI를 띄우게 돼 있었는데 동의하지 않은 일부 사용자에게 UI가 안 떴다.
  • 진단: 해당 값을 반환하는 함수에서 NPE가 발생하고 있었다.
  • 재현 코드: user 객체는 있지만 그 안에 필드가 없는 JSON을 Gson으로 파싱하고 when으로 분기하는 최소 코드로 재현했다.
  • 재현 결과: 출력해보면 non-null로 선언한 필드에 null이 들어간 객체가 그대로 만들어지고, when 분기에서 String의 hashCode 호출로 NPE가 난다.
  • 오해 정정: user 자체가 null이어서 나는 게 아니다. 실제 환경에서 user가 null이면 로그를 남기게 해봤지만 아무것도 찍히지 않았다.
  • 원인 1 Kotlin 버전: 오래전에 만들어진 서비스라 1.4.20을 쓰고 있었고 1.5로 올리자 문제가 사라졌다.
  • when과 if의 차이: 같은 상황에서 when을 if로 바꾸면 예외가 나지 않아 구버전 when 동작에 특이한 부분이 있다고 짐작했다.
  • enum 실험: 필드 타입을 enum으로 바꾸면 최신 2.0.0에서도 if는 통과하고 when에서 ordinal 호출로 NPE가 난다.
  • 원인 2 JSON 라이브러리: Kotlin의 null 구분은 JVM에는 없으므로 파싱 결과에 null이 섞일 수 있다. TypeScript도 같은 이유로 파싱 후 타입 검증을 권한다.
  • Jackson의 동작: kotlinModule을 등록하면 non-nullable 파라미터에 값이 없다는 예외를 파싱 단계에서 던진다.
  • kotlinx의 동작: 필수 필드 누락을 MissingFieldException으로 잡는다.
  • kotlinx의 추가 차이: 필드를 nullable로 선언했을 때 Gson과 Jackson은 문제가 없었지만 kotlinx는 JSON에 그 키가 명시적으로 null로 들어 있어야만 정상 파싱했다.
  • 원인 3 타입 정의: GraphQL 스키마에서는 nullable인 필드였는데 Kotlin 클래스에서 non-null로 선언돼 있었다. GraphQL에 익숙하지 않던 시기의 실수로 본다.
  • 재발 방지: 최근에 쓴 GraphQL 처리 코드는 스키마에서 자동 생성한 클래스 정의를 쓰고 있어 같은 문제가 없다.
왜 읽나Kotlin이나 TypeScript의 타입 선언을 믿고 외부 JSON을 그대로 매핑하고 있다면, 파싱 라이브러리가 그 제약을 지켜주지 않는다는 사실을 실제 예외 메시지로 확인할 수 있다.
카카오 스타일 (지그재그)
카카오 스타일 (지그재그) 블로그
원문은 여기서 이어서 읽을 수 있어요
원문 읽기
읽음 (0)

이 글과 비슷한

  1. 백엔드·twilio-engTwilio Engineering·

    Programmable Messaging에서 Verify API로 마이그레이션하기

    Twilio의 Programmable Messaging API로 자체 OTP 솔루션을 운영하던 서비스가 Verify API로 전환하는 방법을 코드 예시와 함께 설명한다. Verify는 OTP 전송·검증을 위한 전용 API로, 전화번호 구매, 토큰 생성, DB 저장·만료 관리를 내부에서 처리해 개발자가 직접 구현할 코드를 크게 줄인다.

    요약 이어보기
    #authentication#twilio#sms+2
  2. 백엔드·포스타입포스타입·

    포스타입이 개인화 추천을 하는 방법 2부

    포스타입 백엔드 엔지니어가 벡터 기반 개인화 추천 시스템을 실제 운영하며 맞닥뜨린 성능 장애와 용량 문제를 해결한 과정을 담은 2부다. 수백만 개의 벡터 KNN 검색이 피크 시간대에 전체 Elasticsearch 검색 성능을 흔드는 문제부터 클러스터 OOM 사태까지, 쿼리 최적화와 인프라 분리 두 가지 경로로 근본 해결에 이른다.

    요약 이어보기
    #elasticsearch#vector-search#recommendation-system+2
  3. 백엔드·포스타입포스타입·

    포스타입이 개인화 추천을 하는 방법 1부

    포스타입이 태그 기반 추천의 한계를 극복하고 벡터 임베딩 기반 개인화 추천 시스템을 구축한 과정을 담은 1부다. 유사한 콘텐츠가 다른 용어를 쓰거나 동일한 태그가 전혀 다른 톤의 콘텐츠를 가리키는 문제를 임베딩 벡터로 해결하고, OpenSearch의 HNSW ANN 검색으로 수백만 벡터를 실시간 검색하는 시스템을 구축해 구매율 15% 향상을 달성했다.

    요약 이어보기
    #opensearch#vector-search#recommendation-system+2