강의

멘토링

커뮤니티

웹 애플리케이션 이해

웹 서버, 웹 애플리케이션 서버

웹 서버

웹 애플리케이션 서버

차이점

WAS 는 결국 죽는다.

서블릿

서블릿 = 비즈니스 로직을 제외한 다른 작업들을 자동화 해준다.

HTTP 스펙을 어느정도 알아야됨

톰갯 와스 서버 그림

서블릿 컨테이너

정리

서블릿은 싱글톤이다.

이유

요청, 응답은 새로만듬

공유 변수 사용 주의

동시 요청을 위한 멀티 스레드 처리 지원

동시 요청 - 멀티 쓰레드

동시 요청 - 멀티 스레드

백엔드 한테 너무 중요해

그림

서블릿 호출 객체 = 스레드

단일 요청 - 쓰레드 하나 사용

다중 요청 - 쓰레드 하나 사용

그냥 신규 쓰레드 생성

장단 점

쓰레드 풀

실무 팁

MAX THREAD

너무 낮거나 너무 높거나

10개 + 90개(대기) =100개 요청

CPU 사용률 5프로만 사용하네

5~10% = 개발자가 세팅을 잘못 한것이다.

너무 많으면

서버가 그냥 다운 됨

일단 서버 늘리고 튜닝

클라우드가 아니며 니가 해야된다.

적정숫자 찾는법.

너어무 중요한 기본기

1회 디비조회, 2회 디비조회 = 활동량 2배

그리고 하드웨어

성능 테스트를 해봐야 한다.

nGrinder - 네이버 오픈소스 = 좋다.

정리

어쨋든 싱글톤을 조심해라

HTML, HTTP API, CSR, SSR

HTTP API

3가지

유행하는 이야기

세상이 그렇게 만만 하지가 않아요.

이게 고민이다.

SSR 공부해야됨. admin 정도

풀 스택의 환상

자바 백엔드 웹 기술 역사

고대의 자바 웹 기술

에노테이션 기반의 스프링이 통일한다.

서버 내장이 너무 편했다.

최신기술

웹플럭스 소개

사용하는 포인트

뷰 템플릿의 역사

서블릿

프로젝트 생성

톰캣에 별도로 넣는 것을 해보자.

   id war

Tomcat started on port(s): 8080 

자바 직접 실행

롬복 설정

포스트맨

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 - 개요

HttpServletRequest 역할

부가기능

중요
HTTP 강의 듣고와라.

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 요청 데이터 개요.

3가지

Get - 쿼리 파라미터

Post - HTML Form
=  content-type: application/x-www-form-urlencoded

HTTP message body

JSON

Post, Put, Patch

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 요청 데이터 - POST HTML Form

webapp/basic 디렉토리 생성

이렇게 하면된다.

폴더 경로로 바로 접근 가능.

안뜸

정리

테스트 할때마다 만들어야 되나요~

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 {

}
}
{"username":"hello", "age": 20}

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로 회원 관리 웹 애플리케이션 만들기

<%@ page contentType="text/html;charset=UTF-8" language="java" %>

 정리

한계점

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

서블릿 종속성 제거

서블릿 기술을 몰라도 동작할수 있다.

뷰이름의 프리픽스를 제거해보자.

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

맞는 돼지코를 찾아와라.

서포터

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 전체 구조

정리

핸들러 매핑과 핸들러 어댑터

hello/servlet/web/springmvc

컴포넌트 스캔 = 이름을 유니크 = URL

HandlerMapping

HandlerAdapter

순서 정리

@RequestMapping

뷰 리졸버

spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

뷰리졸버

스프링 MVC - 시작하기

MVC가 좀 약했다.

Controller 컴포넌트 인식 방법

다른 코드 추가

스프링 MVC - 컨트롤러 통합

컨트롤러 통합

중복 제거

@RequestMapping("/springmvc/v2/members")
@RequestMapping

스프링 MVC - 실용적인 방식

V3-실무

단점이 아직 있다.

아직 GET,POST를 구분하지 않았다.

스프링은 막지 않는다.

"path": "/springmvc/v3/members/new-form"

정리

스프링 MVC - 기본 기능

프로젝트 생성

왜 jar

war는 추가기능이 들어감.

외부 서버에 빌드된 파일을 넣을때 War를 씁니다.

 index.html

로깅 간단히 알아보기

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

너무 좋네.

참고 : 프로퍼티

org.springframework.validation.BindException

검증에서 따로 이야기 함.

생략 가능함.

프리미티브 = @RequestParam
그외 = @ModelAttribute

(제외 argument resolver 로 지정 타입 )

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

정리

정리

로깅

요청 매핑

헤더 조회

요청 데이터 3가지 방법

@RequestBody <-> @ModelAttribute

HTTP 메시지 컨버터

스프링 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를 무시한다.

정리

한번더 보여드릴게요.

상품 상세

디버그 Long -> long

오류: 
pdf 에서 인텔리제이 로 복사할시 [NUL]이 포함되서 template: "class path resource 발생

상품 등록 폼

활용 1편에서 include로 사용한다.

action

폼을 열때는 GET, 저장할때는 POST

간단한걸 강조하기 위해 비워둔다.

정리

상품 등록 처리 - @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 으로 하는게 좋지 안나.

상품 수정

Redirect 로 바꿀거임

@PostMapping("/{itemId}/edit")
public String editItemV1(@PathVariable Long itemId,
@ModelAttribute Item item){
itemRepository.update(itemId, item);
return "redirect:/basic/items/{itemId}";
}

정리

애매하다.

수정할때만 왜 Redirect 썻을까.

PRG Post/Redirect/Get

심각한 문제가 있다  = 중복 등록이 된다.

우리가 보던 페이지 정보를 다시 전송하겠냐는 경고창.

흐름도

간단한 방법

@PostMapping("/add")
public String addItemV5(Item item){
itemRepository.save(item);
return "redirect:/basic/items/" + item.getId();
}

302 코드 -> 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>

다음으로

다음으로

예외처리 알고 싶던 부분

REST API 상태 코드 400인가 500인가.

저녁에 공부해라.