1. Servlet — 자바가 HTTP를 처리하는 방법
Spring은 Servlet 위에서 동작한다.
Servlet은 자바에서 HTTP 요청을 받아 처리하고 응답을 돌려주는 기술이다.
Node.js의 express에서 라우터 하나와 비슷한 역할을 한다.
// Node.js/Express 방식
app.get('/hello', (req, res) => {
res.send("Hello World");
});
// Java Servlet 방식
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res) {
res.getWriter().write("Hello World");
}
}
구조는 비슷하지만 Servlet은 훨씬 더 많은 설정이 필요하다.
초기에는 각 URL마다 Servlet 클래스를 하나씩 만들어야 했다고 한다.
/login → LoginServlet.java
/users → UsersServlet.java
/orders → OrdersServlet.java
URL이 100개면 Servlet도 100개가 필요했고, 인증 체크나 에러 처리 같은 공통 로직을 모든 Servlet에 반복해서 작성해야 했다.
이 불편함을 해결하기 위해 등장한 것이 Front Controller 패턴이다.
2. Web Server vs Web Application Server
Web Server — 정적인 것만 처리
Web Server는 정적 콘텐츠를 처리한다.
클라이언트가 요청하면 디스크에서 파일을 찾아 그대로 돌려준다.
별도의 로직 실행 없이 단순히 파일을 서빙하는 역할이다.
클라이언트 요청: GET /logo.png
Web Server 응답: (파일 찾아서 그대로 반환)
Web Application Server (WAS) — 동적인 것도 처리
WAS는 정적 콘텐츠뿐만 아니라 동적 콘텐츠도 처리한다.
요청마다 코드를 실행하고, DB를 조회하고, 그 결과로 응답을 만들어낸다.
클라이언트 요청: GET /users/1
WAS 처리: 코드 실행 → DB 조회 → 사용자 데이터 조합 → 응답 생성
| 구분 | Web Server | WAS |
| 처리 대상 | 정적 콘텐츠 (HTML, CSS, 이미지) | 동적 콘텐츠 (비즈니스 로직) |
| 처리 방식 | 파일을 그대로 반환 | 코드를 실행해서 응답 생성 |
| 대표 제품 | Nginx, Apache | Tomcat, Jetty |
💡 WAS도 정적 파일을 처리할 수 있다.
하지만 정적 파일만큼은 Web Server가 훨씬 빠르고 효율적이기 때문에 역할을 나눠 사용한다.
3. Front Controller 패턴
Servlet의 문제를 다시 떠올려보자. URL마다 Servlet을 만들어야 하고, 공통 로직이 반복된다.
Front Controller 패턴은 이 문제를 "입구를 하나로 만들자"는 아이디어로 해결한다.
// 기존 방식 — 입구가 여러 개
/login → LoginServlet (인증 체크, 로깅, 공통처리 + 실제 로직)
/users → UsersServlet (인증 체크, 로깅, 공통처리 + 실제 로직)
/orders → OrdersServlet (인증 체크, 로깅, 공통처리 + 실제 로직)
// Front Controller 방식 — 입구가 하나
모든 요청 → FrontController (공통처리) → 각 Controller에 위임
// FrontController — 공통 처리는 여기서 한 번만
public class FrontController extends HttpServlet {
doGet(request, response) {
checkAuth(request); // 인증 체크 — 한 곳에서
logging(request); // 로깅 — 한 곳에서
// URL을 보고 적절한 Controller로 위임
String url = request.getRequestURI();
if (url.equals("/login")) new LoginController().process();
if (url.equals("/users")) new UsersController().process();
}
}
Spring의 DispatcherServlet이 바로 이 Front Controller의 구현체이다.
Spring을 사용하면 이 역할을 프레임워크가 대신 해주기 때문에 우리는 실제 비즈니스 로직에만 집중할 수 있다.
4. Spring MVC Flow

