no image
JSP와 서블릿을 이용한 MVC 프레임워크 만들기
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다. Model, View, Controller 컨트롤러: HTTP 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행한다. 그리고 뷰에 전달할 결과 데이터를 조회해서 모델에 담는다. 모델: 뷰에 출력할 데이터를 담아둔다. 뷰가 필요한 데이터를 모두 모델에 담아서 전달해주는 덕분에 뷰는 비즈니스 로직이나 데이터 접근을 몰라도 되고, 화면을 렌더링 하는 일에 집중할 수 있다. 뷰: 모델에 담겨있는 데이터를 사용해서 화면을 그리는 일에 집중한다. 여기서는 HTML을 생성하는 부분을 말한다. 참고 컨트롤러에 비즈니스 로직을 둘 수도 있지만, 이렇게 되면 컨트롤러가 너무 많은 역할을 담당한다. 그래서 일반적으로 비즈니..
2024.02.17
no image
[h2] mac에서 h2 데이터베이스 설치
H2 데이터베이스 H2 데이터베이스는 자바 기반의 경량화된 관계형 데이터베이스 관리 시스템(RDBMS)이다.메모리 내에서 실행될 수 있어서 개발이나 테스트 용도로 많이 사용되곤 한다. 그래서 애플리케이션 개발 시 데이터베이스 서버를 별도로 구성하지 않고도 손쉽게 데이터베이스 환경을 구축할 수 있어서 매우 편리하다. H2 데이터베이스의 주요 특징은 아래와 같다.H2는 자바 애플리케이션에 내장될 수 있어서 복잡한 설치 과정 없이 데이터베이스를 바로 사용할 수 있다.메모리에서 직접 실행되므로 빠른 데이터 액세스와 테스트가 가능하다. 개발 중에는 메모리 데이터베이스를 사용하고, 실제 운영 환경에서는 디스크 기반 저장소를 사용하는 식으로 활용할 수 있다.H2는 표준 SQL을 많이 지원한다. 복잡한 쿼리나 함수, ..
2024.02.14
no image
[Spring MVC] 웹 어플리케이션의 이해
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다. 웹 서버, 웹 애플리케이션 서버 웹은 HTTP 프로토콜 기반으로 통신하여 데이터를 주고받는다. HTML, TEXT IMAGE, 음성, 영상, 파일 JSON, XML (API) 등 웹 서버 (Web Server) HTTP 기반으로 동작 정적 리소스 제공, 기타 부가기능 정적 (파일) HTML, CSS, JS, 이미지, 영상 NGINX, APACHE 등이 웹 서버로 사용 웹 애플리케이션 서버 WAS (Wep Application Server) HTTP 기반으로 동작 웹 서버 기능 + 프로그램 코드 실행하여 애플리케이션 로직 수행 동적 HTML, HTTP API(JSON), 서블릿, JSP, 스프링 MVC 톰캣, J..
2024.02.13
no image
[HTTP] 캐시와 조건부 요청
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다. 캐시를 사용하지 않는다면, 서버의 데이터가 변경되지 않았어도 클라이언트는 서버의 데이터를 다운로드 받아야 한다. 때문에 브라우저 로딩 속도가 느려진다. 이는 성질 급한 사용자가 참지 못하고 해당 사이트를 이탈할 수도 있다. 이는 SEO에 안좋은 영향을 미치기 때문에 장기적으로 사이트에 안좋은 영향을 끼칠 수 있다. 또한 인터넷 네트워크는 느리고 비싸기 때문에 캐시를 필수적으로 사용해야 한다. 캐시 기본 동작 캐시 시간 설정 브라우저에서 GET /star.jpg 첫번째 요청을 보내면, 서버는 HTTP 헤더(0.1M) + HTTP 바디=star.jpg 이미지(1.0M)를 담아 응답을 보낸다. 중요한 점은 HTTP ..
2024.02.12
no image
[HTTP] 일반 헤더
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다. HTTP 헤더 개요 HTTP 전송에 필요한 메시지 바디의 내용, 메시지 바디의 크기, 압축, 인증, 요청 클라이언트, 서버 정보, 캐시 관리 등 모든 부가 정보를 헤더에 넣는다. 표준 헤더가 굉장히 많다. 필요시 임의의 헤더 추가가 가능하다. RFC2616 Header General 헤더: 요청/응답 메시지 전체에 적용되는 정보 (Connection: close 등) Request 헤더: 요청 정보 (User-Agent: Mozilla/5.0 등) Response 헤더: 응답 정보 (Server: Apache 등) Entity 헤더: 엔티티 바디 정보 (Content-Type: text/html, Content..
2024.02.11
no image
[HTTP] 상태코드
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다. HTTP 상태코드 상태코드 1xx (Informational) : 요청이 수신되어 처리중 -> 거의 사용이 되지 않음 2xx (Successful) : 요청 정상 처리 3xx (Redirection) : 요청을 완료하려면 추가 행동이 필요 4xx (Client Error) : 클라이언트 오류, 잘못된 문법등으로 서버가 요청을 수행할 수 없음 5xx (Server Error) : 서버 오류, 서버가 정상 요청을 처리하지 못함 HTTP 상태 코드는 클라이언트가 서버로 요청을 보내면 요청이 잘 처리가 되어있는지 문제가 있는지 요청의 처리 상태를 응답에서 알려주는 기능이다. 만약 모르는 상태코드가 나타나면 클라이언트가..
2024.02.10
no image
[HTTP] HTTP 메서드 활용
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다. 클라이언트에서 서버로 데이터 전송 데이터 전달 방식 쿼리 파라미터를 통한 데이터 전송 주로 GET 방식으로 많이 사용하고 검색어로 검색할 때, 게시판 리스트에 정렬 조건을 넣을 때 쿼리 파라미터를 이용해서 많이 사용한다. 메시지 바디를 통한 데이터 전송 클라이언트에서 서버로 전송할 때 HTTP 메시지 바디를 통해서 데이터를 전송한다. POST, PUT, PATCH 방식으로 주로 사용한다. 회원 가입, 상품 주문, 리소스 등록, 리소스 변경 등에 사용된다. 클라이언트에서 서버로 데이터 전송할 때 4가지 상황 정적 데이터 조회 : 이미지, 정적 텍스트 문서 동적 데이터 조회 : 주로 검색, 게시판 목록에서 정렬 필..
2024.02.09
no image
[HTTP] HTTP 메서드
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다. HTTP API 예제 요구사항 및 API URI 설계 회원 정보 관리 API 설계 1. 회원 목록 조회 : /read-member-list 2. 회원 조회 : /read-member-by-id 3. 회원 등록 : /create-member 4. 회원 수정 : /update-member 5. 회원 삭제 : /delete-member 요구사항 기반으로 API를 만들게 되는게 위와 같이 현업에서 잘못된 API URI 설계를 한다. API URI 설계 분리 리소스 : 회원 행위 : 조회, 등록, 수정, 삭제 API URI 설계를 할 때 리소스와 해당 리소스를 대상으로 하는 행위를 분리해야 한다. 회원이라는 리소스만 식..
2024.02.08

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


Model, View, Controller

  • 컨트롤러: HTTP 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행한다. 그리고 뷰에 전달할 결과 데이터를 조회해서 모델에 담는다.
  • 모델: 뷰에 출력할 데이터를 담아둔다. 뷰가 필요한 데이터를 모두 모델에 담아서 전달해주는 덕분에 뷰는 비즈니스 로직이나 데이터 접근을 몰라도 되고, 화면을 렌더링 하는 일에 집중할 수 있다. 
  • 뷰: 모델에 담겨있는 데이터를 사용해서 화면을 그리는 일에 집중한다. 여기서는 HTML을 생성하는 부분을 말한다. 

 

참고
컨트롤러에 비즈니스 로직을 둘 수도 있지만, 이렇게 되면 컨트롤러가 너무 많은 역할을 담당한다.
그래서 일반적으로 비즈니스 로직은 서비스(Service)라는 계층을 별도로 만들어서 처리한다.
그리고 컨트롤러는 비즈니스 로직 이 있는 서비스를 호출하는 역할을 담당한다. 참고로 비즈니스 로직을 변경하면 비즈니스 로직을 호출하는 컨트롤러의 코드도 변경될 수 있다.
앞에서는 이해를 돕기 위해 비즈니스 로직을 호출한다는 표현 보다는, "비즈니스 로직을 실행한다" 고 설명했다.

 

redirect vs forward
리다이렉트는 실제 클라이언트(웹 브라우저)에 리다이렉트 경로를 알려주고, 클라이언트가 redirect 경로로 다시 요청한다. 따라서 클라이언트가 인지할 수 있고, URL 경로도 실제로 변경된다.
반면에 포워드는 서버 내부에서 일어나는 호출이기 때문에 클라이언트가 전혀 인지하지 못한다.
즉, 포워드는 사용자의 요청에 따라 동적으로 컨텐츠를 생성해서 경로 변경없이 보여준다.
/WEB-INF
이 경로안에 JSP가 있으면 외부에서 직접 JSP를 호출할 수 없다.
즉, 이 경로에 있으면 항상 컨트롤러를 통해서 JSP를 호출할 수 있다다.

 

프론트 컨트롤러 패턴

FrontController 패턴 특징

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

 

스프링 웹 MVC와 프론트 컨트롤러

  • 스프링 웹 MVC의 핵심도 바로 FrontController
  • 스프링 웹 MVC의 DispatcherServlet이 FrontController 패턴으로 구현되어 있음

 

MVC  프레임워크 만들기

서블릿을 컨트롤러로 사용하고, JSP를 뷰로 사용해서 MVC 패턴을 적용한다.
Model
HttpServletRequest 객체를 사용한다.

request
request는 내부에 데이터 저장소를 가지고 있다.
- request.setAttribute() : 데이터 저장
- request.getAttribute() : 데이터 조회

 

View를 담당하는 JSP

members.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<a href="/index.html">메인</a>
<table>
    <thead>
    <th>id</th>
    <th>username</th>
    <th>age</th>
    </thead>
    <tbody>
    <c:forEach var="item" items="${members}">
        <tr>
            <td>${item.id}</td>
            <td>${item.username}</td>
            <td>${item.age}</td>
        </tr>
    </c:forEach>
    </tbody>
</table>
</body>
</html>

 

new-form.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!-- 상대경로 사용, [현재 URL이 속한 계층 경로 + /save] -->
<form action="save" method="post">
    username: <input type="text" name="username" />
    age: <input type="text" name="age" />
    <button type="submit">전송</button>
</form>
</body>
</html>

 

save-result.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <meta charset="UTF-8">
</head>
<body>
성공
<ul>
    <li>id=${member.id}</li>
    <li>username=${member.username}</li>
    <li>age=${member.age}</li>
</ul>
<a href="/index.html">메인</a>
</body>
</html>

 

프론트 컨트롤러 도입 - v1

 

프론트 컨트롤러 V1 인터페이스

package hello.servlet.web.frontcontroller.v1;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

public interface ControllerV1 {
    void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}

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

 

MemberFormControllerV1 - 회원 등록 컨트롤러

package hello.servlet.web.frontcontroller.v1.controller;

import hello.servlet.web.frontcontroller.v1.ControllerV1;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

public class MemberFormControllerV1 implements ControllerV1 {
    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String viewPath = "/WEB-INF/views/new-form.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}
RequestDispatcher
요청을 다른 리소스(서블릿, JSP 파일 또는 HTML 파일)로 전송하는 데 사용되는 객체이다.
이를 통해 서블릿 간에 제어를 전달하거나, 데이터를 포함한 요청을 서블릿이나 JSP로 포워드하거나, 요청 처리 결과를 포함하는 컨텐츠를 클라이언트에게 직접 포함하여 응답할 때 사용된다. 주로 서버 내에서의 요청 전달(포워드)과 포함(인클루드)에 사용된다.
dispatcher.forward()
다른 서블릿이나 JSP로 이동할 수 있는 기능.
서버 내부에서 다시 호출이 발생.

 

MemberSaveControllerV1 - 회원 저장 컨트롤러

package hello.servlet.web.frontcontroller.v1.controller;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.v1.ControllerV1;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

public class MemberSaveControllerV1 implements ControllerV1 {
    private MemberRepository memberRepository = MemberRepository.getInstance();
    @Override
    public void process(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);

        String viewPath = "/WEB-INF/views/save-result.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}

 

MemberListControllerV1 - 회원 목록 컨트롤러

package hello.servlet.web.frontcontroller.v1.controller;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.v1.ControllerV1;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.List;

public class MemberListControllerV1 implements ControllerV1 {
    private MemberRepository memberRepository = MemberRepository.getInstance();
    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        List<Member> members = memberRepository.findAll();
        request.setAttribute("members", members);

        String viewPath = "/WEB-INF/views/members.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}

 

FrontControllerServletV1 - 프론트 컨트롤러

package hello.servlet.web.frontcontroller.v1;

import hello.servlet.web.frontcontroller.v1.ControllerV1;
import hello.servlet.web.frontcontroller.v1.controller.MemberFormControllerV1;
import hello.servlet.web.frontcontroller.v1.controller.MemberListControllerV1;
import hello.servlet.web.frontcontroller.v1.controller.MemberSaveControllerV1;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*") ///front-controller/v1` 를 포함한 하위 모든 요청은 이 서블릿에서 받아들인다.
public class FrontControllerServletV1 extends HttpServlet {
    private Map<String, ControllerV1> controllerMap = new HashMap<>();

    public FrontControllerServletV1() {
        //맵에 url 경로와 컨트롤러를 저장
        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());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("FrontControllerServletV1.service");
        String requestURI = request.getRequestURI(); //들어온 경로를 저장
        
        ControllerV1 controller = controllerMap.get(requestURI); //들어온 경로에 맞게 컨트롤러를 맵핑
        if(controller == null) { //이상한 경로로 들어온다면
            response.setStatus(HttpServletResponse.SC_NOT_FOUND); //404 에러
            return;
        }
        controller.process(request, response); //해당 컨트롤러에 있는 process 메소드 호출
    }
}

URL에 따라 컨트롤러가 정해지고, 해당 컨트롤러의 process 메소드가 호출된다.

아래의 코드와 같이 모든 컨트롤러에서 뷰로 이동하는 부분에 중복이 있고, 깔끔하지 않다.

String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);

 

View 분리 - v2

 

MyView

package hello.servlet.web.frontcontroller;

import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.Map;

public class MyView {
    private String viewPath;

    public MyView(String viewPath) {
        this.viewPath = viewPath;
    }

    public void render(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {
         RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
         dispatcher.forward(request, response);
     }

}

 

프론트 컨트롤러 V2 인터페이스

 

package hello.servlet.web.frontcontroller.v2;

import hello.servlet.web.frontcontroller.MyView;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

public interface ControllerV2 {
    MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}

 

MemberFormControllerV2 - 회원 등록 폼

package hello.servlet.web.frontcontroller.v2.controller;

import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v2.ControllerV2;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

public class MemberFormControllerV2 implements ControllerV2 {
    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        return new MyView("/WEB-INF/views/new-form.jsp");
    }
}

각 컨트롤러는 복잡한 dispatcher.forward() 를 직접 생성해서 호출하지 않아도 된다.

단순히 MyView 객체를 생성하고 거기에 뷰 이름만 넣고 반환하면 된다.

 

MemberSaveControllerV2 - 회원 저장

package hello.servlet.web.frontcontroller.v2.controller;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v2.ControllerV2;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

public class MemberSaveControllerV2 implements ControllerV2 {
    private MemberRepository memberRepository = MemberRepository.getInstance();
    @Override
    public MyView process(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);

        return new MyView("/WEB-INF/views/save-result.jsp");
    }
}

 

MemberListControllerV2 - 회원 목록

package hello.servlet.web.frontcontroller.v2.controller;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v2.ControllerV2;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.List;

public class MemberListControllerV2 implements ControllerV2 {
    private MemberRepository memberRepository = MemberRepository.getInstance();
    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        List<Member> members = memberRepository.findAll();
        request.setAttribute("members", members);

        return new MyView("/WEB-INF/views/members.jsp");
    }
}

 

