스프링 MVC 1편 - 섹션1~섹션4 후기

soso·2023년 4월 14일
0

김영한의 스프링 완전 정복 로드맵
스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술
섹션1~섹션4 정리입니다.

섹션1. 웹 애플리케이션 이해

웹 서버, 웹 애플리케이션 서버

웹 서버(Web Server)

  • HTTP기반으로 동작
  • 정적 리소스 제공, 기타 부가 기능
  • 정적(파일) HTML, CSS, JS, 이미지, 영상

웹 애플리케이션 서버(WAS - Web Application Server)

  • HTTP기반으로 동작하며, 웹 서버 기능(정적 리소스 제공)한다.
  • 프로그램 코드를 실행해서 애플리케이션 로직 수행
    • 동적HTML, HTTP API(JSON)
    • 서블릿, JSP, 스프링, MVC

웹 서버, 웹 애플리케이션 서버(WAS)

  • 웹 서버는 정적 리소스(파일), WAS는 애플리케이션 로직
  • 사실은 둘의 용어도 경계가 모호하다
    • 웹 서버도 프로그램을 실행하는 기능을 포함하기도 한다
    • 웹 애플리케이션 서버도 웹 서버의 기능을 제공한다
  • 자바는 서블릿 컨테이너 기능을 제공하면 WAS
    • 서블릿이 없이 실행하는 서버 프레임워크도 있음
  • WAS는 애플리케이션 코드를 실행하는데 더 특화

웹 시스템 구성 - WAS, DB

  • 웹 애플리케이션 서버(WAS),DB 만으로 구성 가능
  • 웹 애플리케이션 서버는 정적 리소스, 애플리케리션 로직 모두 제공 가능
  • WAS가 많은 역활을 담당, 서버 과부화 우려
  • 가장 비싼 어플리케이션 로직이 정적 리소스 때문에 수행이 어려울 수 있음
  • WAS 장애시 오류 화면 노출 불가능

웹 시스템 구성 - WEB, WAS, DB

  • 정적 리소스는 웹 서버가 처리
  • 웹 서버는 애플리케이션 로직같은 동적인 처리가 필요하면 WAS에 요청을 위임
  • WAS는 중요한 애플리케이션 로직 처리 전담
  • 효율적인 리소스 관리
    • 정적 리소스가 많이 사용되면 Web 서버 증설
    • 애플리케이션 리소스가 많이 사용되면 WAS 증설
  • 정적 리소스만 제공하는 웹 서버는 잘 죽지 않음
  • 애플리케이션 로직이 동작하는 WAS 서버는 잘 죽음
  • WAS, DB 장애시 WEB 서버가 오류 화면 제공 가능

서블릿

서블릿 특징

  • urlPatterns(/hello)의 URL이 호출되면 서블릿 코드가 실행
  • HTTP 요청 정보를 편리하게 사용할 수 있는HttpServletRequest
  • HTTP 응답 정보를 편리하게 제공할 수 있는 HttpServletResponse
  • 개발자는 HTTP 스펙을 매우 편리하게 사용

서블릿 HTTP 요청, 응답 흐름

  • WAS는 Request, Response 객체를 새로 만들어서 서블릿 객체 호출
  • 개발자는 Request 객체에서 HTTP 요청 정보를 편리하게 꺼내서 사용
  • 개발자는 Response 객체에 HTTP 응답 정보를 편리하게 입력
  • WAS는 Response 객체에 담겨있는 내용으로 HTTP 응답 정보를 생성

서블릿 서블릿 컨테이너

  • 동시 요청을 위한 멀티 쓰레드 처리 지원
    톰캣처럼 서블릿을 지원하는 WAS를 서블릿 컨테이너라고 함
  • 서블릿 컨테이너는 서블릿 객체를 생성, 초기화, 호출, 종료하는 생명주기 관리 서블릿 객체는 싱글톤으로 관 리
    • 고객의 요청이 올 때 마다 계속 객체를 생성하는 것은 비효율
    • 최초 로딩 시점에 서블릿 객체를 미리 만들어두고 재활용
    • 모든 고객 요청은 동일한 서블릿 객체 인스턴스에 접근
    • 공유 변수 사용 주의
  • JSP도 서블릿으로 변환 되어서 사용
  • 서블릿 컨테이너 종료시 함께 종료

