웹 서버와 웹 애플리케이션 서버
웹 서버 Web Server |
웹 애플리케이션 서버 Web Application Server |
|
공통점 | HTTP 기반으로 동작, 정적 리소스 제공 | |
차이점 | 정적 리소스만 제공 | 웹 서버 기능을 포함하고 동적 리소스까지 제공한다. |
결론 | WAS / DB로 구성 시 WAS에 너무 많은 역할을 담당하므로 웹 서버와 웹 애플리케이션 서버로 나눠 역할을 분담해야 한다. 분담 시 정적 리소스는 웹 서버에서 처리하며, 동적인 처리가 필요하면 WAS에서 동적 리소스를 처리 |
WEB / WAS / DB 분담 시 장점
- 효율적인 리소스 관리 (정적 리소스가 많이 사용되면 Web Server를 , 동적 리소스가 많이 사용되면 WAS를 증설하면 됨)
- 오류를 방지 (정적 리소스를 담당하는 Web Server는 되도록 서버가 죽지 않는다.)
서블릿 로직
이처럼 서버에서 처리해야 할 일은 매우 많다. 하지만 서블릿을 사용하면 [비지니스 로직실행] 부분만 구현하면 된다.!
사진과 같이 HTTP 요청 정보를 편리하게 사용할 수 있는 HttpServletRequset와 응답 정보가 담긴 HttpServletResponse를 통해 HTTP 스펙을 매우 편리하게 사용할 수 있다.
멀티 쓰레드
쓰레드란 애플리케이션 코드를 하나씩 순차적으로 실행시키는 것이다. 동시 처리 필요시 쓰레드를 추가로 생성한다.
단 요청 시마다 쓰레드를 생성하면 비용뿐만 아니라 응답속도, 스위칭 비용이 크게 발생한다. 또한 개수의 제한이 없어지기 때문에 오류가 발생할 수 있다. 그래서 이러한 문제들을 해결하기 위해 쓰레드 풀을 사용한다.
쓰레드 풀
- 필요한 쓰레드를 쓰레드 풀에 보관하여 관리하는 방식
- 쓰레드 사용이 끝나면 쓰레드 풀에 해당 쓰레드를 반납한다.
- 생성과 종료 비용이 감소하고 응답시간이 빨라진다.
- 생성 가능한 쓰레드의 제한(max thread)이 있으므로 안전하게 처리가 가능해진다.
HTTP API
HTTP API는 HTML 코드가 아닌 데이터를 제공하는 방식이다. 주로 json 데이터 형식으로 제공한다.
SSR과 CSR
SSR 서버 사이드 렌더링 |
CSR 클라이언트 사이드 렌더링 |
|
특징1 | HTML 결과를 서버에서 만들어서 웹 브라우저에 전달 | HTML 결과를 JS를 사용하여 웹 브라우저에서 동적으로 생성하여 적용 |
특징 2 | 주로 정적인 화면에 사용 | 주로 동적인 화면에 사용 |
서블릿
1. 스프링 부트 서블릿 환경 구성
@ServletComponentScan을 ServletApplication에 부착!
@ServletComponentScan
@SpringBootApplication
public class ServletApplication {
public static void main(String[] args) {
SpringApplication.run(ServletApplication.class, args);
}
}
- @ServletComponentScans를 통해 서블릿을 직접 등록하여 사용할 수 있다. 애플리케이션 시작 시 서블릿을 자동 등록해 준다.
2. 서블릿 등록
@WebServlet을 통해 서블릿을 등록!
@WebServlet(name = "responseHeaderServlet",urlPatterns = "/response-header")
public class ResponseHeaderServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
... 구현
}}
- @WebServlet은 서블릿 선언 시 사용한다.
- HttpServlet은 HTTP 관련 기능을 처리하기 위한 메서드를 제공하는 추상 클래스
- service()는 클라이언트의 요청을 처리하는 메서드
HttpServletRequset
HTTP 요청 메세지를 파싱하고 그 결과를 HttpServletRequset 객체에 담아서 제공한다.
HTTP 메세지의 start-line, header 정보를 조회할 수 있다.
HTTP 데이터 전달 방법 (클라이언트->서버)
- GET - 쿼리 파라미터
- 메세지 바디 없이, URL의 쿼리 파라미터에 데이터를 포함하여 전달한다.
- POST - HTML Form
- 메세지 바디에 쿼리 파라미터 형식을 전달한다.
- 메시지 바디안에 Content-type이 포함된다.
- HTTP API
- 메세지 바디안에 데이터를 직접 담아서 요청
- 데이터는 주로 JSON 형식이다.
1. GET 쿼리 파라미터
쿼리 파리미터는 ?으로 시작하여 key=value 형식으로 구성되며 &를 기준으로 값이 구분된다.
쿼리 파라미터 조회 메서드
String username = request.getParameter("username"); //단일 파라미터 조회
Enumeration<String> parameterNames = request.getParameterNames(); //파라미터 이름들 모두 조회
Map<String, String[]> parameterMap = request.getParameterMap(); //파라미터를 Map으로 조회
String[] usernames = request.getParameterValues("username"); //복수 파라미터 조회
주로 getParameter() 을 통해 데이터를 조회
2. POST HTML Form
GET 쿼리 파라미터와 동일하게 조회 메서드를 사용할 수 있다. 단 HTTP 메세지 바디에 해당 데이터를 포함해서 보내기 때문에 데이터의 형식(content-type)을 반드시 지정해줘야 한다. 이렇게 Form으로 데이터를 전달하는 형식을 application/x-www-form-urlencoded라고 한다.
3. HTTP API
메세지 바디의 데이터를 읽거나 전송하는 방법, 주로 JSON 형식이다.
HTTP 메세지 바디의 데이터를 읽는 방법
public class RequestBodyJsonServlet extends HttpServlet {
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);
Human h = objectMapper.readValue(messageBody, Human.class);
System.out.println("사람 이름은 = " + h.getName());
resp.getWriter().write("ok");
}}
- ServletInputStream을 통해 데이터를 읽을 수 있다
- InputStream은 바이트 코드를 반환하며, 바이트 코드를 문자로 해석하기 위해 Charset을 지정해줘야 한다.
- JSON 결과를 파싱하여 객체로 변환하려면 ObjectMapper를 사용하면 된다.
HttpServletResponse
HTTP 응답 메세지를 파싱하고 그 결과를 HttpServletResponse 객체에 담아서 제공한다.
HTTP 메세지의 상태 코드, Content-Type 등을 지정하여 반환할 수 있다.
쿠키를 보낼 때
Cookie cookie = new Cookie("Cookie", "myInfo");
cookie.setMaxAge(600); //600초
response.addCookie(cookie);
리다이렉션 구현
//Status Code 302
response.sendRedirect("리다이렉션 할 경로");
응답 데이터 JSON일 때
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void service ....생략 {
response.setHeader("content-type", "application/json");
Human h = new Human();
data.setUsername("kim");
data.setAge(20);
//{"username":"kim","age":20}
String result = objectMapper.writeValueAsString(h);
response.getWriter().write(result);
}
- Content-type을 application/json으로 지정해줘야 한다.
- ObjectMapper writeValueAsString()을 통해 자바 객체를 JSON 문자열으로 변환시켜 준다.
JSP
서블릿과 자바로만 HTML을 구성하면 매우 불편하고 비효율적이다. 따라서 HTML 안에서 동적으로 변하는 부분만을 자바 코드를 통해 구성할 수 있는 방법을 사용하면 더 편리할 것이다. 이것이 JSP를 사용하는 이유이다.
JSP 라이브러리 추가 (build.gradle)
/JSP 추가 시작
implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
implementation 'jakarta.servlet:jakarta.servlet-api' //스프링부트 3.0 이상
implementation 'jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api' //스프링부트
3.0 이상
implementation 'org.glassfish.web:jakarta.servlet.jsp.jstl' //스프링부트 3.0 이상
//JSP 추가 끝
JSP 특징
- <% ~~ %> : 자바 코드를 입력하는 공간
- <%= ~~%> : 자바 코드를 통해 값을 출력하는 공간
JSP의 한계
JSP을 통해 기존 서블릿의 단점들을 해결하였다. 하지만 하나의 서블릿과 JSP 만으로 비지니스 로직과 뷰 렌더링까지 모두 처리하면 너무 역할을 처리하기 때문에 결과적으로 유지보수가 어려워진다. 그리하여 이러한 점들을 해결하기 위해 MVC 패턴이 등장하였다.
MVC 패턴
서블릿이나 JSP로 처리하던 것을 컨트롤러, 뷰, 모델로 나눈 것이 MVC 패턴이다.
- 컨트롤러 : HTTP 요청을 받아 비지니스 로직 처리를 한다. 이후 뷰에 전달할 결과 데이터를 모델에 담는다.
- 컨트롤러에 많은 역할이 주어지기 때문에 서비스라는 계층을 별도로 만들어 처리한다.
- 컨트롤러는 서비스를 호출하는 역할을 담당한다.
- 모델 : 뷰에 출력할 데이터를 담아둔다.
- 뷰 : 모델에 담겨있는 데이터를 통해 화면을 렌더링한다.
이를 통해 뷰는 결과적으로 비지니스 로직처리를 몰라도 되고, 오직 화면을 렌더링하는 일에만 집중할 수 있다.
데이터 보관 방법
- request.setAttribute("data",Data) : requset 객체에 데이터를 보관하여 뷰에 전달하는 함수
- requset.getAttribute("data") : 데이터를 호출
- JSP에서는 ${data} 을 통해 데이터를 편하게 접근 가능
경로 이동
setAttribute()를 데이터를 저장하고 저장한 값을 지정 경로에서 setAttribute()를 통해 확인
String viewPath = "/WEB-INF/..경로";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
- /WEB-INF : 해당 경로안에서 JSP가 존재하면 외부에서 JSP를 호출할 수 없다. 무조건 컨트롤러를 통해서만 접근할 수 있다.
- RequsetDispatcher 클래스는 서블릿 컨테이너 내에서 다른 리소스(서블릿, JSP 등)로의 제어를 전달하는 데 사용된다.
- forward() 는 현재 서블릿이나 JSP에서 다른 자원으로 제어를 넘기는 함수
redirect 와 forward 의 차이
리다이렉트는 실제 클라이언트로 응답이 나갔다가 redirect 경로로 다시 요청하기 때문에 클라이언트에서 인지할 수 있지만 포워드는 서버에서 처리가 되는 호출이기 때문에 클라이언트는 전혀 인지할 수 없다.
MVC 패턴의 한계
이처럼 비지니스 로직처리와 뷰 렌더링 역할이 명확하게 분리됐지만 중복된 코드들이 많고, 사용하지 않는 코드들이 많다 이러한 문제들을 해결하기 위해 공통 기능을 처리해주는 프론트 컨트롤러 패턴을 사용한다.
MVC 프레임워크
MVC 컨트롤러의 단점
- forward() 함수 중복
- 공통 경로 (viewPath) 중복
- HttpServletRequst/Response 코드 중복
- 공통처리가 어려움
등 다양한 문제점들이 있다 이러한 문제점들을 프론트 컨트롤러를 통해 V1부터 V5까지 하나하나씩 해결해보자!
프론트 컨트롤러
컨트롤러 서블릿 하나로 클라이언트의 요청을 받아 요청에 맞는 컨트롤러를 찾아서 호출해준다.
이를 통해 공통 처리가 가능해진다.!!
V1 - 공통처리
프론트 컨트롤러 인터페이스 생성 -> 각 컨트롤러에서 인터페이스 구현 -> 프론트 컨트롤러에서는 인터페이스를 호출하여 구현과 관계없이 일관성을 가져갈 수 있다.
V1 - 핵심 코드 - save 클래스
@Override
public void process(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//given
String username= request.getParameter("username");
int age=Integer.parseInt(request.getParameter("age"));
Member member = new Member(username, age);
//then
memberRepository.save(member);
request.setAttribute("member",member);
//when
String viewPath="/WEB-INF/views/save-result.jsp";
RequestDispatcher requestDispatcher = request.getRequestDispatcher(viewPath);
requestDispatcher.forward(request,response);
}
V1 - 핵심 코드 - 프론트 컨트롤러 클래스
@WebServlet(name = "frontControllerV1",urlPatterns = "/front-controller/v1/*")
public class FrontControllerV1 extends HttpServlet {
private Map<String,ControllerV1> controllerV1Map=new HashMap<>();
public FrontControllerV1() {
controllerV1Map.put("/front-controller/v1/members/new-form",new MemberFormControllerV1());
controllerV1Map.put("/front-controller/v1/members/save",new MemberSaveControllerV1());
controllerV1Map.put("/front-controller/v1/members",new MemberListControllerV1());
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
System.out.println("FrontControllerV1.service");
String requestURI = req.getRequestURI();
ControllerV1 controllerV1 = controllerV1Map.get(requestURI);
if(controllerV1==null){
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
controllerV1.process(req,resp);
}
}
- controllerV1 인터페이스를 통해 프론트 컨트롤러에서 접근이 가능해진다.
- put() 함수를 통해 각 URL에 대응하는 컨트롤러 인스턴스를 추가한다.
- get() 함수를 통해 URL에 대응하는 컨트롤러 인스턴스를 찾는다.
- 프론트 컨트롤러를 만드는 방법은 urlPatterns 의 * 표시이다.
V2 - View 분리
각 컨트롤러마다 뷰에 이동하는 코드가 존재하므로 중복 코드가 많다. 이를 공통 처리하기 위해 MyView를 반환시킨다.
V2 - 핵심 코드 - save 클래스
@Override
public MyView process(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// .. 생략 ..
return new MyView("/WEB-INF/views/save-result.jsp")
}
V2 - 핵심 코드 - MyView 클래스 생성
public class MyView {
private String viewPath;
public void render(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException{
RequestDispatcher requestDispatcher = request.getRequestDispatcher(viewPath);
requestDispatcher.forward(request,response);
}
}
- 공통 부분을 처리하기 위해 MyVeiw 클래스를 만들어 render() 함수에 뷰에 이동 코드를 적용했다.
V2 - 핵심 코드 - 프론트 컨트롤러의 service 함수
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
//.... 생략 ...
MyView view= controllerV2.process(req,resp);
view.render(req,resp);
}
- 각각의 컨트롤러는 Myview 객체를 생성하여 반환만 하면 된다.
V3 - 서블릿 종속성 제거, 뷰 이름 중복 제거
각 컨트롤러마다 HttpServletRequset, HttpServletResponse 가 중복되기 때문에 요청 파라미터의 정보는 Map으로 넘긴다.
또한 뷰의 이름이 중복되는 부분들이 많기 때문에 함수(viewResolver)를 만들어 중복 코드를 줄인다.
V3 - 핵심 코드 - save 클래스
@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 modelView = new ModelView("save-result");
modelView.getModel().put("member",member);
return modelView;
}
- HttpServletRequest/Response 를 받지 않고 paramMap의 get()을 통해 데이터를 전달 받는다.
- 기존 setAttribute() 대신 put() 함수를 통해 ModelView에 데이터를 넣는다.
V3 - 핵심 코드 - ModelView 클래스
public class ModelView {
private String viewName;
private Map<String,Object> model= new HashMap<>();
public ModelView(String viewName) {
this.viewName = viewName;
}
// ..get() .. set() 생략
}
- ModelView 클래스를 통해 requset 나 response 의 데이터를 주고 받는다.
V3 - 핵심 코드 - 프론트 컨트롤러의 service 함수
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// .. 생략 ..
Map<String, String> paramMap = createParamMap(req);
ModelView mv= controllerV3.process(paramMap);
String viewName = mv.getViewName();
MyView myView = viewResolver(viewName);
myView.render(mv.getModel(), req,resp);
}
- createParamMap() 은 반복문을 통해 파라미터의 값들을 Map<String,String> 형식으로 변경해주는 함수이다.
- viewResolver() 은 경로와 확장자를 붙여주는 함수이다.
- createParamMap()을 통해 Map 형식으로 데이터를 전달하고 viewResolver()를 통해 반환하는 뷰의 이름을 최소화했다.
V4 - ModelView 클래스 없애기
V3과 구조는 같지만 컨트롤러가 ModelView 대신 ViewName을 반환한다.
V4 - 핵심 코드 - Save 클래스
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
// .. 생략 ..
model.put("member",member);
return "save-result";
}
- ModelView 대신 model에 데이터를 담는다.
V4 - 핵심 코드 - 프론트 컨트롤러의 service 함수
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
//.. 생략 ..
Map<String, String> paramMap = createParamMap(req);
Map<String,Object> model=new HashMap<>();
String viewName = controllerV4.process(paramMap, model);
MyView myView = viewResolver(viewName);
myView.render(model,req,resp);
}
- MyView 클래스 render() 에 model 매개 변수를 추가하여 값을 받아 처리하도록 함수를 생성
- ModelView 클래스가 없기 때문에 MyView에 viewName과 model 데이터를 넣어준다.
V5 - 유연한 컨트롤러
V1부터 V4까지의 프론트 컨트롤러는 단 한가지의 인터페이스만 사용할 수 있었다. 이처럼 인터페이스끼리 호환이 불가능하다는 단점이 있다. 이를 해결해기 위해 어댑터 패턴을 사용한다.
- 핸들러 어댑터 : 다양한 종류의 컨트롤러를 호출하게 해준다.
- 어댑터 패턴 : 다양한 방식의 컨트롤러를 처리할 수 있게 해주는 패턴
V5 - 핵심 코드 - 어댑터 클래스( V3과 V5를 연결시켜주는 클래스)
@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;
}
- supports ()는 어댑터가 해당 컨트롤러의 처리 가능여부를 판단해준다.
- handle()는 실제 컨트롤러를 호출하는 함수이다 . 반환 형이 ModelView이므로 ModelView로 반환해야한다.
V5 - 핵심 코드 - 프론트 컨트롤러의 핸들러를 찾는 함수
private MyHandlerAdapter getHandlerAdapter(Object handler) {
for (MyHandlerAdapter handlerAdapter : handlerAdapters) {
if(handlerAdapter.supports(handler)){
return handlerAdapter;
}
}
throw new IllegalArgumentException("handler adapter를 찾을수 없습니다. handler="+handler);
}
- handler를 처리할 수 있는 어댑터를 supports()를 통해 찾는다.
V1 ~ V5 정리
V1 | V2 | V3 | V4 | v5 |
프론트 컨트롤러 도입 | View 분류 반복 뷰 로직 분리 |
Model 추가 서블릿 종속성, 뷰 이름 중복 제거 |
단순한 컨트롤러 생성 ModelView를 생성하지 않음 |
유연한 컨트롤러 어댑터를 통해 유연하고 확장성 있게 설계 |
Spring MVC의 구조
직접 만든 MVC 프레임워크 구조
Spring MVC 구조
DispatcherServlet 서블릿 등록
org.springframework.web.servlet.DispatcherServlet
Spring MVC의 프론트 컨트롤러가 바로 디스패처 서블릿이다.
DispatcherServlet 또한 부모 클래스에서 HttpServlet을 상속받아서 사용한다.
스프링 부트는 DispatcherServlet을 서블릿으로 자동으로 등록하면서 모든경로(urlPatterns="/") 에 대해서 매핑한다.
Spring MVC의 동작 순서
1. 핸들러 조회
핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러)를 조회한다.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
2. 핸들러 어댑터 조회
핸들러를 실행할 수 있는 핸들러 어댑터를 조회한다.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
3. 핸들러 어댑터 실행 -> 4. 핸들러 실행 -> 5. ModelAndView 반환
4. 핸들러 어댑터가 실제 핸들러를 실행한다. 5.핸들러 어댑터는 데이터를 ModelAndView 형태로 변환해서 반환한다.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
6. viewResolver 호출 -> 7. View 반환
view의 논리 이름을 물리 이름으로 바꾸고 뷰 객체를 반환한다.
view = resolveViewName(viewName, mv.getModelInternal(), locale, request)
8. 뷰 렌더링
view.render(mv.getModelInternal(), request, response);
주요 구조
스프링 mvc구조 에서 컨트롤러가 실행되기 위해서는 핸들러 매핑과 핸들러 어댑터가 반드시 필요하다.
HandlerMapping
핸들러 매핑에서 해당하는 컨트롤러를 찾을 수 있어야 한다.
우선 순위 | 이름 | 설명 |
0 | RequestMappingHandlerMapping | @기반의 컨트롤러인 @RequestMapping에서 사용 |
1 | BeanNameUrlHandlerMapping | 스프링 빈의 이름으로 핸들러를 찾음 |
HandlerAdapter
핸들러 매핑을 통해서 찾은 핸들러를 실행할 수 있는 핸들러 어댑터가 필요하다.
우선 순위 | 이름 | 설명 |
0 | RequestMappingHandlerAdapter | @기반의 컨트롤러인 @RequestMapping에서 사용 |
1 | HttpRequestHandlerAdapter | HttpRequestHandler로 처리 |
2 | SimpleControllerHandlerAdapter | Controller 인터페이스로 처리 |
ViewResolver
스프링 부트는 InternalResourceViewResolver 라는 뷰 리졸버를 자동으로 등록하는데, 이때
application.properties 에 등록한 spring.mvc.view.prefix , spring.mvc.view.suffix 설정 정보를 사용해서 등록한다.
우선 순위 | 이름 | 설명 |
0 | BeanNameViewResolver | 빈 이름으로 뷰를 찾아서 반환 |
1 | InternalResourceViewResolver | JSP를 처리할 수 있는 뷰를 반환 |
실용적인 스프링 MVC
클래스 단위가 아닌 메서드 단위로 컨트롤러 클래스를 유연하게 하나로 통합할 수 있다.
@Controller
@RequestMapping("통합 경로")
public class SpringMemberController {
private MemberRepository memberRepository=MemberRepository.getInstance();
@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";
}
}
@RequestMapping
매우 유연하고 실용적인 애노테이션기반의 컨트롤러를 지원하는 핸들러 매핑과 어댑터로 실무에서는 대부분 이 방식의 컨트롤러를 사용한다.
- 요청 정보를 매핑한다. 해당 URL 호출 시 해당 메서드가 호출된다.
- 클래스 레벨에 @RequestMapping을 두면 메서드 레벨과 조합이 돼어 중복 코드를 줄일 수 있다.
- @RequestMapping은 @GetMapping과 @PostMapping으로 나눌 수있다.
@Controller
스프링이 자동으로 스프링 빈으로 등록하여 컨트롤러로 인식되게 한다.
Model
원래는 ModelAndView로 직접 생성하고 반환했지만 Model을 사용하여 값을 넘겨줄 수 있다.반환 값 또한 viewName을 직접 반환할 수 있다.
@RequestParam
HTTP 요청 파라미터를 @RequestParam 을 통해 매개변수로 받을 수있다.
로그 라이브러리
여러 로그 라이브러리를 통합해서 인터페이스로 제공하는 것이 SLF4J 라이브러리이다.
LOG LEVEL
로그 레벨을 application.properties 설정 파일에서 레벨을 설정할 수 있다. 설정 시 해당 레벨부터 하위 레벨까지만 출력된다.
#전체 로그 레벨 설정(기본 info)
logging.level.root=info
LEVEL : TRACE > DEBUG > INFO > WARN > ERROR
올바른 로그 사용법
{}을 사용하여 로그를 출력해야 한다. 의미없는 연산을 발생하지 않기 때문이다.
만약 "data="+data 와 같이 로그 출력 시 해당 로그를 사용하지 않으면 연산이 발생하기 때문이다.
log.debug("data={}", data)
로그 사용 시 장점
- 쓰레드나 클래스 이름같은 상세 정보를 확인할 수 있다.
- 로그 레벨을 통해 로그를 상황에 맞게 조절할 수 있다.
- System.out 보다 성능이 훨씬 좋다.
요청 매핑
@RestController
기존 @Controller는 반환 값이 String이면 뷰 이름을 찾는 것으로 인식하여 뷰를 찾고 랜더링된다. 반면 @RestController는 HTTP 메세지 바디에 바로 입력이 된다.
PathVariable(경로 변수)
경로 변수를 사용하여 URL 경로를 유연하게 매개변수 처럼 사용할 수 있다.
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String data) {
log.info("mappingPath userId={}", data);
return "ok";
}
미디어 타입 조건 매핑
HTTP 요청의 헤더(Content-Type, Accept)를 기반으로 미디어 타입을 매핑한다.
1. HTTP 요청 Content-Type - consume
@PostMapping(value = "/mapping-consume", consumes = "application/json")
- 컨트롤러가 처리할 수 있는 요청 타입을 제한하는 것
- 타입에 맞지 않으면 HTTP 415 상태코드를 반환한다.
2. HTTP 요청 Accept - produce
@PostMapping(value = "/mapping-produce", produces = "text/html")
- 클라이언트에게 응답할 만들어진 응답의 헤더를 제한하는 것
- 클라이언트 입장에서 자신이 받을 수 있는 content-type을 서버로 전달해준 것이라고 생각
- 타입에 맞지 않으면 HTTP 406 상태코드를 반환한다.
HTTP 요청 파라미터
@RequestParam
public String requestParamDefault(
@RequestParam(required = false) String username,
@RequestParam(defaultValue = "-1") int age){
log.info("username={}, age={}",username,age);
return "ok";
}
- String, int, Integer 등의 단순 타입 시 @RequestParam 생략 가능
- required : 파라미터 여부를 지정해 주는 설정 값으로 기본이 true이며 false 설정 시 값이 없으면 null이 반환된다.
- true 시 파라미터 이름만 사용하고 값이 없는 경우 빈문자로 반환된다.
- false 시 int가 null이 될수 없으므로 int를 Integer로 변경하거나 , defalutValue를 사용해야 함
- defalutValue : 파라미터에 값이 없는 경우 기본값을 적용할 수 있다.
@ModelAttribute
public String modelAttributeV2(@ModelAttribute HelloData data){
log.info("username={}, age={}",data.getUsername(),data.getAge());
return "ok";
}
- @ModelAttribute는 생략 가능하며, @RequestParam과 헷갈리지 않게 조심해야 한다.
- 단순 타입을 제외한 모든 타입은 @ModelAttribute로 적용된다.
- 전달 받은 값을 set()을 통해 하나하나 넣지 않아도 자동으로 data 객체를 생성하고 값이 할당된다.
- Model에 @ModealAttribute로 지정한 객체를 자동으로 넣어준다. (첫 글자만 소문자로 등록)
단순 텍스트 (JSON)
HTTP Body를 통해 직접 넘어오는 경우에는 @RequestParam과 @ModelAttribute를 사용할 수없다.
1. HttpEntity
public String RequestBodyJsonV4(HttpEntity<HelloData> helloData) throws IOException {
HelloData data=helloData.getBody();
log.info("messageBody = {}" ,helloData);
log.info("username={} age={}",data.getUsername(),data.getAge());
return "ok";
}
- HTTP 메세지 바디나 헤더 정보를 편리하게 조회가 가능하다.
- 응답에도 사용이 가능하다.
2. @RequestBody
public HelloData RequestBodyJsonV5(@RequestBody HelloData helloData) throws IOException {
log.info("username={} age={}",helloData.getUsername(),helloData.getAge());
return helloData;
}
- 편리하게 HTTP 메세지 바디 정보를 조회할 수 있다.
- @RequestBody는 생략 불가능하다.
HTTP 메세지 컨버터
HTTP API 처럼 JSON 데이터를 HTTP 메세지 바디에서 직접 쓰거나 읽는 경우 HTTP 메세지 컨버터를 사용하면 편리하다.
@ResponseBody 사용 원리
- HTTP 바디의 문자 내용을 직접 반환한다.
- viewResolver 대신 HttpMessageConverter이 동작된다.
MessageConverter
대상 클래스 타입과 미디어 타입 둘을 체크하여 사용 여부를 결정한다.
우선 순위 | 이름 | 설명 | 클래스 타입 | 미디어타입 |
0 | ByteArray | byte 데이터 처리 | byte[] | */* |
1 | StringHttp | String 데이터 처리 | String | */* |
2 | MappingJackson2 | JSON 데이터 처리 | 객체 또는 HashMap | JSON |
요청 매핑 핸들러 어댑터 구조
HTTP 메세지 컨버터는 핸들러 어댑터에서 처리가 된다.
RequestMappingHandlerAdapter 동작 방식
ArgumentResolver를 호출하여서 핸들러가 필요로 하는 객체를 생성하고 값을 반환해준다.
ArgumentResolver
- 핸들러가 필요로 하는 다양한 파라미터의 값을 생성해주는 인터페이스
- 동작 방식은 supportsParameter() 을 호출하여 해당 파라미터를 지원하는 체크하고 지원하면 resolverArgument()를 호출하여 객체를 생성한다.
- ArgumentResolver를 통해 인터페이스를 직접 확장할 수 있다.
ReturnValueHandler
- 응답 값을 변환하고 처리하는 인터페이스
- 컨트롤러에서 String으로 뷰이름을 반환해도 동작되는 이유가 해당 인터페이스 덕분이다.
HTTP 메세지 컨버터의 위치
- 요청의 경우@RequestBody와 HttpEntity를 처리하는 ArgumentResolver들이 HTTP 메세지 컨버터를 사용하여 필요한 객체를 생성한다.
- 응답의 경우 @ResponseBody와 HttpEntity를 처리하는 ReturnValueHandler에서 HTTP 메세지 컨버터를 사용하여 응답 결과를 생성한다.
타임리프
JSP 파일에서 JAVA코드와 HTML 코드가 섞여 정상적으로 확인하기 어려우며 오직 서버를 통해서만 JSP를 열어야 하는 불편함이 있다.
타임 리프를 사용하면 순수 HTML 파일을 웹 브라우저에서 내용을 확인할 수 있으며, 서버를 통해 뷰 템플릿을 거치면서 동적으로 변경된 결과를 확인할 수 있다.
타임 리프의 핵심
- th:xxx 가 붙은 부분은 서버사이드에서 렌더링되며 기존것을 대체한다.
- th:xxx 가 없으면 기존 html xxx속성이 그대로 사용된다.
- HTML 파일을 직접 열었을 때는 웹 브라우저가 th: 속성을 무시한다.
타임리프 사용 선언
<html xmlns:th="http://www.thymeleaf.org">
경로 지정
<tr th:href="@{경로}"> </tr>
<tr th:href="@{경로/{itemA}(itemA=${..}) }"> </tr>
- URL 링크 표현식 1 : @{....} - URL 링크를 사용하는 경우 사용된다.
- URL 링크 표현식 2 : @{....} - URL 링크안에 {}에서 변수를 설정해두고 ()에서 값을 할당할 수 있다.
- 속성 변경 : th:href="@{경로}"
이벤트 추가
<button th:onclick="|location.href='@{/basic/items/add}'|"> </button>
- 리터럴 대체 : |...| - 문자와 표현식을 함께 편리하게 사용할 수 있다.
반복 출력
<tr th:each="item:${items}"> ... </tr>
- 반복문 : th:each - 모델에 포함된 items 컬렉션 데이터가 item 변수에 하나식 할당괴고 컬렉션 수 만큼 <tr>이 반복된다.
- 변수 표현식 : ${..} - 모델에 포함된 값에 접근할 때 사용된다.
조건문 : th:if - if 결과값이 true면 th에 적용된 값이 적용된다.
<h2 th:if="${data.TF}" th:text="'참!'"></h2>
PRG
Post / Redirect / Get
상품 등록을 완료하고(Post) 웹 브라우저의 새로고침을 누르면 계속해서 상품이 등록되는 상황일 발생할 수 있다.
이를 방지하기 위해 Post -> Redirect -> Get을 사용한다.
PRG 동작 방식
마지막을 GET을 호출하게 설계하여 중복 상품 등록을 방지할 수 있다. 새로고침을 해도 조회(GET)가 발생한다.
PRG 사용방법
return 시 "redirect:"+경로를 붙인다.
return "redirect:/경로.." + item.getId();
"redirect:경로" + get 호출의 문제점
URL에 변수를 더해서 사용하는 방법은 URL 인코딩이 동작하지 않기 때문에 위험한 한계가 있다 이를 해결하기 위해 RedirectAttributes를 사용한다.
RedirectAttributes
RedirectAttributes을 사용하면 URL 인코딩을 뿐만 아니라 PathVariable, 쿼리 파라미터까지 처리가 가능해진다.
@PostMapping("/add")
public String save(Item item, RedirectAttributes redirectAttributes) {
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true); //쿼리 파라미터 추가
return "redirect:경로{itemId}";
}
출처 : 인프런 - 스프링 MVC 1편 feat.김영한 쌤
'웹 개발' 카테고리의 다른 글
스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 (2) | 2024.02.27 |
---|---|
스프링 DB 1편 - 데이터 접근 핵심 원리 (0) | 2024.02.24 |
스프링 MVC 2편 - 타임리프 (0) | 2024.02.23 |
HTTP 웹 기본 지식 (1) | 2024.01.05 |
SPRING 핵심 원리 - 기본편 정리 (0) | 2023.12.22 |