728x90

 

 

이번에는 handler method 에서 사용할 수 있는 다양한 argument 에 대해 정리한다.

여러 포스트로 나누어 정리하지 않고 이번에 자주 사용하는 방법 위주로 이곳에 정리할 예정이다.

 

spring framework document 의 Method Arguments 와 강의 내용을 참고하여 정리했다.

 

 


정리 내용

- @PathVariable 과 @MatrixVariable

 

 


 

@PathVariable 과 @MatrixVariable

 

PathVariable api 문서를 보면 @RequestMapping annotation 이 붙은 handler method 에서 사용할 수 있는 URI template 변수에 바인딩 되는 parameter 에 사용할 수 있다고 되어 있다.

 

@PathVariable 을 테스트 해보기 위해 다음과 같이 코드를 수정 하였다.

 

 

반환 값으로 사용 할 UserInfo 클래스를 만들었다.

 

package me.nimkoes;

import lombok.Getter;
import lombok.Setter;

@Getter @Setter
public class UserInfo {
    private int id;
    private String name;
}

 

그리고 MyHelloController 클래스의 handler mehtod 를 다음과 같이 @PathVariable 을 사용하도록 수정했다.

 

package me.nimkoes;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MyHelloController {

    @GetMapping("/hello/{id}")
    @ResponseBody
    public UserInfo hello(@PathVariable int id) {
        UserInfo userInfo = new UserInfo();
        userInfo.setId(id);

        return userInfo;
    }

}

 

@GetMapping annotation 의 meta annotation 으로 @RequestMapping annotation 을 사용하기 때문에 @PathVariable 을 사용할 수 있다.

@PathVariable 관련된 문서를 봤다면 알겠지만 바인딩 할 URI template 의 값을 알고 있다면 다음과 같이 메소드 내에서 다른 변수로 사용할 수 있다.

 

package me.nimkoes;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MyHelloController {

    @GetMapping("/hello/{id}")
    @ResponseBody
    public UserInfo hello(@PathVariable(value = "id") int paramId) {
        UserInfo userInfo = new UserInfo();
        userInfo.setId(paramId);

        return userInfo;
    }

}

 

하지만 특별한 경우가 아니라면 굳이 이렇게 사용 할 필요는 없다.

이 hander method 가 정상 동작 하는지 확인해보기 위한 테스트 코드를 작성 했다.

 

package me.nimkoes;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void helloTest() throws Exception {

        mockMvc.perform(get("/hello/1234"))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(jsonPath("id").value(1234))
        ;

    }
}

 

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /hello/1234
       Parameters = {}
          Headers = {}
             Body = <no character encoding set>
    Session Attrs = {}

Handler:
             Type = me.nimkoes.MyHelloController
           Method = public me.nimkoes.UserInfo me.nimkoes.MyHelloController.hello(int)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {Content-Type=[application/json;charset=UTF-8]}
     Content type = application/json;charset=UTF-8
             Body = {"id":1234,"name":null}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

URI 정보에서 argument 를 추출해내는 것과 관련해서 @PathVariable 이외에 @MatrixVariable 이 있다.

MatrixVariable api 문서를 보면 이것 역시 @RequestMapping annotation 이 붙은 handler method 에 사용할 수 있고, URI path 상에 name-value 쌍으로 이루어진 내용을 참고해서 argument 로 사용할 수 있다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

728x90
728x90

 

 

@RequestMapping 으로 HTTP request 에 대한 handler 를 매핑하면 이 handler 를 사용할 때 다양한 매개변수와 반환 값을 유연하게 사용할 수 있다.

 

다음은 공식 문서를 참고해 작성한 handler method 에서 사용할 수 있는 매개변수들과 반환 값의 목록이다.

 

 

Controller method argument (handler method argument)

    : WebRequest
    : NativeWebRequest
    : javax.servlet.ServletRequest
    : javax.servlet.ServletResponse
    : javax.servlet.http.HttpSession
    : javax.servlet.http.PushBuilder
    : java.security.Principal
    : HttpMethod
    : java.util.TimeZone +
    : java.time.ZoneId
    : java.io.InputStream
    : java.io.Reader
    : java.io.OutputStream
    : java.io.Writer
    : @PathVariable
    : @MatrixVariable
    : @RequestParam
    : @RequestHeader
    : @CookieValue
    : @RequestBody
    : HttpEntity<B>
    : @RequestPart
    : java.util.Map
    : org.springframework.ui.Model
    : org.springframework.ui.ModelMap
    : RedirectAttributes
    : @ModelAttribute
    : Errors
    : BindingResult
    : SessionStatus + class-level
    : @SessionAttributes
    : UriComponentsBuilder
    : @SessionAttribute
    : @RequestAttribute
    : Any other argument

 

 

Controller method return value (handler method return value)

    : @ResponseBody
    : HttpEntity<B>
    : ResponseEntity<B>
    : HttpHeaders
    : String
    : View
    : java.util.Map
    : org.springframework.ui.Model
    : @ModelAttribute
    : ModelAndView object
    : void
    : DeferredResult<V>
    : Callable<V>
    : ListenableFuture<V>
    : java.util.concurrent.CompletionStage<V>
    : java.util.concurrent.CompletableFuture<V>
    : ResponseBodyEmitter
    : SseEmitter
    : StreamingResponseBody
    : Reactive types - Reactor, RxJava, or others through ReactiveAdapterRegistry
    : Any other return value

 

 

 

자세한 내용은 문서를 보는게 가장 좋겠지만 쉽지 않은 일일 수 있기 때문에, 자주 사용하는 것 위주로 정리를 할 계획이다.

개인적으로 활용하는 파트도 중요하지만 그 앞의 원리와 설정에 대해 아는게 더 중요하다고 생각한다.

 

handler method 의 argument 와 return type 을 실제로 활용하는 방법에 대해 정리하기 앞서 누가 어디서 뭘 어떻게 했길래 가능한건지부터 살펴보려 한다.

디버그 모드로 서버를 실행한 다음 한줄씩 실행 과정을 따라가 보면서 확인해보기 위해 가장 기본적인 형태의 handler method 를 만들고 DispatcherServlet 클래스의 doService 메소드에 break point 를 걸어두었다.

 

 

package me.nimkoes;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyHelloController {

    @GetMapping("/hello")
    @ResponseBody
    public String hello() {
        return "hi";
    }

}

 

HTTP GET 요청으로 /hello 를 보내면 "hi" 라는 문자열을 HTTP response body 에 실어 보내는 handler method 이다.

 

 

 

DispatcherServlet 의 doService 메소드에 있는 doDispatch 안으로 들어간다.

 

 

하는 일을 보면 크게

1. 요청을 처리 할 handler method 찾기 :: getHandler

2. 요청을 처리할 수 있는 adapter 찾기 :: getHandlerAdapter

3. 실제로 요청을 처리하기 :: ha.handle

4. 응답으로 보낼 결과 처리하기 :: processDispatchResult

정도인 것으로 보인다.

 

지금은 실제로 handler method 를 처리할 때 argument 를 어떻게 처리하는지 알아보는 것을 목적으로 하고 있기 때문에

// Actually invoke the handler. 라고 되어있는 ha.handle 의 안쪽으로 들어가 봤다.

 

한줄씩 실행하다보니 관련된 것 같아 보이는 부분을 발견 하였다.

 

 

argument 를 처리할 수 있는 argumentResolver 와 return value 를 처리하는 returnValueHandlers 를 등록해 주는 부분이 있었다.

한가지 의문은 등록 해준적도 없는 저 구현체들은 어디서 나왔냐는 건데, 실제로 handler method 를 실행하는 invokeHandlerMethod 메소드는 RequestMappingHandlerAdapter 클래스에 정의되어 있는데, 이 클래스에 getDefaultArgumentResolvers 메소드와 getDefaultReturnValueHandlers 메소드에서 등록해주고 있다.

 

 

 

 

그리고 parameterNameDiscoverer 를 등록하는 부분이 있는데, 아무래도 paramter 의 이름을 찾는 일을 하는 것 같다.

 

 

실제로 등록되는 클래스를 보면 Reflection 이나 Local 변수와 관련된 Table 정보를 참고해서 parameter name 을 찾는 것 같아 보인다.

이렇게 전처리?를 다 하고나면, 정말로 handler method 를 실행하는 부분인 invocableMethod.invokeAndHandle 메소드 에서 요청 처리를 시작 한다.

 

 

invokeAndHandle 은 요청을 처리한 다음 앞서 등록한 returnValueHandlers 로 return value 를 처리하도록 되어 있다.

 

 

getMethodArgumentValues 로 args 들을 추출해낸 다음 main 메소드 실행하 듯 doInvoke 를 호출하는 것을 볼 수 있다.

 

 

this.resolvers 에는 앞서 default 로 자동 등록해준 26개의 argument 를 처리할 수 있는 구현체들의 LinkedList 이다.

지금 만든 handler method 는 parameter 가 없기 때문에 return EMPTY_ARGS 를 실행 하겠지만, 만약 그렇지 않았다면 처리 가능한 parameter 인지 검사하고, 처리 가능한 경우 arg 배열에 하나씩 그 값을 추가 한다.

 

resolvers 의 resolveArgument 의 내용을 보면 HandlerMethodArgumentResolver 인터페이스를 구현한 구현체들의 resolveArgument 메소드를 실행하도록 되어 있다.

각 구현체가 어떻게 argument 를 처리하는지 까지는 다루지 않고 어떻게 사용하는지에 대해서만 정리할 예정이다.

 

드디어 모든 준비가 끝나면 doInvoke(args) 를 실행해서 handler method 를 실행 한다.

 

 

java Reflection 을 사용해서 method 의 modifier 가 (접근 제한자) public 이 아닌 경우 public 접근이 가능하도록 수정한 다음 handler method 를 실행 한다.

 

 

테스트 해보기 위해 만든 handler method 의 반환 값인 "hi" 문자열이 returnValue 에 담겼고, 이제 returnValueHandlers 의 handleReturnValue 로 이 값을 처리한다.

 

이후 처리되는 과정도 한줄씩 실행해보면서 보았지만 더 깊은 내용을 정리하는건 좋지 않다고 생각했다.

지금은 DispatcherServlet 의 doService 메소드를 시작점으로 break point 를 잡았지만, 사실 DispatcherServlet 은 단지 하나의 Servlet 일 뿐이라는 것을 생각해보면 filter, interceptor 등 전후에 실행하는 Servlet lifecycle 을 따라야 한다는 것 정도만 기억하고 넘어가도 괜찮다는 생각이다.

 

아무튼, 기나긴 여정을 지나 실행 결과를 받아볼 수 있었다.

 

 

본 글을 마무리 하자면, 맨 위에 목록으로 정리 한 handler method 의 Argument 와 Return value 를 별다른 설정 없이 너무나 당연한 권리 찾듯 사용하는게 어떻게 가능했는지에 대해 간략하게 살펴 보았다.

 

어떻게 해서 사용할 수 있었는지 이제는 알기 때문에 어떻게 활용할 수 있는지에 대해 정리해 보려 한다.

 

 

 

 

728x90
728x90

 

 

이번에 정리 할 내용은 HTTP 요청을 handler method 에 매핑하는 방법이다.

 

정리에 앞서 본 내용과 무관한 코드를 정리했다.

MyHelloController 를 제외한 Person, PersonFormatter, GreetingInterceptor, AnotherInterceptor 를 삭제하고 WebConfig 의 Interceptor, ResourceHandler, Jaxb 설정도 삭제했다.

MyHelloControllerTest 클래스의 내용도 모두 삭제 했다.

 

 

MyHelloController 에 다음과 같이 handler method 를 하나 등록 하였다.

 

package me.nimkoes;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MyHelloController {

    @RequestMapping("/hello")
    @ResponseBody
    public String hello() {
        return "hello";
    }

}

 

@Controller annotation 을 사용해서 component scan 대상으로 만들고, 그 안에 @RequestMapping annotation 을 사용해서 uri 를 지정해주면 handler method 가 된다.

 

만약 11라인의 @ResponseBody 를 사용하지 않으면 return 하는 문자열에 해당하는 view 를 찾고, 사용 할 경우 반환하는 값을 HTTP response body 에 실어 응답 데이터로 보내게 된다.

 

추가로 @RequestMapping 을 사용해서 uri 만 지정할 경우 모든 종류의 HTTP method 를 허용하게 된다.

HTTP method 의 종류로는 GET, POST, HEAD, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH 등이 있다.

실제로 그런지 테스트 코드를 작성해서 확인해 봤다.

 

package me.nimkoes;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void helloTest() throws Exception {
        mockMvc.perform(get("/hello")).andDo(print()).andExpect(status().isOk());
        mockMvc.perform(post("/hello")).andDo(print()).andExpect(status().isOk());
        mockMvc.perform(head("/hello")).andDo(print()).andExpect(status().isOk());
        mockMvc.perform(put("/hello")).andDo(print()).andExpect(status().isOk());
        mockMvc.perform(delete("/hello")).andDo(print()).andExpect(status().isOk());
        mockMvc.perform(options("/hello")).andDo(print()).andExpect(status().isOk());
        mockMvc.perform(patch("/hello")).andDo(print()).andExpect(status().isOk());

    }
}

 

 

만약 특정한 요청에 대해서만 허용하게 하려면 @RequestMapping annotation 에 값을 추가하면 된다.

 

package me.nimkoes;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MyHelloController {

    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    @ResponseBody
    public String hello() {
        return "hello";
    }

}

 

RequestMethod 는 enum 타입으로 HTTP request method 를 정의하고 있다.

 

/*
 * Copyright 2002-2015 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.web.bind.annotation;

/**
 * Java 5 enumeration of HTTP request methods. Intended for use with the
 * {@link RequestMapping#method()} attribute of the {@link RequestMapping} annotation.
 *
 * <p>Note that, by default, {@link org.springframework.web.servlet.DispatcherServlet}
 * supports GET, HEAD, POST, PUT, PATCH and DELETE only. DispatcherServlet will
 * process TRACE and OPTIONS with the default HttpServlet behavior unless explicitly
 * told to dispatch those request types as well: Check out the "dispatchOptionsRequest"
 * and "dispatchTraceRequest" properties, switching them to "true" if necessary.
 *
 * @author Juergen Hoeller
 * @since 2.5
 * @see RequestMapping
 * @see org.springframework.web.servlet.DispatcherServlet#setDispatchOptionsRequest
 * @see org.springframework.web.servlet.DispatcherServlet#setDispatchTraceRequest
 */
public enum RequestMethod {

	GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE

}

 

이제 이 handler 는 HTTP GET 요청만 처리할 수 있다.

테스트 코드를 다음과 같이 고쳐 실행해 보았다.

 