동시 요청 - 멀티 쓰레드

쓰레드

  • 애플리케이션 코드를 하나하나 순차적으로 실행하는 것은 쓰레드
  • 자바 메인 메서드를 처음 실행하면 main이라는 이름의 쓰레드가 실행
  • 쓰레드가 없다면 자바 애플리케이션 실행이 불가능
  • 쓰레드는 한번에 하나의 코드 라인만 수행
  • 동시 처리가 필요하면 쓰레드를 추가로 생성

요청 마다 쓰레드 생성 장단점

장점

  • 동시 요청을 처리할 수 있다.
  • 리소스(CPU, 메모리)가 허용할 때 까지 처리가능
  • 하나의 쓰레드가 지연 되어도, 나머지 쓰레드는 정상 동작한다.

단점

  • 쓰레드는 생성 비용은 매우 비싸다.
  • 고객의 요청이 올 때 마다 쓰레드를 생성하면, 응답 속도가 늦어진다.
  • 쓰레드는 컨텍스트 스위칭 비용이 발생한다.
  • 쓰레드 생성에 제한이 없다.
  • 고객 요청이 너무 많이 오면, CPU, 메모리 임계점을 넘어서 서버가 죽을 수 있다.

쓰레드 풀

요청 마다 쓰레드 생성의 단점 보완

  • 필요한 쓰레드를 쓰레드 풀에 보관하고 관리한다.
  • 쓰레드 풀에 생성 가능한 쓰레드의 최대치를 관리한다. 톰캣은 최대 200개 기본 설정 (변경 가능)

•사용

  • 쓰레드가 필요하면, 이미 생성되어 있는 쓰레드를 쓰레드 풀에서 꺼내서 사용한다.
  • 사용을 종료하면 쓰레드 풀에 해당 쓰레드를 반납한다.
  • 최대 쓰레드가 모두 사용중이어서 쓰레드 풀에 쓰레드가 없으면, 기다리는 요청은 거절하거나 특정 숫자만큼만 대기하도록 설정할 수 있다.

• 장점

  • 쓰레드가 미리 생성되어 있으므로, 쓰레드를 생성하고 종료하는 비용(CPU)이 절약되고, 응답 시간이 빠르다.
  • 생성 가능한 쓰레드의 최대치가 있으므로 너무 많은 요청이 들어와도 기존 요청은 안전하게 처리할 수 있다

HTML, HTTP API, CSR, SSR

정적 리소스

고정된 HTML 파일, CSS, JS, 이미지, 영상 등을 제공,주로 웹 브라우저

HTML 페이지

동적으로 필요한 HTML 파일을 생성해서 전달

HTTP API

HTML이 아니라 데이터를 전달, 주로 JSON 형식 사용

SSR

HTML 최종 결과를 서버에서 만들어서 웹 브라우저에 전달,주로 정적인 화면에 사용
관련기술: JSP, 타임리프

CSR

HTML 결과를 자바스크립트를 사용해 웹 브라우저에서 동적으로 생성해서 적용
주로 동적인 화면에 사용, 웹 환경을 마치 앱 처럼 필요한 부분부분 변경할 수 있음
관련기술: React, Vue.js

자바 백엔드 웹 기술 역사

# 과거기술

  • 서블릿 - HTML 생성이 어려움
  • JSP - HTML 생성은 편리, 비즈니스 로직까지 많은 역할 담당
  • 서블릿, JSP 조합 MVC 패턴 - 모델, 뷰 컨트롤러로 역할을 나누어 개발
  • MVC 프레임워크 - MVC 패턴 자동화

# 현재사용 기술

  • 애노테이션 기반의 스프링 MVC
  • 스프링 부트 - 서버를 내장, 빌드 배포 단순화

# 최신 기술
스프링 웹 플럭스(WebFlux)

  • 비동기 넌 블러킹 처리
  • 최소 쓰레드로 최대 성능 - 쓰레드 컨텍스트 스위칭 비용 효율화
  • 함수형 스타일로 개발 - 동시처리 코드 효율화
  • 서블릿 기술 사용X