프론트 컨트롤러 V2

package hello.servlet.web.frontcontroller.v2;

import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v2.controller.MemberFormControllerV2;
import hello.servlet.web.frontcontroller.v2.controller.MemberListControllerV2;
import hello.servlet.web.frontcontroller.v2.controller.MemberSaveControllerV2;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@WebServlet(name = "frontControllerServletV2", urlPatterns = "/front-controller/v2/*")
public class FrontControllerServletV2 extends HttpServlet {
    private Map<String, ControllerV2> controllerMap = new HashMap<>();

    public FrontControllerServletV2() {
        controllerMap.put("/front-controller/v2/members/new-form", new MemberFormControllerV2());
        controllerMap.put("/front-controller/v2/members/save", new MemberSaveControllerV2());
        controllerMap.put("/front-controller/v2/members", new MemberListControllerV2());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String requestURI = request.getRequestURI();

        ControllerV2 controller = controllerMap.get(requestURI);

        if(controller == null){
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        MyView view = controller.process(request,response);
        view.render(request, response);
    }
}

ControllerV2의 반환 타입이 MyView 이므로 프론트 컨트롤러는 컨트롤러의 호출 결과로 MyView 를 반환 받는다. 그리고view.render() 를 호출하면 forward 로직을 수행해서 JSP가 실행된다.

프론트 컨트롤러의 도입으로 MyView 객체의 render() 를 호출하는 부분을 모두 일관되게 처리할 수 있다. 각각의 컨트롤러는 MyView 객체를 생성만 해서 반환하면 된다.

 

하지만 MyView는 HttpServletRequest, HttpServletResponse 가 필요하지만, 컨트롤러는 HttpServletRequest, HttpServletResponse가 필요없다. -> 서블릿 종속성

V2는 컨트롤러까지 HttpServletRequest, HttpServletResponse 를 전달한다는 단점이 존재한다.

또한 컨트롤러에서 지정하는 뷰 이름에 중복이 있다.

 

Model 추가 - v3

V3는 서블릿 종속성과 뷰 이름 중복을 제거하는 것이 핵심이다.

 

ModelView

서블릿의 종속성을 제거하기 위해 Model을 직접 만들고, 추가로 View 이름까지 전달하는 객체를 만든다.

즉 컨트롤러에서 HttpServletRequest를 사용하지 않는다.

package hello.servlet.web.frontcontroller;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

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

    public ModelView(String viewName) {
        this.viewName = viewName;
    }

    public String getViewName() {
        return viewName;
    }

    public void setViewName(String viewName) {
        this.viewName = viewName;
    }

    public Map<String, Object> getModel() {
        return model;
    }

    public void setModel(Map<String, Object> model) {
        this.model = model;
    }
}

뷰의 이름과 뷰를 렌더링할 때 필요한 model 객체를 가지고 있다. model은 단순히 map으로 되어 있으므로 컨트롤러에서 뷰에 필요한 데이터를 key, value로 넣어주면 된다.

여기서 사용하는 Map은 <String, Object>이다.

Object에 다양한 객체가 저장된다.

 

프론트 컨트롤러 V3 인터페이스

package hello.servlet.web.frontcontroller.v3;

import hello.servlet.web.frontcontroller.ModelView;

import java.util.Map;

public interface ControllerV3 {
    ModelView process(Map<String, String> paramMap);
}

이 컨트롤러는 서블릿 기술을 전혀 사용하지 않는다. 따라서 구현이 매우 단순해지고, 테스트 코드 작성시 테스트 하기 쉽다.
HttpServletRequest
가 제공하는 파라미터는 프론트 컨트롤러가 paramMap에 담아서 호출해주면 된다.
응답 결과로 뷰 이름과 뷰에 전달할 Model 데이터를 포함하는 ModelView 객체를 반환하면 된다.

 

MemberFormControllerV3 - 회원 등록 폼

package hello.servlet.web.frontcontroller.v3.controller;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;

import java.util.Map;

public class MemberFormControllerV3 implements ControllerV3 {
    @Override
    public ModelView process(Map<String, String> paramMap) {
        return new ModelView("new-form");
    }
}

ModelView 를 생성할 때 new-form 이라는 view의 논리적인 이름을 지정한다. 실제 물리적인 이름은 프론트 컨트롤러에서 처리한다.

 

MemberSaveControllerV3 - 회원 저장

package hello.servlet.web.frontcontroller.v3.controller;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;

import java.util.Map;

public class MemberSaveControllerV3 implements ControllerV3 {
    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public ModelView process(Map<String, String> paramMap) {
        String username = paramMap.get("username");
        int age = Integer.parseInt(paramMap.get("age"));

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

        ModelView mv = new ModelView("save-result");
        mv.getModel().put("member", member);
        return mv;
    }
}

mv.getModel().put("member", member);`
-> 모델<String, Object> 에 뷰에서 필요한 member 객체를 담고 반환한다.

 

MemberListControllerV3 - 회원 목록

package hello.servlet.web.frontcontroller.v3.controller;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;

import java.util.List;
import java.util.Map;

public class MemberListControllerV3 implements ControllerV3 {
    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public ModelView process(Map<String, String> paramMap) {
        List<Member> members = memberRepository.findAll();

        ModelView mv = new ModelView("members");
        mv.getModel().put("members", members);

        return mv;
    }
}

mv.getModel().put("members", members);`
-> 모델<String, Object> 에 뷰에서 필요한 members 객체를 담고 반환한다.

 

FrontControllerServletV3

package hello.servlet.web.frontcontroller.v3;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@WebServlet(name = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*")
public class FrontControllerServletV3 extends HttpServlet {
    private Map<String, ControllerV3> controllerMap = new HashMap<>();

    public FrontControllerServletV3() {
        controllerMap.put("/front-controller/v3/members/new-form", new MemberFormControllerV3());
        controllerMap.put("/front-controller/v3/members/save", new MemberSaveControllerV3());
        controllerMap.put("/front-controller/v3/members", new MemberListControllerV3());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String requestURI = request.getRequestURI(); //현재 경로를 가져옴

        ControllerV3 controller = controllerMap.get(requestURI);
        if(controller == null){
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        Map<String, String> paramMap = createParamMap(request); //request의 파라미터를 모두 저장
        ModelView mv = controller.process(paramMap); //선택된 컨트롤러의 process 메서드에 paramMap을 전달 -> 이후 리턴된 ModelView 객체에 저장

        String viewName = mv.getViewName(); //ModelView의 논리적 경로를 가져옴
        MyView view = viewResolver(viewName); //뷰 리졸버를 통해 논리적 뷰를 실제 뷰의 경로를 가져옴
        view.render(mv.getModel(), request, response); // MyView의 render 메서드를 통해서 뷰를 렌더링
    }

    private Map createParamMap(HttpServletRequest request) {
        Map<String, String> paramMap = new HashMap<>();
        request.getParameterNames().asIterator()
                .forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName))); //request의 모든 내용을 맵에 저장
        return paramMap;
    }

    private MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp"); //논리적 뷰 경로를 이용하여 실제 뷰경로를 리턴
    }

}
request.getParameterNames().asIterator()
                .forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));

request.getParameterNames() 메소드를 호출하여 요청에 포함된 모든 파라미터의 이름을 Enumeration<String> 타입으로 반환한다.

asIterator() 메소드는 이 Enumeration을 Iterator로 변환하여, 스트림 연산이나 반복 연산을 사용할 수 있게 한다.

forEachRemaining 메소드는 Iterator의 모든 요소에 대해 주어진 액션(여기서는 람다 표현식)을 실행한다. 이 메소드는 Iterator에 남아 있는 각 요소에 대해 한 번씩 액션을 수행한다.

람다 표현식 paramName -> paramMap.put(paramName, request.getParameter(paramName))는 각 파라미터 이름(paramName)에 대해 다음을 수행한다
- request.getParameter(paramName)을 호출하여 해당 파라미터의 값을 가져온다.
- 가져온 파라미터 이름과 값을 paramMap에 저장한다. 여기서 paramMap은 파라미터 이름을 키로, 파라미터 값을 값으로 하는 Map 객체이다.

 

 

MyView

package hello.servlet.web.frontcontroller;

import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.Map;

public class MyView {
    private String viewPath;

    public MyView(String viewPath) {
        this.viewPath = viewPath;
    }

    public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }

    public void render(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        modelToRequestAttribute(model, request); //모델의 내용을 request의 파라미터로 변환
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }

    private void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
        model.forEach((key, value) -> request.setAttribute(key, value)); //모델의 모든 키-밸류를 request의 파라미터로 변환
        //setAttribute(String name, Object value)
    }
}

모델의 키와 밸류 값을 request로 변환하는 부분이 핵심이다.

 

v3 컨트롤러는 서블릿 종속성을 제거하고 뷰 경로의 중복을 제거하는 등, 잘 설계된 컨트롤러이다. 그런데 실제 컨트톨러 인터페이스를 구현하는 개발자 입장에서 보면, 항상 ModelView 객체를 생성하고 반환해야 하는 부분이 조금은 번거롭다.

 

좋은 프레임워크는 아키텍처도 중요하지만, 그와 더불어 실제 개발하는 개발자가 단순하고 편리하게 사용할 수 있어야 한다. 소위 실용성이 있어야 한다.

 

 

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

v3를 조금 변경해서 실제 구현하는 개발자들이 매우 편리하게 개발할 수 있는 v4 버전을 개발해보자.

기본적인 구조는 V3와 같다. 대신에 컨트롤러가 ModelView(객체) 를 반환하지 않고, ViewName(String) 만 반환한다.

 

 

프론트 컨트롤러 V4 인터페이스

package hello.servlet.web.frontcontroller.v4;
import java.util.Map;
public interface ControllerV4 {
    String process(Map<String, String> paramMap, Map<String, Object> model);
}

 

 

MemberFormControllerV4

package hello.servlet.web.frontcontroller.v4.controller;
import hello.servlet.web.frontcontroller.v4.ControllerV4;
import java.util.Map;
public class MemberFormControllerV4 implements ControllerV4 {
    @Override
    public String process(Map<String, String> paramMap, Map<String, Object> model) {
        return "new-form";
    }
}

리턴 타입이 String이다.

즉, 컨트롤러는 단순히 뷰의 논리적 경로만 반환한다.

 

MemberSaveControllerV4

package hello.servlet.web.frontcontroller.v4.controller;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.v4.ControllerV4;
import java.util.List;
import java.util.Map;
public class MemberListControllerV4 implements ControllerV4 {
    private MemberRepository memberRepository = MemberRepository.getInstance();
    @Override
    public String process(Map<String, String> paramMap, Map<String, Object> model) {
        List<Member> members = memberRepository.findAll();
        model.put("members", members);
        return "members";
    }
}

모델이 파라미터로 전달되기 때문에, 모델을 직접 생성하지 않아도 된다.

 

MemberListControllerV4

package hello.servlet.web.frontcontroller.v4.controller;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.v4.ControllerV4;
import java.util.Map;
public class MemberSaveControllerV4 implements ControllerV4 {
    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public String process(Map<String, String> paramMap, Map<String, Object> model) {
        String username = paramMap.get("username");
        int age = Integer.parseInt(paramMap.get("age"));

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

        return "save-result";
    }
}

 

 

FrontControllerServletV4

package hello.servlet.web.frontcontroller.v4;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v4.controller.MemberFormControllerV4;
import hello.servlet.web.frontcontroller.v4.controller.MemberListControllerV4;
import hello.servlet.web.frontcontroller.v4.controller.MemberSaveControllerV4;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@WebServlet(name = "frontControllerServletV4", urlPatterns = "/front-controller/v4/*")
public class FrontControllerServletV4 extends HttpServlet {
     private Map<String, ControllerV4> controllerMap = new HashMap<>();
     public FrontControllerServletV4() {
         controllerMap.put("/front-controller/v4/members/new-form", new MemberFormControllerV4());
         controllerMap.put("/front-controller/v4/members/save", new MemberSaveControllerV4());
         controllerMap.put("/front-controller/v4/members", new MemberListControllerV4());
     }

     @Override
     protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
         String requestURI = request.getRequestURI(); //경로를 가져옴
         ControllerV4 controller = controllerMap.get(requestURI); //경로에 맞는 컨트롤러 맵핑
         if (controller == null) {
             response.setStatus(HttpServletResponse.SC_NOT_FOUND);
             return;
         }

         Map<String, String> paramMap = createParamMap(request); //request의 파라미터를 모두 저장
         Map<String, Object> model = new HashMap<>(); //모델 생성

