
요청에 대한 동적인 컨텐츠를 만들고 응답하는 역할을 하는 기술이다. 전반적인 흐름은 아래와 같다. JVM위에서 WAS(예시 - Tomcat)가 실행이 되고, WAS 내부에는 Web Server가 있을 수도 있으며, Tomcat이 제공하는 Servlet Container도 존재한다.

Servlet은 Server 측에서 동작하며, Client의 요청에 대한 응답을 생성하고 전송하는 역할을 한다. 주로 Web Application Server (예: Apache Tomcat 등)에서 실행된다. Servlet은 HttpServlet 클래스를 상속받아 구현되며, Web Application의 동적인 컨텐츠를 생성하기 위해 사용되는데, 예를 들어, Servlet을 통해 HTML 페이지를 동적으로 생성하거나, 데이터베이스와의 상호작용을 통해 정보를 처리하고 응답을 생성할 수 있다.
Servlet은 주로 웹 요청을 처리하기 위해 doGet(), doPost()와 같은 메서드를 오버라이딩하여 구현한다. 이러한 메서드를 통해 Client의 요청을 분석하고, 필요한 작업을 수행한 후에는 HttpServletResponse 객체를 사용하여 응답을 생성한다. Servlet은 Web Application 개발에서 많이 사용되며, 동적인 웹 페이지 생성, 데이터 처리, 세션 관리 등 다양한 기능을 구현할 수 있다.
Servlet Container는 Java의 Servlet을 실행하고 관리하는 환경을 제공하는 런타임 환경이며, Web Server와 연동하여 Web Application을 실행하는 역할을 담당한다. 다만, Tomcat은 자체적인 Web Server 기능도 제공한다.
Servlet Container는 Client의 요청을 받아들이고, Servlet의 생명주기를 관리하며, 요청과 응답의 처리를 위해 필요한 기능을 제공한다.
일반적인 Serlvet Container의 생명 주기는 애플리케이션이 실행될 때부터, 종료될 때까지 이며, Serlvet의 생명 주기는 Servlet Container가 생성이 된 후에 초기화, 작업 수행, 종료를 거친다. 주의해야 할 점은 요청이 들어오고 응답을 한다고 해서 Servlet이 소멸되는 게 아니라는 것이다. 다음 요청이 들어오면 소멸되지 않고 계속해서 요청을 받는다.
Tomcat은 Apache Software Foundation에서 개발한 오픈 소스 기반의 Servlet Container이며, 가장 널리 사용되는 Servlet Container 중 하나이다. Servlet 및 JSP(JavaServer Pages) 실행 환경을 제공하며, Web Application Server(WAS)로서의 역할을 수행한다.
자체적으로 Web Server 기능을 포함하고 있으며, HTTP 요청을 받아들여 Servlet 및 JSP와 같은 동적인 웹 컴포넌트를 실행한다. 또한, Client와의 통신을 처리하고, 세션 관리, 보안 기능, 로깅 등 다양한 기능을 제공한다.
Controller는 Spring MVC에서 Web Application의 비즈니스 로직을 처리하고 Client의 요청을 처리하거나 사용자의 요청을 받아들이고 해당 요청을 처리하기 위해 비즈니스 로직을 호출하거나 다른 컴포넌트와의 상호작용을 하기도 한다. HTTP 요청에 따라 생성된 Request 객체에서 필요한 데이터를 가져오고, 비즈니스 로직을 실행한 후에 Response 객체에 데이터를 담아 다시 Client에게 전달한다.
일반적으로 @Controller 또는 @RestController 어노테이션을 사용하여 선언하는데, 해당 애너테이션이 적용된 클래스가 URL 매핑과 요청 처리를 담당하는 역할을 맡고 있다고 명시하는 것이라고 생각하자.
Dispatcher Servlet은 Spring Framework에서 Web Application의 요청 처리를 중앙 집중화하는 핵심적인 컴포넌트이다. 일반적으로 하나의 Web Application에 한 개의 Dispatcher Servlet이 존재한다. 해당 Servlet도 다른 Servlet과 마찬가지로 Servlet Container에서 실행되며, Client의 요청을 받아서 적절한 Handler(Controller)로 전달 후 응답을 생성하여 Client에게 반환하는 역할을 수행한다.
HTTP 메소드(GET, POST 등)에 따라 적절한 핸들러 메소드를 호출합니다. Dispatcher Servlet은 Handler로부터 반환된 Model을 적절한 View에 전달하여 최종적인 응답을 생성하고, Client에게 반환한다.
Dispatcher Servlet은 Web Application에서 여러 가지 기능들을 제공하기도 한다. 설정 파일에 Bean들을 등록하여 사용할 수 있으며, Handler Mapping, Interceptor, View Resolver 등을 설정할 수 있, 이러한 기능들을 통해 Dispatcher Servlet은 Web Application의 구조와 동작을 유연하게 제어할 수 있게 된다.
URL 패턴과 맵핑된 요청을 다룰 수 있는 메서드라고 생각하면 된다. 일반적으로 @RequestMapping를 사용하는데, Method를 생략하고 @GetMapping처럼 사용할 수 있다. http://localhost:8080/hello에 Get요청을 보내면 둘 다 같은 결과를 보여주는 걸 알 수 있다.
@RestController
public class CustomController {
@RequestMapping(method = RequestMethod.GET, path = "/hello")
public String sayHello(int age){
return "hello!";
}
}
--------------------------------------------------------------------------
@RestController
public class CustomController {
@GetMapping("/hello")
public String sayHello(int age){
return "hello!";
}
}
Spring에서는 기본형 타입 뿐만 아니라 객체, 날짜, 시간, 컬렉션, 배열 등 다양한 타입의 Parameter에 대한 기능을 지원한다.
HTTP 요청을 받아 Parameter로 넘어온 값들을 직접 사용할 수 있게 바인딩을 해줄 수 있다. (URL 주소 뒤에 붙은 Parameter = Query Parameter)
메서드 내의 설정된 Parameter 이름과 요청 Parameter의 이름이 동일한 경우에는 @RequestParam를 생략할 수 있다. Parameter로 넘어온 값들은 모두 문자열이고, 해당 문자열을 받으려는 Parameter의 타입으로 형변환을 하는 과정을 거친다고 보면 되는데, 이 과정을 데이터 바인딩이라고도 함.
@DateTimeFormat 어노테이션을 사용하여 형식을 지정할 수 있다. @GetMapping("/me")
public String getUser(@RequestParam("name") String name,
@RequestParam("age") int age) {
return "이름은 " + name + "나이는 " + age + "살";
}
객체를 바인딩하는 데 사용할 수 있다. 주의해야 할 점은, 바인딩을 하고 싶은 객체에 @Setter를 추가하거나, 필드를 받을 수 있는 생성자가 있어야 값을 주입 받을 수 있다. 일반적으로 @ModelAttribute 어노테이션은 생략할 수 있다. Method의 Parameter가 Java Bean(POJO) 타입인 경우에는 해당 객체가 자동으로 @ModelAttribute로 간주된다. 즉, 일반적인 Java 객체인 경우 생략할 수 있다.
@RestController
public class **CustomController** {
@GetMapping("/me")
public String getUser(@ModelAttribute("user") User user){
return "이름은 " + user.getName() + "나이는 " + user.getAge() + "살";
}
}
--------------------------------------------------------------------------
@Getter
public class **User** {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
일반적으로 위의 두 애너테이션을 사용하며, 대부분 생략 가능하기도 해서 쓰는 데 불편함은 없는 수준이다. 요청이 들어오면 데이터를 바인딩(값을 묶어주는)하는 방법은 대략 아래와 같다.
@RequestParam 어노테이션을 사용한 Parameter에 해당하는 요청 Parameter 값을 가져온다.LocalDate 객체로 변환하는 등의 타입 변환이 이루어진다.@ModelAttribute로 표시된 객체의 필드에 바인딩된다.. @ModelAttribute를 사용하지 않고 일반적인 @RequestParam을 사용한 경우, 해당 Parameter가 메서드의 로컬 변수로 바인딩된다.@NotNull 어노테이션을 사용하여 필드가 null인지 검사하거나, 커스텀한 검증 로직을 수행할 수 있다.Servlet 컨테이너에서 제공되는 객체들이다. 요청이 들어오면 HttpServletRequest에 바인딩하여 Servlet의 메서드 내에서 직접 사용되며, HttpServletResponse는 doGet(), doPost() 등 메서드 내에서 응답을 생성하고 반환한다.
Spring MVC가 제공하는 응답을 HttpServletResponse보다 더 쉽게 만들 수 있도록 추상화 한 객체이다. 응답 상태 코드, 헤더, 본문 등 HttpServletResponse보다 더 쉽고 편리한 기능들을 제공한다.
Model은 애플리케이션의 데이터, 비즈니스 로직 담당 및 애플리케이션의 상태와 동작을 관리한다.
데이터의 저장
Model은 요청으로 받은 데이터를 저장하고 관리하는데, 이는 사용자 정보, 상품 데이터, 설정 값, 계산 결과 등과 같은 다양한 데이터를 포함할 수 있고, Model은 데이터베이스, 외부 서비스, 파일 등의 소스로부터 데이터를 가져와 저장하며, 필요한 경우 데이터의 유효성 검사 및 가공을 할 수도 있다.
비즈니스 로직 처리
Model은 애플리케이션의 비즈니스 로직을 수행한다. 이는 데이터를 가공, 조작, 계산하여 원하는 결과를 생성하는 과정을 의미하는데, 예를 들어, 주문을 처리하거나 결제를 수행하는 등의 비즈니스 로직을 Model에서 구현할 수 있고, 이를 통해 데이터의 상태 변화나 조작에 따른 원하는 동작을 수행할 수 있다. (다만, 계층 레이어로 넘어가면 Service 계층에서 비즈니스 로직을 처리한다.)
데이터의 제공
애플리케이션의 다른 구성 요소에게 데이터를 제공한다. Controller나 View와 같은 다른 요소들은 Model을 통해 필요한 데이터를 가져와 사용할 수 있고, 이를 통해 Model은 데이터의 효율적인 관리와 공유를 담당하며, 다른 구성 요소들과의 상호 작용을 중개한다.
상태 관리
Model은 애플리케이션의 상태를 관리하며, 데이터의 변경이나 로직의 수행에 따라 Model의 상태가 변화할 수 있다. 이를 통해 애플리케이션의 다양한 부분들이 Model의 상태에 따라 적절한 동작을 수행할 수 있게 된다.
View는 Web Application에서 Client에게 보여지는 사용자 인터페이스(UI)를 담당하는 부분이다.
View는 Model에서 전달받은 데이터를 사용하여 사용자에게 적절한 형식으로 표현하고, Client에게 전달하는 역할을 수행한다. View는 주로 HTML, CSS, JavaScript 등의 웹 기술을 사용하여 구성되는데, View는 Client의 요청에 대한 응답으로 제공되며, 웹 브라우저에서 해당 View를 렌더링하여 사용자가 시각적으로 확인할 수 있는 형태로 표현된다.
Spring Framework에서는 다양한 View 템플릿 엔진을 지원한다. 대표적인 View 템플릿 엔진으로는 JSP(Java Server Pages), Thymeleaf, Freemarker, Velocity 등이 있다. 이러한 View 템플릿 엔진을 사용하여 View를 구성하고, Model에서 전달받은 데이터를 동적으로 포함시킬 수 있다.
View는 Controller(Controller)와 함께 동작하여 사용자의 요청에 따라 적절한 View를 선택하고, 데이터를 전달하여 Web Application의 응답을 생성한다. 이를 통해 사용자에게 필요한 정보를 제공하고, Web Application의 시각적인 부분을 구성할 수 있다.
따라서, Spring Framework에서 제공하는 JSP, Thymeleaf 등을 이용해서 데이터가 담긴 템플릿을 만들고, 웹 브라우저에 제공하면 웹 브라우저에서 렌더링을 해서 페이지를 보여줄 수 있게 된다.
Interceptor와 Filter는 Web Application에서 요청과 응답을 가로채어 추가적인 처리를 수행하는 기능을 제공하는 개념이다. Interceptor와 Filter는 서로 비슷한 역할을 수행하지만, Interceptor는 Spring Framework의 일부로서 Spring 컨텍스트와 밀접하게 연관되어 작동하며, Controller에 특화된 기능을 제공한다. 반면에 Filter는 jakarta Servlet 스펙의 일부로서 Web Application의 전체적인 요청과 응답에 영향을 미치는 범용적인 기능을 제공한다.
Filter와 Interceptor 둘 다 Parameter 변환, 인코딩, 캐싱, 보안 등과 같은 일반적인 작업을 처리하는 데 사용된다.
Interceptor는 Spring Framework에서 제공하는 기능으로, Controller에 진입하기 전(요청을 받고난 후 -> 즉, 필터가 있다면 적용된 후)과 나온 후에 특정 작업을 수행할 수 있다. Interceptor는 요청 전처리와 응답 후처리를 담당하며, 특정 URL 패턴이나 Controller에 대해 설정하여 동작하는데, Filter와 마찬가지로 인증을 하거나, 로깅을 할 때 Interceptor를 주로 사용한다. preHandle();, postHandle();, afterCompletion(); 등의 메서드를 지원하는데, 순서대로 호출되기 전, 호출된 후, 호출의 성공 실패 여부와 상관없이 실행되는 메서드라고 보면 된다.
Servlet Filter보다 더 편리하게 사용할 수 있고, 요청 Controller에 대한 정보도 알 수 있기 때문에 Filter로 구현된 것 중 Interceptor로도 구현할 수 있으면 Interceptor로 구현하는 게 좋을 듯하다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.addPathPatterns("/interceptor/include")
.excludePathPatterns("/interceptor/exclude");
}
}
class LogInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
request.setAttribute("start-time", System.currentTimeMillis());
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
Long start = (Long) request.getAttribute("start-time");
Long total = System.currentTimeMillis() - start;
System.out.println(total + "ms");
}
}
--------------------------------------------------------------------------
@RestController
public class CustomController {
@GetMapping("/interceptor/include")
public ResponseEntity include(HttpServletRequest req) {
Long start = (Long) req.getAttribute("start-time");
Long time = System.currentTimeMillis() - start;
return ResponseEntity.ok(time + "ms");
}
@GetMapping("/interceptor/exclude")
public ResponseEntity exclude(HttpServletRequest req) {
Long start = (Long) req.getAttribute("start-time");
Long time = System.currentTimeMillis() - start;
return ResponseEntity.ok(time + "ms");
}
}
java.lang.NullPointerException: Cannot invoke "java.lang.Long.longValue()" because "start" is null
at com.scvefg.mvc.CustomController.exclude(CustomController.java:23) ~[classes/:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
‘/interceptor/exclude’ 엔드포인트는 exclude pattern으로 설정했기 때문에, 값이 Interceptor에서 제외되고, Null이 뜨는 걸 볼 수 있다.
Filter는 Servlet(Servlet) 스펙에서 제공하는 기능으로, 요청과 응답의 처리 중간에 위치하여 특정 작업을 수행하는데, Filter는 일반적으로 Web Application의 전체적인 요청과 응답에 영향을 미친다.
@Component
public class CustomFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
req.setAttribute("start-time", System.currentTimeMillis());
chain.doFilter(req,response);
}
}
--------------------------------------------------------------------------
@RestController
public class CustomController {
@GetMapping("/filter")
public ResponseEntity include(HttpServletRequest req) {
Long start = (Long) req.getAttribute("start-time");
Long time = System.currentTimeMillis() - start;
return ResponseEntity.ok(time + "ms");
}
}

