Spring Controller 리턴타입

오늘은 Spring 에서 지원해주는 Controller 리턴타입에 대해서 알아 보도록 하자. 저번에 Spring Controller 파라미터 타입을 작성했는데 나름 인기가 좋아서 리턴타입도 해보려고 한다. 이건 인기가 별로 없을 듯 한데..
어쨌든.. Spring 에서는 많은 리턴타입들을 제공해준다. 대부분 거의 다 알아볼텐데 (필자가 아는 것들은..) 빠진게 있거나 설명이 부족하다면 Spring 문서를 보는 것을 추천한다. 그럼 시작해보자.

String

필자가 가장 자주 사용하며 (뷰템플릿을 사용할 때) 가장 간단한 리턴 타입이다. String 에는 뷰네임을 지정해주면 된다.

@GetMapping("/string")
public String str(Model model) {
  model.addAttribute("user", DATA);
  return "index";
}

Model 안에 데이터를 담고 뷰명을 string 타입으로 작성하면 된다. 그럼 뷰에서는 다음과 같이 해당 코드를 작성하면 된다.

<!DOCTYPE html>
<html>
<body>

<h2>name</h2>
<h3>{{user.name}}</h3>
<h2>email</h2>
<h3>{{user.email}}</h3>

</body>
</html>

뷰템플릿은 mustache를 이용했다. 문법자체는 해당 뷰템플릿을 알아야 한다. 하지만 모델 자체를 받을 때는 비슷하니 다른 뷰템플릿도 동일할 듯 싶다.

ModelAndView

가장 대표적이며 필자도 예전에는 자주 사용한 클래스이다. (왜냐면 옛날 내 사수가 이걸 좋아했기에) 하지만 요즘은 거의 사용하지 않는다. 필자도 바로 위에 작성한 string 타입으로 작성한다.

@GetMapping("/modelview")
public ModelAndView modelview() {
  return new ModelAndView("index").addObject("user", DATA);
}

Model도 파라미터로 받을 필요 없이 ModelAndView 클래스에 addObject 메서드를 이용해서 해당하는 데이터를 담기만 하면 된다. String 타입과 비슷하다. 이것 역시 뷰에 작성할 부분은 동일하다.

//...
<h2>name</h2>
<h3>{{user.name}}</h3>
<h2>email</h2>
<h3>{{user.email}}</h3>
//...

void

Spring 에서는 void 타입도 리턴이 가능하다. void 타입인데 어떻게 view 명을 지정해줄 수 있을까? 딱히 view명을 지정해줄 방법이 보이지 않는데 말이다. Spring은 뷰명을 입력하지 않아도 기본적으로 해당 url을 이용해서 뷰네임을 결정한다. 실제 인터페이스는 RequestToViewNameTranslator 인터페이스이며 DefaultRequestToViewNameTranslator 구현체 하나만을 갖고 있다. 만약 기본적인 뷰명을 변경하고 싶다면 커스텀하게 구현하면 되지 않을까 싶다.

@GetMapping("/void")
public void void(Model model) {
  model.addAttribute("user", DATA);
}

void.html 혹은 그에 따른 뷰템플릿 양식으로 해당파일을 만들면 된다.

//...
<h2>name</h2>
<h3>{{user.name}}</h3>
<h2>email</h2>
<h3>{{user.email}}</h3>
//...

Model Object

여기서 말하는 Model은 Spring에서 제공해주는 Model 클래스가 아니다. 일반 우리가 자주 사용하는 모델 오브젝트를 이야기하는 것이다. Spring 에서는 모델 자체를 리턴해도 된다. 다음과 같이 말이다.

