[Spring] MVC 패턴과 Service, Template Engine, Front Pattern과 DispatcherServlet 그리고 HandlerAdapter

벼랑 끝 코딩·2025년 3월 27일

Spring

목록 보기
8/16

지난번에는 Client가 URL을 입력하여 HTTP Request Message를 보낸 후
서버가 HTTP Response Message를 응답하기 까지의
Spring에서 발생하는 과정에 대해 알아봤다.

하지만 이 과정에는 불편한 점이 한 두가지가 아니었는데
Spring은 이러한 과정을 어떻게 매끄럽게 개선하는지 살펴보자.

MVC Pattern

Spring의 동작을 살펴보기에 앞서 먼저 MVC Pattern에 대해 학습해야 한다.
Spring의 구조가 MVC Pattern을 띄고 있기 때문이다.

MVC는 Model, View, Controller의 약자를 따서 붙여진 이름이다.

Client Request → Controller → Model 생성 및 View 전달 → View 화면 출력
  • Model : View에 출력할 데이터를 담아두는 역할
  • View : Model을 화면에 출력하는 역할
  • Controller : HTTP Request로 전달 받은 데이터를 검증 후 Model에 저장하고
    비즈니스 로직을 실행하여 View에 Model을 전달하는 역할

Service

Controller가 데이터를 검증하여 Model에 저장하고 View에 전달하는 흐름은
Model과 View를 연결한다는 점에서 이상적인 역할이다.
하지만 컨트롤러는 여기서 비즈니스 로직을 수행해야 하는 추가적인 임무가 있다.

컨트롤러에게 너무 많은 역할을 부여하고 있다.
컨트롤러는 말그대로 컨트롤, 제어의 역할을 담당하게 하고
비즈니스 로직을 수행하는 것은 분리해내자.

그래서 보통 MVC Pattern에 추가로
비즈니스 로직을 수행하는 Service를 분리하여 MVC + Service Pattern을 사용한다.

기존 문제점

이전에 살펴본 Spring의 내부 구조와 과정을 살펴보면,
Client의 데이터를 받는 Servlet이 바로 Controller의 역할을 하게 된다.
하지만 Servlet에서 데이터를 검증하여 비즈니스 로직도 실행하고
HttpServletResponse도 생성하여 응답도 구성한다.

현재 구조는 Servlet이 MVC Pattern의 모든 일을 담당하고 있는 것이다!

이러한 설계에서는 문제점이 한 두가지가 아니다.
대표적인 문제로는 각 부분에서 수정이 발생할 경우 클래스 자체를 수정해야 하는
유연하지 못하다는 단점이 여실히 드러나게 된다.

우리는 현재의 구조를 적절히 분리하여 MVC + Service Pattern으로 완성해야 한다.
하나씩 차근 차근 뜯어나가보자.

Template Engine

가장 먼저 보아야 할 점은 바로 HttpServletResponse를 통해
HTTP Response Message를 직접 생성한다는 점이다.
이 부분을 해결해야 본격적으로 Spring MVC 구조로 개편할 수 있다.

텍스트로 응답하든지, HTML로 응답하든지 한줄 한줄 직접 작성해야 하는 점은
사실상 사용이 불가능한 것에 가깝다.
이러한 문제를 해결해주는 것이 Template Engine이다.
Spring은 한줄 한줄 직접 응답을 생성하는 대신 화면을 출력하는 역할을 위임하고
Template Engine이 그 역할을 대신 수행한다.

대표적인 Template Engine으로는 JSP, Thymeleaf가 있다.

JSP

최근에는 주로 Thymeleaf를 사용하지만, 레거시로 JSP를 사용하는 곳도 꽤 존재한다.
이번에는 JSP를 예시로 살펴보고, 다음에 본격적으로 Thymeleaf를 알아보겠다.

먼저 Servlet은 Http Response Message를 생성하여 화면을 출력하는 역할을
Template Engine인 JSP에게 위임한다.
JSP란 Servlet 역할을 대신 수행해내기 위하여,