         String viewName = controller.process(paramMap, model);// 해당 컨트롤러 호출을 통해 논리적 뷰 경로이름을 받아옴
         MyView view = viewResolver(viewName); //뷰 리졸버를 통해 실제 뷰 경로를 받아옴
         view.render(model, request, response); //MyView의 render 메서드를 통해서 뷰를 렌더링
     }
     private Map<String, String> createParamMap(HttpServletRequest request) {
         Map<String, String> paramMap = new HashMap<>();
         request.getParameterNames().asIterator().forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));

         return paramMap;
     }
     private MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
     }
}

 

Map<String, Object> model = new HashMap<>();

모델 객체를 프론트 컨트롤러에서 생성해서 넘겨준다. 컨트롤러에서 모델 객체에 값을 담으면 여기에 그대로 담겨있게 된다.

 

 String viewName = controller.process(paramMap, model);
 MyView view = viewResolver(viewName);

컨트롤러가 직접 뷰의 논리 이름을 반환하므로 이 값을 사용해서 실제 물리 뷰를 찾을 수 있다.

 

유연한 컨트롤러 - v5

만약 어떤 개발자는 `ControllerV3` 방식으로 개발하고 싶고, 어떤 개발자는 `ControllerV4` 방식으로 개발하고 싶다면 어댑터 패턴을 사용해서 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있도록 변경해야 한다.

  • 핸들러 어댑터: 중간에 어댑터 역할을 하는 어댑터가 추가되었는데 이름이 핸들러 어댑터이다. 여기서 어댑터 역 할을 해주는 덕분에 다양한 종류의 컨트롤러를 호출할 수 있다.
  • 핸들러: 컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경했다. 그 이유는 이제 어댑터가 있기 때문에 꼭 컨트롤러 의 개념 뿐만 아니라 어떠한 것이든 해당하는 종류의 어댑터만 있으면 다 처리할 수 있기 때문이다.

 

 

어댑터용 인터페이스

package hello.servlet.web.frontcontroller.v5;

import hello.servlet.web.frontcontroller.ModelView;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.logging.Handler;

public interface MyHandlerAdapter {
    boolean supports(Object handler);

    ModelView handle(HttpServletRequest request, HttpServletResponse response, Object Handler) throws ServletException, IOException;
}

 

boolean supports(Object handler)

  • handler는 컨트롤러를 말한다.
  • 어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 메서드다.

 

ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler)

  • 어댑터는 실제 컨트롤러를 호출하고, 그 결과로 ModelView를 반환해야 한다.
  • 실제 컨트롤러가 ModelView를 반환하지 못하면, 어댑터가 ModelView를 직접 생성해서라도 반환해야 한다.
  • 이전에는 프론트 컨트롤러가 실제 컨트롤러를 호출했지만 이제는 이 어댑터를 통해서 실제 컨트롤러가 호출된다.

 

ControllerV3HandlerAdapter

ControllerV3를 지원하는 어댑터

package hello.servlet.web.frontcontroller.v5.adapter;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;
import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
    @Override
    public boolean supports(Object handler) {
        return (handler instanceof ControllerV3);
    }

    @Override
    public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object Handler) throws ServletException, IOException {
        ControllerV3 controller = (ControllerV3) Handler;

        Map<String, String> paramMap = createParamMap(request);

        ModelView mv = controller.process(paramMap);
        return mv;
    }

    private Map<String, String> createParamMap(HttpServletRequest request) {
        Map<String, String> paramMap = new HashMap<>();
        request.getParameterNames().asIterator()
                .forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
        return paramMap;
    }
}

 

 

ControllerV4HandlerAdapter

package hello.servlet.web.frontcontroller.v5.adapter;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v4.ControllerV4;
import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.util.HashMap;
import java.util.Map;
public class ControllerV4HandlerAdapter implements MyHandlerAdapter {
    @Override
    public boolean supports(Object handler) {
        return (handler instanceof ControllerV4);
    }
    @Override
    public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        ControllerV4 controller = (ControllerV4) handler;
        Map<String, String> paramMap = createParamMap(request);
        Map<String, Object> model = new HashMap<>();
        String viewName = controller.process(paramMap, model);

    	//중요
    	//`ControllerV4` 는 뷰의 이름을 반환했지만, 어댑터는 이것을 ModelView로 만들어서 형식을 맞추어 반환
        ModelView mv = new ModelView(viewName);
        mv.setModel(model);

        return mv;
    }
    private Map<String, String> createParamMap(HttpServletRequest request) {
        Map<String, String> paramMap = new HashMap<>();
        request.getParameterNames().asIterator()
                .forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
        return paramMap;
    }
}

어댑터가 호출하는 ControllerV4 는 뷰의 이름을 반환한다.

그런데 어댑터는 뷰의 이름이 아니라 ModelView 를 만들어서 반환해야 한다. 여기서 어댑터가 꼭 필요한 이유가 나온다.

ControllerV4 는 뷰의 이름을 반환했지만, 어댑터는 이것을 ModelView로 만들어서 형식을 맞추어 반환한다.

 

 

 

FrontControllerServletV5

package hello.servlet.web.frontcontroller.v5;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;
import hello.servlet.web.frontcontroller.v4.controller.MemberFormControllerV4;
import hello.servlet.web.frontcontroller.v4.controller.MemberListControllerV4;
import hello.servlet.web.frontcontroller.v4.controller.MemberSaveControllerV4;
import hello.servlet.web.frontcontroller.v5.adapter.ControllerV3HandlerAdapter;
import hello.servlet.web.frontcontroller.v5.adapter.ControllerV4HandlerAdapter;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
    private final Map<String, Object> handlerMappingMap = new HashMap<>(); //핸들러(컨트롤러)의 맵핑 정보를 저장하는 Map
    private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>(); //어댑터의 리스트를 담고있는 리스트
    public FrontControllerServletV5() {
        initHandlerMappingMap(); //핸들러(컨트롤러)의 맵핑 정보를 저장하는 Map에 v3, v4의 핸들러를 저장
        initHandlerAdapters(); //어댑터의 리스트를 담고있는 리스트에 v3, v4의 어댑터를 저장
    }
    private void initHandlerMappingMap() {
        //v3
        handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
        handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
        handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());

        //v4
        handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4());
        handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4());
        handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4());
    }
    private void initHandlerAdapters() {
        handlerAdapters.add(new ControllerV3HandlerAdapter()); //v3
        handlerAdapters.add(new ControllerV4HandlerAdapter()); //V4
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        Object handler = getHandler(request); //uri에 맵핑된 핸들러를 가져옴
        if (handler == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        MyHandlerAdapter adapter = getHandlerAdapter(handler); //핸들러 어댑터들 중에서 이용 가능한 핸들러 어댑터를 저장
        ModelView mv = adapter.handle(request, response, handler); //선택된 어댑터의 handle 메서드를 통해 ModelView를 반환받고 저장
        MyView view = viewResolver(mv.getViewName()); //뷰 리졸버를 통해 실제 뷰의 경로를 저장
        view.render(mv.getModel(), request, response); //뷰를 렌더링
    }
    private Object getHandler(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        return handlerMappingMap.get(requestURI);
    }
    private MyHandlerAdapter getHandlerAdapter(Object handler) { //핸들러 어댑터들 중에서 매개변수로 들어온 핸들러에 대해 지원 가능 핸들러 찾기
        for (MyHandlerAdapter adapter : handlerAdapters) { //어댑터 리스트를 통해 알맞는 어댑터를 가져옴
            if (adapter.supports(handler)) {
                return adapter;
            }
        }
        throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler);
    }
    private MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }
}

 

어댑터의 handle(request, response, handler) 메서드를 통해 실제 어댑터가 호출된다.

H2 데이터베이스

 

H2 데이터베이스는 자바 기반의 경량화된 관계형 데이터베이스 관리 시스템(RDBMS)이다.

메모리 내에서 실행될 수 있어서 개발이나 테스트 용도로 많이 사용되곤 한다.

그래서 애플리케이션 개발 시 데이터베이스 서버를 별도로 구성하지 않고도 손쉽게 데이터베이스 환경을 구축할 수 있어서 매우 편리하다.

 

H2 데이터베이스의 주요 특징은 아래와 같다.

  • H2는 자바 애플리케이션에 내장될 수 있어서 복잡한 설치 과정 없이 데이터베이스를 바로 사용할 수 있다.
  • 메모리에서 직접 실행되므로 빠른 데이터 액세스와 테스트가 가능하다. 개발 중에는 메모리 데이터베이스를 사용하고, 실제 운영 환경에서는 디스크 기반 저장소를 사용하는 식으로 활용할 수 있다.
  • H2는 표준 SQL을 많이 지원한다. 복잡한 쿼리나 함수, 트리거 같은 고급 데이터베이스 기능도 사용할 수 있다.
  • H2 데이터베이스는 웹 기반의 콘솔을 제공해서, 웹 브라우저를 통해 데이터베이스를 쉽게 관리하고 SQL 쿼리를 실행할 수 있다.
  • 오픈 소스이기 때문에 무료로 사용할 수 있고, 필요에 따라 코드를 수정하거나 확장하는 것도 가능하다.

 

이러한 특성 때문에, H2는 개발 초기 단계나 단위 테스트를 위한 데이터베이스로 아주 적합하다.

 

그러나 메모리 데이터베이스의 특성상 데이터가 영구적이지 않기 때문에, 중요한 데이터를 관리할 때는 디스크 기반의 저장 방식을 사용하는 것이 좋다.

 

H2 데이터베이스 설치

h2 데이터베이스는 아래의 링크에서 다운로드 받을 수 있다.

https://www.h2database.com/html/download.html

 

Downloads

Downloads Version 2.2.224 (2023-09-17) Windows Installer (SHA1 checksum: 1e4cda116519e8f95cac8298b1a4d7cbd50073ec) Platform-Independent Zip (SHA1 checksum: 8de40da72b269ae1d7a899f25aa0bbcb242b6220) Version 2.1.214 (2022-06-13) Windows Installer (SHA1 check

www.h2database.com

 

 

맥은 Platform-Independent Zip를 다운로드 하고 아무곳이나 zip을 풀어준다.

 

버전에 맞게 설치하면 되고, 나는 스프링 부트에서 제공되는 버전이 2.2.224라서 이와 맞는 버전을 다운로드 하였다.

 

다운로드 받은 폴더의 구조는 아래의 그림과 같다.

 

이후 터미널을 열고, 다운로드 받은 폴더로 이동한다.

cd 다운로드 받은 폴더 경로

 

이동했다면 아래의 명령어로 권한을 허락해준다.

chmod 755 ./bin/h2.sh

 

아래의 명령어로 h2.sh를 실행한다.

./bin/h2.sh

 

명령어를 실행시키면 웹 페이지가 하나 뜰텐데, 주소창에서 아이피:8082~~가 아니라

localhost:8082~~가 되도록 수정한다.

localhost 외에 다른 것을 수정하면 권한 문제가 생길 수도 있다.

 

이런 창에서 아래와 같이 수정한다.

JDBC URL을 jdbc:h2:~/test로 최초 한번 설정해준다.

이 경우 연결 시험 을 호출하면 오류가 발생한다. 연결을 직접 눌러주어야 한다.

이후에는 JDBC URL에 jdbc:h2:tcp://localhost/~/test 을 입력하고 연결하면 된다. 

 

- 새로 h2 DB를 만들 때는 먼저 첫 경로 : jdbc:h2: + 저장하고자 하는 디렉터리 경로 → 디렉터리안에 mv.db가 생성된다.
- 첫 생성 이후 접속 : jdbc:h2:tcp://localhost/~ + mv.db가 저장된 경로

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


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

웹은 HTTP 프로토콜 기반으로 통신하여 데이터를 주고받는다.

  • HTML, TEXT
  • IMAGE, 음성, 영상, 파일
  • JSON, XML (API) 등

 

웹 서버 (Web Server)

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

 

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

  • HTTP 기반으로 동작
  • 웹 서버 기능 + 프로그램 코드 실행하여 애플리케이션 로직 수행
  • 동적 HTML, HTTP API(JSON), 서블릿, JSP, 스프링 MVC
  • 톰캣, Jetty, Underflow 등이 WAS로 사용

 

웹 서버와 웹 어플리케이션 서버 (WAS) 차이

  • 웹 서버는 정적 리소스, WAS는 애플리케이션 로직까지 수행
  • 웹 서버도 프로그램 실행할 수도 있고, WAS도 웹 서버의 기능을 제공함  → 용어와 경계가 모호
  • WAS는 애플리케이션 코드를 실행하는데 더 특화

 

웹 시스템 구성 (WAS + DB)

WAS와 DB만으로도 시스템을 구성가능

WAS가 정적리소스와 애플리케이션 로직 모두 제공

 


하지만 이렇게 구성할 경우 WAS가 많은 역할을 담당하여 서버가 과부하 될 수 있다.
비싼 애플리케이션 로직이 정적 리소스 때문에 수행이 어려울 수도 있고, WAS에 장애가 있을시에 오류 화면을 노출을 못할 수도 있다.

 

웹 시스템 구성 (Web Server + WAS + DB)

정적 리소스는 웹 서버가 처리, 동적 리소스는 WAS가 처리 

  • 웹 서버는 애플리케이션 로직같은 동적인 처리가 필요하면 WAS에 요청한다.
  • WAS가 중요한 애플리케이션 로직 처리 전담
  • 리소스 관리를 효율적으로 할 수 있다.

 

  • 정적 리소스 사용이 많다면 → WEB 서버 증설
  • 애플리케이션 리소스 사용이 많다면 → WAS 증설
  • 이런식으로 유연하게 관리 가능

 

WAS, DB 장애시 WEB 서버가 오류 화면 제공 가능

 

서블릿

동적인 웹 페이지를 만들 때 사용되는 자바 기반 웹 애플리케이션 프로그래밍 기술
클라이언트의 요청을 처리 후 결과를 반환해준다.

서블릿이 없다면 위 그림에서 가장 중요한 초록색으로 영역이 칠해진 비즈니스 로직 말고도 개발자가 처리해야할 일들이 너무 많다.

하지만 서블릿이 비즈니스 로직에만 집중할 수 있게 해준다.

즉, 비즈니스 로직 작성 외에 나머지를 서블릿이 모두해준다.

 

서블릿 특징

@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response){
      //애플리케이션 로직
    }
}

 

  • urlPatterns(/hello)의 URL이 호출되면 서블릿 코드 실행
  • HttpsServletRequest - HTTP 요청 정보를 편리하게 사용
  • HttpsServletResponse - HTTP 응답 정보를 편리하게 제공
  • → 서블릿을 통해 개발자는 HTTP 스펙을 편리하게 사용

 

  1. 웹 브라우저에서 localhost:8080/hello 라고 요청을 함
  2. WAS에서 HTTP 요청 메시지를 기반으로 Request와 Response 객체 생성
  3. Request, Response를 파라미터로 넘기면서 Servlet 객체 호출
  4. 개발자는 Request 객체에서 HTTP 요청 정보를 꺼내서 사용하고, Response 객체에 HTTP 응답 정보를 입력할 수 있음.
  5. Servlet이 응답 정보 입력을 완료하면 WAS는 Response 객체 정보로 HTTP 응답 생성
  6. 웹 브라우저에 클라이언트에게 전달

 

