지난 시간에 MVC 패턴을 직접 만들면서, 점점 더 개발자가 이용하기 편한 방식의 패턴을 만들었다.
그리고 마침내 V5버전까지 가면서 현재의 MVC 패턴과 비슷하게 되었다.
이제 스프링 웹 MVC를 본격적으로 파헤쳐보자...!

우리가 만든 프론트 컨트롤러가 디스패쳐 서블릿이라 보면 된다.
디스패쳐 서블릿도 부모 클래스에서 HttpServlet을 상속박아 사용하고, 서블릿으로 동작한다.
흐름: 서블릿 호출시 service()가 호출
스프링 MVC는 디스패쳐 서블릿의 부모인 frameworkServlet에서 service를 오버라이드 해놨다.
이걸 시작으로 여러 메서드가 호출되며 최종적으로 doDispatch가 호출된다

스프링 부트는 자동으로 여러 핸들러 매핑과 어댑터를 등록한다.
그중 자주 나오는 핸들러 매핑과 어댑터는 아래와 같다.
핸들러 매핑
0=RequestMappingHandlerMapping : @RequestMapping에서 사용
1=BeanNameUrlHandlerMapping : 빈의 이름으로
핸들러 어댑터
0 = RequestMappingHandlerAdapter : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
1 = HttpRequestHandlerAdapter : HttpRequestHandler 처리
2 = SimpleControllerHandlerAdapter : Controller 인터페이스(애노테이션X, 과거에 사용) 처리
또한 뷰 리졸버도 자동등록하는데 그 일부를 보면 아래와 같다.
1 = BeanNameViewResolver : 빈 이름으로 뷰를 찾아서 반환한다. (예: 엑셀 파일 생성 기능에 사용)
2 = InternalResourceViewResolver : JSP를 처리할 수 있는 뷰를 반환한다.
그래서 실제로 많이 쓰이는것은?
사실 스프링 웹 MVC 1편 강의를 듣기 전에, 스프링으로 여러 프로젝트를 해보았기 때문에 이 부분은 내가 평소에 궁금했던 부분이나 원리, 혹은 놓친 부분 위주로 정리한다.
1. @RequestMapping
이건 뭐... 스프링 입문이었나? 에서도 썼던걸로 기억하는데. 컨트롤러를 처음 접할때 배워놔서 기억하고 있다.
2. @RestController
@ResponseBody와 @Controller의 결합인데, 쉽게 말해 스트링을 반환할때 뷰 경로가 아니라 메세지 바디 그자체에 반환한 값을 담는다. 요것도 자주 사용하지만 다시 정리해봤다.
3. @RequestParam
자 @Pathvariable, @RequestParam, @RequestBody, @ModelAndView를 컨트롤러 레벨의 메서드에서 파라미터 항목에 자주 들어가는데, 일단은 요청받는 데이터의 형식이 RestAPI(메세지 바디에 JSON 담음)인지 Http form이나 url에 값을 담아 보내는지에 따라 사용법이 갈린다. 사실상 기능 강의에서는 요 부분이 가장 중요한 것 같다.
조금 더 살펴보면,
어노테이션 기반의 스프링 컨트롤러는 다양한 파라미터를 지원한다.
서블릿에서 요청 헤더 꺼내던거 기억나는가?
요청 헤더 정보 조회 방법은 아래와 같다.
@Slf4j
@RestController
public class RequestHeaderController {
@RequestMapping("/headers")
public String headers(HttpServletRequest request,
HttpServletResponse response, HttpMethod httpMethod, Locale locale,
@RequestHeader MultiValueMap<String, String> headerMap,
@RequestHeader("host") String host,
@CookieValue(value = "myCookie", required = false) String cookie) {
return "ok";
}
}
진짜 다양하게 받을 수 있다.
그럼 요청 파라미터는??
서블릿에서 어떻게 했더라?
클라이언트에서 서버로 요청 데이터를 전달할때는 주로 다음 3가지 방법을 사용한다.