public class Hello {
  private String name;
  private String email;

//...

@GetMapping("/hello")
public Hello hello() {
  return new Hello("wonwoo", "wonwoo@test.com");
}

이 또한 역시 뷰네임을 지정해줄 수 없기에 void와 마찬가지로 RequestToViewNameTranslator 이용해서 뷰네임을 지정한다. 또한, 모델에 대한 명도 조금 다르다 앞에서는 모델명을 지정해주었지만 여기서는 해줄수 없다. (물론 해줄수 있는 방법은 있는데 여기서는 설명하지 않겠다.) 그래서 해당 클래스의 명으로 모델명이 지어진다.

//...
<h2>name</h2>
<h3>{{hello.name}}</h3>
<h2>email</h2>
<h3>{{hello.email}}</h3>
//...

앞에서는 user 라는 모델명을 명시했지만 여기서는 명시하지 않았기에 해당 클래스가 모델명이 된다. Hello으로 클래스로 작성했기에 hello란 모델명으로 작성하면 된다.

Map , 기타 Map(Spring)

모델 오브젝트와 동일하게 Map 형태도 리턴타입으로 가능하다. 하지만 모델 오브젝트와 조금 다른점은 개별로 모델이 등록된다는 것이다. Map이라는 클래스니까 map이라는 모델명이 나올 것 같지만 그렇지 않다.

@GetMapping("/map")
public Map<String, String> map() {
  return DATA;
}

만약 위와 같이 작성했다면 우리는 다음과 같이 뷰를 작성해야 된다.

//...
<h2>name</h2>
<h3>{{name}}</h3>
<h2>email</h2>
<h3>{{email}}</h3>
//...

하지만 그닥 추천하지 않는다고 한다. 그래서 더이상 설명은 생략한다.

@ResponseBody

이것 역시 자주 사용하기에 대부분 다 알 것이라고 생각한다. SPA로 개발을 하거나 ajax로 개발을 할 떄 유용한 어노테이션이다. Http 본문자체를 리턴하며 xml 혹은 json 등 여러 메시지 컨버터를 Spring에서 지원하니 그건 따로 살펴봐야 된다.

@GetMapping("/body")
@ResponseBody
public String body() {
  return "<h2>name</h2>\n" +
      "<h3>wonwoo</h3>\n" +
      "<h2>email</h2>\n" +
      "<h3>wonwoo@test.com</h3>";
}

하지만 요즘은 @ResponseBody 어노테이션을 직접사용하지 않고 @Controller 어노테이션과 @ResponseBody 어노테이션을 메타 어노테이션으로 작성해둔 @RestController를 많이 사용하니 참고하면 되겠다.

Spring5

Spring5에서 지원해주는 reactive streams 또한 리턴타입으로 가능하다. 기본적으로 spring5는 reactor를 지원한다. 왜냐하면 자기들이 만들었으니.. 어쨌든 reactor 에 있는 Mono, Flux으로 리턴타입으로 작성해도 된다.

@GetMapping("/spring5")
public Mono<String> hello() {
  return Mono.just("hello world");
}

여기서는 Mono만 했지만 Flux 또한 가능하다.
또한 reactive streams의 인터페이스인 Publisher을 리턴타입으로 명시해도 동작한다.

@GetMapping("/spring5")
public Publisher<String> hello() {
  return Mono.just("hello world");
}

이 외에도 rxjava1, rxjava2에 있는 Observable, Single, Completable, Flowable, Maybe 등 rxjava1과 rxjava2를 모두 지원하니 적당한 디펜더시를 작성한다면 사용해도 좋다. 또한 지금 현재 버전(5.0.7)에서는 java9에 들어간 reactive streams 인터페이스도 지원하니 자기에게 알맞는 구현체 또는 인터페이스를 사용해도 된다.

Rendering

spring5 에서는 Rendering 인터페이스가 추가되었다. reactive streams를 사용할 때 해당하는 뷰와 데이터를 넣을 수 있는 인터페이스이다.

@GetMapping("/")
public Rendering hi() {
  return Rendering
      .view("index")
      .modelAttribute("user", DATA)
      .build();
}

기존의 Model과 비슷한 생김새이다. 그래서 많이 거부감들지 않게 작성할 수 있다.

오늘은 이렇게 Spring에서 지원해주는 Controller 리턴타입에 대해서 알아봤다. 필자가 모르는 것이 있을 수 있기에 더욱 상세한 내용들은 해당문서를 참고하면 좋겠다. 알면 저도 알려주세요.

Spring @Bean에 대해서..

오늘은 Spring @Bean 어노테이션과 관련해서 이야기를 하려한다. 깊게 볼 것은 아니고 특이한 거나 잘 몰랐던거? 알지만 해보지 않았던거? 그런것들을 알아볼 예정이니 그냥 이렇구나 정도만 알면 되겠다. 자주 사용될 일도 없으니 한번씩만 훑고 지나가면 될 것 같다.

요즘 회사를 이직준비 중이라 회사 다닐때 보다 더 바쁘고 스트레스 받는 것 같다.ㅠㅠ 아무튼 한번 알아보자.

Interface

버전은 언제인지 잘 모르겠는데 Interface에도 @Bean 어노테이션을 작성해도 동작한다. 아마 4.x부터 됐을 것으로 예상해보지만 그게 언제인지는 확인해보지 않았다. 예를들어 다음과 같은 설정을 해도 동작한다.


public class SomeBean { public void print() { System.out.println("hello world"); } } @Configuration public interface BeanInterface { @Bean default SomeBean someBean() { return new SomeBean(); } } @Configuration public class BeanClass implements BeanInterface { //... }

위와 같이 작성해도 SomeBean이라는 클래스가 Bean으로 등록된다. 여기서 주의할 것은 그에 맞기 구현체가 있어야 한다는 것이다. 만약 구현체가 없다면 Bean으로 등록되지 않는다.

public static void main(String[] args) {
  ConfigurableApplicationContext context = SpringApplication.run(SpringBeanApplication.class, args);
  SomeBean someBean = context.getBean(SomeBean.class);
  someBean.print();
}

위와 같이 실행시키면 hello world 가 print 된 것을 알 수 있다.

@Bean 명

Bean들은 자기만의 고유한 명이 있다. 뭐 다 알겠지만 그렇다. 기본적으로 아무설정 하지 않았을 경우에는 메서드명이 빈명이 된다.

@Bean
SomeBean someBean() {
  return new SomeBean();
}

위와 같이 사용할 경우 someBean이라는 이름으로 빈명이 등록이 된다.

public static void main(String[] args) {
  ConfigurableApplicationContext context = SpringApplication.run(SpringBeanApplication.class, args);
  String[] beanNames = context.getBeanNamesForType(SomeBean.class);
  System.out.println(beanNames[0]);
}

해당 빈이름을 가져왔을 때 someBean이라는 것이 프린트 될 것이다. 만약 빈명을 변경하고자 할 때는 @Bean 어노테이션 속성 중 value 또는 name을 사용해서 변경 할 수 있다.

@Bean("nothing")
SomeBean someBean() {
  return new SomeBean();
}

close

Spring Bean의 라이프사이클 중에 destroy라는 자원을 해제하거나 빈이 소멸될 때 마지막으로 호출해주는 메서드들이 존재한다. 예를들어 DisposableBean을 구현하거나 @PreDestroy 어노테이션 또는 destroyMethod 속성을 사용해서 자원 등을 해제 할 수 있다. 3가지 방법 말고도 spring 에서는 좀 더 많은 해제 법이 존재한다. 첫 번째 방법으로는 AutoCloseable 인터페이스를 구현하면 된다.

public class SomeBean implements AutoCloseable {