서블릿 컨테이너

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

 

멀티 쓰레드

  • 멀티 쓰레드에 대한 부분은 WAS가 처리
  • 개발자가 멀티 쓰레드 관련 코드를 신경쓰지 않아도 됨
  • 개발자는 마치 싱글 쓰레드 프로그래밍을 하듯이 편리하게 소스 코드를 개발
  • 멀티 쓰레드 환경이므로 싱글톤 객체(서블릿, 스프링 빈)는 주의해서 사용

 

쓰레드

서블릿 호출은 쓰레드가 한다.

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

 

2023.08.01 - [Java Category/Java] - [Java] 멀티 스레드

 

[Java] 멀티 스레드

이 게시글은 이것이 자바다(저자 : 신용권, 임경균)의 책과 동영상 강의를 참고하여 개인적으로 정리하는 글임을 알립니다. 2023.06.27 - [컴퓨터 구조 & 운영체제/운영체제] - [운영체제] 스레드(Threa

rebugs.tistory.com

 

요청마다 쓰레드를 생성


장점

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

 

단점

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

 

쓰레드풀

2023.08.02 - [Java Category/Java] - [Java] 데몬 스레드와 스레드풀

 

[Java] 데몬 스레드와 스레드풀

이 게시글은 이것이 자바다(저자 : 신용권, 임경균)의 책과 동영상 강의를 참고하여 개인적으로 정리하는 글임을 알립니다. 데몬 스레드(Daemon Thread) 데몬 스레드는 주 스레드의 작업을 돕는 보조

rebugs.tistory.com

 

특징

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

 

사용

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

 

장점

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

 

HTML, HTTP API, CSR, SSR

정적 리소스

  • 고정된 HTML 파일, CSS, JS, 이미지 영상 등
  • 주로 웹 브라우저에서 요청하면 웹 서버에서 리소스 파일 전달

 

HTML 페이지

  • 웹 브라우저에서 요청이 오면 WAS는 DB에서 조회 후, 동적으로 HTML 생성하여(뷰 템플릿) 웹 브라우저에게 전달
  • 웹 브라우저는 HTML을 받아서 해석

 

HTTP API

  • HTML이 아닌 데이터만 전달
  • 주로 JSON 형태로 데이터 통신
  • 웹브라우저에서 요청이 오면 WAS는 DB에서 조회하여 주로 JSON 형식 사용하여 웹 브라우저에게 전달

 

  • 다양한 시스템에서 호출(앱, 웹 클라이언트, 서)
  • 데이터만 주고 받음, UI 화면이 필요하면, 클라이언트가 별도 처리

 

CSR (Client Side Rendering)

  • HTML 결과를 자바스크립트를 사용해 웹 브라우저에서 동적으로 생성해서 적용
  • 주로 동적인 화면에 사용, 웹 환경을 마치 앱 처럼 필요한 부분부분 변경할 수 있음
    예) 구글 지도, Gmail, 구글 캘린더
  • 관련기술: React, Vue.js → 웹 프론트엔드 개발자

 

SSR (Server Side Rendering)

  • HTML 최종 결과를 서버에서 만들어서 웹 브라우저에 전달
  • 주로 정적인 화면에 사용
  • 관련기술: JSP, 타임리프 → 백엔드 개발자
    현대에는 타임리프 사용을 권장

 

자바 백엔드 웹 기술 역사

역사

  1. 서블릿 : HTML 생성이 어렵다.
  2. JSP : HTML 생성 편리, 비즈니스 로직까지해서 많은 역할 담당, 유지보수 어려움
  3. 서블릿, JSP 조합의 MVC 패턴 사용 : 모델, 뷰 컨트롤러로 역할을 나눠서 개발
  4. 애노테이션 기반의 스프링 MVC - 현재는 이 기술을 적어도 자바 진영에서는 모두 사용
스프링 부트

-스프링 부트는 서버를 내장
-과거에는 서버에 WAS를 직접 설치하고, 소스는 War 파일을 만들어서 설치한 WAS에 배포
-스프링 부트는 빌드 결과(Jar)에 WAS 서버 포함 -> 빌드 배포 단순화

 

스프링 웹 기술의 분화

  • Web Servlet - Spring MVC : 서블릿을 사용함, 현재 실무에서 가장 많이 사용
  • Web Reactive - Spring WebFlux : 서블릿을 사용 안함, 함수형 스타일, 여러 문제점으로 실무에서 사용안함.

 

자바 뷰 템플릿 역사

HTML을 편리하게 생성하는 뷰 기능

  • JSP : 속도 느림, 기능 부족
  • 프리마커(Freemarker), Velocity(벨로시티) : 속도 문제 해결, 다양한 기능
  • 타임리프(Thymeleaf)
    -내추럴 템플릿: HTML의 모양을 유지하면서 뷰 템플릿 적용 가능
    -스프링 MVC와 강력한 기능 통합
    -최선의 선택, 단 성능은 프리마커, 벨로시티가 더 빠름

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


캐시를 사용하지 않는다면, 서버의 데이터가 변경되지 않았어도 클라이언트는 서버의 데이터를 다운로드 받아야 한다.

때문에 브라우저 로딩 속도가 느려진다. 이는 성질 급한 사용자가 참지 못하고 해당 사이트를 이탈할 수도 있다.

이는 SEO에 안좋은 영향을 미치기 때문에 장기적으로 사이트에 안좋은 영향을 끼칠 수 있다.

또한 인터넷 네트워크는 느리고 비싸기 때문에 캐시를 필수적으로 사용해야 한다.

 

캐시 기본 동작

캐시 시간 설정

 

브라우저에서 GET /star.jpg 첫번째 요청을 보내면, 서버는 HTTP 헤더(0.1M) + HTTP 바디=star.jpg 이미지(1.0M)를 담아 응답을 보낸다.

중요한 점은 HTTP 헤더에 cache-control로 캐시 유효 시간을 설정해서 응답한다는 것이다.

예시는 최대 캐시 유효 시간을 60초로 설정하였다.

 

캐시 유효 시간이 만료 되기 전에 다시 서버에 요청하면 아래의 그림과 같이 서버에 요청하기 전 브라우저 캐시에 요청을 보내고 캐시 유효 시간을 검증한다.

 

캐시 유효시간이 유효하다면 캐시에서 직접 조회하여 브라우저에 이미지를 표시한다.

 

캐시 시간 초과

만약 캐시 유효 시간을 검증했는데 유효 시간을 초과했다면 서버로 요청하게 된다.

서버는 이전과 동일하게 최대 캐시 유효시간을 설정하여  HTTP 헤더(0.1M) + HTTP 바디=star.jpg 이미지(1.0M)를 담아 응답을 보낸다. 

 

또한 클라이언트 웹 브라우저는 이전과 동일하게 브라우저 캐시에 60초 유효 상태로 응답 결과를 저장한다.

 

단순히 캐시의 시간만 설정하는 것은 비효율적일 수도 있다.

캐시 유효 기간이 지났지만, 서버의 데이터가 바뀌지 않았을 경우, 불필요한 데이터 다운로드가 있을 수 있기 때문이다.

이는 검증 헤더로 극복이 가능하다.

 

 

검증 헤더와 조건부 요청

검증 헤더 : 캐시 데이터와 서버 데이터가 같은지 검증하는 데이터

  1. Last-Modified
  2. ETag

 

조건부 요청 헤더 : 검증 헤더로 조건에 따른 분기시 사용

  • If-Modified-Since: Last-Modified를 사용
    If-Modified-Since <->If-Unmodified-Since
  • If-None-Match: ETag 사용
  • 조건이 만족하면 200 OK
  • 조건이 만족하지 않으면, 304 Not Modified

 

Last-Modified

캐시의 유효 시간이 초과된 이후에도 서버의 데이터가 동일하다면, 이미 다운로드된 데이터를 다시 다운로드 받지 않아도 된다.

브라우저 캐시와 서버의 데이터가 동일한지 아닌지를 판단할 수 있는 방법은 검증 헤더를 추가하는 것이다.

 

먼저 브라우저 캐시에 아무것도 없다고 가정하고, 서버에 요청을 보낸다.

그러면 서버는 헤더에 Last-Modified(데이터가 마지막에 수정된 시간)을 설정해서 클라이언트에 데이터를 전송한다.

 

클라이언트는 캐시 유효 시간과 데이터 최종 수정일을 함께 응답 결과를 캐시에 저장한다. 

 

Last-Modified - 캐시 시간 초과

클라이언트가 캐시 만료 후 요청을 보낸다.

요청을 보낼 때 헤더에 'if-modified-since:데이터 최종 수정일'을 붙여 서버에 요청을 보낸다.

데이터 최종 수정일은 GMT 기준으로 작성
Last-Modified: Thu, 04 Jum 2020 07:19:24 GMT

 

서버는 요청 받은 헤더의 if-modified-since 값과 데이터 최종 수정일을 비교하여 데이터가 바뀌었는지 검증한다.

 

만약 캐시의 데이터 최종 수정일과 서버의 데이터 최종 수정일이 같다면(데이터의 변경이 발생하지 않았다면)

(만약 데이터의 변경이 발생하였다면 200 상태 코드를 전달하게 된다. 따라서 서버의 데이터를 다운로드 받는다.)

 

서버는 데이터가 변경되지 않았다는 의미로 HTTP Body 없이, HTTP 헤더에 304 Not Modified를 전송한다.
HTTP Body 없이 HTTP Header 부분만 전송하기 때문에 라이트한 데이터를 보낸다.

 

클라이언트는 서버로부터 304 Not Modified를 응답받았으므로 캐시 데이터(응답 결과)를 재사용하고 헤더 데이터를 갱신한다.

300번대 메시지는 리다이렉트 응답이다.

따라서 캐시로 리다이렉트 된다.

 

클라이언트의 웹 브라우저와 서버의 데이터가 같으니 캐시에서 데이터를 조회하여 사용한다.

 

흐름을 정리하면 아래와 같다.

  1. 클라이언트가 서버에 데이터를 최초 요청한다.
  2. 서버는 cache-control, last-modified를 헤더에 붙여 캐시의 유효 기간, 데이터 최종 수정일을 설정하여 응답을 보낸다.
  3. 브라우저 캐시에 유효 기간과 데이터 최종 수정일과 함께 응답 결과를 저장한다.
  4. 클라이언트가 캐시의 유효기간이 만료된 데이터를 요청하면, 요청 헤더의 if-modified-since에 last-moidifed(데이터 최종 수정일)값을 설정하여 서버에 요청을 보낸다.
  5. 서버는 if-modified-since 값과 데이터의 최종 수정일을 비교하여 데이터 변경이 있는지 검증한다.
  6. 데이터 변경이 없었다면, 서버는 헤더 메타 정보, 304 Not Modified 를 붙여 응답을 보낸다. (메시지 바디 포함 X)
  7. 클라이언트가 304 응답을 받으면, 캐시의 메타 정보(캐시 유효기간 등)을 갱신하고, 캐시에 저장된 데이터를 조회하여 사용한다.

 

서버

  • 캐시가 만료되도 서버의 데이터가 변경되지 않았으면, 서버는 304 Not Modified + 헤더 메타 정보만 응답(HTTP bodyX)

 

클라이언트

  • 클라이언트는 서버가 보낸 응답 헤더 정보로 캐시의 메타 정보를 갱신(캐시 유효 기간도 갱신)
  • 클라이언트는 캐시에 저장된 데이터 재활용
    -> 네트워크 다운로드가 발생하지만 용량이 적은 헤더 정보만 다운로드하기 때문에, 매우 실용적

 

If-Modified-Since -> 데이터가 변경되었을 때와 변경되지 않았을 때

데이터 미변경 예시

  • 캐시: 2020년 11월 10일 10:00:00 vs 서버: 2020년 11월 10일 10:00:00
  • 304 Not Modified, 헤더 데이터만 전송한다. (body X)
  • 따라서 전송 용량 0.1MB (헤더 0.1MB)

데이터 변경 예시

  • 캐시: 2020년 11월 10일 10:00:00 vs 서버: 2020년 11월 10일 11:00:00
  • 200 OK, 모든 데이터 전송 (body O)
  • 따라서 전송 용량 1.1MB (헤더 0.1MB, 바디 1MB)

 

Last-Modified, If-Modified-Since의 단점

  • 1초 미만 단위로 캐시 조정이 불가능하다. (최근 갱신 일자의 단위가 초까지임)
  • 날짜 기반의 로직을 사용해야 한다

 

따라서 데이터를 수정해서 날짜가 달라졌지만, 실질적으로 같은 데이터를 수정해 결과가 똑같은 경우, 서버에서 별도의 캐시 로직을 관리하고 싶은 경우 예) 스페이스나 주석처럼 크게 영향이 없는 변경에서 캐시를 유지하고 싶은 경우

사용을 해야 한다.

 

ETag

ETag (Entity Tag)

  • 캐시용 데이터에 임의의 고유한 버전 이름을 달아두는 구조
    예) ETag: "v1.0", ETag: "a2jiodwjekjl3" , 버전으로 태그를 관리할 수도 있고, Hash 값으로 할 수도 있다.
  • 데이터가 변경되면, 이 이름을 바꾸는 것을 포함해 변경한다. (Hash를 다시 생성한다)
    예) ETag: "aaaaa" -> ETag: "bbbbb"
  • 단순하게 ETag만 보내서 깂이 같으면 유지하고, 다르면 다시 받는다.
  • If-Match <-> If-None-Match

