반응형

bitcamp-spring-webmvc, bitcamp-java-web-library

 

spring-webmvc

spring-webmvc #ex01

        // providedCompile // 프로그래밍 하는 동안에만 사용하고 배치할 때는 제외하는 라이브러리를 가리킨다.

        // 프로그램이 배치되는 런타입 서버(예: 실행 중인 톰캣 서버)에서 라이브러리를 제공하는 경우,

        // 이 옵션으로 프로젝트에 추가한다. // Servlet API 라이브러리 // compileOnly로 대체하여 사용된다.

        // compile // 배치할 때도 함께 배치된다. // implementation으로 대체하여 사용된다.

        // implementation // 배치에 포함된다.

        // testImplementation // 단위 테스트를 수행할 때만 사용한다. 배치에 포함되지 않는다.

src-01 -> Web.xml로 DispatcherServlet의 IoC 컨테이너 설정하기 1

        // src-02 -> 스프링 WebMVC 적용 // DispatcherServlet 배치

        // Spring Web MVC 라이브러리 추가

        // /WEB-INF/app-context.xml 파일 생성 // Spring IoC 컨테이너 설정 파일

        // /WEB-INF/web.xml 파일 준비

        // 프론트 컨트롤러 역할을 수행할 스프링 webmvc 에서 제공하는 서블릿을 배치한다. // DispatcherServlet

<servlet>
    <servlet-name>app</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/app-context.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>

        // DispatcherServlet 배치 방법 1 // 

        // webapp/WEB-INF/app-context.xml 위치 // 이렇게 사용하여야 한다.

        // DispatcherServlet은 자체적으로 IoC 컨테이너를 보유하고 있다.

        // 파라미터를 사용하여 IoC 컨테이너의 설정 파일을 지정한다.

        // 초기화 파라미터명 : contextConfigLocation  // 초기화 파라미터값 : ex) /WEB-INF/app-context.xml

        // 설정하고 싶지 않다면 init-value를 비워두면 된다.

        // <load-on-startup> // 서블릿을 요청하지 않아도 웹 애플리케이션을 시작시킬 때 자동 생성되어

        // IoC 컨테이너를 준비하는 초기화 작업을 수행할 수 있도록 하는 옵션

@Controller
public class HelloController {

        // 프론트 컨트롤러(DispatcherServlet)이 실행할 페이지 컨트롤러는 @Controller 애노테이션을 붙여야 한다.

  @RequestMapping("/hello")

        // 요청이 들어왔을 때 호출될 메서드(request handler)에 @RequestMapping 애노테이션을 붙혀야 한다.

        // ex) @RequestMapping(요청URL)  @RequestMapping("/hello") @RequestMapping(value="/hello")

        // @RequestMapping(path="/hello")   @RequestMapping({"/hello","hello2","/okok"}) // 사용 예

        // 페이지 컨트롤러의 경우, 해당 경로는 /app/hello로 들어가야 한다. // servlet-name으로 설정했기 때문

        // /app을 앞에 붙혀야 페이지 컨트롤러가 관리하는 페이지로 들어갈 수 있다.