  @Override
  public void close() throws Exception {
    System.out.println("close");
  }
}

위와 같이 작성 후에 아무 설정 없이도 SomeBean 이라는 빈이 소멸될 때 close 메서드를 실행 시킨다. 한번씩 확인해 보자.

두 번째 방법으로는 굳이 AutoCloseable 필요 없이 close 메서드만 있어도 된다.

public class SomeBean  {

  public void close() throws Exception {
    System.out.println("close");
  }
}

위와 같이 작성해도 별탈 없이 close 메서드가 실행 된다. 더 정확하게는 그냥 close 메서드를 실행 시키는 것이다. 솔직히 위의 AutoCloseable 여부와 상관없이 그냥 close 메서드를 호출하는 것이다.

마지막 방법으로는 shutdown() 메서드 이다. 이 또한 그냥 shutdown() 메서드를 호출 하는 것이다.

public class SomeBean  {

  public void shutdown() throws Exception {
    System.out.println("close");
  }
}

위와 같이 작성해도 close를 출력하는 것을 확인 할 수 있다. 만약 좀 더 확인하고 싶다면 DisposableBeanAdapter 클래스를 좀 더 확인하면 알 수 있다.

class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable {

  private static final String CLOSE_METHOD_NAME = "close";

  private static final String SHUTDOWN_METHOD_NAME = "shutdown";