실제로 HTTP 요청이 들어왔을 때 Spring 내부에서 어떤 일이 일어나는지 순서대로 살펴보자
① Request
② HandlerMapping
③ HandlerAdapter
④ Controller (+ Service → Repository → DB)
⑤ view name 반환
⑥ ViewResolver
⑦ View 렌더링
⑧ Response
① Request — 요청 진입
클라이언트가 GET /users/1을 요청하면 가장 먼저 DispatcherServlet이 받는다.
모든 요청은 예외 없이 DispatcherServlet을 거친다.
② HandlerMapping — "누가 처리할 거야?"
DispatcherServlet은 HandlerMapping에게 요청으로 들어온 URL을 처리할 수 있는 Controller를 묻는다.
@RestController
public class UserController {
@GetMapping("/users/{id}") // ← HandlerMapping이 이 정보를 기억해둡니다
public User getUser(...) { }
}
HandlerMapping은 애플리케이션이 시작될 때 모든 @GetMapping, @PostMapping 등을 스캔해서
URL과 메서드의 매핑 정보를 저장해둔다.
요청이 들어오면 이 정보를 참고해서 어느 Controller가 처리할지 알려준다.
③ HandlerAdapter — "어떻게 호출할 거야?"
HandlerMapping이 Controller를 찾았는데, 왜 바로 호출하지 않고 Adapter가 필요할까?
Spring은 오랜 역사를 가진 프레임워크라 Controller를 만드는 방식이 시대에 따라 여러 가지였다.
@RequestMapping 방식, Controller 인터페이스 방식, HttpRequestHandler 방식 등이 공존한다.
각 방식마다 호출하는 방법이 다르기 때문에, HandlerAdapter가 그 차이를 흡수해서
DispatcherServlet이 항상 동일한 방식으로 Controller를 호출할 수 있게 해준다.
④ Controller 실행
개발자가 작업하는 영역이다. Controller는 직접 비즈니스 로직을 처리하지 않고 Service에게 위임한다.
@GetMapping("/users/{id}")
public String getUser(@PathVariable Long id, Model model) {
User user = userService.findById(id); // Service에 위임
model.addAttribute("user", user); // 결과를 Model에 담기
return "user/detail"; // view name 반환
}
⑤ view name 반환
Controller가 반환하는 "user/detail"은 아직 실제 파일 경로가 아니다. 그냥 이름이다.
⑥ ViewResolver — "그 이름이 실제 어떤 파일이야?"
ViewResolver는 view name을 실제 뷰 파일 경로로 변환한다.
# application.properties
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
view name: "user/detail"
↓ ViewResolver 변환
실제 경로: /WEB-INF/views/user/detail.jsp
⑦⑧ View 렌더링 → Response
ViewResolver가 찾은 파일(JSP 등)에 Model 데이터를 주입해서 HTML을 완성하고 클라이언트에게 반환한다.
REST API
요즘은 JSP 대신 JSON을 반환하는 REST API 방식을 주로 쓴다. 이 경우 View와 ViewResolver 과정이 생략된다.
@RestController // @Controller + @ResponseBody
public class UserController {
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id);
}
}
| @Controller | @RestController | |
| 반환 방식 | view name → HTML | 객체 → JSON |
| View/ViewResolver | 필요 | 불필요 |
| 주요 용도 | 서버 사이드 렌더링 | REST API |
5. JSP — 서버 사이드 렌더링의 과거
MVC Flow에서 View 역할을 담당하는 JSP(JavaServer Pages)는 HTML 안에 Java 코드를 작성할 수 있는 템플릿 기술이다.
React의 JSX와 비교하면 방향이 정반대이다.
// JSX — JavaScript 안에 HTML
function UserPage({ user }) {
return (
<div>
<h1>{user.name}</h1>
</div>
);
}
<!-- JSP — HTML 안에 Java -->
<div>
<h1><%= user.getName() %></h1>
</div>
JSP의 문제는 HTML과 Java 코드가 뒤섞이면서 코드가 복잡해진다는 점이다.
6. Spring Boot의 내장 서버
Spring Boot 이전에는 서버를 따로 설치하고 배포하는 과정이 복잡했다.
// Spring Boot 이전 방식
1. Tomcat 별도 설치
2. WAR 파일로 프로젝트 빌드
3. Tomcat에 WAR 배포
4. Tomcat 재시작
Spring Boot는 Tomcat을 애플리케이션 안에 내장했다. 덕분에 배포가 크게 단순해졌다.
// Spring Boot 방식
1. JAR 파일로 빌드
2. java -jar myapp.jar
→ 끝. 서버가 자동으로 8080 포트에서 실행된다.
<!-- spring-boot-starter-web 의존성 하나에 Tomcat이 포함되어 있다 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
서버 설정은 application.properties 파일 하나로 처리 가능하다.
server.port=9090 # 포트 변경 (기본값: 8080)
server.servlet.context-path=/api # 컨텍스트 패스 설정
애플리케이션 시작 흐름
java -jar myapp.jar 실행
↓
SpringApplication.run() 호출
↓
내장 Tomcat 자동 시작 (8080 포트)
↓
DispatcherServlet 등록
↓
HTTP 요청 수신 대기
↓
요청 → DispatcherServlet → Controller → Service → Repository → DB
7. 실무 배포 구조 (Nginx + Spring Boot)
Spring Boot 내장 서버만으로도 운영이 가능하지만, 실무에서는 앞단에 Nginx를 함께 사용한다.
[클라이언트 브라우저]
↓
[Nginx — 80/443 포트]
├─ 정적 파일 요청 → React 빌드 파일 직접 서빙 (빠름)
└─ /api/** 요청 → Spring Boot(8080)로 전달
↓
[Spring Boot — 8080 포트]
↓
[DB]
Nginx를 쓰는 이유
- 성능: 정적 파일(이미지, JS, CSS)은 Nginx가 훨씬 빠르게 처리.
- 보안: 8080 포트를 외부에 직접 노출하지 않아도 된다.
- HTTPS: SSL 인증서 처리를 Nginx 한 곳에서 담당.
- 로드 밸런싱: Spring Boot 서버가 여러 대일 때 요청을 분산.
'Learning Log' 카테고리의 다른 글
| [멋사 클라우드 5기] Day 24 - Spring Data JPA (2) (0) | 2026.03.04 |
|---|---|
| [멋사 클라우드 5기] Day 23 - Spring Data JPA (1) (1) | 2026.03.03 |
| [멋사 클라우드 5기] Day 21 - 제어의 역전(IoC)과 의존성 주입(DI) (0) | 2026.02.26 |
| [멋사 클라우드 5기] Day 20 - JavaScript의 비동기 (0) | 2026.02.25 |
| [멋사 클라우드 5기] Day 19 - Object.assign과 structuredClone에 대하여 (0) | 2026.02.24 |
