* 본 포스팅은 김영한님의 SpringMVC 강의를 듣고 내맘대로 요약해서 왜?에 초점을 맞춘 포스팅입니다.
핵심: 막줄
지난 포스팅으로 스프링의 등장 배경과 핵심기능에 대해 살펴보았다.
이번 시리즈에서는 스프링 MVC에 대해 자세히 알아보고자 하는데, 컨셉은 마찬가지로 왜? 에 맞춰볼 예정이다.
사실 나는 스프링 부트 2.x도 제대로 사용해본적 없이 3.0부터 본격적으로 개발을 시작했기 때문에, 이전 스펙에 대해 잘 모른다.
당연히(?) 왜 Spring MVC를 사용하는게 편한지, 이전과 무엇이 달라졌는지도 몰랐다.
서블릿
태초에 웹서버가 있었다...
Web Server → 정적 리소스 제공 (Nginx, apache)
WAS → 웹서버 기능 포함,프로그램 코드 실행 → 로직 수행 가능
(동적 HTML, REST API, 서블릿,JSP)}
웹 시스템 구성 - WAS,DB
WAS가 너무 많은 역할을 담당, 서버 과부화 우려
정적리소스는 싸고 어플로직은 비쌈.
WAS 장애시 오류 화면도 노출 불가능
따라서 웹서버를 앞에 두고 정적 이미지를 여기서 처리
이번엔 서블릿이 뭔지 간단하게 정리해보자.
예를들어 http post 요청이 왔다고 생각하자.