filter 실행부터 응답까지 걸리는 시간이 잘 출력되는 걸 볼 수 있다. 주의할 점은 @Component를 사용하거나 해서 Bean으로 등록해야 되고, 좀 더 다양한 기능을 추후에 다룰 생각이라면 ServletRequest를 HttpServletRequest로 형변환을 해주고 setAttribute를 해주는 게 좋다. 물론 ServletRequest를 사용해도 상관은 없다.
```java
@Component
public class CustomFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
request.setAttribute("start-time", System.currentTimeMillis());
chain.doFilter(request,response);
}
}
--------------------------------------------------------------------------
@RestController
public class CustomController {
@GetMapping("/filter")
public ResponseEntity include(ServletRequest req) {
Long start = (Long) req.getAttribute("start-time");
Long time = System.currentTimeMillis() - start;
return ResponseEntity.ok(time + "ms");
}
}
Spring에서 forward와 redirect는 웹 브라우저에서 한 웹 페이지에서 다른 웹 페이지로 이동하는 방법이다.
forward는 웹 브라우저의 요청을 그대로 다른 웹 페이지로 전달하는 방법이다. forward를 사용하면 현재 웹 페이지의 상태가 유지되는데, 예를 들어, 사용자가 웹 페이지에서 폼을 제출하면, forward를 사용하면 폼의 데이터가 그대로 다른 웹 페이지로 전달되는 방식이다.
redirect는 웹 브라우저에 새로운 요청을 보내는 방법이다. redirect를 사용하면 현재 웹 페이지의 상태가 유지되지 않는데, 예를 들어, 사용자가 웹 페이지에서 링크를 클릭하면, redirect를 사용하면 웹 브라우저는 링크가 가리키는 웹 페이지로 이동하는 방식이다.
Spring에서 forward와 redirect는 다음과 같은 방법으로 사용할 수 있다. @RestController에서는 사용할 수 없다. 목적이 다른 Controller이고, RestController에서 return값으로 String 타입을 반환하게 되면, 문자열이 그대로 가게 됨
// forward
return "forward:/hello";
// redirect
return "redirect:/hello";
@RequestPart는 Spring Framework에서 파일 업로드를 처리할 때 사용되는 애노테이션 중 하나이며, 이 애노테이션은 Controller 메서드의 매개변수에 적용되어, Multipart 요청에서 업로드된 파일을 바인딩하는데, Parameter의 타입은 MultipartFile이어야 한다.
@PostMapping("/upload")
public String upload(@RequestPart("file") MyFile file) {
// 파일 이름을 가져온다.
String fileName = file.getName();
// 파일 콘텐츠를 저장한다.
file.setContent(file.getInputStream());
// 성공 메시지를 반환한다.
return "success";
}
--------------------------------------------------------------------------
@PostMapping("/upload")
public String upload(@MultipartFile("file") MyFile file) {
// 파일 이름을 가져온다.
String fileName = file.getName();
// 파일 콘텐츠를 저장한다.
file.setContent(file.getInputStream());
// 성공 메시지를 반환한다.
return "success";
}
@RestController
public class CustomController {
@PostMapping("/file")
public String sayHello(@RequestPart List<MultipartFile> files){
StringBuilder sb = new StringBuilder();
for (MultipartFile m: files) {
sb.append(m.getOriginalFilename()).append("\n");
}
return sb.toString();
}
}
@RestController
public class CustomController {
@PostMapping("/file")
public String sayHello(@RequestPart List<MultipartFile> files,
@RequestParam int age
) {
StringBuilder sb = new StringBuilder();
for (MultipartFile m : files) {
sb.append(m.getOriginalFilename()).append("\n");
}
return sb.toString() + "그 와중에 나이는 " + age + "살임";
}
}
파일 타입이 아닌 데이터를 같이 전송할 때는 @RequestParam이나 @ModelAttribute를 사용하면 되는데, 중요한 것은 애너테이션을 생략하면 데이터가 안 담기는 경우가 생길 수 있기 때문에 붙여서 데이터를 보내는 것을 권장한다.
쿠키를 적용한 예시는 아래와 같다. 다만, 쿠키의 단점이 보일 수도 있는 코드인데, 쿠키의 값으로 1L(memberID)을 넣고, 누군가에 의해 탈취가 된다면 보안상 문제가 될 수도 있다. 탈취를 한 사람이 만약 다른 Member의 ID를 알고 있다면, 해당 ID로 서버에 접근을 하여 위험한 상황이 발생하게 되는데 쿠키는 계속적인 사용이 가능하기 때문에(반환되는 값이 같다는 전제하에) 큰 문제로 이어질 수 있는 것이다.
이러한 문제점 때문에, 보안이 우선시 되는 서비스의 경우 만료 시점이나 내부적으로 알고리즘이 적용된 세션 또는 JWT 토큰을 사용하는 경우가 많다.
@RestController
public class CustomController {
@GetMapping("/set-cookie")
public ResponseEntity setCookie(HttpServletResponse resp){
//1L = DB의 Member 고유 식별번호
Cookie cookie = new Cookie("ServerCookie", "1L");
resp.addCookie(cookie);
return ResponseEntity.ok("헤더에 쿠키 확인하세요!");
}
}
해당 엔드포인트로 요청을 보낸 후 위와 같은 응답 메시지의 헤더를 받아볼 수 있는데, 여기서 엔드포인트는 라우팅이 된 후의 [host:port/path]라고 보면 된다.
쿠키를 기반으로 전송하지만, 저장되는 곳이 Server라는 점에서 기존의 쿠키 방식과 다르다. 그래서 만료가 되었는지 판단하는 것도 Server에서 가능하다.
일반적으로 Server 개발자만 세션에 담긴 값을 확인할 수 있고, 설정할 수 있다. Server에서는 세션ID만 Client에 전달해주고, Client에서 요청이 올 때마다 Server에서는 HttpSession 객체를 이용해서 원하는 처리를 한다.
간단하게 username만 Query Parameter에서 추출해서 세션의 Key-Value로 username=${username}처럼 설정하였고 로그인 응답으로는 ok라는 메시지와 내부적으로 session에는 username이 담겨서 오게 된다.
@RestController
public class CustomController {
@GetMapping("/login")
public ResponseEntity session(HttpServletRequest req) {
String username = req.getParameter("username");
HttpSession session = req.getSession();
// setMaxInactiveInterval = 초(Second) 단위
session.setMaxInactiveInterval(1800);
session.setAttribute("username", username);
return ResponseEntity.ok("ok");
}
@GetMapping("/home")
public ResponseEntity home(HttpServletRequest req){
HttpSession session = req.getSession();
String username = (String) session.getAttribute("username");
return ResponseEntity.ok("세션에 등록된 ID: " + username);
}
}
잘못된 정보는 지적해주시면 감사하겠습니다.