Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

개발자입니다

[Java] 예제 소스 정리 - Spring WebMVC 본문

네이버클라우드 AIaaS 개발자 양성과정 1기/Spring Framework, Spring Boot

[Java] 예제 소스 정리 - Spring WebMVC

끈기JK 2023. 3. 3. 09:33

eomcs-spring-webmvc

 

 

예제 소스 정리

 

 

spring webmvc

 

 

eomcs-spring-webmvc/src-09

 

 

web.xml 이나 애노테이션이 아닌 다른 방법으로 서블릿을 등록하기
package bitcamp;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration.Dynamic;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

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

// 서블릿 컨테이너가 시작될 때 보고 받는 객체
// 구동 순서:
// 1) 서블릿 컨테이너를 시작한다.
// 2) 서블릿 컨테이너는 /WEB-INF/lib/*.jar 파일을 뒤진다.
// 3) /META-INF/services/javax.servlet.SerlvetContainerInitializer 파일을 찾는다.
// 4) 그 파일에서 ServletContainerInitializer 구현체를 알아낸다.
// 5) 해당 구현체의 인스턴스를 생성한 후 onStartup()을 호출한다.
// 6) 만약 그 구현체가 보고 받고자 하는 타입이 있다면 해당 클래스를 찾아
//    onStartup()을 호출할 때 파라미터로 넘겨준다.
//
// WebApplicationInitializer 호출 과정
// 1) 서블릿 컨테이너(예: 톰캣 서버)를 시작한다.
// 2) spring-web-*.jar 파일에서
//    /META-INF/service/javax.servlet.SerlvetContainerInitializer 파일을 읽는다.
// 3) 이 파일에 등록된 클래스의 인스턴스를 생성한다.
//    => SpringServletContainerInitializer 인스턴스 생성
// 4) SpringServletContainerInitializer 객체에 대해 onStartup()을 호출한다.
//    => 호출할 때 WebApplicationInitializer를 구현한 클래스 목록을 넘겨준다.
// 5) SpringServletContainerInitializer는
//    WebApplicationInitializer 구현체의 인스턴스를 만들고,
//    onStartup()을 호출한다.
//
public class WebApplicationInitializerImpl implements WebApplicationInitializer {

  @Override
  public void onStartup(ServletContext servletContext) throws ServletException {
    System.out.println("WebApplicationInitializerImpl.onStartup()...호출됨!");

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

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

    // => Java Config 클래스가 있는 패키지를 지정하기
    //    단 이럴 경우 Java Config 클래스에는 @Configuration 애노테이션이 선언되어 있어야 한다.
    iocContainer.scan("bitcamp");

    iocContainer.refresh();

    // DispatcherServlet 인스턴스를 생성한다.
    DispatcherServlet servlet = new DispatcherServlet(iocContainer);

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

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

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

}

 

package bitcamp;

import org.springframework.context.annotation.ComponentScan;

@ComponentScan("bitcamp")
public class AppConfig {

}

 

 

eomcs-spring-webmvc/src-10

 

 

WebApplicationInitializer 구현체를 통해 DispatcherServlet을 등록하는 두 번째 방법
package bitcamp;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

// WebApplicationInitializer 구현체를 통해 DispatcherServlet을 등록하는 두 번째 방법
// => 인터페이스를 직접 구현하는 대신에 그 인터페이스를 미리 구현한
//    AbstractAnnotationConfigDispatcherServletInitializer 클래스를 상속 받기
//    - 이 클래스는 미리 AnnotationConfigWebApplicationContext IoC 컨테이너를 준비했다.
//    - 따라서 IoC 컨테이너를 따로 설정할 필요가 없다.
//    - 또한 DispatcherServlet을 등록하는 코드가 이미 작성되어 있기 때문에
//      따로 등록할 필요가 없다.
//    - 즉 인터페이스를 직접 구현하는 것 보다 편하다.
//
public class WebApplicationInitializerImpl
extends AbstractAnnotationConfigDispatcherServletInitializer {

  @Override
  protected Class<?>[] getRootConfigClasses() {
    System.out.println("==> getRootConfigClasses()");
    // ContextLoaderListener의 IoC 컨테이너가 사용할 Java Config 클래스를 리턴한다.
    return null;
  }

  @Override
  protected Class<?>[] getServletConfigClasses() {
    System.out.println("==> getServletConfigClasses()");
    // DispatcherServlet의 IoC 컨테이너가 사용할 Java config 클래스를 리턴한다.
    return new Class<?>[] {AppConfig.class};
  }

  @Override
  protected String[] getServletMappings() {
    System.out.println("==> getServletMappings()");
    // DispatcherServlet을 등록할 때 이 메서드를 호출한다.
    // 이 메서드의 리턴 값이 리턴 값이 URL 패턴으로 사용된다.
    return new String[] {"/app/*"};
  }

  @Override
  protected String getServletName() {
    System.out.println("==> getServletName()");
    // DispatcherServlet을 등록할 때 이 메서드를 호출한다.
    // 이 메서드의 리턴 값이 서블릿의 이름으로 사용된다.
    // 이 메서드를 오버라이딩 하지 않으면 기본 이름("dispatcher")이 사용된다.
    // => 한 개만 등록할 것이라면 오버라이딩 하지 않아도 되지만,
    //    여러 개의 DispatcherServlet을 등록할 것이라면 오버라이딩 하여
    //    반드시 이름을 다르게 해야 한다.
    return "app";
  }

  @Override
  public void onStartup(ServletContext servletContext) throws ServletException {
    // 이 메서드가 호출될 때 간단한 메시지를 출력하기 위해 오버라이딩 하였다.
    // 따라서 원래의 메서드를 반드시 호출해줘야 한다.
    System.out.println("WebApplicationInitializerImpl.onStartup()...호출됨2!");
    super.onStartup(servletContext);
  }
}

 

 

WebApplicationInitializer 구현체를 통해 DispatcherServlet을 등록하는 세 번째 방법
package bitcamp;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.support.AbstractDispatcherServletInitializer;

// WebApplicationInitializer 구현체를 통해 DispatcherServlet을 등록하는 세 번째 방법
// => 인터페이스를 직접 구현하는 대신에 그 인터페이스를 구현한
//    AbstractDispatcherServletInitializer 클래스를 상속 받기
//
//
public class WebApplicationInitializerImpl
extends AbstractDispatcherServletInitializer {

  // AbstractDispatcherServletInitializer 클래스에서 
  // 이미 DispatcherServlet 객체를 생성하여 등록했다.
  // 따라서 이 클래스를 상속 받는 서브 클래스에서 해야 할 일은 
  // 1) ContextLoaderListener 가 사용할 IoC 컨테이너를 설정한다.
  //    => createRootApplicationContext() 메서드 오버라이딩
  // 2) DispatcherServlet이 사용할 IoC 컨테이너를 설정한다.
  //    => createServletApplicationContext() 메서드 오버라이딩
  // 3) DispatcherServlet에 적용할 URL 매핑을 설정한다.
  //    => getServletMappings() 메서드 오버라이딩
  
  // 다음 메서드들은 수퍼 클래스에서 상속 받은 onStartup()에서 호출한다.
  // 즉 onStartup()은 DispatcherServlet 을 준비할 때 다음 메서드의 리턴 값을 참조한다.
  
  @Override
  protected WebApplicationContext createRootApplicationContext() {
    // ContextLoaderListener 가 사용할 IoC 컨테이너를 리턴한다.
    // 만약 null을 리턴한다면,
    // 스프링 WebMVC 프레임워크는 ContextLoaderListener를 생성하지 않을 것이다.
    return null;
  }

  @Override
  protected WebApplicationContext createServletApplicationContext() {
    // DispatcherServlet이 사용할 IoC 컨테이너를 리턴한다.
    // 스프링 WebMVC 프레임워크는 DispatcherServlet을 만들 때
    // 이 메서드가 리턴한 IoC 컨테이너를 DispatcherServlet에 주입할 것이다.

    //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);
    return iocContainer;
  }

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

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

  @Override
  public void onStartup(ServletContext servletContext) throws ServletException {
    // 이 메서드가 호출될 때 간단한 메시지를 출력하기 위해 오버라이딩 하였다.
    // 따라서 원래의 메서드를 반드시 호출해줘야 한다.
    System.out.println("WebApplicationInitializerImpl.onStartup()...호출됨!");
    super.onStartup(servletContext);
  }
}

 

 

eomcs-spring-webmvc/src-12

 

 

package bitcamp.web;

import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.XmlWebApplicationContext;
import org.springframework.web.servlet.support.AbstractDispatcherServletInitializer;

public class AdminDispatcherServletInitializer extends AbstractDispatcherServletInitializer {

  @Override
  protected WebApplicationContext createRootApplicationContext() {
    // ContextLoaderListener 가 사용할 IoC 컨테이너를 리턴한다.
    // AppDispatcherServletInitializer가 이미 리턴했기 때문에
    // 이 객체는 그냥 null을 리턴한다.
    return null;
  }

  @Override
  protected WebApplicationContext createServletApplicationContext() {
    // DispatcherServlet이 사용할 IoC 컨테이너를 리턴한다.
    XmlWebApplicationContext iocContainer = new XmlWebApplicationContext();
    iocContainer.setConfigLocation("/WEB-INF/admin-servlet.xml");
    return iocContainer;
  }

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

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

 

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">
  
  <!-- 프론트 컨트롤러가 사용할 웹 관련 객체가 들어 있는 패키지를 지정한다. -->
  <context:component-scan base-package="bitcamp.web">
    <context:exclude-filter type="regex" expression="bitcamp.web.app.*"/>
  </context:component-scan>
  
</beans>

 

 

package bitcamp.web;

import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.XmlWebApplicationContext;
import org.springframework.web.servlet.support.AbstractDispatcherServletInitializer;

public class AppDispatcherServletInitializer extends AbstractDispatcherServletInitializer {

  @Override
  protected WebApplicationContext createRootApplicationContext() {
    // ContextLoaderListener 가 사용할 IoC 컨테이너를 리턴한다.
    XmlWebApplicationContext iocContainer = new XmlWebApplicationContext();
    iocContainer.setConfigLocation("/WEB-INF/config/app-context.xml");
    return iocContainer;
  }

  @Override
  protected WebApplicationContext createServletApplicationContext() {
    // DispatcherServlet이 사용할 IoC 컨테이너를 리턴한다.
    XmlWebApplicationContext iocContainer = new XmlWebApplicationContext();
    iocContainer.setConfigLocation("/WEB-INF/app-servlet.xml");
    return iocContainer;
  }

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

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

 

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">
  
  <!-- 프론트 컨트롤러가 사용할 웹 관련 객체가 들어 있는 패키지를 지정한다. -->
  <context:component-scan base-package="bitcamp.web">
    <context:exclude-filter type="regex" expression="bitcamp.web.admin.*"/>
  </context:component-scan>
  
</beans>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">
  
  <!-- ContextLoaderListener의 IoC 컨테이너가 관리할 객체가 들어 있는 패키지를 지정한다. 
       => 보통 Service, DAO, 트랜잭션 관련 객체 등을 관리한다.
  -->
  <context:component-scan base-package="bitcamp">
    <context:exclude-filter type="regex" expression="bitcamp.web.*"/>
  </context:component-scan>
  
</beans>

 

package bitcamp.web.admin;

import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import bitcamp.dao.BoardDao;
import bitcamp.vo.Board;

// 페이지 컨트롤러는 보통 DispatcherServlet의 IoC 컨테이너에서 관리한다.
// 주로 다음 애노테이션으로 표시한다.
@Controller
public class BoardController {
  
  @Autowired
  BoardDao boardDao; // 이 객체는 ContextLoaderListener에서 관리하는 객체이다.
  
  @RequestMapping(path = "/board/list", produces = "text/plain;charset=UTF-8")
  @ResponseBody
  public String list() throws Exception {
    List<Board> boards = boardDao.findAll();
    
    StringBuffer sb = new StringBuffer();
    for (Board board : boards) {
      sb.append(board.toString() + "\n");
    }
    return sb.toString();
  }
}

 

package bitcamp.vo;

public class Board {
  private int no;
  private String contents;
  
  public Board() {
  }
  
  public Board(int no, String contents) {
    this.no = no;
    this.contents = contents;
  }
  
  @Override
  public String toString() {
    return "Board [no=" + no + ", contents=" + contents + "]";
  }
  
  public int getNo() {
    return no;
  }
  public void setNo(int no) {
    this.no = no;
  }
  public String getContents() {
    return contents;
  }
  public void setContents(String contents) {
    this.contents = contents;
  }
}

 

package bitcamp.dao;

import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Repository;
import bitcamp.vo.Board;

// 데이터의 입출력을 다루는 객체(퍼시스턴스 객체; persistence)인 경우 다음 애노테이션을 붙인다.
@Repository
public class BoardDao {
  
  ArrayList<Board> boards = new ArrayList<>();
  
  public BoardDao() {
    boards.add(new Board(1, "게시물입니다.111"));
    boards.add(new Board(2, "게시물입니다.222"));
    boards.add(new Board(3, "게시물입니다.333"));
    boards.add(new Board(4, "게시물입니다.444"));
    boards.add(new Board(5, "게시물입니다.555"));
    
  }
  
  public List<Board> findAll() {
    return boards;
  }
  
  public void insert(Board board) {
    boards.add(board);
  }
  
  public Board findBy(int no) {
    for (Board board : boards) {
      if (board.getNo() == no)
        return board;
    }
    return null;
  }
}

 

 

eomcs-spring-webmvc/src-13

 

 

package bitcamp.config;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class AppDispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

