[Java] 서블릿부터 스프링 MVC까지

dondonee·2023년 12월 12일
0
post-thumbnail

서블릿부터 스프링 MVC까지

자바 웹 개발의 코어인 서블릿이 어떤 과정을 거쳐 스프링 MVC 프레임워크까지 진화했는지 그 과정을 정리해보았다.



서블릿


서블릿 이전

최초의 웹 페이지는 HTML로 작성된 문서였다. 웹이 발전함에 따라 동적인 처리가 필요해졌는데, 동적인 애플리케이션을 제공하기 위해서는 핵심 로직 뿐 아니라 서버를 연결하고 HTTP 메시지를 처리하는 등의 과정이 매번 필요했다.

서블릿(Servlet)은 이러한 반복되는 작업을 처리해주고 개발자가 의미있는 비즈니스 로직에 집중할 수 있도록 등장했다.


서블릿 컨테이너

WAS(Web Application Server)

웹 서버는 HTTP 기반으로 동작하며 정적 리소스를 제공하는 기능을 하는데 WAS도 웹 서버의 기능을 제공하기 때문에 둘의 구분이 명확하지 않다. 단 WAS는 애플리케이션 로직에 특화되었다고 이해하면 된다.

WAS와 DB만으로도 웹 애플리케이션을 구성할 수 있지만 로직이 복잡해지는 경우 웹 서버와 WAS를 분리하는 것이 좋다. 웹 서버는 정적 리소스를 담당하고 WAS는 애플리케이션 로직을 담당함으로써 효율적으로 처리할 수 있고 WAS에 장애가 생겨도 웹 서버를 통해 오류 페이지(정적 리소스)를 노출할 수 있다.


서블릿 컨테이너

톰캣처럼 서블릿을 지원하는 WAS를 서블릿 컨테이너라고 한다. 서블릿 컨테이너의 특징은 다음과 같다 :

  1. 서블릿 객체의 생명주기(생성, 초기화, 호출, 종료) 관리
  2. 서블릿 객체를 싱글톤으로 관리하여 리소스 절약
  3. 멀티 쓰레드 지원

고객의 요청마다 서블릿 객체를 생성하는 것은 비효율적이기 때문에 서블릿 컨테이너는 최초 로딩 시점에 하나의 서블릿을 만들어 두고 공유하여 사용한다. 이러한 싱글톤 패턴에서는 모든 고객 요청을 동일한 서블릿 인스턴스가 처리한다.

WAS는 멀티 쓰레드 처리도 지원한다. 애플리케이션이 동시에 여러 요청을 처리하기 위해서는 여러 개의 쓰레드가 생성되어야 하는데 WAS를 사용하면 개발자는 쓰레드 처리를 신경쓰지 않아도 된다.



MVC 패턴


JSP(Java Server Pages)

서블릿은 자바 코드를 사용하기 때문에 HTML을 생성하려면 system.out으로 코드를 한줄씩 출력해야 했다.

이에 자바 웹 애플리케이션의 뷰 로직을 처리하기 위해 JSP가 등장했다. JSP는 HTML 태그와 JSP 태그(<% %>)로 구성된다. JSP 태그는 HTML에 자바 로직을 삽입하기 위해 사용된다.

  • JSP 또한 서블릿으로 변환되어 동작한다.

JSP 덕분에 HTML 태그를 생성하는 것은 편리해졌지만 뷰 로직(HTML 태그)과 비즈니스 로직(JSP 태그로 삽입된 자바 코드)가 하나에 뒤섞여있어 가독성이 떨어지고 유지보수가 어렵다는 한계가 있었다.


MVC 패턴 - JSP + Servlet

MVC 패턴 1

서블릿이나 JSP를 단독 사용하는 경우 관리 형태가 전혀 다른 뷰 로직과 비즈니스 로직이 합쳐져있어 유지보수가 어려웠다.

이에 모델-뷰-컨트롤러라는 MVC 패턴이 등장했다. JSP는 뷰 로직을 담당하고, 서블릿은 비즈니스 로직을 담당하는 컨트롤러 역할을 하며, 뷰와 컨트롤러는 모델이라는 데이터를 담는 객체를 통해 데이터를 공유하는 방식이다.


MVC 패턴 2

그런데 첫 번째 MVC 패턴의 경우 컨트롤러가 너무 많은 로직을 담당하게 된다. 그래서 현재는 서비스 계층과 리포지토리 계층을 분리한 MVC 패턴 2를 주로 사용한다. 이제 컨트롤러는 비즈니스 로직을 호출하고 다른 계층에서 처리한 데이터를 뷰에 전달하는 기능만 담당하면 되므로 훨씬 가벼워졌다.