  @ResponseBody
  public String hello() throws Exception {

        // 리턴하는 String 값이 뷰 컴포넌트(ex JSP)의 URL이 아닌 경우 애노테이션으로 표시한다.

        // @ResponseBody // 리턴하는 문자열이 클라이언트에게 보낼 콘텐트임을 표시한다.

        // 이 애노테이션이 붙어 있으면 프론트 컨트롤러는 리턴 값을 클라이언트에게 전송한다.

src-03 -> Web.xml로 DispatcherServlet의 IoC 컨테이너 설정하기 2

        // DispatcherServlet 배치 방법 2 // 절대 사용하지 말아야 한다.

        // webapp/comfig/app-context.xml 위치 // 일반 웹 디렉토리에 두는 방법.

        // WEB-INF 폴더 안에 있는 데이터는 절대 Client에게 전달되지 않는다.

        // WEB-INF 폴더 외에 있는 데이터는 Client에게 전달 될 수 있다.

        // Spring IoC 설정 파일을 웹 디렉토리에 두기 // 절대로 일반 웹 디렉토리에 두지 말자!

        // 일반 웹 디렉토리는 클라이언트에서 접근할 수 있기 때문에 설정 정보가 노출될 위험이 있다. 

src-04 -> Web.xml로 DispatcherServlet의 IoC 컨테이너 설정하기 3

        // /WEB-INF/app-servlet.xml 로 변경 // app-context이름 변경

        // DispatcherServlet 배치 방법 3 // 

  <servlet>
    <servlet-name>app</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>

        // <param-name>contextConfigLocation</param-name> 삭제

        // <param-value>/WEB-INF/app-context.xml</param-value> 삭제

        // contextConfigLocation 초기화 변수가 없으면 /WEB-INF/서블릿이름-servlet.xml으로 

        // IoC 설정 파일을 자동으로 찾는다. // 해당 파일을 찾지 못하면 예외가 발생한다.

        // 서블릿 이름 // Servlet-name // 위의 예제에서는 app

        // /WEB-INF/app-servlet.xml 이라는 파일이 존재하면 된다. // param-name, param-value 삭제 시

    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value></param-value>
    </init-param>

        // DispatcherServlet에서 IoC 컨테이너를 설정하기 싫다면

        // contextConfigLocation 값을 빈 채로 두면 된다. // DispatcherServlet에서 객체 관리를 안하겠다는 의미이다.

        // 즉, 다른 것을 이용하여 객체 관리를 하겠다는 의미.

        // 정리 // 

        // contextConfigLocation 초기화 변수가 있다면, 지정한 설정 파일을 로딩하여 객체를 준비한다.

        // 만약 변수의 값이 비어있다면, 아무런 객체를 생성하지 않는다.

        // 그러나 변수가 없을 때는 예외가 발생하지만, 변수가 있는데 값이 없는 경우 예외가 발생하지 않는다.

        // 즉 contextConfigLocation 값을 생략하는 것과 비워두는 것은 다른 문제이다.

src-05 -> 스프링 WebMVC 적용 // DispatcherServlet IoC 컨테이너 미사용 // 빈 값으로 둔다.

  <servlet>
    <servlet-name>app</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value></param-value>
    </init-param>

        // contextConfigLocation 값을 비워둔다. // DispatcherServlet IoC 컨테이너를 사용하지 않겠다.

        // 대신 value를 비워두면서 오류가 안뜨게끔 한다.

  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>  
  
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/config/app-context.xml</param-value>
  </context-param>

        // ContextLoaderListener의 IoC 컨테이너를 사용한다는 의미이다.

        // ContextLoaderListener가 사용할 IoC 컨테이너 설정 파일 정보를 말한다. 

        // 여기서는 param-value를 비워두면 안된다. // 또한 context-param 사이에 두어야 한다.

<context:component-scan base-package="bitcamp"/>
<mvc:annotation-driven/>

        // /WEB-INF/config/app-context.xml 에 mvc관련 코드가 추가되어야한다.

        // ContextLoaderListener의 IoC 컨테이너는 웹 관련 애노테이션을 처리하지 못한다.

// @Component, @Controller, @service, @Repository, @RestController 등이 붙은 클래스에 대해 객체를 생성하지만,

        // @RequestMapping, @ResponseBody 등과 같은 웹 관련 애노테이션은 인식하지 못한다.

        // 따라서 페이지 컨트롤러의 요청 핸들러를 관리하는 일을 하지 못한다.

        // DispatcherServlet은 관리가 가능하다.

        // 해결책 // 

        // 웹 관련 애노테이션을 처리할 도우미 객체를 따로 등록해야 한다. // ContextLoaderListener의 경우

        // 즉 WebMVC 관련 애노테이션을 처리할 도우미 객체를 등록하면 된다.

xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd"

        // 맨 상단부에 mvc 관련 애노테이션을 처리할 도우미 객체를 등록하면 된다.

        // mvc 관련 애노테이션 처리 도우미까지 별도로 등록해야 하는 ContextLoaderListener를 사용하는 이유 //

        // 실무에 가면 DispatcherSevlet을 여러개 두는 경우가 있다. 

        // ex) 1. /app/* 관련 처리,   2. /android/*,   3. /admin/* // 이런식으로 여러개를 사용하는 경우가 있다.

        // 각 app, android, admin에 관련된 것를 각 DispatcherServlet에서 담당하고

        // 각 DispatcherServlet에서 공유해야 하는 객체를 ContextLoaderListener에 담아 관리하는 경우가 있다.

        // 각 DispatcherServlet끼리는 객체를 공유할 수 없지만,

        // ContextLoaderListener의 객체는 DispatcherServlet이 사용할 수 있다.

        // 즉, 각 페이지 컨트롤러 관련 객체를 각 페이지 컨트롤러가 관리하고

        // 각 페이지 컨트롤러끼리 공유하는 객체를 Front Controller(ContextLoaderListener)가 가지고 있는다.

src-06 -> ContextLoaderListener와 DispatcherServlet의 관계

        // eclipse(이클립스) // Project - Build Automagically 체크 // src(소스) 변경시 자동 컴파일 하게 해주는 기능이다.

        // ContextLoaderListener의 IoC 컨테이너 //

        // 모든 프론트 컨트롤러 및 페이지 컨트롤러가 공유할 객체를 보관한다.

        // DispatcherServlet의 IoC 컨테이너 //

        // 페이지 컨트롤러, 인터셉터 등 웹 관련 객체를 보관한다.

  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>  
  
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/config/app-context.xml</param-value>
  </context-param>
  
  <servlet>
    <servlet-name>app</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>

</servlet>
  <servlet-mapping>
    <servlet-name>app</servlet-name>
    <url-pattern>/app/*</url-pattern>
  </servlet-mapping>
  
  <servlet>
    <servlet-name>admin</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>admin</servlet-name>
    <url-pattern>/admin/*</url-pattern>
  </servlet-mapping>

        // ContextLoaderListener 생성 // /WEB-INF/config/app-context.xml

        // DispatcherServlet 2개 생성 // 1. /WEB-INF/app-servlet.xml   2. /WEB-INF/admin-servlet.xml

  <context:component-scan base-package="bitcamp">
    <context:exclude-filter type="regex" expression="bitcamp.web.*"/>
  </context:component-scan>

        // /WEB-INF/config/app-context.xml // ContextLoaderListener 설정

        // expression // 해당하는 경로에 있는 것은 제외하고 객체를 생성하라

  <context:component-scan base-package="bitcamp.web">
    <context:exclude-filter type="regex" expression="bitcamp.web.admin.*"/>
  </context:component-scan>

        // /WEB-INF/admin-servlet.xml // app을 관리하는 DispatcherServlet 설정

        // expression을 서렁하여, admin 쪽은 객체를 생성하지 않게 한다. // admin은 admin이 관리

  <context:component-scan base-package="bitcamp.web">
    <context:exclude-filter type="regex" expression="bitcamp.web.app.*"/>
  </context:component-scan>

        // /WEB-INF/admin-servlet.xml // admin을 관리하는 DispatcherServlet 설정

        // expression을 서렁하여, app쪽은 객체를 생성하지 않게 한다. // appapp이 관리

        // ContextLoaderListener는 dao.BoardDao.java, vo.Board.java 객체를 관리하고,

        // DispatcherServlet app관리용은 web.admin.HelloController.java를 관리하고,

        // DispatcherServlet admin관리용은 web.admin.BoardController.java를 관리한다.

src-07 -> Java Config로 DispatcherServlet의 IoC 컨테이너 설정하기

  <servlet>
    <servlet-name>app</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextClass</param-name>
      <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
    </init-param>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>bitcamp.AppConfig</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>app</servlet-name>
    <url-pattern>/app/*</url-pattern>
  </servlet-mapping>

        // web.xml에 설정을 해주어야 한다. // 

        // Java Config로 IoC 컨테이너를 지정할 때는 기본 IoC 컨테이너를 교체해야 한다.

        // 다음과 같이 초기화 파라미터를 통해 DispatcherServlet이 사용할 

        // IoC 컨테이너 클래스와 Java Config 클래스를 설정한다.

        // contextClass라는 이름은 절대 변경되면 안된다. // contextClass라고 해야 한다.

        // value는 사용할 패키지의 context를 넣으면 된다. // 위의 context는 애노테이션을 읽어 객체를 생성한다.

import org.springframework.context.annotation.ComponentScan;

@ComponentScan("bitcamp")
public class AppConfig {

}

        // AppConfig 생성 // bitcamp 밑에 @Component가 붙은 class의 객체를 생성하게 한다.

src-08 -> SerlvetContainerInitializer 구현체의 활용 // 나중에 특강으로 설명하기로 함.

bitcamp-java-web-library 추가

library src-01 -> // mkdir로 폴더 생성후, gradle init - 3: library - 3: java - 1: Groovy - 1: JUnit 4 // 초기화 진행

        // java.servlet.ServletContainerIntializer

src-09 -> WebApplicationInitializer 구현 // web.xml 없이 서블릿 컨텍스트를 초기화할 수 있는 방법

        // web.xml 이나 애노테이션이 아닌 다른 방법으로 서블릿을 등록하기 // Servlet API의 표준 기술 활용

        // 1. 서블릿 컨테이너를 시작한다.

        // 2. 서블릿 컨테이너는 /WEB-INF/lib/*.jar 파일을 뒤진다.

        // 3. /META-INF/services/javax.servlet.SerlvetContainerInitializer 파일을 찾는다.

        // 4. 그 파일에서 ServletContainerInitializer 구현체를 알아낸다.

        // 5. 해당 구현체의 인스턴스를 생성한 후 onStartup()을 호출한다.

// 6. 만약 그 구현체가 필요로 하는 객체가 있다면, 해당 클래스를 찾아 onStartup()을 호출할 때 파라미터로 넘겨준다.

        // 위에 대한 설명은 SpringServlet 기술이 들어가기 전을 의미한다.

        // ServletContainerInitializer 를 구현한 클래스에 @HandlesTypes를 사용하면,

        // @HandlesTypes 안에 지정한 타입의 클래스를 모조리 찾아서 onStartup()의 파라미터로 넣어준다.

        // 위 방법이 Servlet 3.0에 추가된 xml 파일을 사용하지 않고, 서블릿 컨텍스트를 초기화할 수 있는 방법이다.

        // 아래는 해당 기능을 Spring framework에서 사용하는 방법과 순서이다.

        // WebApplicationInitializer 호출 과정 // 

        // 1. 서블릿 컨테이너(예: 톰캣 서버)를 시작한다.

// 2. spring-web-x.x.x.RELEASE.jar 파일에서 /META-INF/service/javax.servlet.SerlvetContainerInitializer 파일을 읽는다.

        // Servlet 3.0에서 추가된 2,3 번의 해당한다. 

        // 3. 이 파일에 등록된 클래스의 인스턴스를 생성한다. // SpringServletContainerInitializer 인스턴스 생성

        // SpringFrameWork에서는 SpringServletContainerInitializer가 Servlet 3.0에서 4번을 담당한다. 

        // 4. SpringServletContainerInitializer 객체에 대해 onStartup()을 호출한다.

        // SpringServletContainerInitializer에는 @HandlesTypes 애노테이션으로 

        // WebApplicationInitializer가 설정 되어 있다.

        // 호출할 때 WebApplicationInitializer를 구현한 클래스 목록을 넘겨준다.

        // @HandlesTypes는 핸들의 권한을 넘겨준다고 이해하면 편할 것 같다.

        // 즉 SpringServletContainerInitializerWebApplicationInitializer에게 권한을 위임한다.

        // 5) SpringServletContainerInitializer는

        // WebApplicationInitializer 구현체의 인스턴스를 만들고, onStartup()을 호출한다.

        // onStartup()에서는 ServletContext 초기화가 이루어지며, 초기화와 함께 URL 패턴까지 함께 지정한다.

@Override
  public void onStartup(ServletContext servletContext) throws ServletException {
    AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
    ac.scan("bitcamp");
    ac.refresh();
    DispatcherServlet servlet = new DispatcherServlet(ac);
    Dynamic registration = servletContext.addServlet("app", servlet);
    registration.setLoadOnStartup(1);
    registration.addMapping("/app/*");
  }

        // onStartup 메서드 구성 //

        // new AnnotationConfigWebApplicationContext(); // DispatcherServlet 에서 사용할 스프링 IoC 컨테이너를 준비

        // Java Config 클래스를 지정하는데에는 두가지 방법이 있다.

        // 1. ac.scan("bitcamp"); // Java Config 클래스가 있는 패키지를 지정하기

        // 2. ac.register(AppConfig.class); // IoC 컨테이너의 설정 정보를 갖고 있는 Java Config 클래스를 직접 지정하기 

        // new DispatcherServlet(ac); // DispatcherServlet 인스턴스를 생성한다.

        // servletContext.addServlet("app", servlet); // 웹 애플리케이션에 DispatcherServlet을 등록한다.

        // setLoadOnStartup(1); // 웹 애플리케이션에 등록된 DispatcherServlet을 설정한다.

        // addMapping("/app/*"); // DispatcherServlet에 URL 패턴을 지정한다.

        // 결론 //

        // WebApplicationInitializer를 상속 받아 DispatcherServlet를 초기화하게 구현하여 사용하면 된다.

src-10 -> WebApplicationInitializer 구현 // AbstractAnnotationConfigDispatcherServletInitializer 상속

        // AbstractAnnotationConfigDispatcherServletInitializer // 

        // 이 클래스는 미리 AnnotationConfigWebApplicationContext IoC 컨테이너를 준비했다.

        // 따라서 IoC 컨테이너를 따로 설정할 필요가 없다.

        // 또한 DispatcherServlet을 등록하는 코드가 이미 작성되어 있기 때문에 따로 등록할 필요가 없다.

        // 즉 src-09와 같이 인터페이스를 상속받아 직접 구현하는 것 보다

        // 미리 어느정도 구현한 추상클래스를 상속받아 구현하는 것이 사용하기 편하다.

        // getServletConfigClasses() // return new Class<?>[] {AppConfig.class};

      // 스프링 Web MVC 프레임워크에서 DispatcherServlet이 사용할 IoC 컨테이너를 준비할 때 이 메서드를 호출한다.

        // 이 메서드가 리턴한 Java config 클래스를 이용하여 IoC 컨테이너를 설정할 것이다.

        // getServletMappings() // return new String[] {"/app/*"};

        // 스프링 프레임워크에서 DispatcherServlet을 등록할 때 이 메서드를 호출한다. 

        // 이 메서드의 리턴 값이 리턴 값이 URL 패턴으로 사용된다. 

        // getServletName() // return "app";

        // 스프링 Web MVC 프레임워크는 웹 애플리케이션에 DispatcherServlet을 등록할 때 이 메서드를 호출한다.

        // 이 메서드의 리턴 값이 서블릿의 이름으로 사용된다.

        // 이 메서드를 오버라이딩 하지 않으면 기본 이름("dispatcher")이 사용된다.

        // 한 개만 등록할 것이라면 오버라이딩 하지 않아도 되지만,

        // 여러 개의 DispatcherServlet을 등록할 것이라면 오버라이딩 하여 이름을 다르게 해야 한다.

src-11 -> WebApplicationInitializer 구현 // AbstractDispatcherServletInitializer 상속

        // protected WebApplicationContext createServletApplicationContext() // return iocContainer;

        // 스프링 Web MVC 프레임워크는 DispatcherServlet이 사용할 IoC 컨테이너를 설정할 때 이 메서드를 호출한다.

        // 리턴 값이 DispatcherServlet의 IoC 컨테이너로 사용된다.

        // 1. XML 기반 IoC 컨테이너를 사용할 경우,

    XmlWebApplicationContext iocContainer = new XmlWebApplicationContext();
    iocContainer.setConfigLocation("/WEB-INF/app-servlet.xml");

        // 2. Java Config 기반 IoC 컨테이너를 사용할 경우, 

    AnnotationConfigWebApplicationContext iocContainer = 
        new AnnotationConfigWebApplicationContext();
    iocContainer.register(AppConfig.class);

        // protected String[] getServletMappings() // return new String[] {"/app/*"}; // 사용법 src-10와 동일

        // protected String getServletName() // return "app"; // 사용법 src-10와 동일

src-12 -> Request Handler 정의하는 방법

app1

Controller01_1 -> 페이지 컨트롤러 만드는 방법

        // @Controller // 페이지 컨트롤러를 만드는 것이기 때문에 이 애노테이션을 붙인다.

@Controller
@RequestMapping("/c01_1")
public class Controller01_1 {

  @RequestMapping
  @ResponseBody
  public String handler() {
    return "c01_1 -> handler()";
  }

 // @RequestMapping("경로명") // 클래스에 붙는 RequestMapping은 경로명에 해당하면 Class가 실행된다는 것이다.

        // @RequestMapping // 메서드에 붙는 RequestMapping은 Class 경로에 해당했을 때 실행될 메서드 이다.

        // @ResponseBody // 메서드의 리턴 값이 클라이언트에게 출력할 내용임을 표시한다.

        // 메서드에 @RequestMapping 붙는 것은 하나여야 한다. // URL 한 개 당 한 개의 핸들러만 연결할 수 있다.

        // 경로가 호출 됐을 때 어떤 메서드를 실행해야 할 지 모르기 때문에 오류가 발생하는 것이다.

Controller01_2 -> 페이지 컨트롤러에 여러 개의 요청 핸들러 두기

@Controller 
public class Controller01_2 {

  @RequestMapping("/c01_2_h1")
  @ResponseBody
  public String handler() {
    return "c01_2_h1";
  }
  
  @RequestMapping("/c01_2_h2")
  @ResponseBody 
  public String handler2() {
    return "c01_2_h2";
  }
  
  @RequestMapping("/c01_2/h3")
  @ResponseBody 
  public String handler3() {
    return "/c01_2/h3";
  }
  
  @RequestMapping("/c01_2/h4")
  @ResponseBody 
  public String handler4() {
    return "/c01_2/h4";
  }
}

        // @RequestMapping("경로명") // 메서드에 경로명이 붙은 RequestMapping이 붙으면,

        // 여러개의 핸들러를 둘 수 있다. // 경로명으로 어떤 메서드가 호출 됐는지 구분이 가능하기 때문

        // Class에 RequestMapping이 없다. // 메서드에 경로명이 붙었기 때문에.

Controller01_3 -> 기본 URL과 상세 URL을 설정하는 방법

@Controller 
@RequestMapping("/c01_3")
public class Controller01_3 {

  @RequestMapping("h1")
  @ResponseBody
  public String handler() {
    return "h1";
  }
  
  @RequestMapping("/h2")
  @ResponseBody 
  public String handler2() {
    return "h2";
  }
  
  @RequestMapping("h3") 
  @ResponseBody 
  public String handler3() {
    return "h3";
  }
  
  @RequestMapping("h4") 
  @ResponseBody 
  public String handler4() {
    return "h4";
  }

        // @RequestMapping("경로명") // Class에 붙으면 기본 경로를 설정할 수 있다.

    // @RequestMapping("상세경로명") // 메서드에 상세 경로명만 붙는다면, 기본 경로에서 추가적인 경로를 의미한다.

        // 상세경로명 // 기본 URL에 뒤에 붙는 상세 URL. // 앞에 /를 붙여도 되고 생략해도 된다.

        // 위의 예시는 /c01_3/h1, /c01_3/h2, /c01_3/h3, /c01_3/h4, 이런식으로 호출된다.

Controller02_1 -> GET, POST 구분하기 1

  @RequestMapping(method = RequestMethod.GET)
  @ResponseBody 
  public String handler1() {
    return "get";
  }
  
  @RequestMapping(method = RequestMethod.POST)
  @ResponseBody 
  public String handler2() {
    return "post";
  }

        // @RequestMapping(method = RequestMethod.POST)

        // GET 요청과 POST 요청을 RequestMapping에서 구분할 수 있다.

Controller02_2 -> GET, POST 구분하기 2

  @GetMapping
  @ResponseBody 
  public String handler1() {
    return "get";
  }
  
  @PostMapping
  @ResponseBody 
  public String handler2() {
    return "post";
  }

        // @GetMapping, @PostMapping // 애노테이션으로 GET POST 요청을 구분할 수 있다.

Controller03_1 -> 파라미터 이름으로 구분하기

@Controller 
@RequestMapping("/c03_1")
public class Controller03_1 {

  @GetMapping(params="name") 
  @ResponseBody 
  public String handler1() {
    return "handler1";
  }
  
  @GetMapping(params="age") 
  @ResponseBody 
  public String handler2() {
    return "handler2";
  }
  
  @GetMapping(params={"age","name"}) 
  @ResponseBody 
  public String handler3() {
    return "handler3";
  }

        // ex) http://localhost:8080/java-spring-webmvc/app1/c03_1?name=kim // handler1() 실행

        // app1/c03_1?age=20 // handler2() 실행 // app1/c03_1?age=20 // handler3() 실행

        // app1/c03_1 // 오류 발생 // 받아줄 메서드가 없기 때문에 오류가 발생한다.

Controller03_2 -> 요청 헤더 이름으로 구분하기

  @GetMapping(headers="name") 
  @ResponseBody 
  public String handler1() {
    return "handler1";
  }
  
  @GetMapping(headers="age") 
  @ResponseBody 
  public String handler2() {
    return "handler2";
  }
  
  @GetMapping(headers={"age","name"}) 
  @ResponseBody 
  public String handler3() {
    return "handler3";
  }
  
  @GetMapping
  @ResponseBody 
  public String handler4() {
    return "handler4";
  }

        // @GetMapping(headers="name") // @RequestMapping(method=RequestMethod.GET, headers="name")

<button id="btn1" type="button">name 헤더 요청</button><br>
<button id="btn2" type="button">age 헤더 요청</button><br>
<button id="btn3" type="button">name + age 헤더 요청</button><br>

<textarea id="ta1" cols="50" rows="10"></textarea>

<script>
var b1 = document.getElementById("btn1");
var b2 = document.getElementById("btn2");
var b3 = document.getElementById("btn3");
var ta1 = document.getElementById("ta1");

b1.onclick = function() {
  var xhr = new XMLHttpRequest();
  xhr.open("get", "../../app1/c03_2", false);
  xhr.setRequestHeader("name", "kim");
  xhr.send();
  ta1.value = xhr.responseText;  
};

b2.onclick = function() {
  var xhr = new XMLHttpRequest();
  xhr.open("get", "../../app1/c03_2", false);
  xhr.setRequestHeader("age", "20");
  xhr.send();
  ta1.value = xhr.responseText;  
};

b3.onclick = function() {
  var xhr = new XMLHttpRequest();
  xhr.open("get", "../../app1/c03_2", false);
  xhr.setRequestHeader("name", "kim");
  xhr.setRequestHeader("age", "20");
  xhr.send();
  ta1.value = xhr.responseText;  
};

</script>
</body>
</html>

        // 요청 헤더 중에서 특정 이름을 갖는 헤더가 있을 때 호출될 메서드를 지정할 수 있다.

        // HTML에서 특정 이름을 갖게끔 설정을 해두었다. //

        // 웹 페이지에서 링크를 클릭하거나 입력 폼에 값을 넣고 등록 버튼을 누르는 

        // 일반적인 상황에서는 요청헤더에 임의의 헤더를 추가할 수 없다.

        // 자바스크립트 등의 프로그래밍으로 임의의 HTTP 요청을 할 때

        // HTTP 프로토콜에 표준이 아닌 헤더가 추가될 수 있다.

        // 그 헤더를 처리하는 메서드를 정의할 때 사용한다.

        // 보통 Open API를 개발하는 서비스 회사에서 많이 사용한다.

        // 헤더가 없는 경우, handler4가 호출된다.

Controller03_3 -> Accept 요청 헤더의 값에 따라 구분하기

        // Accept 요청 헤더 // HTTP 프로토콜이다.

        // HTTP 클라이언트(웹 브라우저)에서 서버에 요청할 때 받고자 하는 콘텐트의 타입을 알려준다.

b1.onclick = function() {
  var xhr = new XMLHttpRequest();
  xhr.open("get", "../../app1/c03_3", false);
  xhr.setRequestHeader("Accept", "text/plain");
  xhr.send();
  ta1.value = xhr.responseText;  
};

b2.onclick = function() {
  var xhr = new XMLHttpRequest();
  xhr.open("get", "../../app1/c03_3", false);
  xhr.setRequestHeader("Accept", "text/html");
  xhr.send();
  ta1.value = xhr.responseText;  
};

b3.onclick = function() {
  var xhr = new XMLHttpRequest();
  xhr.open("get", "../../app1/c03_3", false);
  xhr.setRequestHeader("Accept", "application/json");
  xhr.send();
  ta1.value = xhr.responseText;  
};

        // 버튼 클릭에 따라, Accept에 해당 값을 보내게끔 한다.

  @GetMapping(produces = "text/plain") 
  @ResponseBody 
  public String handler1() {
    return "handler1";
  }
  
  @GetMapping(produces = "text/html") 
  @ResponseBody 
  public String handler2() {
    return "handler2";
  }
  
  @GetMapping(produces = "application/json") 
  @ResponseBody 
  public String handler3() {
    return "handler3";
  }

        // 넘어온 Accept 값을 확인하여 해당 값을 리턴하는 메서드가 실행되게 한다.

        // Accept는 해당 값을 요청하는 헤더이며, produces는 이 값을 제공한다라고 알려주는 것이다.

        // 따라서 요청하는 값에 맞는 produces가 실행되게 하는 것이다.

        // 단, Web Browers에서 Accept의 기본 타입이 text/html이기 때문에 유의하여 사용하여야 한다.

        // 백앤드와 프론트앤드 개발자 간의 정교하게 어떤 타입으로 주고 받을지 논의할 때 많이 사용된다.

Controller03_4 -> Content-Type 헤더의 값에 따라 구분하기

        // Content-Type 요청 헤더 // HTTP 프로토콜이다.

        // HTTP 클라이언트가 보내는 데이터의 콘텐트 타입이다.

        // 프론트 컨트롤러는 보내는 데이터의 타입에 따라 처리를 분리할 수 있다.

        // 클라이언트가 POST로 요청할 때 보내는 데이터의 유형에 따라 호출될 메서드를 구분할 수 있다.

<form action="../../app1/c03_4" method="post">
이름: <input type="text" name="name" value="kim"><br>
나이: <input type="text" name="age" value="20"><br>
<button>application/x-www-form-urlencoded 형식으로 데이터 보내기</button>
</form>
<hr>

<form action="../../app1/c03_4" method="post" enctype="multipart/form-data">
이름: <input type="text" name="name" value="kim"><br>
나이: <input type="text" name="age" value="20"><br>
<button>multipart/form-data 형식으로 데이터 보내기</button>
</form>
<hr>

<button id="btn1" type="button">text/csv 형식으로 데이터 보내기</button><br>
<hr>

<button id="btn2" type="button">application/json 형식으로 데이터 보내기</button><br>
<hr>

<a href="../../app1/c03_4">그냥 요청</a><br>

<textarea id="ta1" cols="50" rows="10"></textarea>

<script>
var b1 = document.getElementById("btn1");
var b2 = document.getElementById("btn2");
var ta1 = document.getElementById("ta1");

b1.onclick = function() {
  var xhr = new XMLHttpRequest();
  xhr.open("post", "../../app1/c03_4", false);
  xhr.setRequestHeader("Content-Type", "text/csv");
  xhr.send("kim,20");
  ta1.value = xhr.responseText;  
};

b2.onclick = function() {
  var xhr = new XMLHttpRequest();
  xhr.open("post", "../../app1/c03_4", false);
  xhr.setRequestHeader("Content-Type", "application/json");
  xhr.send('{"name":"kim","age":20}');
  ta1.value = xhr.responseText;  
};

        // application/x-www-form-urlencoded // 클라이언트가 POST 요청으로 데이터를 보낼 때 기본 형식 

        // 태그에서 enctype 속성에 "mulpart/form-data"를 지정하면 해당 형식으로 서버에 값을 보낸다.

        // 자바스크립트를 사용하여 개발자가 임의의 형식으로 값을 보낼 수 있다.

        // 클라이언트가 POST로 요청할 때 보내는 데이터의 유형에 따라 호출될 메서드를 구분할 수 있다.

  @PostMapping(consumes = "application/x-www-form-urlencoded") 
  @ResponseBody 
  public String handler1() {
    return "handler1";
  }
  
  @PostMapping(consumes = "multipart/form-data") 
  @ResponseBody 
  public String handler2() {
    return "handler2";
  }
  
  @PostMapping(consumes = "text/csv") 
  @ResponseBody 
  public String handler3() {
    return "handler3";
  }
  
  @PostMapping(consumes = "application/json") 
  @ResponseBody 
  public String handler4() {
    return "handler4";
  }

  @RequestMapping 
  @ResponseBody 
  public String handler5() {
    return "handler5";
  }  

        // @PostMapping(consumes = "값") // @PostMapping(consumes = "application/x-www-form-urlencoded") 

        // application/x-www-form-urlencoded 형식의 데이터를 소비한다. // 

        // 클라이언트의 HTTP 요청에서 Content-Type 헤더의 mime 값이 위와 같을 때 이 메서드를 호출하라는 의미다.

Controller04_1 -> 프론트 컨트롤러로부터 받을 수 있는 파라미터 값

        // 프론트 컨트롤러(DispatcherServlet)로부터 받고 싶은 값이 있다면 

        // 요청 핸들러를 정의할 때 받고 싶은 타입의 아규먼트를 선언하라!

        // 그러면 프론트 컨트롤러가 메서드를 호출할 때 해당 타입의 값을 넘겨줄 것이다.

 @Autowired ServletContext sc;
  
  @GetMapping("h1") 
  @ResponseBody 
  public void handler1(
      ServletRequest request, 
      ServletResponse response,
      HttpServletRequest request2, 
      HttpServletResponse response2,
      HttpSession session,
      Map<String,Object> map, 
      Model model, 
      PrintWriter out
      ) {
  }

        // ServletContext는 의존 객체로 주입 받아야 한다. // 요청 핸들러에서 아규먼트로 받을 수 없다.

        // 요청 핸들러에서 받을 수 있는 타입의 아규먼트는 많이 쓰이기 때문에 기억해두자

        // map // JSP에 전달할 값을 담는 임시 보관소

        // model // Map과 같다. 둘 중 한 개만 받으면 된다.

        // out // 클라이언트에게 콘텐트를 보낼 때 사용할 출력 스트림

Controller04_2 -> @RequestParam

        // 클라이언트가 보낸 파라미터 값을 바로 받을 수 있다.

        // 요청 핸들러의 아규먼트로 선언하면 된다. // 파라미터 앞에 @RequestParam 애노테이션을 붙인다.

        // 클라이언트가 보낸 파라미터 이름을 지정한다.

  @GetMapping("h1") 
  @ResponseBody 
  public void handler1(
      PrintWriter out,
      ServletRequest request,
      @RequestParam(value="name") String name1,
      @RequestParam(name="name") String name2,
      @RequestParam("name") String name3,
      String name
      ) {
    
    out.printf("name=%s\n", request.getParameter("name"));
    out.printf("name=%s\n", name1);
    out.printf("name=%s\n", name2);
    out.printf("name=%s\n", name3);
    out.printf("name=%s\n", name);
  }

        // 1. request 객체를 받아서 getParameter로 값을 달라고 할 수도,

        // 2. @RequestParam애노테이션 value값 설정으로 달라고 할 수도,

        // 3. @RequestParam애노테이션 name값 설정으로 달라고 할 수도,

        // 4. @RequestParam애노테이션"name" 지정하여 값을 달라고 할 수도,

        // 5. name을 다이렉트로 호출할 수도 있다.

        // 2, 3 // value와 name은 같은 일을 한다.

        // 4 // value 이름을 생략할 수 있다.

        // 5 // 요청 파라미터 이름과 메서드 파라미터(아규먼트)의 이름이 같다면 애노테이션을 생략해도 된다.

        // 같지 않다면 당연히 사용할 수 없다.

  @GetMapping("h2") 
  @ResponseBody 
  public void handler2(
      PrintWriter out,
      @RequestParam("name1") String name1,
      String name2,
      @RequestParam(value="name3",required=false) String name3, 
      @RequestParam(value="name4",defaultValue="ohora") String name4
      ) {
    
    out.printf("name1=%s\n", name1);
    out.printf("name2=%s\n", name2);
    out.printf("name3=%s\n", name3);
    out.printf("name4=%s\n", name4);
  }

        // 1. @RequestParam애노테이션"name"에 값이 없으면 오류가 발생한다.

        // 애노테이션을 붙이면 필수 항목으로 인지한다. // 파라미터 값이 없으면 예외가 발생한다.

        // 2. name2 // 애노테이션을 붙이지 않으면 선택 항목으로 인지한다. // 파라미터 값이 없으면 null을 받는다.

        // 3. @RequestParam(value="name3",required=false) String name3

        // required 프로퍼티를 false로 설정하면 선택 항목으로 인지한다.

        // 4. @RequestParam(value="name4",defaultValue="ohora") String name4

        // 기본 값을 지정하면 파라미터 값이 없어도 된다.

Controller04_3 -> 도메인 객체(값 객체; Value Object)로 요청 파라미터 값 받기

  public void handler1(
      PrintWriter out,
      String model,
      String maker,
      int capacity,
      boolean auto,
      Car car
      )

        // int capacity // 프론트 컨트롤러가 String 값을 int로 변환해 준다. // 변환할 수 없을 경우 예외 발생

        // @RequestParam(defaultValue="100") 처럼 default 값을 지정해주면 값이 없어도 예외 발생하지 않는다.

        // 단, 파라미터로 넘어오는 값은 무조건 String이기 때문에 ""사이에 넣어야 한다. // String으로 취급해야한다.

        // boolean auto // 프론트 컨트롤러가 String 값을 boolean으로 변환해 준다. // 변환할 수 없을 경우 예외 발생

        // "true", "false"는 대소문자 구분없이 true, false로 변환해 준다. // 

        // 1 : true, 0 : false 로 변환해 준다. 그 외 숫자는 예외 발생!

        // Car car // 아규먼트가 값 객체이면 요청 파라미터 중에서,

        // 값 객체의 프로퍼티 이름과 일치하는 항목에 대해 값을 넣어준다.

        // 값 객체 안에 또 값 객체가 있을 때는 OGNL 방식으로 요청 파라미터 값을 지정하면 된다.

        // ex) ...&engine.model=ok&engine.cc=1980&engine.valve=16

Controller04_4 -> 프로퍼티 에디터 사용하기

        // 요청 파라미터 값(String 타입)을 request handler의 아규먼트 타입(String, int, boolean 등)의 값으로 바꿀 때

        // primitive type에 대해서만 자동으로 변환해 준다.

        // 그 외의 타입에 대해서는 프로퍼티 에디터(타입 변환기)가 없으면 예외를 발생시킨다.

        // Value Object 객체 값 변환 // 프로퍼티 에디터 사용 // 프로퍼티 에디터를 만들고 사용하여야 한다.

        // ex) http://.../c04_4/h2?car=sonata,5,true,2019-4-19 // 

        // 원래라면 car.model=sonata&car.capacity=5 이런식으로 일일히 프로퍼티를 적어야 했다.

  @GetMapping("h2") 
  @ResponseBody 
  public void handler2(
      PrintWriter out,
      @RequestParam("car") Car car
      ) {
    
    out.println(car);
  }

        // @RequestParam("vo객체 이름") // 애노테이션을 붙히면 된다.

        // 프로퍼티 에디터를 프론트 컨트롤러에게 적용하기 //

        // 프론트 컨트롤러는 request handler를 호출하기 전에 넘겨줄 아규먼트 값을 준비해야 한다.

        // 각 아규먼트 값을 준비할 때 @InitBinder가 표시된 메서드를 호출하여 프로퍼티 에디터(변환기)를 준비시킨다.

        // 프로퍼티 에디터를 이용하여 파라미터 값을 request handler의 아규먼트가 원하는 타입의 값을 바꾼다.

        // request handler의 아규먼트 개수 만큼 이 메서드를 호출한다.

@InitBinder
  public void initBinder(WebDataBinder binder) {
    System.out.println("Controller04_4.initBinder()...");
    DatePropertyEditor propEditor = new DatePropertyEditor();
    
    binder.registerCustomEditor(
        java.util.Date.class,
        propEditor
    );
    
    binder.registerCustomEditor(
        Car.class,
        new CarPropertyEditor()
    );
    
    binder.registerCustomEditor(
        Engine.class,
        new EnginePropertyEditor()
    );
  }

        // 프로퍼티 에디터를 등록하려면 그 일을 수행할 객체(WebDataBinder)가 필요하다.

        // request handler 처럼 아규먼트를 선언하여 프론트 컨트롤러에게 달라고 요청하라.

        // binder의 경우 아규먼츠의 갯수만큼 호출 된다.

        // ex) public void handler1( PrintWriter out, String model, int capacity, boolean auto, Date createdDate) {

        // out을 제외한, model, capacity, auto, createdDate때 마다 호출된다. // 즉 initBinder가 4번 다 호출된다.

        // 비효율적이라고 생각해서 createdDate에만 적용하게끔 하려고 하지마라,

        // 스프링에서 이렇게 사용한다. // 즉 엄청나게 비효율적이였으면 진즉에 고쳐졌을 것이다.

        // 코드를 고치는 것 자체가 더 비효율 적이기 때문에 이렇게 사용하는 것이다.

        // binder.registerCustomEditor() // 바인더에 registerCustomEditor()를 호출하여 프로퍼티 에디터를 등록한다.

        // ex) java.util.Date.class, propEditor // Car.class, new CarPropertyEditor() // 파라미터는 2개를 선언할 수 있는데

        // 첫번째 파라미터의 Date.class, Car.class는 String에서 어떤 타입으로 바꿀 것인지를 말한다. 

        // 아규먼츠가 넘어올 때 해당 class 타입이 맞다면 프로퍼티 에디터를 실행한다.

        // 두번째 파라미터의 propEditor, new CarPropertyEditor()가 프로퍼티 에디터를 말한다.

        // 프로퍼티 에디터 // PropertyEditor

        // 문자열을 특정 타입의 프로퍼터의 값으로 변환시킬 때 사용하는 에디터이다.

        // java.beans.PropertyEditor 인터페이스를 구현해야 한다.

        // PropertyEditor를 직접 구현하면 너무 많은 메서드를 오버라이딩 해야 하기 때문에

        // 자바에서는 도우미 클래스인 PropertyEditorSupport 클래스를 제공한다.

  class CarPropertyEditor extends PropertyEditorSupport {
    @Override
    public void setAsText(String text) throws IllegalArgumentException {
      String[] values = text.split(",");
      
      Car car = new Car();
      car.setModel(values[0]);
      car.setCapacity(Integer.parseInt(values[1]));
      car.setAuto(Boolean.parseBoolean(values[2]));
      car.setCreatedDate(java.sql.Date.valueOf(values[3]));
      
      setValue(car);
    }
  }

        // setAsText 메서드를 오버라이딩 한다.

        // 프로퍼티 에디터를 사용하는 측(ex 프론트 컨트롤러)에서

        // 문자열을 Date 객체로 바꾸기 위해 이 메서드를 호출할 것이다.

        // 넘어온 text를 프로퍼티가 원하는 타입으로 변환한 후 저장하면 된다.

        // getValue() 메서드 // 오버라이딩 할 필요 없다. // 알아둬야하는 메서드이다.

        // 프로퍼티 에디터를 사용하는 측(예: 프론트 컨트롤러)에서 변환된 값을 꺼낼 때 호출된다. 

class DatePropertyEditor extends  PropertyEditorSupport {
    SimpleDateFormat format;
    public DatePropertyEditor() {
      format = new SimpleDateFormat("yyyy-MM-dd");
    }
    
    public DatePropertyEditor(SimpleDateFormat format) {
      this.format = format;
    }
    
    @Override
    public void setAsText(String text) throws IllegalArgumentException {
      System.out.println("DatePropertyEditor.setAsText()");
      try {
        setValue(java.sql.Date.valueOf(text));
        
      } catch (Exception e) {
        throw new IllegalArgumentException(e);
      }
    }
  }

        // 이런 식으로 생성자를 활용한 프로퍼티 에디터를 만들 수도 있다. // 알아두자

Controller04_5 -> 글로벌 프로퍼티 에디터 적용

        // 프로퍼티 에디터의 한계 //

        // 다른 페이지 컨트롤러에서 등록한 프로퍼티 에디터는 사용할 수 없다.

        // 각 페이지 컨트롤러 마다 자신이 사용할 프로퍼티 에디터를 등록해야 한다.

    // 따라서 여러 페이지 컨트롤러에서 공통으로 사용하는 프로퍼티 에디터라면 글로벌 프로퍼티 에디터로 등록한다.

        // 글로벌 프로퍼티 에디터 등록 // 

        // @ControllerAdvice 애노테이션이 붙은 클래스에서 @InitBinder 메서드를 정의하면 된다.

        // @ControllerAdvice를 붙은 클래스는 여러 페이지 컨트롤러에 공통적으로 호출되기 때문에

        // @ControllerAdvice가 붙은 클래스 안에, @InitBinder 메서드 안에, 프로퍼티 에디터를 등록한다.

        // 그러면 글로벌 프로퍼티 에디터로 사용이 가능하다. // 여러 페이지 컨트롤러에 적용 가능하다.

        // @ControllerAdvice // 

        // 페이지 컨트롤러를 실행할 때 충고하는 역할을 수행한다.

        // 프론트 컨트롤러가 페이지 컨트롤러의 request handler를 호출하기 전에 

        // 이 애노테이션이 붙은 클래스를 참고하여 적절한 메서드를 호출한다.

@ControllerAdvice
public class GlobalControllerAdvice {

  @InitBinder
  public void initBinder(WebDataBinder binder) {
    
    DatePropertyEditor propEditor = new DatePropertyEditor();
    binder.registerCustomEditor(
        java.util.Date.class, 
        propEditor  
    );
  }
}

        // 이런식으로 ControllerAdvice에서 해당 케이스에 맞는 InitBinder 등등을 알아서 호출한다.

        // 언제 사용하라고 애노테이션만 잘 붙혀두면 된다.

Controller04_6 -> HTTP 요청 헤더를 받는 방법 // @RequestHeader 

        // 클라이언트의 HTTP 요청 헤더를 받고 싶으면 

        // request handler의 아규먼트 앞에 @RequestHeader(헤더명) 애노테이션을 붙여라!

  @GetMapping("h1") 
  @ResponseBody 
  public void handler1(
      PrintWriter out,
      @RequestHeader("Accept") String accept,
      @RequestHeader("User-Agent") String userAgent
      ) {
    
    out.printf("Accept=%s\n", accept);
    out.printf("User-Agent=%s\n", userAgent);
    
    if (userAgent.matches(".*Chrome.*")) {
      out.println("chrome");
    } else if (userAgent.matches(".*Safari.*")) {
      out.println("safari");
    } else if (userAgent.matches(".*Firefox.*")) {
      out.println("firefox");
    } else {
      out.println("etc");
    }
  }

        // @RequestHeader를 사용할때 유의해야 한다. // google 브라우저는 User-Agent를 보내주지 않는다.

        // 이유 // 개인정보에 해당한다고 생각돼서 아예 보내주지 않는다.

        // 따라서 브라우저(유저정보)를 구분하여 처리해야되는 방법은 다른 방안을 생각해보아야 한다.

        // google에서는 브라우저 구분해서 따로 개발하지 말라는 마인드이다. 

        // User-Agent // 어떤 OS를 사용하는지, 어떤 브라우저로 접속 했는지 등등 개인 정보가 담겨져 있다.

        // ex) Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) 

        // Chrome/73.0.3683.103 Safari/537.36

Controller04_7 -> 쿠키 // @Cookie

        // 클라이언트가 보낸 쿠키 꺼내기 //

        // @CookieValue(쿠키명) 애노테이션을 request handler의 아규먼트 앞에 붙인다.

  @GetMapping("h1") 
  @ResponseBody 
  public void handler1(
      PrintWriter out,
      HttpServletResponse response
      ) {
    try {
      response.addCookie(new Cookie("name1", "AB가각"));
      response.addCookie(new Cookie(
              "name2", 
              URLEncoder.encode("AB가각", "UTF-8")
      ));
      response.addCookie(new Cookie("name3", "HongKildong"));
      response.addCookie(new Cookie("age", "30"));
      
    } catch (Exception e) {
      e.printStackTrace();
    }
    
    out.println("send cookie2!");
  }

        // 테스트할 데이터 // 쿠키의 값이 ASCII가 아니라면 URL 인코딩 해야만 데이터가 깨지지 않는다.

        // URL 인코딩을 하지 않으면 ? 문자로 변환된다.

  @GetMapping(value = "h2", produces = "text/plain;charset=UTF-8") 
  @ResponseBody 
  public String handler2(
      @CookieValue(value="name1", required=false) String name1,
      @CookieValue(value="name2", defaultValue="") String name2,
      @CookieValue(value="name3", defaultValue="") String name3,
      @CookieValue(value="age", defaultValue="0") int age
      ) throws Exception {
    
    byte[] originBytes = name2.getBytes("ISO-8859-1"); 
    String namex = new String(originBytes, "UTF-8");
    
    return String.format("name1 = %s\n"
        + "name2 = %s\n"
        + "name3 = %s\n"
        + "age = %d\n", 
        name1, 
        namex, 
        name3, 
        age);
  }
}

        // required와 defaultValue를 설정하는 이유 //

        // @CookieValue는 쿠키 값을 받지 않으면 에러를 발생한다.

        // 따라서 required를 false로 하여 안받아도 되게 설정하거나, defaultValue를 설정한다.

        // CookieValue age의 경우 int 값이다. // 그러나 CookieValue는 항상 문자열이다.

        // 따라서 "0"으로 적어야 한다. // String을 int로 바꿔야 하기 때문이다. // 0을 String화 하여 적는다.

        // namex 확인

        // 예전에는 (ex 톰캣 서버 예전버전)은 한글을 사용하면 무조건 에러가 발생한다.

        // 따라서 보낼 때 무조건 URL 인코딩해서 보내야 했다. // URLEncoder.encode("AB가각", "UTF-8") 

        // 또한 받을 때도 무조건 URL 디코딩을 해서 받아야 했다.

        // 하지만 요즘 최신 버전에서는 자동으로 인코딩 디코딩을 거친다.

        // 인코딩, 디코딩을 해야하는 이유 // 

        // URLEncoder.encode("AB가각", "UTF-8") //

        // JVM 문자열은 UTF-16 바이트 배열이다. // 0041 0042 ac00 ac01

        // UTF-8 바이트로 변환한다. // 41 42 ea b0 80 ea b0 81

        // 8비트 데이터가 짤리지 않도록 URL 인코딩으로 7비트화 시킨다. // "AB%EA%B0%80%EA%B0%81"

        // 41 42 25 45 41 25 42 30 25 38 30 25 45 41 25 42 30 25 38 31

        // 웹 브라우저(클라이언트)에서는 받은 값을 그대로 저장한다.

        // 쿠키를 웹 브라우저(클라이언트)에서 다시 서버로 보낼 때 //

        // 웹 브라우저는 저장된 값을 그대로 전송한다. // "AB%EA%B0%80%EA%B0%81"

        // 41 42 25 45 41 25 42 30 25 38 30 25 45 41 25 42 30 25 38 31

        // 프론트 컨트롤러가 쿠키 값을 꺼낼 때 자동으로 URL 디코딩을 수행한다.

        // 즉 7비트 문자화된 코드를 값을 원래의 8비트 코드로 복원한다. // 41 42 ea b0 80 ea b0 81 

        // 디코딩 하여 나온 바이트 배열을 UTF-16으로 만든다.

        // 문제는 바이트 배열을 ISO-8859-1로 간주한다는 것이다.

        // 그래서 UTF-16으로 만들 때 무조건 앞에 00 1바이트를 붙인다.

        // 0041 0042 00ea 00b0 0080 00ea 00b0 0081

        // 그래서 한글이 깨진 것이다.

        // 추가적으로 해줘야 하는 작업 // 디코딩하는 방법 // 위의 경우일 때

        // byte[] originBytes = name2.getBytes("ISO-8859-1");

        // 클라이언트에서 받은 값은 UTF-16이다. 따라서 먼저 ISO-8859-1 바이트 배열로 변경한다.

        // String namex = new String(originBytes, "UTF-8");

        // 다시 바이트 배열을 UTF-16으로 바꾼다. // 이때 바이트 배열이 UTF-8로 인코딩된 값임을 알려줘야 한다.

        // namex를 사용한다. // 정상적으로 디코딩이 된 것이기 때문

Controller04_8 -> multipart/form-data 형식의 파라미터 값 받기

        // @Autowired ServletContext sc; //

        // ServletContext는 메서드의 아규먼트로 받을 수 없다. // 의존 객체로 주입 받아야 한다.

        // 클라이언트가 멀티파트 형식으로 전송한 데이터를 꺼내기

        // 1. ServletAPI에서 제공하는 Part를 사용한다.

        // 2. Spring에서 제공하는 MultipartFile 타입의 아규먼트를 선언하면 된다.

        // 주의 // 

        // DispatcherServlet을 web.xml을 통해 배치했다면, <multipart-config/> 태그를 추가해야 한다.

 // DispatcherServlet을 WebApplicationInitializer를 통해 배치했다면, customizeRegistration 메서드를 추가해야 한다.

        // 밑 예시에서 상세로 적는다.

  @PostMapping(value="h1", produces="text/html;charset=UTF-8") 
  @ResponseBody 
  public String handler1(
      String name,
      int age,
      Part photo
      ) throws Exception {
    
    String filename = null;
    if (photo.getSize() > 0) {
      filename = UUID.randomUUID().toString();
      String path = sc.getRealPath("/html/app1/" + filename);
      photo.write(path);
    }
        return 
        "<html><head><title>c04_8/h1</title></head><body>" + 
        "<h1>업로드 결과</h1>" + 
        "<p>이름:" + name + "</p>" +  
        "<p>나이:" + age + "</p>" + 
        (filename != null ? "<p><img src='../../html/app1/" + filename + "'></p>" : "") + 
        "</body></html>";
  }

        // 1. Part를 사용하는 방법 // photo를 Part로 선언한다. // ServletAPI의 객체

        // bitcamp-spring-webmvc/app1/c04_8/h1

        // 현재 URL이 다음과 같기 때문에 업로드 이미지의 URL을 이 경로를 기준으로 계산해야 한다.

  @PostMapping(value="h2", produces="text/html;charset=UTF-8") 
  @ResponseBody 
  public String handler2(
      String name,
      int age,
      MultipartFile photo
      ) throws Exception {
    
    String filename = null;
    if (!photo.isEmpty()) {
      filename = UUID.randomUUID().toString();
      String path = sc.getRealPath("/html/app1/" + filename);
      photo.transferTo(new File(path));
    }
        return 
        "<html><head><title>c04_8/h2</title></head><body>" + 
        "<h1>업로드 결과</h1>" + 
        "<p>이름:" + name + "</p>" +  
        "<p>나이:" + age + "</p>" + 
        (filename != null ? "<p><img src='../../html/app1/" + filename + "'></p>" : "") + 
        "</body></html>";
  }

        // 2. MultipartFile를 사용하는 방법 // photo를 MultipartFile로 선언한다. // SpringAPI의 객체 //

@PostMapping(value="h3", produces="text/html;charset=UTF-8") 
  @ResponseBody 
  public String handler3(
      String name,
      int age,
      MultipartFile[] photo  
      ) throws Exception {
    
    StringWriter out0 = new StringWriter();
    PrintWriter out = new PrintWriter(out0);
    out.println("<html><head><title>c04_8/h3</title></head><body>");
    out.println("<h1>업로드 결과</h1>");
    out.printf("<p>이름:%s</p>\n", name);
    out.printf("<p>나이:%s</p>\n", age);
    
    for (MultipartFile f : photo) {
      if (!f.isEmpty()) {
        String filename = UUID.randomUUID().toString();
        String path = sc.getRealPath("/html/app1/" + filename);
        f.transferTo(new File(path));
        out.printf("<p><img src='../../html/app1/%s'></p>\n", filename);
      }
    }
    out.println("</body></html>");
    
    return out0.toString();
  }

        // MultipartFile[] // MultipartFile을 배열로 받는다 // 사진 여러개 처리

        // 1. Servlet 3.0 API의 파일 업로드를 사용 // Part

        // Servlet 3.0 API의 파일 업로드 기능을 사용하려면 DispatcherServlet에 설정을 추가해야 한다.

        // 즉 멀티파트 데이터를 Part 객체로 받을 때는 customizeRegistration 설정을 추가해야 한다.

        // DispatcherServlet을 WebApplicationInitializer를 통해 배치했을 때 //

        // Servlet 3.0 API로 멀티파트 데이터를 받으면, SpringMVC의 MultipartResolver가 작동하지 않는다.

public class App1WebApplicationInitializer
    extends AbstractAnnotationConfigDispatcherServletInitializer {

  String uploadTmpDir;

  public App1WebApplicationInitializer() {
    uploadTmpDir = new File(System.getProperty("java.io.tmpdir")).getAbsolutePath();
    System.out.println("업로드 임시 폴더: " + uploadTmpDir);
  }

  @Override
  protected Class<?>[] getRootConfigClasses() {
    return null;
  }

  @Override
  protected Class<?>[] getServletConfigClasses() {
    return new Class<?>[] {App1Config.class};
  }

  @Override
  protected String[] getServletMappings() {
    return new String[] {"/app1/*"};
  }

  @Override
  protected String getServletName() {
    return "app1";
  }


  @Override
  protected void customizeRegistration(Dynamic registration) {
  registration.setMultipartConfig(new MultipartConfigElement(
  uploadTmpDir,
  10000000,
  15000000,
  2000000
  ));
  }

}

        // customizeRegistration(Dynamic registration) 메서드를 오버라이딩 후,

        // registraction.setMultipartConfig(new MultipartConfigElement()) 메서드를 호출한다.

        // 첫번째 파라미터 // uploadTmpDir // 업로드 한 파일을 임시 보관할 위치를 말한다. 

        // new File(System.getProperty("java.io.tmpdir")).getAbsolutePath();

        // Java가 사용하는 임시 폴더의 위치이다. // 경로는 마음대로 지정하면 된다.

        // 두번째 파라미터 // 10000000 // 최대 업로드할 수 있는 파일들의 총 크기

        // 세번째 파라미터 // 15000000 // 요청 전체 데이터의 크기

        // 네번째 파라미터 // 2000000 // 업로드 되고 있는 파일을 메모리에 임시 보관하는 크기

        // 2. Spring API의 파일 업로드를 사용 // MultipartFile

        // Spring 방식으로 파일 업로드 기능을 사용하려면 AppConfig에 MultipartResolver를 추가해야 한다.

        // 즉 멀티파트 데이터를 MultipartFile 객체로 받을 때는 AppConfig에 MultipartResolver를 추가해야 한다.

        // Servlet 3.0 API로 멀티파트 데이터를 받으면, SpringMVC의 MultipartResolver가 작동하지 않는다.

@ComponentScan("bitcamp.app1")
public class App1Config {
  
  @Bean
  public MultipartResolver multipartResolver() {
    CommonsMultipartResolver mr = new CommonsMultipartResolver();
    mr.setMaxUploadSize(10000000);
    mr.setMaxInMemorySize(2000000);
    mr.setMaxUploadSizePerFile(5000000);
    return mr; 
  }
}

        // CommonsMultipartResolver() 메서드를 통해 MultipartResolver 구현체를 생성한다. 

        // 구현체를 IoC 컨테이너에 들어가게 @Bean 태그를 붙혀주어야 한다.

        // 그래야 MultipartFile 객체를 처리할 수 있다.

Controller04_9 -> 클라이언트가 보낸 데이터 받기 // @RequestBody

        // 클라이언트가 보낸 데이터를 통째로 받기 // request handler의 아규먼트 앞에 @RequestBody를 붙이면 된다.

  @PostMapping(value="h1", produces="text/html;charset=UTF-8") 
  @ResponseBody 
  public String handler1(
      String name,
      int age,
      @RequestBody String data
      ) throws Exception {

Controller05_1 -> 콘텐트를 직접 리턴하기

        // 리턴 값이 클라이언트에게 보내는 콘텐트라면 메서드 선언부에 @ResponseBody를 붙인다.

        // 붙이지 않으면 프론트 컨트롤러는 view URL로 인식한다.

        // 출력 콘텐트는 브라우저에서 기본으로 HTML로 간주한다.

  @GetMapping("h1") 
  @ResponseBody 
  public String handler1() {
    return "<html><body><h1>abc가각간</h1></body></html>";
  }

        // 한글은 ISO-8859-1 문자표에 정의된 코드가 아니기 때문에 클라이언트로 보낼 때 '?' 문자로 바꿔 보낸다. 

  @GetMapping(value="h2", produces="text/html;charset=UTF-8") 
  @ResponseBody 
  public String handler2() {
    return "<html><body><h1>abc가각간<h1></body></html>";
  }

        // 리턴되는 콘텐트의 MIME 타입과 charset을 지정하고 싶다면 애노테이션의 produces 프로퍼티에 설정한다.

  @GetMapping("h3")
  @ResponseBody
  public String handler3(ServletResponse response) {
    response.setContentType("text/html;charset=UTF-8");
    return "<html><body><h1>abc가각간<h1></body></html>";
  }

         // HTML 코드를 리턴하기 때문에, response.setContentType을 설정해도 한글이 나오지 않는다.

         // 한글이 깨져서 나온다. // 한글은 ? 문자로 변환된다.

  @GetMapping("h4") 
  public HttpEntity<String> handler4(HttpServletResponse response) {
    HttpEntity<String> entity = new HttpEntity<>(
        "<html><body><h1>abc가각간<h1></body></html>");
    return entity;
  }

        // HttpEntity 사용 // 

        // HttpEntity 객체에 콘텐트를 담아 리턴할 수 있다. // 한글이 깨진다.

        // 이 경우에는 리턴 타입으로 콘텐트임을 알 수 있기 때문에 @ResponseBody 애노테이션을 붙이지 않아도 된다.

        // 이 경우에는 출력할 때 ISO-8859-1 문자표의 코드로 변환하여 출력한다. // 그래서 한글은 ? 문자로 변환된다.

  @GetMapping(value = "h5", produces = "text/html;charset=UTF-8")
  public HttpEntity<String> handler5(HttpServletResponse response) {
    HttpEntity<String> entity = new HttpEntity<>("<html><body><h1>abc가각간<h1></body></html>");

    return entity;
  }

        // 한글을 제대로 출력하고 싶으면 produces 프로퍼티에 설정한다.

  @GetMapping("h6")
  public HttpEntity<String> handler6(HttpServletResponse response) {
    HttpHeaders headers = new HttpHeaders();
    headers.add("Content-Type", "text/html;charset=UTF-8");
    HttpEntity<String> entity = new HttpEntity<>("<html><body><h1>abc가각간<h1></body></html>", headers);
    return entity;
  }

        // 한글을 제대로 출력하고 싶으면, 응답 헤더에 직접 Content-Type을 설정할 수 있다.

        // 헤더 정보를 별도로 작성하고, entity에 같이 담아 넘긴다.

  @GetMapping("h7")
  public ResponseEntity<String> handler7(HttpServletResponse response) {

    HttpHeaders headers = new HttpHeaders();
    headers.add("Content-Type", "text/html;charset=UTF-8");
    headers.add("BIT-OK", "ohora");

    ResponseEntity<String> entity = new ResponseEntity<>("<html><body><h1>abc가각간<h1></body></html>", headers,
        HttpStatus.OK
    );

    return entity;
  }
}

        // HttpEntity 대신에 ResponseEntity 객체를 리턴 할 수 있다. // 응답 상태 코드를 추가하기 편하다.

        // 또한 보다 구체적인 리턴 타입을 지정할 수 있다.

        // 응답 헤더를 따로 설정하는 방법이 존재하는 이유는 임의의 응답 헤더를 추가하는 경우가 있기 때문이다.

        // ResponseEntity 객체의 경우 // Httpstatus.OK과 같은 응답 status 코드를 지정할 수 있다.

Controller05_2 -> view URL 리턴하기, 리다이렉트, forward/include

  @GetMapping("h1") 
  public String handler1() {
    return "/jsp/c05_2.jsp";
  }

        // 메서드 선언부에 @ResponseBody를 붙이지 않으면 프론트 컨트롤러는 view URL로 간주한다.

        // 리턴 URL의 '/'는 웹 애플리케이션 루트를 의미한다.

  @GetMapping("h2") 
  public String handler2() {
    return "/WEB-INF/jsp/c05_2.jsp";
  }

        // 위 방식의 문제 // h1을 말하는 것.

        // jsp에 다이렉트로 접속이 가능하다. // jsp의 경로를 알고 있을 경우

        // MVC 아키텍처에서는 항상 Controller에 의해 View가 통제되어야 한다.

        // Controller를 경유하지 않고 View를 실행하게 해서는 안된다. // View에 대해 일관성 있는 제어가 가능하다.

        // 페이지 컨트롤러에서 만든 데이터를 가지고 jsp를 실행해야 한다.

        // ex) 페이지 컨트롤러에서 BoardService 등 데이터를 준비해서 jsp를 실행하기 때문이다.

        // 해결 방법 // 

        //  WEB-INF 폴더 밑에 JSP 파일을 위치시킨다. // 

        //  /WEB-INF 폴더에 있는 파일은 클라이언트에서 직접 실행을 요청할 수 없다.

  @GetMapping("h3") 
  public View handler3() {
    return new JstlView("/WEB-INF/jsp/c05_2.jsp");
  }
  

        // View 객체를 리턴하는 방법이 있다. // View는 interface이다.

        // Jstl은 View 인터페이스의 구현체이다. // 실무에서 이렇게 쓰는 사람도 있다. // 이렇게 쓰지는 말자

  @GetMapping("h4") 
  public ModelAndView handler4() {
    ModelAndView mv = new ModelAndView();
    mv.setViewName("/WEB-INF/jsp/c05_2.jsp");
    return mv;
  }

        // ModelAndView에 담아서 리턴하는 경우가 있다.

        // View는 데이터를 못 담지만, ModelAndView는 데이터를 담아 리턴할 수 있다.

  @GetMapping("h5") 
  public String handler5() {
    return "redirect:h4";
  }

        // redirect:주소 // Jsp의 상대 주소를 말한다. 

        // 위 예제를 실행하고 경로에 들어가면 h4로 redirect 된다.

  @GetMapping("h6") 
  public String handler6() {
    return "forward:h4";
  }

        // forward:주소 // Jsp의 상대 주소를 말한다. 

        // 위 예제를 실행하고 경로에 들어가면 h4가 forward된다. // 주소는 h6이다. // forward이기 때문

Controller05_3 -> 요청 핸들러에서 view 컴포넌트(JSP) 쪽에 데이터 전달하기

  @GetMapping("h1") 
  public String handler1(
      ServletRequest request) {
    
    request.setAttribute("name", "홍길동");
    request.setAttribute("age", 20);
    request.setAttribute("working", true);
    
    return "/WEB-INF/jsp/c05_3.jsp";
  }

        // JSP가 사용할 수 있게 ServletRequest에 담는다.

  @GetMapping("h2") 
  public String handler2(Map<String,Object> map) {
    
    map.put("name", "홍길동");
    map.put("age", 20);
    map.put("working", true);
    
    return "/WEB-INF/jsp/c05_3.jsp";
  }

        // Map 객체에 담아 넘겨주는 이유는 Servlet 종속성을 최소화 하기 위해서이다.

  public String handler3(Model model) {
    
    model.addAttribute("name", "홍길동");
    model.addAttribute("age", 20); // auto-boxing
    model.addAttribute("working", true); // auto-boxing
    
    return "/WEB-INF/jsp/c05_3.jsp";
  }

        // Model 객체를 사용할 수도 있다.

  @GetMapping("h4") 
  public ModelAndView handler4() {
    
    ModelAndView mv = new ModelAndView();
    
    mv.addObject("name", "홍길동");
    mv.addObject("age", 20); // auto-boxing
    mv.addObject("working", true); // auto-boxing
    
    mv.setViewName("/WEB-INF/jsp/c05_3.jsp");
    
    return mv;
  }

        // handler4의 경우에는 아예 ModelAndView를 만들어 리턴하는 메서드이다. // 이런 케이스도 있다.

        // request handler에서 ModelAndView 객체를 만들어 리턴한다.

        // 이 객체의 용도는 Model과 view URL을 함께 리턴하는 것이다.

 

spring-webmvc

spring-webmvc #ex02

Controller01_1 -> 기본 View Resolver 사용하기

  @GetMapping("h1") 
  public String handler1(Model model) {
    
    model.addAttribute("name", "홍길동");
    model.addAttribute("age", 20);
    
    return "/jsp/c01_1.jsp";

        // @ResponseBody // 뷰 이름을 리턴 할 때는 이 애노테이션을 붙이면 안된다.

        // 기본 ViewResolver는 리턴 값으로 URL을 받아 웹 애플리케이션 디렉토리에서 JSP를 찾는다.

        // 웹 애프리케이션이 경로가 /bitcamp-spring-webmvc 라면, JSP 경로는 다음과 같다.

        // ex) /bitcamp-spring-webmvc/jsp/c01_1.jsp

        // DispatcherServlet의 기본 ViewResolver 교체 방법 //

  <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
      <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
      <property name="prefix" value="/WEB-INF/jsp/"/>
      <property name="suffix" value=".jsp"/>
  </bean>

        // 1. XML 설정

  @Bean
  public ViewResolver viewResolver() {
    InternalResourceViewResolver vr = new InternalResourceViewResolver(
        "/WEB-INF/jsp2/", ".jsp");
    return vr;
  }

         // 2. Java Config 설정 // InternalResourceViewResolver

         // "/WEB-INF/jsp/" // prefix라고 한다.

         // ".jsp" // suffix 라고 한다.

         // vr // prefix + 페이지 컨트롤러 리턴 값 + suffix // ex) "/WEB-INF/jsp/" + "board/list" + ".jsp"

         // /WEB-INF/jsp2/board/list.jsp

  @GetMapping("h2") 
  public void handler2(Model model) {
    model.addAttribute("name", "홍길동2");
    model.addAttribute("age", 30);
}

        // 위 예제는 오류가 발생한다. // 기본 ViewResolver를 사용할 때는 뷰 이름을 리턴하지 않으면 오류 발생

        // 만약 ViewResolver가 InternalResourceViewResolver로 교체되었다면, void여도 오류를 발생하지 않는다.

  @GetMapping("h3") 
  public String handler3(Map<String,Object> map) {
    
    map.put("name", "홍길동3");
    map.put("age", 40);
    
    return "/WEB-INF/jsp/c01_1.jsp";

        // Map 객체를 받는 경우

Controller01_2 -> InternalResourceViewResolver로 교체 후 사용하기

        // ViewResolver // 

        // 페이지 컨트롤러가 리턴한 뷰 이름에 해당하는 뷰 콤포넌트를 찾아 실행하는 역할.

        // ResourceBundleViewResolver // 

        // *.properties 에서 뷰 이름에 해당하는 콤포넌트의 URL을 찾는다.

        // InternalResouceViewResolver //

        // 미리 지정된 접두사, 접미사를 사용하여 뷰이름으로 콤포넌트의 URL을 완성한다. 

        // View // 

        // 템플릿 엔진을 실행하여 실제 클라이언트로 보낼 콘텐트를 생성한다.

        // FreeMarker, JSP/JSTL, Tiles, RSS/Atom, PDF, Excel 등의 엔진을 이용하여 콘텐트를 생성하는 뷰가 있다.

        // ViewResolver 교체

        // InternalResourceViewResolver를 사용하여 

        // JSP URL의 접두어와 접미사를 미리 설정해 둘 수 있어 URL을 지정하기 편리하다.

        // 교체 방법은 XML에서 설정하는 방법과 Java Config로 설정하는 방법이 있다. // Controller01_1

        // ViewResolver 실행 과정 //

        // 페이지 컨트롤러는 클라이언트가 요청한 작업을 실행한 후 그 결과를 출력할 뷰의 이름을 리턴한다.

        // 프론트 컨트롤러는 request handler가 리턴한 URL을 view resolver에게 전달한다.

        // view resolver는 자신의 정책에 맞춰서 뷰 URL을 준비한다.

        // InternalResourceViewResolver의 경우 // 

        // request handler가 리턴한 URL 앞, 뒤에 접두사와 접미사를 붙여 JSP를 찾는다.

        // ex) "c01_1/h1" URL을 리턴하면, 최종 JSP URL은, "/WEB-INF/jsp2/c01_2/h1.jsp"이다.

  @GetMapping("h2") 
  public void handler2(Model model) {

    model.addAttribute("name", "홍길동2");
    model.addAttribute("age", 30);

        // InternalResourceViewResolver를 사용하는 경우,

        // request handler가 뷰 이름을 리턴하지 않으면 request handler의 URL을 뷰 이름으로 사용한다.

        // 즉 이 핸들러의 URL은 "/c01_2/h2" 이기 때문에 뷰 이름도 "/c01_2/h2"가 된다.

        // InternalResourceViewResolver는 바로 이 URL을 사용하여 다음과 같이 최종 URL을 만든다.

        // ex) "/WEB-INF/jsp2/" + "/c01_2/h2" + ".jsp" // "/WEB-INF/jsp2/c01_2/h2.jsp"

  @GetMapping("h3") 
  public Map<String,Object> handler3() {
    
    HashMap<String,Object> map = new HashMap<>();
    map.put("name", "홍길동3");
    map.put("age", 40);
    
    return map;
  }

        // Map 객체에 값을 담아 리턴하면 프론트 컨트롤러는 

        // Map 객체에 보관되어 있는 값들을 ServletRequest 보관소로 옮긴다.

        // 그리고 view URL은 request handler의 URL을 사용한다.

  @GetMapping("h4") 
  public ModelAndView handler4() {
    
    ModelAndView mv = new ModelAndView();
    mv.addObject("name", "홍길동3");
    mv.addObject("age", 40);
    mv.setViewName("c01_2/h4");
    
    return mv;
  }

        // ModelAndView 객체에 값과 뷰 이름을 담아 리턴하면 

        // 프론트 컨트롤러는 ModelAndView 객체에 보관되어 있는 값들을 

        // ServletRequest 보관소로 옮기고, 설정된 뷰 이름을 ViewResolver에게 넘긴다.

  @GetMapping("h5") 
  public ModelAndView handler5() {
    
    ModelAndView mv = new ModelAndView();
    mv.addObject("name", "홍길동3");
    mv.addObject("age", 40);
    
    return mv;
  }

        // ModelAndView 객체에 값과 뷰 이름을 담아 리턴하면 

        // 프론트 컨트롤러는 ModelAndView 객체에 보관되어 있는 값들을

        // ServletRequest 보관소로 옮기고, 뷰 이름을 지정하지 않으면 

        // request handler의 path를 ViewResolver에게 넘긴다. 

Controller02_1 -> URL 에서 값 추출하기 // @PathVariable

  @GetMapping
  @ResponseBody
  public String handler1(String name, int age) {
    return String.format("name=%s, age=%d", name, age);
  }

        // http://localhost:8080/java-spring-webmvc/app2/c02_1?name=kim&age=20

        // 클라이언트에서 값을 받는 일반적인 방법 // Query String 으로 받는다.

        // URL 다음에 "?변수=값&변수=값" 형태로 값을 받는걸 말한다.

        // ?변수=값&변수=값 // Query String

        // Query String의 값을 request handler에서 받으려면 아규먼트를 선언하면 된다.

        // 아규먼트 앞에 @RequestParam을 붙여도 되고

        // 아규먼트이 이름이 요청 파라미터의 이름과 같다면 @RequestParam을 생략해도 된다.

  @GetMapping("{name}/{age}")
  @ResponseBody
  public String handler2(
      @PathVariable String name,
      @PathVariable int age
      ) {
    return String.format("name=%s, age=%d", name, age);
  }

        // http://localhost:8080/java-spring-webmvc/app2/c02_1/kim/20

        // URL path에 값을 포함하여 전달할 수 있고, 그 값을 아규먼트로 받을 수 있다.

        // URL path에 포함된 값을 받으려면 request handler의 URL을 설정할 때 다음의 문법으로 선언해야 한다.

        // .../{변수명}/{변수명} // ex) c02_1/aaa/20

        // 이렇게 선언된 변수 값을 받으려면 다음과 같이 아규먼트를 선언해야 한다.

        // @PathVariable(변수명) String 아규먼트

        // 변수명과 아규먼트의 이름이 같다면, 다음과 같이 변수명을 생략할 수 있다.

        // @PathVariable String 아규먼트

  @GetMapping("{name}_{age}")
  @ResponseBody
  public String handler3(
      @PathVariable String name,
      @PathVariable int age
      ) {
    return String.format("name=%s, age=%d", name, age);
  }

        // http://localhost:8080/java-spring-webmvc/app2/c02_1/kim_20

Controller02_2 -> URL 에서 값 추출하기 // @MatrixVariable

  @GetMapping(value="{value}", produces="text/plain;charset=UTF-8")
  @ResponseBody
  public String handler2(
      @PathVariable("value") String value,
      @MatrixVariable String name,
      @MatrixVariable int age
      ) {
    return String.format("value:%s \n name:%s, age:%d", value, name, age);
  }

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

        // 

Controller04_1 -> 인터셉터 만들기

        // 인터셉터 // 프론트 컨트롤러와 페이지 컨트롤러 사이에 코드를 삽입하는 기술

        // 프론트 컨트롤러와 뷰 컴포넌트 사이에 코드를 삽입하는 기술

        // 인터셉터를 배치하기 // 프론트 컨트롤러의 IoC 설정 파일에 배치 정보를 추가한다.

public class Controller04_1_Interceptor1 implements HandlerInterceptor {
  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
      throws Exception {
    return true; 
  }
  
  @Override
  public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
      ModelAndView modelAndView) throws Exception {
  }
  
  @Override
  public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
      Object handler, Exception ex) throws Exception {
  }

        // HandlerInterceptor // 인터페이스

        // preHandle() // 페이지 컨트롤러의 핸들러를 호출하기 전에 이 메서드가 먼저 호출된다.

        // 다음 인터셉터나 페이지 컨트롤러를 계속 실행하고 싶다면 true를 리턴한다.

        // 여기서 요청 처리를 완료하고 싶다면 false를 리턴한다.

        // postHandle() // 페이지 컨트롤러의 핸들러가 리턴한 후 이 메서드가 호출된다.

        // afterCompletion() // JSP를 실행한 후 이 메서드가 호출된다.

Controller05_1 -> JSON 콘텐트 출력하기

@Controller 
@RequestMapping("/c05_1")
public class Controller05_1 {

  ArrayList<Board> list = new ArrayList<>();
  
  public Controller05_1() {
    list.add(new Board(1, "제목입니다1", "내용", "홍길동", 10, Date.valueOf("2019-5-1")));
    list.add(new Board(2, "제목입니다2", "내용", "홍길동2", 11, Date.valueOf("2019-5-2")));
    list.add(new Board(3, "제목입니다3", "내용", "홍길동3", 12, Date.valueOf("2019-5-3")));
    list.add(new Board(4, "제목입니다4", "내용", "홍길동4", 13, Date.valueOf("2019-5-4")));
    list.add(new Board(5, "제목입니다5", "내용", "홍길동5", 14, Date.valueOf("2019-5-5")));
    list.add(new Board(6, "제목입니다6", "내용", "홍길동6", 15, Date.valueOf("2019-6-1")));
    list.add(new Board(7, "제목입니다7", "내용", "홍길동7", 16, Date.valueOf("2019-6-1")));
    list.add(new Board(8, "제목입니다8", "내용", "홍길동8", 17, Date.valueOf("2019-6-1")));
    list.add(new Board(9, "제목입니다9", "내용", "홍길동9", 18, Date.valueOf("2019-6-1")));
    list.add(new Board(10, "제목입니다10", "내용", "홍길동10", 19, Date.valueOf("2019-7-1")));
    list.add(new Board(11, "제목입니다11", "내용", "홍길동11", 11, Date.valueOf("2019-8-1")));
    list.add(new Board(12, "제목입니다12", "내용", "홍길동12", 12, Date.valueOf("2019-9-1")));
    list.add(new Board(13, "제목입니다13", "내용", "홍길동13", 13, Date.valueOf("2019-10-1")));
  }
  
  @GetMapping("h1")
  public void handler1(Model model) {
    model.addAttribute("list", this.list);
  }
  
  @GetMapping(value="h2", produces="text/plain;charset=UTF-8")
  @ResponseBody
  public String handler2() {
    return new Gson().toJson(this.list);
  }
  
  
  @GetMapping("h3")
  @ResponseBody
  public Object handler3() {
    return this.list; // JSON 형식의 문자열은 자동으로 UTF-8로 인코딩 된다.
  }
}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>c05_1</title>
</head>
<body>

<h1>JSON 데이터 받기</h1>
<button id="btn1" type="button">JSP에서 Gson을 이용하여 JSON 생성</button><br>
<button id="btn2" type="button">페이지 컨트롤러에서 Gson을 이용하여 JSON 생성</button><br>
<button id="btn3" type="button">프론트 컨트롤러가 Jackson 또는 Gson을 이용하여 JSON 생성</button><br>

<script>
document.querySelector("#btn1").onclick = () => {
  var xhr = new XMLHttpRequest();
  xhr.onreadystatechange = () => {
    if (xhr.readyState != 4 || xhr.status != 200)
      return;
    //console.log(xhr.responseText);
    displayData(xhr.responseText);
  };
  xhr.open("GET", "../../app2/c05_1/h1", true);
  xhr.send();
};

document.querySelector("#btn2").onclick = () => {
  var xhr = new XMLHttpRequest();
  xhr.onreadystatechange = () => {
    if (xhr.readyState != 4 || xhr.status != 200)
      return;
    //console.log(xhr.responseText);
    displayData(xhr.responseText);
  };
  xhr.open("GET", "../../app2/c05_1/h2", true);
  xhr.send();
};

document.querySelector("#btn3").onclick = () => {
  var xhr = new XMLHttpRequest();
  xhr.onreadystatechange = () => {
    if (xhr.readyState != 4 || xhr.status != 200)
      return;
    //console.log(xhr.responseText);
    displayData(xhr.responseText);
  };
  xhr.open("GET", "../../app2/c05_1/h3", true);
  xhr.send();
};

function displayData(jsonStr) {
  var arr = JSON.parse(jsonStr);
  for (var i = 0; i < arr.length; i++) {
    console.log(
            arr[i].no, 
            arr[i].title, 
            arr[i].writer, 
            arr[i].viewCount, 
            arr[i].createdDate);
  }
}
</script>

 

        // btn1은 메서드 h1을 호출하여 방법 1.

        // JSP에서 JSON 형식으로 데이터를 만들어 넘겨주는 방식

        // btn2은 메서드 h2을 호출하여 방법 2.

        // 페이지 컨트롤러에서 Gson을 이용하여 JSON 데이터를 만들어 넘겨주는 방식

        // btn3은 메서드 h3을 호출하여 방법 3.

        // 프론트 컨트롤러가 Jackson 또는 Gson을 이용하여 JSON 데이터를 만들어 넘겨주는 방식

        // h1 //

        // 방법 1. JSP에서 JSON 형식의 콘텐트 출력하기 

<%@ page language="java" 
         contentType="text/plain; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
[
<c:forEach items="${list}" var="b" varStatus="status">
  {
    "no":${b.no}, 
    "title":"${b.title}", 
    "writer":"${b.writer}", 
    "viewCount":${b.viewCount},
    "createdDate":"${b.createdDate}"
  }
  <c:if test="${!status.last}">,</c:if>
</c:forEach>
]

        // 단계 1. jsp에서 java데이터를 불러와서 JSON 형식으로 만드는 방법 

        // java에서 model에 list를 담고, jsp에서 넘겨준 list에서 forEach를 사용하여 데이터를 출력하는 방법이다.

        // 원래는 ${status.index}를 호출하면 나와야 하지만, 호출이 안될 때가 있다. // 왜 그런지 인지는 모르겠다.

        // 그러면 그냥 메소드를 호출하여 직접 얻자. // ${status.getIndex()}

        // 단계 2. AJAX 요청 // HTML에서 사용할 데이터를 JSP에게 요청한다. // 단계1에서 만든 데이터 불러오기

        // HTML에서 해당 jsp(h1)에게 JSON 데이터를 요청할 수 있다. // console창에 출력된다. // console.log이기 때문

        // h2 // 

        // @ResponseBody // 

        // ResponseBody를 애노테이션을 선언한 메서드는 JSON 데이터를 주고 받을 수 있다.

        // 방법 2. 페이지 컨트롤러가 Google Gson 라이브러리를 사용하여 JSON 형식의 콘텐트 출력하기 // btn2의 결과

        // mvnmvnrepository.com 또는 search.maven.org 접속 - 'gson' 검색

        // org.springframework:spring-context // 버전 클릭 // Gradle Groovy DSL 복사

        // - 라이브러리 정보를 dependencies {} 블록에 추가 - 'gradle cleanEclipse' - 'gradle eclipse'

        // 방법 2은 페이지컨트롤러에서 Gson 객체를 사용하여 Json을 생성한다.

        // 이 방법의 경우, String을 날짜로 변경하면서 날짜가 정상적으로 출력이 되지 않는다.

        // 0000-00-00 포멧이 아니다. // 0월 0, 0000 형식으로 출력된다.

        // h3 //

        // 방법 3. 프론트 컨트롤러가 Google Gson 라이브러리 또는

        // Jackson 라이브러리를 사용하여 JSON 형식의 콘텐트 출력하기 // btn3의 결과

        // 방법 3은 프론트 컨트롤러가 Gson 객체를 사용하여 Json을 생성했을 때,

        // Gson은 0000000000000000과 같이 시간을 표시하게 된다. // 밀리세컨드

        // Jackson은 해당 Java파일에 포멧을 설정하기 때문에 0000-00-00 형식으로 출력된다.

        // @JsonFormat(shape=JsonFormat.Shape.STRING,pattern="yyyy-MM-dd") // 애노테이션을 적용하기 때문

        // 페이지 컨트롤러의 리턴 값이 String이 아니면 프론트 컨트롤러는

        // Google의 Gson 라이브러리나 Jackson 라이브러리를 사용하여

        // 자동으로 JSON 형식의 문자열로 만들어 클라이언트로 출력한다.

        // Gson 또는 Jackson 라이브러리가 있어야 한다.

        // 둘 다 있다면 Jackson 라이브러리가 기본으로 사용된다.

        // 따라서 방법의 경우, build.gradle에서 Jackson 라이브러리를 주석 처리 후 사용하여야 한다.

        // 이유는, Gson을 사용하고 싶어도, Jackson이 먼저 호출이 되기 때문이다.

        // Gson을 사용하기 위해서 Jackson을 주석처리 하는 것이다. // Jackson 라이브러리 적용 안되게.

  //@JsonFormat(shape=JsonFormat.Shape.STRING,pattern="yyyy-MM-dd")
  protected Date createdDate;
  
  public Board() {
  }

        // Java파일에서 JsonFormat 애노테이션을 주석처리 한다. // Gson을 사용하기 위함

        // Jackson을 사용하려면, 주석을 풀어 사용하면 된다. // Jackson을 가장 많이 사용한다. 이렇게 사용하자.

  @JsonFormat(shape=JsonFormat.Shape.STRING,pattern="yyyy-MM-dd")
  protected Date createdDate;
  
  public Board() {
  }
  @GetMapping("h3")
  @ResponseBody
  public Object handler3() {
    return this.list; // JSON 형식의 문자열은 자동으로 UTF-8로 인코딩 된다.
  }

         // 또한 Jackson은 Java데이터를 그대로 넘기기 때문에 String이 아닌 Object로 리턴타입을 정할 수 있다.

         // Gson은 날짜 형식을 지정할 수 없으나, // Jackson은 날짜 형식을 지정하여 데이터를 전달할 수 있다.

Controller05_2 -> JSON 콘텐트 출력하기 // @RestController

@RestController
@RequestMapping("/c05_2")
public class Controller05_2 {

  ArrayList<Board> list = new ArrayList<>();
  
  public Controller05_2() {
    list.add(new Board(1, "제목입니다1", "내용", "홍길동", 10, Date.valueOf("2019-5-1")));
    list.add(new Board(2, "제목입니다2", "내용", "홍길동2", 11, Date.valueOf("2019-5-2")));
    list.add(new Board(3, "제목입니다3", "내용", "홍길동3", 12, Date.valueOf("2019-5-3")));
    list.add(new Board(4, "제목입니다4", "내용", "홍길동4", 13, Date.valueOf("2019-5-4")));
    list.add(new Board(5, "제목입니다5", "내용", "홍길동5", 14, Date.valueOf("2019-5-5")));
    list.add(new Board(6, "제목입니다6", "내용", "홍길동6", 15, Date.valueOf("2019-6-1")));
    list.add(new Board(7, "제목입니다7", "내용", "홍길동7", 16, Date.valueOf("2019-6-1")));
    list.add(new Board(8, "제목입니다8", "내용", "홍길동8", 17, Date.valueOf("2019-6-1")));
    list.add(new Board(9, "제목입니다9", "내용", "홍길동9", 18, Date.valueOf("2019-6-1")));
    list.add(new Board(10, "제목입니다10", "내용", "홍길동10", 19, Date.valueOf("2019-7-1")));
    list.add(new Board(11, "제목입니다11", "내용", "홍길동11", 11, Date.valueOf("2019-8-1")));
    list.add(new Board(12, "제목입니다12", "내용", "홍길동12", 12, Date.valueOf("2019-9-1")));
    list.add(new Board(13, "제목입니다13", "내용", "홍길동13", 13, Date.valueOf("2019-10-1")));
  }
  
  // 테스트:
  //   http://.../app2/c05_2/h1
  @GetMapping("h1")
  public Object handler1() {
    return this.list; // JSON 형식의 문자열은 자동으로 UTF-8로 인코딩 된다.
  }

  // 테스트:
  //   http://.../app2/c05_2/h2
  @GetMapping(value="h2", produces="text/plain;charset=UTF-8")
  public String handler2() {
    return "안녕하세요!"; // String 타입은 그대로 출력한다. 
                       // 단 출력 문자열의 인코딩을 지정해야 한글이 깨지지 않는다.
  }
  
  // 테스트:
  //   http://.../app2/c05_2/h3
  @GetMapping("h3")
  public int handler3() {
    return 100; // primitive 타입의 값도 그대로 출력한다.
  }
  
  // 테스트:
  //   http://.../app2/c05_2/h4
  @GetMapping("h4")
  public Object handler4() {
    HashMap<String,Object> content = new HashMap<>();
    content.put("v1", 100);
    content.put("v2", "Hello");
    
    return content;
  }
}

        // @RestController // 

        // RestController 애노테이션을 선언하면 해당 Controller안의 메서드는 JSON 형식을 주거나, 받을 수 있다.

        // 페이지 컨트롤러를 @RestController로 선언하면, 리턴 값은 HttpMessageConverter에 의해 자동으로 변환된다.

        // @ResponseBody를 붙일 필요가 없다. //

        // h2 // 

        // @GetMapping(value="h2", produces="text/plain;charset=UTF-8")

        // 출력 문자열의 인코딩을 지정해줘야 한다.

        // h4 //

        // primitive 타입이나 String 타입의 값을 JSON 형식으로 출력하려면 도메인 객체나 맵 객체 담아 리턴한다.

Controller05_3 -> JSON 콘텐트 입력받기 // @RestController

 

@RestController
@RequestMapping("/c05_3")
public class Controller05_3 {

  @RequestMapping(value="h1", produces="text/plain;charset=UTF-8")
  public Object handler1(
      int no,
      String title,
      String writer,
      int viewCount) {
    
    return String.format("%d,%s,%s,%d", no, title, writer, viewCount);
  }
  
  @RequestMapping(value="h2", produces="text/plain;charset=UTF-8")
  public Object handler2(Board board) {
    return board.toString();
  }

  @RequestMapping(value="h3", produces="text/plain;charset=UTF-8")
  public Object handler3(@RequestBody String content) throws Exception {
    System.out.println(content);
    System.out.println(URLDecoder.decode(content, "UTF-8"));
    return "OK!";
  }
  
  @RequestMapping(value="h4", produces="text/plain;charset=UTF-8")
  public Object handler4(@RequestBody Map<String,Object> content) throws Exception {
    System.out.println(content);
    return "OK!";
  }
  
  @RequestMapping(value="h5", produces="text/plain;charset=UTF-8")
  public Object handler5(@RequestBody Board content) throws Exception {
    System.out.println(content);
    
    return "OK!";
  }
}

        // h1 // 요청 파라미터 값을 낱개로 입력 받기

        // ex) http://.../app2/c05_3/h1?no=1&title=ok&writer=kim&viewCount=100

        // 입력받은 데이터 값을 파라미터에서 일일히 선언하여 받아 사용한다.

        // h2 // 요청 파라미터 값을 객체로 입력 받기

        // ex) http://.../app2/c05_3/h2?no=1&title=ok&writer=kim&viewCount=100

        // Board 객체로 입력받아 사용한다.

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>c05_3</title>
</head>
<body>

<h1>JSON 데이터 전송</h1>
<button id="btn1" type="button">JSON 형식의 데이터 보내기 1 : String</button><br>
<button id="btn2" type="button">JSON 형식의 데이터 보내기 2 : Map</button><br>
<button id="btn3" type="button">JSON 형식의 데이터 보내기 3 : Board 도메인 객체</button><br>

<script>

var board = {
  no: 1,
  title: "제목입니다!",
  content: "내용입니다",
  writer: "홍길동",
  viewCount: 100,
  createdDate: "2019-05-01"
};


document.querySelector("#btn1").onclick = () => {
  console.log(encodeURI(JSON.stringify(board)));
  var xhr = new XMLHttpRequest();
  xhr.onreadystatechange = () => {
    if (xhr.readyState != 4 || xhr.status != 200)
      return;
    console.log("OK!")
  };
  xhr.open("POST", "../../app2/c05_3/h3", true);
  xhr.setRequestHeader("Content-Type", "application/json");
  xhr.send(encodeURI(JSON.stringify(board)));
};

document.querySelector("#btn2").onclick = () => {
  console.log(JSON.stringify(board));
  var xhr = new XMLHttpRequest();
  xhr.onreadystatechange = () => {
    if (xhr.readyState != 4 || xhr.status != 200)
      return;
    console.log("OK!")
  };
  xhr.open("POST", "../../app2/c05_3/h4", true);
  xhr.setRequestHeader("Content-Type", "application/json");
  xhr.send(JSON.stringify(board));
};

document.querySelector("#btn3").onclick = () => {
  console.log(JSON.stringify(board));
  var xhr = new XMLHttpRequest();
  xhr.onreadystatechange = () => {
    if (xhr.readyState != 4 || xhr.status != 200)
      return;
    console.log("OK!")
  };
  xhr.open("POST", "../../app2/c05_3/h5", true);
  xhr.setRequestHeader("Content-Type", "application/json");
  xhr.send(JSON.stringify(board));
};


</script>

</body>
</html>


        // h3 // JSON 형식의 요청 파라미터 값을 통째로 문자열로 받기

        // xhr.setRequestHeader("Content-Type", "application/json");

        // xhr.send(encodeURI(JSON.stringify(board))); // JS에서 JSON 형식으로 만들고 인코딩하여 전달한다.

        // @RequestMapping(value="h3", produces="text/plain;charset=UTF-8")

        // public Object handler3(@RequestBody String content) throws Exception {

        // Java(server)에서는 파라미터에 @RequestBody 애노테이션을 선언한 String으로 받는다. 

        // URLDecoder.decode(content, "UTF-8") // 인코딩한 데이터이기 때문에 디코딩 하여 사용하여야 한다. 

        // h4 // JSON 형식의 요청 파라미터 값을 통째로 맵 객체로 받기

        // xhr.send(JSON.stringify(board)); // 인코딩을 하지 않고 보낸다.

        // 만약 인코딩을 해서 보내면 에러가 발생한다. // parse 처리를 할 수 없기 때문이다. // 파라미터로 받을 수 없다.

        // public Object handler4(@RequestBody Map<String,Object> content) throws Exception {

        // Java(server)에서는 파라미터에 @RequestBody 애노테이션을 선언한 Map 객체로 받는다. 

        // HttpMessageConverter 구현체(예:MappingJackson2HttpMessageConverter)가

        // 클라이언트가 보낸 데이터를 Map 객체에 담아준다

        // Gson 또는 Jackson 라이브러리를 포함하여야 한다. 

        // 그래야 스프링의 DispatcherServlet에서 HttpMessageConverter 구현체를 찾는다.

        // h5 // JSON 형식의 요청 파라미터 값을 도메인 객체로 받기

        // xhr.send(JSON.stringify(board)); // 인코딩을 하지 않고 보낸다.

        // 만약 인코딩을 해서 보내면 에러가 발생한다. // parse 처리를 할 수 없기 때문이다. // 파라미터로 받을 수 없다.

        // public Object handler5(@RequestBody Board content) throws Exception {

        // JS에서 넘어온 요소의 이름이 프로퍼티와 일치해야 한다.

        // ex) JS에서 no면, Java 도메인 안에 setNo가 있어야 한다.

        // nos라던지, index 등등 이런식으로 다르면 프로퍼티를 못찾아서 값을 정상적으로 넣을 수 없다.

        // 클라이언트에서 보낸 날짜 데이터의 문자열 형식이 yyyy-MM-dd 형태여야 한다.

        // 그래야 java.util.Date 타입의 값으로 변환해 준다.

        // 만약 이 형태가 아니면 변환할 수 없어 실행 오류가 발생한다.

        // ex) 2019-05-01 // java.util.Date 변환 성공 // 05/01/2019 // 변환 오류!

        // ex) 2019-5-1 // 최신 버전에서는 자동 변환 되는 것 같다. 원래는 변환 오류이다.

  @JsonFormat(shape=JsonFormat.Shape.STRING,pattern="yyyy-MM-dd")
  public void setCreatedDate(Date createdDate) {
    this.createdDate = createdDate;
  }

        // @JsonFormat 애노테이션 //

        // 애노테이션은 MappingJackson2HttpMessageConverter를 위한 것이다.

        // GsonHttpMessageConverter는 이 애노테이션을 인식하지 않는다.

        // 도메인 객체의 프로퍼티에 이 애노테이션을 붙이면 

        // 2019-05-01 이나 2019-5-1 모두 처리할 수 있다.

        // 뿐만 아니라, 도메인 객체를 JSON 문자열로 변환할 때도 해당 형식으로 변환된다.

Controller06_1 -> Exception 발생시 페이지 처리 // jsp 파일에 에러 출력하기

        // Request Handler에서 예외를 던졌을 때 처리 절차 //

        // 1. 페이지 컨트롤러 안에 예외 처리기가 있다면, 해당 메서드를 호출한다.

        // 2. @ControllerAdvice 객체에 예외 처리기가 있다면, 해당 메서드를 호출한다.

        // 3. web.xml에 지정된 오류 처리 기본페이지가 설정되어 있다면, 해당 페이지를 실행한다.

        // 4. 서블릿 컨테이너의 기본 오류 페이지를 실행한다.

        // 서블릿 컨테이너에서 요청을 처리하는 중에 오류가 발생했을 때, 오류내용을 출력하는 페이지를 설정하기.

<error-page>
  <location>/WEB-INF/jsp2/error1.jsp</location>
</error-page>

        // web.xml 파일에 error를 처리할 페이지를 등록한다.

<p> 상태코드: ${requestScope."javax.servlet.error.status_code"} </p>
<p> 상태코드: ${requestScope."javax.servlet.error.message"} </p>

        // 방법1. jsp 파일에 에러 출력하기

        // jsp 파일에 error 상태 코드와, 메세지를 출력할 수 있다.

        // javax.servlet.error라는 이름은 컨테이너 모두 공통으로 사용하는 이름이다.

        // jsp에서 일반적인 변수 이름이 아니면, ""를 붙여야 하기 때문에 ""를 붙히고,

        // jsp 파일이기 때문에 ${}문법을 사용했다.

        // 이 방식은 에러가 발생하면, 바로 error를 담당하는 jsp로 간다.

ErrorController -> Exception 발생시 페이지 처리 // spring DispatchServlet으로 에러 출력하기

@Controller
@RequestMapping
public class ErrorController {

  @RequestMapping
  public ModelAndView erroe(HttpServletRequest request) {
    ModelAndView mv = new ModelAndView();
    mv.addObject("status", request.getAttribute("javax.servlet.error.status_code"));
    mv.addObject("reason", request.getAttribute("javax.servlet.error.message"));
    mv.setViewName("error2");
    return mv;
  }
}

        // 방법2. Spring에서 처리하기 위해 Error를 담당할 Controller 파일을 생성한다.

        // 이 방법은 Spring DispatchServlet에서 해당 Controller를 호출,

        // Controller에서 해당하는 ModelAndView 객체를 만들어서 jsp로 전달하는 방식이다.

        // 즉 pageController를 경유하는 방식이다.

<error-page>
  <location>/app2/error</location>
</error-page>

        // web.xml 파일에 error를 처리할 페이지를 등록한다.

<p> 상태코드: ${status} </p>
<p> 상태코드: ${reason} </p>

        // error2.jsp 파일을 이처럼 간단하게 적을 수 있다.

        // error를 출력하는 페이지를 보다 코드를 간략하게 작성할 수 있다는 점이 장점이다.

GlobalControllerAdvice -> Exception 발생시 페이지 처리 // @ControllerAdvice으로 에러 출력하기

@ControllerAdvice
public class GlobalControllerAdvice {

  @ExceptionHandler
  public ModelAndView exceptionHandler(Exception ex) {
    ModelAndView mv = new ModelAndView();
    mv.addObject("error", ex);
    mv.setViewName("error3");
    return mv;
  }
  
  @ExceptionHandler
  public ModelAndView ioExceptionHandler(IOException ex) {
    ModelAndView mv = new ModelAndView();
    mv.addObject("error", ex);
    mv.setViewName("error4");
    return mv;
  }
  
  @ExceptionHandler
  public ModelAndView sqlExceptionHandler(SQLException ex) {
    ModelAndView mv = new ModelAndView();
    mv.addObject("error", ex);
    mv.setViewName("error5");
    return mv;
  }
}

        // Request Handler가 던지는 예외에 따라 그 예외를 받을 수 있는 메서드를 호출한다.

        // 단, 그 메서드에는 @ExceptionHandler가 선언되어야 한다.

<p> 예외객체: ${error} </p>
<p> 오류메세지: ${error.getMessage} </p>

         // error라는 이름으로 저장된 ModelAndView를 꺼낼 코드를 jsp에 작성한다.

         // error3. error4, error5.jsp를 각각 만든다. //

         // 만약 아예 경로가 없는 error가(경로가 잘못된 error) 주어진다면, error2가 실행된다.

         // 페이지 컨트롤러에 등록되어 있기 때문

         // 경로 안에서 error가 발생한다면 발생한 error에 따라, error3, error4, error5가 실행된다.

         // IOException이나, SQLException이 발생하면 해당 error4, error5가 실행된다.

         // error3이 실행되지 않는다. // 세밀하게 오류를 처리할 수 있다.

         // 당연히 IOException이나 SQLException을 처리하는 error4나, erro5가 없었다면, error3이 실행된다.

반응형

+ Recent posts