  @Override
  protected Class<?>[] getRootConfigClasses() {
    // ContextLoaderListener 의 IoC 컨테이너가 사용할 Java Config 클래스를 리턴한다.
    return new Class<?>[] {RootConfig.class};
  }

  @Override
  protected Class<?>[] getServletConfigClasses() {
    // DispatcherServlet 의 IoC 컨테이너가 사용할 Java Config 클래스를 리턴한다.
    return new Class<?>[] {AppConfig.class};
  }

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

  @Override
  protected String getServletName() {
    return "app";
  }
}
package bitcamp.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;

@ComponentScan(
    value="bitcamp",
    excludeFilters = {
        @Filter(type = FilterType.REGEX, pattern = "bitcamp.web.*")
    })
public class RootConfig {

}
package bitcamp.config;

import org.springframework.context.annotation.ComponentScan;

@ComponentScan("bitcamp.web.app")
public class AppConfig {

}

 

package bitcamp.config;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class AdminDispatcherServletInitializer  extends AbstractAnnotationConfigDispatcherServletInitializer {

  @Override
  protected Class<?>[] getRootConfigClasses() {
    // ContextLoaderListener 의 IoC 컨테이너가 사용할 Java Config 클래스를 리턴한다.
    // AppDispatcherServletInitializer 에서 이미 설정했기 때문에
    // 여기서는 null을 리턴한다.
    return null;
  }

  @Override
  protected Class<?>[] getServletConfigClasses() {
    // DispatcherServlet 의 IoC 컨테이너가 사용할 Java Config 클래스를 리턴한다.
    return new Class<?>[] {AdminConfig.class};
  }
  @Override
  protected String[] getServletMappings() {
    return new String[] {"/admin/*"};
  }

  @Override
  protected String getServletName() {
    return "admin";
  }
}
package bitcamp.config;

import org.springframework.context.annotation.ComponentScan;

@ComponentScan("bitcamp.web.admin")
public class AdminConfig {

}

 

 

eomcs-spring-webmvc/src-14/~/app1

 

 

package bitcamp.config;

import java.io.File;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

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(
      javax.servlet.ServletRegistration.Dynamic registration) {
    // Servlet 3.0 API의 파일 업로드를 사용하려면 다음과 같이
    // DispatcherServlet에 설정을 추가해야 한다.
    // => 즉 멀티파트 데이터를 Part 객체로 받을 때는
    //    이 설정을 추가해야 한다.

    //
    // 만약 Spring의 방식으로 파일 업로드를 처리하고 싶다면,
    // AppConfig.java의 MultipartResolver를 추가해야 한다.
    // => 즉 멀티파트 데이터를 MultipartFile 객체로 받으려면
    //    AppConfig에 별도로 MultipartResolver를 추가해야 한다.
    // => 단 이 설정을 추가하면 Servlet 3.0 API의 멀티파트 처리 기능이 동작되지 않는다.
    //
    // DispatcherServlet 이 multipart/form-data 으로 전송된 데이터를 처리하려면
    // 해당 콤포넌트를 등록해야 한다.
    //    registration.setMultipartConfig(
    //        new MultipartConfigElement(
    //            uploadTmpDir, // 업로드 한 파일을 임시 보관할 위치
    //            10000000, // 최대 업로드할 수 있는 파일들의 총 크기
    //            15000000, // 요청 전체 데이터의 크기
    //            2000000 // 업로드 되고 있는 파일을 메모리에 임시 임시 보관하는 크기
    //            ));
  }
}
package bitcamp.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.commons.CommonsMultipartResolver;

@ComponentScan("bitcamp.app1")
public class App1Config {

  @Bean
  public MultipartResolver multipartResolver() {
    // 스프링 WebMVC에서 파일 업로드를 처리하고 싶다면,
    // 이 메서드를 통해 MultipartResolver 구현체를 등록해야 한다.
    // 그래야 request handler는 MultipartFile 객체를 받을 수 있다.
    //
    CommonsMultipartResolver mr = new CommonsMultipartResolver();
    mr.setMaxUploadSize(10000000);
    mr.setMaxInMemorySize(2000000);
    mr.setMaxUploadSizePerFile(5000000);
    return mr;
  }
}

 

 

페이지 컨트롤러 만드는 방법
package bitcamp.app1;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller // 이 애노테이션을 붙인다.
@RequestMapping("/c01_1") // 컨트롤러에 URL을 매핑한다.
public class Controller01_1 {

  //@RequestMapping // 이 애노테이션을 붙여서 요청이 들어왔을 때 호출될 메서드임을 표시한다.
  @ResponseBody // 메서드의 리턴 값이 클라이언트에게 출력할 내용임을 표시한다.
  public String handler() {
    return "c01_1 -> handler()";
  }

  // URL 한 개 당 한 개의 핸들러만 연결할 수 있다.
  // 같은 URL에 대해 다른 메서드를 또 정의하면 실행 오류가 발생한다.
  @RequestMapping
  @ResponseBody
  public String handler2() {
    return "c01_1 -> handler2()";
  }
}

 

 

페이지 컨트롤러 만드는 방법 - 한 개의 페이지 컨트롤러에 여러 개의 요청 핸들러 두기
package bitcamp.app1;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class Controller01_2 {

  // 요청이 들어 왔을 때 호출되는 메서드를 "request handler" 라 부른다.

  @RequestMapping("/c01_2_h1") // 핸들러에서 URL을 지정한다.
  @ResponseBody
  public String handler() {
    return "c01_2_h1";
  }

  @RequestMapping("/c01_2_h2") // 핸들러에서 URL을 지정한다.
  @ResponseBody
  public String handler2() {
    return "c01_2_h2";
  }

  @RequestMapping("/c01_2/h3") // URL을 지정할 때 디렉토리 형식으로 지정할 수 있다.
  @ResponseBody
  public String handler3() {
    return "/c01_2/h3";
  }

  @RequestMapping("/c01_2/h4") // URL을 지정할 때 디렉토리 형식으로 지정할 수 있다.
  @ResponseBody
  public String handler4() {
    return "/c01_2/h4";
  }

  // 한 개의 request handler에 여러 개의 URL을 매핑할 수 있다.
  @RequestMapping({"/c01_2/h5", "/c01_2/h6", "/c01_2/h7"})
  @ResponseBody
  public String handler5() {
    return "/c01_2/h5,h6,h7";
  }
}

 

 

페이지 컨트롤러 만드는 방법 - 기본 URL과 상세 URL을 분리하여 설정하기
package bitcamp.app1;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/c01_3") // 핸들러에 적용될 기본 URL을 지정한다.
public class Controller01_3 {

  @RequestMapping("h1") // 기본 URL에 뒤에 붙는 상세 URL. 예) /c01_3/h1
  @ResponseBody
  public String handler() {
    return "h1";
  }

  @RequestMapping("/h2") // 앞에 /를 붙여도 되고 생략해도 된다. 예) /c01_3/h2
  @ResponseBody
  public String handler2() {
    return "h2";
  }

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

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

  @RequestMapping({"h5", "h6", "h7"})
  @ResponseBody
  public String handler5() {
    return "h5,h6,h7";
  }
}

 

 

GET, POST 구분하기
package bitcamp.app1;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/c02_1")
public class Controller02_1 {

  // 테스트 방법:
  // => http://localhost:9999/eomcs-spring-webmvc/html/app1/c02_1.html

  @RequestMapping(method = RequestMethod.GET) // GET 요청일 때만 호출된다.
  @ResponseBody
  public String handler1() {
    return "get";
  }

  @RequestMapping(method = RequestMethod.POST) // POST 요청일 때만 호출된다.
  @ResponseBody
  public String handler2() {
    return "post";
  }
}

 

 

GET, POST 구분하기 II
package bitcamp.app1;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/c02_2")
public class Controller02_2 {

  // 테스트 방법:
  // => http://localhost:9999/eomcs-spring-webmvc/html/app1/c02_2.html

  @GetMapping // GET 요청일 때만 호출된다.
  @ResponseBody
  public String handler1() {
    return "get";
  }

  @PostMapping // POST 요청일 때만 호출된다.
  @ResponseBody
  public String handler2() {
    return "post";
  }
}

 

 

request handler를 구분하는 방법 - 파라미터 이름으로 구분하기
package bitcamp.app1;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

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

  // 테스트 방법:
  // => http://localhost:9999/eomcs-spring-webmvc/app1/c03_1?name=kim
  @GetMapping(params = "name")
  @ResponseBody
  public String handler1() {
    return "handler1";
  }

  // 테스트 방법:
  // => http://localhost:9999/eomcs-spring-webmvc/app1/c03_1?age=20
  @GetMapping(params = "age")
  @ResponseBody
  public String handler2() {
    return "handler2";
  }

  // 테스트 방법:
  // => http://localhost:9999/eomcs-spring-webmvc/app1/c03_1?name=kim&age=20
  @GetMapping(params = {"age", "name"})
  @ResponseBody
  public String handler3() {
    return "handler3";
  }

  // 테스트 방법:
  // => http://localhost:9999/eomcs-spring-webmvc/app1/c03_1
  @GetMapping
  @ResponseBody
  public String handler4() {
    return "handler4";
  }
}

 

 

request handler를 구분하는 방법 - 요청 헤더 이름으로 구분하기
package bitcamp.app1;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/c03_2")
public class Controller03_2 {

  // 테스트 방법:
  // => http://localhost:9999/eomcs-spring-webmvc/html/app1/c03_2.html
  // => 요청 헤더 중에서 특정 이름을 갖는 헤더가 있을 때 호출될 메서드를 지정할 수 있다.
  // => 웹 페이지에서 링크를 클릭하거나 입력 폼에 값을 넣고 등록 버튼을 누르는
  //    일반적인 상황에서는 요청헤더에 임의의 헤더를 추가할 수 없다.
  // => 자바스크립트 등의 프로그래밍으로 임의의 HTTP 요청을 할 때
  //    HTTP 프로토콜에 표준이 아닌 헤더를 추가할 수 있다.
  //    그 헤더를 처리하는 메서드를 정의할 때 사용한다.
  // => 보통 Open API를 개발하는 서비스 회사에서 많이 사용한다.

  // @GetMapping(headers="name")
  @RequestMapping(method = RequestMethod.GET, headers = "name")
  @ResponseBody
  public String handler1() {
    return "handler1";
  }

  // @GetMapping(headers="age")
  @RequestMapping(method = RequestMethod.GET, 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";
  }
}

 

 

request handler를 구분하는 방법 - Accept 요청 헤더의 값에 따라 구분하기
package bitcamp.app1;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/c03_3")
public class Controller03_3 {

  // 테스트 방법:
  // => http://localhost:9999/eomcs-spring-webmvc/html/app1/c03_3.html
  // => 요청 헤더 중에서 Accept의 값에 따라 구분할 때 사용한다.
  //
  // Accept 헤더?
  // => HTTP 클라이언트(웹 브라우저)에서 서버에 요청할 때
  //    받고자 하는 콘텐트의 타입을 알려준다.

  @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";
  }

  @GetMapping
  @ResponseBody
  public String handler4() {
    return "handler4";
  }
}

 

 

request handler를 구분하는 방법 - Content-Type 헤더의 값에 따라 구분하기
package bitcamp.app1;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/c03_4")
public class Controller03_4 {

  // Content-Type 요청 헤더
  // => HTTP 클라이언트가 보내는 데이터의 콘텐트 타입이다.
  // => 프론트 컨트롤러는 보내는 데이터의 타입에 따라 처리를 구분할 수 있다.

  // 테스트 방법:
  // => http://localhost:9999/eomcs-spring-webmvc/html/app1/c03_4.html
  // => 클라이언트가 POST 요청으로 데이터를 보낼 때 기본 형식은 다음과 같다.
  //      application/x-www-form-urlencoded
  // => <form> 태그에서 enctype 속성에 "mulpart/form-data"를 지정하면
  //    해당 형식으로 서버에 값을 보낸다.
  // => 자바스크립트를 사용하여 개발자가 임의의 형식으로 값을 보낼 수 있다.
  //
  // 클라이언트가 POST로 요청할 때 보내는 데이터의 유형에 따라 호출될 메서드를 구분할 때 사용한다.

  // 다음 메서드는 application/x-www-form-urlencoded 형식의 데이터를 소비한다.
  // => 즉 클라이언트의 HTTP 요청에서 Content-Type 헤더의 값이 위와 같을 때
  //    이 메서드를 호출하라는 의미다.
  @PostMapping(consumes = "application/x-www-form-urlencoded")
  @ResponseBody
  public String handler1() {
    return "handler1";
  }

  // 다음 메서드는 multipart/form-data 형식의 데이터를 소비한다.
  @PostMapping(consumes = "multipart/form-data")
  @ResponseBody
  public String handler2() {
    return "handler2";
  }

  // 다음 메서드는 text/csv 형식의 데이터를 소비한다.
  @PostMapping(consumes = "text/csv")
  @ResponseBody
  public String handler3() {
    return "handler3";
  }

  // 다음 메서드는 application/json 형식의 데이터를 소비한다.
  @PostMapping(consumes = "application/json")
  @ResponseBody
  public String handler4() {
    return "handler4";
  }

  // 다음 메서드는 Content-Type 헤더가 없을 때 호출된다.
  @RequestMapping
  @ResponseBody
  public String handler5() {
    return "handler5";
  }
}

 

 

요청 핸들러의 아규먼트 - 프론트 컨트롤러로부터 받을 수 있는 파라미터 값
package bitcamp.app1;

import java.io.PrintWriter;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/c04_1")
public class Controller04_1 {