HTML 코드와 자바 코드가 혼합하여 View를 작성하는 기술이다.
(하지만 View의 역할만을 수행하기 위해 HTML 코드만을 작성하는 편이다.)

JSP는 외부에서 URL을 입력하는 것이 아닌 직접 호출하는 것을 막기 위하여
/WEB-INF 하위 디렉터리에 .jsp파일을 위치시킨다.

참고로 정적 리소스는 /resources/static에,
타임리프는 src/main/resources/templates에 위치시킨다.

RequestDispacher.forward()

RequestDispatcher requestDispatcher = 
httpServletRequest.getRequestDistpacher("/WEB-INF/jspFile.jsp");

requestDispatcher.forward(httpServletRequest, httpServletResponse);

JSP에 View를 작성하는 것을 위임하기 위해서는,
.jsp 파일의 path를 HttpServletRequest getRequestDispacher() 메서드에 전달하여
RequestDisptcher 객체를 생성해야 한다.
이후 역할을 위임하는 메서드인 RequestDispatcher.forward() 메서드에
HttpServletRequest, HttpServletResonse 객체를 파라미터로 전달하면 된다.

HttpServletRequest를 Model로 사용하기 위해
httpServletRequest.setAttribute() 메서드를 사용하여 Model을 저장하고,
JSP에서는 내부적으로 HttpServletRequest.getAttribute() 메서드를 사용하여
데이터에 접근하는 프로퍼티 접근을 사용한다.

Client가 URL을 입력하여 HTTP Request가 도착하면
Servlet이 메시지를 확인하여 데이터를 추출하고 비즈니스 로직을 수행하여
응답을 생성하는 것까지 홀로 담당했지만,
이제 응답을 생성하는 것을 분리해내어 View의 역할이 완성됐다.

View

하지만 클래스마다 파일의 모든 path 경로를 입력하고
RequestDispatcher객체를 생성하여
forward() 메서드를 일일이 호출하는 것은 매우 번거롭다.

코드를 간소화하기 위해 RequestDispatcher 객체를 생성하고
forward() 메서드를 호출
하는 View 클래스를 설계했다.
이제 View 객체를 생성하고 render() 메서드를 호출하여 간단히 해결할 수 있다.

class View {
	
    String path;
    
    public View(String path) {
    	this.path = path;
    }
    
    public void render(HttpServletRequest httpServletRequest,
    				   HttpServletResponse httpServletResponse) {
    	RequestDispatcher requestDispatcher = 
				httpServletRequest.getRequestDistpacher("/WEB-INF/jspFile.jsp");

		requestDispatcher.forward(httpServletRequest, httpServletResponse);
    }
}

모든 path 경로를 입력하는 번거로움을 해소하기 위해서는 다음을 수행하면 된다.
템플릿 엔진이 다른 경우 설정 방법이 상이할 수 있다.

// ** application.properties 작성, jsp 예시 **

spring.mvc.view.prefix=/WEB-INF/ 
spring.mvc.view.suffix=.jsp

이제 파일의 논리적인 이름만 있으면 파일의 path가 자동으로 완성된다.

View Resolver

Spring은 렌더링할 파일의 논리적 이름만 전달하면
View Resolver가 path 경로를 자동으로 해석한다.
이후 path 경로에 따라 View 객체를 자동으로 생성하고,
RequestDispatcher의 forward() 메서드를 호출하는 작업을 자동으로 수행한다.

즉, 화면을 출력하기 위해 Spring에게 파일의 path만 전달하면 되는것이다!

View Resolver 덕분에 View를 분리해내는 모든 작업이 자동으로 수행된다.
이로써 View는 완벽히 분리해내는데 성공한 것 같다!
(뿌듯)

Front Controller Pattern

현재 구조는 매우 비효율적이다.
Servlet은 상대적으로 무거운 구조인데 지금은 url마다 대응하는
Servlet 클래스를 각각 생성해야 한다.
100개의 url이 있다면, 100개의 Servlet 클래스를 생성해야 한다.

