[Project] Controller 생성 & API 호출 (1) - EP02

반응형

이번 시리즈에서는 스프링 부트로 프로젝트를 처음부터 생성 및 구축해보도록 하겠습니다.  

# 목차 
1. 스프링 부트 프로젝트 생성 & 실행 - EP01
2. Controller 생성 & API 호출 (1) - EP02
3. Controller 생성 & API 호출 (2) - EP03

Controller 생성 

이제 Contoller 를 하나 생성해보도록 하겠습니다. 
프로젝트 경로 밑에 web 이라는 폴더를 생성하고 다음과 같이 
HelloController 클래스를 생성합니다. 

생성한 HelloController 클래스 상단에는.. 이 클래스가 RestController 의 역할을 한다는 걸 명시하기 위해 
@RestController 어노테이션을 작성해줍니다. 

@RestController 어노테이션은 @Controller 어노테이션과 @ResponseBody 의 기능을 통합적으로 제공하는 어노테이션으로
해당 어노테이션이 선언된 Controller 에..  작성된 @RequestMapping 메서드들은..
기본적으로 @ResponseBody 를 내포하고 있다고 가정합니다. 


기존의 @Controller 는 return 값으로 ModelAndView 형태로.. 정적인 화면을 return 했던 것과 달리
@RestController 는 web 의 reponse body 형태로 결과값을 return 합니다. (일반적으로 json 값.. 👀)

API 생성 

이제 @GetMapping 어노테이션을 사용해서 다음과 같이 /hello 라는 GET API 를 생성합니다.
GET /hello 에 mapping 된 hello() 메서드는 
request 가 들어오면 "hello world" 라는 String 값을 return 해주는 간단한 API 입니다. 

@GetMapping 어노테이션은 @RequsetMapping 에 RequestMethod.GET 을 선언한것 과 동일하게 동작하며.
이 어노테이션을 통해 HTTP GET request 를 HelloConroller 의 hello() 메서드에 mapping 할 수 있게 됩니다. 

API 호출


자.. 이제 위에서 생성한 API 를 호출하고 결과값을 확인해보겠습니다. 
프로젝트를 실행하고.. 다음과 같이 브라우저를 열어 "localhost:8080/hello" 를 입력해보면
의도했던 대로 "hello world" 가 정상적으로 화면에 return 되는 것을 확인할 수 있습니다. 

👏👏👏 이렇게 해서 스프링부트 프로젝트에서..
손쉽게 API 를 생성하고 호출하는 방법에 대해 알아보았습니다.


자, 이제.. 도대체 어떻게?? 이렇게 손쉽게 API 를 호출하고 결과값을 출력할 수 있는지 간단하게 살펴보겠습니다.

Debug 

먼저 GET /hello request 가 호출되었을 때 스프링부트 프로젝트의 내부가 어떻게 동작하는지 추적 해보기 위해..  
IntelliJ 에서 벌레모양을 눌러 debug 모드로 프로젝트를 실행시켜줍니다.

다음으로 hello() 메서드 좌측을 마우스 왼쪽 클릭해 dubug break point 로 선택한 뒤 

이전과 동일하게.. 브라우저를 열어 "localhost:8080/hello" 를 입력하면
IntelliJ 좌측 하단 Debugger 탭의 Frames 창에 다음과 같이 무수히 많은 Stack 들이 찍혀있는 것을 확인할 수 있습니다.  

이는 실제로 GET /hello HelloController 의 hello() 메서드가 실행되기 위해 거쳤던 흐름?으로
Apache Tomcat 혹은 Spring 에서 구현해놓은 기능들입니다. (이렇게 많은 작업들이 뒤에서 돌아가고 있었군.. 🤦🏻‍♂️)

그럼 위의 Stack 들을 하나하나 간단히 따라가 보도록 하겠습니다.. 

가장 먼저 실행되는 Stack 은 Thread 입니다. 

Thread 는 일반적으로 program 을 의미하므로..
GET /hello request 의 요청을 처리하기 위한 Tomcat Thread (program) 이 생성되었다고 이해하면 될 것 같습니다. 