package me.nimkoes;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void helloTest() throws Exception {

        mockMvc.perform(get("/hello"))
            .andDo(print())
            .andExpect(status().isOk())
        ;

        mockMvc.perform(post("/hello"))
            .andDo(print())
            .andExpect(status().isMethodNotAllowed())
        ;

    }

}

 

handler 가 GET 방식의 요청만 처리할 수 있게 되었으므로 POST 요청에 대한 status 로 405 Method Not Allowed 를 반환하게 된다.

만약 HTTP GET 과 POST 를 모두 허용하고 싶으면 handler 를 다음과 같이 method 를 배열 형태로 수정하면 된다.

 

package me.nimkoes;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MyHelloController {

    @RequestMapping(value = "/hello", method = {RequestMethod.GET, RequestMethod.POST})
    @ResponseBody
    public String hello() {
        return "hello";
    }

}

 

지금은 @RequestMapping annotation 을 사용해서 HTTP 의 어떤 타입의 요청을 처리할지 mehotd 값을 지정해 주었지만 한가지 타입의 요청만 처리하도록 한다면 @GetMapping 과 같은 다른 annotation 을 사용해서 코드를 간결하게 작성할 수 있다.

 

package me.nimkoes;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MyHelloController {

    @GetMapping("/hello")
    @ResponseBody
    public String hello() {
        return "hello";
    }

}

 

@GetMapping 을 포함하여 @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping 등이 있다.

그리고 이 모든 HTTP request method 에 매칭하는 annotation 은 @RequestMapping annotation 을 meta annotation 으로 사용하고 있다.

@GetMapping 을 대표로 문서를 보면 @RequestMapping(method = RequestMethod.GET) 의 축약 표현이라고 정의되어 있다.

 

추가로 @RequstMapping 은 method 가 아닌 class 레벨에 작성할 수도 있다.

 

package me.nimkoes;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping(method = RequestMethod.GET)
public class MyHelloController {

    @RequestMapping("/hello")
    @ResponseBody
    public String hello() {
        return "hello";
    }

    @PostMapping
    @ResponseBody
    public String hi() {
        return "hi";
    }

}

 

class 레벨에 @ReauestMapping 으로 GET 요청만 받을 수 있도록 정의 했기 때문에 이 안에 작성된 모든 handler 는 GET 요청만 처리할 수 있게 된다.

실제로 그런지 궁금해서 일부러 @PostMapping 을 추가하고 테스트 코드를 작성해봤다.

 

package me.nimkoes;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void helloTest() throws Exception {

        mockMvc.perform(get("/hello")).andExpect(status().isOk());                 // 200
        mockMvc.perform(post("/hello")).andExpect(status().isMethodNotAllowed());  // 405
        mockMvc.perform(get("/hi")).andExpect(status().isNotFound());              // 404
        mockMvc.perform(post("/hi")).andExpect(status().isNotFound());             // 404

    }

}

 

특이했던건 @PostMapping 을 정의한 handler 에 대해서 405 가 아닌 404 응답을 받았다는 점이다.

 

 


 

 

HTTP request 를 handler 에 매핑할 때 uri 가 완전히 동일한 handler 만 사용했는데 패턴을 사용하는 방법이 있다.

패턴은 크게 다음 세 가지를 사용할 수 있다.

 

? 길이 1의 문자 필수 /hello? /hello?? /hello/? /hello/?? /hello?/?/? /hello/?test?
* 길이 0 이상의 문자열 /hello* /hello/* /hello/*/test /hello/*/te*st
** 하위 모든 path /hello/** /hello/**/test

 

사용하기에 따라 굉장히 복잡한 규칙을 만들 수 있겠지만 그런 경우가 얼마나 있을까 싶은 생각이다.

 

package me.nimkoes;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyHelloController {

    @GetMapping("/hello?")
    public String hello_case_1() { return "hello case 1"; }

    @GetMapping("/hello/?")
    public String hello_case_2() { return "hello case 2"; }

    @GetMapping("/hello/??")
    public String hello_case_3() { return "hello case 3"; }

    @GetMapping("/hello/?/?")
    public String hello_case_4() { return "hello case 4"; }

    @GetMapping("/hello*")
    public String hello_case_5() { return "hello case 5"; }

    @GetMapping("/hello/*/test")
    public String hello_case_6() { return "hello case 6"; }

    @GetMapping("/hello/*/te*st")
    public String hello_case_7() { return "hello case 7"; }

    @GetMapping("/hello/**")
    public String hello_case_8() { return "hello case 8"; }

    @GetMapping("/hello/**/test")
    public String hello_case_9() { return "hello case 9"; }

}

 

어떻게 사용할 수 있는지 테스트 해보기 위해 몇가지 경우만 만들어 보았다.

 

package me.nimkoes;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void helloTest() throws Exception {

        mockMvc.perform(get("/hello1"))                    .andExpect(content().string("hello case 1"));
        mockMvc.perform(get("/hello!"))                    .andExpect(content().string("hello case 1"));
        mockMvc.perform(get("/hello/z"))                   .andExpect(content().string("hello case 2"));
        mockMvc.perform(get("/hello/nk"))                  .andExpect(content().string("hello case 3"));
        mockMvc.perform(get("/hello/n/k"))                 .andExpect(content().string("hello case 4"));
        mockMvc.perform(get("/hello"))                     .andExpect(content().string("hello case 5"));
        mockMvc.perform(get("/helloTest"))                 .andExpect(content().string("hello case 5"));
        mockMvc.perform(get("/hello/nimkoes/test"))        .andExpect(content().string("hello case 6"));
        mockMvc.perform(get("/hello/nk/te_some_st"))       .andExpect(content().string("hello case 7"));
        mockMvc.perform(get("/hello/nk/some"))             .andExpect(content().string("hello case 8"));
        mockMvc.perform(get("/hello/nk/some/another"))     .andExpect(content().string("hello case 8"));
        mockMvc.perform(get("/hello/nk/some/another/test")).andExpect(content().string("hello case 9"));

    }

}

 

uri 패턴 매핑 방법 중에 확장자를 매핑하는 방법도 있다.

이 방법은 요청 uri 가 특정 파일의 확장자를 포함하는 경우인데 /hello.* 와 같은 uri 패턴을 사용할 경우 /hello.json, /hello.zip 등의 uri 를 사용할 수 있는 것을 뜻한다.

 

하지만 확장자를 사용한 uri 패턴을 사용할 경우 RFD 공격과 같은 보안 이슈가 있기 때문에 권장하는 방법이 아니며, Spring Boot 에서는 이런 uri 의 요청을 사용하지 못하도록 막고 있다.

 

 


 

@RequestMapping 또는 @RequestMapping 을 meta annotation 으로 사용하는 @GetMapping 과 같은 annotation 을 조금 더 자세히 보면 consumes 와 produces 라는 값을 설정할 수 있다.

 

// 생략...

package org.springframework.web.bind.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.core.annotation.AliasFor;

// 생략...

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {

    // 생략...

    /**
     * The consumable media types of the mapped request, narrowing the primary mapping.
     * <p>The format is a single media type or a sequence of media types,
     * with a request only mapped if the {@code Content-Type} matches one of these media types.
     * Examples:
     * <pre class="code">
     * consumes = "text/plain"
     * consumes = {"text/plain", "application/*"}
     * </pre>
     * Expressions can be negated by using the "!" operator, as in "!text/plain", which matches
     * all requests with a {@code Content-Type} other than "text/plain".
     * <p><b>Supported at the type level as well as at the method level!</b>
     * When used at the type level, all method-level mappings override
     * this consumes restriction.
     * @see org.springframework.http.MediaType
     * @see javax.servlet.http.HttpServletRequest#getContentType()
     */
    String[] consumes() default {};

    /**
     * The producible media types of the mapped request, narrowing the primary mapping.
     * <p>The format is a single media type or a sequence of media types,
     * with a request only mapped if the {@code Accept} matches one of these media types.
     * Examples:
     * <pre class="code">
     * produces = "text/plain"
     * produces = {"text/plain", "application/*"}
     * produces = MediaType.APPLICATION_JSON_UTF8_VALUE
     * </pre>
     * <p>It affects the actual content type written, for example to produce a JSON response
     * with UTF-8 encoding, {@link org.springframework.http.MediaType#APPLICATION_JSON_UTF8_VALUE} should be used.
     * <p>Expressions can be negated by using the "!" operator, as in "!text/plain", which matches
     * all requests with a {@code Accept} other than "text/plain".
     * <p><b>Supported at the type level as well as at the method level!</b>
     * When used at the type level, all method-level mappings override
     * this produces restriction.
     * @see org.springframework.http.MediaType
     */
    String[] produces() default {};

}

 

consumes 와 produces 모두 header 의 Mediatype 을 참조해서 handler 를 매핑하는데 사용한다.

차이가 있다면 consumes 는 HTTP 요청의 Content-Tpye 을 사용하고 produces 는 Accept 를 사용 한다.

 

package me.nimkoes;

import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MyHelloController {

    @GetMapping(value = "/hello", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
    @ResponseBody
    public String hello() {
        return "hello";
    }

}

 

HTTP GET 요청을 받는 /hello handler method 는 consumes 로 APPLICATION_JSON_UTF8_VALUE 를 사용하고 있다.

사실 MediaType 타입의 값 대신 문자열을 사용해도 되지만 오타가 있을 경우 찾아내기 매우 어려울 수 있기 때문에 되도록 정의 된 상수를 사용하는걸 권장한다.

 

APPLICATION_JSON_UTF8_VALUE 는 "application/json;charset=UTF-8" 값이 할당되어 있다.

 

그래서 이 handler 는 HTTP GET 방식의 uri 가 /hello 여야 하기도 하지만, Content-Type 이 json 형식이라고 명시되어 있어야 매핑할 수 있다.

 

package me.nimkoes;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void helloTest() throws Exception {

        mockMvc.perform(
                get("/hello")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
            )
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(content().string("hello"))
        ;

    }

}

 

contentType 을 지정해주지 않거나 json 형식이 아닌 경우 테스트는 실패한다.

지금은 contentType 을 올바르게 설정 했기 때문에 테스트는 성공한다.

 

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /hello
       Parameters = {}
          Headers = {Content-Type=[application/json;charset=UTF-8]}
             Body = null
    Session Attrs = {}

Handler:
             Type = me.nimkoes.MyHelloController
           Method = public java.lang.String me.nimkoes.MyHelloController.hello()

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {Content-Type=[text/plain;charset=UTF-8], Content-Length=[5]}
     Content type = text/plain;charset=UTF-8
             Body = hello
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

HTTP request 를 보낼 때 header 에 내가 보내는 데이터의 형식이 무엇인지 알려주는 Content-Type 뿐만 아니라, 요청에 대한 응답 데이터의 형식이 어떤 형식이기를 바란다는 의미로 Accept 값을 설정할 수 있다.

그리고 handler 에도 HTTP request 의 Accept 에 매칭하는 값으로 produces 값을 설정할 수 있는데, 이 handler 의 응답 데이터 형식이 무엇인지 명시할 수 있다.

 

package me.nimkoes;

import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MyHelloController {

    @GetMapping(
        value = "/hello",
        consumes = MediaType.APPLICATION_JSON_UTF8_VALUE,
        produces = MediaType.TEXT_PLAIN_VALUE
    )
    @ResponseBody
    public String hello() {
        return "hello";
    }

}

 

이 handler 의 응답 데이터 형식을 "text/plain" 이라고 명시해 주었기 때문에 HTTP request 의 Accept 값이 다른 값인 경우 오류가 발생 한다.

 

package me.nimkoes;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void helloTest() throws Exception {

        mockMvc.perform(
                get("/hello")
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON)
            )
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(content().string("hello"))
        ;

    }

}

 

28라인에서 차라리 accept 를 설정하지 않았다면 테스트는 오히려 성공 했을 텐데, 이 요청에 대한 응답 결과 json 형식의 데이터를 기대한다고 명시했기 때문에 406 Not Acceptable 오류가 발생 한다.

 

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /hello
       Parameters = {}
          Headers = {Content-Type=[application/json]Accept=[application/json]}
             Body = <no character encoding set>
    Session Attrs = {}

생략...

MockHttpServletResponse:
           Status = 406
    Error message = null
          Headers = {}
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

생략...

java.lang.AssertionError: Status 
Expected :200
Actual   :406

 

정리하면 @RequestMapping 또는 이 annotation 을 meta annotation 으로 사용하는 @GetMapping, @PostMapping 등을 사용할 때 consumes 와 produces 값을 추가로 설정할 수 있는데,

consumes 는 HTTP request header 의 Content-Type 을 참조하고, produces 는 HTTP request header 의 Accept 값을 참조 한다.

 

Content-Type 이 일치하지 않는 경우 415 Unsupported Media Type 오류가 발생하고

Accept 값이 일치하지 않는 경우 406 Not Acceptable 오류가 발생 한다.

 

 


 

@RequestMapping 에 설정할 수 있는 값으로 headers 가 있다.

 

// 생략...

package org.springframework.web.bind.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.core.annotation.AliasFor;

// 생략...

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {

    // 생략...

    /**
     * The headers of the mapped request, narrowing the primary mapping.
     * <p>Same format for any environment: a sequence of "My-Header=myValue" style
     * expressions, with a request only mapped if each such header is found
     * to have the given value. Expressions can be negated by using the "!=" operator,
     * as in "My-Header!=myValue". "My-Header" style expressions are also supported,
     * with such headers having to be present in the request (allowed to have
     * any value). Finally, "!My-Header" style expressions indicate that the
     * specified header is <i>not</i> supposed to be present in the request.
     * <p>Also supports media type wildcards (*), for headers such as Accept
     * and Content-Type. For instance,
     * <pre class="code">
     * &#064;RequestMapping(value = "/something", headers = "content-type=text/*")
     * </pre>
     * will match requests with a Content-Type of "text/html", "text/plain", etc.
     * <p><b>Supported at the type level as well as at the method level!</b>
     * When used at the type level, all method-level mappings inherit
     * this header restriction (i.e. the type-level restriction
     * gets checked before the handler method is even resolved).
     * @see org.springframework.http.MediaType
     */
    String[] headers() default {};

    // 생략...

}

 

이 값은 HTTP request 에 특정한 header 가 있는 경우 또는 그 header 의 값이 특정한 값인 경우에만 handler 를 매핑해주고 싶을 때 사용할 수 있다.

 

package me.nimkoes;

import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MyHelloController {

    @GetMapping(value = "/hello", headers = HttpHeaders.AUTHORIZATION)
    @ResponseBody
    public String hello() {
        return "hello";
    }

}

 