MVC 패턴의 문제점

MVC 패턴 덕분에 뷰와 비즈니스 로직을 분리할 수 있었다.

그러나 초기의 MVC 패턴은 컨트롤러 코드에 너무 많은 중복이 발생한다는 문제가 있었다. 프로그램이 복잡할 수록 공통 처리가 필요해지기 때문에 중복은 치명적인 단점이다.

@WebServlet(name = "mvcMemberSaveServlet", 
	urlPatterns = "/servlet-mvc/members/save")
public class MvcMemberSaveServlet extends HttpServlet {

  private MemberRepository memberRepository = MemberRepository.getInstance();

  @Override
  protected void service(HttpServletRequest request, 
	  HttpServletResponse response) throws ServletException, IOException {

	  String username = request.getParameter("username");
	  int age = Integer.parseInt(request.getParameter("age"));

	  Member member = new Member(username, age);
	  memberRepository.save(member);

	  request.setAttribute("member", member);  //Model에 데이터를 보관한다.
	  String viewPath = "/WEB-INF/views/save-result.jsp";
	  RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
	  dispatcher.forward(request, response);
  }
}

어떤 중복이 발생하는 지 예시를 통해 살펴보자.

위 코드는 회원가입 폼에서 정보를 받아 회원을 저장하는 서블릿 코드이다. 클라이언트로부터 받은 가입 정보를 꺼내고, 리포지토리 메서드를 호출해 저장하고, 모델에 데이터를 담고, 뷰(JSP)에 포워드한다.

애플리케이션에서 사용되는 컨트롤러와 리소스가 여러 개일 것임을 생각해보면, 컨트롤러마다 포워드 코드(RequestDispatcher 등)가 중복되고 JSP 경로인 viewPath에서도 "/WEB-INF/" 등의 공통 폴더 경로와 ".jsp"의 확장자 부분도 중복된다.

또한 서블릿은 파라미터로 HttpServletRequestHttpServletResponse을 받는데 response는 사용되지 않을 때가 많다. 이 두 객체는 테스트가 어렵다는 문제도 있다.



스프링 MVC 프레임워크


프론트 컨트롤러 패턴

기존의 MVC 패턴은 중복 코드가 많아 공통 처리가 어렵다는 문제가 있었다. 이 문제에 대한 해결법으로 프론트 컨트롤러 패턴이 등장했다. 프론트 컨트롤러가 모든 클라이언트 요청을 우선적으로 받도록 하고 요청에 맞는 컨트롤러를 찾아 호출하도록 했다.

프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않는다. 프론트 컨트롤러만이 포워드 코드를 호출한다. 일반 컨트롤러들은 이제 웹에 의존하지 않기 때문에 테스트도 쉬워졌다.

프론트 컨트롤러 패턴은 스프링 MVC의 핵심이다. 스프링은 DispatcherServlet이라는 프론트 컨트롤러를 사용하며 더 많은 기능을 포함한다.


Dispatcher Servlet

스프링 MVC 프레임워크는 디스패처 서블릿이라는 프론트 컨트롤러를 중심으로 설계되었다. 디스패처 서블릿은 HttpServlet를 상속받아 동작한다.

스프링 MVC의 동작

클라이언트로부터 들어오는 모든 HTTP 요청은 우선적으로 디스패처 서블릿이 받는다. 응답까지의 스프링 MVC 내부 동작을 개괄적으로 살펴보자 :

  1. 디스패처 서블릿은 목록에서 요청에 맞는 핸들러를 조회한다.
  2. 해당 핸들러를 실행할 수 있는 핸들러 어댑터 목록을 조회한다.
  3. 핸들러 어댑터 실행
  4. 핸들러 어댑터가 핸들러 실행
  5. ModelAndView 반환
  6. 뷰 리졸버 호출
  7. 뷰 반환
  8. 모델 정보를 가지고 뷰 렌더링
  9. HTML 응답

디스패처 서블릿은 요청을 처리하기 위해 HandlerMapping, HandlerAdapter, ViewResolver 등 특별한 빈에 처리를 위임한다. 특히 스프링 MVC는 핸들러 어댑터(HandlerAdapter) 덕분에 다양한 형태의 컨트롤러를 처리할 수 있다.

  • 핸들러는 들어온 HTTP 요청에 대해 특정 부분을 처리하는 객체(?)를 말한다. 핸들러에는 컨트롤러가 포함되고 그 외에도 로케일 정보나 URL 정보를 처리하는 핸들러도 존재한다. (핸들러의 개념을 속시원히 설명해주는 문서를 찾지 못했다 😞)
  • 핸들러 인터셉터는 핸들러가 요청을 처리하기 전에 요청을 가로채서 다른 처리를 하는 객체이다. 핸들러를 더 유연하게 사용할 수 있도록 한다.