  // 프론트 컨트롤러(DispatcherServlet)로부터 받고 싶은 값이 있다면
  // 요청 핸들러를 정의할 때 받고 싶은 타입의 파라미터를 선언하라!
  // 그러면 프론트 컨트롤러가 메서드를 호출할 때 해당 타입의 값을 넘겨줄 것이다.

  // ServletContext는 의존 객체로 주입 받아야 한다.
  // 요청 핸들러에서 파라미터로 받을 수 없다.
  @Autowired
  ServletContext sc;

  // 요청 핸들러에서 받을 수 있는 타입의 아규먼트를 선언해 보자!
  @GetMapping("h1")
  @ResponseBody
  public void handler1(
      // ServletContext sc,
      // ServletContext는 파라미터로 받을 수 없다. 예외 발생!
      // 의존 객체로 주입 받아야 한다.
      ServletRequest request,
      ServletResponse response,
      HttpServletRequest request2,
      HttpServletResponse response2,
      HttpSession session,
      Map<String, Object> map, // JSP에 전달할 값을 담는 임시 보관소
      Model model, // Map과 같다. 둘 중 한 개만 받으면 된다.
      PrintWriter out // 클라이언트에게 콘텐트를 보낼 때 사용할 출력 스트림
      ) {

    out.printf("ServletContext: %b\n", sc != null);
    out.printf("ServletRequest: %b\n", request != null);
    out.printf("ServletResponse: %b\n", response != null);
    out.printf("HttpServletRequest: %b\n", request2 != null);
    out.printf("HttpServletResponse: %b\n", response2 != null);
    out.printf("HttpSession: %b\n", session != null);
    out.printf("Map: %b\n", map != null);
    out.printf("Model: %b\n", model != null);
    out.printf("ServletRequest == HttpServletRequest : %b\n", request == request2);
    out.printf("ServletResponse == HttpServletResponse : %b\n", response == response2);
  }
}

 

 

요청 핸들러의 아규먼트 - @RequestParam
package bitcamp.app1;

import java.io.PrintWriter;
import javax.servlet.ServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/c04_2")
public class Controller04_2 {

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

  // => 요청 핸들러의 파라미터로 선언하면 된다.
  //    단 파라미터 앞에 @RequestParam 애노테이션을 붙인다.
  //    그리고 클라이언트가 보낸 파라미터 이름을 지정한다.
  // 테스트:
  // => http://localhost:9999/eomcs-spring-webmvc/app1/c04_2/h1?name=kim
  @GetMapping("h1")
  @ResponseBody
  public void handler1(
      PrintWriter out,
      ServletRequest request,
      @RequestParam(value = "name") String name1,
      @RequestParam(name = "name") String name2, // value와 name은 같은 일을 한다.
      @RequestParam("name") String name3, // value 이름을 생략할 수 있다.
      /* @RequestParam("name") */ 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);
  }

  // 테스트:
  // http://.../app1/c04_2/h2?name1=kim&name2=park
  @GetMapping("h2")
  @ResponseBody
  public void handler2(
      PrintWriter out,

      @RequestParam("name1") String name1, // 애노테이션을 붙이면 필수 항목으로 간주한다.
      // 따라서 파라미터 값이 없으면 예외가 발생한다.

      String name2, // 애노테이션을 붙이지 않으면 선택 항목으로 간주한다.
      // 따라서 파라미터 값이 없으면 null을 받는다.

      @RequestParam(value = "name3", required = false) String name3,
      // required 프로퍼티를 false로 설정하면 선택 항목으로 간주한다.

      @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);
  }
}

 

 

요청 핸들러의 아규먼트 - 도메인 객체(값 객체; Value Object)로 요청 파라미터 값 받기
package bitcamp.app1;

import java.io.PrintWriter;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/c04_3")
public class Controller04_3 {

  // 클라이언트가 보낸 요청 파라미터 값을 값 객체에 받을 수 있다.

  // => 요청 핸들러의 아규먼트가 값 객체라면,
  //    프론트 컨트롤러는 메서드를 호출할 때 값 객체의 인스턴스를 생성한 후
  //    요청 파라미터와 일치하는 프로퍼티에 대해 값을 저장한다.
  //    그리고 호출할 때 넘겨준다.
  //
  // 테스트:
  // => http://.../c04_3/h1?model=sonata&maker=hyundai&capacity=5&auto=true&engine.model=ok&engine.cc=1980&engine.valve=16
  @GetMapping("h1")
  @ResponseBody
  public void handler1(
      PrintWriter out,

      String model,

      String maker,

      @RequestParam(defaultValue = "100") int capacity, // 프론트 컨트롤러가 String 값을 int로 변환해 준다.
      // 단 변환할 수 없을 경우 예외가 발생한다.

      boolean auto,
      // 프론트 컨트롤러가 String 값을 boolean으로 변환해 준다.
      // 단 변환할 수 없을 경우 예외가 발생한다.
      // "true", "false"는 대소문자 구분없이 true, false로 변환해 준다.
      // 1 ==> true, 0 ==> false 로 변환해 준다. 그 외 숫자는 예외 발생!

      Car car
      // 아규먼트가 값 객체이면 요청 파라미터 중에서 값 객체의 프로퍼티 이름과 일치하는
      // 항목에 대해 값을 넣어준다.
      // 값 객체 안에 또 값 객체가 있을 때는 OGNL 방식으로 요청 파라미터 값을
      // 지정하면 된다.
      // 예) ...&engine.model=ok&engine.cc=1980&engine.valve=16
      ) {

    out.printf("model=%s\n", model);
    out.printf("maker=%s\n", maker);
    out.printf("capacity=%s\n", capacity);
    out.printf("auto=%s\n", auto);
    out.printf("car=%s\n", car);
  }

}

 

 

요청 핸들러의 아규먼트 - 프로퍼티 에디터 사용하기
package bitcamp.app1;

import java.beans.PropertyEditorSupport;
import java.io.PrintWriter;
import java.util.Date;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/c04_4")
public class Controller04_4 {

  // 클라이언트가 보낸 요청 파라미터 값(String 타입)을
  // request handler의 아규먼트 타입(String, int, boolean 등)의 값으로 바꿀 때
  // primitive type에 대해서만 자동으로 변환해 준다.
  // 그 외의 타입에 대해서는 프로퍼티 에디터(타입 변환기)가 없으면 예외를 발생시킨다.

  // 테스트:
  // http://.../c04_4/h1?model=sonata&capacity=5&auto=true&createdDate=2019-4-19
  @GetMapping("h1")
  @ResponseBody
  public void handler1(
      PrintWriter out,
      String model,
      @RequestParam(defaultValue = "5") int capacity, // String ===> int : Integer.parseInt(String)
      boolean auto, // String ===> boolean : Boolean.parseBoolean(String)
      Date createdDate // 프로퍼티 에디터를 설정하지 않으면 변환 오류 발생
      ) {

    out.printf("model=%s\n", model);
    out.printf("capacity=%s\n", capacity);
    out.printf("auto=%s\n", auto);
    out.printf("createdDate=%s\n", createdDate);
  }

  // 테스트:
  // http://.../c04_4/h2?car=sonata,5,true,2019-4-19
  @GetMapping("h2")
  @ResponseBody
  public void handler2(PrintWriter out,
      // 콤마(,)로 구분된 문자열을 Car 객체로 변환하기?
      // => String ===> Car 프로퍼티 에디터를 등록하면 된다.
      @RequestParam("car") Car car) {

    out.println(car);
  }

  // 테스트:
  // http://.../c04_4/h3?engine=bitengine,3500,16
  @GetMapping("h3")
  @ResponseBody
  public void handler3(PrintWriter out,
      // 콤마(,)로 구분된 문자열을 Engine 객체로 변환하기?
      // => String ===> Engine 프로퍼티 에디터를 등록하면 된다.
      @RequestParam("engine") Engine engine) {

    out.println(engine);
  }



  // 이 페이지 컨트롤러에서 사용할 프로퍼티 에디터 설정하는 방법
  // => 프론트 컨트롤러는 request handler를 호출하기 전에
  //    그 메서드가 원하는 아규먼트 값을 준비해야 한다.
  //    각 아규먼트 값을 준비할 때
  //    @InitBinder가 표시된 메서드(request handler를 실행할 때 사용할 도구를 준비하는 메서드)
  //    를 호출하여 프로퍼티 에디터(변환기)를 준비시킨다.
  //    그리고 이 준비된 값 변환기(프로퍼티 에디터)를 이용하여 파라미터 값을
  //    request handler의 아규먼트가 원하는 타입의 값을 바꾼다.
  //    request handler의 아규먼트 개수 만큼 이 메서드를 호출한다.
  // => 따라서 프로퍼티 에디터를 적용하기에
  //    @InitBinder가 표시된 메서드가 적절한 지점이다.
  //    즉 이 메서드에 프로퍼티 에디터를 등록하는 코드를 둔다.
  //

  @InitBinder
  // => 메서드 이름은 마음대로.
  // => 작업하는데 필요한 값이 있다면 파라미터로 선언하라.
  public void initBinder(WebDataBinder binder) {
    System.out.println("Controller04_4.initBinder()...");
    // 프로퍼티 에디터를 등록하려면 그 일을 수행할 객체(WebDataBinder)가 필요하다.
    // request handler 처럼 아규먼트를 선언하여
    // 프론트 컨트롤러에게 달라고 요청하라.

    //String ===> java.util.Date 프로퍼티 에디터 준비
    DatePropertyEditor propEditor = new DatePropertyEditor();

    // WebDataBinder에 프로퍼티 에디터 등록하기
    binder.registerCustomEditor(
        java.util.Date.class, // String을 Date 타입으로 바꾸는 에디터임을 지정한다.
        propEditor // 바꿔주는 일을 하는 프로퍼티 에디터를 등록한다.
        );


    // WebDataBinder에 프로퍼티 에디터 등록하기
    binder.registerCustomEditor(
        Car.class, // String을 Car 타입으로 바꾸는 에디터임을 지정한다.
        new CarPropertyEditor() // 바꿔주는 일을 하는 프로퍼티 에디터를 등록한다.
        );

    // WebDataBinder에 프로퍼티 에디터 등록하기
    binder.registerCustomEditor(Engine.class, // String을 Engine 타입으로 바꾸는 에디터임을 지정한다.
        new EnginePropertyEditor() // 바꿔주는 일을 하는 프로퍼티 에디터를 등록한다.
        );
  }

  // PropertyEditor 만들기
  // => 문자열을 특정 타입의 프로퍼터의 값으로 변환시킬 때 사용하는 에디터이다.
  // => java.beans.PropertyEditor 인터페이스를 구현해야 한다.
  // => PropertyEditor를 직접 구현하면 너무 많은 메서드를 오버라이딩 해야 하기 때문에
  //    자바에서는 도우미 클래스인 PropertyEditorSupport 클래스를 제공한다.
  //    이 클래스는 PropertyEditor를 미리 구현하였다.
  //    따라서 이 클래스를 상속 받은 것 더 낫다.
  class DatePropertyEditor extends PropertyEditorSupport {

    @Override
    public void setAsText(String text) throws IllegalArgumentException {
      System.out.println("DatePropertyEditor.setAsText()");
      // 프로퍼티 에디터를 사용하는 측(예: 프론트 컨트롤러)에서
      // 문자열을 Date 객체로 바꾸기 위해 이 메서드를 호출할 것이다.
      // 그러면 이 메서드에서 문자열을 프로퍼티가 원하는 타입으로 변환한 후 저장하면 된다.
      try {

        // 1) String ==> java.util.Date
        // SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
        // Date date = format.parse(text); // String ===> java.util.Date
        // setValue(date); // 내부에 저장

        // 2) String ==> java.sql.Date
        setValue(java.sql.Date.valueOf(text));

      } catch (Exception e) {
        throw new IllegalArgumentException(e);
      }
    }

    @Override
    public Object getValue() {
      System.out.println("DatePropertyEditor.getValue()");
      // 이 메서드는 프로퍼티 에디터를 사용하는 측(예: 프론트 컨트롤러)에서
      // 변환된 값을 꺼낼 때 호출된다.
      // 이 메서드를 오버라이딩 하는 이유는 이 메서드가 호출된 것을
      // 확인하기 위함이다. 원래는 오버라이딩 해야 할 이유가 없다.
      return super.getValue();
    }
  }

  // String ===> Car 프로퍼티 에디터 만들기
  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);
    }
  }

  class EnginePropertyEditor extends PropertyEditorSupport {
    @Override
    public void setAsText(String text) throws IllegalArgumentException {
      String[] values = text.split(",");

      Engine engine = new Engine();
      engine.setModel(values[0]);
      engine.setCc(Integer.parseInt(values[1]));
      engine.setValve(Integer.parseInt(values[2]));

      setValue(engine);
    }
  }
}

 

 

요청 핸들러의 아규먼트 - 글로벌 프로퍼티 에디터 적용하기
package bitcamp.app1;

import java.io.PrintWriter;
import java.util.Date;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller 
@RequestMapping("/c04_5")
public class Controller04_5 {

  // 다른 페이지 컨트롤러에서 등록한 프로퍼티 에디터는 사용할 수 없다.
  // 각 페이지 컨트롤러 마다 자신이 사용할 프로퍼티 에디터를 등록해야 한다.
  // 따라서 만약 여러 페이지 컨트롤러에서 공통으로 사용하는 프로퍼티 에디터라면 
  // 글로벌 프로퍼티 에디터로 등록하는 것이 편하다.
  
  // 글로벌 프로퍼티 에디터 등록하기
  // @ControllerAdvice 애노테이션이 붙은 클래스에서 
  // @InitBinder 메서드를 정의하면 된다.
  // => GlobalControllerAdvice 클래스를 참고하라!
  