  //...
}

참고로 DisposableBean 인터페이스와 위의 세가지 방법을 같이 쓰면 DisposableBean 의 destroy 메서드가 우선순위가 높다.

Lite Mode

Spring Bean에는 lite 모드라는 것이 있다. 그게 뭐냐고 물어보면 조금 뭐라고 해야할지.. lite니까 조금 가벼운거?
아무튼 lite 모드는 해당 설정에 @Configuration 어노테이션이 아닌 @Component어노테이션을 작성하면 그게 lite mode가 된다. lite 모드로 하면 설정이 Configuration으로 하는 것보다는 빠르지만 그렇게 확 티가 날 정도는 아닌 걸로 기억하고 있다. 물론 많으면 말이 달라지겠지만.. 아무튼 그럼 차이가 뭘까? 차이는 Cglib을 사용하냐 하지 않느냐 차이가 가장 큰 차이 같다.

@Configuration 같은 경우에는 cglib을 사용하지만 @Component 경우에는 cglib을 사용하지 않는다.

@Configuration
public class Config {

  @Bean
  SomeBean someBean() {
    return new SomeBean();
  }

  @Bean
  NothingBean nothingBean() {
    return new NothingBean(someBean());
  }
}

예를 들어 위와 같은 설정이 있다고 가정하자. NothingBean은 SomeBean을 디펜더시 받고 있다. 그래서 위와 같이 설정을 했다고 하자. 그럼 someBean 의 메서드는 몇 번 호출 이 될까? 일반적으로 볼 때에는 두번 호출 되는 게 맞다고 생각한다. SomeBean을 빈으로 등록할 때와 NothingBean을 빈으로 등록할 때 someBean() 메서드를 호출하니 두번이 맞다. 하지만 @Configuration를 사용하면 아까 말했다시피 cglib을 이용해서 위의 someBean을 한번만 호출 하도록 한다.

@Component
public class Config {

  @Bean
  SomeBean someBean() {
    return new SomeBean();
  }

  @Bean
  NothingBean nothingBean() {
    return new NothingBean(someBean());
  }
}

하지만 이 경우에는 어떨까? 이 경우에는 우리가 일반적으로 생각하는게 맞다. 이 때에는 someBean() 메서드가 두번 호출된다.
@Configuration 은 cglib을 사용해서 일반적으로 @Component 를 사용할 때보다 조금 느린것 같다. 하지만 그 차이는 눈에 보이지 않는 다는 거..
만약 config을 lite mode로 사용할 경우에는 좀 더 신중하게 사용해야 될 듯 싶다. 그냥 필자는 @Configuration을 사용하련다..

오늘은 Spring의 Bean에 대해서 몇 가지 알아봤다. Spring을 사용하는데에는 아주 쓸모있는 기능은 아니지만 알아둬서 나쁠껀 없으니 한번씩 해보도록 하자.

Spring jsr305

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

이상!