Chapter5. 스프링 웹 애플리케이션 만들기

5.1 스프링 MVC 시작하기

Spring MVC

  • MVC 패턴을 기반으로 유연한 웹 기반 어플리케이션 개발 모듈

The Spring Web model-view-controller (MVC) framework is designed around a DispatcherServlet that dispatches requests to handlers, with configurable handler mappings, view resolution, locale, time zone and theme resolution as well as support for uploading files.


"Open for extension…​" A key design principle in Spring Web MVC and in Spring in general is the "Open for extension, closed for modification" principle.

5.1.1 스프링 MVC를 이용한 요청 추적

"Front Controller" design pattern

Spring’s web MVC framework is, like many other web MVC frameworks, request-driven, designed around a central Servlet that dispatches requests to controllers and offers other functionality that facilitates the development of web applications.

  1. 요청이 전달되어 스프링의 DispatcherServlet(Front Controller)에 도착.
  2. DispatcherServlet이 Handler Mapping을 참조하여 전달할 Controller를 선택.
  3. DispatcherServlet은 선택한 컨트롤러에게 요청을 전달.(Service -> Repo -> ...)
  4. Controller에서 처리된 결과를 사용자에게 전달해주기 위한 정보(Model)과 View 이름을 DispatcherServlet에게 되돌려줌.
  5. DispatcherServlet는 View Resolver을 참조하여 View Mapping 정보를 획득.
  6. DispatcherServlet는 렌더링하기 위한 View에 Model 데이터 전달.
  7. View 렌더링 후, 응답객체에 의해 전달.

5.1.2 스프링 MVC 설정하기

1.DispatcherServlet 설정하기

