# 웹 서버, 웹 애플리케이션 서버 (WAS)
- 웹서버
- HTTP 기반으로 동작
- 정적 리소스 제공
- NGINX, APACHE
- WAS
- HTTP 기반으로 동작
- 웹서버 기능 + 프로그램 코드를 실행하여 애플리케이션 로직 수행
- 동적 HTML / JSON
- 톰캣, Jetty, Undertow
- WAS가 모든 요청을 처리할 수 있지만, 실제로는 역할을 분리하여 운영한다.
- 정적 리소스는 웹 서버(Nginx 등)가 처리하고, 동적 로직이 필요한 경우에만 WAS에 위임한다.
- 이렇게 하면 정적 리소스 트래픽이 많아질 때는 웹 서버를, 애플리케이션 로직 부하가 커질 때는 WAS를 독립적으로 증설할 수 있다.
- 정적 리소스만 제공하는 웹서버는 상대적으로 안정적이고, 애플리케이션 로직이 수행되는 WAS는 더 잘 죽는다.
- WAS / DB 장애시 웹서버가 오류 화면을 제공할 수 있게 된다.
- 서블릿은 Java로 HTTP 요청/응답을 처리하기 위한 표준 인터페이스이다.
- 소켓/스트림 처리, 메서드 구분, URL 파싱, 헤더 파싱 등 반복적인 저수준 작업을 추상화한다.
- 덕분에 개발자는 HTTP 스펙을 편리하게 사용하면서 비즈니스 로직 작성에만 집중할 수 있다.
- HTTP 요청 처리 흐름
- WAS(Tomcat)가 HTTP 요청을 받아 HttpServletRequest, HttpServletResponse 객체 생성
- 요청 URL에 매핑된 서블릿을 서블릿 컨테이너에서 조회 후 실행
- 서블릿의 비즈니스 로직 수행 (개발자 작성 코드)
- 로직 종료 후 HttpServletResponse 객체를 기반으로 HTTP 응답 메시지 생성
- 클라이언트에 응답 전달
- 서블릿을 지원하는 WAS를 서블릿 컨테이너라고 함. (ex - 톰캣)
- 서블릿 컨테이너는 서블릿 객체를 생성, 초기화, 호출, 종료하는 생명주기를 관리
- 서블릿 객체는 싱글톤으로 관리
- WAS 최대 튜닝포인트는 최대 스레드 수이다.
- 낮으면: 동시 요청이 많을때 서버 리소스가 여유롭지만 클라이언트의 응답 지연
- 높으면: 동시요청이 많으면 서버 리소스 임계점 초과 가능성 존재
- 클라우드 기반 환경에서 장애 발생시에는 서버부터 늘리고 튜닝
- 스레드 풀 적정 숫자는 아파치 ab, 제이미터, nGrinder등의 툴을 활용하여 성능 테스트 후 결정한다.
- 스레드 생성 소멸 / 풀 관리 / 할당 등의 작업들은 WAS에서 해주기 때문에, 개발자는 서블릿 로직만 작성하면 된다.
- Spring Bean / 서블릿 객체는 싱글톤 기반이기 때문에 멤버 변수를 사용하지 않는 것이 관례
# 서블릿
- 서블릿은 HTTP 요청 / 응답 처리의 저수준 작업을 대신 해주는 자바 표준 인터페이스이다.
- 서블릿은 톰캣같은 WAS를 직접 설치하고 그 위에 서블릿 코드를 클래스 파일로 빌드하여 올린 후 톰캣 서버를 실행해야 한다.
- 이 과정은 번거롭지만 스프링에는 톰캣이 내장되어 있기 때문에 저 과정을 거치지 않아도 된다.
@ServletComponentScan을 사용하면 서블릿을 직접 등록하여 사용 가능하다.
@ServletComponentScan
@SpringBootApplication
public class ServletApplication {
public static void main(String[] args) {
SpringApplication.run(ServletApplication.class, args);
}
}
// ..
@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("HelloServlet.service");
System.out.println("request = " + req);
System.out.println("response = " + resp);
String username = req.getParameter("username");
System.out.println("username = " + username);
resp.setContentType("text/plain");
resp.setCharacterEncoding("utf-8");
resp.getWriter().write("hello " + username);
}
}
- HTTP를 통해 매핑된 url이 호출되면 서블릿 컨테이너가 service 메서드를 실행한다.
- urlPatterns에 와일드카드가 없으면 정확히 일치할때만 매핑에 성공한다.
# HttpServletRequest
- 서블릿은 개발자가 HTTP 요청 메시지를 편리하게 사용할 수 있도록 개발자 대신 HTTP 요청 메시지를 파싱한다. 이후 결과를 HttpServletRequest 객체에 담아 제공한다.
- HttpServletRequest는 HTTP 요청 정보 외에 아래 부가 기능도 제공한다.
- 임시 저장소 기능
- HTTP 요청 시작부터 종료까지 유지되는 임시 저장소
request.setAttribute(name, value)— 데이터 저장request.getAttribute(name)— 데이터 조회
- 세션 관리 기능
request.getSession(create)— true면 세션 없을 때 새로 생성, false면 기존 세션만 반환
- 임시 저장소 기능
# HTTP 요청 데이터 전달
- HTTP 요청 메시지를 통해 클라이언트에서 서버로 데이터를 전달하는 방법은 다음과 같다.
- GET - 쿼리 파라미터
- POST - HTML Form
- HTTP message body
- GET 쿼리 파라미터
- URL에
?로 시작하여&로 이어 붙인다. - 예:
/hello?username=jun&age=29 request.getParameter(keyName): 단일 파라미터 조회request.getParameterNames()등 다양한 조회 메서드를 제공한다.
- URL에
- HTML Form
Content-Type: application/x-www-form-urlencoded형식으로 메시지 바디에 담아 전송- GET 쿼리 파라미터와 형식이 동일해서 서버에서
request.getParameter()로 동일하게 조회 가능 - POST 방식이므로 반드시
Content-Type헤더를 명시해야 한다.
- HTTP message body
- InputStream으로 바디 데이터를 읽어들이며, 바이트 코드를 반환하므로 Charset 지정이 필요하다.
- 단순 텍스트:
Content-Type: text/plain, JSON:Content-Type: application/json - JSON 결과를 Java 객체로 변환하기 위해 Jackson 라이브러리의 ObjectMapper를 사용한다.
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ServletInputStream inputStream = req.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
System.out.println("messageBody = " + messageBody);
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
System.out.println("helloData.username = " + helloData.getUsername());
System.out.println("helloData.age = " + helloData.getAge());
resp.getWriter().write("ok");
}
Lombok Getter Setter
- Lombok 라이브러리의
@Getter,@Setter어노테이션을 사용하면 컴파일 시점에 자동으로 getter/setter 메서드를 생성해준다. - ObjectMapper가 JSON을 객체로 변환할 때 내부적으로 getter/setter를 사용하므로, Lombok으로 간결하게 대체할 수 있다.
import lombok.Getter;
import lombok.Setter;
@Getter @Setter
public class HelloData {
private String username;
private int age;
}
# HttpServletResponse
- HTTP 응답 메시지 생성
- HTTP 응답 코드 지정
- 헤더 생성
- 바디 생성
- Content-Type, 쿠키, Redirect 기능 제공
# HTTP 응답 데이터 전달
- HTTP 응답 데이터를 전달하는 방법들은 다음과 같다.
writer.println("message");를 통한 단순 텍스트 응답 전달- HTML 응답
- HTTP API - MessageBody JSON 응답
- HTML 응답 방식
resp.setContentType("text/html")로 지정resp.getWriter()로 PrintWriter객체를 얻고,writer.println("<html></html>")로 html을 작성한다.
- HTTP 응답으로 데이터를 JSON으로 반환할때는 Content-Type을
application/json으로 지정해야 한다. objectMapper.writeValueAsString()을 사용하면 객체를 JSON 문자로 변환해준다.
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setHeader("content-type", "application/json");
resp.setCharacterEncoding("utf-8");
HelloData data = new HelloData();
data.setUsername("kim");
data.setAge(20);
String result = objectMapper.writeValueAsString(data);
resp.getWriter().write(result);
}
# JSP
- HTTP 응답 메시지에 HTML을 직접 작성하는 방식은 매우 번거롭다.
- JSP와 같은 템플릿 엔진을 사용하면 동적 데이터를 HTML에 삽입하여 완성된 페이지를 쉽게 만들 수 있다.
- 서블릿만 사용 시 HTML 작업과 자바 코드가 섞여 지저분하고 복잡했다.
- JSP를 통해 HTML과 자바 코드를 어느 정도 분리하고, 동적 변경이 필요한 부분에만 자바 코드를 부분적으로 적용할 수 있었다.
- 단, JSP 내에 비즈니스 로직과 자바 코드가 여전히 혼재하는 한계가 있었다.
- 이를 해결하기 위해 MVC 패턴이 등장했다. 비즈니스 로직은 서블릿(Controller)에서 처리하고, JSP는 뷰를 그리는 일에만 집중하는 방식으로 역할을 분리한다.
- JSP는 현재 잘 사용되지 않으며, Thymeleaf 등의 템플릿 엔진으로 대체됨
# MVC 패턴
- MVC 패턴은 하나의 서블릿이나 하나의 JSP로 처리하던 것을 컨트롤러와 뷰 영역으로 역할을 나눈 것을 의미한다.
- Controller: HTTP요청을 받아 파라미터 검증 / 비즈니스 로직 실행 / 뷰에 전달할 결과 데이터 조회 후 모델에 담음
- 컨트롤러가 비대해지는 문제를 개선하기 위해 비즈니스 로직은 Service 계층을 만들어 처리하는 것이 일반적이다.
- Model: 뷰에 출력할 데이터를 담아둠. 뷰는 비즈니스 로직이나 데이터 접근을 몰라도 되고 화면 렌더링에만 집중 가능
- View: 모델에 담겨있는 데이터를 사용하여 화면 그리는 일에 집중
@WebServlet(name = "mvcMemberFormServlet", urlPatterns = "/servlet-mvc/members/new-form")
public class MvcMemberFormServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
dispatcher.forward(req,resp);
}
}
- Java EE(Jakarta EE)의 WAS 자체 스펙상
/WEB-INF하위 경로는 외부 접근이 불가능하다.- MVC 패턴 기준으로는 항상 컨트롤러 내에서의 비즈니스 로직을 통해서만 JSP를 호출하겠다는 것을 의미한다.
- RequestDispatcher는 요청을 다른 리소스로 전달하는 객체이다.
redirect vs forward
- 리다이렉트는 실제 클라이언트에 응답이 나갔다가 클라이언트가 redirect 경로로 다시 요청하게 된다. 클라이언트가 인지할 수 있고 URL 경로도 변경된다.
- 포워드는 서버 내부에서 일어나는 호출이기 때문에 클라이언트가 인지하지 못한다.
- JSP 기반 MVC 패턴의 한계
- HttpServletRequest, HttpServletResponse를 사용하지 않는 컨트롤러도 있어 인터페이스 통일이 어렵다.
- 포워드, 뷰 경로 설정 등 공통 로직이 각 컨트롤러마다 중복된다.
- 공통 처리가 어렵다는 문제가 있다.
# 프론트 컨트롤러 패턴
- 기본적인 MVC는 컨트롤러마다 공통 로직이 중복되는 한계가 있다.
- 프론트 컨트롤러 패턴은 모든 클라이언트 요청을 서블릿 하나로 받아 요청에 맞는 컨트롤러를 찾아 호출하는 패턴이다.
- 공통 로직을 프론트 컨트롤러 한 곳에서 처리할 수 있다.
- HTTP 관련 처리를 프론트 컨트롤러가 담당하므로, 나머지 컨트롤러는 서블릿 없이 순수 Java 클래스로 작성 가능하다.
- Spring MVC의 DispatcherServlet이 프론트 컨트롤러 패턴으로 구현되어 있다.
# V1
@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
public class FrontControllerServletV1 extends HttpServlet {
private Map<String, ControllerV1> controllerMap = new HashMap<>();
public FrontControllerServletV1() {
controllerMap.put("/front-controller/v1/members/new-form", new MemberFormControllerV1());
controllerMap.put("/front-controller/v1/members/save", new MemberSaveControllerV1());
controllerMap.put("/front-controller/v1/members", new MemberListControllerV1());
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String requestURI = req.getRequestURI();
ControllerV1 controller = controllerMap.get(requestURI);
if (controller == null) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
controller.process(req, resp);
}
}
- URI와 컨트롤러 매핑 정보를 Map으로 관리하고, 요청 URI로 컨트롤러를 조회한다.
- 조회된 컨트롤러가 있으면
process()실행, 없으면 404 반환 - 모든 컨트롤러는
ControllerV1인터페이스를 구현하여process()메서드를 반드시 구현하도록 추상화되어 있다.
- 조회된 컨트롤러가 있으면
# V2
- 기존에는 모든 컨트롤러에서 JSP 포워딩 코드가 중복되었다.
- 이를 해결하기 위해 뷰 이동 로직을
MyView객체로 분리하고, 렌더링은 프론트 컨트롤러가 담당하도록 위임한다.
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);
}
}
- 동작 흐름
- 클라이언트 요청 → 프론트 컨트롤러
- 프론트 컨트롤러 → 컨트롤러 호출
- 컨트롤러 →
MyView객체 반환 (뷰 경로만 담아서) - 프론트 컨트롤러 →
myView.render()호출 → JSP 포워딩 → 응답
# V3
- 리팩토링 이후 컨트롤러들은 서블릿 관련 코드 없이 비즈니스 로직에만 집중할 수 있다.
HttpServletRequest,HttpServletResponse불필요- JSP 물리 경로는 프론트 컨트롤러의
viewResolver가 처리 - 컨트롤러는 논리적 뷰 이름만 반환
- 컨트롤러 인터페이스와 ModelView
// 인터페이스: 서블릿 의존성 제거, paramMap만 받아 ModelView 반환
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
// ModelView: 논리적 뷰 이름 + JSP 렌더링에 필요한 데이터 보관
public class ModelView {
@Getter @Setter
private String viewName;
@Getter @Setter
private Map<String, Object> model = new HashMap<>();
public ModelView(String viewName) {
this.viewName = viewName;
}
}
- 컨트롤러 구현체 예시 (멤버 리스트 조회)
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); // JSP에서 접근할 데이터 저장
return mv;
}
}
- 프론트 컨트롤러
@WebServlet(name = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*")
public class FrontControllerServletV3 extends HttpServlet {
private Map<String, ControllerV3> controllerMap = new HashMap<>();
// ...
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String requestURI = req.getRequestURI();
ControllerV3 controller = controllerMap.get(requestURI);
if (controller == null) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
Map<String, String> paramMap = createParamMap(req); // request 파라미터 추출
ModelView mv = controller.process(paramMap); // 컨트롤러 실행
MyView view = viewResolver(mv.getViewName()); // 논리 → 물리 경로 변환
view.render(mv.getModel(), req, resp); // 렌더링
}
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");
}
}
- MyView 렌더링
public class MyView {
private String viewPath;
public MyView(String viewPath) {
this.viewPath = viewPath;
}
public void render(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
modelToRequestAttribute(model, request); // model 데이터를 request attribute로 이동
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response); // JSP 포워딩
}
private void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
model.forEach((key, value) -> request.setAttribute(key, value));
}
}
- 동작 흐름
- HTTP 요청 → 프론트 컨트롤러 호출
- 요청 URI로 컨트롤러 맵 조회 (없으면 404 반환)
createParamMap()으로 request 파라미터를 Map으로 추출- 컨트롤러
process(paramMap)실행 →ModelView반환ModelView.model에 JSP 렌더링에 필요한 데이터 저장
viewResolver()로 논리적 뷰 이름 → 물리적 JSP 경로 변환,MyView생성view.render()호출model데이터를request.setAttribute()로 옮김RequestDispatcher.forward()로 JSP 포워딩
- JSP에서
requestattribute 접근하여 동적 HTML 렌더링 후 클라이언트에 응답
# V4
- 각 컨트롤러에서 ModelView 객체를 직접 생성하고 있었는데, 뷰 이름 문자열값만 리턴하는 역할로 리팩토링 할 수 있다.
public class MemberListControllerV4 implements ControllerV4 {
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";
}
}
- 파라미터의 모델뷰 맵 객체를 참조하여 JSP에서 활용할 데이터를 직접 넣어주는 방식이다.
# 어댑터 패턴 활용
- 서로 다른 인터페이스 명세를 가진 컨트롤러들을 동시에 지원하기 위해 핸들러 어댑터 패턴을 사용한다.
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
public interface ControllerV4 {
String process(Map<String, String> paramMap, Map<String, Object> model);
}
public interface MyHandlerAdapter {
boolean supports(Object handler);
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
- 프론트 컨트롤러는 핸들러 매핑 맵과 어댑터 목록을 초기화하여 관리한다.
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
private final Map<String, Object> handlerMappingMap = new HashMap<>();
private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
public FrontControllerServletV5() {
initHandlerMappingMap();
initHandlerAdapters();
}
private void initHandlerMappingMap() {
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());
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());
handlerAdapters.add(new ControllerV4HandlerAdapter());
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Object handler = getHandler(req);
if (handler == null) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
MyHandlerAdapter adapter = getHandlerAdapter(handler);
ModelView mv = adapter.handle(req, resp, handler);
MyView view = viewResolver(mv.getViewName());
view.render(mv.getModel(), req, resp);
}
private Object getHandler(HttpServletRequest request) {
return handlerMappingMap.get(request.getRequestURI());
}
private MyHandlerAdapter getHandlerAdapter(Object handler) {
for (MyHandlerAdapter adapter : handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
throw new IllegalArgumentException("handler adapter를 찾을 수 없음: " + handler);
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
- URL 경로에 v3/v4가 포함되어 어떤 컨트롤러 타입으로 요청할지 구분한다.
/front-controller/v5/v3/members/new-form→ V3 컨트롤러/front-controller/v5/v4/members/new-form→ V4 컨트롤러
- 핸들러 매핑 맵은
Object타입으로 컨트롤러를 저장하고, 조회 후 어댑터 내에서 다운캐스팅하여 사용한다. - 어댑터는
supports()로 자신이 처리할 수 있는 핸들러인지 확인하고, 처리 가능하면handle()을 통해 비즈니스 로직을 수행한다.
// V3 어댑터
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);
return controller.process(paramMap);
}
}
// V4 어댑터: ControllerV4는 viewName(String)을 반환하므로 ModelView로 변환
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) throws ServletException, IOException {
ControllerV4 controller = (ControllerV4) handler;
Map<String, String> paramMap = createParamMap(request);
Map<String, Object> model = new HashMap<>();
String viewName = controller.process(paramMap, model);
ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;
}
}
- V3는
ModelView를 직접 반환하지만, V4는String(논리적 뷰 이름)을 반환하므로 어댑터에서ModelView로 변환하여 프론트 컨트롤러에 반환한다. - 각 컨트롤러의 인터페이스 차이는 어댑터 내부에서 처리되므로, 프론트 컨트롤러는 항상
ModelView만 받아 동일하게 처리할 수 있다.- 새로운 컨트롤러 인터페이스 추가 시 어댑터만 구현하면 된다.
- 동작 흐름
- HTTP 요청 → 프론트 컨트롤러 호출
handlerMappingMap에서 요청 URI로 핸들러 조회 (없으면 404 반환)handlerAdapters를 순회하며supports()로 처리 가능한 어댑터 획득- 어댑터의
handle()호출 → 내부에서paramMap생성 후 컨트롤러process()실행 - 어댑터가
ModelView반환 (V4는 String → ModelView 변환 포함) viewResolver()로 논리적 뷰 이름 → 물리적 JSP 경로 변환view.render()로 model 데이터를 request attribute로 옮긴 뒤 JSP 포워딩- JSP에서 동적 HTML 렌더링 후 클라이언트에 응답
# Spring MVC 구조
- 직접 구현한 구조와 Spring MVC 구조 비교
| 직접 구현 | Spring MVC | 역할 |
|---|---|---|
FrontController | DispatcherServlet | 모든 요청을 받아 핸들러 조회 → 어댑터 실행 → 렌더링까지 담당 |
handlerMappingMap | HandlerMapping | URL과 핸들러(컨트롤러) 매핑 정보 관리 |
MyHandlerAdapter | HandlerAdapter | supports()로 처리 가능 여부 확인, handle()로 컨트롤러 실행 후 ModelView 반환 |
ModelView | ModelAndView | 뷰 이름과 데이터 모델을 함께 보관하는 객체 |
viewResolver | ViewResolver | 논리적 뷰 이름 → 물리적 뷰 경로로 변환 |
MyView | View | RequestDispatcher.forward()로 실제 뷰 렌더링 수행 |
# DispatcherServlet 구조
- 스프링 MVC 역시 프론트 컨트롤러 패턴 기반으로 구현되어 있고, 스프링 MVC의 프론트 컨트롤러가
DispatcherServlet이다. - DispatcherServlet도 HttpServlet을 상속받아 사용하여 서블릿으로 동작한다.
- Spring MVC 요청 흐름
- 클라이언트 요청 →
DispatcherServlet호출 →HttpServlet.service()실행 FrameworkServlet.service()가 오버라이딩되어 있어 이를 거쳐DispatcherServlet.doDispatch()까지 호출된다.
- 클라이언트 요청 →
doDispatch()처리 흐름getHandler()— 요청 URI 기반으로 핸들러 조회getHandlerAdapter()— 핸들러를 처리할 수 있는 어댑터 조회adapter.handle()— 어댑터 실행 → 실제 핸들러 실행 → 컨트롤러 비즈니스 로직 수행 →ModelAndView반환processDispatchResult()— 뷰 리졸버로 뷰 조회 후 렌더링 수행
- HandlerMapping, HandlerAdapter, ViewResolver, View 인터페이스 구현 후
DispatcherServlet에 등록하면 직접 컨트롤러를 만들 수 있다.
# HandlerMapping, HandlerAdapter
@Component("/springmvc/controller")
public class MyController implements Controller {
// ..
}
- HandlerMapping과 HandlerAdapter는 쌍으로 동작한다.
| HandlerMapping | HandlerAdapter | 방식 |
|---|---|---|
RequestMappingHandlerMapping | RequestMappingHandlerAdapter | @Controller 어노테이션 기반 (현재 주류) |
BeanNameUrlHandlerMapping | SimpleControllerHandlerAdapter | Controller 인터페이스 구현 (레거시) |
BeanNameUrlHandlerMapping | HttpRequestHandlerAdapter | HttpRequestHandler 인터페이스 구현 (레거시) |
- 실무에서는
RequestMappingHandlerMapping+RequestMappingHandlerAdapter조합만 사용한다. - 여러 HandlerMapping이 등록된 경우 우선순위 순으로 조회하며,
RequestMappingHandlerMapping이 가장 높은 우선순위(0)를 가진다.
# ViewResolver
@Component("/springmvc/old-controller")
public class OldController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
return new ModelAndView("new-form"); // 논리적 뷰 이름만 반환
}
}
- Spring Boot는
InternalResourceViewResolver를 자동으로 등록하며,application.properties의 설정을 기반으로 논리적 뷰 이름을 물리적 경로로 변환한다.
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
- 위 설정 시
"new-form"반환 →/WEB-INF/views/new-form.jsp로 변환되어 렌더링된다. - Spring Boot가 자동으로 등록하는 주요 뷰 리졸버
| 우선순위 | 뷰 리졸버 | 설명 |
|---|---|---|
| 1 | BeanNameViewResolver | 뷰 이름과 일치하는 Bean을 찾아 반환 |
| 2 | InternalResourceViewResolver | prefix + 뷰 이름 + suffix로 JSP 경로를 생성하여 반환 |
- 뷰 리졸버는 우선순위 순으로 조회하며, 앞 순위에서 뷰를 찾으면 이후 리졸버는 조회하지 않는다.
InternalResourceViewResolver는 우선순위가 가장 낮아 다른 리졸버에서 찾지 못한 경우 마지막에 조회된다.
# Spring MVC 시작
@Controller
public class SpringMemberFormControllerV1 {
@RequestMapping("/springmvc/v1/members/new-form")
public ModelAndView process() {
return new ModelAndView("new-form");
}
}
@Controller- 내부에
@Component가 포함되어 있어 컴포넌트 스캔 대상이 되며 Spring Bean으로 자동 등록된다. RequestMappingHandlerMapping이@RequestMapping계열 어노테이션을 스캔하여 핸들러로 인식한다.
- 내부에
@RequestMapping: 해당 URL로 요청이 오면 메서드를 호출한다.- 어노테이션 기반이므로 메서드 이름은 자유롭게 정의 가능하다.
ModelAndView: 뷰 이름과 모델 데이터를 함께 담아 반환하는 객체
@Controller
public class SpringMemberSaveControllerV1 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@RequestMapping("/springmvc/v1/members/save")
public ModelAndView process(HttpServletRequest request, HttpServletResponse response) {
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(username, age);
memberRepository.save(member);
ModelAndView mv = new ModelAndView("save-result");
mv.addObject("member", member);
return mv;
}
}
ArgumentResolver가 메서드 파라미터 타입을 분석하여HttpServletRequest,HttpServletResponse등을 자동으로 주입한다. 파라미터 순서는 무관하다.@RequestMapping에 매핑된 URL로 요청이 오면 메서드가 호출된다. GET/POST 구분 없이 모두 처리하며, 특정 메서드만 허용하려면@GetMapping,@PostMapping을 사용한다.mv.addObject(): 모델에 데이터를 추가하며, ViewResolver를 거쳐 JSP에서 해당 데이터에 접근할 수 있다.
# Spring MVC 통합
@Controller
@RequestMapping("/springmvc/v2/members")
public class SpringMemberControllerV2 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@RequestMapping("/new-form")
public ModelAndView newForm() {
return new ModelAndView("new-form");
}
@RequestMapping("/save")
public ModelAndView save(HttpServletRequest request, HttpServletResponse response) {
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(username, age);
memberRepository.save(member);
ModelAndView mv = new ModelAndView("save-result");
mv.addObject("member", member);
return mv;
}
@RequestMapping
public ModelAndView members() {
List<Member> members = memberRepository.findAll();
ModelAndView mv = new ModelAndView("members");
mv.addObject("members", members);
return mv;
}
}
- 메서드 단위로 적용되는 매핑을 하나의 클래스에 모아 처리할 수 있다.
# Spring 부가기능 활용
@Controller
@RequestMapping("/springmvc/v3/members")
public class SpringMemberControllerV3 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@GetMapping("/new-form")
public String newForm() {
return "new-form";
}
@PostMapping("/save")
public String save(
@RequestParam("username") String username,
@RequestParam("age") int age,
Model model
) {
Member member = new Member(username, age);
memberRepository.save(member);
model.addAttribute("member", member);
return "save-result";
}
@GetMapping
public String members(Model model) {
List<Member> members = memberRepository.findAll();
model.addAttribute("members", members);
return "members";
}
}
@GetMapping,@PostMapping:@RequestMapping에 HTTP 메서드 제한을 추가한 어노테이션으로, 더 직관적인 표현이 가능하다.- 클래스 레벨에
@RequestMapping을 선언하면 하위 메서드의 URL 앞에 자동으로 붙는다.- 예:
/springmvc/v3/members+/new-form→/springmvc/v3/members/new-form
- 예:
@RequestParam: HTTP 요청 파라미터를 메서드 파라미터로 바인딩하며, 타입 변환도 자동으로 처리한다.- GET 쿼리 파라미터, POST Form 방식 모두 지원
Model:ArgumentResolver가 자동 주입하며,addAttribute()로 데이터를 담으면 ViewResolver를 거쳐 JSP에서 접근 가능하다.- 논리적 뷰 이름을 반환하면
InternalResourceViewResolver가application.properties의 prefix/suffix를 조합하여 물리적 JSP 경로로 변환한다.
@ModelAttribute
- 추후 나올 ModelAttribute를 사용하면 addAttribute를 내부적으로 자동으로 처리해준다.
@PostMapping("/add")
public String addItem(
@ModelAttribute Item item // Model model 파라미터 생략 가능
) {
itemRepository.save(item);
// model.addAttribute("item", item); - @ModelAttribute가 자동 처리
return "basic/item";
}
# Spring MVC 기본기능
# HTTP 메서드 매핑
@RequestMapping(value = "/hello-basic", method = RequestMethod.POST)
public String helloBasic() {
// ..
return "ok";
}
- 위와 같이 method 파라미터로 메서드를 명시하면 해당 메서드만 처리한다. 명시하지 않으면 HTTP 메서드와 무관하게 호출된다.
- 메서드 지정을 해둔 상태로 다른 메서드로 요청을 하게 되면 405 (Method not allowed)를 반환한다.
@GetMapping,@PostMapping,@PutMapping,@DeleteMapping,@PatchMapping으로 매핑 축약이 가능하다.- PathVariable을 사용하면 리소스 경로에 식별자를 넣는 케이스에 쉽게 대응이 가능하다.
@RestController
public class MappingController {
private Logger log = LoggerFactory.getLogger(getClass());
@GetMapping("/mapping/{userId}")
public String helloBasic(@PathVariable("userId") String data) {
log.info("data: {}", data);
return "ok";
}
}
public String mappingPath(@PathVariable String userId, @PathVariable Long orderId)와 같이 다중 파라미터도 받을 수 있다.@PostMapping(value="/mapping-consume", consumes="application/json")형태로 미디어 타입 기준 매핑 조건을 걸 수 있다.- 매핑에 실패한 경우 415 (Unsupported Media Type) 발생
produces = "text/html"구조로 미디어 타입 매핑도 가능하다.- 매핑 실패한 경우 406 (Not Acceptable) 발생
# HTTP 헤더 조회
@Slf4j
@RestController
public class RequestHeaderController {
@RequestMapping("/headers")
public String headers(
HttpServletRequest request,
HttpServletResponse response,
HttpMethod httpMethod,
Locale locale,
@RequestHeader MultiValueMap<String, String> headerMap,
@RequestHeader("host") String host,
@CookieValue(value = "myCookie", required = false) String cookie
) {
log.info("request = {}", request);
log.info("response = {}", response);
log.info("httpMethod = {}", httpMethod);
log.info("locale = {}", locale);
log.info("headerMap = {}", headerMap);
log.info("header host ={}", host);
log.info("myCookie = {}", cookie);
return "ok";
}
}
@RequestHeader MultiValueMap<String, String> headerMap: 모든 헤더 정보를 Map으로 조회@RequestHeader("host") String host: 특정 헤더 정보 조회required = true(기본값): 해당 값이 요청에 없으면 400 Bad Request 반환defaultValue설정 시 해당 값이 요청에 없어도 매핑에 성공하며, 설정한 기본값이 파라미터에 주입된다.MultiValueMap: 하나의 키에 여러 값을 List로 저장할 수 있는 자료구조map.get("key")→List<String>반환map.getFirst("key")→ 첫 번째 값만 반환- 동일한 키로 여러 값이 오는 요청에서 사용 (예:
?username=jun&username=kim)
# HTTP 요청 파라미터 조회
- 클라이언트에서 서버로 요청 데이터를 전달하는 방법
- GET 쿼리 파라미터:
http://..?username=park&age=28 - POST HTML Form: HTTP 메시지 바디에 포함
- 두 방식 모두
request.getParameter()로 동일하게 조회 가능
- GET 쿼리 파라미터:
// V1: HttpServletRequest 직접 사용
@RequestMapping("/request-param-v1")
public void requestParamV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
response.getWriter().write("ok");
}
// V2: @RequestParam 사용 - 타입 변환 자동 처리
@ResponseBody
@RequestMapping("/request-param-v2")
public String requestParamV2(
@RequestParam("username") String memberName,
@RequestParam("age") int memberAge
) {
return "ok";
}
@RequestParam: 요청 파라미터를 메서드 파라미터로 바인딩하며 타입 변환도 자동 처리@ResponseBody: 뷰 조회 없이 반환값을 HTTP 메시지 바디에 직접 입력- HTTP 파라미터 이름과 변수명이 같으면
@RequestParam자체도 생략 가능String username→@RequestParam("username") String username과 동일
// required, defaultValue 예시
@ResponseBody
@RequestMapping("/request-param-required")
public String requestParamRequired(
@RequestParam(required = true) String username,
@RequestParam(required = false) Integer age // int → Integer로 변경
) {
return "ok";
}
required = true(기본값): 파라미터 없이 요청 시 400 에러required = false: 파라미터 없으면 null 주입- 기본 타입(
int,long)은 null을 받을 수 없으므로 래퍼 타입(Integer,Long)을 사용해야 함
- 기본 타입(
defaultValue: 파라미터가 없을 때 주입할 기본값 (문자열로만 지정, 파라미터 타입으로 자동 변환)defaultValue설정 시required는 의미 없음 (항상 매핑 성공)- 빈 문자열(
"") 요청도 기본값으로 처리됨
@RequestParam Map<String, Object> paramMap: 전체 파라미터를 Map으로 한번에 조회- 하나의 키에 여러 값이 올 수 있는 경우
MultiValueMap사용
- 하나의 키에 여러 값이 올 수 있는 경우
@ModelAttribute
- 요청 파라미터를 받아 객체를 생성하고 값을 주입하는 과정을
@ModelAttribute로 자동화할 수 있다.
// @ModelAttribute 없이 직접 처리
@RequestParam String username;
@RequestParam int age;
HelloData helloData = new HelloData(username, age);
// @Data: @Getter, @Setter, @ToString, @EqualsAndHashCode, @RequiredArgsConstructor 한번에 적용
@Data
public class HelloData {
private String username;
private int age;
}
// @ModelAttribute 사용
@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
@ModelAttribute동작 순서HelloData객체 생성- 요청 파라미터 이름으로 객체의 프로퍼티 조회
- 해당 프로퍼티의
setter호출하여 값 바인딩- 예:
username파라미터 →setUsername()호출
- 예:
@ModelAttribute도 생략 가능하다. (단,@RequestParam과 혼용 시 혼란스러울 수 있어 명시하는 것이 좋다.)
# HTTP 요청 메시지 바디 조회
- GET 쿼리 파라미터, HTML Form 방식과 달리 HTTP 메시지 바디로 전달되는 텍스트/JSON은 별도 방식으로 읽어야 한다.
// V1: InputStream 직접 사용
@PostMapping("/request-body-string-v1")
public void requestBodyString(HttpServletRequest request, HttpServletResponse response) throws IOException {
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("messageBody={}", messageBody);
response.getWriter().write("ok");
}
// V2: 스프링이 지원하는 InputStream, Writer 파라미터 직접 사용
@PostMapping("/request-body-string-v2")
public void requestBodyStringV2(InputStream inputStream, Writer responseWriter) throws IOException {
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("messageBody={}", messageBody);
responseWriter.write("ok");
}
// V3: HttpEntity 사용
@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) {
String messageBody = httpEntity.getBody();
log.info("messageBody={}", messageBody);
return new HttpEntity<>("ok");
}
HttpEntity: HTTP 헤더 + 바디를 편리하게 조회/반환 가능- 요청 파라미터 조회 불가 (쿼리 파라미터, Form 데이터는
@RequestParam사용) - 응답으로 사용 시 뷰 조회 없이 메시지 바디에 직접 입력
- 요청 파라미터 조회 불가 (쿼리 파라미터, Form 데이터는
RequestEntity:HttpEntity상속, HTTP 메서드 + URL 정보 추가 (요청에 사용)ResponseEntity:HttpEntity상속, HTTP 상태코드 설정 가능 (응답에 사용)return new ResponseEntity<>("Hello", responseHeaders, HttpStatus.CREATED)
// V4: @RequestBody 사용 (가장 간편)
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) {
log.info("messageBody={}", messageBody);
return "ok";
}
@RequestBody: HTTP 메시지 바디를 직접 파라미터에 바인딩- 헤더 정보가 필요한 경우
@RequestHeader또는HttpEntity사용
# HTTP JSON 데이터 형식 조회
// V1: InputStream + ObjectMapper 직접 사용
@PostMapping("/request-body-json-v1")
public void requestBodyJsonV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
HelloData data = objectMapper.readValue(messageBody, HelloData.class);
log.info("username={}, age={}", data.getUsername(), data.getAge());
response.getWriter().write("ok");
}
// V2: @RequestBody로 바디 문자열 조회 후 ObjectMapper로 변환
@ResponseBody
@PostMapping("/request-body-json-v2")
public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException {
HelloData data = objectMapper.readValue(messageBody, HelloData.class);
log.info("username={}, age={}", data.getUsername(), data.getAge());
return "ok";
}
// V3: @RequestBody에 객체 타입 지정 → HttpMessageConverter가 JSON을 객체로 자동 변환
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData data) {
log.info("username={}, age={}", data.getUsername(), data.getAge());
return "ok";
}
// V4: HttpEntity 제네릭 타입으로 객체 지정
@ResponseBody
@PostMapping("/request-body-json-v4")
public String requestBodyJsonV4(HttpEntity<HelloData> httpEntity) {
HelloData data = httpEntity.getBody();
log.info("username={}, age={}", data.getUsername(), data.getAge());
return "ok";
}
- V3, V4처럼 객체 타입을 지정하면
HttpMessageConverter가 JSON을 객체로 자동 변환해준다.- 요청 헤더의
Content-Type: application/json이 반드시 필요하다.
- 요청 헤더의
@ResponseBody로 객체를 반환하면HttpMessageConverter가 객체를 JSON으로 자동 변환하여 응답 바디에 입력한다.
@RestController
@Controller+@ResponseBody조합으로, 클래스 내 모든 메서드에@ResponseBody가 적용된다.- 뷰 템플릿을 사용하지 않고 HTTP 메시지 바디에 데이터를 직접 입력하는 방식으로 동작한다.
- 현재 REST API 서버 개발의 표준 방식이다.
# HTTP 메시지 컨버터
- 뷰 템플릿으로 HTML을 생성하여 응답하는 것이 아닌 HTTP API처럼 JSON 데이터를 HTTP 메시지 바디에 직접 읽거나 쓰는 경우 HTTP 메시지 컨버터를 사용하면 편리하다.
- @ResponseBody 사용 원리는 다음과 같다.
- HTTP 바디에 문자 내용을 직접 반환한다.
- viewResolver 대신 HttpMessageConverter가 동작한다.
- plain text 처리는
StringHttpMessageConverter - 기본 객체 처리는 (JSON)
MappingJackson2HttpMessageConverter - 바이트 처리 등 (byte[]) HttpMessageConverter가 기본 등록되어 있음
- 스프링 MVC는 다음 경우에 HTTP 메시지 컨버터를 적용한다.
- HTTP 요청: @RequestBody, HttpEntity(RequestEntity)
- HTTP 응답: @ResponseBody, HttpEntity(ResponseEntity)
- 메시지 컨버터는 대상 클래스 타입과 미디어타입을 체크하여 사용여부를 결정한다.
- 여러 컨버터 타입이 있는데, 해당되지 않으면 다음 순위로 넘어간다.
- 요청 데이터를 읽는 과정은 다음과 같다.
- HTTP 요청이 오고 컨트롤러에서 @RequestBody, HttpEntity 파라미터를 사용한다.
- 메시지 컨버터가 메시지를 읽을 수 있는지 canRead 메서드를 내부적으로 호출한다.
- byte[], String, 커스텀 클래스 타입 지원여부
- HTTP Content-Type 미디어 타입 체크 (text/plain, application/json)
- canRead 조건을 만족하면 read를 호출하여 객체 생성 후 반환
- 응답 데이터를 생성하는 과정은 다음과 같다.
@ResponseBody,HttpEntity로 값 반환- 메시지 컨버터가 메시지를 쓸수있는지 확인하기 위해 canWrite 호출
- 클래스 타입 지원여부
- 클라이언트 요청 헤더의 Accept 미디어 타입 지원여부
- 조건 만족시 write 호출 후 메시지 바디에 데이터 쓰기
# RequestMappingHandlerAdapter 구조
RequestMappingHandlerAdapter가 HTTP 메시지 컨버터를 포함한 전체 처리를 담당한다.동작 흐름
- ArgumentResolver: 컨트롤러 메서드의 파라미터 타입과 어노테이션을 분석하여 필요한 데이터를 생성 및 주입
- 지원 타입:
HttpServletRequest,Model,@RequestParam,@ModelAttribute,@RequestBody,HttpEntity등 @RequestBody,HttpEntity처리 시 내부적으로HttpMessageConverter호출
- 지원 타입:
- 핸들러(컨트롤러 메서드) 호출
- ReturnValueHandler: 컨트롤러 반환값을 변환하여 응답 생성
- 지원 타입:
ModelAndView,@ResponseBody,HttpEntity등 @ResponseBody,HttpEntity처리 시 내부적으로HttpMessageConverter호출
- 지원 타입:
- ArgumentResolver: 컨트롤러 메서드의 파라미터 타입과 어노테이션을 분석하여 필요한 데이터를 생성 및 주입
HttpMessageConverter는ArgumentResolver와ReturnValueHandler양쪽에서 호출된다.- 요청:
Content-Type헤더 기반으로 컨버터 선택 → JSON을 객체로 변환 - 응답:
Accept헤더 기반으로 컨버터 선택 → 객체를 JSON으로 변환
- 요청:
ArgumentResolver
- 어노테이션 기반 컨트롤러가 다양한 파라미터를 유연하게 지원할 수 있는 이유는
ArgumentResolver덕분이다.supportsParameter()호출 → 해당 파라미터 타입/어노테이션 지원 여부 확인- 지원 시
resolveArgument()호출 → 실제 객체 생성 - 생성된 객체를 컨트롤러 메서드 호출 시 파라미터로 전달
ReturnValueHandler도 유사하게 동작한다.supportsReturnType()호출 → 반환 타입 지원 여부 확인- 지원 시
handleReturnValue()호출 → 반환값 변환 및 응답 처리
- HTTP 메시지 컨버터는 ArgumentResolver, ReturnValueHandler에 위치하고 있다.
웹 브라우저 새로고침
- 웹 브라우저의 새로고침은 마지막으로 서버에 전송한 요청을 다시 전송한다.
- 마지막 요청이 POST였다면 새로고침 시 중복 POST 요청이 발생한다.
- 이를 해결하기 위해 PRG(Post-Redirect-Get) 패턴을 사용한다.
- POST 처리 후 리다이렉트로 GET 요청을 유도하면, 새로고침 시 GET 요청만 반복된다.
- 리다이렉트 시 동적 값을 URL에 포함하려면
RedirectAttributes를 사용한다.- 직접 문자열로 URL을 조합하면(
"/basic/items/" + item.getId()) 인코딩 문제로 오동작할 수 있음
- 직접 문자열로 URL을 조합하면(
@PostMapping("/add")
public String addItem(
@ModelAttribute Item item,
RedirectAttributes redirectAttributes
) {
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId()); // URL 템플릿 {itemId}에 바인딩
redirectAttributes.addAttribute("status", true); // URL 템플릿에 없으면 쿼리 파라미터로 자동 추가
return "redirect:/basic/items/{itemId}"; // → /basic/items/3?status=true
}
addAttribute동작 방식- URL 템플릿(
{변수명})에 해당하는 값 → URL에 바인딩 - URL 템플릿에 없는 값 → 쿼리 파라미터로 자동 추가
- URL 템플릿(
- URL에 노출시키지 않으려면
addFlashAttribute사용 (세션에 임시 저장, 리다이렉트 후 1회 사용 후 자동 삭제)
# 자주 쓰이는 Lombok 어노테이션
| 어노테이션 | 설명 | 주요 사용처 |
|---|---|---|
@Getter | 모든 필드의 getter 메서드 생성 | 엔티티, DTO |
@Setter | 모든 필드의 setter 메서드 생성 | 지양 (엔티티에는 사용 자제) |
@NoArgsConstructor | 파라미터 없는 기본 생성자 생성 | JPA 엔티티 (필수) |
@AllArgsConstructor | 모든 필드를 파라미터로 받는 생성자 생성 | DTO |
@RequiredArgsConstructor | final / @NonNull 필드 생성자 생성 | Service, Repository (의존성 주입) |
@Builder | 빌더 패턴 자동 생성 | 엔티티, DTO 생성 시 |
@ToString | toString() 메서드 자동 생성 | 디버깅 (양방향 연관관계 주의) |
@EqualsAndHashCode | equals(), hashCode() 자동 생성 | (양방향 연관관계 주의) |
@Data | @Getter + @Setter + @ToString + @EqualsAndHashCode + @RequiredArgsConstructor | 실무에서 지양 |
@Slf4j | log 변수 자동 생성 (Logger) | 모든 계층 |
# 실무에서 자주 쓰는 조합
// Service - 의존성 주입
@Service
@RequiredArgsConstructor
@Slf4j
public class ItemService {
private final ItemRepository itemRepository;
}
// JPA 엔티티
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class Item {
@Id @GeneratedValue
private Long id;
private String itemName;
}
// DTO
@Getter
@AllArgsConstructor
public class ItemResponse {
private Long id;
private String itemName;
public static ItemResponse from(Item item) {
return new ItemResponse(item.getId(), item.getItemName());
}
}