오늘은 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 어노테이션에 대해서 알아봤다. 물론 더 많은 기능이 있을지는 모른다. 필자가 알아본 부분은 여기까지이다. 더 많은 정보가 있다면 댓글로..
이상!