오늘은 Spring5 부터 지원하는 jsr305 어노테이션에 대해서 알아보자. 많은 이야기는 아니지만 Spring 에서 이 어노테이션을 몇가지 기능을 지원해 주고 있다.

Spring에서 사용하는 Nullable, NonNull, NonNullApi 어노테이션은 jsr305의 메타 어노테이션을 사용한다. 실제 간단히 코드를 보자면 다음과 같다.

//...
import javax.annotation.Nonnull;
import javax.annotation.meta.TypeQualifierDefault;

//...
@Nonnull
@TypeQualifierDefault({ElementType.METHOD, ElementType.PARAMETER})
public @interface NonNullApi {
}

jsr305 어노테이션은 그냥 메타 어노테이션으로만 사용하고 있다. 하지만 Spring 에서는 몇가지 기능을 지원해주고 있으니 알아보도록 하자.

Controller

Spring web에서 흔히 파라미터로 받을 경우 사용할 수 있다. @RequestParam 어노테이션을 사용할 경우 required 속성의 기본값은 true이다. 그래서 name이라는 파라미터를 보내지 않을 경우 에러가 발생한다.

@GetMapping("/")
public String hello(@RequestParam String name) {
  return "hello " + name + "!";
}

만약 위와 같이 @RequestParam의 required 속성을 false로 하지 않을 경우 아래와 같이 에러가 발생한다.

http :8080

HTTP/1.1 400
Connection: close
Content-Type: application/json;charset=UTF-8
Date: Mon, 07 May 2018 12:38:23 GMT
Transfer-Encoding: chunked

{
    "error": "Bad Request",
    "message": "Required String parameter 'name' is not present",
    "path": "/",
    "status": 400,
    "timestamp": "2018-05-07T12:38:23.120+0000"
}

물론 required 속성을 false로 해도 되지만 Spring5 부터는 @Nullable 어노테이션을 사용해서 null을 허용해도 된다.

@GetMapping("/")
public String hello(@RequestParam @Nullable String name) {
  return "hello " + name + "!";
}

위와 같이 @Nullable 어노테이션을 사용했을 경우에는 아래와 같이 에러가 발생하지 않는다.

http :8080

HTTP/1.1 200
Content-Length: 11
Content-Type: text/plain;charset=UTF-8
Date: Mon, 07 May 2018 12:41:14 GMT

hello null!

Endpoint

위와 비슷한 동일한 맥략이다. 커스텀한 Endpoint를 만들 경우에 @Nullable 어노테이션을 사용할 수 있다.

@Endpoint(id = "hello")
@Component
public class HelloEndpoint {

  @ReadOperation
  public String hello(String name) {
    return "hello " + name + "!";
  }
}

만약 위와 같은 코드를 작성했을 경우 name이라는 파라미터를 보내지 않으면 위와 동일하게 에러가 발생한다.

http :8080/actuator/hello

HTTP/1.1 400
Connection: close
Content-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8
Date: Mon, 07 May 2018 12:44:03 GMT
Transfer-Encoding: chunked

{
    "error": "Bad Request",
    "message": "Missing parameters: name",
    "path": "/actuator/hello",
    "status": 400,
    "timestamp": "2018-05-07T12:44:03.471+0000"
}

하지만 여기에서도 @Nullable 어노테이션을 작성하여 null을 허용할 수 있다.

@Endpoint(id = "hello")
@Component
public class HelloEndpoint {

  @ReadOperation
  public String hello(@Nullable String name) {
    return "hello " + name + "!";
  }
}

다음과 같이 작성할 경우에는 파라미터를 보내지 않아도 에러가 발생하지 않는다.

http :8080/actuator/hello
HTTP/1.1 200
Content-Length: 11
Content-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8
Date: Mon, 07 May 2018 12:45:17 GMT

hello null!

Null injection

Spring5 에서는 null을 허용하는 주입을 @Nullable 어노테이션을 사용하여 주입하면 된다. 예전에는 @Autowired 어노테이션의 required 속성을 false로 하면 주입하는 Object가 null 이어도 에러가 발생하지 않고 null 그대로 주입한다. 또한 필자는 요즘에 주입받는 Object에 @Autowired를 잘 작성하지 않는다. Spring 4.3 부터는 생성자 혹은 빈의 디펜더시 받는 Object에 @Autowired 가 존재 하지 않아도 자동으로 Spring이 주입을 해주고 있어서 좋은 기능 같다.

