Java 애플리케이션 개발에서 NullPointerException(NPE)은 오랫동안 골칫거리였다. 이를 해결하기 위해 다양한 시도가 있었으나, 완벽한 표준은 부재했다.
최근 Spring Framework 7.0과 JSpecify 1.0의 결합은 Java 생태계의 null 안전성을 근본적으로 개선하는 중요한 전환점이 되었다. 2025년 하반기 Spring Boot 4.0 출시 이후 Spring 포트폴리오 전반에 JSpecify가 도입됨에 따라, 실무 개발자들 역시 이 새로운 표준을 명확히 이해하고 적용해야 할 시점이다.
Null 처리를 위해 Java 8부터 도입된 Optional은 유용한 도구이지만 모든 null 문제를 해결하는 만능 키는 아니다.
첫째, 래퍼 객체를 생성하므로 런타임 오버헤드가 발생한다. 향후 Project Valhalla를 통해 값 타입 최적화가 이루어지면 개선될 여지가 있으나, 현재로서는 성능에 영향을 미친다.
둘째, 기존 API 시그니처를 변경해야 하므로 하위 호환성이 중요한 프로젝트에서는 사실상 마이그레이션이 불가능하다.
셋째, Optional은 애초에 메서드의 반환값 용도로 설계되었다. 이를 파라미터나 클래스 필드에 남용할 경우 코드와 API의 복잡도를 불필요하게 증가시킨다.
과거 Spring 프레임워크는 JSR-305 기반의 비공식 스펙을 활용하여 @Nullable 어노테이션을 지원했다. 그러나 이 방식은 명확한 라이선스가 없어 표준 의존성으로 사용하기에 부담이 컸다. 더욱 치명적인 단점은 제네릭 타입이나 배열 요소의 null 가능성을 제대로 표현할 수 없다는 것이었다.
이로 인해 정적 분석 도구마다 어노테이션의 의미를 다르게 해석하는 파편화 현상이 발생하여 일관된 널 안전성을 보장하기 어려웠다.
JSpecify는 단일 도구나 특정 벤더의 종속성에서 벗어나, Java 생태계 전반을 아우르는 명확한 문서를 작성하는 것을 목표로 출발했다. 사양에 대한 합의를 이루는 데만 5년이 걸렸을 정도로 신중하고 정교하게 설계되었다.
가장 큰 특징은 특정 IDE나 분석 도구에 종속되지 않는다는 점이다. JetBrains의 IntelliJ IDEA나 Uber의 NullAway 같은 도구 공급업체 및 플러그인 생태계가 동일한 사양을 바탕으로 일관된 검증 동작을 구현할 수 있도록 표준화된 지침을 제공한다.
Spring 코드베이스의 약 90%는 기본적으로 null을 허용하지 않는 non-null 구조로 이루어져 있다. 따라서 모든 필드나 반환값에 어노테이션을 일일이 붙이는 대신, 패키지 단위로 기본값을 non-null로 설정하고 예외적인 경우에만 @Nullable을 명시하는 방식이 적극적으로 권장된다.
package-info.java 파일을 활용하여 패키지 전체에 @NullMarked를 적용할 수 있다.
@NullMarked
package org.example;
import org.jspecify.annotations.NullMarked;
이렇게 설정하면 해당 패키지 내의 모든 타입은 기본적으로 non-null로 취급되며, null이 허용되어야 하는 곳에만 명시적으로 어노테이션을 추가한다.
package org.example;
import org.jspecify.annotations.Nullable;
interface TokenExtractor {
/**
* @param input the input to process
* @return the extracted token or null if not found
*/
@Nullable String extractToken(String input);
}
주의할 점은 package-info.java의 설정은 정확히 해당 패키지에만 적용되며 하위 패키지에는 상속되지 않는다는 것이다. 따라서 널 안전성을 확보하려는 프로젝트 내 모든 패키지에 각각 설정 파일을 두는 것이 권장된다.
JSpecify는 기존 방식에서 불가능했던 배열의 정교한 null 처리를 지원한다. 배열 자체가 null인 경우와 배열 내부의 요소가 null인 경우를 문법적으로 명확히 구분할 수 있다.
interface TokenExtractor {
// 배열 자체는 non-null이지만, 내부 요소는 null일 수 있음을 명시
String @Nullable[] extractTokens(String input);
}
IDE 수준의 단순 경고를 넘어 컴파일 타임에 NPE 발생 가능성을 원천 차단하려면, NullAway와 같은 정적 분석 도구를 빌드 파이프라인에 통합해야 한다. NullAway는 오류가 발생하기 쉬운 코드를 찾아내도록 돕는 Google의 Error Prone 기반 위에서 확장된 도구다.
Gradle을 사용하는 프로젝트의 경우 다음과 같이 빌드 스크립트를 구성하여 검증을 자동화할 수 있다.
plugins {
id 'java'
id 'net.ltgt.errorprone' version '4.1.0'
}
dependencies {
implementation 'org.jspecify:jspecify:1.0.0'
errorprone 'com.google.errorprone:error_prone_core:2.36.0'
errorprone 'com.uber.nullaway:nullaway:0.10.25'
}
tasks.withType(JavaCompile).configureEach {
options.errorprone {
// 불필요한 다른 Error Prone 검사는 비활성화
disableAllChecks = true
// NullMarked로 지정된 코드에서만 NullAway 활성화
option("NullAway:OnlyNullMarked", "true")
// 제네릭 및 배열 요소 추가 검사를 위한 JSpecify 모드 활성화
option("NullAway:JSpecifyMode", "true")
// 경고 수준을 빌드 에러로 격상
error("NullAway")
}
}
빌드 스크립트 작성이 번거롭다면 현재 Spring 프레임워크 내부에서도 사용 중인 Spring Gradle Nullability 플러그인(https://github.com/spring-gradle-plugins/nullability-plugin)을을) 도입하여 구성을 간소화할 수 있다.
위와 같이 설정하면 로직 내에서 @Nullable로 선언된 값을 별도의 null 체크 없이 사용하려 할 때 컴파일러가 빌드 에러를 발생시켜 잠재적인 결함을 운영 환경 배포 이전에 방지한다.
JSpecify는 파편화되어 있던 Java 생태계의 null 안전성을 통합하는 강력하고 실질적인 표준이다. 단순히 어노테이션을 추가하는 수준을 넘어, 도구 독립적인 사양 표준화와 강력한 빌드 타임 검증을 통해 프로덕션 환경의 안정성을 비약적으로 높여준다.
불필요한 Optional 남용을 지양하고, 패키지 단위의 @NullMarked 설정을 기반으로 코드 가독성을 확보하며, NullAway를 활용한 빌드 타임 검증 체계를 갖추는 것이 실무 적용의 핵심이다. 점진적인 마이그레이션 전략을 통해 코드베이스를 개선해 나간다면 NPE로 인한 런타임 장애를 극적으로 줄일 수 있을 것이다.