웹 애플리케이션 이해
서블릿
동시 요청 - 멀티 쓰레드
동시 요청 - 멀티 스레드
백엔드 한테 너무 중요해
그림
서블릿 호출 객체 = 스레드
단일 요청 - 쓰레드 하나 사용
다중 요청 - 쓰레드 하나 사용
그냥 신규 쓰레드 생성
장단 점
쓰레드 풀
실무 팁
MAX THREAD
너무 낮거나 너무 높거나
10개 + 90개(대기) =100개 요청
CPU 사용률 5프로만 사용하네
5~10% = 개발자가 세팅을 잘못 한것이다.
너무 많으면
서버가 그냥 다운 됨
일단 서버 늘리고 튜닝
클라우드가 아니며 니가 해야된다.
적정숫자 찾는법.
너어무 중요한 기본기
1회 디비조회, 2회 디비조회 = 활동량 2배
그리고 하드웨어
성능 테스트를 해봐야 한다.
nGrinder - 네이버 오픈소스 = 좋다.
정리
어쨋든 싱글톤을 조심해라
HTML, HTTP API, CSR, SSR
서블릿
프로젝트 생성
Hello 서블릿
@ServletComponentScan //서블릿 자동 등록
패키지 생성
@WebServlet(name="helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
}
단축키 : Override Methods
열쇠 모양(protected) 잠긴것
서블릿이 호출이되면 서비스가 호출된다.
super 지움
단축키 : soutm 클래스명
빈화면의 이유
이름 변경
단축키 : soutv 변수
System.out.println("HelloServlet.service");
System.out.println("request = " + request);
System.out.println("response = " + response);
단축키 : ctr+r 최근 다시 시작.
http://localhost:8080/hello?username=kim
단축키 : 코드 완성
단축키 : 변수 추출
String username = request.getParameter("username");
System.out.println("username = " + username);
응답
response.setContentType("text/plain");
response.setCharacterEncoding("utf-8");
EUC-KR 쓰면 안됨.
response.getWriter().write("hello " + username);
서블릿 이름, URL 매핑은 겹치면 안된다.
resources/application.properties
logging.level.org.apache.coyote.http11=debug;
운영서버 넣을때는 고민해보세요.
정리
정리끝
Directory webapp 생성
index.html, basic.html 생성
복붙
HttpServletRequest - 기본 사용법
사용법
basic/request 패키지 생성
request/RequestHeaderServlet.java
@WebServlet(name="requestHeaderServlet", urlPatterns = "/request-header")
public class RequestHeaderServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
}
첫라인 불러오기
단축키 : 리팩토링
단축키 : 함수로 뽑기
http://localhost:8080/request-header
Accept-Language
GET = ContentType null
request.getHeader("host");
Remote = 오는것의 정보
Local = 나의 정보
참고 v6->v4
HTTP 요청 데이터 - 개요
HTTP 요청 데이터 - GET 쿼리 파라미터
GET 쿼리 파라미터
RequestParamServlet.java
/**
* 1.파라미터 전송 기능
* http://localhost:8080/request-param?username=-hello&age=20
*/
@WebServlet(name="requestParamServlet", urlPatterns = "request-param")
public class RequestParamServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.service(req, resp);
}
}
전체 파라미터
* http://localhost:8080/request-param?username=-hello&age=20
System.out.println("[전체 파라미터 조회] - start");
request.getParameterNames().asIterator()
.forEachRemaining(paramName ->
System.out.println(
paramName + "=" + request.getParameter(paramName)));
System.out.println("[전체 파라미터 조회] - end");
System.out.println("[단일 파라미터 조회]");
String username = request.getParameter("username");
String age = request.getParameter("age");
System.out.println("username = " + username);
System.out.println("age = " + age);
요걸 하나 설명 드릴게요.
우선순위에서 먼저 잡히는애가 나옴.
System.out.println("[이름이 같은 복수 파라미터 조회]");
String[] usernames = request.getParameterValues("username");
for (String name : usernames) {
System.out.println("username = " + name);
}
response.getWriter().write("ok");
HTTP 요청 데이터 - POST HTML Form
HTTP 요청 데이터 - API 메시지 바디 - 단순 텍스트
HTTP 요청 데이터 - API 메시지 바디 - 단순 텍스트
JSON 통일
POST, PUT, PATCH
RequestBodyStringServlet
@WebServlet(name="requestBodyStringServlet", urlPatterns = "/request-body-string")
public class RequestBodyStringServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
ServletInputStream inputStream = request.getInputStream();
}
}
바이트코드 -> 스트링
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
HTTP 요청 데이터 - API 메시지 바디 - JSON
HTTP 요청 데이터 - API 메시지 바디 -JSON
hello/servlet/basic/HelloData.java
단축키 : 제네레이트
@Getter @Setter
public class HelloData {
private String username;
private int age;
}
hello/servlet/basic/request/RequestBodyJsonServlet.java
@WebServlet(name="requestBodyJsonServlet", urlPatterns = "/request-body-json")
public class RequestBodyJsonServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
}
JSON도 결국 문자
라이브러리 : jackson
private ObjectMapper objectMapper= new ObjectMapper();
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
ServletInputStream inputStream = request.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());
}
참고
HttpServletResponse - 기본 사용법
HttpServletResponse
hello/servlet/basic/response
response.setStatus(200);
response.setStatus(HttpServletResponse.SC_OK);
response.setHeader("Content-Type", "text/plain");
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setHeader("my-header", "hello");
PrintWriter writer = response.getWriter();
writer.println("ok");
response.setHeader("Content-Type", "text/plain;charset=utf-8");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
Content 들의 편의 메서드
//[Header 편의 메서드]
content(response);
//[Cookie 편의 메서드]
cookie(response);
그리니티 천문대 시간
쿠키