클라이언트가 서버에 데이터를 요청한다.

서버는 ETag와 함께 데이터를 클라이언트에 보내준다.

 

응답 결과를 브라우저 캐시에 저장한다.

 

ETag - 캐시 시간 초과

설정하였던 캐시가 시간 초과가 될 경우, if-None-Match: ETag 로 서버에 데이터가 아직 변함 없는지 확인 요청을 보낸다.

 

데이터가 아직 수정되지 않았다면 304 Not Modified 상태 코드를 보낸다.

(만약 데이터가 수정되었다면, 200 OK 상태코드를 받게되고, 서버로부터 데이터를 다운로드 받는다. Body는 당연히 있다.)

이때 HTTP Body가 없이 헤더만 보낸다.

 

서버가 보낸 헤더를 클라이언트가 받고, 클라이언트는 브라우저 캐시에 있는 데이터를 재사용하게 되고 헤더 데이터를 갱신한다.

304 상태코드는 리다이렉트이다.

따라서 캐시된 페이지로 리다이렉트 된다.

 

단순하게 ETag만 서버에 보내서 같으면 유지하고, 다르면 다시 받는다.

캐시 제어 로직을 서버에서 완전하게 관리한다.

클라이언트는 단순히 이 값을 서버에 제공(클라이언트는 캐시 메커니즘을 모름)

예시)
서버는 배타 오픈 기간인 3일 동안 파일이 변경되어도, ETag를 동일하게 유지한다.
애플리케이션 배포 주기에 맞춰 ETag를 모두 갱신

이렇게 ETag와 If-None-Match를 사용할시, 클라이언트의 입장에서는 캐시 메커니즘이 완전히 블랙박스가 되어버린다.

 

프록시 캐시

클라이언트와 거리가 먼 서버로 요청과 응답은 당연히 느릴 수 밖에 없다.

이를 극복하기 위해 프록시 캐시 서버를 사용한다.

proxy cache라는 서버를 도입해서 브라우저(클라이언트의)가 원(origin) 서버가 아닌 proxy cache 서버를 거쳐오도록 만든다.

 

DNS에 요청을 하면 원 서버로 바로 가는 것이 아닌 프록시 서버로 요청을 1차적으로 보내게 되는 것이다.

원 서버는 미국에 있지만, 프록시 서버는 한국 아니면 미국보다 가까운 곳에 위치하기 때문에 훨씬 빠르다.

  • private cache: 내 로컬 환경이나 브라우저에 저장되는 캐시
  • public cache: 여러 유저가 공용해서 사용하는 캐시
CDN Contents Delivery Network 콘텐츠 전송 네트워크

지리적으로 분산된 여러개의 서버 네트워크를 두어  병목 현상을 방지하고 효율적인 네트워크 이동을 제공한다.
자주 사용하는 파일들을 Caching 하여 웹 서버의 부하를 줄인다.
Streaming 기술을 제공하여 실시간으로 많은 사용자가 원하는 콘텐츠를 전송해준다.
서버의 트래픽을 덜어주어 비용을 감소하는 효과가 있고, 공격 트래픽을 완화할 수 있어 Dos 공격에 대해서 어느정도 보호해줄 수 있다.

 

캐시와 조건부 요청 헤더

캐시의 제어와 관련된 헤더들은 아래와 같다.

  • Cache-Control: 캐시 제어
  • Pragma: 캐시 제어(하위 호환)
  • Expires: 캐시 유효 기간(하위 호환)

 

Cache-Control - 캐시 지시어(directives)

  • Cache-Control: max-age - 캐시 유효 시간, 초 단위로 작성
  • Cache-Control: no-cache - 데이터는 캐시해도 되지만, 항상 프록시 서버가 아닌 원(origin) 서버에 검증하고 사용
    (로컬 캐시에 있는 정보가 바뀌었는지 여부를 항상 서버에 검증을 받고 사용)
  • Cache-Control: no-store - 데이터에 민감한 정보가 있으므로 저장하면 안될때 사용.
    (메모리에서 사용하고 최대한 빨리 삭제)
  • Cache-Control: public : 응답이 public 캐시에 저장되어도 됨
  • Cache-Control: private : 응답이 해당 사용자만을 위한 것임, private 캐시에 저장해야 함(기본값)
  • Cache-Control: s-maxage : 프록시 캐시에만 적용되는 max-age
  • Age: 60 (HTTP 헤더) : 오리진 서버에서 응답 후 프록시 캐시 내에 머문 시간(초)

 

Pragma - 캐시 제어(하위 호환)

  • Pragma: no-cache - Cashe-Control의 no-cach와 기능 같음
  • HTTP 1.0 이하 버전일 때 사용

 

Expires - 캐시 만료일 지정(하위 호환)

  • expires: Mon, 01 Jan 1990 00:00:00 GMT
  • 캐시 만료일을 정확한 날짜로 지정할 수 있다.
  • HTTP 1.0부터 사용됨, 지금은 더 유연한 Cache-Control: max-age를 권장
  • 현재 버전에서 Cache-Control: max-age와 함께 사용하면 Expires는 무시된다.

 

캐시 무효화

캐시 데이터를 사용하면 안될 상황이 있다. 예를 들어 나의 통장 잔고 조회등이 있다.

따라서 캐시를 무효화할 필요가 있다.

이때 아래와 같이 캐시 무효화를 해주는 헤더 정보를 입력하면 캐시를 무효화할 수 있다.

  • Cache-Control: no-cache, no-store, must-revalidate 
  • Pragma: no-cache -> http 1.0 이하를 위해

 

  • Cache-Control: no-cache : 데이터는 캐시해도 되지만, 항상 원 서버에 검증하고 사용(이름에 주의!)
  • Cache-Control: no-store : 데이터에 민감한 정보가 있으므로 저장하면 안됨
    (메모리에서 사용하고 최대한 빨리 삭제)
  • Cache-Control: must-revalidate : 캐시 만료후 최초 조회시 원 서버에 검증해야함
    원 서버 접근 실패시 반드시 오류가 발생해야함 - 504(Gateway Timeout)
    캐시 유효 시간이라면 캐시를 사용함
  • Pragma: no-cache : HTTP 1.0 하위 호환

 

no-cache vs must-revalidate

no-cache는 프록시 캐시를 거쳐 원 서버에 검증을 요청하게 된다.

 

하지만 프록시 캐시와 원 서버가 통신 상태가 불량하다면, 오류를 발생하는 것이 아닌 프록시 캐시에 있는 캐시를 클라이언트로 응답하게 된다.

 

반면에 must-revalidate 는 프록시 캐시와 원 서버의 통신이 불량하다면 오류를 발생시킨다.

 

'네트워크 > HTTP' 카테고리의 다른 글

[HTTP] 일반 헤더  (1) 2024.02.11
[HTTP] 상태코드  (1) 2024.02.10
[HTTP] HTTP 메서드 활용  (1) 2024.02.09
[HTTP] HTTP 메서드  (1) 2024.02.08
[HTTP] HTTP 기본  (1) 2024.02.07

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


HTTP 헤더 개요

HTTP 전송에 필요한 메시지 바디의 내용, 메시지 바디의 크기, 압축, 인증, 요청 클라이언트, 서버 정보, 캐시 관리 등 모든 부가 정보를 헤더에 넣는다. 표준 헤더가 굉장히 많다. 

필요시 임의의 헤더 추가가 가능하다. 

 

RFC2616

Header

  • General 헤더: 요청/응답 메시지 전체에 적용되는 정보 (Connection: close 등)
  • Request 헤더: 요청 정보 (User-Agent: Mozilla/5.0  등)
  • Response 헤더: 응답 정보 (Server: Apache  등)
  • Entity 헤더: 엔티티 바디 정보 (Content-Type: text/html, Content-Length: 3423  등)

 

BODY

메시지 본문을 통해 엔티티 본문을 전달 하는데 사용한다.

엔티티 헤더는 엔티티 본문의 데이터를 해석할 수 있는 데이터 유형, 데이터 길이, 압축 정보 등을 제공한다. 

 

표현(Representation) - RFC7230(최신)

  1. 1999년 RFC2616 폐기
  2. 2014년 RFC7230~7235 등장
  3. 엔티티(Entity) -> 표현(Representation)

 

HTTP BODY

  • 메시지 본문(message body)를 통해 표현 데이터 전달
  • 메시지 본문 = 페이로드(payload)
  • 표현 = 요청이나 응답에서 전달할 실제 데이터(표현 헤더 + 표현 데이터)
  • 표현 헤더는 표현 데이터를 해석할 수 있는 정보 제공

 

HTTP HEADER

Content-Type

  • Content-Type: 표현 데이터의 형식
  • Content-Encoding: 표현데이터의 압축 방식
  • Content-Language: 표현 데이터의 자연 언어
  • Content-Length: 표현 데이터의 길이

 

콘텐츠 협상

클라이언트가 선호하는 표현 요청

  • Accept : 클라이언트가 선호하는 미디어 타입 전달
  • Accept-Charset : 클라이언트가 선호하는 문자 인코딩
  • Accept-Encoding : 클라이언트가 선호하는 압축 인코딩
  • Accept-Language : 클라이언트가 선호하는 자연 언어

협상 헤더는 요청시에만 사용

 

Accept-Language 적용 전

클라이언트에서 서버로 요청을 보낼 때 한국어 정보를 원하는지 영어 정보를 원하는지 아무 정보가 없다.

그러면 서버는 기본값인 영어 관련된 내용으로 한국어 브라우저로 응답을 해준다. 

 

Accept-Language 적용 후

클라이언트가 선호하는 언어에 한국어 정보를 입력해서 서버에게 전달한다.

서버는 기본 언어가 영어지만 한국어도 지원하기 때문에 클라이언트가 원하는 한국어로 데이터를 전달한다.

 

Accept-Language  복잡한 예시

클라이언트가 선호하는 언어를 한국어 정보를 입력해서 서버에게 전달하는데, 서버가 기본은 독일어, 또 다른 언어로 영어만 지원한다.

서버가 한국어 지원을 하지 않아서 기본값인 독일어로 보내게 된다. 

 

협상과 우선순위

Quality Values(q) 값으로 사용한다. 0~1로 값이 클수록 우선순위가 높고 1은 생략이 가능하다.

GET /event
Accept-Language:ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
  1. ko-KR;q=1
  2. ko;q=0.9
  3. en-US;q=0.8
  4. en;q=0.7

위에 표현된 값으로 우선순위를 파악해서 어떤 언어를 클라이언트가 선호하는지 알 수 있다.

위 예시에서는 독일어보다 영어가 우선순위가 높기 때문에 영어로된 데이터를 전송하게 된다.


구체적일 수록 우선순위가 높다. 

GET /event
Accept: text/*text/plaintext/plain;format=flowed*/*
  1. text/plain;format=flowed
  2. text/plain
  3. text/*
  4. */*

구체적인 것을 기준으로 미디어 타입을 맞춘다.

클라이언트에서 요청한 text/html;level의 우선순위가 가장 높고 서버가 해당 미디어 타입을 지원하므로 text/html;level 미디어타입으로 응답한다.

 

전송 방식

단순 전송 (Content-Length)

데이터 전체를 한 번에 보낼 때 사용

 

압축 전송 (Content-Encoding)

전송해야하는 데이터가 커서 압축해서 보낼 때 사용(압축 방식은 다양함)

 

분할 전송 (Transfer-Encoding)

chunked는 덩어리라는 뜻이다.

덩어리로 쪼개서 전송을 한다.

5byte로 Hello를 서버에서 클라이언트로 보낸다.

또 5byte로 World를 보내고 마지막으로 0byte로 src를 보내면 끝이라는 걸 표현한다.

분할 전송할 때는 Content-Length를 넣으면 안된다.

 

범위 전송 (Content-Range)

Range 설정해서 요청 -> Content-Range 설정해서 응답

  • Range: bytes=클라이언트가 요청한 데이터의 범위
  • Content-Range: bytes 클라이언트가 요청한 데이터의 범위 / 전체 데이터의 길이
  •  Content-Length: 실제 전송된 데이터의 길이

어떠한 이유로 중간에 재요청해야할 때, 범위를 지정하여 사용

처음부터 다시 받지 않고, 이후 부분부터 받는다.

 

일반 정보

From

유저 에이전트의 이메일 정보이다. 일반적으로 잘 사용하지 않는다. 검색 엔진 담당자한테 사이트에 대해서 정보를 알려줄 때 연락할 수 있는 방법이 필요할 때 요청할 때 사용한다.

 

Referer(요청)

현재 요청된 페이지의 이전 웹 페이지 주소이다. A에서 B로 이동하는 경우 B를 요청할 때 Referer A를 포함해서 요청한다.

Referer를 사용해서 데이터 분석 할 때 유입 경로 분석을 가능하다. 

referer는 referrer의 오타

 

User-Agent(요청)

  • 유저 에이전트(클라이언트) 애플리케이션 정보 (웹 브라우저 정보..)
  • 장애가 발생하는 브라우저 파악, 통계 정보 사용

 

Server (응답)

  • 요청을 처리하는 ORIGIN 서버의 소프트웨어 정보
  • ORIGIN 서버: 실제로 응답을 보낸 서버(HTTP 요청을 보내면, 실제로 많은 프록시 서버를 거쳐 응답을 받게됨)

 

Date (응답)

  • 메시지가 발생한 날짜와 시간

 


특별한 정보

Host(요청)

요청에서 사용하고 필수값이다. 다른 헤더에서는 거의 없고 Host는 필수 헤더이다. 하나의 서버가 여러 도메인을 처리해야 할 때, 하나의 IP 주소에 여러 도메인이 적용되어 있을 때 구분해줘야 한다.

가상호스트를 통해서 하나의 서버가 지금 IP : 200.200.200.2 가 있는데 서버 안에 여러 개의 애플리케이션이 다른 도메인으로 구동되어 있다.

클라이언트가 /hello 로 GET 방식으로 요청을 했는데 서버 입장에서는 /hello 와 관련된 애플리케이션에 어디로 들어갈지 모른다. 

TCP/IP 통신으로 Host 헤더로 입력하면 서버에서 Host 헤더가 있기 때문에 요청에 맞는 도메인주소를 들어갈 수 있다.  