클래스를 각각 생성함으로써 RequestDispatcher 객체 생성과 forward() 메서드처럼
View에게 역할을 위임하는 반복적인 코드가 발생한다.
이러한 문제점들을 처리하기 위한 패턴이 바로 Front Controller Pattern이다.

Dispatcher Servlet

Front Controller Pattern은 요청 URL에 각각 대응하도록 설계한 클래스 사이에

마치 입구와 같은 클래스 하나를 두고, 다시 클래스에 대응하는 방식이다.

HttpServlet을 상속해 HttpServletRequest, HttpServletResponse을 제공하고
@WebServlet 애노테이션 url을 "/"로 설정하는 것과 유사하게
모든 Request의 요청을 우선 Dispatcher Servlet으로 향하게 한다.

또한 Request 요청이 오면 자동으로 doDispatch() 메서드를 호출하여
사용자의 요청을 처리할 적절한 컨트롤러를 조회한다.

// ** HttpServletRequest, HttpServletResponse 사용 가능 **
@WebServlet(name = dispatcherServlet, urlPatterns = "/")
class DispatcherServlet extends HttpServlet {
	
    @Override
    protected void doDispatch(HttpServletRequest request, 
    		HttpServletResponse response) throws Exeption {
        
        // 사용자 요청을 처리할 컨트롤러 조회
    }
}

... 아니, 마치 유통 구조를 늘리는 것처럼 간략한 구조에
왜 굳이 입구를 하나 추가해서 복잡하게 만드는 것인가?
입구를 하나 추가한것 만으로도 어마어마한 효과가 생기기 때문이다.
추가한 입구를 DispatcherServlet이라고 부른다.

N Servlet → 1 Servlet

Servlet은 일반 객체보다 무거운 존재이다.
이러한 Servlet을 URL에 각각 대응하기 위해 몇 십, 몇 백개를 설계하면
애플리케이션의 속도는 상당히 저하될 것이다.

Servlet이 필요했던 이유를 생각해보자.
Servlet은 HttpServletRequest와 HttpServletResponse를 제공하여
각각 요청 메시지와 응답 메시지를 편리하게 사용할 수 있게 만들어주었다.
요청과 응답은 하나인데, 여러개의 Servlet이 이걸 가지고 있을 필요가 있을까?
Servlet은 무거우니, 대표 Servlet이 하나만 들고있고 이걸 뿌려주는 방식은 어떨까?

Front Controller Pattern은 입구에 DispatcherServlet을 두어서
Servlet을 하나만 설계하고, 요청을 확인하고 응답을 생성하는데에 필요한
HttpServletRequest와 HttpServletResponse를
각각의 Controller에게 전달
하는 역할을 수행한다.

심지어 둘 중 필요한 것만 전달하는 것도 가능하다!
이렇게 애플리케이션의 자원을 절약하고 성능을 향상할 수 있다.

공통 로직 처리

DispatcherServlet을 두면 마치 스프링 컨테이너와 빈후처리기처럼
공통 로직을 하나의 클래스에서 적용할 수 있다.

예를 들어 요청 발생 시 로그를 발생시켜야 하는 상황이라면
각각의 Controller가 수행할 필요 없이 Controller 수행 전
DispatcherServlet이 그 역할을 공통으로 수행해낼 수 있다.

반복적으로 작성해야하는 코드를 획기적으로 제거할 수 있다.

@RequestMapping

기존에는 URL에 대응하기 위해 클래스를 생성하던 방식에서는
bean 이름으로 대응하는 클래스를 구분하는 방식을 사용했다.

하지만 이러한 방식은 URL에 각각 대응하기 위해
클래스를 각각 생성해야 한다는 비효율적인 문제를 해결하지 못한다.
그렇게 등장한 방식이 @RequestMapping 애노테이션 방식이다.

// 기존 - 다수의 클래스 생성
@Component("/url1")
class ControllerClass1 implements Controller {
	// 코드
}

@Component("/url2")
class ControllerClass2 implements Controller {
	// 코드
}


// ** @RequestMapping **
@Controller
class ControllerClass {