이 경우 HTTP request header 에 AUTHORIZATION 이 있는 경우 매핑될 수 있다.

HttpHeaders 는 header 로 사용할 수 있는 문자열을 정의 한 클래스이다.

 

 

테스트 코드를 작성할 때 보면 headers 로 key-value 쌍으로 설정할 수 있다.

현재 AUTHORIZATION 이 있을 경우에 handler 에 매핑될 수 있기 때문에 없는 경우와 있는 경우 모두 테스트 해보았다.

 

headers 를 아무것도 설정하지 않았을 때는 404 오류가 발생했다.

 

java.lang.AssertionError: Status 
Expected :200
Actual   :404

 

package me.nimkoes;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void helloTest() throws Exception {

        mockMvc.perform(
                get("/hello")
                .header(HttpHeaders.AUTHORIZATION,"1234")
            )
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(content().string("hello"))
        ;

    }

}

 

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /hello
       Parameters = {}
          Headers = {Authorization=[1234]}
             Body = <no character encoding set>
    Session Attrs = {}

Handler:
             Type = me.nimkoes.MyHelloController
           Method = public java.lang.String me.nimkoes.MyHelloController.hello()

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {Content-Type=[text/plain;charset=UTF-8], Content-Length=[5]}
     Content type = text/plain;charset=UTF-8
             Body = hello
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

HTTP request header 에 특정한 key 가 있는 경우에만 매핑할 뿐만 아니라 그 값에 대해서도 검사할 수 있다.

예를 들어 지금 AUTHORIZATION 값으로 "1234" 를 설정 했는데, @GetMapping 에서도 이 값이 올바른 경우에만 매핑해줄 수 있다.

 

package me.nimkoes;

import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MyHelloController {

    @GetMapping(value = "/hello", headers = HttpHeaders.AUTHORIZATION + "=" + "4321")
    @ResponseBody
    public String hello() {
        return "hello";
    }

}

 

handler 에 설정한 headers 를 보면 HttpHeaders.AUTHORIZATION 의 값이 "4321" 인 경우에만 매핑하도록 설정했기 때문에 테스트를 다시 실행해보면 실패한다.

이 경우에도 앞서 AUTHORIZATION 이 없었을 때와 마찬가지로 404 오류를 발생한다.

테스트 코드에서 AUTHORIZATION 값을 "4321" 로 수정한 다음 실행하면 테스트가 성공한다.

 

package me.nimkoes;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.HttpHeaders;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void helloTest() throws Exception {

        mockMvc.perform(
                get("/hello")
                .header(HttpHeaders.AUTHORIZATION,"4321")
            )
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(content().string("hello"))
        ;

    }

}

 

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /hello
       Parameters = {}
          Headers = {Authorization=[4321]}
             Body = <no character encoding set>
    Session Attrs = {}

Handler:
             Type = me.nimkoes.MyHelloController
           Method = public java.lang.String me.nimkoes.MyHelloController.hello()

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {Content-Type=[text/plain;charset=UTF-8], Content-Length=[5]}
     Content type = text/plain;charset=UTF-8
             Body = hello
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

headers 설정과 관련하여 특정 header 가 있는 경우와 그 header 의 값이 특정한 값인 경우에 매핑하는 방법에 대해 알아보았다.

반대로 특정 header 가 없는 경우에 매핑하고 싶은 경우 ! 연산자를 사용해서 설정할 수 있다.

 

package me.nimkoes;

import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MyHelloController {

    @GetMapping(value = "/hello", headers = HttpHeaders.AUTHORIZATION + "=" + "4321")
    @ResponseBody
    public String hello() {
        return "hello";
    }

    @GetMapping(value = "/hi", headers = "!" + HttpHeaders.AUTHORIZATION)
    @ResponseBody
    public String hi() {
        return "hi";
    }

}

 

새로운 handler 를 정의 하였고, 이 handler 는 HttpHeaders.AUTHORIZATION 이 없는 경우 매핑되도록 설정 했다.

 

package me.nimkoes;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.HttpHeaders;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void helloTest() throws Exception {

        mockMvc.perform(
                get("/hello")
                .header(HttpHeaders.AUTHORIZATION,"4321")
            )
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(content().string("hello"))
        ;

        mockMvc.perform(
                get("/hi")
                .header(HttpHeaders.AUTHORIZATION,"1234")
            )
            .andDo(print())
            .andExpect(status().isNotFound())
        ;

        mockMvc.perform(get("/hi"))
            .andDo(print())
            .andExpect(status().isOk())
        ;

    }

}

 

지금까지 header 에 특정한 key 가 있어야 하는 경우, 있을 때 특정한 값이어야 하는 경우, 특정한 key 가 없어야 하는 경우에 대해 정리해 보았다.

추가로 parameter 에 대해서도 header 를 설정한 것과 매우 유사하게 사용할 수 있는데, 단순히 headers 대신 params 를 사용하면 된다.

 

    @GetMapping(value = "/helloParam", params = "test")
    @ResponseBody
    public String helloParam() {
        return "hello";
    }

 

HTTP request 를 이 handler 에 매핑하기 위해서는 "test" 라는 param 이 있어야 한다.

 

        mockMvc.perform(
                get("/helloParam")
                .param("test", "1234")
            )
            .andDo(print())
            .andExpect(status().isOk())
        ;

 

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /helloParam
       Parameters = {test=[1234]}
          Headers = {}
             Body = <no character encoding set>
    Session Attrs = {}

Handler:
             Type = me.nimkoes.MyHelloController
           Method = public java.lang.String me.nimkoes.MyHelloController.helloParam()

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {Content-Type=[text/plain;charset=UTF-8], Content-Length=[5]}
     Content type = text/plain;charset=UTF-8
             Body = hello
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

headers 에서와 마찬가지로 params 도 특정 param 이 없어야 하거나, 특정 값이어야 하는 경우 설정 방법이 동일하기 때문에 굳이 반복해서 정리하지 않기로 했다.

 

 


 

 

Spring Web MVC 를 사용하면 만들지 않아도 HTTP HEAD 와 OPTIONS 요청을 처리할 수 있다.

HTTP HEAD 명세HTTP OPTIONS 명세를 한 번 보면 좋을것 같다.

 

MyHelloController 에 GET 과 POST 요청을 받을 수 있도록 handler 를 수정했다.

 

package me.nimkoes;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MyHelloController {

    @RequestMapping(value = "/hello", method = {RequestMethod.GET, RequestMethod.POST})
    @ResponseBody
    public String hello() {
        return "hello";
    }

}

 

HTTP HEAD 와 OPTIONS 요청을 처리하도록 명시하지 않았지만, Spring Web MVC 를 사용하기 때문에 기본적으로 사용할 수 있다.

정말로 그런지 HEAD 와 OPTIONS 요청에 대한 결과를 테스트 코드를 통해 확인해 봤다.

 

package me.nimkoes;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.head;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void helloTest() throws Exception {

        mockMvc.perform(head("/hello"))
            .andDo(print())
            .andExpect(status().isOk())
        ;

        mockMvc.perform(options("/hello"))
            .andDo(print())
            .andExpect(status().isOk())
        ;

    }
}

 

 

MockHttpServletRequest:
      HTTP Method = HEAD
      Request URI = /hello
       Parameters = {}
          Headers = {}
             Body = <no character encoding set>
    Session Attrs = {}

Handler:
             Type = me.nimkoes.MyHelloController
           Method = public java.lang.String me.nimkoes.MyHelloController.hello()

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {Content-Type=[text/plain;charset=UTF-8], Content-Length=[5]}
     Content type = text/plain;charset=UTF-8
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

MockHttpServletRequest:
      HTTP Method = OPTIONS
      Request URI = /hello
       Parameters = {}
          Headers = {}
             Body = <no character encoding set>
    Session Attrs = {}

Handler:
             Type = org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping$HttpOptionsHandler
           Method = public org.springframework.http.HttpHeaders org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping$HttpOptionsHandler.handle()

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {Allow=[GET,HEAD,POST,OPTIONS]}
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

이곳을 통해 각 HTTP method 에 대한 명세를 확인할 수 있다.

 

 

 


 

 

마지막으로 커스텀 애노테이션을 만들어서 요청을 매핑하는 방법이 있다.

annotation 중에 다른 annotation 들을 조합해서 만든 annotation 을 composed annotation 이라고 한다.

@GetMapping 역시 composed annotation 이다.

 

// 생략...

package org.springframework.web.bind.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.core.annotation.AliasFor;

// 생략...

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.GET)
public @interface GetMapping {

    // 생략...

}

 

 

그리고 @Target, @Retention, @Documented, @RequestMapping 과 같이 다른 annotation 에 정의해서 사용할 수 있는 것을 meta annotation 이라고 한다.

 

uri 가 /hello 인 HTTP GET 요청을 처리하는 handler 라고 알려줄 수 있는 annotation 을 만들어 봤다.

 

 

GetHelloMapping 이라는 이름으로 annotation 을 하나 만들었다.

 

 

package me.nimkoes;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@RequestMapping(value = "/hello", method = RequestMethod.GET)
public @interface GetHelloMapping {
}

 

 

 

 

 

새로 만든 annotation 을 controller 의 handler 에 적용했다.

 

package me.nimkoes;

import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyHelloController {

    @GetHelloMapping
    public String hello() {
        return "hello";
    }

}

 

새로 만든 annotation 을 사용해서 handler 매핑이 잘 되는지 확인해보기 위해 테스트 코드를 만들어 실행해 봤다.

 

package me.nimkoes;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void helloTest() throws Exception {

        mockMvc.perform(get("/hello"))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(content().string("hello"))
        ;

    }
}

 

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /hello
       Parameters = {}
          Headers = {}
             Body = <no character encoding set>
    Session Attrs = {}

Handler:
             Type = me.nimkoes.MyHelloController
           Method = public java.lang.String me.nimkoes.MyHelloController.hello()

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {Content-Type=[text/plain;charset=UTF-8], Content-Length=[5]}
     Content type = text/plain;charset=UTF-8
             Body = hello
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

Java annotation 에 대해 예전에 정리했던 글을 링크로 남겨둔다.

 

 

 

 

728x90
728x90

 

 

리소스 핸들러는 HTML, CSS, Javascript, image 등 정적인 자원을 처리해주는 핸들러 이다.

 

정적 자원을 static resource, 동적 자원을 dynamic resource 라고도 부른다.

이 둘은 HTTP 요청에 따라 응답해주는 자원의 형태가 바뀔 수 있는지에 따라 구분할 수 있다.

 

웹 애플리케이션을 개발할 때 리소스를 요청하는 쪽을 클라이언트, 제공하는 쪽을 서버라고 한다.

이 때 일반적으로 생각하는 웹 요청을 처리하는 서버는 크게 두가지로 구분할 수 있다.

하나는 웹 서버로 정적인 자원 처리를 전담하고, 다른 하나는 웹 애플리케이션 서버로 동적인 자원 처리를 전담한다.

 

그럼 정적인 자원과 동적인 자원을 처리하는 서버를 구분하는 이유는 무엇일까.

쉽게 얘기하면 닭잡는 칼로 소를 잡지 않고, 소 잡는 칼로 닭을 잡지 않겠다는 뜻이다.

즉, 정적인 자원을 처리하는데 굳이 동적인 자원을 처리하는 서버가 처리해서 서버 자원을 사용중이라면, 동적인 자원을 처리해야할 때 정적인 자원을 처리중이라는 이유로 해야할 일을 하지 못할 수도 있기 때문이다.

(보안상의 이유도 있지만) 그렇기 때문에 이 둘을 논리적 또는 물리적으로 구분하여 구성하는것이 일반적이다.

 

최초 서버가 클라이언트로부터 리소스 요청을 받으면 웹 서버가 처리할 수 있는 정적인 자원에 대한 요청인지 확인한다.

맞을 경우 웹 서버는 굳이 웹 애플리케이션 서버에 이 요청을 보내지 않고 클라이언트에 리소스를 전달 한다.

그런데 만약 웹 서버가 처리할 수 없는 리소스 요청을 받으면 웹 서버는 웹 애플리케이션 서버에 이 요청을 전달해서 응답으로 전달 할 리소스를 동적으로 생성하도록 한다.

 

 

지금 생각나는 동적 자원으로 쇼핑몰에 등록 된 상품의 재고 수량이 몇개인지 확인하는걸 예로 들 수 있을것 같다.

상품의 재고 수량은 쇼핑몰의 상황에 따라 더 많아질 수도 있고, 판매를 많이 하면 줄어들 수도 있다.

즉, 같은 요청이라고 해도 매번 그 결과가 달라질 수 있고 또 이런 요청에 대한 처리는 웹 애플리케이션 서버에서 Servlet 이 데이터를 가공하여 클라이언트에 반환 해주어야 한다.

 

 

얘기가 조금 다른 길로 새어나갔는데, 다시 본론으로 돌아와서 리소스 핸들러는 정적인 자원을 처리해주는 핸들러이다.

 

모든 Servlet Container 에는 default Servlet 이라는게 등록되어 있다.

실제로 tomcat 을 설치한 다음 conf 디렉토리 하위의 web.xml 을 열어보면 다음과 같이 default Servlet 이 등록되어 있는것을 확인할 수 있다.

 

<!-- 생략... -->

    <servlet>
        <servlet-name>default</servlet-name>
        <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
        <init-param>
            <param-name>debug</param-name>
            <param-value>0</param-value>
        </init-param>
        <init-param>
            <param-name>listings</param-name>
            <param-value>false</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

<!-- 생략... -->

    <servlet>
        <servlet-name>jsp</servlet-name>
        <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
        <init-param>
            <param-name>fork</param-name>
            <param-value>false</param-value>
        </init-param>
        <init-param>
            <param-name>xpoweredBy</param-name>
            <param-value>false</param-value>
        </init-param>
        <load-on-startup>3</load-on-startup>
    </servlet>

<!-- 생략... -->

    <!-- The mapping for the default servlet -->
    <servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <!-- The mappings for the JSP servlet -->
    <servlet-mapping>
        <servlet-name>jsp</servlet-name>
        <url-pattern>*.jsp</url-pattern>
        <url-pattern>*.jspx</url-pattern>
    </servlet-mapping>

<!-- 생략... -->

 

 

이 Servlet 에 대한 class 파일이 있어서 무슨 내용인지 열어보기로 했다.

 

 

 

내용이 길어서 일부 눈에 들어온 부분만 가져와 봤다.

 

// 생략...

public class DefaultServlet extends HttpServlet {

    // 생략...
    