Q. Port와 Host는 비슷한 개념?
A.해당 IP에서 Host를 찾고 -> 그 안에서 Port로 구분

 

Location (응답)

페이지 리다이렉션

  • 3xx(Redirection)의 Location 값: 요청을 자동으로 리다렉션하기 위한 대상 리소스(이동할 위치)
  • 201(Created)의 Location 값: 요청에 의해 생성된 리소스의 URI

 

Allow (응답)

허용 가능한 HTTP 메서드

  • 405(Method Not Allowed) 에서 응답에 포함해야 함

 

Retry-After

유저 에이전트가 다음 요청을 하기까지 기다려야 하는 시간

  • 503 (Service Unavailable): 서비스가 언제까지 불가인지 알려줄 수 있음
  • 날짜, 초단위 표기

 

 

쿠키

 
  • Set-Cookie: 서버->클라이언트 쿠키 전달(응답)
  • Cookie: 클라이언트가 서버에서 받은 쿠키를 저장, 클라이언트->서버 쿠키 전달(요청)

 

로그인을 하면 서버에서는 Set-Cookie로 홍길동 정보를 쿠키로 만들어서 보내준다.

클라이언트의 웹 브라우저 내부에는 쿠키 저장소가 있는데 서버가 응답에서 만든 쿠키를 쿠키 저장소에 저장을 한다.

 

로그인 이후에 웹 브라우저가 /welcome에 들어오면 자동으로 웹 브라우저는 쿠키를 담아 서버에 요청한다.

 

지정한 서버 쿠키는 모든 요청 정보에 쿠키 정보를 자동으로 포함을 한다. 

 

  1. 웹 브라우저가 id, password를 담아 서버에 로그인을 요청함
  2. 서버는 id, password 검증에 성공하면, 해당 사용자에 대한 sessionId(쿠키)를 생성함
  3. 서버는 Set-Cookie에 sessionId를 담아 웹 브라우저에 로그인 성공을  응답함
  4. 웹 브라우저는 쿠키 저장소에 sessionId(쿠키)를 저장함
  5. 이후 웹 브라우저가 쿠키 접근가능한 도메인에 요청을 보낼 때마다 자동으로 쿠키 저장소에서 꺼낸 sessionId(쿠키)를 Cookie에 담아 서버에 요청을 보냄
  6. 서버는 sessionId(쿠키)의 유효성을 검사해 클라이언트를 식별함

 

쿠키 정보는 항상 서버에 전송된다.
-> 네트워크 트래픽 추가 유발
-> 최소한의 정보만 사용(세션 id, 인증 토큰)

보안에 민감한 데이터는 쿠키/웹스토리지에 저장하면 안된다.

 

생명주기

Set-Cookie: expires=Sat, 26-Dec-2020 04:39:21 GMT
  • expires : 쿠키를 무제한으로 보관할 수 없다. GMT기준으로 만료일이 되면 쿠키를 자동으로 삭제한다. 

 

Set-Cookie: max-age=3600
  • max-age :  초 단위로 구성되어 있고 0이나 음수를 지정하면 쿠키가 삭제한다. 

 

종류

  • 세션 쿠키 : 만료 날짜를 생략하면 브라우저가 종료할 때까지 유지
  • 영속 쿠키 : 만료 날짜를 입력하면 해당 날짜까지 유지(브라우저가 종료되어도 클라이언트 시간이 남아있는 한 유지)

 

도메인

  • 명시 : 도메인을 명시를 하면 명시한 문서 기준 도메인과 서브 도메인을 포함을 해서 다 전송
    - example.org 지정을 해서 쿠키를 생성하면 dev.example.org도 쿠키가 같이 접근할 수 있다.
  • 생략 :현재 문서 기준 도메인만 적용
    - example.org에서는 쿠키로 접근할 수 있고, dev.example.org는 쿠키로 접근할 수 없다.

 

경로

경로를 포함한 하위 경로 페이지만 쿠키를 접근 할 수 있다. 일반적으로 path=/ 루트로 지정한다

path=/home 지정

  • /home ->  가능
  • /home/level1 ->  가능
  • /home/level1/level2 ->  가능
  • /hello ->  불가능

 

보안

Secure, HttpOnly, SameSite

Secure

  • https인 경우메만 쿠키를 전송
  • 쿠키는 원래 http, https 를 구분하지 않고 쿠키를 서버에 전송함

 

HttpOnly

  • XSS 공격 방지 목적
  • 자바스크립트에서 쿠키 접근X(document.cookie), HTTP 전송에서만 쿠키 사용

 

SameSite

  • XSRF 공격 방지 목적
  • 요청 도메인과 쿠키에 설정된 도메인이 같은 경우만 쿠키 전송

'네트워크 > HTTP' 카테고리의 다른 글

[HTTP] 캐시와 조건부 요청  (0) 2024.02.12
[HTTP] 상태코드  (1) 2024.02.10
[HTTP] HTTP 메서드 활용  (1) 2024.02.09
[HTTP] HTTP 메서드  (1) 2024.02.08
[HTTP] HTTP 기본  (1) 2024.02.07

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


HTTP 상태코드

상태코드

1xx (Informational) : 요청이 수신되어 처리중  ->  거의 사용이 되지 않음
2xx (Successful) : 요청 정상 처리
3xx (Redirection) : 요청을 완료하려면 추가 행동이 필요
4xx (Client Error) : 클라이언트 오류, 잘못된 문법등으로 서버가 요청을 수행할 수 없음
5xx (Server Error) : 서버 오류, 서버가 정상 요청을 처리하지 못함

HTTP 상태 코드는 클라이언트가 서버로 요청을 보내면 요청이 잘 처리가 되어있는지 문제가 있는지 요청의 처리 상태를 응답에서 알려주는 기능이다. 

 

만약 모르는 상태코드가 나타나면

클라이언트가 인식할 수 없는 상태코드를 서버가 반환하면 클라이언트는 상위 상태코드로 해석해서 처리하면 된다.

미래에 새로운 상태 코드가 추가되어도 클라이언트를 변경하지 않아도 된다.

299  -> 2xx (Successful)
451  ->  4xx (Client Error)
599  ->  5xx (Server Error)

 

100번대 - 정보

요청이 수신되어 처리 중이다. 거의 사용하지 않는다.

 

200번대 - 성공

200 OK 

  1. 클라이언트가 서버에  GET으로 리소스를 요청함
  2. 서버가 리소스를 조회함
  3. Stauts code: 200 과 리소스를 HTTP 응답 메시지를 만들어 클라이언트에 보냄

 

201 Created

요청 성공 후 새로운 리소스 생성됨

  1. 클라이언트가 서버에  POST로 신규 자원을 등록 요청함
  2. 서버가 리소스를 생성하고 리소스의 URI를 관리함
  3. Stauts code: 201, Location: 생성된 리소스의 URI 로 HTTP 응답 메시지를 만들어 클라이언트에 보냄

 

202 Accepted

요청이 접수되었으나 처리가 완료되지 않은 상태코드이다.

작업을 미뤄야할 때나 특수한 경우에 사용한다.

잘 사용하지 않는다. 

 

204 No Content

서버가 요청을 성공적으로 수행했지만 응답 페이로드 본문에 데이터가 없는 상태코드이다.

말 그대로 요청을 받고 정상적으로 수행을 했는데 보내줄 값, 함수로 따지면 return 할 값이 없다는 의미이다.

 

300번대 - 리다이렉션

300 : Multiple Choices
301 : Moved Permanently
302 : Found
303 : See Other
304 : Not Modified
307 : Temporary Redirect
308 : Permanent Redirect

 

리다이렉션 이해

웹 브라우저는 300번대 응답의 결과에 Location 헤더가 있으면 Location 위치로 자동으로 이동한다.

 

기존 /event 이벤트 페이지에서 /new-event 라는 페이지를 쓰기로 변경했다면 기존 경로를 북마크를 해둘 수 있고 기존의 링크가 여러 군데로 공유가 될 수 있다.

 

클라이언트가 /event 로 요청하면 서버 입장에서는 /event 를 더이상 쓰지 않으니까 /new-event 를 클라이언트에게 알려줄 때 301 상태코드로 알려준다.

 

 

클라이언트는 301 상태코드를 보고 /new-event 를 URL 서버에게 다시 요청을 한다.

 

정리를 하면, 기존의 경로로 요청을 하면 서버는 새로 만들어진 경로를 알려주게되고, 서버에게 받은 새로운 경로로 다시 요청을 하게 된다.

 

리다이렉션의 종류

1) 영구 리다이렉션: 특정 리소스의 URI가 영구적으로 이동
-> 이벤트 페이지 URI가 바뀌어 더이상 사용하면 안되거나 내부적으로 URI 경로가 아예 바뀐 경우
- /members -> /users
- /event -> /new-event

2) 일시 리다이렉션: 일시적인 변경
- 주문 완료 후 주문 내역 화면으로 이동
- PRG: Post/Redirect/Get

3) 특수 리다이렉션: 결과 대신 캐시를 사용

 

영구 리다이렉션

영구 리다이렉션은 상태코드가 301, 308이 있다.

리소스의 URI가 영구적으로 이동했다는 것 알려준다. 원래의 URI를 더 이상 하면 안된다.

검색 엔진들이 원래의 URI로 들어올 수 있지만 검색 엔진에서도 변경을 인지한다. 

 

301 Moved Permanently

리다이렉트시 요청 메서드가 GET으로 변하고, 본문이 제거될 수 있음(MAY)

 

  1. 클라이언트가 메세지 바디에 리소스를 담아 POST 방식으로 /event 를 서버에 요청하면
  2. 서버는 메세지 바디를 제거하고 301 코드와 Location에 /new-event를 담아 응답한다.
  3. 웹 브라우저의 URL이 자동 리다이렉트 되고
  4. 클라이언트는 메시지를 제거하고 GET방식으로 /new-event 를 서버에 요청한다.
  5. 서버는 200 코드와 /new-event 의 리소스를 응답한다.
  6. 이벤트 페이지에서 회원 정보를 등록하려고 했으나, 메시지 바디가 제거된 채로 자동 리다이렉트 되어 입력한 정보가 날라가서 처음부터 회원 정보를 다시 입력해서 등록해야 한다.

이러한 불편함 때문에 308이 나왔다.

 

308 Permanet Redirect

리다이렉트시 요청 메서드와 본문 유지

  • 클라이언트가 메세지 바디에 리소스를 담아 POST방식으로 /event 를 서버에 요청하면
  • 서버는 메세지 바디를 제거하고 308 코드와 Location에 /new-event를 담아 응답한다.
  • 웹 브라우저의 URL이 자동 리다이렉트 되고 클라이언트는 메시지를 유지해서 POST방식으로 /new-event 를 서버에 요청한다.
  • 서버는 200 코드와 /new-event 의 리소스를 응답한다.
    -> 이벤트 페이지에서 회원 정보를 등록하면 자동 리다이렉트 되어 입력한 정보가 유지되어 회원 정보가 정상 등록된다.

 

실무에서는 308을 거의 사용하지 않는다고 한다.

만약 /event 에서 /new-event로 URI가 바뀌면 내부적으로 전달해야하는 데이터 자체가 바뀌기 때문에, POST로 요청을 받아도 리다이렉트시 GET으로 바꾼다고 한다.

 

일시적인 리다이렉션

리소스의 URI를 일시적으로 변경한다.

검색 엔진 등에서 URI 변경하면 안될 때 사용한다.

 

302 Found

리다이렉트 시 요청 메서드가 GET으로 변하고 본문이 제거될 수 있다.

프레임워크나 기술 레벨에서 보면 라이브러리들이 기본값으로 302를 많이 쓰인다. 

 

307 Temporary Redirect

리다이렉트시 요청 메서드와 본문이 유지된다. 요청 메서드를 변경하면 안된다. 

 

303 See Other

리다이렉트 시 요청 메서드가 GET으로 변한다. 

 

302, 303, 307 정리

  • 302 - 메서드가 GET으로 변할 수 있음
  • 307 - 메서드가 변하면 안됨
  • 303 - 메서드가 GET으로 변경됨

현실적으로 307, 303을 권장하지만 이미 많은 애플리케이션 라이브러리들이 302를 기본값으로 사용한다.

자동 리다이렉션 시에 GET으로 변해도 되면 302를 사용해도 큰 문제가 없다.

 

PRG (Post/Redirect/Get)

POST로 주문 후에 웹 브라우저를 새로고침하면 중복 주문이 될 수 있다. 이러한 경우, PRG를 사용한다.

  • POST로 주문 후에 주문 결과 화면으로 리다이렉트
  • 새로고침해도 결과화면을 GET으로 서버에 요청
  • 중복 주문 대신에 결과 화면만 GET으로 다시 요청
새로고침(Refresh)는 마지막에 요청한 request를 재요청한다.

 

PRG 사용 전

1. 클라이언트가 마우스 1개를 POST로 주문 요청한다.
2. 서버는 주문 데이터(마우스 1개)를 DB에 저장한다.
3. 서버가 200 코드와 주문완료 html 리소스를 클라이언트에 응답한다.
4. 결과 화면을 새로고침하면 마지막 POST 요청을 새로고침 하게 된다.
5. 클라이언트가 마우스 1개를  POST로 중복 주문 요청을 한다.
6. 서버는 주문 데이터(마우스 1개)를 DB에 중복 저장한다.
7.  서버가 200 코드와 주문완료 html 리소스를 클라이언트에 응답한다.
-> 서버차원에서 중복 주문 방지: "잘못된 주문코드번호입니다"같은 응답으로 중복 주문을 방지해야 함

 

PRG 사용 후

클라이언트차원에서 중복 주문을 방지한다는 개념이다.

주문 요청을 할 때는 POST 메소드를 사용 하고 주문 완료창으로 리다이렉트 시킨다.

주문 완료창은 GET 메소드를 사용한다.

따라서 새로고침해도 GET으로 결과 화면만 조회한다.

 

기타 리다이렉션

300 Multiple Choice

안 쓴다.

 

304 Not Modified - 캐시 목적