하지만 기술적 난이도 매우 높고 RDB 지원 부족하가 때문에 실무에서 아직 많이 사용하지는 않음

섹션2. 서블릿

프로젝트 생성

Packaging는 War를 선택한다. JSP를 실행하기 위해서 필요

IntelliJ 세팅

Build Tools : Gradle -> IntelliJ IDEA
Lombok plugin 설치
Annotation Processors > Enable annotation processing

📌주의
IntelliJ 무료 버전의 경우 해당 설정을 IntelliJ IDEA가 아니라 Gradle로 설정해야 한다.
Jar 파일의 경우는 문제가 없는데, War의 경우 톰캣이 정상 시작되지 않는 문제가 발생한다.

무료 버전을 사용하는 나는 처음에 시작되지 않아서 애먹었다🥲

Hello 서블릿

@WebServlet 서블릿 애노테이션 (중복 불가)
name: 서블릿 이름
urlPatterns: URL 매핑

HttpServletRequest - 개요

개발자 대신에 HTTP 요청 메시지를 파싱한다

HttpServletRequest - 제공하는 기본 기능

  • start-line 정보
  • 헤더 정보
  • Header 편리한 조회
    • Host, Accept-Language, cookie, Content ...
  • 기타 정보
    • Remote, Local

HTTP 요청 데이터 - 개요

GET - 쿼리 파라미터

  • /url?username=hello
  • 메시지 바디 없이, URL의 쿼리 파라미터에 데이터를 포함해서 전달
    • 예) 검색, 필터, 페이징등에서 많이 사용하는 방식

POST - HTML Form

  • content-type: application/x-www-form-urlencoded
  • 메시지 바디에 쿼리 파리미터 형식으로 전달 username=hello&age=20
    • 예) 회원 가입, 상품 주문, HTML Form 사용

HTTP message body

  • HTTP API에서 주로 사용 (JSON, XML, TEXT)
  • 데이터 형식은 주로 JSON 사용(POST, PUT, PATCH)

HTTP 응답 데이터 - 단순 텍스트, HTML

  • 단순 텍스트 응답, HTML 응답, HTTP API - MessageBody JSON 응답
  • Content-type : text/html

HTTP 응답 데이터 - API JSON

  • HTTP 응답으로 JSON을 반환할 때는 content-type: application/json 로 지정
  • Jackson 라이브러리가 제공하는 objectMapper.writeValueAsString() 를 사용하면 객체를 JSON로 문자로 변경 가능

섹션3. 서블릿, JSP, MVC 패턴

MVC 패턴 - 개요

# 너무 많은 역할

  • 하나의 서블릿이나 JSP만으로 비즈니스 로직과 뷰 렌더링까지 모두 처리하면 너무 많은 역할을하게되고, 결과적으로 유지보수가 어려워진다.

# 변경의 라이프 사이클

  • UI를 수정하는 일과 비즈니스 로직을 수정하는 일은 각각 다르게 발생할 가능성이 매우 높고 대부분 서로에게 영향을 주지 않는다

# 기능 특화

  • JSP 같은 뷰 템플릿은 화면을 렌더링 하는데 최적화 되어 있기 때문에 이 부분의 업무만 담당하는 것이 가장 효과적이다

Model-View-Controller

  • MVC 패턴은 지금까지 학습한 것 처럼 하나의 서블릿이나, JSP로 처리하던 것을 컨트롤러(Controller)와 뷰(View)라는 영역으로 서로 역할을 나눈 것을 말한다.
  • 웹 애플리케이션은 보통 이 MVC 패턴을 사용

# 컨트롤러
HTTP 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행한다.
그리고 뷰에 전달할 결과 데이터를 조회해서 모델에 담는다.

# 모델
뷰에 출력할 데이터를 담아둔다.
뷰가 필요한 데이터를 모두 모델에 담아서 전달해주는 덕분에 뷰는 비즈니스 로직이나 데이터 접근을 몰라도 되고, 화면을 렌더링 하는 일에 집중할 수 있다.