다음으로도.. 이것저것 ThreadPoolExecutor 라던지.. SocketProcessorBase 라던지..
여러가지 apache level 의 스택들이 출력되어 있는데.. 우리가 관심있어 하는 내용은 spring 에 대한 내용이므로.. 
이부분은 건너뛰겠습니다. 😅 (사실 잘 모르겠습니다.)

ApplicationFilterChain 

자. 이제 다음으로 확인해볼 Stack 은 ApplicationFilterChain 입니다.
ApplicationFilterChain 클래스는 FilterChain Interface 를 구현한 클래스로 

request 에 대한 여러가지 filter 들의 set 을 execution 하는 역할을 수행하며..
모든 filter 들이 실행된 뒤에는 request 에 해당하는 servlet 의 service method 를 호출하는 역할을 수행합니다.

즉, GET /hello request 가 호출되면
1. 미리 정의되어있던 여러 filter 들을 사용해.. 해당 요청에서.. 거를껀 거르고 붙일건 붙인뒤
2. GET /hello 에 mapping 된 servlet (controller) 의 method (hello() 메서드)를 실행시킵니다. 

실제로 ApplicationFilterChain 에는 기본적으로 다음 4가지의 Filter 들이 포함되어 있습니다. 

1. CharacterEncodingFilter
2. FormContentFilter
3. RequestContextFilter
4. TomcatWebSocketFilter

그런데.. 아래의 Stack 을 살펴보면 각 Filter 들의 실행 앞단에
OncePerRequsetFilter 들이 하나씩 꼭 붙어있는 것을 확인할 수 있습니다. 
이 친구는 무엇일까요..? 

OncePerRequestFilter

이 filter는 하나의 request 요청에.. (예를 들어 GET /hello 요청) 
동일한 filter 가 여러번 타지 않도록 방지해주는 filter 입니다.

내부를 동작을 조금 들여다 보면.. 
예를 들어 CharacterEncodingFilter 가 호출되기 전에 
OncePerRequsetFiler 가 호출되게 되고.. 

이때 HttpServletRequest 의 'CharacterEncodingFilter.FILTERD' attribute 값이 'true' 라면
이는.. 이미 처리된 Filter 로 판단하고 해당 Filter 처리를 건너뛰게 됩니다. (굳이 동일한 filter 를 여러번 거칠 필요는 없으니..) 


만약 처음 호출되는 Filter 라면 Filter 를 호출하기 전 attribute 값에 
마찬가지로 'CharacterEncodingFilter.FILTERD' 값을 ture 로 셋팅해줌으로써 해당 Filter 는 이미 처리되었음을 명시해줍니다. 

( HttpServletRequset 의 attributes 에는 다음과 같이 무수히 많은 값들이 등록되어 있습니다. )

동일하게 Filter 처리가 필요없는 Async 요청일 경우에도 해당 Filter 를 건너뛰도록 설계가 되어있는데..

하나의 request 요청을 처리할 때 만약 
Async 로 Thread 를 새로 생성해서 무언가를 처리하는 로직이 있다면..
이미 앞단에서 동일 single request 에 대한 Filter 처리를 했음에도 불구하고.. 
또다시 위 FilterChain 을 통해 Filter 가 호출되게 되므로.. 중복처리를 방지하기 위한 로직이라고 이해하면 될 것 같습니다. 

혹은 아예 shouldNotFilter 메서드를 override 해서 특정 request 들은 filter 가 타지 않도록 제어 할 수 도 있습니다.

결론적으로.. 
OncePerRequestFilter 는 ApplicationFilterChain 이 호출한 각 Filter 의 앞단에서 실행되어..
하나의 request 에 대해서 (GET /hello) 

1. 동일 Filter 가 중복으로 호출된 요청
2. single request 내부에서 Async 로 호출된 요청 중 Filter 처리가 불필요한 요청
3. shouldNotFilter 메서드를 통해 제어된 Filter 가 필요없는 요청