클라이언트에게 리소스가 수정되지 않았음을 알려줌 -> 클라이언트는 로컬PC에 저장된 캐시 재사용(캐시로 리다이렉트

  • 응답에 메시지 바디를 포함하면 안됨(로컬 캐시를 사용해야하기 때문)
  • 조건부 GET, HEAD 요청 시 사용

 

400번대 - 클라이언트 오류

클라이언트의 요청에 잘못된 문법 등으로 서버가 요청을 수행할 수 없는 상태코드이다.

오류의 원인이 클라이언트에 있다.

클라이언트가 이미 잘못된 요청을 데이터를 보내고 있기 때문에 똑같이 재시도하면 실패한다. 

 

400 Bad Request

클라이언트가 잘못된 요청을 해서 서버가 요청을 처리할 수 없음

  • 요청 구문, 메시지 등 오류
  • 클라이언트는 요청 내용을 재검토 후 보내야 함
  • 요청 파라미터가 잘못되거나, API 스펙이 맞지 않을 때

 

401 Unauthorized 

클라이언트가 해당 리소스에 대한 인증이 필요하다.

오류 발생시 응답에 WWW-Authenticate라는 헤더와 함께 인증하는 방법을 설명을 해줘야 한다. 

인증(Authentication) : 본인이 누구인지 확인하는 과정
인가(Authorization) : 특정 리소스에 접근할 수 있는 권한이 있는 사람만 볼 수 있는 권한 부여
오류 메시지가 Unauthorized이지만 실제로는 인증이 되지않아 생긴 오류이다.

 

403 Forbidden

클라이언트가 보낸 요청을 서버가 이해했지만 승인을 거부하는 상태코드이다.

주로 인증 자격 증명은 있지만 접근 권한이 없을 때 볼 수 있다.

어드민 등급이 아닌 사용자가 다른 리소스로 로그인은 했지만 어드민 등급의 리소스에 접근할 때 오류가 나타난다.

 

404 Not Found

요청 리소스를 찾을 수 없는 상태코드이다.

클라이언트가 권한이 부족한 리소스에 접근할 때, 해당 리소스를 숨기고 싶을 때 사용할 수도 있다.

 

500번대 - 서버 오류

서버 오류

  • 오류의 원인 = 서버 ( NullPointerException, DB 접근 불가 등)
  • 서버에 문제가 있으므로 복구된 뒤 재시도하면 성공할 수도 있음

왠만하면 5xx 오류는 내면 안된다. 심각한 서버 오류가 있을 때만 500번대 오류를 내야 한다.

 

500 Internal Server Error

서버 내부 문제로 오류 발생한 상태코드이다.

애매한 오류는 500 상태코드이다.

 

503 Service Unavaliable

서버가 일시적인 과부하 되거나 예정된 작업으로 잠시 요청을 처리할 수 없는 것을 나타냄

Retry-After 헤더로 얼마 뒤에 복구되는지 예상 시간을 보낼수 있다.

'네트워크 > HTTP' 카테고리의 다른 글

[HTTP] 캐시와 조건부 요청  (0) 2024.02.12
[HTTP] 일반 헤더  (1) 2024.02.11
[HTTP] HTTP 메서드 활용  (1) 2024.02.09
[HTTP] HTTP 메서드  (1) 2024.02.08
[HTTP] HTTP 기본  (1) 2024.02.07

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


클라이언트에서 서버로 데이터 전송

데이터 전달 방식

쿼리 파라미터를 통한 데이터 전송

주로 GET 방식으로 많이 사용하고 검색어로 검색할 때, 게시판 리스트에 정렬 조건을 넣을 때 쿼리 파라미터를 이용해서 많이 사용한다.

 

메시지 바디를 통한 데이터 전송

클라이언트에서 서버로 전송할 때 HTTP 메시지 바디를 통해서 데이터를 전송한다.

POST, PUT, PATCH 방식으로 주로 사용한다.

회원 가입, 상품 주문, 리소스 등록, 리소스 변경 등에 사용된다.

 

클라이언트에서 서버로 데이터 전송할 때 4가지 상황

  • 정적 데이터 조회 : 이미지, 정적 텍스트 문서
  • 동적 데이터 조회 : 주로 검색, 게시판 목록에서 정렬 필터
  • HTML Form을 통한 데이터 전송 : 회원가입, 상품 주문, 데이터 변경
  • HTTP API를 통한 데이터 전송 : 회원 가입, 상품 주문, 데이터 변경, 서버 to 서버, 앱 클라이언트 웹 클라이언트(Ajax)

 

데이터 조회

정적 데이터 조회 - 쿼리 파라미터 미사용

클라이언트에서 /static/star.jpg 인 경로만 입력하면 서버에서 star 이미지를 클라이언트에게 응답해준다.

정적 데이터를 조회할 때는 이미지나 정적 텍스트 문서를 조회하기 때문에 GET으로 사용한다.

일반적으로 정적 데이터는 쿼리 파라미터 없이 단순한 리소스 경로로 조회가 가능하다.

 

동적 데이터 조회 - 쿼리 파라미터 사용

검색할 때 검색어나 추가 조건을 데이터를 전달 할 때 쿼리 파라미터들을 사용해서 서버에게 요청한다.

쿼리 파라미터를 사용하기에 서버에서 key와 value를 볼 수 있다. 이에 대한 결과를 클라이언트에게 응답한다.

주로 검색을 하거나 게시판 목록에서 정렬하거나 필터할 때 추가 데이터들이 쿼리 파라미터로 요청한다.

조회이기 때문에 GET 방식으로 이용해서 쿼리 파라미터를 사용해서 데이터를 클라이언트에서 서버로 전달한다.

동적 데이터는 쿼리 파라미터를 사용해서 조회 가능하다.

 

HTML Form을 통한 데이터 전송

POST 전송의 저장

post method로 된 Form 태그의 submit 버튼을 누르면, 웹 브라우저가 HTML Form의 데이터를 읽어서 HTTP 요청 메시지를 생성해준다.

key - value 형태의 데이터를 HTTP 바디 메시지에 넣고 서버에 전송을 한다.

서버에서 POST 메시지를 받으면 데이터를 저장을 한다. 

htnl form 태그를 사용하여 post 방식으로 요청하거나, ajax 등의 요청을 할때 default Content-Type은 application/json이 아니라 application/x-www-form-urlencoded 이다.

전송 데이터를 url encoding 으로처리한다.

 

GET 전송의 저장

form을 통해서 데이터 전송할 때 GET 메서드로 변경하면 GET은 메시지 바디를 사용하지 않으므로 쿼리 파라미터 형식으로 서버에 전달한다.

리소스를 변경할 때 GET 메서드를 사용하면 안된다. GET은 조회할 때 사용한다

 

GET전송의 조회

get method로 된 Form 태그의 submit 버튼을 누르면, 웹 브라우저가 HTML Form의 데이터를 읽어서 HTTP 요청 메시지를 생성해준다.

쿼리 파라미터에 key-value 형태로 form 의 데이터를 넣어서 전송한다.

서버가 GET 메시지를 받으면 데이터를 필터링(조회)한 결과를 응답한다.

url에 key-value가 노출된다.

 

multipart/form-data

multipart/form-data는 파일(binary data)을 전송할 때 쓰는 인코딩 타입이다.

Content-type은 multipart/form-data로 지정되고 boundary는 웹으로 자동 생성해서 경계로 나눠진다.

HTML Form 태그의 enctype="multipart/form-data"로 설정하고 submit 버튼을 누르면 웹 브라우저가 username, age, file1 데이터를 분리하여 message-body에 담아 HTTP 요청 메시지를 생성해준다.

 

HTML Form을 통한 데이터 전송 정리

HTML Form submit 시 POST 전송

- 회원 가입, 상품 주문, 데이터 변경
▶ Content-Type: application/x-www-urlencoded 사용
form의 내용을 message-body에 담아 전송(key=value, 쿼리 파라미터 형식)
전송 데이터를 url encoding 처리
e.g. abc김 → abc%EA%B9%80

▶ Content-Type: multipart/form-data 사용
파일 업로드 같은 바이너리 데이터 전송시 사용
다른 종류의 여러 파일과 폼의 내용 함께 전송O 2) HTML Form submit 시 GET 전송
form의 내용을 쿼리 파라미터에 담아 전송

HTML Form 전송은 GET, POST 만 지원한다 다른 메서드들은 자바스크립트의 이벤트 핸들러를 통해 처리해야 한다.

 

HTTP API 데이터 전송

  • 서버 to 서버 : 백엔드 서버끼리 통신
  • 앱 클라이언트 : 아이폰, 안드로이드
  • 웹 클라이언트 : HTML Form이 아니라 자바스크립트를 통해서 Ajax로 통신
  • HTTP API는 메시지 바디를 통해 POST, PUT, PATCH로 데이터 전송 
  • GET도 마찬가지로 HTTP API로 쓸수 있지만 조회할 때는 항상 쿼리 파라미터 형식으로 전달해야 된다. 
  • Content-Type 으로 application/json을 주로 사용(TEXT, XML, JSON)
Form 태그에서 POST와 HTTP API에서의 POST
form 태그 - default Content-Type: application/x-www-form-urlencoded
HTTP API - default Content-Type: application/json

 

HTTP API 설계

<HTTP API 설계 예시 3가지>

POST 기반으로 등록, PUT기반으로 등록하는 2가지 경우의 특징을 아는 것이 중요
대부분 POST 기반 신규 자원 등록 방법(컬렉션)을 많이 사용한다.

1) HTTP API - 컬렉션 : POST 기반 등록
e.g. 회원 관리 API 제공

2) HTTP API - 스토어 : PUT 기반 등록
e.g. 정적 컨텐츠 관리, 원격 파일 관리

3) HTML FORM 사용 : 웹 페이지 회원 관리
- GET, POST 만 지원

POST 기반 등록

컬렉션 = /members

회원 관리 시스템
1. 회원 목록 : /members  ->  GET

2. 회원 등록 : /members  ->  POST
3. 회원 조회 : /members/{id}  ->   GET
4. 회원 수정 : /members/{id}  ->   PATCH, PUT, POST
5. 회원 삭제 : /members/{id}  ->   DELETE 
  • 클라이언트는 등록될 리소스의 URI를 모른다.
    서버가 알아서 회원을 식별해서 URI를 만들어준다.
회원 등록 : /members  -> POST
POST /memebers
  • 클라이언트가 결정하는 게 아니라 서버가 새롭게 등록된 리소스의 URI를 생성한다. 
HTTP/1.1 201 Created
Location : /members/100
  • 컬렉션(Collection)은 서버가 관리하는 리소스 디렉토리이다. 리소스의 URI를 생성하고 관리한다. 
/members
회원 수정은 PATCH, PUT, POST 중 무엇으로 구현해야 하나?

1) 개념적으로는 리소스 부분 변경인 PATCH로 하는 것이 가장 좋다.
2) 기존 리소스를 덮어써도 문제가 없는 경우, PUT을 사용할 수는 있지만 그런 경우는 거의 없다. 클라이언트에서 회원의 모든 데이터(id, 이름, email 등)을  다 보내야하기 때문이다.
- 게시글 수정같은 경우, 완전히 덮어써도 문제X
3) 애매한 경우, POST를 쓰면 된다.

 

PUT 기반 등록

스토어 = /files

파일 관리 시스템
1. 파일 목록 : /files  ->  GET
2. 파일 조회 : /files/{filename}  ->   GET
3. 파일 등록 : /files/{filename}  ->   PUT
4. 파일 삭제 : /files/{filename}  ->   DELETE
5. 파일 대량 등록 : /files  ->   POST 
  • 클라이언트가 리소스 URI를 알고 있어야 한다. 클라이언트가 직접 리소스의 URI를 지정해서 생성된 리소스를 관리해야 한다. 
파일 등록 : /file/{filename}  ->  POST
PUT /files/star.jpg
  • 스토어(Store)는 클라이언트가 관리하는 리소스 저장소이다. 
/files

파일 등록같은 경우는 해당 파일이 있으면 기존 것을 덮어써야하기 때문에, PUT을 쓴다.

 

HTML Form 사용

회원 관리 시스템
1. 회원 목록 : /members  ->  GET
2. 회원 등록 폼 : /members/new  ->   GET

3. 회원 등록 : /members/new, /members  ->   POST
4. 회원 조회 : /members/{id}  ->   GET
5. 회원 수정 폼 : /members/{id}/edit  ->   GET
6. 회원 수정 : /members/{id}/edit, /members/{id}  ->   POST
7. 회원 삭제 : /members/{id}/delete  ->   POST
  •  순수한 HTML과 HTML Form은 GET, POST만 지원하기 때문에 제약이 있다.
    -> AJAX 같은 기술을 사용해서 해결
  • 제약을 해결하기 위해 동사로 된 리소스 경로를 사용을 하는데 이걸 컨트롤 URI이라 한다. 
    -> HTML FORM은 GET, POST만 지원하기 때문에 어쩔 수 없이 컨트롤 URI를 써서 /members/{id}/delete
    POST로 설계

/members/new
/members/{id}/edit

/members/{id}delete

 

정리

1) HTTP API - 컬렉션
- POST 기반 등록
- 서버가 리소스 URI 결정

2) HTTP API - 스토어
- PUT 기반 등록
- 클라이언트가 리소스 URI 결정

3) HTML FORM 사용
- 순수 HTML + HTML form 사용
- GET, POST만 지원 → 컨트롤 URI로 해결

 

참고하면 좋은 URI 설계 개념

  • 문서(document) : 파일 하나, 객체 인스턴스, 데이터베이스 row 같은게 단일 개념이다.
/members/100
/files/star.jpg
  • 컬렉션(Collection) : 서버가 관리하는 리소스 디렉토리이다. 클라이언트는 요청만 하고 서버가 리소스의 URI를 생성하고 관리한다.
/members
  • 스토어(Store) : 클라이언트가 관리하는 자원 저장소이다. 클라이언트가 리소스의 URI를 알고 관리한다.
/files
  • 컨트롤러(Controller), 컨트롤 URI : 문서, 컬렉션, 스토어로 해결하기 어려운 추가 프로세스 실행할 때 동사를 직접 사용한다.
/members/{id}/delete

주로 컬렉션을 많이 사용하고, 파일 /게시판 같은 경우에 스토어를 사용할 수 있다.

문서, 컬렉션, 스토어로 해결하기 어려운 추가 프로세스를 실행할 때, 컨트롤 URI를 사용한다.

'네트워크 > HTTP' 카테고리의 다른 글

