Spring Boot NullPointerException 원인 분석과 예방 패턴
Spring Boot 개발에서 자주 발생하는 NPE 원인 7가지와 Optional, 방어적 코딩, 테스트로 근본적으로 예방하는 방법.
TestForge Team ·
NPE가 위험한 이유
java.lang.NullPointerException: Cannot invoke
"com.example.UserService.getUser(Long)" because "this.userService" is null
스택 트레이스만으로는 왜 null인지 파악하기 어렵습니다.
Java 14+의 Helpful NPE는 메시지가 상세해졌지만, 예방이 최선입니다.
원인 1: @Autowired 필드 주입 + 생성자 직접 호출
// 위험
@Service
public class OrderService {
@Autowired
private UserService userService; // 필드 주입
// 테스트에서 new OrderService()로 생성하면 userService = null
}
// 안전: 생성자 주입
@Service
@RequiredArgsConstructor // Lombok
public class OrderService {
private final UserService userService; // null 불가능
}
생성자 주입을 사용하면:
- 의존성이 명시적
- 테스트에서
new OrderService(mockUserService)가능 final필드 → NPE 원천 차단
원인 2: JPA Optional 무시
// 위험
User user = userRepository.findById(id).get(); // 없으면 NoSuchElementException
// 또는
User user = userRepository.findByEmail(email); // null 반환 가능
// 안전
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found: " + id));
// 또는 null safe 처리
Optional<User> optUser = userRepository.findByEmail(email);
optUser.ifPresent(u -> sendWelcomeEmail(u.getEmail()));
원인 3: Map.get() 결과 미확인
Map<String, Config> configMap = getConfigMap();
// 위험
String value = configMap.get("key").getValue(); // null.getValue() → NPE
// 안전
Config config = configMap.get("key");
if (config != null) {
String value = config.getValue();
}
// 또는
String value = Optional.ofNullable(configMap.get("key"))
.map(Config::getValue)
.orElse("default");
// 또는
Config config = configMap.getOrDefault("key", Config.defaultConfig());
원인 4: 외부 API 응답 필드 무시
// 위험
ExternalResponse response = apiClient.call();
String name = response.getData().getUser().getName(); // 연쇄 NPE
// 안전
String name = Optional.ofNullable(response)
.map(ExternalResponse::getData)
.map(Data::getUser)
.map(User::getName)
.orElse("Unknown");
원인 5: 컬렉션 반환값
// 위험: null 반환
public List<Order> getOrders(Long userId) {
if (!userExists(userId)) return null; // 호출자가 null 체크 안 하면 NPE
// ...
}
// 안전: 빈 컬렉션 반환
public List<Order> getOrders(Long userId) {
if (!userExists(userId)) return Collections.emptyList();
// ...
}
규칙: 컬렉션을 반환하는 메서드는 절대 null을 반환하지 않는다.
원인 6: @Value 주입 실패
@Value("${app.secret}")
private String secret;
// 프로퍼티가 없으면 BeanCreationException (null이 아닌 예외)
// 하지만 기본값 설정 없으면 운영 환경에서 문제
// 안전: 기본값 또는 필수 표시
@Value("${app.secret:}")
private String secret; // 없으면 빈 문자열
// 필수 값은 @NotBlank로 검증
@NotBlank
@Value("${app.secret}")
private String secret;
원인 7: @RequestParam / @PathVariable 처리
// 위험
@GetMapping("/users")
public ResponseEntity<?> getUsers(@RequestParam String email) {
User user = userService.findByEmail(email);
return ResponseEntity.ok(user.toDto()); // user가 null이면 NPE
}
// 안전
@GetMapping("/users")
public ResponseEntity<?> getUsers(@RequestParam String email) {
return userService.findByEmail(email)
.map(user -> ResponseEntity.ok(user.toDto()))
.orElse(ResponseEntity.notFound().build());
}
예방: Null 분석 도구
<!-- SpotBugs + Find Bugs -->
<dependency>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-annotations</artifactId>
<scope>provided</artifactId>
</dependency>
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class UserService {
@Nonnull
public User getUser(@Nonnull Long id) { ... }
@Nullable
public User findByEmail(@Nonnull String email) { ... }
}
IDE(IntelliJ)가 @Nullable 반환값을 null 체크 없이 사용하면 경고를 표시합니다.
빠른 예방 규칙
- 생성자 주입 사용 (
@RequiredArgsConstructor) - Optional.orElseThrow / orElse 적극 활용
- 컬렉션 반환 시 null 대신 빈 컬렉션
- 외부 데이터는 Optional 체인으로 처리
- @Nonnull / @Nullable 어노테이션으로 의도 명시
- 단위 테스트에서 null 케이스 반드시 포함
// 테스트 예시
@Test
void getUser_notFound_throwsException() {
when(userRepository.findById(999L)).thenReturn(Optional.empty());
assertThatThrownBy(() -> userService.getUser(999L))
.isInstanceOf(UserNotFoundException.class)
.hasMessageContaining("999");
}