그럼 서버에서 어떤 일이 일어날까
WAS를 우리가 밑바닥부터 다 만든다고 하면,
연결부터 HTTP 메세지를 싸악 다 분해해야하고 ....
ex) 서버 tcp/ip 연결 대기, 소켓 연결, http 요청메세지를 파싱해서 읽기, 메서드 확인, url 확인, content-type 확인, 메세지 바디 내용 파싱(파라미터 내용) - 비즈니스 로직 실행 - http 응답 메세지 생성 - http 시작 라인 생성, 헤더 생성, 메시지 바디에 html 생성해서 입력, tcp/ip에 응답 전달, 소켓 종료
요걸 요청마다 반복한다고 생각하면 끔찍하다.
그래서 비즈니스 로직 실행 을 제외한 기능을 WAS의 서블릿이 자동화해줌.
특: url 호출 어노테이션 있음
HttpServlet을 상속해 Http 요청,응답 정보를 편리하게 제공하는 HttpServletRequest, HttpServletResponse 제공
서블릿 컨테이너 → 내가 서블릿 객체를 만드는게 아니라,
WAS안에 서블릿 컨테이너가 자동으로 생성하고 호출하고 생명주기에 맞게 관리해준다.
서블릿 객체는 싱글톤으로 관리하므로 공유변수 사용에 주의해야한다.
스레드
클라이언트가 요청을 하면 TCP/IP 연결이 되고 서블릿이 호출이 되는데, 이 서블릿을 누가 호출하는데?
이게 바로 쓰레드
동시처리가 필요하면 쓰레드를 추가로 생성해줘야 함
실무 팁: 성능 튜닝
WAS의 주요 튜닝 포인트는 최대 thread 수이다.
이 값을 잘 조절하는게 중요
낮게 설정: 최대 쓰레드 10개 설정 → cpu 5% 사용
높게 설정: 동시요청이 너무 많아서 CPU 메모리 임계점 초과로 서버 다운
장애 발생시 → 클라우드면 일단 서버부터 늘리고 튜닝
클라우드가 아니면 튜닝 열심히!
서블릿을 지원하는 WAS는 멀티쓰레드도 지원한다.
멀티쓰레드에 대한 부분은 WAS가 처리
개발자가 멀티 쓰레드 관련 코드를 신경쓰지 않아도 됨
마치 싱글 쓰레드 프로그래밍을 하듯이 편리하게 소스코드를 개발하면 된다.
그 다음은 HTML, HTTP API, CSR, SSR 에 대한 내용이다.
웹은 역시 양이 많고 복잡해서 그런지 왜?에 대한 내용을 파고들려면 이것저것 알아야하는게 많다.
정적 리소스 → 주로 웹 브라우저
(이미 생성된 리소스 파일)
동적인 HTML 파일을 생성하여 전달
→ WAS가 로직 처리후 동적으로 html을 생성하는데 jsp, 타임리프 등등을 이용한다.
→ 주문 정보 조회한 데이터를 가지고 웹브라우저에 내려주는 방법은?
HTTP API
html전달이 아닌, JSON을 전달
다양한 시스템에서 호출 → 웹 클라 to 서버, 서버 to 서버, 앱 클라 to 서버
→ DATA JSON(”주문번호”:100, “금액”:5000)
당연한 기능들이며 한번씩은 다 경험해 봤다.
강의에는 백엔드의 고민 세가지가 등장한다.
1. 정적 리소스 어케 제공?
2. 동적 제공하는 html 페이지 어케 제공?
3. http api 어케 제공?
요렇게 3가지 인데, 여기서 SSR, CSR의 개념이 나온다.
SSR: 서버에서 최종 html을 생성하여 클라에게 전달
타임리프 등 동적에서 생성한 뒤 웹에 html 그대로 렌더링하여 보여주는 것
CSR: HTML 결과를 js를 이용해 웹 브라우저에서 동적 생성하여 적용
주로 동적인 화면에 적용, 웹 환경을 마치 앱처럼 필요한 부분부분 변경할 수 있음
CSR 과정
- 서버에 html 요청 → 요건 ssr든 csr이든 동일
- 서버가 텅텅빈 html에 자바스크립트 링크를 담아 응답
- 자바스크립트 요청 → 클라로직+ html 렌더링 코드
- HTTP API - 데이터 요청 → JSON으로 응답
- 자바스크립트로 html 결과 렌더링
강사님은 백엔드의 웹 프론트 기술 학습은 옵션이라고 하신다.
웹 프론트도 깊이 있게 하려면 많은 시간이 필요하고 백엔드도 정말 수많은 기술을 알아야 하기에...
하지만 난 프론트도 배워보겠어!(나중에)
자바 백엔드 웹 기술 역사
이제까지 내용을 정리하고 나면 이제 웹 기술이 왜 변화해 왔는지 이해할 수 있다.
과거 기술
서블릿 - 1997 - tcp 연결, 멀티쓰레드 등 문제해결 (자바로 html 생성이 어려움)
jsp은 편하지만 비즈니스 로직까지 너무 많은 역할 담당
서블릿, JSP 조합 MVC 패턴 사용
- 모델, 뷰, 컨트롤러로 역할을 나누어 개발
- 스프링 MVC, MVC 패턴 자동화
현재: 어노테이션 기반의 스프링 MVC : @Controller
종결
스프링 부트의 등장
스프링 부트는 서버를 내장
과거에는 서버에 WAS를 직접 설치하고, 소스는 WAR 파일을 만들어서 설치한 WAS에 배포
스프링부트는 빌드 결과 (jar)에 WAS 서버 포함 → 빌드 배포 단순화
Web Servlet - sping mvc
Web Reactive- spring webflux - 채신 기술
-> 비동기 논 블로킹 처리
최소 쓰레드로 최대 성능 → 쓰레드 컨텍스트 스위칭 비용 효율화( thread를 CPU 코어 수에 맞춤)
함수형 스타일로 개발 → 동시 처리 코드 효율화
서블릿 기술 사용X - netty라는 웹 프레임워크를 사용
but 기술 난이도 매우매우 높음
아직 RDB 지원 부족 - 레디스. elastic -search, mongoDB
일반 MVC 쓰레드 모델도 충분히 빠르다.
아직 실무에서 많이 사용하지 않음...이라고 하지만 난 이미 이걸 사용한 플젝을 너무 많이 봤다.
그리고 직접 경험하기 위해 webflux를 다음 플젝에 사용해볼 예정이다. 으흐흐..
암튼 이렇게 살짝 역사를 알아봤으면 이제 자바 뷰 템플릿 역사도 살짝 볼건데,
html 을 동적으로 편리하게 생성하는 뷰 기능의 역사를 정리해보자.
JSP: 속도 느림
프리마커, 벨로시티 → 속도 문제 해결, 기능 지원, 발전이 없음
Thymeleaf
내추럴 템플릿: html 모양을 유지하며 뷰 템플릿 적용 가능 → 깔끔
스프링 MVC와 기능 통합이 강력함
최선의 선택이나 성능은 벨로시티나 프로마커가 더 좋음(체감은 잘 안됨).
이렇다고 한다.
자 그럼 스프링 MVC가 등장하게 된 배경은 어느 정도 알것 같다.
자바 서블릿으로 tcp 연결, 멀티쓰레드 등 문제해결을 했으나, 자바로 동적 html을 생성하는데 너무 어려움이 있었다.
jsp은 편하지만 비즈니스 로직까지 너무 많은 역할을 담당했었다.
그래서 서블릿, JSP 조합 MVC 패턴을 사용하여 모델, 뷰, 컨트롤러로 역할을 나누는 개발이 나왔다.
어... 그럼 이미 해결이 된거 아닐까요? 왜 스프링 MVC라는 또 새로운 프레임워크가 나온거죠?
그냥 더 편해서겠지... 굳이 서블릿과 jsp, mvc의 개발을 직접 해봐야 할까 싶은 생각이 들었는데,,,,
ㅠㅠㅠ 강의가 내용이 너무 많아서 도저히 스킵할 수가 없었다.
불편을 겪어봐야 신세계를 알 수 있는 법
겪어보자.
우선은 스프링 웹(내장 서버) 없이 개발 해보기
서블릿은 톰캣같은 웹 어플 서버를 직접 설치하고, 그 위에 서블릿 코드를 클래스 파일로 빌드해서 올린다음, 톰캣 서버를 실행하면 되나, 너무 번거롭다. 스프링 부트는 톰캣 서버를 내장하고 있으니 서버 설치 없이 실행이 가능하다.
서블릿 컴포넌트 스캔 → 빈 등록처럼 서블릿을 직접 등록하여 사용
웹 어플 개발을 해보기 전에 알아둬야 할 개념이 하나 있다.
@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("HelloServlet.service");
System.out.println("req = " + req);
System.out.println("resp = " + resp);
String username = req.getParameter("username");
System.out.println("username = " + username);
resp.setContentType("text/plain");
resp.setCharacterEncoding("utf-8");
resp.getWriter().write("hello"+username);
}
}
Http 요청이 왔을때 서블릿 컨테이너의 동작
Http 요청이 오면 서블릿 컨테이너가 response를 만들어 던져줌
중요
이 요청,응답객체가 Http 요청, 응답을 편리하게 사용하도록 도와주는 객체임.
따라서 잘 이용하려면 HTTP 스펙이 제공하는 요청, 응답 메세지 자체를 이해해야 함.
HttpServletRequest - 기본 사용법
많은 http 요청에 대한 정보를 볼 수 있음
Http 요청 데이터를 볼때는 3가지 방법을 사용한다.
Get - 쿼리파라미터 (검색,필터, 페이징)
Post -Html form (회원가입)
Http Message body- 직접 데이터를 담아서 (Rest API)
HttpServletResponse
응답도 마찬가지로 여러 기능이 있다.
200,400등 코드 설정
헤더 및 바디 생성,
Content-Type, 쿠키, Redirect..
코드로 한번만 보자....
(응답 관련 편의 메서드)
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//status Line
resp.setStatus(200);
resp.setStatus(HttpServletResponse.SC_OK);
resp.setHeader("Content-Type","text/plain");
resp.setHeader("Cache-Control","no-cache,no-store,must-revalidate");
resp.setHeader("Pragma","no-cache");
resp.setHeader("my-header","hello");
PrintWriter writer= resp.getWriter();
writer.println("ok");
}
private void content(HttpServletResponse response) {
//Content-Type: text/plain;charset=utf-8
//Content-Length: 2
//response.setHeader("Content-Type", "text/plain;charset=utf-8");
response.setContentType("text/plain");
response.setCharacterEncoding("utf-8");
//response.setContentLength(2); //(생략시 자동 생성)
}
private void cookie(HttpServletResponse response) {
//Set-Cookie: myCookie=good; Max-Age=600;
//response.setHeader("Set-Cookie", "myCookie=good; Max-Age=600");
Cookie cookie = new Cookie("myCookie", "good");
cookie.setMaxAge(600); //600초
response.addCookie(cookie);
}
private void redirect(HttpServletResponse response) throws IOException {
//Status Code 302
//Location: /basic/hello-form.html
//response.setStatus(HttpServletResponse.SC_FOUND); //302
//response.setHeader("Location", "/basic/hello-form.html");
response.sendRedirect("/basic/hello-form.html");
}
자 그러면 진짜 해보자.
먼저 서블릿으로 회원관리 웹 어플 만들기
근데 자바 부분은 빼고, 웹 요청 응답 부분만 볼것이다.
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
PrintWriter writer = response.getWriter();
writer.write("<!DOCTYPE html>\n" +
"<html>\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <title>Title</title>\n" +
"</head>\n" +
"<body>\n" +
"<form action=\"/servlet/members/save\" method=\"post\">\n" +
" username: <input type=\"text\" name=\"username\" />\n" +
" age: <input type=\"text\" name=\"age\" />\n" +
" <button type=\"submit\">전송</button>\n" +
"</form>\n" +
"</body>\n" +
"</html>\n");
//와씨....
}
코드 진짜 실화냐고
응답
@WebServlet(name = "memberSaveServlet", urlPatterns = "/servlet/members/save")
public class MemberSaveServlet extends HttpServlet {
private MemberRepository memberRepository= MemberRepository.getInstance();
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("MemberSaveServlet.service");
String username = req.getParameter("username");
int age = Integer.parseInt(req.getParameter("age"));
Member member= new Member(username,age);
memberRepository.save(member);
resp.setContentType("text/html");
resp.setCharacterEncoding("utf-8");
PrintWriter w = resp.getWriter();
w.write("<html>\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
"</head>\n" +
"<body>\n" +
"성공\n" +
"<ul>\n" +
" <li>id="+member.getId()+"</li>\n" +
" <li>username="+member.getUsername()+"</li>\n" +
" <li>age="+member.getAge()+"</li>\n" +
"</ul>\n" +
"<a href=\"/index.html\">메인</a>\n" +
"</body>\n" +
"</html>");
}
}
근데 잘 보면 동적 html임을 알 수 있다.