	@RequestMapping("/url1")
    public void controllerMethod1() {
    	// 메서드 바디
    }
    
    @RequestMapping("/url2")
    public void contrillerMethod2() {
    	// 메서드 바디
    }
}

@RequestMapping("/url") 애노테이션으로 인식하는 방식을 사용하면
기존에 URL에 대응하기 위해 클래스로 나누던 문제를 해결하고

하나의 클래스의 메서드들에 애노테이션을 선언, 효율적으로 관리할 수 있다.

또한, 기존에 HTTP Method가 GET, POST로 나누어지는 등의 경우 처리가 복잡했으나
@GetMapping("/url"), @PostMapping("/url")과 같은 애노테이션으로

HTTP Method에 따라 메서드를 구분하여 간단하게 처리할 수 있게 됐다.

@GetMapping("/url")
public String method() {
	// 메서드 바디
}

// ** url이 같아도 HTTP 메서드에 따라 다르게 지정 가능 **
@PostMapping("/url")
pbulic String method() {
	// 메서드 바디
}
  • @RequestMapping : 기본, element를 통해 HTTP Method 지정 가능
  • @GetMapping : element 작성 없이 GET 메서드 자동 지정
  • @PostMapping : element 작성 없이 POST 메서드 자동 지정
  • @PutMapping : element 작성 없이 PUT 메서드 자동 지정
  • @DeleteMapping : element 작성 없이 DELETE 메서드 자동 지정
  • @PatchMapping : element 작성 없이 PATCH 메서드 자동 지정

애노테이션에 다양한 element 설정을 추가할 수 있다.


상대적으로 무거웠던 Servlet을 하나만 생성하여 성능을 향상시키고, @RequestMapping 방식으로 클래스를 여러개 생성해야 했던 문제도 해결했다!

HTTP Request Message 확인

View를 완전히 분리해냈고, DispatcherServlet을 두어
Servlet을 한 개만 생성하고 클래스 대신 메서드를 생성하여 관리할 수 있게 됐다.
이제 Client가 URL을 요청하는 것부터 하나씩 살펴보자.

HTTP Request Message의 헤더에 대한 정보와
Client가 query, HTML Form 방식, HTTP API(JSON) 형식으로 전송하는 데이터를
기존에는 HttpServletRequest에서 직접 데이터를 꺼내어 확인했다.

Header 조회

이제는 HttpServletRequest의 메서드를 통해 직접 확인하는 것이 아닌
대부분의 정보를 파라미터로 전달 받을 수 있다.

@RequestMapping("/url")
public void requestMethod(HttpMethod httpMethod, Local local) {
	// 메서드 바디
}

파라미터에 Header 객체를 입력하면 Header 정보를 확인 및 활용할 수 있다.

@PathVariable

경로 변수라고 불리는 @PathVariable은 URL의 일부를 데이터로 추출하는 방식이다.

URL에서 추출하고자 하는 데이터에 {parameter}를 사용하고,
@PathVariable("parameter")에 이름을 일치시켜 변수로 받아볼 수 있다.

// ** Client URL : /url/kim **

@RequestMapping("/url/{name}")
public void requestMethod(@PathVariable("name") String name) {
	// ** name = "kim" **
}

@RequestParam

HTTP GET Method + query, POST + HTML Form 방식에 사용 가능하다.
key=value 형식에서 key의 이름을 입력하여 파라미터로 전달받는 방식이다.
@RequestParam은 단일 값을 바인딩 하지만
@ModelAttribute는 객체를 바인딩한다는 점에서 차이가 있다.

// ** URL : /url?name=kim 또는 message body : name=kim **

@RequestMapping("/url")
public void requestMethod(@RequestParam("name") String name) {
	// ** name = "kim" **
}

required element로 파라미터 필수 여부를,
defalutValue element로 기본값을 설정할 수 있다.
required = false의 경우 파라미터 값에 null이 들어갈 수 있는데,
기본형에는 null 대입이 불가능하기 때문에 기본형의 경우 예외가 발생한다.

