Java는 굳이 설명할 필요도 없이, 유명하고 장점이 매~우 많은 언어이다.
또, 오래된 언어이지만 최근까지도 빠르면서도 안정적으로 새로운 버전들이 빠르게 릴리즈 되고 있다.
개인적으로도 아주 즐겁게 개발을 하고 있다.
그러나 But!
개인적으로 Java의 거지(?)같은 점 Top1이 있다고 생각하는데, 바로 Nullable한 타입 표현이 없는 것이다.
다양한 Null 표현 라이브러리가 있지만, 언어 레벨에서 지원하는 기능이 아니라서 서로 호환이 안되거나 특정 IDE에서는 잘 동작하지 않는 경우들이 있다.
언어 레벨에서 추가된 기능은 아니지만, 25년 11월 20일에 릴리즈된 Spring boot 4에는 JSpecify라는 Null 표현 라이브러리가 표준으로 채택이 되었다.
그렇다면, 이제부터 본론으로 들어가서 Spring boot에서 JSpecify를 사용하여 Nullable한 타입을 선언하는 방법을 알아보자.
JSpecify는 어노테이션들을 제공하며, 이 어노테이션들로 타입에 대해서 Nullable 표현을 명확히 할 수 있도록 도와준다.
@NullMarked@NullMarked
@RestController
@RequestMapping("/tests")
public class TestController {
// ...
}
모듈, 패키지, 타입, 메서드 등등 거의 모든 곳에 사용할 수 있고, 해당 어노테이션이 붙은 곳의 모든 타입들은 Null이 될 수 없는 것으로 간주된다.
또, 아래에서 자세히 설명하겠지만, NullAway 라이브러리와 조합하면 Null 체크가 제대로 되지 않을 경우 컴파일 단계에서 에러를 발생시킬수도 있게 된다.
@NullUnMarked@NullUnMarked
@RestController
@RequestMapping("/tests")
public class TestController {
// ...
}
모듈, 패키지, 타입, 메서드 등등 거의 모든 곳에 사용할 수 있고, 해당 어노테이션이 붙은 곳은 Null 체크 범위에서 제외한다. 즉, Java의 기본 상태와 동일하다.
예를 들어서, 특정 패키지에 @NullMarked가 적용되어 있는데 해당 패키지 하위에 특정 클래스에만 @NullMarked를 제외시키고 싶은 경우에 해당 클래스에 @NullUnMarked를 사용하면 되는 것이다.
@Nullable@NullMarked
@RestController
@RequestMapping("/tests")
public class TestController {
@GetMapping
public String test(
@RequestParam @Nullable String firstName,
@RequestParam String lastName
) {
if (firstName == null) {
// Null 처리
}
// ...
}
}
타입에 사용할 수 있으며, 어노테이션명에서도 알 수 있듯이 Nullable한 타입임을 의미하는 것이다.
@NonNull@NullMarked
@RestController
@RequestMapping("/tests")
public class TestController {
@GetMapping
public String test(
@RequestParam @NonNull String firstName,
@RequestParam String lastName
) {
// ...
}
}
타입에 사용할 수 있으며, 어노테이션명에서도 알 수 있듯이 Null이 될 수 없는 타입임을 의미하는 것이다.
💡 JSpecify뿐만 아니라 NullAway도 함께 사용하도록 하여, 컴파일 레벨에서 Null 체크가 되는 예제를 작성합니다.
plugins {
id 'java'
id 'org.springframework.boot' version '4.0.0'
id 'io.spring.dependency-management' version '1.1.7'
id("net.ltgt.errorprone") version "4.3.0" // ⭐️
}
group = 'test'
version = '1.0.0'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(25)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
errorprone("com.uber.nullaway:nullaway:0.12.12") // ⭐️
errorprone("com.google.errorprone:error_prone_core:2.45.0") // ⭐️
}
test {
useJUnitPlatform()
}
// ⭐️ 컴파일 시, NullMarked가 적용되어 있는 코드들에 대해서 Null처리가 제대로 되지 않은 경우 에러를 내도록 하는 설정
tasks.withType(JavaCompile).configureEach {
options.errorprone {
disableAllChecks = false
option("NullAway:OnlyNullMarked", "true")
error("NullAway")
}
}
스프링 부트와 JSpecify, NullAway를 사용하기 위한 build.gradle이고, 중요한 부분은 ⭐️로 표시해두었다.
@NullMarked
@RestController
@RequestMapping("/tests")
public class TestController {
@GetMapping
public String test(
@RequestParam @Nullable String firstName,
@RequestParam String lastName
) {
boolean firstNameTest = firstName.contains("test");
boolean lastNameTest = lastName.contains("test");
if (firstNameTest || lastNameTest) {
return "test";
}
var fullName = firstName + lastName;
System.out.println(fullName);
return fullName;
}
}
클래스에 @NullMarked를 붙였고, firstName 쿼리 파라미터에 @Nullable을 붙은 예제이다.

Nullable한 타입에 대해서 Null 처리가 제대로 되어 있지 않을 경우 IDE에서 Warning을 보여준다.

보통은 Warning이 있어도 서버는 실행되지만, NullAway 설정을 적용했기 때문에 서버 실행 시 컴파일 에러가 나는 것을 확인할 수 있다. 👍
개인적으로 언어 레벨에서의 지원이 아니라 아쉽긴 하지만, 스프링에서 표준으로 채택한 것이니 어쩌면 언어 레벨의 지원보다도 더 큰 임팩트를 줄 수도 있지 않을까 싶다. ㅎㅎ
아무튼 드디어 표준으로 채택된 Null 라이브러리가 생겨서 좋고, 어노테이션만 잘 사용해준다면 (NullAway를 곁들여줘야하긴 하지만) 컴파일 레벨에서 NPE를 예방할 수 있게 되어서 아주 좋은 것 같다.
사실, @NullMarked를 내가 적용하고자하는 곳에 하나 하나 붙여서 사용하는 방식이 살짝 번거롭긴 한데, 좀 더 간편하면서 합리적으로 사용할 수 있는 방법에 대해서는 좀 더 고민을 해봐야겠다.
어느덧 Java와 스프링으로 개발한지 3~4년정도가 되었는데, 개발을 하면 할수록 빠르면서도 안정적으로 생티계가 발전해나가는 모습이 매우 신기하고 이러한 작업들을 해나가는 오라클과 브로드컴과 오픈 소스 커뮤니티에 참여하는 많은 개발자 분들에게 존경심이 드는 것 같다. 뜬금없지만 존경을 표하며 짧은 글을 마치도록 하겠다. 🙏
p.s 개발을 할수록 스프링은 진짜 말도 안되게 잘 만들어진 생태계라는 생각이 많이 든다. 🤔