  // 테스트:
  //    http://.../c04_5/h1?model=sonata&capacity=5&auto=true&createdDate=2019-4-19
  @GetMapping("h1") 
  @ResponseBody 
  public void handler1(
      PrintWriter out,
      String model,
      int capacity, 
      boolean auto, 
      Date createdDate
      ) {
    
    out.printf("model=%s\n", model);
    out.printf("capacity=%s\n", capacity);
    out.printf("auto=%s\n", auto);
    out.printf("createdDate=%s\n", createdDate);
  }
  
  //테스트:
  //    http://.../c04_5/h2?car=sonata,5,true,2019-4-19
  @GetMapping("h2") 
  @ResponseBody 
  public void handler2(
      PrintWriter out,
      @RequestParam("car") Car car) {
    
    out.println(car);
  }
  
  //테스트:
  //    http://.../c04_5/h3?engine=bitengine,3500,16
  @GetMapping("h3") 
  @ResponseBody 
  public void handler3(
      PrintWriter out,
      @RequestParam("engine") Engine engine) {
    
    out.println(engine);
  }
}

 

package bitcamp.app1;

import java.beans.PropertyEditorSupport;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.InitBinder;

// @ControllerAdvice
// => 이름에 이미 역할에 대한 정보가 담겨있다.
// => 페이지 컨트롤러를 실행할 때 충고하는 역할을 수행한다.
//    즉 프론트 컨트롤러가 페이지 컨트롤러의 request handler를 호출하기 전에
//    이 애노테이션이 붙은 클래스를 참고하여 적절한 메서드를 호출한다.
//
@ControllerAdvice
public class GlobalControllerAdvice {

  // 이 클래스에 프로퍼티 에디터를 등록하는 @InitBinder 메서드를 정의한다.
  @InitBinder
  public void initBinder(WebDataBinder binder) {

    DatePropertyEditor propEditor = new DatePropertyEditor();
    binder.registerCustomEditor(java.util.Date.class, propEditor);

    binder.registerCustomEditor(Car.class, new CarPropertyEditor());

    binder.registerCustomEditor(Engine.class, new EnginePropertyEditor());
  }

  class DatePropertyEditor extends PropertyEditorSupport {
    @Override
    public void setAsText(String text) throws IllegalArgumentException {
      try {
        // String ==> java.util.Date
        // SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
        // Date date = format.parse(text);
        // setValue(date);

        // String ==> java.sql.Date
        setValue(java.sql.Date.valueOf(text));

      } catch (Exception e) {
        throw new IllegalArgumentException(e);
      }
    }
  }

  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);
    }
  }

  class EnginePropertyEditor extends PropertyEditorSupport {
    @Override
    public void setAsText(String text) throws IllegalArgumentException {
      String[] values = text.split(",");

      Engine engine = new Engine();
      engine.setModel(values[0]);
      engine.setCc(Integer.parseInt(values[1]));
      engine.setValve(Integer.parseInt(values[1]));

      setValue(engine);
    }
  }
}

 

 

요청 핸들러의 아규먼트 - @RequestHeader
package bitcamp.app1;

import java.io.PrintWriter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/c04_6")
public class Controller04_6 {

  // 클라이언트의 HTTP 요청 헤더를 받고 싶으면
  // request handler의 아규먼트 앞에 @RequestHeader(헤더명) 애노테이션을 붙여라!

  // 테스트:
  // http://.../c04_6/h1
  @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(".*Edg.*")) {
      out.println("Edge");
    } else 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");
    }
  }

  public static void main(String[] args) {
    String str =
        "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";
    // String str = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/605.1.15 (KHTML,
    // like Gecko) Version/12.1 Safari/605.1.15";
    // String str = "AA BB Aa Ba $12,000";

    // 정규 표현식으로 패턴을 정의한다.
    // String regex = "Chrome";
    // String regex = "Chrome.*Safari";
    String regex = "[^(Chrome.*)]Safari";
    Pattern pattern = Pattern.compile(regex);

    // 주어진 문자열에서 패턴과 일치하는 정보를 찾는다.
    Matcher matcher = pattern.matcher(str);

    // 일치 여부를 확인한다.
    if (matcher.find()) {
      System.out.println("OK!");
      // for (int i = 1; i < matcher.groupCount(); i++) {
      // System.out.println(matcher.group(1));
      // }
    } else {
      System.out.println("NO!");
    }
  }
}

 

 

요청 핸들러의 아규먼트 - @Cookie
package bitcamp.app1;

import java.io.PrintWriter;
import java.net.URLEncoder;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/c04_7")
public class Controller04_7 {

  // 클라이언트가 보낸 쿠키 꺼내기
  // => @CookieValue(쿠키명) 애노테이션을 request handler의 아규먼트 앞에 붙인다.

  // 테스트:
  // http://.../c04_7/h1
  @GetMapping("h1")
  @ResponseBody
  public void handler1(
      PrintWriter out,
      HttpServletResponse response
      ) {
    // 이 메서드에서 쿠키를 클라이언트로 보낸다.
    try {
      // 쿠키의 값이 ASCII가 아니라면 URL 인코딩 해야만 데이터가 깨지지 않는다.
      // URL 인코딩을 하지 않으면 ? 문자로 변환된다.
      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 cookie!");
  }

  // 테스트:
  // http://.../c04_7/h2
  @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 // String ===> int 자동 변환
      ) throws Exception {

    //
    // 1) 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
    // ==> 웹 브라우저에서는 받은 값을 그대로 저장
    //
    // 2) 쿠키를 다시 서버로 보내기
    // ==> 웹 브라우저는 저장된 값을 그대로 전송
    //     "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
    //     그래서 한글이 깨진 것이다.
    //
    // 해결책:
    // => UTF-16을 ISO-8859-1 바이트 배열로 변경한다.
    //    41 42 ea b0 80 ea b0 81
    byte[] originBytes = name2.getBytes("ISO-8859-1");

    // => 다시 바이트 배열을 UTF-16으로 바꾼다.
    //    이때 바이트 배열이 UTF-8로 인코딩된 값임을 알려줘야 한다.
    //    0041 0042 ac00 ac01
    String namex = new String(originBytes, "UTF-8");

    return String.format(//
        "name1=%s\n name2=%s\n name2=%s\n name3=%s\n age=%d\n", //
        name1, name2, namex, name3, age);
  }
}

 

 

요청 핸들러의 아규먼트 - multipart/form-data 형식의 파라미터 값 받기
package bitcamp.app1;

import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.UUID;
import javax.servlet.ServletContext;
import javax.servlet.http.Part;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

@Controller
@RequestMapping("/c04_8")
public class Controller04_8 {

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

  // 클라이언트가 멀티파트 형식으로 전송한 데이터를 꺼내기
  // => Servlet API에서 제공하는 Part를 사용하거나
  //    또는 Spring에서 제공하는 MultipartFile 타입의 아규먼트를 선언하면 된다.
  //
  // 주의!
  // => DispatcherServlet을 web.xml을 통해 배치했다면,
  //    <multipart-config/> 태그를 추가해야 한다.
  // => WebApplicationInitializer를 통해 DispatcherServlet을 배치했다면,
  //    App1WebApplicationInitializer 클래스를 참고하라!
  //

  // 테스트:
  // http://.../html/app1/c04_8.html
  @PostMapping(value = "h1", produces = "text/html;charset=UTF-8")
  @ResponseBody
  public String handler1(
      String name,
      int age,
      Part photo // Servlet API의 객체
      ) 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>" +
        // 현재 URL이 다음과 같기 때문에 업로드 이미지의 URL을 이 경로를 기준으로 계산해야 한다.
        // http://localhost:8080/java-spring-webmvc/app1/c04_8/h1
        //
        (filename != null ? "<p><img src='../../html/app1/" + filename + "'></p>" : "")
        + "</body></html>";
  }

  // MultipartFile로 멀티파트 데이터를 받으려면,
  // Spring WebMVC 설정에서 MultipartResolver 객체를 등록해야 한다.
  //
  // 테스트:
  // http://.../html/app1/c04_8.html
  @PostMapping(value = "h2", produces = "text/html;charset=UTF-8")
  @ResponseBody
  public String handler2(//
      String name, //
      @RequestParam(defaultValue = "0") int age, //
      MultipartFile photo // Spring API의 객체
      ) 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>" +
        // 현재 URL이 다음과 같기 때문에 업로드 이미지의 URL을 이 경로를 기준으로 계산해야 한다.
        // http://localhost:8080/java-spring-webmvc/app1/c04_8/h2
        //
        (filename != null ? "<p><img src='../../html/app1/" + filename + "'></p>" : "")
        + "</body></html>";
  }

  // 테스트:
  // http://.../html/app1/c04_8.html
  @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();
  }


  // 테스트:
  // http://.../html/app1/c04_8.html
  @PostMapping(value = "h4", produces = "text/html;charset=UTF-8")
  @ResponseBody
  public String handler4(
      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) {
      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();
  }
}

 

 

요청 핸들러의 아규먼트 - @RequestBody : 클라이언트가 보낸 데이터를 한 덩어리로 받기
package bitcamp.app1;

import java.io.PrintWriter;
import java.io.StringWriter;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller 
@RequestMapping("/c04_9")
public class Controller04_9 {

  // 클라이언트가 보낸 데이터를 통째로 받기
  // => request handler의 아규먼트 앞에 @RequestBody를 붙이면 된다.
  
  // 테스트:
  //    http://.../html/app1/c04_9.html
  @PostMapping(value="h1", produces="text/html;charset=UTF-8") 
  @ResponseBody 
  public String handler1(
      String name,
      int age,
      // 클라이언트가 보낸 데이터를 통째로 받으려면 @RequestBody 애노테이션을 붙인다.
      @RequestBody String data
      ) throws Exception {
    
    StringWriter out0 = new StringWriter();
    PrintWriter out = new PrintWriter(out0);
    out.println("<html><head><title>c04_9/h1</title></head><body>");
    out.println("<h1>결과</h1>");
    out.printf("<p>이름:%s</p>\n", name);
    out.printf("<p>나이:%s</p>\n", age);
    out.printf("<p>통데이터:%s</p>\n", data);
    out.println("</body></html>");
    return out0.toString();
  }
}

 

 

요청 핸들러의 리턴 값 - 콘텐트를 직접 리턴하기
package bitcamp.app1;

import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

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

  // 테스트:
  // http://localhost:9999/eomcs-spring-webmvc/app1/c05_1/h1
  @GetMapping("h1")
  @ResponseBody
  public String handler1() {
    // 리턴 값이 클라이언트에게 보내는 콘텐트라면
    // 메서드 선언부에 @ResponseBody를 붙인다.
    // => 붙이지 않으면 프론트 컨트롤러는 view URL로 인식한다.
    // => 출력 콘텐트는 브라우저에서 기본으로 HTML로 간주한다.
    //    단 한글은 ISO-8859-1 문자표에 정의된 코드가 아니기 때문에
    //    클라이언트로 보낼 때 '?' 문자로 바꿔 보낸다.
    return "<html><body><h1>abc가각간</h1></body></html>";
  }

  // 테스트:
  // http://localhost:9999/eomcs-spring-webmvc/app1/c05_1/h2
  // => 리턴되는 콘텐트의 MIME 타입과 charset을 지정하고 싶다면
  //    애노테이션의 produces 프로퍼티에 설정하라.
  @GetMapping(value = "h2", produces = "text/html;charset=UTF-8")
  @ResponseBody
  public String handler2() {
    return "<html><body><h1>abc가각간<h1></body></html>";
  }

  // 테스트:
  // http://localhost:9999/eomcs-spring-webmvc/app1/c05_1/h3
  @GetMapping("h3")
  @ResponseBody
  public String handler3(HttpServletResponse response) {

    // HttpServletResponse에 대해 다음과 같이 콘텐트 타입을 설정해봐야 소용없다.
    response.setContentType("text/html;charset=UTF-8");

    return "<html><body><h1>abc가각간<h1></body></html>";
  }

  // 테스트:
  // http://localhost:9999/eomcs-spring-webmvc/app1/c05_1/h4
  @GetMapping("h4")
  public HttpEntity<String> handler4(HttpServletResponse response) {
    // HttpEntity 객체에 콘텐트를 담아 리턴할 수 있다.
    // 이 경우에는 리턴 타입으로 콘텐트임을 알 수 있기 때문에
    // @ResponseBody 애노테이션을 붙이지 않아도 된다.

    HttpEntity<String> entity = new HttpEntity<>(
        "<html><body><h1>abc가각간<h1></body></html>");

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

    return entity;
  }

  // 테스트:
  // http://localhost:9999/eomcs-spring-webmvc/app1/c05_1/h5
  @GetMapping(value = "h5", produces = "text/html;charset=UTF-8")
  public HttpEntity<String> handler5(HttpServletResponse response) {
    // 한글을 제대로 출력하고 싶으면 위 애노테이션의 produces 속성에 콘텐트 타입을 지정한다.
    HttpEntity<String> entity = new HttpEntity<>(
        "<html><body><h1>abc가각간<h1></body></html>");

    return entity;
  }

  // 테스트:
  // http://localhost:9999/eomcs-spring-webmvc/app1/c05_1/h6
  @GetMapping("h6")
  public HttpEntity<String> handler6(HttpServletResponse response) {
    // 한글을 제대로 출력하고 싶으면,
    // 응답 헤더에 직접 Content-Type을 설정할 수 있다.

    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;
  }

  // 테스트:
  // http://localhost:9999/eomcs-spring-webmvc/app1/c05_1/h7
  @GetMapping("h7")
  public ResponseEntity<String> handler7(HttpServletResponse response) {
    // HttpEntity 대신에 ResponseEntity 객체를 리턴 할 수 있다.
    // 이 클래스의 경우 응답 상태 코드를 추가하기 편하다.

    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;
  }
}

 

 