리다이렉트
//[Redirect 편의 메서드]
redirect(response);
//[message body]
PrintWriter writer = response.getWriter();
writer.println("ok");
HTTP 응답 데이터 - 단순 텍스트, HTML
HTTP 응답 데이터 - 단순 텍스트, HTML
3가지
단축키 : 프로젝트 1
단축키 : 화면 늘이기 줄이기
@WebServlet(name="responseHtmlServlet", urlPatterns = "/response-html")
public class ResponseHtmlServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//Content-Type: text/html; charset=utf-8
response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
PrintWriter writer = response.getWriter();
writer.println("<html>");
writer.println("<body>");
writer.println(" <div>안녕?</div>");
writer.println("</body>");
writer.println("</html>");
}
}
HTTP 응답 데이터 - API JSON
HTTP 응답 데이터 - API JSON
@WebServlet(name="responseJsonServlet", urlPatterns = "/response-json")
public class ResponseJsonServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
}
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//Content-Type: application/json
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
HelloData helloData = new HelloData();
helloData.setUsername("kim");
helloData.setAge(20);
//{"username":"kim", "age":20}
String result = objectMapper.writeValueAsString(helloData);
response.getWriter().write(result);
}
참고
response.getOutputStream()은 charset=utf-8파라미터를 추가 하지 않는다.
정리
순수 HTML은 POST만 FORM을 전송할수 있다.
서블릿, JSP, MVC 패턴
회원 관리 웹 애플리케이션 요구사항
싱글톤 = 생성자를 private 로 막기
/**
* 동시성 문제가 고려되어 있지 않음, 실무에서는 ConcurrentHashMap, AtomicLong 사용 고려
*/
public class MemberRepository {
private Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
private static final MemberRepository instance = new MemberRepository();
public static MemberRepository getInstance(){
return instance;
}
private MemberRepository(){
}
}
밖에서 조작해도 store(원본)를 건드리고 싶지 않아서 이렇게 짠다.
public List<Member> findAll(){
return new ArrayList<>(store.values());
}
싱글톤이라 new 를 쓸수 없다.
중요 = Disable Add on-demand static import
서블릿으로 회원 관리 웹 애플리케이션 만들기
@WebServlet(name="memberFormServlet", urlPatterns = "/servlet/members/new-form")
public class MemberFormServlet extends HttpServlet {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
}
response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
PrintWriter w = response.getWriter();
w.write("<!DOCTYPE html>\n" +
"<html>\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <title>Title</title>\n" +
"</head>\n" +
"<body>\n" +
"<form action=\"/servlet/members/save\" method=\"post\">\n" +
" username: <input type=\"text\" name=\"username\" />\n" +
" age: <input type=\"text\" name=\"age\" />\n" +
" <button type=\"submit\">전송</button>\n" +
"</form>\n" +
"</body>\n" +
"</html>\n");
항상 문자로 추출하니까 숫자로 바꿔 줘야 한다.
@WebServlet(name="memberSaveServlet", urlPatterns = "/servlet/members/save")
public class MemberSaveServlet extends HttpServlet {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("MemberSaveServlet.service");
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
}
}
Member member = new Member(username, age);
memberRepository.save(member);
response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
PrintWriter w = response.getWriter();
w.write("<html>\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
"</head>\n" +
"<body>\n" +
"성공\n" +
"<ul>\n" +
" <li>id="+member.getId()+"</li>\n" +
" <li>username="+member.getUsername()+"</li>\n" +
" <li>age="+member.getAge()+"</li>\n" +
"</ul>\n" +
"<a href=\"/index.html\">메인</a>\n" +
"</body>\n" +
"</html>");
@WebServlet(name="memberListServlet", urlPatterns = "/servlet/members")
public class MemberListServlet extends HttpServlet {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
List<Member> members = memberRepository.findAll();
response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
PrintWriter w = response.getWriter();
w.write("<html>");
w.write("<head>");
w.write(" <meta charset=\"UTF-8\">");
w.write(" <title>Title</title>");
w.write("</head>");
w.write("<body>");
w.write("<a href=\"/index.html\">메인</a>");
w.write("<table>");
w.write(" <thead>");
w.write(" <th>id</th>");
w.write(" <th>username</th>");
w.write(" <th>age</th>");
w.write(" </thead>");
w.write(" <tbody>");
/*
w.write(" <tr>");
w.write(" <td>1</td>");
w.write(" <td>userA</td>");
w.write(" <td>10</td>");
w.write(" </tr>");
*/
for (Member member : members) {
w.write(" <tr>");
w.write(" <td>" + member.getId() + "</td>");
w.write(" <td>" + member.getUsername() + "</td>");
w.write(" <td>" + member.getAge() + "</td>");
w.write(" </tr>");
}
w.write(" </tbody>");
w.write("</table>");
w.write("</body>");
w.write("</html>");
}
}
url 끝에 / 붙이면 내용 안뜸
디버깅이 거의 불가능
정리
index.html 수정
JSP로 회원 관리 웹 애플리케이션 만들기
MVC 패턴 - 적용
모델 = MODEL = 데이터 채널
request.setAttribute()
request.getAttribute()
hello/servlet/web/servletmvc
@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 {
super.service(req, resp);
}
}
getRequestDispatcher() = 컨트롤러에서 뷰로 이동해야한다.
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request,response);
액션 조심
WEB-INF
외부에서 접근 불가 = 내부에서만 사용하고 싶다.
@WebServlet(name="mvcMemberSaveServlet", urlPatterns = "/servlet-mvc/members/save")
public class MvcMemberSaveServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.service(req, resp);
}
}
private MemberRepository memberRepository = MemberRepository.getInstance();String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(username, age);
memberRepository.save(member);
//Model에 데이터 보관.
request.setAttribute("member", member);
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
<li>id=<%=((Member)request.getAttribute("member")).getId()%></li>
<li>username=<%=((Member)request.getAttribute("member")).getUsername()%></li>
<li>age=<%=((Member)request.getAttribute("member")).getAge()%></li>
<li>id=${member.id}</li>
<li>username=${member.username}</li>
<li>age=${member.age}</li>
@WebServlet(name="mvcMemberListServlet", urlPatterns = "/servlet-mvc/members")
public class MvcMemberListServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.service(req, resp);
}
}
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<c:forEach var="item" items="${members}">
<tr>
<td>${item.id}</td>
<td>${item.username}</td>
<td>${item.age}</td>
</tr>
</c:forEach>
디버깅 : member -> members 오타 수정
정리
MVC 프레임워크 만들기
프론트 컨트롤러 도입 - v1
구조 v1
hello/servlet/web/frontcontroller/v1
제일 중요한게 controller 인터페이스 모양
회원 등록 컨트롤러
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);
}
}
jsp를 상대경로로 했던 이유
중요 :
private Map<String, ControllerV1> controllerV1Map = new HashMap<>();
매핑 할때는 한번 실행 해두자.
로직 만들기 전에 다시 그림 설명
@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 request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("FrontControllerServletV1.service");
String requestURI = request.getRequestURI();
//url -> 객체 반환
ControllerV1 controller = controllerMap.get(requestURI);
if(controller == null){
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
controller.process(request, response);
}
}
기본편에서 했었다. 기억이 안나는데.
save는 외부 접근이 안된다.
실무 에서도 아키택처를 개선해야 할일이 생긴다.
구조를 건들때는 구조만 건든다.
참아야 된다.
한번에 하면 짬뽕 된다.
구조 개선 -> 커밋 -> 내부 개선
프론트는 건들지 마세요.
View 분리 - v2
구조개선 = 분리
V2 구조
hello/servlet/web/frontcontroller/MyView.java
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 interface ControllerV2 {
MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
단축키 : 리펙토링 + 인라인
단축키 : 인라인
복사 붙여넣기 하면 망하는 이유
@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();
//url -> 객체 반환
ControllerV2 controller = controllerMap.get(requestURI);
if(controller == null){
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
controller.process(request, response);
}
}
단축키 : 변수 받기
내부 구조만 리펙토링 한것이다.
마이뷰의 유래
Model 추가 - v3
V3
서블릿 종속성 제거
서블릿 기술을 몰라도 동작할수 있다.
뷰이름의 프리픽스를 제거해보자.
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
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 = 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;
}
}
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;
}
}
너무 디테일하다. = 메서드를 뽑아야된다.
단축키 : 메소드 추출
실제 뷰를 찾아주는 기능.
이렇게 하면 되는데 너무 디테일하다.
데이터를 같이 넘겨줘야된다.
따라오기 힘들수 있다.
이렇게 변수를 돌려서 넣을 때는 의미 있는 이름이 필요하다.
public void render(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
modelToRequestAttribute(model, 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));
}
코드 흐름
정리
완전히 이해해야 한다.
단순하고 실용적인 컨트롤러 - v4
유연한 컨트롤러1 - v5
V5
맞는 돼지코를 찾아와라.
서포터
V3 를 처리할수 있는 어뎁터를 꺼내는 것
핸들러 = 호출용.
modelview를 반환하면 반환하는데로
반환하지 않으면 생성해서라도 반환해줘야된다.
개념적으로 설명
코딩
단축키 : 이름변경 shift + f6
public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
@Override
public boolean supports(Object handler) {
return false;
}
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
return null;
}
}
public interface MyHandlerAdapter {
boolean supports(Object handler);
ModelView handle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
인터페이스 검증
return (handler instanceof ControllerV3);
유연하게 하기 위해서 Object로 하는것이며 그러기 때문에 할수 있는것이 없다.
불리언으로 걸러진 값을 사용할것이다.
v3 는 modelview를 반환하기 때문에 딱딱 맞는다.
단축키 : 제네레이터
v3 를 지원하는 애를 먼저 만드는중이다.
@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() {
handlerMappingMap.put("/front-controller/v3/members/new-form", new MemberFormControllerV3());
handlerMappingMap.put("/front-controller/v3/members/save", new MemberSaveControllerV3());
handlerMappingMap.put("/front-controller/v3/members", new MemberListControllerV3());
handlerAdapters.add(new ControllerV3HandlerAdapter());
}
}
public FrontControllerServletV5() {
initHandlerMappingMap();
initHandlerAdapters();
}
v3에서 가져오고 리펙토링중
어댑터 찾아오는 부분
잡기 : 컬랙션.iter = for문
핸들러 찾아와
어댑터 찾아와
Object handler = getHandler(request);
if(handler == null){
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
MyHandlerAdapter adapter = getHandlerAdapter(handler);
ModelView mv = adapter.handle(request, response, handler);
String viewName = mv.getViewName();
MyView view = viewResolver(viewName);
view.render(mv.getModel(),request, response);
그림
1. 컨트롤러 매핑 정보 = 기존 어뎁터 목록
2. 컨트롤러 어댑터 목록 = 기존 어뎁터를 새로운 구조에 맞게 바꿔주는 돼지코 목록
돌려봅시다.
저도 안됩니다.
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
URL 부터 확인하자.
정리
이름 변경의 이유
유연한 컨트롤러2 - v5
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);
HashMap<String, Object> model = new HashMap<>();
String viewName = controller.process(paramMap, model);
return null;
}
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;
}
}
어뎁터 다운 역할
ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;
난 안되네
/5 -> /v5 디버깅
정리
그림
확장이 쉬워 졌다.
설정을 주입해주면 OCP를 완벽하게 지킬수 있음.
기능을 확장하더라도 코드를 변경할 필요가 없다.
정리
이름 != 객체 를 어뎁터가 해결해 준다.
인터페이스를 느껴라
바꾸고 싶은 부분만 꽂아 넣는것이 가능하다.
스프링 MVC - 구조 이해
스프링 MVC 전체 구조
정리
핸들러 매핑과 핸들러 어댑터
스프링 MVC - 기본 기능
로깅 간단히 알아보기
import org.slf4j.Logger;
원래는 View가 반환되야된다.
RestController 는 문자가 그대로 반환 된다.
[스레드 이름]
#hello.springmvc 패키지와 그 하위 로그 레벨 설정
logging.level.hello.springmvc=trace
운영 서버는 Info 부터 남긴다.
로그 폭탄 맞는다.
@RestController 설명
정리
#전체 로그 레벨 설정(기본 info)
logging.level.root=info
@Slf4j
로그의 올바른 사용법
{}를 사용하는 이유(중요)
자바 언어의 특징
함수를 부르기 전에 " " 과 변수를 더해버린다.
연산이 일어난다. 쓸모없는 리소스 사용.
이렇게 쓰면 안됨.
중요
로그 용량 차면 장애남
요청 매핑
@RestController 한번더 설명
URL 배열 제공가능
/hello-basic == /hello-basic/
스프링 은 메서드와 무관하게 호출한다.(이전에 했던 내용)
나중에 예외 처리에 대해서 알려주겟다.
요즘에 중요하다.
/**
* PathVariable 사용
* 변수명이 같으면 생략 가능
* @PathVariable("userId") String userId -> @PathVariable userId
*/
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String data) {
log.info("mappingPath userId={}", data);
return "ok";
}
/**
* PathVariable 사용 다중
*/
@GetMapping("/mapping/users/{userId}/orders/{orderId}")
public String mappingPath(@PathVariable String userId, @PathVariable Long
orderId) {
log.info("mappingPath userId={}, orderId={}", userId, orderId);
return "ok";
}
6. 스프링 MVC - 기본 기능.pdf
@PathVariable (String)-빠짐 userId
쿼리파라미터 조건 매핑
특정 헤더 조건 매핑
미디어 타입 매핑 - header
/**
* Content-Type 헤더 기반 추가 매핑 Media Type
* consumes="application/json"
* consumes="!application/json"
* consumes="application/*"
* consumes="*\/*"
* MediaType.APPLICATION_JSON_VALUE
*/
@PostMapping(value = "/mapping-consume", consumes = "application/json")
public String mappingConsumes() {
log.info("mappingConsumes");
return "ok";
}
header 쓰지말고 이걸 써야된다.
미디어 타입 매핑 - Accept 헤더
json 줘 => Text/html 밖에 없는데?
HTTP 강의가 중요합니다.
import org.springframework.web.bind.annotation.PostMapping;
@PostMapping(value = "/mapping-consume", consumes = MediaType.APPLICATION_JSON_VALUE)
@PostMapping(value = "/mapping-produce", produces = MediaType.TEXT_HTML_VALUE)
요청 매핑 - API 예시
정리
HTTP 요청 - 기본, 헤더 조회
@RestController = 뷰를 찾지 않는다.
@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";
}
MultiValueMap
맵 [키, 배열]
몇몇 값은 디폴트를 제공한다.
flexible signature 유연하게 여러 파라미터를 지원한다.
메소드 파라미터 설명
크롬 번역 플러그인
리턴 밸류 설명
HTTP 요청 파라미터 - 쿼리 파라미터, HTML Form
@Slf4j
@Controller
public class RequestParamController {
@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"));
log.info("username={}, age={}", username, age);
response.getWriter().write("ok");
}
}
HTTP 요청 파라미터 - @RequestParam
@RequestParam
@ResponseBody
@ResponseBody
@RequestMapping("/request-param-v2")
public String requestParamV2(
@RequestParam("username") String memberName,
@RequestParam("age") int memberAge
) {
log.info("username={}, age={}", memberName, memberAge);
return "ok";
}
v3 정도가 좋지 않나 생각함
벨리데이션 = 거부
서버코드 = 500 에러
자바
int a = null; 넣을수가 없다.
그래서 Integer는 null 가능하다.
주의
null != ""
빈문자는 null 이 아니라 통과한다.
int != null
defaultValue
defaultValue 가 있으면 required 가 필요 없다.
빈문자 = defaultValue
HTTP 요청 파라미터 - @ModelAttribute
HTTP 요청 메시지 - 단순 텍스트
바이트 -> 스트링 = UTF-8 지정해줘야됨.
StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
java.io.InputStream
@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");
}
메시지 컨버터
Body 구나 라는걸 인식 = UTF-8 컨버팅 해줄게.
@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringV3(
HttpEntity<String> httpEntity
) throws IOException {
String messageBody = httpEntity.getBody();
log.info("messageBody={}", messageBody);
return new HttpEntity<>("ok");
}
설명
getHeader도 가능
요청 파라미터랑 아무 상관이 없다.
GET = 쿼리 파라미터,POST = html FORM 일때만
@RequestParam, @ModelAttribute 사용.
그외는 HttpEntity 로 직접 꺼내야 한다.
public HttpEntity<String> requestBodyStringV3(
RequestEntity<String> httpEntity
) throws IOException {
String messageBody = httpEntity.getBody();
log.info("messageBody={}", messageBody);
return new ResponseEntity<>("ok", HttpStatus.CREATED);
}
그래서 에노테이션이 제공 됩니다.
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(
@RequestBody String messageBody
) {
log.info("messageBody={}", messageBody);
return "ok";
}
결국 실무에서 이걸 쓴다.
강조
HTTP 요청 메시지 - JSON
JSON
/**
* {"username":"hello", "age":20}
* content-type: application/json
*/
@Slf4j
@Controller
public class RequestBodyJsonController {
private ObjectMapper objectMapper = new ObjectMapper();
@PostMapping("/request-body-json-v1")
public void requestBoeyJsonV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("messageBoy={}", messageBody);
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
response.getWriter().write("ok");
}
}
@RequestBody
생략하면 쿼리 스트링으로 인식할듯?
@ModelAttribute
@ModelAttribute = 관대하다.
@ResponseBody
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData data) {
log.info("username={}, age={}", data.getUsername(), data.getAge());
return data;
}
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
Accept : application/json 옵션도 확인
응답 - 정적 리소스, 뷰 템플릿
요청도 3가지 (get, post form html, body)
응답(정적리소스, 뷰페이지, http 메시지 사용)
정적 리소스
뷰 템플릿
경로를 잘못 만듬
Error resolving template [response/hello], template might not exist or might not be accessible
디버그 : 폴더 옮기고 경로가 변경됨
templates/response/hello => response/hello
@ResponseBody 쓰면 그냥 문자로 나감.
권장하지 않는다 = V3
뭐 이름이 유니크한 빈이니까 URL 도 유니크 하면
뭐 생각 가능한 방법이긴하네.
Thymeleaf
HTTP 응답 - HTTP API, 메시지 바디에 직접 입력
주의 에노테이션 이기 때문에 응답 코드를 동적으로 설정 할수 없다.
@GetMapping("/response-body-json-v1")
public ResponseEntity<HelloData> responseBodyJsonV1(){
HelloData helloData = new HelloData();
helloData.setUsername("userA");
helloData.setAge(20);
return new ResponseEntity<>(helloData, HttpStatus.OK);
}
@ResponseBody를 붙이는것이 귀찮다.
@Controller + @ResponseBody =
@RestController
@Slf4j
@RestController
public class ResponseBodyController {
@GetMapping("/response-body-string-v1")
public void responseBodyV1(HttpServletResponse response) throws IOException {
response.getWriter().write("ok");
}
@GetMapping("/response-body-string-v2")
public ResponseEntity<String> responseBodyV2() {
return new ResponseEntity<>("ok", HttpStatus.OK);
}
// @ResponseBody
@GetMapping("/response-body-string-v3")
public String responseBodyV3() {
return "ok";
}
@GetMapping("/response-body-json-v1")
public ResponseEntity<HelloData> responseBodyJsonV1(){
HelloData helloData = new HelloData();
helloData.setUsername("userA");
helloData.setAge(20);
return new ResponseEntity<>(helloData, HttpStatus.OK);
}
@ResponseStatus(HttpStatus.OK)
// @ResponseBody
@GetMapping("/response-body-json-v2")
public HelloData responseBodyJsonV2(){
HelloData helloData = new HelloData();
helloData.setUsername("userA");
helloData.setAge(20);
return helloData;
}
}
실무에서는 V2를 많이 쓴다.
HTTP 메시지 컨버터
HTTP 메시지 컨버터 = API,JSON
그림
@ResponseBody
결국 컨트롤러를 URL 에 등록된 것을 찾아서 적용하듯이
ACCEPT 헤더와 컨트롤러 반환 타입으로 등록된 컨버터를 찾아서 작동한다.
주요 컨버터
HTTP 요청 데이터 처리 = BodyType, Content-Type
hello/springmvc/basic/request/RequestBodyJsonController.java
HTTP 응답 데이터 생성 = Accept
hello/springmvc/basic/response/ResponseBodyController.java
예시
우선 순위는 Body 의 조건
안되는 경우
객체 -> text/html = ??
콜 시점.
요청 매핑 헨들러 어뎁터 구조
@Controller
RequestMappingHandlerAdapter 동작방식
ArgumentResolver
hello/springmvc/basic/request/RequestHeaderController.java
로그인 만들때 해보자.
ReturnValueHandler
그래서 HTTP 메시지 컨버터는 어디에 있나요.
그림
ArgumentResolver : http 메시지를 바로 처리해야되네, 컨버터를 호출해야지
기능 확장할 일은 거의 없다.
WebMvcConfiturer
스프링 MVC - 웹 페이지 만들기
프로젝트 생성
패키지 네임에 "-" 이런거 들어가면 안됨.
상품 도메인 개발
@Data는 toString 같은거 때문에 위험하다.
순수한 Dto에서는 괜찮다.
Long 의 이유는 Item의 ID랑 맞춘거다.
실제는 HashMap 말고 다른거 (스레드 세이프, 다른 강의에서 이미 설명함)
static 사용 = 따로 생성을 막는다.
ConcurrentHashMap
long 쓰면 안되고 atomic Long
내부 값 조작하지 못하게
return new ArrayList<>(store.values());
정석은 Dto를 생성해서 3개만 딱 받을수 있게 하는것이 좋다.
중복하더라도 명확성을 가져가라.
테스트는 Fail로 시작해야한다.
상품 서비스 HTML
부트스트랩
넣고 조심해야 할것이 있다.
/Users/kch/code/item-service/out
http://localhost:8080/css/bootstrap.min.css
안열리면 out 폴더 Delete 하고 서버 다시키기
동작 확인 방법 2가지
/Users/kch/code/item-service/src/main/resources/static/html/addForm.html
There was an unexpected error (type=Method Not Allowed, status=405).
정적 리소스는 get일때만 작동한다.
보여줄 필요없는 파일 넣어두지마.
상품 목록 - 타임리프
@Controller
@RequestMapping("/basic/items")
@RequiredArgsConstructor
public class BasicItemController {
private final ItemRepository itemRepository;
// @Autowired
// public BasicItemController(ItemRepository itemRepository){
// this.itemRepository = itemRepository;
// }
@GetMapping
public String items(Model model){
List<Item> items = itemRepository.findAll();
model.addAttribute("items", items);
return "basic/item";
}
@PostConstruct
public void init(){
itemRepository.save(new Item("itemA", 10000, 10));
itemRepository.save(new Item("itemB", 20000, 20));
}
}
타임리프 적용
Error resolving template [basic/item]
같은 태그는 덮어버린다.
<link th:href ="@{/css/bootstrap.min.css}"
th:onclick="|location.href='@{/basic/items/add}'|"
<td><a href="item.html" th:href="@{/basic/items/{itemId}(itemId=${item.id})}" th:text="${item.id}">회원id</a></td>
<td><a href="item.html" th:href="@{|/basic/items/${item.id}|}" th:text="${item.itemName}">상품명</a></td>
보통 html이 다 깨진다.
for문 들어가고 이러면 난리 난다.
html은 th를 무시한다.
정리
한번더 보여드릴게요.
상품 상세
상품 등록 폼
상품 등록 처리 - @ModelAttribute
@PostMapping("/add")
public String addItemV2( @ModelAttribute("item") Item item,
Model model ){
itemRepository.save(item);
model.addAttribute("item", item);
return "basic/item";
}
// model.addAttribute("item", item);
객체를 만들고 추가해준다.
대신 위에서 지정해준 이름으로 추가해 준다.
지정안해주면
Item -> item으로 설정된다.
public String addItemV2( @ModelAttribute("item") Item item,
@PostMapping("/add")
public String addItemV3( @ModelAttribute Item item){
itemRepository.save(item);
return "basic/item";
}
public String addItemV4(Item item){
V3 으로 하는게 좋지 안나.
상품 수정
PRG Post/Redirect/Get
RedirectAttributes
고객 입장에서 저장이 된건지 안된건지 확신이 안된다. 메시지를 보여주세요.
리 다이렉트 할때 파라미터를 붙여서 보내면 된다.
redirectAttributes.addAttribute("itemId", savedItem.getId());
return "redirect:/basic/items/{itemId}";
http://localhost:8080/basic/items/4?status=true
templates/basic/item.html
<h2 th:if="${param.status}" th:text="'저장 완료'"></h2>