잘 떠올려보면 request.getParameter에서 get,post(Html Form) 의 값 모두 받을 수 있었다.
request.getParameter()
여기서는 단순히 HttpServletRequest가 제공하는 방식으로 요청 파라미터를 조회했다.
그걸 이제 어노테이션으로 어떻게 받느냐..?
바로 @RequestParam인데,
@ResponseBody
@RequestMapping("/request-param-v2")
public String requestParamV2(
@RequestParam("username") String memberName,
@RequestParam("age") int memberAge
){
log.info("username={}, age={}",memberName,memberAge);
return "ok";
}
@ResponseBody
@RequestMapping("/request-param-v3")
public String requestParamV3(
//변수명과 속성이름이 같으면 생략가능
@RequestParam String username,
@RequestParam int age) {
log.info("username={}, age={}", username, age);
return "ok";
}
@ResponseBody
@RequestMapping("/request-param-v4")
public String requestParamV4(
//변수명과 요청파라미터 이름이 같으면 생략가능
String username, int age) {
log.info("username={}, age={}", username, age);
return "ok";
}
}
진짜 편리하긴 하다.
그리고 생략에서 조금 생각해볼것은, @ModelAttribute과 겹친다는 것인데 이건 조금 이따 언급
그리고 required등을 이용해서 필수여부를 결정할 수 있다!
내가 잘 모르던거
개발을 하다보면 요청 파라미터를 받아서 필요한 객체를 만들고, 그 객체에 값을 넣어주어야 한다.
@ResponseBody
@RequestMapping("/model-attribute-v1")
// public String modelAttributeV1(@RequestParam String username, @RequestParam int age){
// HelloData helloData= new HelloData();
// helloData.setUsername(username);
// helloData.setAge(age);
//
// log.info("username={}, age={}", helloData.getUsername(),
// helloData.getAge(), helloData.toString());
//
// return "ok";
// }
public String modelAttributeV1(@ModelAttribute HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(),
helloData.getAge());
return "ok";
}
주석 처리된 부분과 아닌 부분이 동일한 기능을 하는구나...
스프링 MVC는 @ModelAttribute가 있으면 다음을 실행한다.
객체 생성-> 요청 파라미터의 이름으로 객체의 프로퍼티를 찾는다.
그리고 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력(바인딩) 한다.
예) 파라미터 이름이 username이면 setUsername() 메서드를 찾아서 호출하면서 값을 입력한다.
그리고 이 모델 어트리뷰트는 생략이 된다!
@ResponseBody
@RequestMapping("/model-attribute-v2")
public String modelAttributeV2(HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(),
helloData.getAge());
return "ok";
}
그럼 @RequestParam도 생략가능한데 헷갈리는데요!
스프링은 생략시 이런 규칙을 적용한다.
String , int 등 단순타입 -> @RequestParam
나머지 = @ModelAttribute
지금까지는 요청 파라미터를 어떻게 처리하는지 알아봤다.
다음은 Http 메세지 바디에 데이터가 직접 넘어오는걸 보자.
HTTP message body에 데이터를 직접 담아서 요청 HTTP API에서 주로 사용, JSON, XML, TEXT 데이터 형식은 주로 JSON 사용
요청 파라미터와 다르게, HTTP 메시지 바디를 통해 데이터가 직접 넘어오는 경우는 @RequestParam ,@ModelAttribute 를 사용할 수 없다. (물론 HTML Form 형식으로 전달되는 경우는 요청 파라미터로 인정된다.)
@Slf4j
@Controller
public class RequestBodyStringController {
@PostMapping("/request-body-string-v1")
public void requestBodyString(HttpServletRequest request, HttpServletResponse response)throws IOException{
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("messageBody={}",messageBody);
response.getWriter().write("ok");
}
@PostMapping("/request-body-string-v2")
public void requestBodyStringV2(InputStream inputStream, Writer responseWriter)throws IOException{
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("messageBody={}",messageBody);
responseWriter.write("ok");
}
}
원래는 이런식으로 메세지 바디를 받는다.
InputStream(Reader): HTTP 요청 메시지 바디의 내용을 직접 조회
OutputStream(Writer): HTTP 응답 메시지의 바디에 직접 결과 출력
@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity)throws IOException{
//http message converter동작
String messageBody= httpEntity.getBody();
log.info("messageBody={}",messageBody);
return new HttpEntity<>("ok");
}
/**
*
* HttpEntity: HTTP header, body 정보를 편리하게 조회
* - 메시지 바디 정보를 직접 조회(@RequestParam X, @ModelAttribute X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
* 응답에서도 HttpEntity 사용 가능
* - 메시지 바디 정보 직접 반환(view 조회X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*/
진짜 강조하는거: 요청파라미터와는 상관없이 메세지 바디를 가져오는 방법이라는거
public HttpEntity<String> requestBodyStringV3(RequestEntity<String> httpEntity)throws IOException{
//http message converter동작
String messageBody= httpEntity.getBody();
log.info("messageBody={}",messageBody);
return new ResponseEntity<>("ok", HttpStatus.CREATED);
}
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody)throws IOException{
log.info("messageBody={}",messageBody);
return "ok";
}
@HttpEntity는? @RequestHeader는? 헤더 필요할때
* 메세지 바디를 직접 조회하는 것과, 요청 파라미터를 조회하는@RequestParam, @ModelAttribute와는 정말 아무런 관련이 없다.
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData helloData) throws IOException {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData helloData) throws IOException {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
@ResponseBody
@PostMapping("/request-body-json-v4")
public String requestBodyJson43(HttpEntity<HelloData> httpEntity) throws IOException {
HelloData data = httpEntity.getBody();
log.info("username={}, age={}", data.getUsername(), data.getAge());
return "ok";
}
/**
* @RequestBody 생략 불가능(@ModelAttribute 가 적용되어 버림)
* HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter (content-type:
application/json)
*
* @ResponseBody 적용
* - 메시지 바디 정보 직접 반환(view 조회X)
* - HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter 적용(Accept:
application/json)
*/
@ResponseBody
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData data) {
log.info("username={}, age={}", data.getUsername(), data.getAge());
//반환까지 http 메세지 컨버터 작동
return data;
}
내용이 좀 긴데, 결국 어떻게 변화되었는지는 한번만 보고, 그 다음으로는 써야할 핵심 기능만 기억하자!
응답
⇒ 스프링에서 응답 데이터를 만드는 방법은 3가지
- 정적 리소스 → 정적 html, css 등 제공시
- 뷰 템플릿 → 동적 html 제공시
- HTTP 메세지 → API 제공시 html이 아니라 데이터를 전달해야함 → 메세지 바디에 JSON 같은 형식으로 데이터를 실어 보냄
정적 리소스 → 클래스 path의 아래 디렉토리에 있는 정적 리소스를 제공
/static , /public , /resources ,/META-INF/resources
src/main/resources 는 리소스를 보관하는 곳이고, 또 클래스패스의 시작 경로이다.
따라서 다음 디렉토리에 리소스를 넣어두면 스프링 부트가 정적 리소스로 서비스를 제공한다
자 이제 내가 좀 싫어하는 부분이 나왔는데...(싫어할 자격이 있는지는 모르겠지만)
나는 JSP가, 타임리프가 싫다...ㅠㅠ Rest API로 메세지 바디만 전달하고 싶지 모델 + 뷰를 내가 다루고 싶지가 않다...
물론 기업마다 프론트를 따로 취급하는 곳도, 서버사이드 렌더링을 하는 곳이 다르긴한데... ㅠㅠ
암튼 그래서 난 JSON형식으로 데이터를 보내는 부분만 정리하고, 렌더링을 배울 필요를 체감하게 되면 이 부분으로 돌아와서 다시 정리하겠다...
@Slf4j
@Controller
public class ResponseBodyController {
@GetMapping("/response-body-string-v1")
public void responseBodyV1(HttpServletResponse response) throws IOException{
response.getWriter().write("ok");
}
@GetMapping("/response-body-string-v2")
public ResponseEntity<String> responseBodyV2(){
return new ResponseEntity<>("ok", HttpStatus.OK);
}
@ResponseBody
@GetMapping("/response-body-string-v3")
public String responseBodyV3(){
return "ok";
}
//JSON 처리
@GetMapping("/response-body-json-v1")
public ResponseEntity<HelloData> responseBodyJsonV1(){
HelloData helloData = new HelloData();
helloData.setUsername("userA");
helloData.setAge(20);
return new ResponseEntity<>(helloData,HttpStatus.OK);
}
@ResponseStatus(HttpStatus.OK)
@ResponseBody
@GetMapping("/response-body-json-v2")
public HelloData responseBodyJsonV2(){
HelloData helloData = new HelloData();
helloData.setUsername("userA");
helloData.setAge(20);
return helloData;
//상태 코드를 못바꿈
//그래서 응답코드 어노테이션제공
}
}
뷰 템플릿으로 HTML을 생성해서 응답하는 것이 아니라, HTTP API처럼 JSON 데이터를 HTTP 메시지 바디에서 직
접 읽거나 쓰는 경우 HTTP 메시지 컨버터를 사용하면 편리하다.
EX) request에서 스트림읽어서 변환, response.getWriter()등…
스프링MVC는 다음 경우에 HTTP 메세지 컨버터 적용
HTTP 요청: @RequestBody , HttpEntity(RequestEntity)
HTTP 응답: @ResponseBody , HttpEntity(ResponseEntity)
메세지 컨버터는 http요청, 응답 둘다 사용된다.
canRead(),canWrite(): 메세지컨버터가 해당 클래스,미디어타입을 지원하는지 체크
read(),write(): 메세지 컨버터를 통해 메세지 읽고 쓰는기능
그럼 어느 부분에 컨버터가 있는거지?
RequestMappingHandlerAdapter 와 관련이 있다.
얘가 어떻게 동작하는데?
리퀘스트 매핑 핸들러 어댑터 호출
→ 컨트롤러를 호출해줘야함
코드를 한번 보자.