애노테이션 기반 컨트롤러

스프링은 처음에 MVC 기능이 약해서 다른 프레임워크를 붙여 사용하곤 했는데 @RequestMapping으로 대표되는 애노테이션 기반 컨트롤러가 등장한 뒤 MVC 부분도 스프링의 완승으로 마무리가 되었다고 한다.

스프링 MVC 프레임워크는 요청 매핑, 요청 입력, 예외 처리 등의 방법을 애노테이션으로 명시한다. 또한 필요에 따라 컨트롤러의 전달 인자나 리턴 타입을 선택할 수 있다. 스프링 MVC가 얼마나 유연한지 살펴보자.

  • 가장 일반적인 컨트롤러 컴포넌트에는 @Controller 애노테이션을 달아주면 된다.
  • @RestController는 REST API를 사용하는 컨트롤러로 @Controller@ResponseBody를 결합한 것이다.

a. 요청 매핑

@RequestMapping을 통해 요청을 컨트롤러 메서드에 매핑할 수 있다. URL, HTTP 메서드, 요청 파라미터, 헤더 등에 대한 다양한 속성과 변형 타입이 있다.

  • @GetMapping
  • @PostMapping
  • @PutMapping
  • @DeleteMapping
  • @PatchMapping

컨트롤러 메서드는 대부분 특정 HTTP 메서드에 대해 매핑되어야 하는데, 클래스 레벨에서 @RequestMapping을 정하고 메서드 레벨에서 세부 URL 매핑을 할 수 있다.

@RestController
@RequestMapping("/persons")
class PersonController {

	@GetMapping("/{id}")
	public Person getPerson(@PathVariable Long id) {
		// ...
	}

	@PostMapping
	@ResponseStatus(HttpStatus.CREATED)
	public void add(@RequestBody Person person) {
		// ...
	}
}

b. 컨트롤러 전달인자(arguments)

스프링 MVC 컨트롤러는 다양한 전달 인자를 처리할 수 있다.

  • @PathVariable: URI 템플릿 변수에 접근한다. 컨트롤러 메서드가 /user/{id}와 같은 URL 매핑되었을 때 {id} 값을 원하는 타입의 변수로 가져올 때 사용할 수 있다.
  • @RequestParam: 요청 쿼리 파라미터의 값을 가져올 수 있다.
  • @ModelAttribute: 요청 파라미터의 이름으로 객체의 프로퍼티(setter)를 찾아 값을 바인딩한다. 주로 폼 데이터를 가져올 때 사용된다.
  • Errors, BindingResults: @ModelAttrubute를 검증할 때 사용되는 객체로, 오류 정보를 보관한다.

c. 리턴값

  • @ResponseBody: JSON 응답에 사용된다.
  • String: 문자열을 반환하면 뷰 이름으로 처리된다.
  • @ModelAttribute: 모델에 추가되는 속성이다.

소개한 것은 아주 일부이고 더 많은 선택지가 있다. 이렇게 스프링 MVC 프레임워크가 다양한 형태의 컨트롤러를 유연하게 처리할 수 있는 것은 핸들러 어댑터 덕분이다.




정리

  1. 서블릿은 웹 애플리케이션을 위한 부수적인 부분을 대신 처리해줌으로써 개발자가 핵심 로직에 집중할 수 있도록 도와주기 위해 등장했다. 자바 웹 개발의 핵심이라고 할 수 있다.

  2. 그러나 서블릿은 자바 코드이기 때문에 HTML 문서를 생성하기 번거롭다는 문제가 있었다. 이에 화면을 담당하는 JSP가 등장했다.

  3. 서블릿이나 JSP를 단독 사용하는 경우 비즈니스 로직과 뷰 로직이 뒤섞여있어 유지보수가 어려웠다. 때문에 서블릿은 비즈니스 로직을, JSP는 뷰 로직을 담당하게 하고 필요한 데이터는 모델이라는 별개의 객체를 통해 공유하는 MVC 패턴이 개발되었다.

  4. 초기의 MVC 패턴은 중복되는 코드가 매우 많았다. 때문에 모든 컨트롤러의 선두에서 공통 로직을 처리하는 프론트 컨트롤러를 통해 중복을 제거했다.

  5. 스프링 MVC 프레임워크 또한 프론트 컨트롤러 패턴의 디스패처 서블릿을 중심으로 한다. 스프링 MVC는 애노테이션 기반으로, 다양한 형태의 컨트롤러를 처리할 수 있으며 이러한 유연함의 핵심은 핸들러 어댑터이다.



🔗 References

0개의 댓글