// ** 파라미터 값 필수, 값이 없는 경우 예외 발생 **
public void requestMethod(@RequestParam(value = "name", required = true) String name)

// ** 기본형 null 불가, 값이 없는 경우 예외 발생 **
public void requestMethod(@RequestParam(value = "name", required = false) int age)

// ** 파라미터에 값이 없다면 기본값 배정 **
public void requestMethod(@RequestParam(value = "name", defaultValue = "kim") String name)

key=value 형식으로 나열된 데이터를 Map 형태로 전달받을 수 있다.
value의 값이 여러개인 경우 MultiValueMap을 사용한다.

public void requestMethod(@RequestParam Map<String,Object> parameterMap)
public void requestMethod(@RequestParam MultiValueMap<String,Object> parameterMap)

@ModelAttribute

GET + query, POST + HTML Form 방식의 key=value 값을 바탕으로

객체를 자동 생성하여 파라미터로 전달받는 방식이다.

내부적으로 set() 메서드를 사용하는 프로퍼티 방식으로 객체를 생성한다.

@RequestParam으로 데이터를 받아서 직접 객체를 생성하는 것보다 간편하다.
기본형, 래퍼 클래스는 @RequestParam을 사용하고
사용자 정의 클래스는 @ModelAttribute를 사용한다.
@ModelAttribute는 생략 가능하나 가독성이 저하되는 점을 고려해야 한다.

class User {
	String name;
    int age;
}

// ** URL : /url?name=kim&age=20 또는 message body : name=kim&age=20 **

@RequestMapping("/url")
public void requestMethod(@ModelAttribute("user") User user) {
	// ** user.name = "kim", user.age = 20 **
}

HTML Form에 동적으로 데이터를 제공하기 위해서는
HttpServletRequest를 Model로 사용하여 RequestDispatcher에 전달해야 한다.
Model에 데이터를 저장하기 위해서는 setAttribute() 메서드를 호출해야 하는데
@ModelAttribute로 전달받은 데이터는 별도로 메서드를 호출하지 않아도

자동으로 Model에 저장되어 아주 편리하게 사용할 수 있다!

Model에 저장한 데이터는 HTML Form에 선언되어 있지 않거나
사용하지 않아도 전혀 문제가 되지 않는다.
@ModelAttribute("name")에 parameter 이름(name)을 생략하는 경우
클래스의 맨 앞 대문자를 소문자로 바꾸어 자동으로 설정된다.

// ** User -> user 자동 설정 **
public void requestMethod(@ModelAttribute User user) 

@ModelAttribute 애노테이션을 메서드 레벨에 선언하면
@RequestMapping이 호출되기 전에 반환 값을 자동으로 Model에 저장한다.

@ModelAttribute("user")  // ** View에서 user 이름으로 사용 가능 **
public User userMethod() {
	return new User("kim", 20);
}

ModelAndView

@ModelAttribute를 사용하면 자동으로 Model에 데이터가 저장되지만,
내부적으로는 DispatcherServlet에서 parameter Map을 만들어
사용자 전송 데이터를 보관, 데이터를 토대로 모델 객체를 생성해 model Map에 보관한다.

ModelAndView는 path 경로와 model Map을 보유한 객체이다.
Template Engine을 호출하는 Controller는
내부적으로 ModelAndView를 만들어 반환하는 역할을 수행한다.

ModelAndView의 path 경로 데이터로 적절한 View를 렌더링하고,
model Map을 HttpServletRequest에 저장하여
View에서 데이터를 사용할 수 있도록 하는 역할을 수행한다.
(데이터 저장은 HttpServletRequest.setAttribute() 또는
ModelAndView.addObject() 메서드를 호출하여 수행한다.)

// ** Controller 로직 수행 결과로 ModelAndView 반환 **
ModelandView modelAndView = controller.execute();

// ** ModelAndView의 path 경로와 model 데이터 전달하여 View 생성 **
View view = new View(modelAndView.getPath(), modelAndView.getModel());

