API 개발중 카테고리 ID를 입력하면 이를 바탕으로 해당 카테고리 내부의 카드를 페이징 조회하는 쿼리를 만들어야 했다. 이 때문에 categoryId 인자가 반드시 필요하다.
보통 NPE 문제를 막고 인자에 유연하게 동적으로 쿼리를 생성하기 위해 Querydsl에서 다음과 같은 BooleanExpression을 작성한다.
private BooleanExpression eqCategoryId(Id categoryId) {
return categoryId == null ? null : card.categoryId.eq(categoryId);
}
해당 코드의 장점은 유연하고 동적으로 처리 가능한 쿼리를 작성하는데 도움을 준다는 것이다. 그러나 내 케이스의 경우엔 반드시 CategoryId의 입력을 강제해야 한다. 즉 인자의 입력이 절대로 null여서는 안된다는 것이다.
그래서 Null을 체크해야 하는 로직을 최상단에 구현해야 했다. (npe 결과를 기다리는 것이 아니라 미리 체크해서 디버깅을 용이하게 하고 실패지점을 명확히 한다)
null을 체크하는 방법에는 크게 3가지가 있다.
가장 정석적인 방법이다 메서드 Body 최상단에 해당 인자에 대한 null 체크 코드를 짜는 것이다.
@Override
public CardCursorPageWithCategory findCardByCategoryIdUsingCursorPaging(
int pageSize,
Id lastCardId,
Id categoryId,
CardSearchOption cardSearchOption
) {
if (categoryId == null) {
throw new IllegalArgumentException("categoryId should be not null");
}
// ,,,,,,.......
}
본문에 코드를 써야 하지만, 가장 직관적인 코드라 할 수 있겠다.
gradle
dependencies {
implementation 'com.google.guava:guava:30.1.1-jre'
}
의존성을 등록하고 사용하면 된다.
사용 방식은 사실 1번의 직접 구현과 의도는 같고, 조금 더 짧게 사용해서 메인 로직에 집중할 수 있는 의도로 사용한다.
Preconditions.checkNotNull(name, "Name must not be null");
굉장히 간단하게 코드를 사용함을 알 수 있다.
우선 Lombok 의존성을 등록한다
// lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok:'
testAnnotationProcessor 'org.projectlombok:lombok'
Lombok 의존성을 등록 이후에는 다음과 같이 사용하면 된다.
@Override
public CardCursorPageWithCategory findCardByCategoryIdUsingCursorPaging(
int pageSize,
Id lastCardId,
@NonNull Id categoryId,
CardSearchOption cardSearchOption
) {
// ,,,,,,.......
}
이제 build -> classes -> 저장된 패키지 -> 파일 로 들어가면 다음과 같이 코드가 생성되었음을 알 수 있다.
기본은 NullPointerException을 던진다. 위의 코드같이 IllegalArgumentException을 던지고 싶다면 lombok.config를 프로젝트 최상단에 생성하고 다음과 같은 코드를 써준다.
config.stopbubbling=true
lombok.nonnull.exceptiontype=IllegalArgumentException
stopbubbling=true는 lombok 스스로 예외처리를 수행하는 것을 방지하는 역할을 한다. 이를 개발자에게 위임함으로써 조금 더 안정적인 코드를 짤 수 있게 해준다.
exceptiontype은 @NonNull이 던질 exceptiontype을 설정하는 것이다.
1번, 2번, 3번 모두 실패의 원자성을 보장한다는 점에서 다 매력적인 선택지가 될 수 있다. 그러나 나는 3번의 @NonNull을 선택했다. 그 이유는 다음과 같다.
1번과 2번의 경우 null check 코드를 메서드 본문에 작성해야 한다. 그러나 Lombok의 @NonNull을 사용하면 Annotation Processor를 활용해 컴파일 단계에서 빌드시 1번과 같은 validation 코드를 자동으로 생성해준다. 실제 소스 코드에서는 메서드 본문에는 Main Logic만 있고 파라미터에는 @NonNull 어노테이션만 붙임으로써 해당 인자가 NonNull임을 쉽게 알 수 있다.
그리고 이 방법은 실제 이펙티브 자바에서 추천하는 방법이기도 하다. 1번, 2번 코드는 실제 코드를 들여다보기 전까지 categoryId가 null을 허용하는지 여부를 알 수 있는 방법이 없다. 오직 메서드의 이름을 보고 유추해야 할 뿐이다. 이러한 명명패턴을 활용해야 한다면 이펙티브 자바에서는 마커 어노테이션을 활용해서 가독성을 높이는 방법을 추천한다.
인텔리제이에서는 똑똑하게도 컴파일러 분석 외에도 자체적인 정적 분석 도구를 지원한다. inspection의 probable bugs가 그 예이다.
Intellij에서 @NonNull 어노테이션이 붙은 메서드의 인자에 null을 직접 사용해보면 IDE가 분석후 다음과 같은 경고를 보낸다.
인텔리제이의 정적 분석 도구를 통해 null 입력에 대한 오류를 사전에 방지할 수 있다.
만약 warning이 아니라 error를 통해 이러한 null 실수 입력을 강제로 방지하고 싶다면 다음과 같이 설정하면 된다.
File -> settings -> editor -> inspections -> java -> Probable bugs -> Constant condition & expections
해당 경로에서 Security 설정을 Warnings에서 Error로 설정해주면 된다.
그럼 위의 코드와 같이 null입력에 대한 위험성을 줄일 수 있다.
만약 Querydsl과 같은 코드를 활용해서 jpaRepository의 구현체 코드를 작성한다고 하면 @NonNull을 그냥 사용하면 NPE 에러가 뜨지만 만약 IllegalArgumentException을 호출하도록 설정한다고 하면 그대로 에러가 뜨지 않는다. 해당 에러를 감싼 InvalidDataAccessApiUsageException이 발생한다.
Spring Data JPA의 Repository 구현체에서 IllegalArgumentException을 직접 노출시키면, Spring Data JPA는 이 예외를 InvalidDataAccessApiUsageException으로 감싸서 노출시킨다. 이는 Spring Data JPA가 제공하는 레이어에서 발생하는 예외를 통일된 방식으로 처리하기 위해 사용되기 때문이다.
모두 좋은 방법이지만 나는 Lombok의 @NonNull 어노테이션이 main과 validation 로직을 분리하고 짧은 코드로 가독성을 높일 수 있으며 Intellij의 정적 분석 도구를 활용할 수 있다는 점이 굉장히 매력적으로 다가왔다. 이를 통해 NPE의 위험성을 더 줄일 수 있기 때문이다. 하지만 다른 블로그 이야기를 들어보면 @NonNull을 추천하지 않는 곳도 있다. 자동으로 생성되는 코드의 테스트 커버리지를 채워야 하기 때문이라고 한다. 그리고 필드에 선언시 잘못 사용하면 동작하지 않는 문제점이 있다고 한다. 하지만 나는 이런 단점들 보다 장점이 명확하다고 생각해서 null check는 Lombok의 어노테이션을 쓸 것 같다.
잘 읽고갑니다.