본문 바로가기
스프링(부트)/스프링 내용 정리

[스프링] Spring MVC 예제 및 단위테스트

by doflamingo 2020. 4. 26.

목차

     

    Spring MVC는 이전 블로깅에서도 말했다시피 아래와 같은 플로우를 따른다. 

     

    Spring MVC 플로우 

     

    여기서는 Spring MVC를 사용해서 6가지 간단한 웹 플로우를 만드는 예제를 만들 것이다. 

     

    • Flow 1.  뷰가 없는 컨트롤러, 자체적으로 컨텐츠 제공 
    • Flow 2. 뷰가 있는 컨트롤러 (JSP)
    • Flow 3. 뷰가 있고, ModelMap을 사용하는 컨트롤러
    • Flow 4. 뷰가 있고, ModelAndView를 사용하는 컨트롤러
    • Flow 5. 간단한 폼의 컨트롤러
    • Flow 6. 벨리데이션이 있는 간단한 폼의 컨트롤러

     

    기본 설정을 해야하는 것은

    1. Spring MVC의 의존성을 추가한다. 
    2. DispatcherServlet을 web.xml에 추가한다.
    3. Spring Application Context를 생성한다.  

    Spring MVC에 대한 의존성을 추가

     

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>4.1.4.RELEASE</version>
    </dependency>

     

    DispatcherServlet을 web.xml에 추가

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

     

     

    스프링 애플리케이션 컨텍스트 생성

     

    <beans> <!-- Schema Definition removed --> 
        <context:component-scan base-package="me.bosuksh.springmvc" />
        <mvc:annotation-driven />
        <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" />
        <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" />
    
        <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
            <property name="prefix" value="/WEB-INF/views/" />
            <property name="suffix" value=".jsp" />
        </bean>
    </beans>

     

     

     

    Flow 1 - 뷰 없는 컨트롤러

    Spring MVC 컨트롤러 

     

    @Controller
    public class BasicController {
    
        @RequestMapping(value = "/welcome")
        @ResponseBody
        public String welcome() {
            return "Welcome to Spring MVC";
        }
    
    }

     

    @Controller : 요청 매핑을 포함할 수 있는 Spring MVC 컨트롤러를 정의한다. 

    URL을 컨트롤러 메소드에 매핑한다. 

     

    @RequestMapping(value="/welcome"): URL /welcome을 welcome 메서드에 매핑하는 것을 정의한다. 

    클라이언트가 /welcome에 요청을 보내면, Spring MVC는 welcome 메서드를 실행한다. 

     

    @ResponseBody: welcome 메서드가 반환한 텍스트가 Response로 클라이언트에게 전송된다. 

     

    실행화면

    단위 테스트

     

    단위 테스트는 유지 관리가 가능한 애플리케이션을 개발하는데 있어서 매우 중요하다. 여기서는 Spring MVC 모킹 프레임워크를 이용해서 컨트롤러 단위테스트를 할 것이다. Spring MVC 모킹 프레임워크를 사용하기 위해서 스프링 테스트 프렝미워크에 의존성을 추가한다. 

     

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>4.0.5.RELEASE</version>
      <scope>test</scope>
    </dependency>

     

    이제 단위 테스트를 위해서 테스트할 컨트롤러를 설정하고 테스트 코드를 작성하면 된다. 

     

    테스트를 위한 컨트롤러 설정 

     

    public class BasicControllerTest {
        private MockMvc mockMvc;
    
        @Before
        public void setUp() {
            this.mockMvc = MockMvcBuilders.standaloneSetup(
                    new BasicController()
            ).build();
    
        }
    }

     

    MockMVC: 실제로 애플리케이션 서버에 배포하지 않고서도 Spring MVC를 실행할 수 있는 클래스이다. 

    @Before: 이 어노테이션이 붙으면 @Test 어노테이션 이전에 수행된다. 보통 테스트를 위한 전 작업을 할때 사용한다. 

    MockMvcBuilders.standaloneSetup : 테스트할 클래스를 mockMVC 객체로 등록해주는 역할을 한다. 

     

    테스트 메서드 작성

     

     

    @Test
    public void basicTest() throws Exception {
    	this.mockMvc.perform(
          	get("/welcome")
          		.accept(MediaType.parseMediaType("application/html;charset=UTF-8")))
          	.andExpect(status().isOk())
          	.andExpect(content().contentType("application/html;charset=UTF-8"))
          	.andExpect(content().string("Welcome to Spring MVC"));
    }

     

     

    테스트 수행결과

     

    Flow2 - 뷰를 가진 컨트롤러 플로우

    Flow1에서는 브라우저에 표시할 텍스트가 컨트롤러에 직접 하드 코딩되었다. 그러나 이것은 좋은 습관이 아니다. 

    여기서는 컨트롤러에서 뷰로 리다이렉션이 된다. 

     

    Spring MVC 컨트롤러

    @Controller
    public class BasicController {
    
        @RequestMapping(value = "/welcome-view")
        public String welcomeView() {
            return "welcome";
        }
    }
    여기는 @ResponseBody 어노테이션이 없다. 따라서 Spring MVC는 반환된 문자열인 welcome을 뷰와 일치시키려고 시도한다. 

    View 생성

    src/main/webapp/WEB-INF/views/에 welcome.jsp를 작성한다. 

    <html>
    <head>
        <title>Welcome</title>
    </head>
    <body>
        <p>Welcome! This is Coming from a view - a JSP</p>
    </body>
    </html>

     

    View Resolver

    View Resolver는 View이름을 실제 JSP 페이지로 해석한다. 이것이 가능한 이유는 위에 Application Context를 설정시 빈으로 View Resolover를 빈으로 등록했기 때문인데 위의  dispatcher-servlet.xml에서 확인할 수 있다. 

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

    InternalResourceViewResolver 클래스가 prefix와 suffix가 붙은 파일들을 찾아서 해당하는 view이름을 가진 jsp파일과 매핑을 해준다. 이것 때문에 컨트롤러가 이름만 가지고 해당하는 jsp 파일을 찾아서 연결할 수 있게 되는 것이다. 

     

    실행화면

     

    단위 테스트

    단위 테스트도 Flow 1과 같은 방법으로 이루어지는데, 테스트 컨트롤러 설정과 테스트 코드 작성을 하면된다. 

     

    테스트 컨트롤러 설정

     @Before
     public void setUp() {
     	this.mockMvc = MockMvcBuilders.standaloneSetup(
     		new BasicController()
     	).setViewResolvers(viewResolver())
     	.build();
    
    }
    
    private ViewResolver viewResolver() {
    	InternalResourceViewResolver viewResolver =
    	new InternalResourceViewResolver();
    	viewResolver.setViewClass(JstlView.class);
    	viewResolver.setPrefix("/WEB-INF/views/");
    	viewResolver.setSuffix(".jsp");
    	return viewResolver;
    }

    setUp함수에서 어떤 mockMVC에게 어떤 Controller에 대해서 테스트를 할건지 지정해준다. 

    그리고 MockMVC의 경우 view를 실제로 실행하지 않기 때문에 가짜로 view Resolver를 만들어주어서 실제로 데이터 값이 view에 제대로 전달되는지의 확인정도만 가능하다. 

     

    테스트 메서드 작성

     

    @Test
    public void testWelcomeView() throws Exception {
    	this.mockMvc.perform(
    	get("/welcome-view")
    	    .accept(MediaType.parseMediaType("application/html;charset=UTF-8"))
    	.andExpect(view().name("welcome"));
    }
    위에서 view Resolver를 생성해줬기 때문에 view의 이름을 확인 할 수 있다. 

     

     

    Flow3 - 모델이 있는 View로 전환하는 컨트롤러 

    일반적으로 뷰를 생성하면 그 안에 데이터를 전달한다. Spring MVC에서는 모델을 사용해서 View에 데이터를 전달 할 수 있다.

     

    Spring MVC 컨트롤러

     

    @Controller
    public class BasicModelMapController {
    
        @RequestMapping("/welcome-model-map")
        public String modelAndMap(ModelMap modelMap) {
            modelMap.put("name","xyz");
            return "welcome-model-map";
        }
    }
    대부분이 앞의 내용과 동일하지만 ModelMap이라는 파라미터를 받아서 modelMap에 key-value 형태로 값을 넣을 수 있다. 이 model은 후에 View에서 사용이 가능하다.

     

    View 생성

    WEB-INF/views/ 밑에 welcome-model-map.jsp 을 생성한다. 

     

    <html>
    <head>
        <title>Welcome</title>
    </head>
    <body>
        <p>Welcome ${name}! This is Coming from a view - a JSP</p>
    </body>
    </html>
    ${name} : EL(Expression Language)를 사용해서 모델의 속성을 접근할 수 있다. 

     

    실행 화면

     

    단위테스트

     

    단위 테스트는 이전의 내용과 거의 동일하다. 테스트할 컨트롤러 설정사항은 같고 테스트 메서드에서 모델의 속성을 확인해주는 테스트코드만 추가하면 된다. 

     

    테스트 컨트롤러 설정

     

    - Flow2 와 동일 

     

    테스트 메서드 작성

    @Test
    public void testModelMap() throws Exception {
    	mockMvc.perform(
    	    get("/welcome-model-map")
    	    .accept(MediaType.parseMediaType("application/html;charset=UTF-8")))
    	.andExpect(model().attribute("name","xyz"))
    	.andExpect(view().name("welcome-model-map"));
    }
    model().attribute()를 통해서 model에 들어가는 attribute key 와 value를 확인할 수 있다. 

     

     

     

    Flow4- ModelAndView를 사용해 View로 전환하는 컨트롤러 

    이전 Flow에서는 View이름을 반환하고 뷰에 사용될 속성으로 모델을 채웠다. Spring MVC에서는 ModelAndView라는 객체를 사용해서 대체할 수 있다. 

     

    @Controller
    public class BasicModelViewController {
        @RequestMapping("/welcome-model-and-view")
        public ModelAndView welcome(ModelMap modelMap) {
            modelMap.put("name","abc");
            return new ModelAndView("welcome-model-and-view",modelMap);
        }
    }

    대부분은 Flow3과 동일하다. 대신 String으로 View의 이름을 Return해줬다면 이번에는 ModelAndView 클래스를 이용해서 Return을 해준다. 내용 자체는 앞의 내용과 동일하게 진행된다. 

     

    View 생성

     

    View 생성

    WEB-INF/views/ 밑에 welcome-model-and-view.jsp 을 생성한다. 

    <html>
    <head>
        <title>Welcome</title>
    </head>
    <body>
        <p>Welcome ${name}! This is Coming from a view - a JSP</p>
    </body>
    </html>

     

    실행화면

     

    단위 테스트

     

    Flow4는 Flow3와 내용상 같기 때문에 테스트 코드도 동일하게 적용할 수 있다. 내용은 생략하겠다. 

     

     

     

     

    Flow 5 - 폼이 있는 뷰로 전환하는 컨트롤러

    사용자의 입력을 받기 위한 간단한 폼을 만드는 방법을 살펴보자.

     

    • 간단한 POJO를 만든다. 
    • 2개의 컨트롤러 메서드를 작성한다. 하나는 폼을 표시하는 메서드 하나는 폼에 입력된 세부사항을 받을 메서드이다. 
    • 폼으로 간단한 뷰를 작성한다. 

    명령 또는 폼 백엔드 오브젝트 작성

     

    public class User {
    
        private String guid;
        private String name; 
        private String userId;
        private String password;
        private String password2;
        
        // 생성자
        // Getter/ Setter
        // toString
    
    }

    이 클래스에는 어노테이션 또는 스프링 관련 매핑이 없다. 모든 빈은 폼 백엔드 객체로 작동할 수 있다. 

    폼에 이름, 사용자 ID 및 암호를 캡처할 것이다. 암호확인 필드인 password2 와 고유 식별자 필드인 guid가 있다. 

    간결하게 하기 위해 생성자, getter,setter, toString은 표시하지 않았다. 

     

    폼을 표시하는 컨트롤러 메서드

     

    일단 컨트롤러 하나를 생성한다.

    @Controller
    public class UserController {
    
        private Log logger = LogFactory.getLog(UserController.class);
    
    }

     

     

    폼이 있는 뷰를 불러오는 메서드

     

    @RequestMapping(value = "/create-user",method = RequestMethod.GET)
    public String showCreateUserPage(ModelMap model) {
        model.addAttribute("user", new User());
        return "user";
    }
    /create-user 라는 URI를 매핑하는데 HTTP메서드 중 GET방식으로 들어온 것을 매핑한다. 

     

    폼 제출을 처리하는 메서드

    @RequestMapping(value = "/create-user", method = RequestMethod.POST)
    public String addToUser(User user) {
        logger.info("user detail : "+ user);
        return "redirect:list-users";
    }
    
    @RequestMapping(value = "/list-users", method = RequestMethod.GET)
    public String listUser() {
        return "list-users";
    }

    /create-user 라는 URI를 매핑하는데 HTTP메서드 중 POST방식으로 들어온 것을 매핑한다. 

    여기서는 폼 백엔드 객체를 파라미터로 사용하고 있다. Spring MVC는 폼의 값을 폼 백엔드 객체에 자동으로 바인딩한다.

    그후 return으로 redirect를 시켜서 다른 URL로 리다이렉팅한다.

     

     

    폼을 가진 뷰 생성

    <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
    <%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
    <%@taglib uri="http://www.springframework.org/tags/form" prefix="form"%>
    <%@taglib uri="http://www.springframework.org/tags" prefix="spring"%>
    
    <H1>Create User</H1>
    <form:form method="post" modelAttribute="user">
        <form:errors path="" cssClass="text-warning"/>
        <form:hidden path="guid" />
    
        <fieldset>
            <form:label path="name">Name</form:label>
            <form:input path="name" type="text" required="required" />
            <form:errors path="name" cssClass="text-warning"/>
        </fieldset>
    
        <fieldset>
            <form:label path="userId">User Id</form:label>
            <form:input path="userId" type="text" required="required" />
            <form:errors path="userId" cssClass="text-warning"/>
        </fieldset>
    
        <fieldset>
            <form:label path="password">Password</form:label>
            <form:input path="password" type="password" required="required" />
            <form:errors path="password" cssClass="text-warning"/>
        </fieldset>
    
        <fieldset>
            <form:label path="password2">Reenter Password</form:label>
            <form:input path="password2" type="password" required="required" />
            <form:errors path="password2" cssClass="text-warning"/>
        </fieldset>
    
        <input class="btn btn-success" type="submit" value="Submit" />
    </form:form>
    

     

     

    단위 테스팅

     

    Flow 6에서 밸리데이션을 추가할 때 단위 테스팅에 대해 설명할 것이다. 

     

     

    Flow 6 - 벨리데이션이 있는 간단한 폼의 컨트롤러

    이전 Flow에서 데이터는 추가 했지만 데이터에 Validation에 대해서 체크하지 않았다. 

    폼 내용의 유효성을 검증하는데 자바스크립트로 작성할 수는 있지만, 서버에서 벨리데이션을 수행하는 것이 안전하다. 

    이 Flow에서는 앞서 스프링 MVC를 사용해 서버 측에서 작성한 폼에 벨리데이션을 추가할 수 있다. 

     

    하이버네이트 벨리데이터 의존성 추가

    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-validator</artifactId>
        <version>5.0.2.Final</version>
    </dependency>

    빈에 대한 검증

    @Size(min = 6, message = "Enter atleast 6 characters")
    private String name;
    
    @Size(min = 6, message = "Enter atleast 6 characters")
    private String userId;
    
    @Size(min = 8, message = "Enter atleast 8 characters")
    private String password;
    
    @Size(min = 8, message = "Enter atleast 8 characters")
    private String password2;
    @Size 어노테이션을 통해 필드의 최소 글자수를 지정해주고 이 벨리데이션을 통과하지 못하면 메시지 속성의 텍스트가 벨리데이션 에러메시지로 사용된다. 

    이런 Validation을 검증하는 어노테이션은 아래의 어노테이션을 포함해 더 많이 존재한다. 

    @NotNull : Null이면 안된다. 

    @Size(min= , max= ): 최대 글자수와 최소 글자수를 지정한다. 

    @Past: 과거의 날짜이어야 한다. 

    @Future: 미래의 날짜이어야 한다.

    @Pattern: 제공된 정규 표현식과 일치해야한다. 

    @Max: 필드의 최대값

    @Min: 필드의 최소값

    이제 컨트롤러 메서드를 가져와 폼을 벨리데이트 할 수 있다. 

     

    @RequestMapping(value = "/create-user-with-validation", method = RequestMethod.POST)
    public String addTodo(@Valid User user, BindingResult result) {
        if (result.hasErrors()) {
            return "user";
        }
    
        logger.info("user details " + user);
        return "redirect:list-users"
    }
    Flow5의 메서드와는 달리 파라미터로 @Valid라는 어노테이션과  BindingResult를 추가로 받았는데 @Valid 어노테이션이 사용되면 Spring MVC는 빈을 validate한다. 벨리데이션 결과는 BindingResult 인스턴스 결과에서 사용할 수있다. 

     

    커스텀 벨리데이션

     @AssertTrue(message = "Password fields don't match")
     private boolean isValid() {
         return this.password.equals(this.password2);
     }
    @AssertTrue는 벨리데이션이 실패했을 경우 표시할 메시지이다. 이러한 방법으로 다중 필드의 복잡한 벨리데이션 로직을 구현할 수 있다. 

    단위 테스팅 

     

    이부분에 대한 단위 테스팅은 벨리데이션 에러를 확인하는데 중점을 둔다. 여기서는 4개의 벨리데이션 에러를 확인하는 빈 폼에 대한 테스트를 작성할 것이다. 

     

    컨트롤러 설정 및 테스틑 메서드 

    이전과 마찬가지의 방식으로 MockMvc를 사용하여 가짜 객체를 만들어서 setup해준다. 

    public class UserValidationControllerTest {
    
        private MockMvc mockMvc;
    
        @Before
        public void setUp() {
            mockMvc = MockMvcBuilders.standaloneSetup(
                    new UserValidationController())
                    .setViewResolvers(viewResolver())
                    .build();
        }
    
        private ViewResolver viewResolver() {
            InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
            viewResolver.setViewClass(JstlView.class);
            viewResolver.setPrefix("/WEB-INF/views/");
            viewResolver.setSuffix(".jsp");
            return viewResolver;
        }
    
        @Test
        public void basicTest_WithAllValidationErrors() throws Exception {
            this.mockMvc.perform(
                    post("/create-user-with-validation")
                    .accept(MediaType.APPLICATION_JSON))
                    .andExpect(status().isOk())
                    .andExpect(model().errorCount(4))
                    .andExpect(model().attributeHasFieldErrors("user" ,"name","Size"));
        }
    }

    .post()로 지정된 uri에 대한 포스트 요청을 작성한다. 

    model().errorCount(4) : 모델에 4개의 벨이데이션 에러가 있는지 점검한다.

    model().attributeHasFieldErrorCode("user","name","Size"): user 속성에 Size라는 벨리데이션 에러가 있는 name 필드가 있는지 확인한다.  

     

    이렇게 간단한 6개의 mvc에 대한 예제에 대해서 작성했다. 

     

    해당소스는

    https://github.com/bosuksh/springbase/tree/master/springpuremvc

    에서 확인 할 수 있다. 

     

     

    출처: Mastering Spring 5.0

    댓글