// ** model 데이터를 HttpServletReqeust에 저장하고,
    RequestDispatcher.forward(httpServletRequest, httpServletResponse) 수행 **
view.render();

Spring은 이렇게 View를 렌더링 하는 방식을 자동으로 수행하는데,
@ModelAttribute를 사용하면 이 과정에서 Model을 저장하는 역할도 자동으로 수행한다.

Model

HttpServletRequest를 Model로 사용하여 데이터를 저장하는 방식은
어떠한 코드를 의미하는지 가독성을 저하시키기 때문에 HttpServletRequest 대신
파라미터로 Model을 전달받아 데이터를 저장하여 명확한 의도를 드러낼 수 있다.

@RequestMapping("/url)
public String requestMethod(Model model) {  // ** 파라미터로 Model 획득 **
	model.addAttribute("user", new User("kim", 20));
}

하지만 @ModelAttribute 하나면 Model 데이터를 저장하는 문제도
자동으로 수행해주기 때문에 깔끔하게 번거로움을 해결할 수 있다!

HttpEntity<String>, HttpEntity<Object>, RequestEntity<>

HttpEntity<String>는 파라미터로 message body를 직접 조회하는 방식이다.
GET + query, POST + HTML Form 방식이 아닌 HTTP API 방식에서 유효하다.

getBody() 메서드를 활용하여 데이터를 확인할 수 있다.

@RequestMapping("/url")
public void requestMethod(HttpEntity<String> httpEntity) {
	String messageBody = httpEntity.getBody();
}

HttpEntity<Object>의 경우 message body의 데이터를
변환된 객체로 받아볼 수 있다.

@RequestMapping("/url")
public void requestMethod(HttpEntity<User> httpEntity) {
	User user = httpEntity.getBody();
}

RequestEntity<>에는 HTTP 응답 코드가 포함되어 있다는 점에서 차이가 있다.
getMethod() 메서드를 통해 HTTP Method를 조회할 수 있다.

@RequestBody

message body의 문자열을 읽어오거나 데이터 객체로 변환하는 방식이다.

HttpEntity<>와 동작이 유사하나 HttpEntity는 헤더 정보를 포함할 수 있고
@RequestBody는 messageBody만을 다룬다는 점에서 차이가 있다.

@ModelAttribute가 GET + query, POST + HTML Form 방식에서 사용되는 것과 달리
HTTP API 방식에서 사용된다는 점에서 차이가 있고, 생략 불가능하다.

@RequestMapping("/url")
public void requestMethod(@RequestBody User user) {
	// 메서드 바디
}

@RequestHeader

@RequestBody가 message body를 확인할 수 있었다면,
@RequestHeader은 Header 정보를 확인할 수 있다.

@RequestMapping("/url")
public void requestMethod(@RequestHeader MultiValueMap<String, String> headerMap,
		@RequestHeader("host") String host) {
	// 메서드 바디
}

ArgumentResolver

ArgumentResolver는 위와 같이 파라미터에 선언된 애노테이션에 따라
Spring에서 클라이언트의 데이터를 처리하는 방식을 자동으로 결정하는 역할을 수행한다.

Redirection

사용자가 URL 주소를 입력하여 Request하는데, URL 주소가 변경되었을 수 있다.
이땐 사용자에게 변경된 주소를 안내해주어야 하는데 이걸 Redirection이라고 한다.

RedirectAttributes

파라미터에 RedirectAttributes를 전달 받고
addAttribute() 메서드를 호출하여 변경할 주소를 설정한다.
"redirect:/url"을 반환하여 변경된 주소를 안내할 수 있다.
redirect의 HTTP Method는 GET 방식이다.

@RequestMapping("/url")
public String method(RedirectAttributes redirectAttributes) {
	redirectAttributes.addAttribute("parameter1", 1)
    redirectAttributes.addAttribute("parameter2", 2)
    
    // ** url : /url/1?parameter2=2 **
    return "redirect:/url/{parameter1}
}

url에 {}를 사용하여 parameter을 지정하여 값을 설정할 수 있고,
parameter로 지정하지 않은 값은 query로 추가된다.
addAttribute() 메서드를 추가한 순서대로 더해진다.

PRG (POST Redirect GET)

꼭 주소가 변경되었을때만 Redirection을 수행해야 하는 것은 아니다.
대표적인 문제로 POST + 새로고침 문제가 있다.

POST 메서드로 Request를 한 이후에 새로고침하면 POST 메서드로 다시 제출된다.
예를 들어 POST 메서드로 물건을 주문한 뒤 새로고침을 할 경우
또 다시 동일한 물건을 주문하는 문제가 발생한다.

이러한 문제를 해결하려면 GET 방식으로 요청하는 URL을 redirect하여
새로고침 이후에도 데이터를 전송하는 POST가 아닌 조회의 GET을 사용하도록 유도한다.

@PostMapping("/url")
public String postMethod() {
	return "redirect:/redirectUrl";
}

@GetMapping("/redirectUrl")
public String redirectMethod() {
	// 메서드 바디
}

HandlerAdapater

예를 들어 어떤 @RequestMapping 메서드는 int를, 어떤 메서드는 String을,
또 다른 메서드는 각각 User와 Member를 반환한다고 가정해보자.
반환 타입 별로 응답 메시지를 생성하는 방식이 다를 수 있을 것이다.
이럴때 처리하는 방식이 같은 타입 별로 로직을 구현해두지 않는다면,
타입 하나 하나마다 응답 메시지를 생성하기 위한 로직을 작성해야할 것이다.

처리하는 방식이 같은 타입은 동일한 로직을 처리할 수 있도록 유도하는 역할이
바로 HandlerAdapter이다.

먼저 Client의 요청이 들어오면, @RequestMapping과 URL을 매칭하여
처리 가능한 Handler(Controller) 메서드를 조회하고
@RequestMapping에 선언된 애노테이션, 반환 값 등을 참조하여
해당 핸들러 메서드를 처리할 수 있는 HandlerAdapter가 있는지 조회한다.
(이전에는 Controller, HttpRequestHandler 인터페이스 구현체 중에 조회)

이후 HandlerAdapter를 실행하여 처음에 조회한 Handler를 실행하고
결과를 반환받아 응답 메시지를 생성한다.

HTTP Response Message 생성

Spring에서 @RequestMapping 애노테이션이 선언된 메서드에
어떤 반환 타입을 등장할 수 있는지 먼저 알아보자.

@Controller + String

@Controller
class ControllerClass {
	
    @ReuqestMapping("/home")
    public String homeMethod() {
    	return "home";  // ** 뷰 이름으로 인식 **
    }
}

@Controller 애노테이션이 선언된 클래스에
@RequestMapping 애노테이션이 선언된 메서드가 String을 반환하는 경우,
해당 문자열을 뷰 이름으로 인식한다.
이때는 View Resolver가 동작한다.

예를 들어 위 코드는 /WEB-INF/home.jsp 파일을 찾아
Spring이 자동으로 렌더링한다(JSP View Resolver의 경우).

참고로 @RequestMapping의 url을 배열로 선언할 수 있다.

@ReuqestMapping({"/home", "/house"})

@RestController + String

@RestController는 @Controller와 @ResponseBody가 합쳐진 애노테이션이다.
애노테이션을 클래스에 선언하면 자동으로 모든 메서드에 @ResponseBody가 적용된다.
@Controller로 선언 후 메서드마다 별도로 @ResponseBody를 선언할 수 있다.

@ResponseBody 애노테이션이 선언된 클래스의 메서드 또는 메서드의
반환 타입이 String이라면 해당 문자열은 message body에 그대로 작성된다.
message body만 작성할 수 있다.

@RestController
@RequestMapping("/url")
public String requestMethod() {
	return "messageBody";  // ** messageBody에 문자열 그대로 작성 **
}

HttpEntity<String>, HttpEntity<Object>, ResponseEntity<Object>

메서드 반환 타입이 HttpEntity<String>인 경우
message body에 직접 데이터를 작성한다.
@RestController + String 방식은 message body만 작성할 수 있지만,
HttpEntity는 header 정보도 작성하여 전달할 수 있다는 점에서 차이가 있다.

@RequestMapping("/url")
public HttpEntity<String> requestMethod() {
	HttpHeaders headers = new HttpHeaders();
    headers.set("CustomHeader", "value");
    
    // ** Header 정보 설정하여 생성 가능 **
    return new HttpEntity<>("messageBody", headers);
}

메서드의 반환 타입이 HttpEntity<Object> 또는 ResponseEntity<Object>인 경우
객체를 JSON 형태로 변환하여 message body에 작성한다.
ResponseEntity<Object>에는 HTTP 응답 코드를 설정할 수 있다는 점에서 차이가 있다.

ResponseEntity<>("messageBody", HttpStatus.OK);

기존에는 ObjectMapper를 사용하여 messageBody에 입력했지만,
반환 타입을 설정하면 별도로 객체를 생성해서 처리할 필요는 없다.

@RestController + Object

@ResponseBody 애노테이션이 선언된 클래스의 메서드 또는 메서드의
반환 타입이 Object라면 JSON 형태로 자동 변환되어 message body에 작성된다.

@RestController
@RequestMapping("/url")
public User requestMethod() {
	return new User("kim", 20);  // ** JSON 형태로 변환하여 message body에 작성 **
}

HTTP Message Converter

@Controller + String가 뷰 이름을 인식하여 View Resolver가 동작하는 것 외에
다른 방식은 모두 HTTP Message Converter가 동작한다.

HTTP Message Converter는 반환 값을 문자열, JSON 형태 등으로 변환하여
message body에 작성해주는 역할을 수행한다.

@RestController
@RequestMapping("/url")
public User requestMethod() {
	return new User("kim", 20);  // ** JSON 형태로 변환하기 위해 Message Converter 동작 **
}

HandlerAdapter가 반환 타입에 따라 어떻게 로직을 수행할 지 결정하는 역할이라면,
HTTP Message Converter실제로 로직을 수행하는 역할인 것이다.

ReturnValueHandler

선언된 애노테이션에 따라 반환 값을 어떻게 처리할지 결정하는 역할을 수행한다.
지금까지 소개한 애노테이션, 반환 값이 각각 다른 메서드들이 적절한 로직을 수행하도록
Spring에서 자동으로 HandlerAdapter를 조회하는 객체이다.

예를 들어 @Controller + String이라면 뷰 이름으로 해석하는 HandlerAdpter를,
@ResponseBody + Object라면 message body에 JSON 형태로 변환하여 작성하는
HandlerAdapter를 호출하여 일관적으로 처리하는 것이다!

마무리 정리

애플리케이션을 운영하는 대표적인 구조인 MVC + Service Pattern에 대해 알아보고
Spring에서 MVC + Service 구조로 동작하는 과정을 알아봤다.
정리해보면 순서는 다음과 같다.

  1. Client URL Request
  2. DispatcherServlet가 Client의 모든 URL Request를 받게 된다.
  3. DispatcherServlet이 @RequestMapping 방식으로
    요청을 처리할 Handler(Controller)를 조회한다.
  4. 조회한 Handler를 처리할 HandlerAdapter를 조회한다.
  5. 조회한 HandlerAdapter가 Handler를 실행한다.
  6. Handler 실행 과정에서 Client가 전송한 데이터를
    @ModelAttribute 등 다양한 방식으로 확인한다.
  7. Handler는 실행 결과로 @Controller + String의 ModelAndView 또는
    @ResponseBody + String(또는 Object)과 같은 다양한 타입을 반환한다.
  8. View는 반환 타입에 따라 적절하게 화면을 출력하거나 데이터를 전송한다.

Spring은 거대한 생태계를 이루고 있는 만큼 그 내용이 매우 방대하다.
학습해야할 요소가 많지만 동작 과정과 원리를 이해하고
슬기롭게 Spring을 사용해보자.

profile
복습에 대한 비판과 지적을 부탁드립니다

0개의 댓글