요청 핸들러의 리턴 값 - view URL 리턴하기, 리다이렉트, forward/include
package bitcamp.app1;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.view.JstlView;

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

  // 테스트:
  // http://localhost:9999/eomcs-spring-webmvc/app1/c05_2/h1
  @GetMapping("h1")
  public String handler1() {
    // 메서드 선언부에 @ResponseBody를 붙이지 않으면
    // 프론트 컨트롤러는 view URL로 간주한다.
    // => 리턴 URL의 '/'는 웹 애플리케이션 루트를 의미한다.
    return "/jsp/c05_2.jsp";
  }

  // 테스트:
  // http://localhost:9999/eomcs-spring-webmvc/app1/c05_2/h2
  @GetMapping("h2")
  public String handler2() {
    // MVC 패턴에서는 항상 Controller에 의해 View가 통제되어야 한다.
    // Controller를 경유하지 않고 View를 실행하게 해서는 안된다.
    // 그래야 View에 대해 일관성 있는 제어가 가능하다.
    // 문제는 jsp 파일을 웹 애플리케이션의 일반 폴더에 두게 되면
    // 다음과 같이 클라이언트에서 직접 실행을 요청할 수 있다.
    // http://localhost:9999/eomcs-spring-webmvc/jsp/c05_2.jsp
    //
    // 이것을 막으려면, 다음과 같이 WEB-INF 폴더 밑에 JSP 파일을 두어라.
    // /WEB-INF 폴더에 있는 파일은 클라이언트에서 직접 실행을 요청할 수 없다.
    return "/WEB-INF/jsp/c05_2.jsp";
  }

  // 테스트:
  // http://localhost:9999/eomcs-spring-webmvc/app1/c05_2/h3
  @GetMapping("h3")
  public View handler3() {
    return new JstlView("/WEB-INF/jsp/c05_2.jsp");
  }

  // 테스트:
  // http://localhost:9999/eomcs-spring-webmvc/app1/c05_2/h4
  @GetMapping("h4")
  public ModelAndView handler4() {
    System.out.println("===> /app1/c05_2/h4");
    ModelAndView mv = new ModelAndView();
    mv.setViewName("/WEB-INF/jsp/c05_2.jsp");
    return mv;
  }

  // 테스트:
  // http://localhost:9999/eomcs-spring-webmvc/app1/c05_2/h5
  @GetMapping("h5")
  public String handler5() {
    // 리다이렉트를 지정할 때는 URL 앞에 "redirect:" 접두어를 붙인다.
    // 즉 HTTP 응답이 다음과 같다.
    // HTTP/1.1 302
    // Location: h4
    // Content-Language: ko-KR
    // Content-Length: 0
    // Date: Fri, 19 Apr 2019 07:57:00 GMT

    return "redirect:h4";
  }

  // 테스트:
  // http://localhost:9999/eomcs-spring-webmvc/app1/c05_2/h6
  @GetMapping("h6")
  public String handler6() {
    // 포워드를 지정할 때는 URL 앞에 "forward:" 접두어를 붙인다.
    // 인클루드를 지정할 때는 URL 앞에 "include:" 접두어를 붙인다.
    return "forward:h4";
  }
}

 

 

요청 핸들러에서 view 컴포넌트(JSP) 쪽에 데이터 전달하기
package bitcamp.app1;

import java.util.Map;
import javax.servlet.ServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

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

  // 테스트:
  //   http://localhost:9999/eomcs-spring-webmvc/app1/c05_3/h1
  @GetMapping("h1")
  public String handler1(
      ServletRequest request) {

    // JSP가 꺼내 쓸 수 있도록 ServletRequest 객체에 직접 담는다.
    request.setAttribute("name", "홍길동");
    request.setAttribute("age", 20); // auto-boxing: int ===> Integer 객체
    request.setAttribute("working", true); // auto-boxing: boolean ===> Boolean 객체

    return "/WEB-INF/jsp/c05_3.jsp";
  }

  // 테스트:
  //   http://localhost:9999/eomcs-spring-webmvc/app1/c05_3/h2
  @GetMapping("h2")
  public String handler2(Map<String,Object> map) {

    // 아규먼트에 Map 타입의 변수를 선언하면
    // 프론트 컨트롤러는 빈 맵 객체를 만들어 넘겨준다.
    // 이 맵 객체의 용도는 JSP에 전달할 값을 담는 용이다.
    // 맵 객체에 값을 담아 놓으면 프론트 컨트롤러가 JSP를 실행하기 전에
    // ServletRequest로 복사한다.
    // 따라서 ServletRequest에 값을 담는 것과 같다.
    //
    map.put("name", "홍길동");
    map.put("age", 20); // auto-boxing
    map.put("working", true); // auto-boxing

    return "/WEB-INF/jsp/c05_3.jsp";
  }

  // 테스트:
  //   http://localhost:9999/eomcs-spring-webmvc/app1/c05_3/h3
  @GetMapping("h3")
  public String handler3(Model model) {

    // 아규먼트에 Model 타입의 변수를 선언하면
    // 프론트 컨트롤러는 모델 객체를 만들어 넘겨준다.
    // 이 객체의 용도는 Map 객체와 같다.
    //
    model.addAttribute("name", "홍길동");
    model.addAttribute("age", 20); // auto-boxing
    model.addAttribute("working", true); // auto-boxing

    return "/WEB-INF/jsp/c05_3.jsp";
  }

  // 테스트:
  //   http://localhost:9999/eomcs-spring-webmvc/app1/c05_3/h4
  @GetMapping("h4")
  public ModelAndView handler4() {

    // request handler에서 ModelAndView 객체를 만들어 리턴한다.
    // => 이 객체의 용도는 Model과 view URL을 함께 리턴하는 것이다.
    //
    ModelAndView mv = new ModelAndView();

    // JSP가 사용할 데이터를 담고
    mv.addObject("name", "홍길동");
    mv.addObject("age", 20); // auto-boxing
    mv.addObject("working", true); // auto-boxing

    // JSP 주소도 담는다.
    mv.setViewName("/WEB-INF/jsp/c05_3.jsp");

    return mv;
  }
}

 

 

eomcs-spring-webmvc/src-14/~/app2

 

 

package bitcamp.config;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class App2WebApplicationInitializer 
  extends AbstractAnnotationConfigDispatcherServletInitializer {

  @Override
  protected Class<?>[] getRootConfigClasses() {
    return null;
  }
  
  @Override
  protected Class<?>[] getServletConfigClasses() {
    return new Class<?>[] {App2Config.class};
  }
  
  @Override
  protected String[] getServletMappings() {
    return new String[] {"/app2/*"};
  }
  
  @Override
  protected String getServletName() {
    return "app2";
  }
}

 

helper.setRemoveSemicolonContent(false);

    // DispatcherServlet의 MVC Path 관련 설정을 변경한다.
    configurer.setUrlPathHelper(helper);
  }

  // 이 설정을 사용하는 프론트 컨트롤러에 적용할 인터셉터 설정하기
  @Override
  public void addInterceptors(InterceptorRegistry registry) {

    // 1) 모든 요청에 대해 실행할 인터셉터 등록하기
    // => 인터셉터를 적용할 URL을 지정하지 않으면
    // 현재 프론트 컨트롤러의 모든 요청에 대해 적용된다.
    //
    registry.addInterceptor(new Controller04_1_Interceptor1());

    // 2) 특정 요청에 대해 실행할 인터셉터 등록하기
    // => 패턴: /c04_1/*
    // 적용 가능:
    // /c04_1/x
    // /c04_1/y
    // 적용 불가:
    // /x
    // /c03_1/x
    // /c04_1/a/x
    // /c04_1/a/b/x
    // 즉, /c04_1/ 바로 밑의 있는 자원에 대해서만 인터셉터를 적용한다.
    //
    registry
    .addInterceptor(new Controller04_1_Interceptor2())
    .addPathPatterns("/c04_1/*");

    // 3) 특정 요청에 대해 실행할 인터셉터 등록하기
    // => 패턴: /c04_1/**
    // 적용 가능:
    // /c04_1/x
    // /c04_1/y
    // /c04_1/a/x
    // /c04_1/a/b/x
    // 적용 불가:
    // /x
    // /c03_1/x
    // 즉, /c04_1/ 의 모든 하위 경로에 있는 자원에 대해서만 인터셉터를 적용한다.
    //
    registry
    .addInterceptor(new Controller04_1_Interceptor3())
    .addPathPatterns("/c04_1/**");

    // 4) 특정 요청에 대해 인터셉터 적용을 제외하기
    // => 패턴: /c04_1/** (include), /c04_1/a/** (exclude)
    // 적용 가능:
    // /c04_1/x
    // /c04_1/y
    // /c04_1/b/x
    // /c04_1/b/c/x
    // 적용 불가:
    // /x
    // /c03_1/x
    // /c04_1/a/x
    // /c04_1/a/b/x
    // 즉, /c04_1/ 의 모든 하위 경로에 있는 자원에 대해서만 인터셉터를 적용한다.
    // 단 /c04_1/a/ 의 모든 하위 경로에 있는 자원은 제외한다.
    registry
    .addInterceptor(new Controller04_1_Interceptor4())
    .addPathPatterns("/c04_1/**")
    .excludePathPatterns("/c04_1/a/**");
  }
}

 

 

기본 View Resolver 사용하기
package bitcamp.app2;

import java.util.Map;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

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

  // 테스트:
  // http://localhost:9999/eomcs-spring-webmvc/app2/c01_1/h1
  @GetMapping("h1")
  // @ResponseBody // 뷰 이름을 리턴 할 때는 이 애노테이션을 붙이면 안된다.
  public String handler1(Model model) {

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

    return "/jsp/c01_1.jsp";
    // 기본 ViewResolver는 리턴 값으로 URL을 받아
    // 웹 애플리케이션 디렉토리에서 JSP를 찾는다.
    // 웹 애플리케이션이 경로가 /eomcs-spring-webmvc 라면,
    // JSP 경로는 다음과 같다.
    // ==> /eomcs-spring-webmvc/jsp/c01_1.jsp
    //
    // InternalResourceViewResolver로 교체한 다음의 JSP URL은?
    // => /WEB-INF/jsp2//jsp/c01_1.jsp.jsp
  }

  // 테스트:
  // http://localhost:9999/eomcs-spring-webmvc/app2/c01_1/h2
  @GetMapping("h2")
  public void handler2(Model model) {
    model.addAttribute("name", "홍길동2");
    model.addAttribute("age", 30);

    // 기본 ViewRosolver를 사용할 때는
    // 뷰 이름을 리턴하지 않으면 오류 발생!
    //
    // InternalResourceViewResolver로 교체한 다음은?
    // => 리턴 값이 없으면 요청 URL(/c01_1/h2)을 리턴 값으로 사용한다.
    // => 따라서 ViewResolver가 계산한 최종 URL은
    // /WEB-INF/jsp2/c01_1/h2.jsp
    //

  }

  // 테스트:
  // http://localhost:9999/eomcs-spring-webmvc/app2/c01_1/h3
  @GetMapping("h3")
  public String handler3(Map<String, Object> map) {

    map.put("name", "홍길동3");
    map.put("age", 40);

    return "/WEB-INF/jsp/c01_1.jsp";
    // MVC 모델에서는 JSP는 뷰 콤포넌트로서 출력이라는 역할을 담당한다.
    // 출력할 데이터를 준비하는 일은 페이지 컨트롤러가 담당한다.
    // 그래서 JSP를 실행할 때는 항상 페이지 컨트롤러를 통해 실행해야 한다.
    // 페이지 컨트롤러가 하는 일이 없어도 프로그래밍의 일관성을 위해
    // 직접 JSP을 요청하지 않고, 페이지 컨트롤러를 통해 요청해야 한다.
    //
    // 그런데 웹 디렉토리에 JSP를 두면 클라이언트에서 JSP를 직접 요청할 수 있다.
    // 페이지 컨트롤러를 경유하지 않은 상태에서 실행해봐야 소용없지만,
    // 그래도 요청은 할 수 있다.
    // 이런 의미 없는 요청을 막는 방법으로,
    // JSP 파일을 /WEB-INF 폴더 아래에 두는 것을 권장한다.
    //
    // 웹 브라우저에서 다음 URL의 JSP를 요청해보라!
    // 1) http://localhost:8888/bitcamp-java-spring-webmvc/jsp/c01_1.jsp
    // => 클라이언트가 요청할 수 있다.
    // 2) http://localhost:8888/bitcamp-java-spring-webmvc/WEB-INF/jsp/c01_1.jsp
    // => 클라이언트가 요청할 수 없다.
    // => /WEB-INF 폴더에 있는 자원들은 클라이언트에서 직접 요청할 수 없다.
    // => 그래서 잘못된 요청을 막을 수 있다.
    // 실무에서는 이 방법을 사용한다.
  }
}

 

 

기본 View Resolver 교체 - InternalResourceViewResolver 사용하기
package bitcamp.app2;

import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping("/c01_2")
public class Controller01_2 {

