개요
Spring Framework 7.0.0-M2 버전이 2025년 2월 13일에 공개 되었고(https://spring.io/blog/2025/02/13/spring-framework-7-0-0-M2-available-now) M2버전에서는 null safety(null 안정성 : null로부터 안전한 코드를 작성하는 것) 지원을 위해 JSpecify을 사용하도록 적용이 되었다.
Spring Framework GIT에 소스 코드를 보면 JSpecity를 사용하여 null처리를 하도록 적용되어 있다.
@NullMarked
package org.springframework.context;
import org.jspecify.annotations.NullMarked;
이전 포스팅에서는 JSpecify에 Nullness User Guide에(https://blog.igooo.org/166) 대하여 알아보았고, 실제로 JSpecify를 사용하여 Java 프로젝트에서 null safety 한 코드를 작성하는 방법에 대하여 알아본다.
문서를 작성할수록 왜 요즘 개발자들이 kotlin처럼 Null safty를(https://kotlinlang.org/docs/null-safety.html) 지원하는 언어를 점점 더 많이 사용되는지 생각하게 되었다. 자바도 빠르게 언어 레벨에서 처리되기를 희망한다.
Getting started
예제로 사용할 프로젝트는 Gradle을 사용하어 프로젝트를 생성하고, JSpecify 의존성을 추가여 어노테이션을 사용하는 방법에 대하여 알아본다. (상세한 사용법은 이전 포스팅을 참조한다. https://blog.igooo.org/166)
Dependency 추가
JSpecify 의존성을 추가한다. (참고 : https://jspecify.dev/docs/using/)
dependencies {
// add JSpecify
implementation 'org.jspecify:jspecify:1.0.0'
testImplementation platform('org.junit:junit-bom:5.10.0')
testImplementation 'org.junit.jupiter:junit-jupiter'
}
@NullMarked Annotation
JSpecify에는 null 여부를 지정하기 위한 @Nullable, @NonNull과 같은 어노테이션을 제공한다. 하지만 모든 클래스와 메서드, 변수에 어노테이션을 적용하는 일은 생각보다 귀찮은 작업이다. 그래서 JSpecify에서는 @NullMarked 어노테이션을 제공하며, @NullMarked는 선언된 위치에(모듈, 패키지, 클래스, ...) 해당 범위 파일은 모두 @NonNull로 처리된다.
@Documented
@Target({ElementType.MODULE, ElementType.PACKAGE, ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.RUNTIME)
public @interface NullMarked {
}
Example
nullness 패키지
nullness라는 패키지를 생성하고, package-info.java를 생성하여 @NullMarked 어노테이션을 적용한다.
@NullMarked
package org.igooo.jspecify.nullness;
import org.jspecify.annotations.NullMarked;
nullness 패키지에 Calculator 클래스를 생성하고 add(), minus() 메서드를 아래와 같이 추가한다.
- add() 메서드는 별도의 어노테이션을 추가하지 않는다.
- minus() 메서드는 파라미터 b에 @Nullable 어노테이션을 추가한다.
package org.igooo.jspecify.nullness;
import org.jspecify.annotations.Nullable;
public class Calculator {
public Integer add(Integer a, Integer b) {
return a + b;
}
// parameter b is nullable
public Integer minus(Integer a, @Nullable Integer b) {
return a + (b == null ? 0 : b);
}
}
Calculator 클래스에 대한 테스트 코드를 생성한다.
- add() 메서드는 파라미터에 정상적인 Integer값을 설정하여 테스트에 성공한다.
- addWithNull() 메서드는 첫 번째 파라미터에 null을 설정하여 테스트에 실패한다.
- minus() 메서드는 두 번째 파라미터에 null을 설정하지만, 두 번째 파라미터는 @Nullable 하기 때문에 테스트에 성공한다.
class CalculatorTests {
@Test
void add() {
var calculator = new Calculator();
assertEquals(3, calculator.add(1, 2));
}
@Test
void addWithNull() {
var calculator = new Calculator();
// test fail
assertEquals(2, calculator.add(null, 2));
}
@Test
void minus() {
var calculator = new Calculator();
assertEquals(2, calculator.minus(2, null));
}
}
IntelliJ IDEA를 사용여 테스트 코드에 warnings 내용을 확인해 보면 아래와 같다.
addWithNull() 메서드는 파라미터로 null을 설정하고 있어서 오류가 발생하다는 내용으로 null 체크를 해주고 있다.
nullness 패키지에 @NullMarked 어노테이션을 사용하면 패키지에 속한 클래스는 자동으로 @NonNull 처리가 되는 것을 확인할 수 있다..
Sub Package
추가적으로 위에서 테스트했던 nullness 패키지 하위로 sub라는 패키지를 만들어서 동일하게 테스트를 진행해 보자.
sub라는 이름의 패키지를 nullness 하위에 추가하고, SubCalculator라는 클래스를 만들어 준다.
Calculator 클래스와는 다르게 minus() 메서드에는 @NonNull 어노테이션을 추가한다.
// nullness 하위에 sub 패키지 생성
// package-info.java는 생성하지 않음
package org.igooo.jspecify.nullness.sub;
import org.jspecify.annotations.NonNull;
public class SubCalculator {
public Integer add(Integer a, Integer b) {
return a + b;
}
public Integer minus(@NonNull Integer a, @NonNull Integer b) {
return a + b;
}
}
SubCalculator에 대한 테스트 코드를 작성해 준다.
- add() 메서드는 파라미터에 정상적인 Integer값을 설정하여 테스트에 성공한다.
- addWithNull() 메서드는 첫 번째 파라미터에 null을 전송하고 있어서 오류가 발생한다. (IDE에서는 null 체크를 해주지 않는다.)
- minus() 메서드는 두 번째 파라미터에 null을 입력하여 테스트에 실패한다.
class SubCalculatorTests {
@Test
void add() {
var subCalculator = new SubCalculator();
assertEquals(3, subCalculator.add(1, 2));
}
@Test
void addWithNull() {
var subCalculator = new SubCalculator();
assertEquals(2, subCalculator.add(null, 2));
}
@Test
void minus() {
var subCalculator = new SubCalculator();
assertEquals(2, subCalculator.minus(2, null));
}
}
IntelliJ IDEA를 사용여 테스트 코드에 warnings 내용을 확인해 보면 아래와 같다.
minus() 메서드는 파라미터에 null을 설정하고 있어서 오류가 발생하다는 내용으로 null 체크를 해주고 있다.
addWithNull() 메서드도 null을 설정하여 테스트에 실패하지만, null 체크에 대한 어노테이션이 지정되지 않아서 warnings에는 표시되지 않는다. (실제 서비스 코드라면 null로 호출되는 경우 NullPointerException이 발생한다.)
sub 패키지 상위에 nullness 패키지에서 @NullMarked 어노테이션을 적용했지만 하위 패키지까지는 적용되지 않는다.
그러므로 SubCalculator 클래스의 minus() 메서드의 @NonNull 어노테이션에 대해서만 null 체크가 가능하다.
요약
null에 대해서는 Tony Hoare가 The Billion-Dollar Mistake라고(https://en.wikipedia.org/wiki/Null_pointer#History) 말한 적이 있다. 실제로도 Java로 개발된 서비스를 담당하는 개발자라면 서비스 개발 중 NullPointerException은 한 번쯤은 경험해 봤을 거다. 요즘은 Kotlin은 사용한 개발이 많아졌지만, 아직 Java로 개발된 서비스를 운영 중이라면 JSpecify을 사용하여 null-safety 한 개발을 검토해 볼 만하다.
전체 소스코드는 GitHub에서 확인 가능하다.
'dev > java' 카테고리의 다른 글
JDK 24 새로운 기능 (0) | 2025.03.30 |
---|---|
JSpecify Nullness User Guide (0) | 2025.02.19 |
Win10 SDKMAN으로 JAVA 설치하기 (0) | 2025.01.14 |
Building a SpringBoot Monorepo with Gradle (2) | 2024.11.06 |
Java 23 : Structured Concurrency (0) | 2024.09.28 |