public class PersonService {
  //nothing
}

@Bean
ApplicationRunner applicationRunner(@Nullable PersonService personService) {
  return args -> {
  };
}

//생성자
public Some(@Nullable PersonService personService) {
  this.personService = personService;
}

위와 같이 PersonService는 빈으로 등록되지 않은 Object이다. 그래서 만약 이 상태로 주입받으려 하면 PersonService 라는 빈이 존재하지 않아 에러가 발생한다. 하지만 이제부터는 @Nullable 어노테이션을 사용해서 null을 허용하면 null이 주입된다.

만약 위와 같이 사용한다면 null check 는 꼭 해줘야 할 것 같다.

Spring data

Spring data 프로젝트에서도 jsr305 어노테이션을 지원한다. Spring data에서 query method를 사용할 경우 파라미터와 리턴값에 위와 같은 어노테이션을 작성할 수 있다.

여기서 주의할 점은 해당 패키지안에 package-info.java 를 작성해줘야 한다. 이때 사용하는 어노테이션은 @NonNullApi 어노테이션이다.

@NonNullApi
package ml.wonwoo.springjsr305.domain;

import org.springframework.lang.NonNullApi;

위와 같이 작성한다면 기본적으로 파라미터 와 리턴값은 null이 아니어야 한다.

public interface PersonRepository extends JpaRepository<Person, Long> {

  Person findByName(String name);
}

만약 NonNullApi 사용하고 위와 같이 사용한다면 name에는 null이 될수 없고 반환값도 null이 될 수 없다. 만약 null을 넣거나 null을 리턴한다면 IllegalArgumentException exception이 발생한다. 한번 controller로 테스트를 해보자.

@GetMapping("/hello")
public Person helloName(String name) {
  return personRepository.findByName(name);
}
http :8080/hello

HTTP/1.1 500
Connection: close
Content-Type: application/json;charset=UTF-8
Date: Mon, 07 May 2018 13:09:16 GMT
Transfer-Encoding: chunked

{
    "error": "Internal Server Error",
    "message": "Parameter of type String at index 0 in PersonRepository.findByName must not be null!",
    "path": "/hello",
    "status": 500,
    "timestamp": "2018-05-07T13:09:16.024+0000"
}

위와 같이 호출할 경우 파라미터가 null이라고 에러가 발생한다. 이번에는 리턴값이 null인 것으로 호출해보자.

http :8080/hello name==foo

HTTP/1.1 500
Connection: close
Content-Type: application/json;charset=UTF-8
Date: Mon, 07 May 2018 13:11:29 GMT
Transfer-Encoding: chunked

{
    "error": "Internal Server Error",
    "message": "Result must not be null!",
    "path": "/hello",
    "status": 500,
    "timestamp": "2018-05-07T13:11:29.367+0000"
}

이번에는 리턴값이 null이라고 에러가 발생한다. 한번 제대로 된 값으로 호출해보자.

http :8080/hello name==wonwoo

HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Date: Mon, 07 May 2018 13:12:59 GMT
Transfer-Encoding: chunked

{
    "id": 1,
    "name": "wonwoo"
}

정상적으로 호출이된다. 만약 위와 다르게 null을 허용하고 싶다면 이때까지 알아본 @Nullable 어노테이션을 사용하면 된다. 이것은 파라미터와 메서드위에 작성할 수 있다.

public interface PersonRepository extends JpaRepository<Person, Long> {

  Person findByNameIsLike(@Nullable String name);

  @Nullable
  Person findByName(String name);
}

위와 같이 한다면 findByNameIsLike 파라미터는 null을 허용하고 즉 findByNameIsLike(null) 같이 메소드를 호출해도 된다. (하지만 해당 메서드는 null을 넣으면 에러가 발생한다. jsr305 때문이 아니라 쿼리 자체가 null을 허용하지 않는 듯 하다.)
또한 findByName 메서드는 파라미터에는 null을 작성하면 안되고 리턴값 자체를 null값 이어도 상관 없다.

오늘 이렇게 Spring 에서 지원해주는 jsr305 어노테이션에 대해서 알아봤다. 물론 더 많은 기능이 있을지는 모른다. 필자가 알아본 부분은 여기까지이다. 더 많은 정보가 있다면 댓글로..

이상!