여기 컨트롤러를 호출하려면 @RequestParam, @ModelAttribute들을 다 만들어서 넘겨줘야하는데, 이걸 Argument Resolver가 처리해줌
ArgumentResolver
생각해보면, 애노테이션 기반의 컨트롤러는 매우 다양한 파라미터를 사용할 수 있었다. HttpServletRequest , Model 은 물론이고, @RequestParam , @ModelAttribute 같은 애노테이션
그리고 @RequestBody , HttpEntity 같은 HTTP 메시지를 처리하는 부분까지 매우 큰 유연함을 보여주었다. 이렇게 파라미터를 유연하게 처리할 수 있는 이유가 바로 ArgumentResolver 덕분이다.
RequestMappingHandlerAdapter 는 이 ArgumentResolver 를 호출해서, 다양한 파라미터의 값을 생성하고, 다 준비되면 컨트롤러를 호출해 값을 넘겨준다.
결국 컨버터는 Argument Resolver, ReturnValue Resolver앞에 있다.

메세지 컨버터까지 봤으면 어느정도 내용은 다 살펴보았고, 다음은 예제다.
그러나 예제 구현은 생략하기로 했다.
내용이 타임리프와 동적렌더링의 과정을 실습하는 내용이라 내가 공부하고 싶은 내용이 많이 들어가 있지 않았다.
타임리프는 나중에 다시 제대로 공부하기로 하고, 일단은 스프링 자체를 좀 더 깊이 파려한다.
마무리
MVC 2편 강의에는 검증, 쿠키-세션 직접 구현, 필터-인터셉터등 꼭 알아야할 부분이 많아서,
결국 내가 진짜 원하는걸 알기 위해선 MVC2편을 수강해야하는 것 같다…
MVC 1은 내가 어느정도 아는 부분, 잘 모르지만 지금 당장은 넘어가도 될 부분 등 여러 개념이 섞여 공부하기가 조금 어려웠다.
다만 다시 포스팅의 컨셉으로 돌아가 스프링 MVC가 왜 등장했고 어떻게 진화해왔는지, 그리고 그중 핵심 내용은 뭔지 정도는 짧게 정리하고 넘어가보자.
스프링 MVC 프레임워크의 진화
웹 애플리케이션 개발의 초기에는 개발자가 HTTP 요청과 응답을 직접 처리해야 했음
자바에서는 서블릿(Servlet)이 이 역할을 담당. 서블릿이 클라이언트의 요청을 처리하고 응답을 생성
단점
- HTML 생성을 위한 복잡한 코드 작성 필요
- 비즈니스 로직과 화면 로직의 혼재
- 중복 코드 발생
JSP의 등장
서블릿의 한계를 극복하기 위해 JSP(JavaServer Pages)가 도입, JSP는 HTML 내에 자바 코드를 작성할 수 있어 화면 개발이 용이
단점
- 비즈니스 로직과 뷰 로직이 한 파일에 혼재
- 유지보수의 어려움
MVC 패턴의 도입
이러한 문제를 해결하기 위해 MVC(Model-View-Controller) 패턴이 도입 MVC 패턴은 애플리케이션을 세 가지 역할로 구분
- Model: 데이터와 비즈니스 로직 처리
- View: 사용자에게 정보 표시 (JSP)
- Controller: 요청을 받아 모델과 뷰를 조정 (서블릿)
또 문제점
- 중복되는 코드 (viewPath 지정, forward 호출 등)
- 공통 처리의 어려움
- 개발자마다 다른 MVC 패턴 구현 방식
프론트 컨트롤러 패턴의 도입
이러한 문제점을 해결하기 위해 프론트 컨트롤러(Front Controller) 패턴이 도입, 프론트 컨트롤러는 모든 요청을 단일 컨트롤러에서 먼저 받아 처리
프론트 컨트롤러 패턴의 도입 이후 개선된 점
- 공통 로직 처리 가능
- 일관된 구조 제공
- 유연한 URL 매핑
V1: 기본 기능 구현
- URL 매핑 정보를 통해 각 컨트롤러 호출
V2: View 분리
- MyView 객체 도입으로 뷰 처리 로직 분리
- 각 컨트롤러는 MyView를 반환하고, 프론트 컨트롤러가 렌더링
V3: Model 추가
- ModelView 객체 도입으로 컨트롤러가 서블릿 기술에 종속되지 않도록 함
- 뷰 이름 중복 제거를 위한 ViewResolver 도입
V4: 단순화된 컨트롤러
- 컨트롤러에서 ModelView를 반환하지 않고 ViewName(String)만 반환
- 단순화된 컨트롤러 구조
V5: 유연한 컨트롤러
- 어댑터 패턴 도입으로 다양한 종류의 컨트롤러 처리 가능
- 핸들러 어댑터를 통한 확장성 확보
스프링 MVC 구조
- DispatcherServlet: 프론트 컨트롤러 역할
- HandlerMapping: 요청 URL과 핸들러(컨트롤러) 매핑
- HandlerAdapter: 핸들러 실행을 위한 어댑터
- ViewResolver: 뷰 이름을 실제 뷰로 변환
- View: 뷰 렌더링
@RequestMapping 기반 컨트롤러
- 관심사의 분리: 비즈니스 로직과 화면 로직 분리
- 중복 코드 제거: 공통 기능의 중앙화
- 유연한 설계: 인터페이스와 디자인 패턴을 통한 확장성 확보
- 개발자 친화적 API: 애노테이션 기반 프로그래밍으로 편의성 증대
후
총정리 끝!!!
왜 불편했는지 알아야, 이게 왜 편한지 알고 제일 중요한 기능이 뭔지 체감이 된다는 것을 조금씩 깨닫는줄 ...
'Spring_Why?' 카테고리의 다른 글
| [Spring/MVC] 직접 만드는 MVC 패턴 - FrontController 도입 (1) | 2025.04.24 |
|---|---|
| [Spring/MVC] 스프링 MVC 이전의 개발 (0) | 2025.04.16 |
| [Spring/기본] 스프링의 핵심 기능 (2) | 2025.04.11 |
| [Spring/기본] 스프링, 내가 생각한 핵심 개념 (0) | 2025.04.07 |