BlueNyang
[Kotlin Spring Boot] 데이터 정합성을 지키는 코틀린의 스마트함과 JPA Converter의 함정
Spring

[Kotlin Spring Boot] 데이터 정합성을 지키는 코틀린의 스마트함과 JPA Converter의 함정

BlueNyangBlueNyang
·
·
약 3분
·
# attribute-converter# data-integrity# rdbms-null-handling# safe-call-operator

지난 글에서는 코틀린의 문법을 활용해 예외 처리 코드를 다이어트하는 방법을 살펴봤습니다. 이번에는 DB와 맞닿아 있는 영속성 계층(Persistence Layer), 그중에서도 JPA AttributeConverter를 마이그레이션하며 겪었던 아찔한 경험을 이야기해 볼까 합니다.

Note
해당 글은 Kotlin Spring Boot 3.x 및 JDK 21 환경을 기준으로 작성되었습니다.

1. 엘비스 연산자

코틀린을 쓰다 보면 자바의 널(Null) 처리를 엘비스 연산자(?:)로 기계적으로 치환하고 싶은 유혹에 빠지기 쉽습니다. 애플리케이션 레벨에서는 에러 없이 잘 돌아가니까요. 하지만 이 짧은 생각이 데이터베이스 레벨에서는 정합성을 깨뜨리는 원인이 될 수 있습니다.

1.1. 자바 코드의 기계적 변환이 낳은 실수

제가 프로젝트를 진행하며 작성했던 코드 중, 회원 가입이나 게시글 작성 시 입력받은 데이터의 양 끝 공백을 제거하여 DB에 저장하기 위한 TrimmedStringConverter를 예제로 들어보겠습니다. 자바에서는 if (attribute == null) return null;과 같이 방어 로직을 짰지만, 코틀린으로 마이그레이션하면서 코드를 줄이고 싶은 욕심에 아래와 같이 엘비스 연산자를 사용하는 실수를 범했습니다.

kotlin
@Converter
class TrimmedStringConverter : AttributeConverter<String, String> {
    override fun convertToDatabaseColumn(attribute: String?): String {
        // 실수: NullPointerException을 막기 위해 null을 빈 문자열("")로 치환해버림
        return attribute?.trim { it <= ' ' } ?: ""
    }

    override fun convertToEntityAttribute(dbData: String?): String {
        return dbData?.trim { it <= ' ' } ?: ""
    }
}

언뜻 보면 NullPointerException을 완벽하게 방어하고 공백까지 제거하는 우아한 코드처럼 보입니다. 하지만 이 코드는 데이터베이스 생태계의 아주 중요한 규칙을 위반하고 있습니다.

1.2. 데이터베이스가 바라보는 Null과 빈 문자열의 차이

PostgreSQL, MySQL 등을 비롯한 대부분의 관계형 데이터베이스에서 NULL("아직 값이 할당되지 않음")과 빈 문자열 ""("값이 비어있는 상태로 할당됨")은 완전히 다른 데이터로 취급됩니다.

위 코드처럼 엘비스 연산자로 인해 엔티티의 null 필드가 DB에 ""로 강제 저장된다면 어떻게 될까요? 나중에 쿼리에서 WHERE attribute IS NULL 조건으로 검색하는 로직이 전부 고장 나게 됩니다. 또한, 해당 컬럼에 UNIQUE 제약 조건이 걸려 있다면, 빈 문자열이 중복 저장되면서 Constraint Violation 예외를 마주하는 당황스러운 상황이 벌어지게 됩니다. 이 때문에 당장 서버가 죽는 대참사까지는 아니더라도, 정상적인 서비스 이용을 막는 크리티컬한 버그로 이어질 수 있습니다.

2. 코틀린의 타입 시스템과 안전한 호출 연산자

코틀린은 이러한 문제를 개발자의 억지스러운 꼼수 (쌀먹) 대신, 언어 차원에서 제공하는 타입 시스템과 안전한 연산자를 활용해 자연스럽게 해결하도록 유도합니다.

2.1. 제네릭 타입의 Null 명시 (String? vs String)

가장 먼저 수정해야 할 부분은 인터페이스의 제네릭 타입입니다. DB 컬럼과 엔티티 속성 모두 본질적으로 null을 허용하는 데이터라면, 이를 감추지 말고 코틀린의 Nullable 타입(String?)으로 명확히 선언해야 합니다.

kotlin
// 수정 전: AttributeConverter<String, String>
// 수정 후: AttributeConverter<String?, String?>

이렇게 타입을 정의하는 것만으로도 컴파일러는 이 컨버터가 null을 입력받고 null을 반환할 수 있다는 사실을 인지하고, 후속 로직에서 발생할 수 있는 타입 불일치를 사전에 차단합니다.

2.2. 안전한 호출 연산자(?.)와 내장 trim()

이제 자바에서 사용하던 if-else 블록이나 위험했던 엘비스 연산자를 모두 걷어낼 차례입니다. 코틀린의 안전한 호출 연산자(?.)는 객체가 null이 아닐 때만 뒤의 함수를 실행하고, null이라면 전체 표현식의 결과를 자연스럽게 null로 반환합니다. 여기에 코틀린이 기본으로 제공하는 깔끔한 .trim() 확장 함수를 결합하면 코드가 한결 단정해집니다.

kotlin
@Converter
class TrimmedStringConverter : AttributeConverter<String?, String?> {

    // attribute가 null이면 null 반환, 아니면 양옆 공백 제거 후 반환
    override fun convertToDatabaseColumn(attribute: String?) = attribute?.trim()

    // dbData가 null이면 null 반환, 아니면 양옆 공백 제거 후 반환
    override fun convertToEntityAttribute(dbData: String?) = dbData?.trim()
}

이 코드는 자바 원본 코드가 가졌던 'Null-safe'와 'Trim'이라는 두 가지 의도를 100% 충족시키면서도, 데이터베이스의 NULL 동기화를 완벽하게 보장합니다. 단 두 줄의 코드로 영속성 계층의 정합성이 지켜집니다.

3. 마무리하며 - 꼼수 대신 언어 믿기

JPA 컨버터를 마이그레이션하면서 겪은 이 짧은 삽질은, 코틀린을 단순히 '자바 코드를 줄여주는 도구'로만 접근해서는 안 된다는 것을 깨닫게 해주었습니다.

자바에서 발생할 수 있는 NullPointerException을 막겠다고 엘비스 연산자로 빈 문자열("")을 강제 할당하는 것은, DB 생태계에서 NULL이 가지는 의미를 훼손하는 꼼수에 불과했습니다.

결국 올바른 해결책은 코틀린의 타입 시스템을 그대로 믿고 활용하는 것이었습니다. '값이 없을 수도 있다'는 사실을 제네릭 타입(String?)으로 명확히 인지하고, 안전한 호출 연산자(?.)를 통해 그 상태를 데이터베이스까지 온전히 전달하는 것. 단 두 줄의 코드로 영속성 계층의 정합성을 지켜내는 이 과정에서 Nullable과 Empty String의 차이, 데이터베이스 설계에 대한 단순하고도 기본적인 개념을 상기시킬 수 있었던 것 같습니다.

BlueNyang
작성자BlueNyang
라이선스
CC BY NC
BlueNyang

BlueNyang

BlueNyang의 개발 log

카테고리

  • Development
  • Framework
  • Language
  • Dev Tools
  • DevOps & Infra
  • Studies

페이지

© 2026 BlueNyang. All rights reserved.

Made with Nuxt.js and Directus