  // 테스트:
  // http://localhost:9999/eomcs-spring-webmvc/app2/c01_2/h1
  @GetMapping("h1")
  public String handler1(Map<String, Object> map) {

    map.put("name", "홍길동");
    map.put("age", 20);

    //
    // ViewResolver?
    // => 실행할 뷰를 찾는 일을 한다.
    // => 페이지 컨트롤러가 리턴한 뷰 이름에 해당하는 뷰 콤포넌트를 찾는 역할.
    // => ResourceBundleViewResolver
    //    - *.properties 에서 뷰 이름에 해당하는 콤포넌트의 URL을 찾는다.
    // => InternalResouceViewResolver
    //    - 미리 지정된 접두사, 접미사를 사용하여 뷰이름으로 콤포넌트의 URL을 완성한다.
    //
    // View?
    // => 뷰를 실행하는 일을 한다.
    // => 템플릿 엔진을 실행하여 실제 클라이언트로 보낼 콘텐트를 생성한다.
    // => Thymeleaf, FreeMarker, JSP/JSTL, Tiles, RSS/Atom, PDF, Excel 등의
    //    엔진을 이용하여 콘텐트를 생성하는 뷰가 있다.
    //
    // ViewResolver 교체
    // => InternalResourceViewResolver를 사용하여
    //    JSP URL의 접두어와 접미사를 미리 설정해 둘 수 있어 URL을 지정하기 편리하다.
    // => 교체 방법은 XML에서 설정하는 방법과 Java Config로 설정하는 방법이 있다.
    //    자세한 것은 App2Config 클래스를 참고하라!
    //
    // ViewResolver 실행 과정?
    // => 페이지 컨트롤러는 클라이언트가 요청한 작업을 실행한 후
    //    그 결과를 출력할 뷰의 이름을 리턴한다.
    // => 프론트 컨트롤러는 request handler가 리턴한 URL을
    //    view resolver에게 전달한다.
    // => view resolver는 자신의 정책에 맞춰서 뷰 URL을 준비한다.
    // => InternalResourceViewResolver의 경우
    //    request handler가 리턴한 URL 앞, 뒤에
    //    접두사와 접미사를 붙여 JSP를 찾는다.
    //    예를 들어 다음 URL을 리턴하면,
    //      "c01_1/h1"
    //    최종 JSP URL은,
    //      "/WEB-INF/jsp2/c01_2/h1.jsp"
    //    이다.
    // => 프론트 컨트롤러는 ViewResolver가 준비한 URL을 가지고
    //    View 객체를 통해 해당 URL의 자원을 실행한다.
    //
    return "c01_2/h1"; // => /WEB-INF/jsp2/c01_1/h1.jsp
  }

  // 테스트:
  // http://localhost:9999/eomcs-spring-webmvc/app2/c01_1/h2
  @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을 만든다.
    // => "/WEB-INF/jsp2/" + "/c01_2/h2" + ".jsp"
    // => "/WEB-INF/jsp2/c01_2/h2.jsp"
    //
    // 실무에서는 이 방법을 많이 사용한다.
  }

  // 테스트:
  // http://localhost:9999/eomcs-spring-webmvc/app2/c01_2/h3
  @GetMapping("h3")
  public Map<String, Object> handler3() {

    HashMap<String, Object> map = new HashMap<>();
    map.put("name", "홍길동3");
    map.put("age", 40);

    // Map 객체에 값을 담아 리턴하면
    // 프론트 컨트롤러는 Map 객체에 보관되어 있는 값들을 ServletRequest 보관소로 옮긴다.
    // 그리고 view URL은 request handler의 URL을 사용한다.
    // => "/WEB-INF/jsp2" + "/c01_2/h3" + ".jsp" = "/WEB-INF/jsp2/c01_2/h3.jsp"
    return map;
  }

  // 테스트:
  // http://localhost:9999/eomcs-spring-webmvc/app2/c01_2/h4
  @GetMapping("h4")
  public ModelAndView handler4() {

    ModelAndView mv = new ModelAndView();
    mv.addObject("name", "홍길동3");
    mv.addObject("age", 40);
    mv.setViewName("c01_2/h4");

    // ModelAndView 객체에 값과 뷰 이름을 담아 리턴하면
    // 프론트 컨트롤러는 ModelAndView 객체에 보관되어 있는 값들을
    // ServletRequest 보관소로 옮기고,
    // 설정된 뷰 이름을 ViewResolver에게 넘긴다.
    //
    return mv;
  }

  // 테스트:
  // http://localhost:9999/eomcs-spring-webmvc/app2/c01_2/h5
  @GetMapping("h5")
  public ModelAndView handler5() {

    ModelAndView mv = new ModelAndView();
    mv.addObject("name", "홍길동3");
    mv.addObject("age", 40);

    // ModelAndView 객체에 값과 뷰 이름을 담아 리턴하면
    // 프론트 컨트롤러는 ModelAndView 객체에 보관되어 있는 값들을
    // ServletRequest 보관소로 옮기고,
    // 뷰 이름을 지정하지 않으면 request handler의 path를 ViewResolver에게 넘긴다.
    // => /c01_2/h5
    // InternalResourceViewResolver 는 위 URL을 다음과 같이 바꾼다.
    // => /WEB-INF/jsp2/c01_2/h5.jsp
    // 그런 후에 해당 URL의 JSP를 실행한다.
    return mv;
  }
}

 

 

URL 에서 값 추출하기 - @PathVariable
package bitcamp.app2;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/c02_1")
public class Controller02_1 {

  // 테스트:
  //   http://localhost:9999/eomcs-spring-webmvc/app2/c02_1?name=kim&age=20
  @GetMapping
  @ResponseBody
  public String handler1(String name, int age) {
    // 클라이언트로부터 값을 받는 일반적인 방법
    // => Query String 으로 받는다.
    // => 즉 URL 다음에 "?변수=값&변수=값" 형태로 값을 받는다.
    // => Query String의 값을 request handler에서 받으려면
    //    아규먼트를 선언하면 된다.
    //    아규먼트 앞에 @RequestParam을 붙여도 되고
    //    아규먼트이 이름이 요청 파라미터의 이름과 같다면 @RequestParam을 생략해도 된다.
    return String.format("name=%s, age=%d", name, age);
  }

  // 테스트:
  //   http://localhost:9999/eomcs-spring-webmvc/app2/c02_1/kim/20
  @GetMapping("{name}/{age}")
  @ResponseBody
  public String handler2(
      /*
      @PathVariable("name") String name,
      @PathVariable("age") int age
       */
      // URL의 변수 이름을 생략하면 아규먼트 이름을 사용한다.
      @PathVariable String name,
      @PathVariable int age
      ) {
    // URL path에 값을 포함하여 전달할 수 있고, 그 값을 아규먼트로 받을 수 있다.
    // URL path에 포함된 값을 받으려면 request handler의 URL을 설정할 때
    // 다음의 문법으로 선언해야 한다.
    // => .../{변수명}/{변수명}
    // 이렇게 선언된 변수 값을 받으려면 다음과 같이 아규먼트를 선언해야 한다.
    // => @PathVariable(변수명) String 아규먼트
    // 변수명과 아규먼트의 이름이 같다면, 다음과 같이 변수명을 생략할 수 있다.
    // => @PathVariable String 아규먼트
    //
    return String.format("name=%s, age=%d", name, age);
  }

  // 테스트:
  //   http://localhost:9999/eomcs-spring-webmvc/app2/c02_1/kim_20
  @GetMapping("{name}_{age}")
  @ResponseBody
  public String handler3(
      @PathVariable String name,
      @PathVariable int age
      ) {
    return String.format("name=%s, age=%d", name, age);
  }
}

 

 

URL 에서 값 추출하기 - @MatrixVariable
package bitcamp.app2;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.MatrixVariable;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/c02_2")
public class Controller02_2 {

  // 테스트:
  // http://.../app2/c02_2?name=kim&age=20
  @GetMapping
  @ResponseBody
  public String handler1(String name, int age) {
    // Query String 으로 값 받기
    return String.format("name=%s, age=%d", name, age);
  }

  // 테스트:
  // http://.../app2/c02_2/name=kim;age=20
  @GetMapping(value = "{value}", produces = "text/plain;charset=UTF-8")
  @ResponseBody
  public String handler2(//
      @PathVariable("value") String value,
      // value 값 중에서 name 항목의 값을 받고 싶을 때 @MatrixVariable 을 사용한다.
      // 단 value의 형식은 "이름=값;이름=값;이름=값" 형태여야 한다.
      // @MatrixVariable("name") String name,
      // @MatrixVariable("age") int age

      // 매트릭스 변수명을 생략하면 아규먼트의 이름을 사용한다.
      @MatrixVariable String name,
      @MatrixVariable int age
      ) {

    // @MatrixVariable 애노테이션을 사용하려면
    // IoC 컨테이너에서 이 애노테이션을 활성화시키는 설정을 추가해야 한다.
    // 1) XML 설정
    //    => <mvc:annotation-driven enable-matrix-variables="true"/>
    // 2) Java Config 설정
    //    => @EnableWebMvc 애노테이션을 활성화시킨다.
    //    => WebMvcConfigurer 구현체를 정의한다.
    //    => UrlPathHelper 객체의 removeSemicolonContent 프로퍼티 값을 false로 설정한다.

    // 테스트1
    // http://.../app2/c02_2/name=kim;age=20
    // => @PathVariable("value") : name=kim <== 첫 번째 세미콜론의 값만 가져온다.
    // => @MatrixVariable("name") : kim
    // => @MatrixVariable("age") : 20
    //
    // 테스트2
    // http://.../app2/c02_2/user;name=kim;age=20
    // => @PathVariable("value") : user <== 첫 번째 세미콜론의 값만 가져온다.
    // => @MatrixVariable("name") : kim
    // => @MatrixVariable("age") : 20
    return String.format("value:%s \n name:%s, age:%d", value, name, age);
  }

  // 테스트:
  // http://.../app2/c02_2/name=teamA;qty=5/title=work1;state=1
  @GetMapping(value = "{team}/{task}", produces = "text/plain;charset=UTF-8")
  @ResponseBody
  public String handler3(
      // 여러 개의 패스 변수가 있을 때 값을 꺼내는 방법
      @MatrixVariable String name,
      @MatrixVariable int qty,
      @MatrixVariable String title,
      @MatrixVariable int state
      ) {

    return String.format("team: %s(%d)\n task: %s, %d", name, qty, title, state);
  }

  // http://.../app2/c02_2/h4/name=teamA;qty=5/name=work1;qty=1
  @GetMapping(value = "h4/{team}/{task}", produces = "text/plain;charset=UTF-8")
  @ResponseBody
  public String handler4(
      // 여러 개의 패스 변수가 있을 때 값을 꺼내는 방법
      // => 만약 항목의 이름이 같다면?
      @MatrixVariable(name = "name", pathVar = "team") String name1,
      @MatrixVariable(name = "qty", pathVar = "team") int qty1,
      @MatrixVariable(name = "name", pathVar = "task") String name2,
      @MatrixVariable(name = "qty", pathVar = "task") int qty2) {

    return String.format("team: %s(%d)\n task: %s, %d", name1, qty1, name2, qty2);
  }
}

 

 

URL 에서 값 추출하기 - 정규표현식으로 URL 다루기
package bitcamp.app2;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller 
@RequestMapping("/c02_3")
public class Controller02_3 {

  // 테스트:
  //   http://.../app2/c02_3/h1/hongkildong/010-1111-2222/man
  @GetMapping(
      value="h1/{name}/{tel}/{gender}", 
      produces="text/plain;charset=UTF-8")
  @ResponseBody
  public String handler1(
      @PathVariable String name, 
      @PathVariable String tel,
      @PathVariable String gender) {
    
    return String.format("name: %s\n tel: %s \n gender: %s", 
        name, tel, gender);
  }
  
  // 테스트:
  //   http://.../app2/c02_3/h1/hongkildong/010-1111-2222/man
  @GetMapping(
      value="h2/{name:[a-zA-Z0-9]+}/{tel:[0-9]+-[0-9]+-[0-9]+}/{gender:man|woman}", 
      produces="text/plain;charset=UTF-8")
  @ResponseBody
  public String handler2(
      @PathVariable String name, 
      @PathVariable String tel,
      @PathVariable String gender) {
    
    // 패스 변수를 사용할 때,
    // 패스 변수의 값 규칙을 정규표현식으로 정의할 수 있다.
    // 정규 표현식에 어긋간 URL의 경우 예외가 발생할 것이다.
    //
    return String.format("name: %s\n tel: %s \n gender: %s", 
        name, tel, gender);
  }
}

 

 

세션 다루기 - HttpSession 직접 사용하기
package bitcamp.app2;

import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

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

  // 테스트:
  //   http://.../app2/c03_1/h1
  @GetMapping(value="h1", produces="text/plain;charset=UTF-8")
  @ResponseBody
  public String handler1(HttpSession session) {
    // HttpSession 객체를 사용하려면 아규먼트로 받아야 한다.
    //
    session.setAttribute("name", "홍길동");
    session.setAttribute("age", "20");

    return "세션에 값을 보관했음!";
  }

  // 테스트:
  //   http://.../app2/c03_1/h2
  @GetMapping(value="h2", produces="text/plain;charset=UTF-8")
  @ResponseBody
  public String handler2(HttpSession session) {
    // HttpSession 객체를 사용하려면 아규먼트로 받아야 한다.
    //
    return String.format("name=%s, age=%s",
        session.getAttribute("name"),
        session.getAttribute("age"));
  }

  // 테스트:
  //   http://.../app2/c03_1/h3
  @GetMapping(value="h3", produces="text/plain;charset=UTF-8")
  @ResponseBody
  public String handler3(HttpSession session) {
    // HttpSession 객체를 사용하려면 아규먼트로 받아야 한다.
    //

    // 세션을 무효화시키기
    session.invalidate();

    return "세션을 무효화시켰음!";
  }

  @GetMapping(value="h4", produces="text/plain;charset=UTF-8")
  @ResponseBody
  public String handler4(
      // 현재 페이지 컨트롤러의 @SessionAttributes 에 지정된 이름이 아니라면,
      // 프론트 컨트롤러는 요청 파라미터에서 해당 이름의 값을 찾아 넘겨준다.
      // 요청 파라미터에 해당 이름의 값이 없다면,
      // 프론트 컨트롤러는 빈 문자열을 넘겨준다.
      @ModelAttribute("name") String name,
      @ModelAttribute("age") String age,
      @ModelAttribute("name2") String name2,
      @ModelAttribute("age2") String age2) {

    return String.format("name=%s, age=%s, name2=%s, age2=%s",
        name, age, name2, age2);
  }
}

 

 