    public void init() throws ServletException {
        if (this.getServletConfig().getInitParameter("debug") != null) {
            this.debug = Integer.parseInt(this.getServletConfig().getInitParameter("debug"));
        }

        if (this.getServletConfig().getInitParameter("input") != null) {
            this.input = Integer.parseInt(this.getServletConfig().getInitParameter("input"));
        }

        if (this.getServletConfig().getInitParameter("output") != null) {
            this.output = Integer.parseInt(this.getServletConfig().getInitParameter("output"));
        }

        this.listings = Boolean.parseBoolean(this.getServletConfig().getInitParameter("listings"));
        if (this.getServletConfig().getInitParameter("readonly") != null) {
            this.readOnly = Boolean.parseBoolean(this.getServletConfig().getInitParameter("readonly"));
        }

        this.compressionFormats = this.parseCompressionFormats(this.getServletConfig().getInitParameter("precompressed"), this.getServletConfig().getInitParameter("gzip"));
        if (this.getServletConfig().getInitParameter("sendfileSize") != null) {
            this.sendfileSize = Integer.parseInt(this.getServletConfig().getInitParameter("sendfileSize")) * 1024;
        }

        this.fileEncoding = this.getServletConfig().getInitParameter("fileEncoding");
        this.globalXsltFile = this.getServletConfig().getInitParameter("globalXsltFile");
        this.contextXsltFile = this.getServletConfig().getInitParameter("contextXsltFile");
        this.localXsltFile = this.getServletConfig().getInitParameter("localXsltFile");
        this.readmeFile = this.getServletConfig().getInitParameter("readmeFile");
        if (this.getServletConfig().getInitParameter("useAcceptRanges") != null) {
            this.useAcceptRanges = Boolean.parseBoolean(this.getServletConfig().getInitParameter("useAcceptRanges"));
        }

        if (this.input < 256) {
            this.input = 256;
        }

        if (this.output < 256) {
            this.output = 256;
        }

        if (this.debug > 0) {
            this.log("DefaultServlet.init:  input buffer size=" + this.input + ", output buffer size=" + this.output);
        }

        this.resources = (WebResourceRoot)this.getServletContext().getAttribute("org.apache.catalina.resources");
        if (this.resources == null) {
            throw new UnavailableException("No resources");
        } else {
            if (this.getServletConfig().getInitParameter("showServerInfo") != null) {
                this.showServerInfo = Boolean.parseBoolean(this.getServletConfig().getInitParameter("showServerInfo"));
            }

        }
    }

    // 생략...

    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        if (req.getDispatcherType() == DispatcherType.ERROR) {
            this.doGet(req, resp);
        } else {
            super.service(req, resp);
        }
    }

    // 생략...

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        this.serveResource(request, response, true, this.fileEncoding);
    }

    // 생략...

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        this.doGet(request, response);
    }

    // 생략...

}

 

실제로 처리하는 부분은 this.serveResource 인데 너무 길어서 첨부하지는 않았다.

관심있으면 꼭 한번 열어봤으면 좋겠다.

 

Tomcat Servlet Container 의 default Servlet 에 대한 공식 문서를 첨부하니 이 내용 을 참고해도 좋을것 같다.

 

Spring 은 이렇게 등록 되어 있는 default Servlet 에 요청 처리를 위임해서 정적인 자원을 처리한다.

그리고 Spring Boot 를 사용할 경우 아무 설정을 하지 않아도 기본 설정으로 정적인 자원에 대한 처리가 되어 있다.

 

이 부분에 대해서 Spring Boot 의 WebMvcAutoConfiguration 클래스 파일에 등록하는 부분이 정의되어 있다.

 

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        if (!this.resourceProperties.isAddMappings()) {
            logger.debug("Default resource handling disabled");
            return;
        }
        Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
        CacheControl cacheControl = this.resourceProperties.getCache()
                .getCachecontrol().toHttpCacheControl();
        if (!registry.hasMappingForPattern("/webjars/**")) {
            customizeResourceHandlerRegistration(registry
                    .addResourceHandler("/webjars/**")
                    .addResourceLocations("classpath:/META-INF/resources/webjars/")
                    .setCachePeriod(getSeconds(cachePeriod))
                    .setCacheControl(cacheControl));
        }
        String staticPathPattern = this.mvcProperties.getStaticPathPattern();
        if (!registry.hasMappingForPattern(staticPathPattern)) {
            customizeResourceHandlerRegistration(
                    registry.addResourceHandler(staticPathPattern)
                            .addResourceLocations(getResourceLocations(
                                    this.resourceProperties.getStaticLocations()))
                            .setCachePeriod(getSeconds(cachePeriod))
                            .setCacheControl(cacheControl));
        }
    }

 

22번째 라인의 this.resourceProperties.getStaticLocations() 를 따라가보면

 

// 생략...

@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties {

    private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
            "classpath:/META-INF/resources/", "classpath:/resources/",
            "classpath:/static/", "classpath:/public/" };

    /**
     * Locations of static resources. Defaults to classpath:[/META-INF/resources/,
     * /resources/, /static/, /public/].
     */
    private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;

    /**
     * Whether to enable default resource handling.
     */
    private boolean addMappings = true;

    private final Chain chain = new Chain();

    private final Cache cache = new Cache();

    public String[] getStaticLocations() {
        return this.staticLocations;
    }

// 생략...

}

 

String[] 타입의 this.staticLocations 를 반환하는 getStaticLocations() 가 정의되어 있고, 이 변수는 final 변수 CLASSPATH_RESOURCE_LOCATIONS 에 정의된 기본 경로가 자동으로 등록 된다.

 

그리고 이 내용으로 한가지 더 알 수 있는 점은, application.properties 파일을 통해 spring.resources 를 사용해서 수정이 가능하다는 것이다.

 

 

실제로 매핑되는 Servlet 이 없을 경우 default Servlet 에 의해 static resource 를 사용할 수 있는지 확인해보자.

Spring Boot 를 사용하고 있고, 기본적으로 classpath:/static 을 사용하고 있기 때문에 이 위치에 html 파일을 하나 만들고 요청 결과를 확인해보자.

 

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>Hello Nimkoes</h1>
</body>
</html>

 

 

테스트 해보기 위해 기존에 만들어 둔 MyHelloControllerTest 에 테스트 케이스를 하나 더 추가했다.

 

package me.nimkoes;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class MyHelloControllerTest {

    // @WebMvcTest annotation 을 정의하면 MockMvc 를 주입받을 수 있다.
    @Autowired
    MockMvc mockMvc;

    @Test
    public void hello() throws Exception {
        this.mockMvc.perform(get("/hello")
                .param("name", "nimkoes"))
            .andDo(print())
            .andExpect(content().string("hello nimkoes"));
    }

    @Test
    public void staticTest() throws Exception {
        this.mockMvc.perform(get("/index.html"))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(content().string(Matchers.containsString("Hello Nimkoes")));
    }

}

 

 

아래쪽에 추가한 staticTest 테스트를 실행해보면 테스트가 성공한다.

 

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /index.html
       Parameters = {}
          Headers = {}
             Body = null
    Session Attrs = {}

Handler:
             Type = org.springframework.web.servlet.resource.ResourceHttpRequestHandler

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {Last-Modified=[Wed, 07 Jul 2021 13:40:37 GMT], Content-Length=[145], Content-Type=[text/html], Accept-Ranges=[bytes]}
     Content type = text/html
             Body = <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>Hello Nimkoes</h1>
</body>
</html>
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

 

마지막으로 직접 리소스 핸들러를 등록하는 방법을 정리해 본다.

이것 역시 WebMvcConfigurer 인터페이스를 오버라이딩 해서 핸들러를 추가할 수 있다.

 

 

package me.nimkoes;

import java.util.concurrent.TimeUnit;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.CacheControl;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new GreetingInterceptor())
            .addPathPatterns("/hi")
            .order(0);
        registry.addInterceptor(new AnotherInterceptor()).order(-1);
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/m/**")            // 어떤 패턴의 요청을 처리할지 정의
            .addResourceLocations("classpath:/m/")      // 리소스를 찾을 위치
            .setCacheControl(CacheControl.maxAge(10, TimeUnit.MINUTES));    // 리소스가 변경되지 않은 동안 캐싱을 유지할 시간
    }
}

 

classpath 중 resource 경로에 m 폴더를 만들고 테스트를 해보자.

 

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>Hello Mobile Nimkoes</h1>
</body>
</html>

 

 

 

그리고 다음과 같이 테스트 코드를 추가하여 테스트를 실행해 보았다.

 

    @Test
    public void staticTest_M() throws Exception {
        this.mockMvc.perform(get("/m/index.html"))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(content().string(Matchers.containsString("Hello Mobile Nimkoes")))
            .andExpect(header().exists(HttpHeaders.CACHE_CONTROL));
    }

 

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /m/index.html
       Parameters = {}
          Headers = {}
             Body = null
    Session Attrs = {}

Handler:
             Type = org.springframework.web.servlet.resource.ResourceHttpRequestHandler

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {Last-Modified=[Wed, 07 Jul 2021 13:59:20 GMT], Cache-Control=[max-age=600], Content-Length=[152], Content-Type=[text/html], Accept-Ranges=[bytes]}
     Content type = text/html
             Body = <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>Hello Mobile Nimkoes</h1>
</body>
</html>
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

테스트 내용이 의도한 대로 동작 하였으며, 아까와 달리 Header 에 추가 등록한 Cache-Control 에 대한 max-age 값도 600초 (10분) 으로 정상적으로 설정된 것을 볼 수 있었다.

 

 

추가로 addResourceHandler 로 리소스 핸들러를 등록할 때, 리소스를 찾는 경로로 classpath 뿐만 아니라 file system 을 줄 수도 있다.

그리고 classpath 또는 file 과 같은 값을 주지 않고 그냥 경로만 입력할 경우 src/main/webapp 경로를 사용 한다.

 

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/m/**")            // 어떤 패턴의 요청을 처리할지 정의
            .addResourceLocations("classpath:/m/", "file:/Users/nimkoes/files/", "/myStatic/")  // 리소스를 찾을 위치
            .setCacheControl(CacheControl.maxAge(10, TimeUnit.MINUTES));  // 리소스가 변경되지 않은 동안 캐싱을 유지할 시간
    }

 

위와 같이 작성한 경우 src/main/webapp/myStatic/ 에서도 /m/** 패턴의 요청에 대해 리소스를 찾는 경로로 사용할 수 있다.

 

 

 

 

728x90
728x90

 

 

HandlerInterceptor 에 대해 정리 한다.

HandlerInterceptor 는 HandlerMapping 에 적용할 수 있다.

HandlerMapping 은 요청을 처리 할 handler method 를 찾아주는 역할을 한다.

 

HandlerInterceptor 에 정의한 기능들은 HandlerMapping 이 찾아 준 handler method 에 적용 된다.

실습을 해보기에 앞서 HandlerInterceptor 에 대한 api 문서를 먼저 보자.

 

문서에 따르면 handler method 의 구현 내용을 수정하지 않고 공통으로 처리 할 작업들을 정의할 수 있다고 되어 있다.

 

이 인터페이스는 세가지 default method 를 가지고 있는 것으로 보아 굳이 구현하지 않아도 괜찮아 보인다.

 

package org.springframework.web.servlet;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.lang.Nullable;
import org.springframework.web.method.HandlerMethod;

public interface HandlerInterceptor {

    /**
     * Intercept the execution of a handler. Called after HandlerMapping determined
     * an appropriate handler object, but before HandlerAdapter invokes the handler.
     * <p>DispatcherServlet processes a handler in an execution chain, consisting
     * of any number of interceptors, with the handler itself at the end.
     * With this method, each interceptor can decide to abort the execution chain,
     * typically sending a HTTP error or writing a custom response.
     * <p><strong>Note:</strong> special considerations apply for asynchronous
     * request processing. For more details see
     * {@link org.springframework.web.servlet.AsyncHandlerInterceptor}.
     * <p>The default implementation returns {@code true}.
     * @param request current HTTP request
     * @param response current HTTP response
     * @param handler chosen handler to execute, for type and/or instance evaluation
     * @return {@code true} if the execution chain should proceed with the
     * next interceptor or the handler itself. Else, DispatcherServlet assumes
     * that this interceptor has already dealt with the response itself.
     * @throws Exception in case of errors
     */
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {

        return true;
    }

    /**
     * Intercept the execution of a handler. Called after HandlerAdapter actually
     * invoked the handler, but before the DispatcherServlet renders the view.
     * Can expose additional model objects to the view via the given ModelAndView.
     * <p>DispatcherServlet processes a handler in an execution chain, consisting
     * of any number of interceptors, with the handler itself at the end.
     * With this method, each interceptor can post-process an execution,
     * getting applied in inverse order of the execution chain.
     * <p><strong>Note:</strong> special considerations apply for asynchronous
     * request processing. For more details see
     * {@link org.springframework.web.servlet.AsyncHandlerInterceptor}.
     * <p>The default implementation is empty.
     * @param request current HTTP request
     * @param response current HTTP response
     * @param handler handler (or {@link HandlerMethod}) that started asynchronous
     * execution, for type and/or instance examination
     * @param modelAndView the {@code ModelAndView} that the handler returned
     * (can also be {@code null})
     * @throws Exception in case of errors
     */
    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
            @Nullable ModelAndView modelAndView) throws Exception {
    }

    /**
     * Callback after completion of request processing, that is, after rendering
     * the view. Will be called on any outcome of handler execution, thus allows
     * for proper resource cleanup.
     * <p>Note: Will only be called if this interceptor's {@code preHandle}
     * method has successfully completed and returned {@code true}!
     * <p>As with the {@code postHandle} method, the method will be invoked on each
     * interceptor in the chain in reverse order, so the first interceptor will be
     * the last to be invoked.
     * <p><strong>Note:</strong> special considerations apply for asynchronous
     * request processing. For more details see
     * {@link org.springframework.web.servlet.AsyncHandlerInterceptor}.
     * <p>The default implementation is empty.
     * @param request current HTTP request
     * @param response current HTTP response
     * @param handler handler (or {@link HandlerMethod}) that started asynchronous
     * execution, for type and/or instance examination
     * @param ex exception thrown on handler execution, if any
     * @throws Exception in case of errors
     */
    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
            @Nullable Exception ex) throws Exception {
    }

}

 

전체적인 실행 흐름을 다음과 같이 그림을 그려볼 수 있다.

 

 

preHandle 에서는 보통 Servlet 에서 사용하던 Filter 와 비슷하지만 보다 구체적인 작업을 할 수 있다.

예를 들어 preHandler 의 메소드 파라미터 중에 Object handler 가 있는 것을 볼 수 있다.

즉, Filter 와 다르게 어떤 class 의 어떤 handler method 가 호출 될 것인지에 대한 정보를 전달 받기 때문에 요청에 대해 보다 구체적인 처리가 가능하다.

 