토할거 같애 이제 안할래..
서블릿이 뭔가 자바 코드 실행하는거, 파라미터 받아오는건 괜찮아보인다. HttpServletRequest 덕분에
근데 html은 진짜 카아아악!!!
자바 코드로 HTML을 만들어 내는 것 보다 차라리
HTML 문서에 동적으로 변경해야 하는 부분만 자바 코드를 넣을 수 있다면 더 편리할텐데...
그래서 템플릿 엔진을 사용한다.
→ JAVA에 html을 넣지 않고,
→ html안에 java를 넣는거임
그게 JSP, Thymeleaf임
오케 이제 JSP로 해보자
템플릿 엔진을 사용하면 뭐 얼마나 편할까
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<form action="/jsp/members/save.jsp" method="post">
username: <input type="text" name="username" />
age: <input type="text" name="age" />
<button type="submit">전송</button>
</form>
</body>
</html>

안 편해... 그냥 내가 html을 작성하기 너무 싫어해서 그런가 ㅋㅋㅋ
암튼 진짜 문제는 이게 아니라 다른 쪽에 있다.
서블릿으로 개발할 때는 뷰(View)화면을 위한 HTML을 만드는 작업이 자바 코드에 섞여서 지저분하고 복잡했다.
JSP를 사용한 덕분에 뷰를 생성하는 HTML 작업을 깔끔하게 가져가고, 중간중간 동적으로 변경이 필요한 부분에만 자바 코드를 적용했다. 그런데 이렇게 해도 해결되지 않는 몇가지 고민이 남는다.
회원 저장 JSP를 보자. 코드의 상위 절반은 회원을 저장하기 위한 비즈니스 로직이고, 나머지 하위 절반만 결과를 HTML로 보여주기 위한 뷰 영역이다. 회원 목록의 경우에도 마찬가지다.
코드를 잘 보면, JAVA 코드, 데이터를 조회하는 리포지토리 등등 다양한 코드가 모두 JSP에 노출되어 있다. JSP가 너무 많은 역할을 한다.