# 뷰
모델에 담겨있는 데이터를 사용해서 화면을 그리는 일에 집중한다.
여기서는 HTML을 생성하는 부분을 말한다

MVC 패턴1

MVC 패턴2

MVC 패턴 - 적용

서블릿을 컨트롤러로 사용하고, JSP를 뷰로 사용해서 MVC 패턴을 적용

  • Model은 HttpServletRequest 객체를 사용
  • request 내부 데이터 저장소(equest.setAttribute() , request.getAttribute())를 사용하면 데이터를 보관, 조회할 수 있다

MVC 패턴 - 한계

  • MVC 패턴을 적용한 덕분에 컨트롤러의 역할과 뷰를 렌더링 하는 역할을 명확하게 구분할 수 있다
  • 하지만 컨트롤러는중복이 많고, 필요하지 않는 코드들도 많이 보인다

MVC 컨트롤러의 단점

# 포워드 중복

  • View로 이동하는 코드가 항상 중복 호출(메서드로 공통화해도 되지만, 해당 메서드도 항상 직접 호출필요)

# ViewPath에 중복

  • prefix: /WEB-INF/views/
  • suffix: .jsp
    만약 jsp가 아닌 thymeleaf 같은 다른 뷰로 변경한다면 전체 코드를 다 변경해야 한다

# 사용하지 않는 코드

  • 특히 response는 현재 코드에서 사용되지 않음

# 공통 처리가 어렵다

  • 공통 기능을 메서드로 뽑아도 결과적으로 해당 메서드를 항상 호출해야 함
  • 컨트롤러 호출 전에 먼저 공통 기능을 처리 필요
  • 프론트 컨트롤러(Front Controller) 패턴도입 (수문장 역할)

섹션4. MVC 프레임워크 만들기

프론트 컨트롤러 패턴 소개

프론트 컨트롤러 도입 전/후

# 프론트 컨트롤러 도입 전

클라이언트들은 요청이 들어왔을 때 Controller에 대해 각각 호출
공통 로직은 별도로 처리되어 있지 않고 각 Controller에 포함되어 있다

# 프론트 컨트롤러 도입 후

클라이언트에서 요청이 들어왔을 때 먼저 Front Controller은 각 요청에 맞는 컨트롤러를 찾아서 호출
공통 코드에 대해서는 Front Controller에서 처리하고, 서로 다른 코드들만 각 Controller에서 처리할 수 있도록 한다

FrontController 패턴 특징

  • 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받음
  • 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출
  • 공통 처리 가능
  • 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 됨

프론트 컨트롤러 도입 - v1

  • 클라이언트가 HTTP요청을 보내면 FrontController는 URL 매핑정보에서 컨트롤러 조회, 컨트롤러를 호출한다
    컨트롤러에서 JSP forward하고 HTML 응답을 해준다

  • 서블릿과 비슷한 모양의 컨트롤러 인터페이스를 도입, 각 컨트롤러들은 이 인터페이스를 구현하면 된다. 프론트 컨트롤러는 이 인터페이스를 호출해서 구현과 관계없이 로직의 일관성을 가져갈 수 있다

  • 내부 로직은 기존 서블릿과 거의 같다.

# urlPatterns

@WebServlet(name = "frontControllerServletV1", urlPatterns = "/frontcontroller/v1/*") // vl에 속한 모든 하위 호출 가능
public class FrontControllerServletV1 extends HttpServlet {

# controllerMap

key: 매핑 URL("/front-controller/v1/members/new-form")
value: 호출될 컨트롤러 new MemberListControllerV1())

private Map<String, ControllerV1> controllerMap = new HashMap<>();
 public FrontControllerServletV1() {
   controllerMap.put("/front-controller/v1/members/new-form", new MemberFormControllerV1());
   controllerMap.put("/front-controller/v1/members/save", new MemberSaveControllerV1());
   controllerMap.put("/front-controller/v1/members", new MemberListControllerV1());
 }

View 분리 - v2

모든 컨트롤러에서 뷰로 이동하는 부분에 중복이 있고, 깔끔하지 않기 때문에 별도로 뷰를 처리하는 객체 생성

Model 추가 - v3