preHandle 에서 한가지 더 눈여겨 볼 점은 return type 이 boolean 이라는 것인데, 이 메소드 실행 결과 true 를 반환하면 다음 interceptor 의 preHandler 등 처리를 진행하지만, 그렇지 않고 false 를 반환하면 요청 처리를 더 이상 진행하지 않고 멈춘다.

 

handler method 까지 실행을 마치고 나서 view 를 렌더링 하기 직전에 postHandle 을 실행 한다.

이 postHandle 은 ModelAndView 타입의 객체를 매개변수로 받는다.

그렇기 때문에 이 곳에서는 model 객체에 추가 정보를 넣거나 view 를 변경하는 등의 작업을 할 수 있다.

 

afterCompletion 은 veiw 렌더링을 마친 다음으로 모든 요청 처리가 끝난 다음에 호출 된다.

그리고 preHandler 의 반환 값이 true 인 경우 postHandle 실행과 상관 없이 실행 된다.

 

 

 

어떠한 요청 처리 전 후에 필요한 작업을 한다는 점에서 Servlet 의 Filter 와 Spring 의 HandlerInterceptor 는 별다른 차이가 없어 보일 수 있다.

하지만 앞서 정리했던 것처럼 Filter 는 HandlerInterceptor 만큼의 정보를 알고 있지 않다.

그렇다고 해서 무조건 HandlerInterceptor 를 쓰는게 좋은것은 아니다.

만약 전후처리 내용이 Spring 과 밀접한 관련이 있다면 당연히 HandlerInterceptor 를 사용하는게 좋고 또 그래야 하겠지만, 그렇지 않은 성격의 전후처리라면 여전히 Servlet 의 Filter 를 사용하는게 문맥상 더 자연스럽다고 생각한다.

 

예를 들어 XSS 공격은 특정 handler 에 대한 것도 아니고 Spring 과 무관한 내용이므로 HandlerInterceptor 가 아닌 Servlet Filter 에서 처리하도록 구현 되어야 한다.

 

 

 

그럼 마지막으로 HandlerInterceptor 를 구현해보자.

구현하는 방법은 이 인터페이스를 구현하는 클래스를 만들고 Interceptor 로 등록해주면 된다.

 

Interceptor 로 GreetingInterceptor 클래스를 만들었다.

 

 

package me.nimkoes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

public class GreetingInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("========== GreetingInterceptor preHandle ==========");
        System.out.println(">>>>>>>>>> handler");
        System.out.println("getClass : " + handler.getClass());
        System.out.println("toString : " + handler.toString());
        System.out.println();
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("========== GreetingInterceptor postHandle ==========");
        System.out.println("isEmpty : " + (modelAndView == null));
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("========== GreetingInterceptor afterCompletion ==========");
    }
}

 

 

특별한 기능을 넣지는 않고 단순히 테스트 해보기 위해 몇가지 값을 출력하도록 작성 했다.

구현한 HandlerInterceptor 를 등록하기 위해 WebMvcConfigurer 를 구현한 WebConfig 설정 파일에 다음과 같이 Interceptor 를 등록 하였다.

 

 

package me.nimkoes;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new GreetingInterceptor());
    }
}

 

그리고나서 미리 작성 되어있던 테스트 코드를 실행해서 결과를 확인해 보았다.

 

========== GreetingInterceptor preHandle ==========
>>>>>>>>>> handler
getClass : class org.springframework.web.method.HandlerMethod
toString : public java.lang.String me.nimkoes.MyHelloController.hello(me.nimkoes.Person)

========== GreetingInterceptor postHandle ==========
isEmpty : true
========== GreetingInterceptor afterCompletion ==========

 

preHandle 이 false 를 반환하도록 고친 다음 실행 해보았다.

 

========== GreetingInterceptor preHandle ==========
>>>>>>>>>> handler
getClass : class org.springframework.web.method.HandlerMethod
toString : public java.lang.String me.nimkoes.MyHelloController.hello(me.nimkoes.Person)

 

테스트 코드는 깨졌고 로그는 preHandle 까지만 출력 되었다.

 

GreetingInterceptor 의 preHandle 을 다시 true 를 반환 하도록 고치고 HandlerInterceptor 를 하나 더 추가 했다.

 

 

package me.nimkoes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

public class AnotherInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("========== AnotherInterceptor preHandle ==========");
        System.out.println(">>>>>>>>>> handler");
        System.out.println("getClass : " + handler.getClass());
        System.out.println("toString : " + handler.toString());
        System.out.println();
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("========== AnotherInterceptor postHandle ==========");
        System.out.println("isEmpty : " + (modelAndView == null));
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("========== AnotherInterceptor afterCompletion ==========");
    }
}

 

새로 만든 AnotherInterceptor 를 포함하여 두 개의 Interceptor 를 등록했다.

 

package me.nimkoes;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new GreetingInterceptor());
        registry.addInterceptor(new AnotherInterceptor());
    }
}

 

실행해보면 재밌는 결과가 출력 된 것을 볼 수 있다.

 

========== GreetingInterceptor preHandle ==========
>>>>>>>>>> handler
getClass : class org.springframework.web.method.HandlerMethod
toString : public java.lang.String me.nimkoes.MyHelloController.hello(me.nimkoes.Person)

========== AnotherInterceptor preHandle ==========
>>>>>>>>>> handler
getClass : class org.springframework.web.method.HandlerMethod
toString : public java.lang.String me.nimkoes.MyHelloController.hello(me.nimkoes.Person)

========== AnotherInterceptor postHandle ==========
isEmpty : true
========== GreetingInterceptor postHandle ==========
isEmpty : true
========== AnotherInterceptor afterCompletion ==========
========== GreetingInterceptor afterCompletion ==========

 

특별한 order 를 주지 않았을 경우 addInterceptor 순서대로의 order 를 갖는다.

그래서 preHandle 같은 경우 GreetingInterceptor 를 먼저 실행한 것은 너무 당연하다.

하지만 그 다음 postHandle 과 afterCompletion 는 역순으로 실행 되는 것을 볼 수 있다.

 

 

Interceptor 를 테스트 해본다고 handler method 에 로그를 남긴 다는 것을 깜빡했다.

다음과 같이 MyHelloController 의 hello 에 로그를 하나 출력 하도록 수정 했다.

 

 

package me.nimkoes;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyHelloController {

    @GetMapping("/hello")
    public String hello(@RequestParam("name") Person person) {
        System.out.println("handler method execute !!");
        return "hello " + person.getName();
    }

}

 

테스트 코드를 다시 한 번 실행해보면 preHandle 다음에 handler method 의 로그를 출력 하는 것을 볼 수 있다.

 

========== GreetingInterceptor preHandle ==========
>>>>>>>>>> handler
getClass : class org.springframework.web.method.HandlerMethod
toString : public java.lang.String me.nimkoes.MyHelloController.hello(me.nimkoes.Person)

========== AnotherInterceptor preHandle ==========
>>>>>>>>>> handler
getClass : class org.springframework.web.method.HandlerMethod
toString : public java.lang.String me.nimkoes.MyHelloController.hello(me.nimkoes.Person)

handler method execute !!
========== AnotherInterceptor postHandle ==========
isEmpty : true
========== GreetingInterceptor postHandle ==========
isEmpty : true
========== AnotherInterceptor afterCompletion ==========
========== GreetingInterceptor afterCompletion ==========

 

둘 중 하나의 preHandle 이 false 를 반환하면 어떤 결과가 나오는지 확인해 보았다.

우선 GreetingInterceptor 의 preHandle 이 false 를 반환하도록 했다.

 

========== GreetingInterceptor preHandle ==========
>>>>>>>>>> handler
getClass : class org.springframework.web.method.HandlerMethod
toString : public java.lang.String me.nimkoes.MyHelloController.hello(me.nimkoes.Person)

 

당연히 테스트는 실패 했고, GreetingInterceptor 의 preHandle 까지만 실행하고 다음 작업은 하지 않았다.

그럼 반대로 GreetingInterceptor 는 true 를 반환하고 AnotherInterceptor 는 false 를 반환 하도록 해보았다.

 

========== GreetingInterceptor preHandle ==========
>>>>>>>>>> handler
getClass : class org.springframework.web.method.HandlerMethod
toString : public java.lang.String me.nimkoes.MyHelloController.hello(me.nimkoes.Person)

========== AnotherInterceptor preHandle ==========
>>>>>>>>>> handler
getClass : class org.springframework.web.method.HandlerMethod
toString : public java.lang.String me.nimkoes.MyHelloController.hello(me.nimkoes.Person)

========== GreetingInterceptor afterCompletion ==========

 

재미있는 결과를 볼 수 있었는데, true 를 반환하는 Interceptor 에 대해서는 afterCompletion 은 실행 하는 것을 볼 수 있다.

결국 Interceptor chain 이 있을 때, 최초로 false 를 반환하는 Interceptor 까지는 afterCompletion 을 실행 하는것 같다.

그리고 하나라도 false 를 반환 한다면 handler method 는 실행되지 않는다는 것도 확인 할 수 있었다.

 

 

 

마지막으로 Interceptor chain 을 구성할 때 등록 순서에 상관 없이 order 를 줄 수 있다.

 

package me.nimkoes;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new GreetingInterceptor()).order(0);
        registry.addInterceptor(new AnotherInterceptor()).order(-1);
    }
}

 

숫자가 낮을수록 높은 우선순위를 갖는다.

따라서 지금 등록한 두 개의 Interceptor 의 preHandle 이 모두 true 를 반환 하도록 수정한 다음 테스트 코드를 실행하면 아까와는 다른 실행 순서를 확인할 수 있다.

 

========== AnotherInterceptor preHandle ==========
>>>>>>>>>> handler
getClass : class org.springframework.web.method.HandlerMethod
toString : public java.lang.String me.nimkoes.MyHelloController.hello(me.nimkoes.Person)

========== GreetingInterceptor preHandle ==========
>>>>>>>>>> handler
getClass : class org.springframework.web.method.HandlerMethod
toString : public java.lang.String me.nimkoes.MyHelloController.hello(me.nimkoes.Person)

handler method execute !!
========== GreetingInterceptor postHandle ==========
isEmpty : true
========== AnotherInterceptor postHandle ==========
isEmpty : true
========== GreetingInterceptor afterCompletion ==========
========== AnotherInterceptor afterCompletion ==========

 

이번에는 AnotherInterceptor 가 더 높은 우선순위를 가지고 실행된 것을 볼 수 있다.

 

그리고 만약 Interceptor 를 등록한 것은 좋은데, 모든 handler 에 대해 적용 되기를 바라지 않을 수 있다.

이런 경우 pattern 을 설정하여 특정 요청에 대해서만 Interceptor 가 동작하도록 할 수 있다.

 

다음은 GreetingInterceptor 에 대해 /hi 요청에 대해서만 동작하도록 설정한 코드이다.

 

package me.nimkoes;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new GreetingInterceptor())
            .addPathPatterns("/hi")
            .order(0);
        registry.addInterceptor(new AnotherInterceptor()).order(-1);
    }
}

 

========== AnotherInterceptor preHandle ==========
>>>>>>>>>> handler
getClass : class org.springframework.web.method.HandlerMethod
toString : public java.lang.String me.nimkoes.MyHelloController.hello(me.nimkoes.Person)

handler method execute !!
========== AnotherInterceptor postHandle ==========
isEmpty : true
========== AnotherInterceptor afterCompletion ==========

 

테스트 코드는 /hello 요청을 보내기 때문에 AnotherInterceptor 만 동작한 것을 확인할 수 있다.

 

등록한 각각의 Interceptor 에 대해 order 와 pattern 을 적정하게 사용하면 중복 코드를 줄이고 관리하기 편한 애플리케이션을 만들 수 있을것 같다.

 

 

 

 

728x90
728x90

 

 

이번에는 Spring Web MVC 에서 Formatter 를 설정하는 방법에 대해 정리 한다.

Formatter 인터페이스에 대한 api 문서를 보면 크게 두 가지 인터페이스를 상속 받고 있다.

Printer<T> 는 T 타입 객체를 문자열로 출력하는 방법에 대해, Parser<T> 는 문자열을 T 타입 객체로 출력하는 방법에 대한 것이다.

 

 

 

프로젝트에서 정리에 사용 할 내용만 남기고 다른 내용은 삭제 했다.

 

 

package me.nimkoes;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyHelloController {

    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }

}

 

GET 방식의 /hello 요청에 대해 Http Response Body 영역에 "hello" 문자열을 담아 응답하는 handler method 하나만 남기고 모두 삭제 하였다.

 

이 코드를 테스트 해보기 위해 테스트 코드를 만들었다.

IntelliJ IDEA 기준으로 handler method 영역에 커서를 위치 시키고 ctrl + shift + t 를 누르면 다음과 같이 테스트 코드 파일을 쉽게 만들 수 있다.

 

 

 

테스트 코드는 JUnit4 를 사용했다.

 

 

package me.nimkoes;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

import static org.junit.Assert.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;

@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {

    // @WebMvcTest annotation 을 정의하면 MockMvc 를 주입받을 수 있다.
    @Autowired
    MockMvc mockMvc;

    @Test
    public void hello() throws Exception {
        this.mockMvc.perform(get("/hello"))
            .andDo(print())
            .andExpect(content().string("hello"));
    }

}

 

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /hello
       Parameters = {}
          Headers = []
             Body = null
    Session Attrs = {}

Handler:
             Type = me.nimkoes.MyHelloController
           Method = me.nimkoes.MyHelloController#hello()

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"text/plain;charset=UTF-8", Content-Length:"5"]
     Content type = text/plain;charset=UTF-8
             Body = hello
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

그런데 만약 /hello 가 아닌 /hello/nimkoes 라고 요청을 하면 'hello' 가 아닌 'hello nimkoes' 응답을 원한다고 하자.

 

    @Test
    public void hello() throws Exception {
        this.mockMvc.perform(get("/hello/nimkoes"))
            .andDo(print())
            .andExpect(content().string("hello nimkoes"));
    }

 

이 테스트는 무조건 실패한다.

왜냐하면 handler method 를 수정하지 않았기 때문이다.

 

이 요청을 의도한 대로 처리할 수 있도록 handler method 를 다음과 같이 수정할 수 있다.

 

package me.nimkoes;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyHelloController {

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

}

 

@PathVariable annotation 을 사용해서 url path 의 값을 가져와 사용할 수 있다.

handler method 를 위와 같이 수정하면 테스트가 성공하지만 Formatter 를 사용하도록 고쳐보자.

Formatter 는 서두에 설명해 두었지만, 문자열을 객체로, 객체를 문자열로 변환할 수 있는 인터페이스이다.

 