[HTTP] 일반 헤더  (1) 2024.02.11
[HTTP] 상태코드  (1) 2024.02.10
[HTTP] HTTP 메서드  (1) 2024.02.08
[HTTP] HTTP 기본  (1) 2024.02.07
[HTTP] URI와 웹 브라우저 요청 흐름  (1) 2024.02.06

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


HTTP API 예제

요구사항 및 API URI 설계

회원 정보 관리 API 설계
1. 회원 목록 조회 :  /read-member-list
2. 회원 조회 :  /read-member-by-id
3. 회원 등록 : /create-member
4. 회원 수정 : /update-member
5. 회원 삭제 : /delete-member

요구사항 기반으로 API를 만들게 되는게 위와 같이 현업에서 잘못된 API URI 설계를 한다. 

 

API URI  설계 분리

리소스 : 회원
행위 : 조회, 등록, 수정, 삭제

API URI 설계를 할 때 리소스와 해당 리소스를 대상으로 하는 행위를 분리해야 한다.

회원이라는 리소스만 식별하고 회원 리소스를 URI에 매핑을 하면 된다. 

리소스를 식별하는 것이 가장 중요하다.

 

API URI 재설계

회원 정보 관리 API 재설계
1. 회원 목록 조회 :  /members
2. 회원 조회 :  /members/{id} 
3. 회원 등록 : /members/{id}
4. 회원 수정 : /members/{id}
5. 회원 삭제 : /members/{id}

API URI 재설계를 했지만 행위는 구분이 되지 않는다.

구분하는 방법은 URI 리소스만 식별해 놓으면 HTTP 메서드인 GET, POST, PUT, DELETE 이런 것들이 조회, 등록, 수정, 삭제 역할을 대신해준다

계층 구조상 상위를 컬렉션으로 보고 복수 단어 사용 권장(member  ->  members)


HTTP 메서드 - GET, POST

HTTP 메서드 종류

  • GET : 리소스를 조회
  • POST : 요청 데이터를 담아서 처리 
  • PUT : 리소스를 대체, 해당 리소스가 없으면 생성
  • PATCH : 리소스 부분 변경
  • DELETE : 리소스 삭제
  • HEAD : GET과 동일하지만 메시지 부분을 제외하고 상태 줄과 헤더만 반환
  • OPTIONS : 대상 리소스에 대한 통신 가능 옵션(메서드)를 설명 (주로 CORS에서 사용)
  • TRACE : 대상 리소스에 대한 경로를 따라 메시지 루프백 테스트를 수행

 

GET

리소스를 조회할 때 주로 사용한다.

서버에 전달하고 싶은 데이터는 쿼리 파라미터 또는 쿼리 스트링을 통해서 전달한다.

GET은 메시지 바디를 전달할 수 있지만 실무에서는 바디에 보통 데이터를 넣지 않는다.

지원하지 않는 서버들이 많아서 권장하지 않는다. 

 

예를들면 게시판의 게시물을 조회할 때 쓸 수 있다.

GET을 통한 요청은 URL 주소 끝에 파라미터로 포함되어 전송되며, 이 부분을 쿼리 스트링 (query string) 이라고 부른다.

방식은 URL 끝에 " ? " 를 붙이고 그다음 변수명1=값1&변수명2=값2... 형식으로 이어 붙이면 된다.

 

예를들어 다음과 같은 방식이다. 
http://www.example.com/show?name1=value1&name2=value2
서버에서는 name1 과 name2 라는 파라미터 명으로 각각 value1 과 value2 의 파라미터 값을 전달 받을 수 있다.

 

 

GET 요청과 응답

클라이언트가 /members/100 GET하고 요청하면 서버에서 GET 메시지가 도착한다.

서버에서는 /members/100 에서 데이터베이스를 조회해서 응답 메시지를 만들어서 클라이언트에게 보낸다. 

 

특징

  • 캐시 가능  : GET을 통해 서버에 리소스를 요청할 때 웹 캐시가 요청을 가로채 서버로부터 리소스를 다시 다운로드하는 대신 리소스의 복사본을 반환한다.
    HTTP 헤더에서 cache-control 헤더를 통해 캐시 옵션을 지정할 수 있다.
  • GET 요청은 브라우저 히스토리에 남는다.
  • GET 요청은 북마크 될 수 있다.
  • 길이 제한  : GET 요청의 길이 제한은 표준이 따로 있는건 아니고 브라우저마다 제한이 다르다고 한다. 
  • 보안 취약 : GET 요청은 파라미터에 다 노출되어 버리기 때문에 중요한 정보는 GET 방식을 사용하면 안된다.
  • GET은 데이터를 요청할때만 사용 된다.

 

POST

클라이언트에서 메시지 바디를 통해서 서버로 요청하고, 서버가 데이터를 처리하는 모든 기능을 수행한다.

주로 전달된 데이터로 신규 리소스 등록하거나 변경된 프로세스를 바꿀 때 많이 이용한다. 

 

POST는 클라이언트에서 서버로 리소스를 생성하거나 업데이트하기 위해 데이터를 보낼 때 사용 되는 메서드다.

예를들면 게시판에 게시글을 작성하는 작업 등을 할 때 사용할 된다.


POST는 전송할 데이터를 HTTP 메시지 body 부분에 담아서 서버로 보낸다

GET에서 URL 의 파라미터로 보냈던 name1=value1&name2=value2 가 body에 담겨 보내진다 생각하면 된다.

POST 로 데이터를 전송할 때 길이 제한이 따로 없어 용량이 큰 데이터를 보낼 때 사용하거나 GET처럼 데이터가 외부적으로 드러나는건 아니라서 보안이 필요한 부분에 많이 사용된다. (데이터를 암호화하지 않으면 body의 데이터도 결국 볼 수 있는건 똑같다.)

POST를 통한 데이터 전송은 보통 HTML form 을 통해 서버로 전송된다.

 

리소스를 /members POST로 전달하기전, 서버와 클라이언트는 그 데이터를 내부적으로 어떻게 쓸 것인지 미리 서로 약속되어있다.

따라서 클라이언트가 필요한 데이터를 전달하고 서버에서는 신규로 등록을 위해서  /members에 100 신규 리소스 식별자를 생성한다.

서버는 다시 클라이언트로 신규로 자원이 생성된 경로를 응답메시지로 보낸다.   

 

POST 예시

1. HTML 양식에 입력된 필드와 같은 데이터 블록을 데이터 처리 프로세스에 제공 

  • HTML 폼에 입력한 정보로 회원가입하거나 주문 문 등을 처리

2. 게시판, 뉴스 그룹, 메일링 리스트, 블로그 또는 유사한 기사 그룹에 메시지 게시

  • 게시판에 글쓰거나 댓글 달기
  • 게시판에 글을 쓰고 Submit 누르면 POST로 글이 서버로 전달하고 서버가 게시판에 글을 저장한다.

3. 서버가 아직 식별하지 않은 새 리소스 생성

  • 신규 주문을 생성하거나 신규 회원을 생성할 때 

4. 기존 자원의 끝에 데이터를 추가

  • 한 문서 끝에 내용 추가하기 

 

POST 정리

1. 새 리소스 생성(등록)

  • 서버가 아직 식별하지 않은 새 리소스 생성

2. 요청 데이터 처리

  • 단순히 데이터를 생성하거나 변경하는 것을 넘어서 프로세스를 처리할 때 서버에서 큰 변화가 일어남
  • 주문에서 결제완료 -> 배달 시작 -> 배달완료 처럼 단순히 값 변경을 넘어 프로세스의 상태가 변경되는 경우
  • POST의 결과로 새로운 리소스가 생성되지 않을 수 있음
  • 컨트롤 URI : POST /orders/{orderld}/start-delivery -> 실무에서 어쩔 수 없이 리소스만으로 URI 설계가 힘들 경우도 있다. 그때 동사만을 사용하여 URI를 구성

3. 다른 메서드로 처리하기 애매한 경우

  • JSON으로 조회 데이터를 넘겨야 하는데 GET 메서드를 지원하지 않는 서버가 있어서 사용하기 어려운 경우
  • 애매하면 POST 사용

 

특징

  • 캐시되지 않는다.
  • 브라우저 히스토리에 남지 않는다.
  • 북마크 되지 않는다.
  • 데이터 길이에 제한이 없다.

 

GET 과 POST 차이

  • 사용목적 : GET은 서버의 리소스에서 데이터를 요청할 때, POST는 서버의 리소스를 새로 생성하거나 업데이트할 때 사용한다.
    DB로 예시를 들면, GET은 SELECT 에 가깝고, POST는 Create 에 가깝다고 보면 된다.
  • 요청에 body 유무 : GET 은 URL 파라미터에 요청하는 데이터를 담아 보내기 때문에 HTTP 메시지에 body가 없다. POST 는 body 에 데이터를 담아 보내기 때문에 당연히 HTTP 메시지에 body가 존재한다.
  • 멱등성 (idempotent) : GET 요청은 멱등이며, POST는 멱등이 아니다.
멱등성
멱등의 사전적 정의는 연산을 여러 번 적용하더라도 결과가 달라지지 않는 성질을 의미한다.
GET은 리소스를 조회한다는 점에서 여러 번 요청하더라도 응답이 똑같을 것 이다.
반대로 POST는 리소스를 새로 생성하거나 업데이트할 때 사용되기 때문에 멱등이 아니라고 볼 수 있다. (POST 요청이 발생하면 서버가 변경될 수 있다.)

GET과 POST는 이런 차이들이 있기 때문에 사용하려는 목적에 맞는 확인한 후에 사용해야한다.

 

HTTP 메서드 - PUT, PATCH, DELETE

PUT

리소스가 있으면 완전히 대체(덮어쓰기)하고 리소스가 없으면 생성한다.

POST와 다른 점은 PUT은 클라이언트가 구체적인 리소스의 전체 위치를 알고 URI를 지정해서 서버에게 전달한다. 

 

PUT - 리소스가 있을 경우

클라이언트가 /members/100 리소스 지정해서 데이터를 서버에게 보내면 서버도 /members/100 리소스가 있다. 그러면 클라이언트가 보낸 리소스로 대체해버린다. 

 

PUT - 리소스가 없을 경우

클라이언트가 members/100 리소스 지정해서 데이터를 서버에게 보냈는데 서버에서 해당 리소스가 없으면 신규 리소스로 생성이 된다.

 

PUT 주의사항

클라이언트가 /members/100 데이터에 username이 없고 age로 지정해서 보내면 서버에서는 age를 업데이트를 하는데 username 자체가 날아가버린다.

기존 리소스를 새로운 리소스로 완전히 대체한다. 이렇게 되면 리소스를 수정하기 어렵다. 

 

PATCH

리소스를 부분 변경한다.

서버에 /members/100 데이터에 age가 없고, 클라이언트가 age로 지정해서 보내면 서버에서는 username은 남아있고 age만 변경한 것이다. 

 

DELETE

리소스를 삭제한다.

클라이언트가 /members/100 를 삭제해달라고 요청하면 서버에서 리소스를 삭제한다. 


HTTP 메서드의 속성

HTTP 메서드의 속성

 

안전(Safe)

호출해도 리소스가 변경하지 않는 특징

  • GET은 단순히 조회만 하기 때문에 안전하다.  한번 호출해도 여러번 호출해도 변경이 일어나지 않아서 안전하다.
  • POST, PUT, PATCH, DELETE는 안전하지 않다.
  • 만약에 그래도 계속 호출해서 서버에서 로그가 계속 쌓게되서 서버 장애가 일어날 때는 안전은 그런 부분까지 고려하지 않는다. 안전은 해당 리소스만 고려하기 때문이다. 

 

멱등(Idempotent)

한 번 호출해도 두 번 호출해도 100번 호출해도 결과는 동일한 특징

여러 번이라는 키워드를 중심으로 이해해야 한다.

  • GET은 한 번 조회하든 두 번 조회하든 같은 결과로 조회된다.
  • PUT은 결과를 대체하기 때문에 같은 요청을 여러 번 해도 최종 결과는 동일하다.
  • DELETE는 결과를 삭제하기 때문에 같은 요청을 여러 번 해도 삭제된 결과는 동일하다.
  • POST는 멱등이 아니다. 여러번 호출하면 같은 결제등이 중복으로 발생해서 새로운 리소스로 구별이 된다.
사용자 1 : GET -> username: A, age: 20
사용자 2 : PUT -> username: A, age: 30
사용자 1 : GET -> username: A, age: 30 -> 사용자2의 영향으로 바뀐 데이터 조회

멱등은 외부 요인으로 중간에 리소스가 변경되는 것까지 고려하지 않는다.

클라이언트가 동일한 요청을 똑같은 클라이언트가 동일한 요청했을 때만 멱등한다.

즉, 멱등은 동시성 문제까지 고려하지 않는다. 

 

멱등의 활용 

  • 자동 복구 매커니즘로 활용할 수 있다.
  • 클라이언트가 DELETE를 호출했는데 서버에서 잘 되고 있는지 안 되고 있는지 응답이 없을 경우, 클라이언트가 다시 재차 DELETE를 시도 해도 멱등하기 때문에 안전하다.

 

캐시가능(Cacheable)

웹 브라우저에 용량이 큰 이미지를 한번 요청을 하면 그 다음에 똑같이 용량이 큰 이미지를 요청할 필요없다.

똑같은 이미지를 서버에서 다운로드 받는 것은 불필요한 행동이기 때문이다.

그래서 로컬 PC에 웹 브라우저 저장을 하고 저장된 것을 이용한다.

 

캐시는 GET, HEAD, POST, PATCH 가 가능 하지만 실제로는 GET, HEAD 정도만 캐시로 사용한다.

POST, PATCH는 캐시를 하려면 본문 내용으로 리소스랑 캐시 키가 맞아아야 되는데 복잡해서 구현이 쉽지 않다.

GET, HEAD는 URI만 캐시 키로 캐시해서 간단하다. 

'네트워크 > HTTP' 카테고리의 다른 글

[HTTP] 상태코드  (1) 2024.02.10
[HTTP] HTTP 메서드 활용  (1) 2024.02.09
[HTTP] HTTP 기본  (1) 2024.02.07
[HTTP] URI와 웹 브라우저 요청 흐름  (1) 2024.02.06
[HTTP] 인터넷 네트워크  (1) 2024.02.05