에 대해 해당 Filter 를 실행하지 않고 건너뛰도록 제어합니다.

이제 각 Filter 들의 역할을 살펴보면..

CharacterEncodingFilter

먼저 CharacterEncodingFilter 는 
request 의 encoding type 을 결정해 주는 Filter 로.. 

CharacterEncodingFilter 를 거친 request 는 
프로젝트에 default 로 설정되어 있는 encoding 값으로 (제 프로젝트는 UTF-8) 
characterEncoding 값을 셋팅하게 됩니다. 

FormContentFilter

다음으로.. FormContentFilter 는 
HTTP PUT / PATCH / DELETE requset 의 form-data 데이터를 
Servlet 의 request parameter 로 변환시켜주는 역할을 수행합니다.

잘은 모르겠지만.. Servlet Spec 의 규격이 POST method 에 대해서만 from data 를 사용할 수 있도록 정의되어 있어
PUT / PATCH / DELETE 메서들은 이 Filter 를 거치지 않으면 Form Data 를 사용할 수 없는 것 같습니다.
위 부분은 나중에 한번 실험해보도록 하겠습니다. (현재는 일단 넘어가겠습니다.)

결론적으로 PUT / PATCH / DELETE 메서드의 경우
request 의 params 값 (form-data) 들을
FormContentRequestWrapper 라는 Wrapper 클래스로 감싸서 다음 Filter 로 값을 넘기게 됩니다. 

RequestContextFilter

다음은 RequestContextFilter 입니다.
이 Filter 는 request/response 의 값들을
현재의 thread 어디에서나 접근할 수 잇도록 ContextHolder 객체에 담아두는 역할을 수행하는데.. 

그 중 LocalContextHolder 에는 request 의 Local 정보 (예를 들어 ko-kr) 를 담아두게 되고.
RequestContextHolder 에는 HttpServeltRequest 의 attributes 의 정보를 담아두게 됩니다. 

이때 각 ContextHolder 값 들을 '자식 thread에도 전파' 할 지를 결정하는 
threadContextInheritable 변수값을 ContextHolder 에 같이 전달하게 됩니다. 

threadContextInheritable 의 default 값은 false 이므로 
GET /hello request 요청 attributes 의 값들은 자식 thread 에 전파되지는 않습니다.

WsFilter 

마지막으로 살펴볼 Filter 는 WsFilter 로
이 Filter 는 WebSocket conneciton 에 대한 초기화를 담당해주는 Filter ? 인 것 같습니다. 

이 Filter 는 request 자체가 WebSocket upgrade request 일 경우에만 타게되는 Filter 인 것 같은데.
자세한 내용은 잘 모르겠으니.. 일단 넘어가도록 하겠습니다. 😅

결론적으로 모든 Filter 처리가 완료된 뒤.. 드디어 그토록 찾아 해메던 HttpServlet 이 호출되게 됩니다.. 🤩
아직 GET /hello request 가 HelloController 에 도달하기까지 많은 Stack 들이 남아 있습니다.
해당하는 내용은 다음글에서 확인해보도록 하겠습니다.. 😭


결론

결론적으로 HTTP Request가 호출되면

0. Tomcat Thread 가 실행되고. 이후에 요청을 처리하기 위한 이것저것? apache 쪽이 작업이 수행된 뒤에

1. ApplicationFilterChain 이 다음의 4가지 Filter 를 channing 형식으로 호출하며
   - ChracterEncodingFilter
   - FormDataFilter
   - RequestContextFilter
   - WsFilter

2. 각 Filter 의 처리 앞단에는 OncePerRequestFilter 가 호출되어 다음의 상황에서 Filter 처리를 건너뛰고
   - Single Request 에서의 중복된 Filter 요청
   - Single Request 에서 파생된 Async 요청에서 Filter 처리가 불필요한 요청
   - ShouldNotFilter 로 정의된 Filter 처리가 불필요한 요청

그 다음으로 드디어.. HttpServlet 을 호출한다로 이해할 수 있습니다. 

반응형

댓글

Designed by JB FACTORY