# 서블릿 종속성 제거

  • 컨트롤러 입장에서 HttpServletRequest, HttpServletResponse이 꼭 필요하지 않음
  • 요청 파라미터 정보는 자바의 Map을 사용 (컨트롤러가 서블릿 기술을
    몰라도 동작)
  • request 객체를 Model로 사용하는 대신에 별도의 Model 객체를 만들어서 반환하면 된다

# 뷰 이름 중복 제거

  • 컨트롤러에서 지정하는 뷰 이름에 중복이 있음(/WEB-INF/views/...)
  • 컨트롤러는 뷰의 논리 이름(ex. ew-form)을 반환하고, 실제 물리 위치(/WEB-INF/views/new-form.jsp)의 이름은 프론트 컨트롤러에서 처리하도록 단순화

#ModelView

기존 방식

컨트롤러에서 서블릿에 종속적인 HttpServletRequest를 사용하고, Model은 request.setAttribute() 를 통해 데이터를 저장하고 뷰에 전달

서블릿의 종속성을 제거

Model을 직접 만들고, View 이름까지 전달하는 객체 생성

public class ModelView {
   private String viewName;
   private Map<String, Object> model = new HashMap<>();

ControllerV3

  • 해당 컨트롤러는 서블릿 기술을 전혀 사용하지 않음
  • 구현이 매우 단순해지고, 테스트 하기 쉽다
  • HttpServletRequest가 제공하는 파라미터는 프론트 컨트롤러가 paramMap에 담아서 호출
  • 응답 결과로 뷰 이름과 뷰에 전달할 Model 데이터를 포함하는 ModelView 객체를 반환
public ModelView process(Map<String, String> paramMap) 

# MemberFormControllerV3 - 회원 등록 폼

ModelView를 생성할 때 논리 이름 지정하고 반환 (물리 이름은 프론트 컨트롤러에서 처리)

return new ModelView("new-form"); // 논리적인 이름을 지정

# MemberSaveControllerV3 - 회원 저장

  • 파라미터 정보는 map에 담겨있고 map에서 필요한 요청 파라미터를 조회
String username = paramMap.get("username");
int age = Integer.parseInt(paramMap.get("age"));
  • member 객체에 모델을 담고 반환
ModelView mv = new ModelView("save-result");
 mv.getModel().put("member", member);
 return mv;

# FrontControllerServletV3

뷰 리졸버

MyView view = viewResolver(viewName)
컨트롤러가 반환한 논리 뷰 이름을 실제 물리 뷰 경로로 변경, 실제 물리 경로가 있는 MyView 객체를 반환

  • 논리 뷰 이름: members
  • 물리 뷰 경로: /WEB-INF/views/members.jsp

view.render(mv.getModel(), request, response)

  • 뷰 객체 통해서 HTML 화면 렌더링
  • 뷰 객체의 render() 는 모델 정보도 함께 받는다.
  • JSP는 request.getAttribute() 로 데이터를 조회하기 때문에, 모델의 데이터를 꺼내서 request.setAttribute() 로 담아둔다
  • JSP로 포워드 해서 JSP를 렌더링

단순하고 실용적인 컨트롤러 - v4

  • 기본적인 구조는 V3와 같지만, 컨트롤러가 ModelView를 반환하지 않고 ViewName만 반환

ControllerV4

ModelView가 없음 model 객체는 파라미터로 전달, 뷰의 이름만 반환

public interface ControllerV4 {
 /**
 * @param paramMap
 * @param model
 * @return viewName
 */
 String process(Map<String, String> paramMap, Map<String, Object> model);
}

# MemberFormControllerV4/ MemberSaveControllerV4/ MemberListControllerV4

  • new-form 이라는 뷰의 논리 이름만 반환
  • 모델이 파라미터로 전달되기 때문에, 직접 생성하지 않아도 됨

# FrontControllerServletV4

  • V3 버전과 거의 동일, 추가 된 부분 모델 객체 전달
  • 모델 객체를 프론트 컨트롤러에서 생성해서 넘겨줌
  • 컨트롤러에서 모델 객체에 값을 담으면 여기에 그대로 담겨있게 된다
Map<String, Object> model = new HashMap<>(); //추가
  • 컨트롤로가 직접 뷰의 논리 이름을 반환하므로 이 값을 사용해서 실제 물리 뷰를 찾을 수 있다.
String viewName = controller.process(paramMap, model);
MyView view = viewResolver(viewName);

유연한 컨트롤러1 - v5

어댑터 패턴