XML(web.xml)

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

    <servlet-mapping>
        <servlet-name>example</servlet-name>
        <url-pattern>/example/*</url-pattern>
    </servlet-mapping>

</web-app>

Java Config

public class MyWebApplicationInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext container) {
        ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet());
        registration.setLoadOnStartup(1);
        registration.addMapping("/example/*");
    }

}

WebApplicationInitializer is an interface provided by Spring MVC that ensures your code-based configuration is detected and automatically used to initialize any Servlet 3 container. An abstract base class implementation of WebApplicationInitializer named AbstractDispatcherServletInitializer makes it even easier to register the DispatcherServlet


  • SpringBootServletInitializer/AbstractDispatcherServletInitializer/AbstractAnnotationConfigDispatcherServletInitializer implements WebApplicationInitializer


DispatcherServlet 설정

  1. WebApplicationInitializer 직접 구현
  2. AbstractDispatcherServletInitializer/AbstractAnnotationConfigDispatcherServletInitializer 상속
AbstractAnnotationConfigDispatcherServletInitializer
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

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

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

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

}
AbstractDispatcherServletInitializer
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {

    @Override
    protected WebApplicationContext createRootApplicationContext() {
        return null;
    }

    @Override
    protected WebApplicationContext createServletApplicationContext() {
        XmlWebApplicationContext cxt = new XmlWebApplicationContext();
        cxt.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
        return cxt;
    }

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

}

2.두 어플리케이션 컨텍스트에 대한 이야기

In the Web MVC framework, each DispatcherServlet has its own WebApplicationContext, which inherits all the beans already defined in the root WebApplicationContext

  • DispatcherServlet이 시작되면서 스프링 어플리케이션 컨텍스트를 생성하고 이를 통해 클래스나 설정 파일로 선언된 빈으로 로딩하기 시작한다.

Single root context in Spring Web MVC

주의

  • 클래스를 사용한 DispatcherServlet 설정방식은 톰캣7과 같이 서블릿 3.0이상을 지원하는 서버에만 적용가능
웹 어플리케이션의 컨텍스트 구성
  • Servlet Context와 Root Context 계층구조
  • Root Context 단일 구조
    • 스프링mvc를 안쓰고 서드파티 mvc 프레임워크를 사용할 경우등.
  • Servlet Context 단일 구조

스프링 MVC 활성화하기

Java Config

@Configuration
@EnableWebMvc
public class WebConfig {

}

XML Config

 <mvc:annotation-driven/>

위의 선언으로 다음과 같은 기능들이 활성화됨

  • @RequestMapping
  • @ExceptionHandler
  • @Controller + @Valid
  • @RequestBody
  • @NumberFormat
  • HttpMessageConverter 등등..
스프링 MVC 세부기능 커스터마이징
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    // Override configuration methods...

}
WebMvcConfigurerAdapter Configuration
  • Conversion and Formatting
  • Validation
  • Interceptors
  • Content Negotiation
  • View Controllers
  • View Resolvers
  • Serving of Resources
  • Path Matching
  • Message Converters 등등..

5.1.3 Spittr 애플리케이션 소개

5.2 간단한 컨트롤러 작성하기

Spring framework 4.3

http://docs.spring.io/spring/docs/4.3.0.BUILD-SNAPSHOT/spring-framework-reference/htmlsingle/#new-in-4.3

간단한 컨트롤러 작성

@Controller
public class HomeController {

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String home(){
        return "home";
    }
}
  • @Controller
    • @Component를 기반을 둔 어노테이션
    • 컴포넌트 스캔을 통해서 스프링 어플리케이션 컨텍스트의 빈으로 등록
  • @RequestMapping
    • value
    • request path 명시
    • http method 기술
  • return "home"
    • view의 이름을 리턴
    • DispatcherServlet은 ViewResolver에게 전달
    • InternalResourceViewResolver의 설정으로 /WEB-INF/view/home.jsp 적용

5.2.1 컨트롤러 테스팅

테스트 코드 작성

public class HomeControllerTest {
  @Test
  public void testHomePage() {
    HomeController controller = new HomeController();
    MockMvc mockMvc = standaloneSetup(controller).build();
    mockMvc.perform(get("/"))
            .andExpect(view().name("home"))

  }
}
  • standaloneSetup(controller).build() 인스턴스생성
  • get 요청으로 '/' 호출
  • view() 를 통해 view의 이름에 대한 기대 값 설정

5.2.2 클래스 레벨 요청 처리 정의하기

@RequestMapping 분할하기
@Controller
@RequestMapping("/")
public class HomeController {

  @RequestMapping(method=GET)
  public String home(){
    return "home";
  }
}
  • 컨트롤러 클래스에 붙은 @RequestMapping은 모든 메소드에 적용
  • home() 메서드의 @RequestMapping은 클래스에 붙은 @RequestMapping과 조합하여 "/" 에 대한 GET요청을 처리
매핑 추가하기
@Controller
@RequestMapping({"/", "/homepage"})
public class HomeController {
  ...
}
  • HomeController는 "/"와 "/homepage" 요청을 처리한다.

5.2.3 뷰에 모델 데이터 전달하기

Spitter

public class Spitter {

  private Long id;
  private String username;
  private String password;
  private String firstName;
  private String lastName;
  private String email;

  public Spitter() {}

  public Spitter(String username, String password, String firstName, String lastName, String email) {
    this(null, username, password, firstName, lastName, email);
  }

  public Spitter(Long id, String username, String password, String firstName, String lastName, String email) {
    this.id = id;
    this.username = username;
    this.password = password;
    this.firstName = firstName;
    this.lastName = lastName;
    this.email = email;
  }

...getter/setter

  @Override
  public boolean equals(Object that) {
    return EqualsBuilder.reflectionEquals(this, that, "firstName", "lastName", "username", "password", "email");
  }

  @Override
  public int hashCode() {
    return HashCodeBuilder.reflectionHashCode(this, "firstName", "lastName", "username", "password", "email");
  }

}
  • Apache Commons Lang
    • equals(), hashCode() 구현

/spittles의 GET요청 처리를 위한 SpittleController 테스트하기

@Test
  public void shouldShowRecentSpittles() throws Exception {
    List<Spittle> expectedSpittles = createSpittleList(20);
    SpittleRepository mockRepository = mock(SpittleRepository.class); //Mock 저장소
    when(mockRepository.findSpittles(Long.MAX_VALUE, 20))
        .thenReturn(expectedSpittles);

    SpittleController controller = new SpittleController(mockRepository);
    MockMvc mockMvc = standaloneSetup(controller) //Mock 스프링 MVC
        .setSingleView(new InternalResourceView("/WEB-INF/views/spittles.jsp"))
        .build();

    mockMvc.perform(get("/spittles")) // /spittles 확보
       .andExpect(view().name("spittles"))
       .andExpect(model().attributeExists("spittleList"))
       .andExpect(model().attribute("spittleList", //Asset 예상
                  hasItems(expectedSpittles.toArray())));
  }
  • 다른 컨틀로어와 달린 테스트에서는 MockMvc 빌더에서 setSingleView() 를 호출한다.

SpittleController


@Controller
@RequestMapping("/spittles")
public class SpittleController {

  private static final String MAX_LONG_AS_STRING = "9223372036854775807";

  private SpittleRepository spittleRepository;

  @Autowired
  public SpittleController(SpittleRepository spittleRepository) {
    this.spittleRepository = spittleRepository;
  }

  @RequestMapping(method=RequestMethod.GET)
  public List<Spittle> spittles(
      @RequestParam(value="max", defaultValue=MAX_LONG_AS_STRING) long max,
      @RequestParam(value="count", defaultValue="20") int count) {
    return spittleRepository.findSpittles(max, count);
  }
}
  • @Autowired
    • 생성자 @Autowired를 통해서 SpittleRepository 객체 주입
  • Model
    • Model을 통해서 Spittle 리스트로 모델을 채우는 것이 가능해짐.
    • Map 자료구조를 이용해서 (key-value) View에 넘겨져서 클라이언트에 렌더링된다.
    • java.util.Map으로 모델을 넘길 수 있다.
  • View Name
    • View 이름으로 "spittle"를 반환

Request Path를 통한 View Name 추론

@RequestMapping(value="/{spittleId}", method=RequestMethod.GET)
  public List<Spittle> spittle(
      @PathVariable("spittleId") long spittleId, 
      Model model) {
    return spittleRepository.findOne(spittleId);
  }
  • 메소드의 이름을 통해서 View 이름을 처리한다.
    • 위의 경우 메소드이름 spittle이 View 이름 spittle이 된다.

5.3 요청 입력받기

스프링 MVC에서 클라이언트에서 서버(컨트롤러의 핸들러 메소드)로 데이터 전달 방법
  • 쿼리 파라미터
  • 폼 파라미터
  • 패스 변수

@Test
  public void shouldShowRecentSpittles() throws Exception {
    List<Spittle> expectedSpittles = createSpittleList(20);
    SpittleRepository mockRepository = mock(SpittleRepository.class);
    when(mockRepository.findSpittles(Long.MAX_VALUE, 20))
        .thenReturn(expectedSpittles);

    SpittleController controller = new SpittleController(mockRepository);
    MockMvc mockMvc = standaloneSetup(controller)
        .setSingleView(new InternalResourceView("/WEB-INF/views/spittles.jsp"))
        .build();

    mockMvc.perform(get("/spittles"))
       .andExpect(view().name("spittles"))
       .andExpect(model().attributeExists("spittleList"))
       .andExpect(model().attribute("spittleList", 
                  hasItems(expectedSpittles.toArray())));
  }

5.3.1 쿼리 파라미터 입력받기

@RequestParam
  • request 파라미터를 컨트롤러의 메소드 파라미터의 값으로 채워준다.
  • Optional Element

    • defaultValue(String) - 파라미터가 값이 없을 경우 기본값으로 사용.
    • name(String) - request 파라미터의 이름.
    • required(boolean) - 필수 입력 값인지 여부.(default : true)
    • value(String) - name의 별칭.
  • 메소드 파라미터가 Map<String, String> or MultiValueMap<String, String> 일 경우, 이름을 명시할 필요가 없다.

springframework 4.2.6 API 참조

예제 코드

Spittr 어플리케이션에서 페이지화된 Spittles 리스트 요청 기능 구현

  • [TEST] 페이징 처리를 위한 테스트 메소드

    @Test
    public void shouldShowPagedSpittles() throws Exception {
      List<Spittle> expectedSpittles = createSpittleList(50);
      SpittleRepository mockRepository = mock(SpittleRepository.class);
      when(mockRepository.findSpittles(238900, 50))
          .thenReturn(expectedSpittles);
    
      SpittleController controller = new SpittleController(mockRepository);
      MockMvc mockMvc = standaloneSetup(controller)
          .setSingleView(new InternalResourceView("/WEB-INF/views/spittles.jsp"))
          .build();
    
      mockMvc.perform(get("/spittles?max=238900&count=50"))
        .andExpect(view().name("spittles"))
        .andExpect(model().attributeExists("spittleList"))
        .andExpect(model().attribute("spittleList", 
                   hasItems(expectedSpittles.toArray())));
    }
    
  • [Controller] 페이징 처리를 위한 컨트롤러 메소드

    @Controller
    @RequestMapping("/spittles")
    public class SpittleController {
    
      @RequestMapping(method=RequestMethod.GET)
      public List<Spittle> spittles(
          @RequestParam(value="max") long max,
          @RequestParam(value="count") int count) {
        return spittleRepository.findSpittles(max, count);
      }
    }
    
  • [Controller] 페이징 처리를 위한 컨트롤러 메소드(default value)

    private static final String MAX_LONG_AS_STRING = "9223372036854775807";
    //private static final String MAX_LONG_AS_STRING = Long.toString(Long.MAX_VALUE);
    
    @RequestMapping(method=RequestMethod.GET)
    public List<Spittle> spittles(
        @RequestParam(value="max", defaultValue=MAX_LONG_AS_STRING) long max,
        @RequestParam(value="count", defaultValue="20") int count) {
      return spittleRepository.findSpittles(max, count);
    }
    

    private static final String MAX_LONG_AS_STRING = Long.toString(Long.MAX_VALUE) 를 defaultValue에 사용할 경우 "attribute value must be constant" compile error 발생.


5.3.2 패스 파라미터를 통한 입력받기

@PathVariable
  • URI template 변수를 컨트롤러의 메소드 파라미터의 값으로 채워준다.
  • Optional Element
    • value(String) - The URI template 변수의 이름.
  • 메소드 파라미터가 Map<String, String> or MultiValueMap<String, String> 일 경우, URI path의 모든 template 변수들이 map으로 값이 채워진다.

springframework 4.2.6 API 참조

예제 코드

Spittr 어플리케이션에서 주어진 ID에 대해 하나의 Spittle 요청 기능 구현.
쿼리 파라미터를 이용한 처리(/spittles/show?spittle_id=12345)는 리소스 지향 관점에서 보면 이상적인 방식이 아님.
URL 패스에 의해 식별되는 방법(/spittles/12345)이 이상적인 방식.

  • [TEST] 패스 변수로 ID를 명시하는 Spittle의 요청에 대한 테스트

    @Test
    public void testSpittle() throws Exception {
      Spittle expectedSpittle = new Spittle("Hello", new Date());
      SpittleRepository mockRepository = mock(SpittleRepository.class);
      when(mockRepository.findOne(12345)).thenReturn(expectedSpittle);
    
      SpittleController controller = new SpittleController(mockRepository);
      MockMvc mockMvc = standaloneSetup(controller).build();
    
      mockMvc.perform(get("/spittles/12345"))
        .andExpect(view().name("spittle"))
        .andExpect(model().attributeExists("spittle"))
        .andExpect(model().attribute("spittle", expectedSpittle));
    }
    
  • [Controller] 패스 변수로 ID를 명시하는 Spittle 요청 컨트롤러 메소드

    @RequestMapping(value="/{spittleId}", method=RequestMethod.GET)
    public String spittle(
        @PathVariable("spittleId") long spittleId, 
        Model model) {
      model.addAttribute(spittleRepository.findOne(spittleId));
      return "spittle";
    }
    
  • [Controller] 패스 변수로 ID를 명시하는 Spittle 요청 컨트롤러 메소드(value 파라미터 생략)

    @RequestMapping(value="/{spittleId}", method=RequestMethod.GET)
    public String spittle(@PathVariable long spittleId, Model model) {
      model.addAttribute(spittleRepository.findOne(spittleId));
      return "spittle";
    }
    

5.4 폼 처리하기

폼 처리 과정은 폼 보여 주기와 폼을 통해 제출한 데이터 처리하기 두 가지 과정으로 나눌 수 있다.

예제 코드

Spittr 어플리케이션에서 새로운 사용자가 어플리케이션에 등록을 하기 위한 폼 구현.

  • [TEST] 폼을 표시하는 컨트롤러 메소드 테스트

    @Test
    public void shouldShowRegistration() throws Exception {
      SpitterRepository mockRepository = mock(SpitterRepository.class);
      SpitterController controller = new SpitterController(mockRepository);
      MockMvc mockMvc = standaloneSetup(controller).build();
      mockMvc.perform(get("/spitter/register"))
             .andExpect(view().name("registerForm"));
    }
    
  • [Controller] 사용자가 랩에 가입할 수 있는 폼을 표시

    @RequestMapping("/spitter")
    public class SpitterController {
    
      @RequestMapping(value="/register", method=GET)
      public String showRegistrationForm() {
        return "registerForm";
      }
    }
    

5.4.1 폼 처리 컨트롤러 작성

request 파라미터의 이름과 동일한 이름의 프로퍼티를 가진 객체를 메소드 파라미터로 사용해서 동일 이름의 request 파라미터 값들이 채워지도록 한다.
InternalResourceViewResolver는 뷰 명세에서 접두사 redirect:, forward: 를 뷰 이름이 아닌 별도의 redirect, forward로 처리한다.

예제 코드

등록 폼에서 POST 요청을 처리할 때, 폼 데이터를 받고 Spitter 객체로 저장하는 기능 구현

  • [TEST] 폼 처리 컨트롤러를 테스트하는 메소드

    @Test
    public void shouldProcessRegistration() throws Exception {
      SpitterRepository mockRepository = mock(SpitterRepository.class);
      Spitter unsaved = new Spitter("jbauer", "24hours", "Jack", "Bauer", "[email protected]");
      Spitter saved = new Spitter(24L, "jbauer", "24hours", "Jack", "Bauer", "[email protected]");
      when(mockRepository.save(unsaved)).thenReturn(saved);
    
      SpitterController controller = new SpitterController(mockRepository);
      MockMvc mockMvc = standaloneSetup(controller).build();
    
      mockMvc.perform(post("/spitter/register")
             .param("firstName", "Jack")
             .param("lastName", "Bauer")
             .param("username", "jbauer")
             .param("password", "24hours")
             .param("email", "[email protected]"))
             .andExpect(redirectedUrl("/spitter/jbauer"));
    
      verify(mockRepository, atLeastOnce()).save(unsaved);
    }
    
  • [Controller] 새로운 사용자를 등록하기 위한 폼을 제출하기

    @Controller
    @RequestMapping("/spitter")
    public class SpitterController {
    
      private SpitterRepository spitterRepository;
    
      @Autowired
      public SpitterController(SpitterRepository spitterRepository) {
        this.spitterRepository = spitterRepository;
      }
    
      @RequestMapping(value="/register", method=GET)
      public String showRegistrationForm() {
        return "registerForm";
      }
    
      @RequestMapping(value="/register", method=POST)
      public String processRegistration(Spitter spitter) {
    
        spitterRepository.save(spitter);
        return "redirect:/spitter/" + spitter.getUsername();
      }
    
      @RequestMapping(value="/{username}", method=GET)
      public String showSpitterProfile(@PathVariable String username, Model model) {
        Spitter spitter = spitterRepository.findByUsername(username);
        model.addAttribute(spitter);
        return "profile";
      }
    }
    

    public String processRegistration(Spitter spitter) 메소드에서 파라미터로 Spitter 객체를 받는다. 이 객체는 firstName, lastName, username, password 프로퍼티를 갖고, 같은 이름의 request 파라미터로부터 값이 채워지게 된다.


5.4.2 폼 검증하기

폼 데이터를 검증하기 위한 방법으로 스프링에서는 자바 인증(Validation) API(a.k.a JSR-303)을 제공(스프링 3.0 부터 지원)
추가적인 설정 없이 자바 인증이 스프링 MVC에서 동작하지만, 별도의 자바 인증 API 구현체가 필요(e.g. Hibernate Validator, Apache BVal ...)

  • 자바 인증 API에서 제공되는 검증 Annotation 목록
Annotation Description
@AssertFalse Boolean 타입에 false 값이어야만 하는 요소에 붙이는 Annotation
@AssertTrue Boolean 타입에 true 값이어야만 하는 요소에 붙이는 Annotation
@DecimalMax 값이 BigDecimalString 보다 작거나 같은 값을 갖는 숫자여야 하는 요소에 붙는 Annotation
@DecimalMin 값이 BigDecimalString 보다 크거나 같은 값을 갖는 숫자여야 하는 요소에 붙는 Annotation
@Digits 십진수 값을 갖는 숫자여야 하는 요소에 붙는 Annotation
@Future 값이 미래의 날짜여야 하는 요소에 붙는 Annotation
@Max 주어진 값보다 작거나 같은 값을 갖는 숫자여야 하는 요소에 붙는 Annotation
@Min 주어진 값보다 크거나 같은 값을 갖는 숫자여야 하는 요소에 붙는 Annotation
@NotNull 값이 null이 아니어야 하는 요소에 붙는 Annotation
@Null 값이 null이어야 하는 요소에 붙는 Annotation
@Past 값이 과거의 날짜여야 하는 요소에 붙는 Annotation
@Pattern 값이 주어진 정규 표현식에 맞아야 하는 요소에 붙는 Annotation
@Size 크기가 주어진 값과 같은 String, collection, array이어야 하는 요소에 붙는 Annotation
예제 코드

SpittleForm validation 처리 기능 추가

  • [Spitter] SpittleForm: SpittlePost 요청에서 제출된 필드만 전달하기

    public class Spitter {
    
      private Long id;
    
      @NotNull
      @Size(min=5, max=16)
      private String username;
    
      @NotNull
      @Size(min=5, max=25)
      private String password;
    
      @NotNull
      @Size(min=2, max=30)
      private String firstName;
    
      @NotNull
      @Size(min=2, max=30)
      private String lastName;
    .
    .
    ...
    }
    
  • [Controller] processRegistration(): 제출된 데이터의 적합성 확인

    @RequestMapping(value="/register", method=POST)
    public String processRegistration(
        @Valid Spitter spitter, 
        Errors errors) {
      if (errors.hasErrors()) {
        return "registerForm";
      }
    
      spitterRepository.save(spitter);
      return "redirect:/spitter/" + spitter.getUsername();
    }
    

results matching ""

    No results matching ""