즉, 문자열인 name 에 대해 다음과 같이 객체로 변환해서 입력받을 수 있다.

 

package me.nimkoes;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyHelloController {

    @GetMapping("/hello/{name}")
    public String hello(@PathVariable("name") Person person) {
        return "hello " + person.getName();
    }

}

 

변환에 사용 할 Person 클래스를 정의 한다.

 

 

 

package me.nimkoes;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class Person {

    String name;

}

 

현재 이 상태에서는 name 이 Person 타입의 객체에 들어갈 수 없다.

왜냐하면 컴퓨터는 이걸 어떻게 집어 넣어야 하는지 알 수 없기 때문이다.

그래서 Formatter 를 만들어서 어떻게 변환해야 하는지 알려주기 위해 Formatter<Person> 인터페이스를 구현하는 PersonFormatter 클래스를 새로 정의 했다.

 

 

package me.nimkoes;

import java.text.ParseException;
import java.util.Locale;
import org.springframework.format.Formatter;

public class PersonFormatter implements Formatter<Person> {

    @Override
    public Person parse(String text, Locale locale) throws ParseException {
        Person person = new Person();
        person.setName(text);
        return person;
    }

    // 현재 사용하고 있지 않지만 임시로 toString 을 반환하도록 구현
    @Override
    public String print(Person object, Locale locale) {
        return object.toString();
    }
}

 

이렇게 만든 Formatter 를 등록하는 방법은 크게 두 가지가 있다.

하나는 WebMvcConfigurer 를 사용해서 addFormatter 로 추가해 주는 방법과

Spring Boot 를 사용할 경우 bean 으로 등록해서 Formatter 를 자동 등록하는 방법이다.

 

 

우선 WebMvcConfigurer 를 사용해서 Formatter 를 등록해 보자.

WebMvcConfigurer 인터페이스를 구현하는 설정 파일을 하나 만들어 준다.

 

 

package me.nimkoes;

import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addFormatter(new PersonFormatter());
    }
}

 

addFormatter 에는 Formatter 뿐만 아니라 Converter 도 추가로 등록할 수 있다.

 

 

Converter 는 Formatter 보다 일반적인 경우에 사용할 수 있다.

예를 들면 변환 대상이 굳이 문자열이 아니어도 사용할 수 있다. Formatter 에 비해 더 좋아 보이는 듯 해보이기도 하지만 Web 과 관련해서는 문자열을 객체로 변환하는 경우가 더 많기 때문에 Formatter 를 더 많이 사용하게 된다.

 

Formatter 를 추가했기 때문에 이제는 Spring 은 문자열을 Person 타입의 객체로 어떻게 변환해야 하는지 알게 된다.

다시 한 번 테스트 코드를 실행해보면 이번엔 성공하는 것을 볼 수 있다.

 

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /hello/nimkoes
       Parameters = {}
          Headers = []
             Body = null
    Session Attrs = {}

Handler:
             Type = me.nimkoes.MyHelloController
           Method = me.nimkoes.MyHelloController#hello(Person)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"text/plain;charset=UTF-8", Content-Length:"13"]
     Content type = text/plain;charset=UTF-8
             Body = hello nimkoes
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

또한 Formatter 는 PathVariable 이 아닌 RequestParam 으로 전달해도 사용 할 수 있다.

RequestParam 은 query string 이라고 생각하면 된다.

테스트 코드의 GET 요청을 query string 을 사용하도록 수정한다.

 

package me.nimkoes;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

import static org.junit.Assert.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;

@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {

    // @WebMvcTest annotation 을 정의하면 MockMvc 를 주입받을 수 있다.
    @Autowired
    MockMvc mockMvc;

    @Test
    public void hello() throws Exception {
        this.mockMvc.perform(get("/hello")
                .param("name", "nimkoes"))
            .andDo(print())
            .andExpect(content().string("hello nimkoes"));
    }

}

 

handler method 도 @PathVariable 대신 @RequestParam 을 사용하도록 수정 한다.

 

package me.nimkoes;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyHelloController {

    @GetMapping("/hello")
    public String hello(@RequestParam("name") Person person) {
        return "hello " + person.getName();
    }

}

 

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /hello
       Parameters = {name=[nimkoes]}
          Headers = []
             Body = null
    Session Attrs = {}

Handler:
             Type = me.nimkoes.MyHelloController
           Method = me.nimkoes.MyHelloController#hello(Person)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"text/plain;charset=UTF-8", Content-Length:"13"]
     Content type = text/plain;charset=UTF-8
             Body = hello nimkoes
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

의도한 대로 동작했고 테스트도 성공 했다.

마지막으로 Spring Boot 를 사용할 때 Formatter 를 등록할 수 있는 방법을 사용해 본다.

무조건 이렇게 할 필요는 없다. 왜냐면 지금 사용중인 프로젝트도 Spring Boot 프로젝트라는 사실을 잊지 말아야 한다.

Spring Boot 프로젝트면 사용할 수 있는 방법이 하나 더 있을 뿐이다.

 

이 방법은 Formatter 를 bean 으로 등록하면 자동으로 Formatter 에 등록이 되는 방법이다.

그래서 WebMvcConfigurer 를 구현해서 addFormetter 에 추가해 주지 않아도 된다.

 

WebConfig 파일을 삭제 해버려도 되지만 내용만 지우기로 했다.

 

package me.nimkoes;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
}

 

그리고 PersonFormatter 를 @Component annotation 을 사용해서 Component Scan 의 대상이 될 수 있도록 하여 bean 으로 등록한다.

 

package me.nimkoes;

import java.text.ParseException;
import java.util.Locale;
import org.springframework.format.Formatter;
import org.springframework.stereotype.Component;

@Component
public class PersonFormatter implements Formatter<Person> {

    @Override
    public Person parse(String text, Locale locale) throws ParseException {
        Person person = new Person();
        person.setName(text);
        return person;
    }

    // 현재 사용하고 있지 않지만 임시로 toString 을 반환하도록 구현
    @Override
    public String print(Person object, Locale locale) {
        return object.toString();
    }
}

 

Spring Boot 버전이 올라가면서 구현이 조금 바뀐것 같긴 하지만, bean 으로만 등록 되어 있어도 자동으로 Formatter 로 등록해 주는 부분을 찾아 보았다.

Spring Boot 2.1.1.RELEASE 버전 기준으로 org.springframework.boot:spring-boot-autoconfigure 의 META-INF 하위 spring.factories 파일에 정의 된 자동 설정 중 WebMvcAutoConfiguration 클래스가 있다.

이 클래스는 @ConditionalOnMissingBean({WebMvcConfigurationSupport.class}) 가 정의되어 있다.

이 annotation 의 의미는 WebMvcConfigurationSupport 타입의 bean 이 없으면 자동설정을 한다는 것을 의미한다.

그리고 이 클래스는 @EnableWebMvc annotation 을 작성하면 @Import 하는 DelegatingWebMvcConfiguration.class 클래스가 상속 받은 클래스이다.

 

정리하면 @EnableWebMvc annotation 을 사용하지 않고 Spring Boot 의 Web 자동 설정을 사용하면 WebMvcAutoconfiguration 클래스를 실행해서 Web 관련 자동 설정을 해준다는 것을 의미한다.

 

그리고 이 자동 설정 내용 중에 다음 내용이 포함되어 있다.

 

    @Override
    public void addFormatters(FormatterRegistry registry) {
        for (Converter<?, ?> converter : getBeansOfType(Converter.class)) {
            registry.addConverter(converter);
        }
        for (GenericConverter converter : getBeansOfType(GenericConverter.class)) {
            registry.addConverter(converter);
        }
        for (Formatter<?> formatter : getBeansOfType(Formatter.class)) {
            registry.addFormatter(formatter);
        }
    }

    private <T> Collection<T> getBeansOfType(Class<T> type) {
        return this.beanFactory.getBeansOfType(type).values();
    }

 

beanFactory 에 등록 된 bean 의 타입 중 Formatter 타입의 bean 을 FormatterRegistry 에 자동으로 추가해주는 부분이다.

이런 내용이 있어서 @Component 로 Formatter<Person> 을 구현 한 PersonFormatter 클래스가 Formatter 로 자동 등록 될 수 있는 것이다.

 

 

다시 테스트 코드로 돌아가서 실행해보면 예상과 달리 테스트가 실패 한다.

 

 

하지만 애플리케이션을 구동해서 브라우저를 통해 실행해보면 의도한 대로 잘 동작하는 것을 확인할 수 있다.

 

 

그 이유는 테스트 코드에 @WebMvcTest annotation 을 사용했기 때문이다.

이 annotation 은 slice 테스트를 위한 것으로 Web 과 관련 된 bean 만 등록해서 테스트를 진행 한다.

@Component 는 Web 과 관련 된 bean 으로 인식하지 않기 때문에 의도한 결과를 받아보지 못한 것이다.

 

slice 테스트 관련된 대표적인 annotation 으로는 @WebMvcTest, @WebFluxTest, @DataJpaTest, @JsonTest, @RestClientTest 등이 있다.

그리고 @WebMvcTest 는 @Controller, @ControllerAdvice, @JsonComponent, Converter, GenericConverter, Filter, WebMvcConfigurer, HandlerMethodArgumentResolver 등에 대해서만 bean 으로 등록 한다.

 

조금 더 자세한 slice 테스트 관련한 내용을 링크로 남겨 둔다.

 

 

그럼 이 테스트는 성공할 수 없는 것일까.

그렇지 않다. slice 테스트를 하지 않으면 된다.

직접 PersonFormatter 를 테스트에 사용 할 bean 이라고 명시해주는 방법도 있지만 모든 bean 을 등록해서 테스트를 진행하도록 수정해 보았다.

 

@WebMvcTest 대신 @SpringBootTest annotation 을 사용 했고, 이렇게 하면 더이상 MockMvc 에 대한 의존성을 주입받을 수 없기 때문에 @AutoConfigureMockMvc annotation 을 추가로 작성해 주었다.

 

package me.nimkoes;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class MyHelloControllerTest {

    // @WebMvcTest annotation 을 정의하면 MockMvc 를 주입받을 수 있다.
    @Autowired
    MockMvc mockMvc;

    @Test
    public void hello() throws Exception {
        this.mockMvc.perform(get("/hello")
                .param("name", "nimkoes"))
            .andDo(print())
            .andExpect(content().string("hello nimkoes"));
    }

}

 

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /hello
       Parameters = {name=[nimkoes]}
          Headers = {}
             Body = null
    Session Attrs = {}

Handler:
             Type = me.nimkoes.MyHelloController
           Method = public java.lang.String me.nimkoes.MyHelloController.hello(me.nimkoes.Person)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {Content-Type=[text/plain;charset=UTF-8], Content-Length=[13]}
     Content type = text/plain;charset=UTF-8
             Body = hello nimkoes
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

모든 bean 을 등록 했기 때문에 @Component annoation 에 대해서도 bean 으로 등록 되었고, @EnableWebMvc 를 사용하지 않은 Spring Boot 자동 설정 내용을 기준으로 PersonFormatter 를 FormatterRegistry 에 등록해 주어 정상적으로 테스트가 성공하는 것을 확인할 수 있다.

 

 

 

 

728x90
728x90

 

 

Spring Boot 의 Web MVC 설정 방법에 대해 정리 한다.

환경이 바뀌었으니 새로운 프로젝트를 만드는 것도 좋겠지만 정리에 사용중인 프로젝트를 수정하려고 한다.

 

우선 POM.xml 파일을 수정해서 Spring Boot 를 사용할 수 있도록 수정한다.

 

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>me.nimkoes</groupId>
    <artifactId>SpringWebMVCSandbox</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <name>SpringWebMVCSandbox Maven Webapp</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.1.3.RELEASE</version>
        </dependency>
    </dependencies>

    <build>
        <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
            <plugins>
                <plugin>
                    <artifactId>maven-clean-plugin</artifactId>
                    <version>3.1.0</version>
                </plugin>
                <plugin>
                    <artifactId>maven-resources-plugin</artifactId>
                    <version>3.0.2</version>
                </plugin>
                <plugin>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.8.0</version>
                </plugin>
                <plugin>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <version>2.22.1</version>
                </plugin>
                <plugin>
                    <artifactId>maven-war-plugin</artifactId>
                    <version>3.2.2</version>
                </plugin>
                <plugin>
                    <artifactId>maven-install-plugin</artifactId>
                    <version>2.5.2</version>
                </plugin>
                <plugin>
                    <artifactId>maven-deploy-plugin</artifactId>
                    <version>2.8.2</version>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

 

Spring Boot 를 사용하기 위해

    1. parent POM 을 지정

    2. 의존성에 view template 과 spring web 과 관련한 starter 를 추가

    3. war 가 아닌 jar 패키징

    4. build 영역의 plugin 설정

을 수정 했다.

 

view template 과 관련 된 thymeleaf 와 lombok 그리고 starter test 는 굳이 추가하지 않아도 된다.

 

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.2</version>
        <relativePath/>
    </parent>

    <groupId>me.nimkoes</groupId>
    <artifactId>SpringWebMVCSandbox</artifactId>
    <version>1.0-SNAPSHOT</version>

    <name>SpringWebMVCSandbox Maven Webapp</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

 

그리고 Spring Boot 가 기본적으로 사용하는 디렉토리 구조를 맞춰주기 위해 src/main 경로에 resources, static, templates 그리고 application.properties 파일을 만들어 주었다.

이 경로와 파일은 따로 설정하지 않아도 Spring Boot 가 기본적으로 바라보고 사용하는 것이다.

static 경로의 리소스는 정적인 자원을, templates 는 view template 인 thymeleaf 를 사용할 경우 view 에 대한 리소스 기본 경로이다.

 

 

다음은 resources 폴더를 resource 로 인식할 수 있도록 IDE 설정을 해야 한다.

File > Project Structure 메뉴에 들어간 다음 Modules 의 Sources 탭에서 resources 폴더를 Resources 로 지정해 준다.

 

 

여기까지 수정한 다음 지금까지 해왔던 것처럼 WAS (tomcat 서버) 를 실행하면 정상적으로 동작하지 않는 것을 확인할 수 있다.

그건 Spring Boot 애플리케이션이 Spring Web MVC 만 사용헀을 때와 다른 점이 있기 떄문이다.

 

물론 Spring Boot 애플리케이션도 외부 WAS 를 사용할 수 있지만, 기본 컨셉은 '독립 실행 가능한 웹 애플리케이션'을 만드는 것이기 때문이다.