  • 기존 컨트롤러는 한가지 방식의 컨트롤러 인터페이스만 사용할 수 있다(전히 다른 인터페이스, 호환이 불가능)
  • 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있도록 변경

# 핸들러 어댑터

  • 중간에 어댑터 역할을 하는 어댑터
  • 다양한 종류의 컨트롤러를 호출 가능

# 핸들러

  • 컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경

# MyHandlerAdapter

  • boolean supports(Object handler)
    • handler는 컨트롤러를 말한다
    • 어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 메서드
  • ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
    • 어댑터는 실제 컨트롤러를 호출, 그 결과로 ModelView를 반환해야 한다
    • 실제 컨트롤러가 ModelView를 반환하지 못하면, 어댑터가 ModelView를 직접 생성해서라도 반환해야 한다
    • 이전에는 프론트 컨트롤러가 실제 컨트롤러를 호출했지만, 이제는 이 어댑터를 통해서 실제 컨트롤러가 호출된다

# ControllerV3HandlerAdapter

  • ControllerV3 을 처리할 수 있는 어댑터를 뜻한다
public boolean supports(Object handler) {
 return (handler instanceof ControllerV3);
}
  • handler를 컨트롤러 V3로 변환하고 V3 형식에 맞도록 호출
    (ModelView를 반환하므로 그대로 ModelView를 반환하면 된다)
ControllerV3 controller = (ControllerV3) handler;
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
return mv;

# FrontControllerServletV5

컨트롤러(Controller) → 핸들러(Handler)

  • 이전에는 컨트롤러를 직접 매핑해서 사용
  • 어댑터가 지원하기만 하면, 어떤 것이라도 URL에 매핑해서 사용가능

매핑 정보

private final Map<String, Object> handlerMappingMap = new HashMap<>();
  • 매핑 정보의 값을 인터페이스에서 아무 값이나 받을 수 있는 Object 로 변경

핸들러 매핑

Object handler = getHandler(request)
  • URL에 매핑된 핸들러(컨트롤러) 객체를 찾아서 반환
private Object getHandler(HttpServletRequest request) {
 String requestURI = request.getRequestURI();
 return handlerMappingMap.get(requestURI);

핸들러를 처리할 수 있는 어댑터 조회

MyHandlerAdapter adapter = getHandlerAdapter(handler)
  • handler를 처리할 수 있는 어댑터를 adapter.supports(handler) 를 통해서 찾는다
  • handler가 ControllerV3 인터페이스를 구현했다면, ControllerV3HandlerAdapter 객체 반환

어댑터 호출

ModelView mv = adapter.handle(request, response, handler);
  • 실제 어댑터가 호출
  • 어댑터는 handler(컨트롤러)를 호출하고 그 결과를 어댑터에 맞추어 반환

유연한 컨트롤러2 - v5

# FrontControllerServletV5 에 ControllerV4 기능도 추가

  • 핸들러 매핑(handlerMappingMap)에 ControllerV4를 사용하는 컨트롤러를 추가
  • 해당 컨트롤러를 처리할 수 있는 어댑터인 ControllerV4HandlerAdapter도 추가

# ControllerV4HandlerAdapter

public boolean supports(Object handler) {
 return (handler instanceof ControllerV4);
}
  • handler가 ControllerV4인 경우에만 처리하는 어댑터
  • handler를 ControllerV4로 케스팅 하고, paramMap, model을 만들어서 해당 컨트롤러를 호출, viewName을 반환

어댑터 변환

ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;
  • 어댑터가 호출하는 ControllerV4는 뷰의 이름을 반환
  • 어댑터는 뷰의 이름이 아니라 ModelView 를 만들어서 반환(String -> modelView)
    ControllerV4는 뷰의 이름을 반환했지만, 어댑터는 이것을 ModelView로 만들어서 형식을 맞추어 반환한다
profile
오늘의 기록

0개의 댓글