제일 중요한 변경의 라이프 사이클
사실 이게 정말 중요한데, 진짜 문제는 둘 사이에 변경의 라이프 사이클이 다르다는 점이다. 예를 들어서 UI를 일부 수정하는 일과 비즈니스 로직을 수정하는 일은 각각 다르게 발생할 가능성이 매우 높고 대부분 서로에게 영향을 주지 않는다. 이렇게 변경의 라이프 사이클이 다른 부분을 하나의 코드로 관리하는 것은 유지보수하기 좋지 않다. (물론 UI가 많이 변하면 함께 변경될 가능성도 있다.)
그래서 MVC 패턴이 등장했다.!!
아 이제 좀 편해지려나??
서블릿이나 JSP 만 가지고 처리하기엔 어려움이 있었다.
하나의 서블릿과 JSP만으로 비즈니스 로직+ 뷰 렌더링까지 다 하려니 너무 힘듦
분리
- 컨트롤러: HTTP 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행한다. 그리고 뷰에 전달할 결과 데이터를 조회해서 모델에 담는다.
- 모델: 뷰에 출력할 데이터를 담아둔다. 뷰가 필요한 데이터를 모두 모델에 담아서 전달해주는 덕분에 뷰는 비즈니스 로직이나 데이터 접근을 몰라도 되고, 화면을 렌더링 하는 일에 집중할 수 있다.
- 뷰: 모델에 담겨있는 데이터를 사용해서 화면을 그리는 일에 집중한다. 여기서는 HTML을 생성하는 부분을 말한다.
서블릿을 컨트롤러로, JSP를 뷰로
모델은?
Model은 HttpServletRequest 객체를 사용.
request는 내부에 데이터 저장소를 가지고 있다.
request.setAttribute()를 통해 데이터 설정 ㄱㄴ
예시를 봅시다.
@WebServlet(name = "mvcMemberFormServlet",urlPatterns = "servlet-mvc/members/new-form")
public class MvcMemberFormServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String viewPath= "/WEB-INF/views/new-form.jsp";
RequestDispatcher requestDispatcher = req.getRequestDispatcher(viewPath);
requestDispatcher.forward(req,resp);
}
}
dispatcher.forward() : 다른 서블릿이나 JSP로 이동할 수 있는 기능이다. 서버 내부에서 다시 호출이 발생한다.
여기서 dispatcher는 컨트롤러에서 뷰로 이동하기 위해 사용함
/WEB-INF
이 경로안에 JSP가 있으면 외부에서 직접 JSP를 호출할 수 없다. 우리가 기대하는 것은 항상 컨트롤러(서블릿)를 통해서 JSP를 호출하는 것이다.
redirect vs forward
리다이렉트는 실제 클라이언트(웹 브라우저)에 응답이 나갔다가, 클라이언트가 redirect 경로로 다시 요청한다. 따라서 클라이언트가 인지할 수 있고, URL 경로도 실제로 변경된다. 반면에 포워드는 서버 내부에서 일어나는 호 출이기 때문에 클라이언트가 전혀 인지하지 못한다.
컨트롤러의 역할, 뷰의 역할을 잘 분리했음
단순하게 모델에서 데이터를 꺼내고 화면을 만들면 되는데,
아… 포워드 중복이 있네 → 메서드화 할까? -> 근데 메서드화 해도 호출 중복
jsp가 다 뿌리므로 response 객체가 잘 안쓰이기도 함
공통 처리가 어려움
기능이 복잡해질 수록 공통 처리할 부분이 많아질텐데, 이게 어렵다
이 문제를 해결하려면 컨트롤러 호출 전에 먼저 공통 기능을 처리해야 한다. 소위 수문장 역할을 하는 기능이 필요하다.
- 프론트 컨트롤러(Front Controller) 패턴을 도입하면 이런 문제를 깔끔하게 해결할 수 있다.(입구를 하나로!)
스프링 MVC의 핵심도 바로 이 프론트 컨트롤러에 있다고 한다.
자 그럼 스프링 MVC를 진짜 배워볼까?!!
하지만 우리는 직접 프론트 컨트롤러 패턴을 도입하여 프레임워크를 만들거고, 그 다음에 스프링 MVC로 넘어가도록 하겠따...
1줄 요약
스프링 MVC 왜 나옴?
동적 html 처리 -> 서블릿 등장 -> 서블릿은 Tcp 연결, 멀티쓰레드 등 문제해결 but 자바로 html 생성이 어려움 -> JSP -> 편하지만(공감 못함) 너무 많은 로직이 담겨있어 유지보수 에바 -> 둘 합친 MVC 패턴 -> 훌륭한데 중복이 너무 많음(공통처리가 어려움)
-> 프론트 컨트롤러가 나오고 발전되어 스프링 MVC로 정리
자 이제 프론트 컨트롤러 패턴과 그 뒤 부분은 다음 포스팅으로 정리하도록 하겠다.
(오전 8시... 배고파)
'Spring_Why?' 카테고리의 다른 글
| [Spring/MVC] 스프링 웹 MVC 마무리 (1) | 2025.05.02 |
|---|---|
| [Spring/MVC] 직접 만드는 MVC 패턴 - FrontController 도입 (1) | 2025.04.24 |
| [Spring/기본] 스프링의 핵심 기능 (2) | 2025.04.11 |
| [Spring/기본] 스프링, 내가 생각한 핵심 개념 (0) | 2025.04.07 |