inblog logo
|
sxias
    스프링부트

    [스프링부트] 7. Request 객체

    문정준's avatar
    문정준
    Mar 14, 2025
    [스프링부트] 7. Request 객체
    Contents
    1. Header2. Dispatcher3. Reflection4. Dispatcher 보완5. 완성
    ✏️

    WAS에서 제공하는 객체

    • Request : 클라이언트 측에서 전송한 URL(URI), Body, Header가 담겨 있음
    • Response : 서버 측에서 제공할 클래스/메서드의 Return + HTML
     

    1. Header

    • 클라이언트 측에서 요청을 보낼 때 함께 전달되는 수식어
    • 클라이언트의 정보, 요청하는 메서드 종류 등이 담겨있음
    • 이 정보를 이용하여 서버에서는 사용자를 판단하고 그에 맞는 응답 가능
    package org.example.demo6; 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.Enumeration; @WebServlet("*.do") public class DemoServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // String method = req.getHeader("User-Agent"); // System.out.println("User-Agent : " + method); // // String host = req.getHeader("Host"); // System.out.println("Host : " + host); Enumeration<String> headerNames = req.getHeaderNames(); while (headerNames.hasMoreElements()) { String headerName = headerNames.nextElement(); String headerValue = req.getHeader(headerName); System.out.println(headerName + ": " + headerValue); } } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { } }
     
    notion image
     
    • POST 요청 시 Body 내용을 Parsing → getParameter를 통해 확인 가능
    • key에 따른 value 값 확인 가능
      • 일반 평문 (text/plain)으론 불가능
      • Context-Type : x-www-form-urlencoded으로 지정해야 파싱 가능
    package org.example.demo6; 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.BufferedReader; import java.io.IOException; import java.util.Enumeration; @WebServlet("*.do") public class DemoServlet extends HttpServlet { //localhost:8080/demo.do?username=ssar&password=1234 @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // String method = req.getHeader("User-Agent"); // System.out.println("User-Agent : " + method); // // String host = req.getHeader("Host"); // System.out.println("Host : " + host); // Enumeration<String> headerNames = req.getHeaderNames(); // // while (headerNames.hasMoreElements()) { // String headerName = headerNames.nextElement(); // String headerValue = req.getHeader(headerName); // System.out.println(headerName + ": " + headerValue); // } // 주소에 걸리는 queryString = DB 내의 where String path = req.getRequestURI(); System.out.println("path: " + path); String method = req.getMethod(); System.out.println("method: " + method); String queryString = req.getQueryString(); System.out.println("queryString: " + queryString); String username = req.getParameter("username"); System.out.println("username: " + username); String password = req.getParameter("password"); System.out.println("password: " + password); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // String body = ""; // // BufferedReader br = req.getReader(); // while(true) { // String line = br.readLine(); // if(line == null) break; // body += line; // } // System.out.println("body: " + body); // Context_Type : x-www-form-urlencoded String method = req.getMethod(); System.out.println("method: " + method); String username = req.getParameter("username"); System.out.println("username: " + username); String password = req.getParameter("password"); System.out.println("password: " + password); } }
     
    • username, password 입력 후 전송 버튼을 누르면 localhost:8080/hello.do로 요청 전송
      • body : username=ssar&password=12345
      • method : POST → body parsing 후 username, password 출력
     
    notion image
     
     
    ✏️

    Form action vs Redirect

    • action에 적힌 링크로 method 수행 자체는 서버에 보내는 요청 (Request)
    • 페이지가 이동되는 것은 내부 로직으로 인해 수행되는 리다이렉트
      • 서버에 대한 응답이며, 요청이 2번 들어가는 것과 같음
    notion image
     
     

    2. Dispatcher

    • 한 서버 내에는 여러 개의 View를 소유하고 있을 수 있음
    • 클라이언트의 요청에 따라 올바른 View를 응답해야 함
    • View와 클라이언트를 1:1로 연결할 경우, 코드의 재사용이 불가
      • a, b, c 세 곳에서 전부 DB를 사용한다면, 세 곳에 전부 다 DB를 연결해야 함
    • 이를 해결하기 위해 사용하는 것이 DS (Dispatcher Servlet)
        1. 경로 찾기
            • 클라이언트가 요청하는 View를 path 파싱을 통해 찾아서 응답
        1. 공통 로직
            • 한 코드 내에서 path에 따라 다른 응답을 제공할 수 있으므로 공통 로직 수행 가능
              • 각 View에 바로 접근하는 것이 아닌, Servlet에 접근한 후 View에 접근
    • DB 연결 (Model), Dispatcher (Controller), View (View)를 사용한 서버 구동 방식
      • MVC Pattern
    notion image
     
    package org.example.demo8; 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; // localhost:8080/hello.do?path=a @WebServlet("*.do") public class FrontController extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println("common logic"); String path = req.getParameter("path"); if (path.equals("a")) { req.getRequestDispatcher("a.jsp").forward(req, resp); } else if (path.equals("b")) { req.getRequestDispatcher("b.jsp").forward(req, resp); } else if (path.equals("c")) { req.getRequestDispatcher("c.jsp").forward(req, resp); } else { } } }
     
    • a.jsp
      • b, c는 <h1> 내용만 다름
    <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <h1>A view</h1> </body> </html>
     

    결과

    • path=a
    notion image
    • 공통 로직 수행 확인
    notion image
     
    • path=b
    notion image
    notion image
    • path=c
    notion image
    notion image
     
    📌

    이 코드의 문제점

    • 주소창에 localost:8080/a.jsp로 바로 접속 시, 공통 로직 수행 불가
      • Dispatcher로 이동시킬 강제성이 없음
      • 이는 Spring Boot에서 Default로 설정되어 있음
     
     

    3. Reflection

    • Servlet의 코드들은 Reflection을 사용하면 더욱 편하게 사용 가능
      • Spring Boot
    package org.example.second.controller; import jakarta.servlet.http.HttpServletResponse; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ResponseBody; @Controller // PrintWriter 응답 @Controller -> requestDispatcher public class DemoController { @GetMapping("/haha") public @ResponseBody String haha(){ return "haha"; } @GetMapping("/home") public void home(HttpServletResponse response){ response.setStatus(302); response.setHeader("Location", "/haha"); } }
     

    결과

    • localhost:8080/home 요청 → 302 코드 (Redirection) + Redirect Location (/haha) 응답
    • localhost:8080/haha 재요청(Redirection) → haha 응답
     
     

    4. Dispatcher 보완

    • Dispatcher를 무조건 거치게 하기 위해서는 기존 jsp 파일을 숨겨야 함
    • WEB-INF 파일 사용 (보안 폴더 : 외부 접근 차단)
      • 내부 접근만 가능
    • 내부 접근을 통해, Dispatcher의 공통 로직을 무조건 거칠 수 있도록 설계
     
    • Dispatcher
      • New Class ViewResolver : path 단축
      • Business에 따라, 또는 개발자 성향에 따라 더욱 단축 가능
    package org.example.demo9; 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; // http://localhost:8080/ @WebServlet("/") // 모든 요청이 여기로 몰린다. ex) /, /abc, /a/b/c public class DemoServlet extends HttpServlet { // http://localhost:8080?path=a @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // body -> path=값&name=값 // queryString -> ?path=값&name=값 String path = req.getParameter("path"); if (path.equals("a")) { req.setAttribute("name", "ssar"); req.getRequestDispatcher(ViewResolver.viewName("a")).forward(req, resp); } else if (path.equals("b")) { req.setAttribute("age", 20); req.getRequestDispatcher(ViewResolver.viewName("b")).forward(req, resp); } } }
     
    • ViewResolver
      • path를 받아 전체 path를 return
      • 추가 메서드 제작으로 RequestDispatcher 항목을 더욱 단축 가능
    package org.example.demo9; public class ViewResolver { private static final String prefix = "/WEB-INF/views/"; private static final String suffix = ".jsp"; public static String viewName(String filename) { return prefix + filename + suffix; } }
     
    • a.jsp와 b.jsp는 WEB-INF > views로 이동
      • 보안 폴더 내부로 이동 시켜 외부 접근 차단
      • 많은 페이지들이 담겨있을 수 있고, 설정 파일이 존재할 수 있으므로 폴더로 구분
    📌

    내부 폴더에 옮겼을 때의 장점

    • 내부 로직 수행 가능
      • 코드 재사용 가능, 리소스 감소
        • 리다이렉션 등 재요청에서 계속 req, resp를 new할 필요가 없어짐
        • DB (Model) 연결 시 내부 로직에 묶어놓으면 한 번만 연결해도 됨
    • 보안 향상
      • 외부에서 직접 접근할 수 없음 (코드 노출 감소)
     

    결과

    • localhost:8080/?path=a
    notion image
     
    • localhost:8080/?path=b
    notion image
     
    • 직접 파일 Access 시도 시 차단 (WEB-INF 폴더 내부에 있음 : 외부 접근 차단)
      • 기본 디렉토리 : webapp → /a.jsp는 ./webapp/a.jsp를 찾음
        • 파일 위치를 옮겼으므로 찾을 수 없음
    notion image
    • WEB-INF/views/b.jsp 직접 연결 시도 시 차단
      • 보안 폴더 내부
    notion image
     
    💡

    내부 폴더 사용 = Dispatcher 강제성 부여

    Dispatcher 강제성 부여 : 내부 로직 사용
    • 내부 로직 사용 : 코드 재사용 가능, 서버 내 Resource 사용량 감소
    내부 폴더 사용 : 외부 접근 차단
    • 파일의 직접 노출 차단 : 보안 향상
    • Dispatcher 강제성 부여 가능
     

    5. 완성

    💡

    WAS 기능을 만들지 말고, Servlet을 구현하기

    • WAS 내의 구조는 개발자가 직접 컨트롤할 수 없음
    • WAS에서 제공되는 자원을 활용하여 Servlet을 구현
      • 기본 기능을 활용하여 새 기능 구현
    Component Scan
    • Annotation (@Controller)
    componentScan
    package org.example.demo10.core; import jakarta.servlet.ServletContext; import java.io.File; import java.util.HashSet; import java.util.Set; public class ComponentScan { private final ServletContext servletContext; public ComponentScan(ServletContext servletContext) { this.servletContext = servletContext; } // 클래스를 스캔하는 메소드 public Set<Object> scanClass(String pkg) { Set<Object> instances = new HashSet<>(); try { // 톰캣의 webapps 폴더 내 WEB-INF/classes 경로 가져오기 String classPath = servletContext.getRealPath("/WEB-INF/classes/"); // C:\Program Files\Apache Software Foundation\Tomcat 11.0\webapps\ROOT\WEB-INF\classes\ File slashDir = new File(classPath); File dotToSlashDir = new File(slashDir, pkg.replace(".", File.separator)); for (File file : dotToSlashDir.listFiles()) { System.out.println(file.getName()); String className = pkg + "." + file.getName().replace(".class", ""); System.out.println(className); try { Class cls = Class.forName(className); if (cls.isAnnotationPresent(Controller.class)) { System.out.println("Controller 어노테이션"); Object instance = cls.getDeclaredConstructor().newInstance(); instances.add(instance); } } catch (Exception e) { throw new RuntimeException(e); } } return instances; } catch (Exception e) { throw new RuntimeException(e); } } }
    Reflection & Annotation
    Controller
    package org.example.demo10.core; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface Controller { }
    RequestMapping
    package org.example.demo10.core; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RequestMapping { String value(); }
    DispatcherServlet
    package org.example.demo10; import jakarta.servlet.ServletConfig; 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 org.example.demo10.core.ComponentScan; import org.example.demo10.core.RequestMapping; import org.example.demo10.core.ViewResolver; import java.io.IOException; import java.lang.reflect.Method; import java.util.Set; @WebServlet("*.do") public class DispatcherServlet extends HttpServlet { private Set<Object> controllers; @Override public void init(ServletConfig config) throws ServletException { // 1. 컴포넌트 스캔 ComponentScan componentScan = new ComponentScan(config.getServletContext()); controllers = componentScan.scanClass("org.example.demo10.controller"); //System.out.println(controllers.size()); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // localhost:8080/user.do?path=join String path = req.getParameter("path"); // 2. 라우팅 String templatePath = route(path); // 3. 리다이렉션 if(templatePath.contains("redirect:")) { String redirectPath = templatePath.replace("redirect:", ""); // resp.setStatus(302); // resp.setHeader("Location", "?path=" + redirectPath); resp.sendRedirect("?path=" + redirectPath); return; } // 4. 이동 if (templatePath == null) { resp.setStatus(404); resp.getWriter().println("<h1>404 Not Found</h1>"); } else { req.getRequestDispatcher(ViewResolver.viewName(templatePath)).forward(req, resp); } } private String route(String path) { for (Object instance : controllers) { Method[] methods = instance.getClass().getMethods(); for (Method method : methods) { RequestMapping rm = method.getAnnotation(RequestMapping.class); if (rm == null) continue; // 다음 for문으로 바로 넘어감 if (rm.value().equals(path)) { try { return (String) method.invoke(instance); } catch (Exception e) { throw new RuntimeException(e); } } } } return null; } }
    Controller
    UserController
    package org.example.demo10.controller; import org.example.demo10.core.Controller; import org.example.demo10.core.RequestMapping; @Controller public class UserController { @RequestMapping("join") public String join() { System.out.println("UserController join"); return "join"; } @RequestMapping("login") public String login() { System.out.println("UserController login"); return "login"; } }
     

    결과

    • localhost:8080/*.do?path=join
    notion image
    • localhost:8080/*.do?path=login
    notion image
    • path가 다를 경우 : 404 Not Found 출력
    notion image
    Share article
    Contents
    1. Header2. Dispatcher3. Reflection4. Dispatcher 보완5. 완성

    Sxias ㆍ a32176740@gmail.com

    RSS·Powered by Inblog