Spring Boot 는 Java application 을 실행하듯 웹 애플리케이션을 구동할 수 있고, 그렇기 때문에 WAS 도 내부에서 만들어 사용하고 DispatcherServlet 도 내부에서 만들어 내부 WAS 에 직접 등록해서 사용하도록 되어 있다.

일반적으로 알고 있던 WAS 위에 내가 만든 웹 애플리케이션이 배포되어 구동하는 것이 아닌, 자바 애플리케이션 안에 WAS 가 만들어져 사용하는 구조이다.

 

지금 상태는 Spring Boot 를 사용할 수 있는 준비는 했지만, 사용하고 있지는 않은 상태이다.

'독립 실행 가능한 웹 애플리케이션' 이라는 말을 기억하면서 Spring Boot 를 사용한 웹 애플리케이션을 구동해 보자.

 

서블릿과 같은 특별한 형태를 제외하고 모든 일반적인 Java 프로그램의 시작점은 main 메소드이다.

어떤 이름을 사용해도 상관 없지만 프로그램의 시작점이 될 App.class 파일을 만들고 main 메소드를 실행하면 Spring Boot 애플리케이션을 구동하도록 작성 하였다.

 

 

그리고 현재 사용하지 않는 설정 파일인 MyWebConfig 와 WebApplication 클래스 파일을 삭제 했다.

 

package me.nimkoes;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }

}

 

이제 App.class 클래스를 Java Application 을 실행하듯 구동 시켜보면 정상적으로 동작 하는 것을 확인할 수 있다.

 

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.5.2)

2021-07-04 15:59:13.816  INFO 24428 --- [           main] me.nimkoes.App                           : Starting App using Java 1.8.0_181 on DESKTOP-ISRSTA1 with PID 24428 (D:\_\ijProjects\SpringWebMVCSandbox\target\classes started by resor in D:\_\ijProjects\SpringWebMVCSandbox)
2021-07-04 15:59:13.818  INFO 24428 --- [           main] me.nimkoes.App                           : No active profile set, falling back to default profiles: default
2021-07-04 15:59:14.618  INFO 24428 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2021-07-04 15:59:14.625  INFO 24428 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2021-07-04 15:59:14.625  INFO 24428 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.48]
2021-07-04 15:59:14.747  INFO 24428 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2021-07-04 15:59:14.747  INFO 24428 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 884 ms
2021-07-04 15:59:14.969  WARN 24428 --- [           main] ion$DefaultTemplateResolverConfiguration : Cannot find template location: classpath:/templates/ (please add some templates or check your Thymeleaf configuration)
2021-07-04 15:59:15.024  INFO 24428 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2021-07-04 15:59:15.031  INFO 24428 --- [           main] me.nimkoes.App                           : Started App in 1.534 seconds (JVM running for 2.314)
2021-07-04 15:59:21.257  INFO 24428 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2021-07-04 15:59:21.257  INFO 24428 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2021-07-04 15:59:21.257  INFO 24428 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 0 ms

 

Console 영역을 보면 애플리케이션이 정상적으로 구동 되었고, 8080 포트에 서버가 올라온 것도 확인할 수 있다.

꽤 오래전에 만들어 두었던 MyHelloController 에 Get 요청을 처리하는 /hello 요청을 보내보면 다음과 같이 정상 실행 되는 것을 볼 수 있다.

 

 

이것으로 Spring Web MVC 만 사용했던 애플리케이션을 Spring Boot 를 사용하는 애플리케이션으로 전환이 완료 되었다.

실제로 어떤 bean 이 등록 되어 있는지 확인해 보기 위해 DispatcherServlet 클래스의 doService 메소드에 debugger 를 설정하고 확인해 보았다.

 

 

기본적으로 등록되는 bean 중 handlerMapping, handlerAdapter 그리고 viewResolver 위주로 확인해 보았다.

꽤 많은 handler 가 기본적으로 등록 되어 있었다.

viewResolver 의 경우 ContentNegotiatingViewResolver 가 등록 되어 있는데, 이 bean 은 직접 처리하지 않고 다른 viewResolver 에게 처리를 위임하는 역할을 한다.

그래서 ContentNegotiatingViewResolver 안에 viewResolver 가 따로 List 형태로 참조 되어 있고, 이 내용은 viewResolvers 에 정의되어 있는 것과 동일하다.

ContentNegotiatingViewResolver 는 http request header 정보를 분석해서 어떤 viewResolver 에게 처리를 위임할지 정하게 된다.

 

 

그럼 이렇게 자동으로 등록 된 bean 들에 대한 정보는 어디에서 왔을까.

Spring Web MVC 에서는 DispatcherServlet.properties 파일을 참조해서 가지고 왔는데, Spring Boot 에서는 spring.factories 라는 파일을 참조 한다.

 

이 파일 내용 중 'org.springframework.boot.autoconfigure.EnableAutoConfiguration' 이 있는데, 여기에 작성되어 있는 자동 설정을 사용하여 baen 이 등록 된다.

수많은 자동 설정 내용 중에 자세히 살펴볼 내용 중 하나는 web 설정과 관련 된 WebMvcAutoConfiguration 이다.

 

// 생략 ...

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {

    // 생략 ...

}

 

자동 설정 클래스 파일에 작성 한 annotation 중에 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) 가 있는데, 이 타입의 클래스 파일이 없는 경우에 Web MVC 자동 설정을 하겠다는 것을 의미한다.

 

WebMvcConfigurationSupport.class 클래스는 지난번 DelegatingWebMvcConfiguration.class 의 부모 클래스였다.

DelegatingWebMvcConfiguration.class 클래스는 @EnableWebMvc annotation 이 @Import 하는 설정 파일 이었다.

즉, 만약 @EnableWebMvc annotation 을 사용해서 Web MVC 설정을 하게 된다면 Spring Boot 가 기본 제공하는 web 관련 자동 설정을 사용하지 않게 됨을 뜻한다.

 

 

지난번 내용을 포함하여 Web 설정 방법에 대해 정리 하면 다음과 같다.

 

1. Spring Boot 의 MVC 자동 설정 사용 + 기능 확장 (추가설정)

    → @Configuration + Implements WebMvcConfigurer

2. Spring Boot 의 MVC 자동 설정 사용하지 않음 + 기능 확장 (추가설정)

    → @Configuration + Implements WebMvcConfigurer + @EnableWebMvc

 

그렇다고 이 방법만 있는 것은 절대 아니며, 위와 같이 설정하는게 일반적이고 직접 bean 을 등록 하는 것보다 편리할 뿐이다.

하지만 1, 2번 방법으로 설정을 하는 방법은 어쨌든 java 코드를 작성 해야 하는데, java 코드를 작성하지 않고 설정을 변경할 수 있는 방법이 있다.

Spring Boot 의 Web MVC 자동 설정과 관련된 WebMvcAutoConfiguration.class 클래스 파일을 보면 properties 들을 꽤 많이 참조하고 있는것을 알 수 있다.

이 properties 들은 기본적으로 resources 로 지정된 디렉토리 하위의 application.properties 파일의  내용을 참조 하도록 되어 있다.

 

그래서 대부분의 설정 값을 수정하는 방법으로 application.properties 파일을 사용하고, 개발, 테스트, 스테이징, 운영 등 환경에 따라 다른 설정을 해야하는 경우 이 파일 기준으로 관리하는게 좋은 방법이라고 생각 한다.

 

 

 

마지막으로 war 배포에 대해 정리해 본다.

다들 jar, war 라고 하는데 정확한 의미에 대해 모르고 쓰는 사람들도 있는것 같다.

 

jar 는 java archive (자바 아카이브) 의 줄임말로 class 파일들의 묶음이라고 생각해도 큰 무리는 없을것 같다.

단순히 class 파일 묶음 이라고 생각해도 좋지만 조금 더 나아가서 '실행 가능한 jar' 라는 말이 있다.

class 파일을 실행할 수 있다는 것은 어딘가에 main 메소드가 있다는 것을 뜻한다.

하지만 컴퓨터는 어디에 main 메소드가 있는지 알지 못한다.

그래서 실행 가능한 jar 파일은 보통 최상단에 META-INF 디렉토리를 만들고 그 안에 있는 MENIFEST.MF 파일에 Main-Class 를 정의하여 java -jar 명령으로 jar 를 실행할 때 시작 점을 컴퓨터에 알려준다.

 

위에 만든 Spring Boot 애플리케이션을 maven package 를 실행하여 target 디렉토리에 jar 파일을 만들면 다음과 같은 결과를 볼 수 있다.

 

 

다음으로 war 는 web application archive (웹 애플리케이션 아카이브) 의 줄임말로 웹 애플리케이션과 관련된 리소스들을 묶은 파일이다.

이 파일은 WAS (web application server) 에 압축이 풀리면서 배포가 되는 목적을 가지고 있다.

그래서 내용을 보면 WAS 가 이해할 수 있는 구조를 가지도록 파일들이 각자의 위치에 배치되어 있는 것을 볼 수 있다.

 

war 배포에 대해 정리 해본다고 했는데, Spring Boot 로 만드는 웹 애플리케이션을 war package 형태로 배포하기 위해서는 몇가지 작업을 해주어야 한다.

 

 

우선 POM.xml 파일에 packaging 을 war 로 수정한다.

 

 

그 다음 SpringBootServletInitializer 클래스를 상속 받는 임의의 클래스를 하나 정의 한다.

지금은 ServletInitializer 라는 이름을 사용 했다.

 

 

package me.nimkoes;

import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;

public class ServletInitializer extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(App.class);
    }

}

 

SpringBootServletInitializer.class 클래스에 대한 api 문서를 보면 왜 이 클래스를 상속 받은 클래스가 있어야 하는지 알 수 있다.

 

An opinionated WebApplicationInitializer to run a SpringApplication from a traditional WAR deployment. Binds Servlet, Filter and ServletContextInitializer beans from the application context to the server.

To configure the application either override the configure(SpringApplicationBuilder) method (calling SpringApplicationBuilder.sources(Class...)) or make the initializer itself a @Configuration. If you are using SpringBootServletInitializer in combination with other WebApplicationInitializers you might also want to add an @Ordered annotation to configure a specific startup order.


Note that a WebApplicationInitializer is only needed if you are building a war file and deploying it. If you prefer to run an embedded web server then you won't need this at all.

 

이 클래스에 대한 마지막 설명을 보면 embedded web server 를 사용하려 한다면 굳이 이 클래스를 사용 할 필요가 없다고 하고 있다.

왜냐하면 war 포맷으로 packaging 한다는 것은 웹 애플리케이션 외부에 있는 WAS 에서 실행한다는 의미가 강하고, jar 포맷으로 packaging 한다는 것은 웹 애플리케이션 내부에 있는 WAS 에서 실행한다는 의미이기 때문이다.

 

 

IDE 설정을 local 에 설치한 tomcat 으로 맞춰준 다음 실행하면 이번에는 다음과 같이 외부 WAS 에 war 파일이 배포되어 실행되는 것을 확인할 수 있다.

 

 

조금 잘려 나오긴 했지만, 자바 애플리케이션을 구동하여 실행했을 때와 로그를 비교해보면 독립 실행 했을 때와 WAS 위에 배포했을 때의 다른 점을 로그를 통해 알 수 있다.

독립 실행 했을 때는 'Spring' 배너가 로깅 되는 것을 시작으로 WAS 에 대한 port 정보 등이 로깅 되었었는데, WAS 에 war 를 배포했을 때는 WAS 가 구동하는 로그가 먼저 출력 되고 Sping boot 애플리케이션을 구동하고 나서는 WAS 에 대한 port 정보가 출력되지 않고 있는것을 볼 수 있다.

 

 

마지막에 다룬 war 배포 관련하여 maven 에 대해 정리할 때 관련있는 내용을 조금 다룬적이 있었다.

링크로 남겨둔 게시물의 아래쪽에 정리 된 'war packaging custom' 내용을 참고해도 좋을것 같다.

 

 

 

 

728x90
728x90

 

 

framework 와 library 는 명확한 차이가 있다.

library 는 사용자가 능동적으로 어떤 도구를 사용할지 고르고 골라 필요한걸 선택해서 사용한다.

framework 는 이미 구조가 짜여져 있고, 고정된 틀과 흐름이 있고 사용자는 그 흐름에 맞춰 원하는 결과가 나올 수 있도록 재료들을 제공해줘야 한다.

 

좀 더 쉽게 정리하면 library 는 사용자가 골라서 사용하는 거고, framework 는 사용자가 맞춰줘야 한다.

 

갑자기 library 와 framework 의 차이를 정리한 이유는 Spring 은 library 가 아니고 framework 이기 때문이다.

즉, framework 의 생명주기(lifecycle)에 대해 알고 언제 무슨 일이 일어나며 그 일이 일어날 때 사용자가 무엇을 제공해 주어야 원하는 결과를 받아볼 수 있기 때문이다.

 

그런 의미에서 지금부터 Spring Web MVC 설정하는 방법에 대해 정리한다.

 

 

 

앞선 내용에서 자주 언급 했지만, Servlet WebApplicationContext 를 만드는 DispatcherServlet 을 사용해서 Spring Web MVC 를 사용하면 DispatcherServlet.properties 파일을 참고하여 기본 전략이 설정 된다.

만약 이 기본 설정을 바꾸고 싶으면 그 타입의 bean 을 등록하여 덮어 쓰거나 추가할 수 있다.

그 예로 ViewResolver 타입의 InternalResourceViewResolver 클래스를 bean 으로 등록하여 prefix 와 suffix 설정을 추가한 ViewResolver 를 사용하도록 했었다.

 

    @Bean
    public ViewResolver myCustomViewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/");
        viewResolver.setSuffix(".jsp");

        return viewResolver;
    }

 

하지만 일반적으로 이렇게 사용 할 bean을 설정하는 일은 거의 없다.

왜냐하면 보편적으로 많이 사용하는 Web 설정에 대해 Spring 에서 @EnableWebMvc annotation 을 지원하기 때문이다.

 

package me.nimkoes;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration
@ComponentScan
@EnableWebMvc
public class MyWebConfig {

    @Bean
    public ViewResolver myCustomViewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/");
        viewResolver.setSuffix(".jsp");

        return viewResolver;
    }

}

 

@EnableWebMvc annotation 을 열어보면 DelegatingWebMvcConfiguration.class 를 import 하고 있다.

주석을 삭제한 코드는 다음과 같다.

 

package org.springframework.web.servlet.config.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.context.annotation.Import;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}

 

DelegatingWebMvcConfiguration.class 파일을 열어보면 WebMvcConfigurationSupport.class 를 상속 받고 있는것을 볼 수 있다.

 

package org.springframework.web.servlet.config.annotation;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.Validator;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.HandlerExceptionResolver;