세션 다루기 - @SessionAttributes, @ModelAttribute
package bitcamp.app2;

import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.SessionAttributes;

@Controller 
@RequestMapping("/c03_2")

// request handler 가 뷰 컴포넌트(jsp)에 전달하는 값 중에서 
// 세션에 보관할 값의 이름을 지정하면 
// 프론트 컨트롤러는 그 값을 HttpSession 객체에도 보관해 둔다.
// 또한 @ModelAttribute에서 지정한 이름의 값을 세션에서 꺼낼 때도 사용한다.
// 즉 @SessionAttributes 에 이름을 지정해 두지 않으면 
// 세션에 해당 값이 들어 있어도 
// @ModelAttribute가 붙은 아규먼트에 값을 넣어주지 않는다.
@SessionAttributes({"name2","age2"})

public class Controller03_2 {

  // 테스트:
  //   http://.../app2/c03_2/h1
  @GetMapping(value="h1", produces="text/plain;charset=UTF-8")
  @ResponseBody
  public String handler1(Model model) {
    
    // Model 객체에 값을 담으면 프론트 컨트롤러는 ServletRequest 보관소에 값을 옮긴다.
    // 만약 @SessionAttributes 에서 지정한 이름의 값이라면
    // HttpSession 객체에도 보관된다.
    model.addAttribute("name2", "임꺽정");
    model.addAttribute("age2", "30");
    
    // @SessionAttributes에 등록되지 않은 이름의 값은 세션에 보관되지 않는다.
    model.addAttribute("tel2", "1111-2222");
    
    return "세션에 값 보관했음!";
    
  }
  
  // 테스트:
  //   http://.../app2/c03_2/h2
  @GetMapping(value="h2", produces="text/plain;charset=UTF-8")
  @ResponseBody
  public String handler2(HttpSession session) {
    return String.format("name2=%s, age2=%s, tel2=%s", 
        session.getAttribute("name2"),
        session.getAttribute("age2"),
        session.getAttribute("tel2"));
  }
  
  // 테스트:
  //   http://.../app2/c03_2/h3
  @GetMapping(value="h3", produces="text/plain;charset=UTF-8")
  @ResponseBody
  public String handler3(
      // @ModelAttribute 에 지정된 이름이 @SessionAttributes에 있는 경우 
      // => 세션에 해당 값이 있으면 아규먼트에 넣어 준다.
      // => 세션에 해당 값이 없으면 예외가 발생한다.
      @ModelAttribute("name2") String name2,
      @ModelAttribute("age2") String age2,
      
      // @ModelAttribute 에 지정된 이름이 @SessionAttributes에 없는 경우 
      // => 요청 파라미터에 tel2 값이 있다면 그 값을 넣어준다.
      // => 요청 파라미터에 값이 없으면 아규먼트에 빈 문자열을 넣어 준다.
      // => @SessionAttributes에 등록한 이름이 아니기 때문에 세션에서 값을 꺼내지 않는다.
      @ModelAttribute("tel2") String tel2) {
    
    return String.format("name2=%s, age2=%s, tel2=%s", 
        name2, age2, tel2);
  }

  // 테스트:
  //   1) http://.../app2/c03_1/h1 을 실행하여 name과 age 값을 세션에 보관한다.
  //   2) http://.../app2/c03_2/h1 을 실행하여 name2와 age2 값을 세션에 보관한다.
  //   3) http://.../app2/c03_2/h4 을 실행하여 세션에 보관된 값을 꺼낸다.
  @GetMapping(value="h4", produces="text/plain;charset=UTF-8")
  @ResponseBody
  public String handler4(
      // c03_1/h1 에서 세션에 저장한 값 꺼내기
      // => 꺼내지 못한다. name과 age에는 빈 문자열이 저장된다.
      // => 왜?
      //    @ModelAttribute는 현재 페이지 컨트롤러의 @SessionAttributes에 
      //    지정된 이름에 한해서만 세션에서 값을 꺼내기 때문이다.
      @ModelAttribute("name") String name,
      @ModelAttribute("age") String age,
      
      // 현재 컨트롤러에서 세션에 저장한 값 꺼내기
      @ModelAttribute("name2") String name2,
      @ModelAttribute("age2") String age2) {
    
    return String.format("name=%s, age=%s, name2=%s, age2=%s", 
        name, age, name2, age2);
  }
}

 

 

세션 다루기 - 세션의 값을 무효화시키는 방법
package bitcamp.app2;

import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;

@Controller
@RequestMapping("/c03_3")

// 세션에 보관된 값 중에서 현재 페이지 컨트롤러에서 사용할 값을 지정한다.
// 또한 세션에 보관할 값이기도 하다.
@SessionAttributes({"name", "name2"})
public class Controller03_3 {

  // 테스트:
  // http://.../app2/c03_3/h1
  @GetMapping(value = "h1", produces = "text/plain;charset=UTF-8")
  @ResponseBody
  public String handler1(HttpSession session) {
    return String.format("name=%s, age=%s, name2=%s, age2=%s",
        session.getAttribute("name"),
        session.getAttribute("age"),
        session.getAttribute("name2"),
        session.getAttribute("age2"));
  }

  // 테스트:
  // http://.../app2/c03_3/h2
  @GetMapping(value = "h2", produces = "text/plain;charset=UTF-8")
  @ResponseBody
  public String handler2(HttpSession session) {
    // 세션을 완전히 무효화시키기
    session.invalidate();
    // 용도:
    // => 페이지 컨트롤러에 상관없이 모든 세션 값을 삭제할 때 사용하라.
    // 세션 자체를 무효화시켜 다음에 요청이 들어 왔을 때 새로 세션을 만들게 한다.
    //
    return "session.invalidate()";
  }

  // 테스트:
  // http://.../app2/c03_3/h3
  @GetMapping(value = "h3", produces = "text/plain;charset=UTF-8")
  @ResponseBody
  public String handler3(SessionStatus status) {
    // 현재 페이지 컨트롤러의 @SessionAttributes 에 지정된 값만 무효화시키기
    status.setComplete();
    // 용도:
    // => 보통 페이지 컨트롤러는 서로 관련된 작업을 처리하는 요청 핸들러를 정의한다.
    //    예) BoardController : add(), detail(), list(), update(), delete()
    // => 또는 트랜잭션과 관련된 작업을 처리하는 요청 핸들러를 두기도 한다.
    //    예) BookOrderController: 장바구니담기(), 주문하기(), 결제하기()
    // => 이렇게 특정 작업에 관계된 요청 핸들러가 작업하는 동안
    //    공유할 데이터가 있다면 세션에 보관하면 편할 것이다.
    //    작업이 완료되면 그 작업을 처리하는 동안 세션에 보관했던 데이터는 삭제해야 한다.
    //    세션의 모든 데이터가 아니라
    //    현재 페이지 컨트롤러가 보관한 데이터만 삭제하고 싶을 때
    //    바로 이 방식으로 처리하면 된다.
    // => 즉 세션을 그대로 유지한채로 이 페이지 컨트롤러에서
    // @SessionAttributes로 지정한 값만 무효화시킬 때 사용한다.
    return "status.setComplete()";
  }

  @GetMapping(value="h4", produces="text/plain;charset=UTF-8")
  @ResponseBody
  public String handler4(
      @ModelAttribute("name") String name,
      @ModelAttribute("age") String age,
      @ModelAttribute("name2") String name2,
      @ModelAttribute("age2") String age2) {

    return String.format("name=%s, age=%s, name2=%s, age2=%s",
        name, age, name2, age2);
  }
}

 

 

스프링 인터셉터 다루기
package bitcamp.app2;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller 
@RequestMapping("/c04_1")
public class Controller04_1 {

  // 테스트:
  //   http://.../app2/c04_1/h1
  @GetMapping("h1")
  public String handler1() {
    System.out.println("c04_1.handler1() 호출");
    return "c04_1";
  }
  
  // 테스트:
  //   http://.../app2/c04_1/a/h2
  @GetMapping("a/h2")
  public String handler2() {
    System.out.println("c04_1.handler2() 호출");
    return "c04_1";
  }
  
  
  // 테스트:
  //   http://.../app2/c04_1/b/h3
  @GetMapping("b/h3")
  public String handler3() {
    System.out.println("c04_1.handler3() 호출");
    return "c04_1";
  }
}

 

package bitcamp.app2;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

// 인터셉터 만들기
// => 프론트 컨트롤러와 페이지 컨트롤러 사이에 코드를 삽입하는 기술
// => 프론트 컨트롤러와 뷰 컴포넌트 사이에 코드를 삽입하는 기술
//
// 인터셉터를 배치하기
// => 프론트 컨트롤러의 IoC 설정 파일에 배치 정보를 추가한다.
//
public class Controller04_1_Interceptor1 implements HandlerInterceptor {
  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
      throws Exception {
    // 페이지 컨트롤러의 핸들러를 호출하기 전에 이 메서드가 먼저 호출된다.
    System.out.println("Interceptor1.preHandle()");

    // 다음 인터셉터나 페이지 컨트롤러를 계속 실행하고 싶다면 true를 리턴한다.
    // 여기서 요청 처리를 완료하고 싶다면 false를 리턴한다.
    return true;
  }

  @Override
  public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
      ModelAndView modelAndView) throws Exception {
    // 페이지 컨트롤러의 핸들러가 리턴한 후 이 메서드가 호출된다.
    System.out.println("Interceptor1.postHandle()");
  }

  @Override
  public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
      Object handler, Exception ex) throws Exception {
    // JSP를 실행한 후 이 메서드가 호출된다.
    System.out.println("Interceptor1.afterCompletion()");
  }
}

 

 

JSON 콘텐트 출력하기
AJAX 요청 테스트:
  //   http://.../html/app2/c05_1.html
  //
  @GetMapping("h1")
  public void handler1(Model model) {
    model.addAttribute("list", this.list);
  }

  // 2) Google Gson 라이브러리를 사용하여 JSON 형식의 콘텐트 출력하기
  // => mvnrepository.com에서 gson 검색하여 라이브러리 정보를 가져온다.
  // => build.gradle 파일에 추가한다.
  // => '$gradle eclipse' 실행한다.
  // => 이클립스에서 프로젝트를 리프래시 한다.
  // 테스트:
  //   http://.../app2/c05_1/h2
  @GetMapping(value="h2", produces="text/plain;charset=UTF-8")
  @ResponseBody
  public String handler2() {
    return new Gson().toJson(this.list);
  }


  // 3) Google Gson 라이브러리를 사용하여 JSON 형식의 콘텐트 출력하기 II
  // => 페이지 컨트롤러의 리턴 값이 String이 아니면
  //    프론트 컨트롤러는
  //    Google의 Gson 라이브러리나 Jackson 라이브러리를 사용하여
  //    자동으로 JSON 형식의 문자열로 만들어 클라이언트로 출력한다.
  // => 단 Gson 또는 Jackson 라이브러리가 있어야 한다.
  //    둘 다 있다면 Jackson 라이브러리가 기본으로 사용된다.
  // => build.gradle 파일에서 gson 또는 jackson 라이브러리를 추가하는 부분의
  //    주석을 참고하라!
  // 테스트:
  //   http://.../app2/c05_1/h3
  @GetMapping("h3")
  @ResponseBody
  public Object handler3() {
    return this.list; // JSON 형식의 문자열은 자동으로 UTF-8로 인코딩 된다.
  }
}

 

 

JSON 콘텐트 출력하기 - @RestController
package bitcamp.app2;

import java.sql.Date;
import java.util.ArrayList;
import java.util.HashMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
// => 페이지 컨트롤러를 @RestController로 선언하면,
//    리턴 값은 HttpMessageConverter에 의해 자동으로 변환된다.
// => @ResponseBody를 붙일 필요가 없다.
//
@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() {
    // primitive 타입이나 String 타입의 값을 JSON 형식으로 출력하려면
    // 도메인 객체나 맵 객체 담아 리턴하라!
    HashMap<String,Object> content = new HashMap<>();
    content.put("v1", 100);
    content.put("v2", "Hello");

    return content;
  }
}

 

 

