자바 생태계에서 NPE(NullPointException)은 자주 마주치는 문제이다.
많은 개발자들이 이를 미연에 방지하고자 Null-safety를 강화하는 도구나 애노테이션을 활용했지만 표준화된 방식이 존재하지 않았다
그래서 stackoverflow에 이러한 글도 있다.
어떤 '@NotNull' 애노테이션을 사용해야 하는지 묻는 글이다.
JSR-305의 @Nonnull
, Jetbrain에 @NotNull
, lombok의 @NonNull
등 다양하다.
이처럼 Null-safety에 대한 다양한 애노테이션이 있지만 표준화가 되어 있지는 않다.
이러한 문제를 해결하기 위해 나온 것이 JSpecify이다.
스프링 7.0 부터 적용되고 Spring 내부에서 사용되는 스프링자체 null-safety 애노테이션은 사용되지 않을 예정이다.
그렇다면 JSpecify에 대해 더 자세히 살펴보자
JSpecify는 자바(Java) 생태계에서 널 가능성을 일관적이고 표준화된 방식으로 표시하고 검증하기 위해 탄생한 오픈소스다.
쉽게 말해 자바 코드에서 이 변수(또는 파라미터)는 널이 될 수 있다/없다
를 명확하게 표기하고 이를 도구(IDE, 정적 분석기 등)가 공통적으로 이해하고 체크할 수 있도록 표준 규약을 제시한다.
자바에서 가장 흔한 런타임 에러 중 하나가 NPE(NullPointException)이다.
이를 방지하고자 개발자들은 IDE의 널 체크 기능 또는 @Nullable
, @NonNull
같은 애노테이션을 사용해 왔다.
앞서 설명했듯이 자바 생태계에는 많은 널 관련 애노테이션들이 존재한다.
목적은 모두 Null 가능성을 명시하고 런타임 에러를 줄이자!
이지만 저마다 다른 패키지나 룰을 가지고 있다.
도구마다 호환성이 조금씩 다르기 때문에 문제가 발생할 수 있다. 이 문제를 해결하기 위한게 JSpecify다.
JSpecify는 크게 @Nullable
, @NonNull
, @NullMarked
, @NullUnmarked
라는 네 가지 키워드로 널 가능성을 표현한다.
널에 대한 지정 없음 상태
로 되돌리기이미 IntelliJ, Checker Framework, NullAway 등 다양한 도구들이 JSpecify를 지원하고 있다.
JSpecify를 사용하면 이 도구들이해당 변수·메서드가 널일 수 있는지 여부
를 더 명확히 판단해줄 수 있어, 개발 단계에서 NPE를 더 빠르고 정확히 파악할 수 있다.
@NullMarked
클래스 예시import org.jspecify.annotations.Nullable;
import org.jspecify.annotations.NullMarked;
@NullMarked
class Strings {
// @NullMarked로 선언된 클래스 내부에서는
// 별도의 Nullable/NonNull 표기가 없으면 모두 NonNull로 간주된다.
// 반환 타입에 @Nullable 명시 → null을 반환할 수 있음
static @Nullable String emptyToNull(String x) {
return x.isEmpty() ? null : x;
}
// 매개변수에 @Nullable 명시 → null을 파라미터로 받을 수 있음
static String nullToEmpty(@Nullable String x) {
return x == null ? "" : x;
}
void doSomething() {
// nullToEmpty(null)는 허용: null 인자를 받을 수 있는 @Nullable 파라미터
int length1 = nullToEmpty(null).length();
System.out.println("length1: " + length1);
// emptyToNull("")는 빈 문자열이면 null을 반환 → 반환값은 @Nullable
// 바로 .length() 호출 시 정적 분석기나 IDE가 NPE 가능성 경고를 줄 수 있음
int length2 = emptyToNull("").length(); // 잠재적 NPE
System.out.println("length2: " + length2);
}
}
@NullMarked
: 해당 클래스(또는 패키지, 메서드) 범위 안에서 별도 표시가 없는 모든 참조 타입은 NonNull
이라는 의미로 해석된다.@Nullable
: 메서드 파라미터나 반환값이 null일 수 있음을 명시한다.import org.jspecify.annotations.Nullable;
import org.jspecify.annotations.NullMarked;
@NullMarked
class LocalVariableExample {
void handleStrings(@Nullable String maybeNull, String definitelyNonNull) {
// @NullMarked 범위라서 'String definitelyNonNull'는 null이 될 수 없다고 간주
// @Nullable String maybeNull 은 null 가능성이 존재
String fromNullable = maybeNull; // IDE/정적 분석기: fromNullable은 잠재적 null
String fromNonNull = definitelyNonNull; // NonNull
// 로컬 변수 선언 시, root type에는 @Nullable/@NonNull을 직접 붙이지 않는 것이 권장됨
// (분석기가 대입되는 값을 보고 추론하기 때문)
String either = randomCondition() ? maybeNull : definitelyNonNull;
// either는 null 가능성이 있다고 분석될 수 있음
if (either != null) {
// 여기선 either가 null이 아님이 보장된 상태
System.out.println(either.toUpperCase());
}
}
private boolean randomCondition() {
return Math.random() > 0.5;
}
}
@Nullable
을 붙이는 대신 파라미터/필드에서 널 가능성을 명시해 주고import org.jspecify.annotations.Nullable;
import org.jspecify.annotations.NullMarked;
import java.util.ArrayList;
import java.util.List;
@NullMarked
public class Methods {
// <T> 메서드에서 @Nullable T를 반환할 수 있음
public static <T> @Nullable T firstOrNull(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
// <T extends @Nullable Object>로 선언하면,
// T가 @Nullable String 등 널이 가능한 타입으로도 대체될 수 있음
public static <T extends @Nullable Object> T firstOrDefault(List<T> list, T defaultValue) {
return list.isEmpty() ? defaultValue : list.get(0);
}
public static void exampleUsage() {
// 1) List<String> → firstOrNull은 @Nullable String 반환
List<String> nonNullList = List.of("A", "B");
@Nullable String first = firstOrNull(nonNullList); // possibly null
System.out.println("first: " + first);
// 2) List<@Nullable String>도 가능
List<@Nullable String> nullableList = new ArrayList<>();
nullableList.add(null);
nullableList.add("Hello");
// firstOrDefault에서 T는 @Nullable String이 될 수 있음
@Nullable String result = firstOrDefault(nullableList, null);
// result는 null일 수도 있고, "Hello"일 수도 있음
System.out.println("result: " + result);
}
}
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.NullUnmarked;
@NullMarked
public class MixedScopes {
// 이 클래스 전체는 NullMarked로 처리
// → 별도 표기가 없는 모든 참조 타입은 NonNull
public String hello(String name) {
// 여기서 'String name'은 null이 아니라고 가정
return "Hello, " + name;
}
@NullUnmarked
public String fetchValue(String key) {
// NullUnmarked 스코프에선 'String key'가 널인지 아닌지 지정되지 않음(unspecified)
// 정적 분석기가 완벽히 추론하기 어려울 수 있음
if (key == null) {
return "Got a null key!";
}
return "Value for: " + key;
}
}
JSpecify가 제시하는 널 애노테이션(@Nullable, @NonNull
)은 type-use 위치에서 적용할 수 있다.
이는 자바 8부터 도입된 Type Annotations”
개념을 활용하는 것으로, 어떤 타입에 @Nullable을 붙이느냐
에 따라 의미가 달라질 수 있다.
@Nullable String[]
@NullMarked
하에서 NonNull로 간주됨String @Nullable []
배열 객체 자체가 null일 수 있다
는 의미@Nullable String @Nullable []
이상으로 왜 JSpecify가 필요한지부터 어떤 특징을 가지는지 간략하게 살펴봤다.
앞으로 자바 생태계가 계속 성장·발전하면서, JSpecify는 널 안전성을 위한 사실상 표준이 될 가능성이 높다.
물론 jdk에서 Nonull/Nullable에 대한 지원 계획을 가지고 있다.
그래도 JSpecify은 널 안정성을 지원하는 JDK와 지원하지 않는 JDK 사이에서 연결 다리 역할을 할 수 있는 것은 명확한거 같다.
앞으로 관심있게 지켜보면 좋을 것 같다.
Spring 6.2의 Duration 포맷 지원 (오픈소스 기여까지!) (0) | 2024.12.16 |
---|---|
Spring Assert클래스는 왜 Supplier를 활용하고 있을까? (0) | 2024.06.05 |
Spring 프로젝트 컨트리뷰터 된 후기 (ThreadLocal.set(null) vs ThreadLocal.remove() ) (0) | 2024.05.24 |
분산락을 이용한 메시지 좋아요 기능 동시성 문제 해결 방안 (0) | 2024.05.16 |
Spring Retry를 활용한 메시지 좋아요 기능의 동시성 문제 해결 방안 (0) | 2024.04.11 |