/**
 * A subclass of {@code WebMvcConfigurationSupport} that detects and delegates
 * to all beans of type {@link WebMvcConfigurer} allowing them to customize the
 * configuration provided by {@code WebMvcConfigurationSupport}. This is the
 * class actually imported by {@link EnableWebMvc @EnableWebMvc}.
 *
 * @author Rossen Stoyanchev
 * @since 3.1
 */
@Configuration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {

    private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();


    @Autowired(required = false)
    public void setConfigurers(List<WebMvcConfigurer> configurers) {
        if (!CollectionUtils.isEmpty(configurers)) {
            this.configurers.addWebMvcConfigurers(configurers);
        }
    }

    // 생략...

}

 

WebMvcConfigurationSupport.class 파일을 열어보면 실제로 등록하는 bean 들을 볼 수 있다.

내용을 첨부 하기에는 너무 길기 때문에 관심이 있으면 직접 열어보고 대신 WebMvcConfigurationSupport.class api 문서를 링크로 첨부 한다.

문서를 통해서도 어떤 bean 을 등록하는지에 대한 정보도 확인할 수 있다.

 

This is the main class providing the configuration behind the MVC Java config.
It is typically imported by adding @EnableWebMvc to an application @Configuration class.
An alternative more advanced option is to extend directly from this class and override methods as necessary, remembering to add @Configuration to the subclass and @Bean to overridden @Bean methods.
For more details see the javadoc of @EnableWebMvc.

This class registers the following HandlerMappings:

 - RequestMappingHandlerMapping ordered at 0 for mapping requests to annotated controller methods.
 - HandlerMapping ordered at 1 to map URL paths directly to view names.
 - BeanNameUrlHandlerMapping ordered at 2 to map URL paths to controller bean names.
 - RouterFunctionMapping ordered at 3 to map router functions.
 - HandlerMapping ordered at Integer.MAX_VALUE-1 to serve static resource requests.
 - HandlerMapping ordered at Integer.MAX_VALUE to forward requests to the default servlet.


Registers these HandlerAdapters:

 - RequestMappingHandlerAdapter for processing requests with annotated controller methods.
 - HttpRequestHandlerAdapter for processing requests with HttpRequestHandlers.
 - SimpleControllerHandlerAdapter for processing requests with interface-based Controllers.
 - HandlerFunctionAdapter for processing requests with router functions.


Registers a HandlerExceptionResolverComposite with this chain of exception resolvers:

 - ExceptionHandlerExceptionResolver for handling exceptions through ExceptionHandler methods.
 - ResponseStatusExceptionResolver for exceptions annotated with ResponseStatus.
 - DefaultHandlerExceptionResolver for resolving known Spring exception types


Registers an AntPathMatcher and a UrlPathHelper to be used by:

 - the RequestMappingHandlerMapping,
 - the HandlerMapping for ViewControllers
 - and the HandlerMapping for serving resources

Note that those beans can be configured with a PathMatchConfigurer.


Both the RequestMappingHandlerAdapter and the ExceptionHandlerExceptionResolver are configured with default instances of the following by default:

 - a ContentNegotiationManager
 - a DefaultFormattingConversionService
 - an OptionalValidatorFactoryBean if a JSR-303 implementation is available on the classpath
 - a range of HttpMessageConverters depending on the third-party libraries available on the classpath.

 

WebMvcConfigurationSupport.class 에서 눈여겨 볼 만한 부분이 있다.

 

    /**
     * Provide access to the shared handler interceptors used to configure
     * {@link HandlerMapping} instances with.
     * <p>This method cannot be overridden; use {@link #addInterceptors} instead.
     */
    protected final Object[] getInterceptors() {
        if (this.interceptors == null) {
            InterceptorRegistry registry = new InterceptorRegistry();
            addInterceptors(registry);
            registry.addInterceptor(new ConversionServiceExposingInterceptor(mvcConversionService()));
            registry.addInterceptor(new ResourceUrlProviderExposingInterceptor(mvcResourceUrlProvider()));
            this.interceptors = registry.getInterceptors();
        }
        return this.interceptors.toArray();
    }

    /**
     * Override this method to add Spring MVC interceptors for
     * pre- and post-processing of controller invocation.
     * @see InterceptorRegistry
     */
    protected void addInterceptors(InterceptorRegistry registry) {
    }

 

전통적인 servlet 생명주기에서 보았던 interceptor 를 java 코드로 추가할 수 있도록 장치가 마련되어 있다.

InterceptorRegistry 클래스를 열어보니 ORDER 정보로 정렬 한 ArrayList 타입의 값을 사용하도록 되어 있었다.

 

 

@EnableWebMvc annotation 을 사용하기 전과 후에 어떤 bean 이 등록 되는지 비교해 보았다.

실제로 어떤 bean 이 등록 되는지 보기 위해 DispatcherServlet 클래스의 initStrategis 메소드에 debgger 를 설정했다.

 

 

@EnableWebMvc annotation 을 사용할 때 주의할 점이 있는데, ServletContext 를 반드시 설정해 주어야 한다.

그렇지 않으면 bean 설정이 제대로 되지 않고 오류가 발생 한다.

 

package me.nimkoes;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

public class WebApplication implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();

        /*
         * @EnableWebMvc annotation 을 사용하면
         * DispatcherServlet 이 ServletContext 를 참조하기 때문에 반드시 설정해 주어야 정상적으로 동작 한다.
         */
        context.setServletContext(servletContext);
        context.register(MyWebConfig.class);
        context.refresh();

        // DispatcherServlet 을 만들 때 설정 정보를 가지고 있는 context 를 넘겨 준다.
        DispatcherServlet dispatcherServlet = new DispatcherServlet(context);

        // ServletContext 에 DispatcherServlet 을 등록 한다.
        ServletRegistration.Dynamic app = servletContext.addServlet("app", dispatcherServlet);
        app.addMapping("/app/*");
    }
}

 

왼쪽은 @EnableWebMvc annotation 을 사용하지 않았을 때, 오른쪽은 사용했을 경우이다.

handlerMappings, handlerAdapters, viewResolvers 세 항목 위주로 다른 점을 확인 해보았다.

 

 

handlerMappings 와 handlerAdapters 를 보면 등록 된 baen 은 같은데 순서가 달라졌다.

작은 차이이지만 이 순서도 마냥 무시할 수 있는것은 아닌데, 처리 가능한 handler 를 찾기 위해 반복문을 수행 하는데, 더 자주 사용하는 bean 을 배열 인덱스의 앞쪽에 위치 함으로 반복 횟수를 조금이라도 줄일 수 있다.

 

viewResolvers 는 @EnableWebMvc annotation 을 사용했을 때 ViewResolverComposite bean 이 하나 더 등록 되는것을 볼 수 있었다.

 

 

마지막으로 확인해볼 내용은 DelegatingWebMvcConfiguration.class 이다.

이 클래스는 @EnableWebMvc annotation 이 import 하고 있는 클래스이다.

본문 앞쪽에서는 이 클래스에 대해 WebMvcConfigurationSupport.class 클래스를 상속 받는다고 단순하게 정리하고 넘어갔다.

하지만 이 클래스는 Spring Web MVC 를 사용할 때 기능을 손쉽게 확장할 수 있도록 해주는 중요한 클래스이다.

 

이 delegation 구조를 활용해서 기능을 확장하는 방법은 WebMvcConfigurer 인터페이스를 구현하는 것이다.

인터페이스를 구현 한다고 해서 확장하지도 않을 기능에 대한 추상 메소드를 구현해야만 하는 일은 없다.

왜냐하면 이 인터페이스는 전부 default 메소드를 사용하기 때문이다.

 

/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.web.servlet.config.annotation;

import java.util.List;

import org.springframework.core.convert.converter.Converter;
import org.springframework.format.Formatter;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.Validator;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;

/**
 * Defines callback methods to customize the Java-based configuration for
 * Spring MVC enabled via {@code @EnableWebMvc}.
 *
 * <p>{@code @EnableWebMvc}-annotated configuration classes may implement
 * this interface to be called back and given a chance to customize the
 * default configuration.
 *
 * @author Rossen Stoyanchev
 * @author Keith Donald
 * @author David Syer
 * @since 3.1
 */
public interface WebMvcConfigurer {

    /**
     * Helps with configuring HandlerMappings path matching options such as trailing slash match,
     * suffix registration, path matcher and path helper.
     * Configured path matcher and path helper instances are shared for:
     * <ul>
     * <li>RequestMappings</li>
     * <li>ViewControllerMappings</li>
     * <li>ResourcesMappings</li>
     * </ul>
     * @since 4.0.3
     */
    default void configurePathMatch(PathMatchConfigurer configurer) {
    }

    /**
     * Configure content negotiation options.
     */
    default void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
    }

    /**
     * Configure asynchronous request handling options.
     */
    default void configureAsyncSupport(AsyncSupportConfigurer configurer) {
    }

    /**
     * Configure a handler to delegate unhandled requests by forwarding to the
     * Servlet container's "default" servlet. A common use case for this is when
     * the {@link DispatcherServlet} is mapped to "/" thus overriding the
     * Servlet container's default handling of static resources.
     */
    default void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
    }

    /**
     * Add {@link Converter Converters} and {@link Formatter Formatters} in addition to the ones
     * registered by default.
     */
    default void addFormatters(FormatterRegistry registry) {
    }

    /**
     * Add Spring MVC lifecycle interceptors for pre- and post-processing of
     * controller method invocations. Interceptors can be registered to apply
     * to all requests or be limited to a subset of URL patterns.
     * <p><strong>Note</strong> that interceptors registered here only apply to
     * controllers and not to resource handler requests. To intercept requests for
     * static resources either declare a
     * {@link org.springframework.web.servlet.handler.MappedInterceptor MappedInterceptor}
     * bean or switch to advanced configuration mode by extending
     * {@link org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport
     * WebMvcConfigurationSupport} and then override {@code resourceHandlerMapping}.
     */
    default void addInterceptors(InterceptorRegistry registry) {
    }

    /**
     * Add handlers to serve static resources such as images, js, and, css
     * files from specific locations under web application root, the classpath,
     * and others.
     */
    default void addResourceHandlers(ResourceHandlerRegistry registry) {
    }

    /**
     * Configure cross origin requests processing.
     * @since 4.2
     */
    default void addCorsMappings(CorsRegistry registry) {
    }

    /**
     * Configure simple automated controllers pre-configured with the response
     * status code and/or a view to render the response body. This is useful in
     * cases where there is no need for custom controller logic -- e.g. render a
     * home page, perform simple site URL redirects, return a 404 status with
     * HTML content, a 204 with no content, and more.
     */
    default void addViewControllers(ViewControllerRegistry registry) {
    }

    /**
     * Configure view resolvers to translate String-based view names returned from
     * controllers into concrete {@link org.springframework.web.servlet.View}
     * implementations to perform rendering with.
     * @since 4.1
     */
    default void configureViewResolvers(ViewResolverRegistry registry) {
    }

    /**
     * Add resolvers to support custom controller method argument types.
     * <p>This does not override the built-in support for resolving handler
     * method arguments. To customize the built-in support for argument
     * resolution, configure {@link RequestMappingHandlerAdapter} directly.
     * @param resolvers initially an empty list
     */
    default void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
    }

    /**
     * Add handlers to support custom controller method return value types.
     * <p>Using this option does not override the built-in support for handling
     * return values. To customize the built-in support for handling return
     * values, configure RequestMappingHandlerAdapter directly.
     * @param handlers initially an empty list
     */
    default void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
    }

    /**
     * Configure the {@link HttpMessageConverter HttpMessageConverters} to use for reading or writing
     * to the body of the request or response. If no converters are added, a
     * default list of converters is registered.
     * <p><strong>Note</strong> that adding converters to the list, turns off
     * default converter registration. To simply add a converter without impacting
     * default registration, consider using the method
     * {@link #extendMessageConverters(java.util.List)} instead.
     * @param converters initially an empty list of converters
     */
    default void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    }

    /**
     * A hook for extending or modifying the list of converters after it has been
     * configured. This may be useful for example to allow default converters to
     * be registered and then insert a custom converter through this method.
     * @param converters the list of configured converters to extend.
     * @since 4.1.3
     */
    default void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    }

    /**
     * Configure exception resolvers.
     * <p>The given list starts out empty. If it is left empty, the framework
     * configures a default set of resolvers, see
     * {@link WebMvcConfigurationSupport#addDefaultHandlerExceptionResolvers(List)}.
     * Or if any exception resolvers are added to the list, then the application
     * effectively takes over and must provide, fully initialized, exception
     * resolvers.
     * <p>Alternatively you can use
     * {@link #extendHandlerExceptionResolvers(List)} which allows you to extend
     * or modify the list of exception resolvers configured by default.
     * @param resolvers initially an empty list
     * @see #extendHandlerExceptionResolvers(List)
     * @see WebMvcConfigurationSupport#addDefaultHandlerExceptionResolvers(List)
     */
    default void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
    }

    /**
     * Extending or modify the list of exception resolvers configured by default.
     * This can be useful for inserting a custom exception resolver without
     * interfering with default ones.
     * @param resolvers the list of configured resolvers to extend
     * @since 4.3
     * @see WebMvcConfigurationSupport#addDefaultHandlerExceptionResolvers(List)
     */
    default void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
    }

    /**
     * Provide a custom {@link Validator} instead of the one created by default.
     * The default implementation, assuming JSR-303 is on the classpath, is:
     * {@link org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean}.
     * Leave the return value as {@code null} to keep the default.
     */
    @Nullable
    default Validator getValidator() {
        return null;
    }

    /**
     * Provide a custom {@link MessageCodesResolver} for building message codes
     * from data binding and validation error codes. Leave the return value as
     * {@code null} to keep the default.
     */
    @Nullable
    default MessageCodesResolver getMessageCodesResolver() {
        return null;
    }

}

 

View 를 반환할 때 사용했던 ViewResolver 타입의 InternalResourceViewResolver 객체를 직접 만들어 bean 으로 등록하는 대신 이 인터페이스를 사용해서 다음과 같이 동일하게 동작하도록 할 수 있다.

 

package me.nimkoes;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@ComponentScan
@EnableWebMvc
public class MyWebConfig implements WebMvcConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.jsp("WEB-INF", ".jsp");
    }

}

 

등록된 bean 정보를 확인해보면 직접 bean 을 등록 했을 때와 달리 ViewResolvers 에 하나의 bean 만 등록 되어있고 설정한 값이 정상적으로 적용된 것도 같이 볼 수 있다.

 

 

이렇게 사용하는 방법은 Spring Boot 에서 지원하는 내용은 아니니 구분해서 알아두는게 좋을것 같다.

 

 

 

 

728x90

+ Recent posts