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

문정준's avatar
Mar 14, 2025
[스프링부트] 7. Request 객체
✏️

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"); } }
 

결과

 
 

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

sxias