JSON 콘텐트 입력받기 - @RestController
HttpMessageConverter 구현체(예:MappingJackson2HttpMessageConverter)가
  //    클라이언트가 보낸 데이터를 Map 객체에 담아준다.
  // => 이 기능을 쓰고 싶다면 Gson 또는 Jackson 라이브러리를 반드시 포함해야 한다.
  //    그래야 스프링의 DispatcherServlet에서 찾는다.
  // 테스트
  //   http://.../html/app2/c05_3.html
  @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!";
  }

  // 5) JSON 형식의 요청 파라미터 값을 도메인 객체로 받기
  // => HttpMessageConverter 구현체(예: MappingJackson2HttpMessageConverter)가
  //    클라이언트가 보낸 데이터를 도메인 객체(예: Board, Member, Lesson 등)에 담아준다.
  // => Json 데이터의 프로퍼티 명과 도메인 객체의 프로퍼티 명이 일치해야 한다.
  //
  // 테스트
  //   http://.../html/app2/c05_3.html
  @RequestMapping(value="h5", produces="text/plain;charset=UTF-8")
  public Object handler5(@RequestBody Board content) throws Exception {
    System.out.println(content);

    // 주의!
    // => 클라이언트에서 보낸 날짜 데이터의 문자열 형식이 yyyy-MM-dd 형태여야 한다.
    //    그래야 java.util.Date 타입의 값으로 변환해 준다.
    //    예) 2019-05-01 ===> java.util.Date 객체 변환 성공!
    // => 만약 이 형태가 아니면 변환할 수 없어 실행 오류가 발생한다.
    //    예) 05/01/2020 또는 2020-5-1 ===> 변환 오류!
    //
    // @JsonFormat 애노테이션 사용
    // => 이 애노테이션은 MappingJackson2HttpMessageConverter를 위한 것이다.
    //    GsonHttpMessageConverter는 이 애노테이션을 인식하지 않는다.
    // => 도메인 객체의 프로퍼티에 이 애노테이션을 붙이면
    //    2019-05-01 이나 2019-5-1 모두 처리할 수 있다.
    // => 뿐만 아니라, 도메인 객체를 JSON 문자열로 변환할 때도
    //    해당 형식으로 변환된다.
    //

    return "OK!";
  }
}
package bitcamp.app2;

import java.sql.Date;
import com.fasterxml.jackson.annotation.JsonFormat;

public class Board {
  protected int no;
  protected String title;
  protected String content;
  protected String writer;
  protected int viewCount;

  // Jackson 라이브러리를 사용한다면
  // Date 필드의 JSON 출력 형식을 지정할 수 있다.
  // 다음 애노테이션을 setter나 field에 붙이면 된다.
  @JsonFormat(
      shape=JsonFormat.Shape.STRING,
      pattern="yyyy-MM-dd")
  protected Date createdDate;

  public Board() {
  }

  public Board(
      int no, String title, String content, String writer,
      int viewCount, Date createdDate) {
    this.no = no;
    this.title = title;
    this.content = content;
    this.writer = writer;
    this.viewCount = viewCount;
    this.createdDate = createdDate;
  }

  @Override
  public String toString() {
    return "Board [no=" + no + ", title=" + title + ", content=" + content + ", writer=" + writer
        + ", viewCount=" + viewCount + ", createdDate=" + createdDate + "]";
  }

  public int getNo() {
    return no;
  }
  public void setNo(int no) {
    this.no = no;
  }
  public String getTitle() {
    return title;
  }
  public void setTitle(String title) {
    this.title = title;
  }
  public String getContent() {
    return content;
  }
  public void setContent(String content) {
    this.content = content;
  }
  public String getWriter() {
    return writer;
  }
  public void setWriter(String writer) {
    this.writer = writer;
  }
  public int getViewCount() {
    return viewCount;
  }
  public void setViewCount(int viewCount) {
    this.viewCount = viewCount;
  }
  public Date getCreatedDate() {
    return createdDate;
  }

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

 

 

예외 다루기
package bitcamp.app2;

import java.io.IOException;
import java.sql.SQLException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping("/c06_1")
public class Controller06_1 {

  // 테스트:
  // http://.../app2/c06_1/get
  @GetMapping("get")
  public void get() {}

  // 테스트:
  // http://.../app2/c06_1/post
  @PostMapping("post")
  public void post() {}

  // 테스트:
  // http://.../app2/c06_1/error1
  @GetMapping("error1")
  public void error1() throws Exception {
    throw new Exception("request handler 오류 발생!");
    // Request Handler에서 예외를 던졌을 때 처리 절차!
    // 1) 페이지 컨트롤러 안에 예외 처리기가 있다면,
    // => 해당 메서드를 호출한다.
    // 2) @ControllerAdvice 객체에 예외 처리기가 있다면,
    // => 해당 메서드를 호출한다.
    // 3) web.xml 에 지정된 오류 처리 기본 페이지가 설정되어 있다면,
    // => 해당 페이지를 실행한다.
    // 4) 서블릿 컨테이너의 기본 오류 처리 페이지를 실행한다.
    //
  }

  // 테스트:
  // http://.../app2/c06_1/error2
  @GetMapping("error2")
  public void error() throws Exception {
    throw new IOException("request handler 오류 발생!");
  }

  // 테스트:
  // http://.../app2/c06_1/error3
  @GetMapping("error3")
  public void error3() throws Exception {
    throw new SQLException("request handler 오류 발생!");
  }

  @ExceptionHandler
  public ModelAndView exceptionHandler(Exception ex) {
    ModelAndView mv = new ModelAndView();
    mv.addObject("error", ex);
    mv.setViewName("error6");
    return mv;
  }
}

 

 

예외 다루기
package bitcamp.app2;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/c06_2")
public class Controller06_2 {

  // 테스트:
  // http://.../app2/c06_2/error1
  @GetMapping("error1")
  public void error1() throws Exception {
    throw new Exception("request handler 오류 발생!");
    // Request Handler에서 예외를 던졌을 때 처리 절차!
    // 1) 페이지 컨트롤러 안에 예외 처리기가 있다면,
    // => 해당 메서드를 호출한다.
    // 2) @ControllerAdvice 객체에 예외 처리기가 있다면,
    // => 해당 메서드를 호출한다.
    // 3) web.xml 에 지정된 오류 처리 기본 페이지가 설정되어 있다면,
    // => 해당 페이지를 실행한다.
    // 4) 서블릿 컨테이너의 기본 오류 처리 페이지를 실행한다.
    //
  }
}

 

 

// 예외 다루기
package bitcamp.app2;

import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class ErrorController {

  // 테스트:
  // http://.../app2/error
  @RequestMapping("/error")
  public ModelAndView error(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;
  }
}

 

 

// 모든 페이지 컨트롤러에 적용할 작업을 설정한다.
package bitcamp.app2;

import java.io.IOException;
import java.sql.SQLException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;

@ControllerAdvice
public class GlobalControllerAdvice {

  // DispatcherServlet은
  // request handler가 던지는 예외에 따라
  // 그 예외를 받을 수 있는 메서드를 찾아 호출한다.
  // 단, @ExceptionHandler 가 선언된 메서드여야 한다.
  //
  @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;
  }
}

 

 

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                      http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
  version="4.0" metadata-complete="false">

  <description>
    스프링 Web MVC 프레임워크 예제 테스트
  </description>

  <display-name>java-spring-webmvc</display-name>
  
  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
    <welcome-file>index.htm</welcome-file>
    <welcome-file>default.htm</welcome-file>
  </welcome-file-list>

  <!-- 
    서블릿 컨테이너에서 요청을 처리하는 중에 오류가 발생했을 때
    오류 내용을 출력하는 기본 페이지를 교체하기 
  -->
  <!-- 1)오류가 발생했을 때 직접 JSP를 실행  -->
  <!--  
  <error-page>
    <location>/WEB-INF/jsp2/error1.jsp</location>
  </error-page>
  -->
  <!-- 2)오류가 발생했을 때 페이지 컨트롤러를 경유하여 JSP 실행  -->
  <error-page>
    <location>/app2/error</location>
  </error-page>
</web-app>

 

 

 

build.gradle

 

build-01.gradle

plugins {
    id 'java'
    id 'eclipse-wtp'
    id 'war'
}

tasks.withType(JavaCompile) {
    options.encoding = 'UTF-8';
} 

// 이클립스에서 출력할 프로젝트 이름을 설정한다.
eclipse {
    project {
        name = "eomcs-spring-webmvc"
    }
}

war {
  archiveBaseName = "spring"
}

repositories {
    mavenCentral()
}

dependencies {
    // compileOnly?
    // - 프로그래밍(컴파일) 하는 동안에만 사용한다.
    // - 배포 파일(.jar, .war, .ear)에는 포함하지 않는다.
    // - 프로그램이 배치되는 런타입 서버(예: 실행 중인 톰캣 서버)에서 
    //   라이브러리를 제공하는 경우 굳이 배포할 필요가 없기 때문에 
    //   이 옵션을 사용한다.
    // => Servlet API 라이브러리
    //compileOnly 'javax.servlet:javax.servlet-api:4.0.1'
    
    // providedCompile?
    // - compileOnly 처럼 컴파일 할 때만 사용한다.
    // - 배포 파일에는 포함하지 않는다.
    // - 단 이 옵션은 'war' 플러그인을 사용할 때만 설정할 수 있다.
    providedCompile 'javax.servlet:javax.servlet-api:4.0.1'
    
    // implementation?
    // - 컴파일 할 때 사용한다.
    // - 배포 파일에도 포함한다.
    // => JSTL 명세를 구현한 라이브러리
    implementation 'javax.servlet:jstl:1.2'

    implementation 'com.google.guava:guava:28.2-jre'

    // testImplementation?
    // - 단위 테스트를 수행할 때만 사용한다. 
    // - 배포 파일(.jar, .war, ear)에 포함되지 않는다.
    testImplementation 'junit:junit:4.12'
}

 

 

build-08.gradle

plugins {
    id 'java'
    id 'eclipse-wtp'
    id 'war'
}

tasks.withType(JavaCompile) {
    options.encoding = 'UTF-8';
} 

// 이클립스에서 출력할 프로젝트 이름을 설정한다.
eclipse {
    project {
        name = "eomcs-spring-webmvc"
    }
}

war {
  archiveBaseName = "spring"
}

repositories {
    mavenCentral()
}

dependencies {
    providedCompile 'javax.servlet:javax.servlet-api:4.0.1'
    implementation 'javax.servlet:jstl:1.2'
    implementation 'org.springframework:spring-webmvc:5.3.7'
    implementation 'org.apache.logging.log4j:log4j-core:2.14.1'

    // gradle을 통해 자동으로 다운로드 받은 라이브러리가 아니라,
    // 별도로 다운로드 받은 라이브러리 파일을 프로젝트 포함시키는 방법.
    //   implementation fileTree(
    //      dir: '현재 프로젝트 폴더를 기준으로 한 상대 경로 또는 절대 경로',
    //      include: '프로젝트에 포함한 라이브러리 파일들'
    //
    implementation fileTree(dir: 'lib',  include: '*.jar')

    implementation 'com.google.guava:guava:28.2-jre'

    testImplementation 'junit:junit:4.12'
}

 

 

build-14.gradle

plugins {
    id 'java'
    id 'eclipse-wtp'
    id 'war'
}

tasks.withType(JavaCompile) {
    options.encoding = 'UTF-8';
} 

// 이클립스에서 출력할 프로젝트 이름을 설정한다.
eclipse {
    project {
        name = "eomcs-spring-webmvc"
    }
}

war {
  archiveBaseName = "spring"
}

repositories {
    mavenCentral()
}

dependencies {
    providedCompile 'javax.servlet:javax.servlet-api:4.0.1'
    implementation 'javax.servlet:jstl:1.2'
    implementation 'org.springframework:spring-webmvc:5.3.7'
    implementation 'org.apache.logging.log4j:log4j-core:2.14.1'

    // 파일 업로드 처리
    implementation group: 'commons-fileupload', name: 'commons-fileupload', version: '1.4'

    // JSON 형식을 다루는 라이브러리
    // @Controller가 붙은 일반적인 페이지 컨트롤러의 요청 핸들러를 실행할 때
    // 요청 파라미터의 문자열을 int나 boolean 등으로 바꾸기 위해
    // 기본으로 장착된 변환기를 사용한다.
    // 그 변환기는 HttpMessageConverter 규칙에 따라 만든 변환기이다.
    //
    // 또한 요청 핸들러가 리턴한 값을 문자열로 만들어 클라이언트로 출력할 때도
    // 이 HttpMessageConverter를 사용한다.
    // 즉 클라인트가 보낸 파라미터 값을 핸들러의 아규먼트 타입으로 바꿀 때도 이 변환기를 사용하고
    // 핸들러의 리턴 값을 클라이언트로 보내기 위해 문자열로 바꿀 때도 이 변환기를 사용한다.
    //
    // 스프링이 사용하는 기본 데이터 변환기는 MappingJackson2HttpMessageConverter 이다.
    // 만약 이 변환기가 없다면 Google의 Gson 변환기를 사용한다.
    // 구글의 Gson 변환기 마저 없다면 컨버터가 없다는 예외를 발생시킨다.
    // 컨버터가 하는 일은 JSON 데이터로 변환하는 것이다.
    //      클라이언트가 보낸 JSON 요청 파라미터 ===> 자바 객체
    //      핸들러가 리턴하는 자바 객체 ===> JSON 형식의 문자열
    //
    // MappingJackson2HttpMessageConverter?
    // => 요청 파라미터로 JSON 문자열을 받으면 요청 핸들러를 호출할 때 자바 객체로 변환시킨다.
    // => 요청 핸들러가 자바 객체를 리턴할 때 JSON 문자열로 변환한다.
    //
    // 주의!
    // => MappingJackson2HttpMessageConverter를 사용하려면
    //    다음과 같이 의존하는 라이브러리를 추가해야 한다.
    //
    implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.0'

    // => 그런데 JSON 데이터를 처리할 때
    //    MappingJackson2HttpMessageConverter 대신 GsonHttpMessageConverter 를 사용할 수 있다.
    //    단 GsonHttpMessageConverter를 사용하려면
    //    다음과 같이 이 클래스가 들어있는 의존 라이브러리를 추가해야 한다.
    // => 만약 동시에 추가한다면 기본으로 Jackson 라이브러리를 사용한다.
    //
    implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.9'

    implementation 'com.google.guava:guava:28.2-jre'
    testImplementation 'junit:junit:4.12'
}