본문 바로가기

책 읽기/스프링 책 읽기

스프링 책 읽기(Spring in action) - 5. 스프링 MVC 시작하기(1)

스프링 MVC 프레임워크.

* 마치 '골드버그  게임' 처럼, 다양한 내리막, 시소 등의 자애물을 통과하는 것 처럼, 스프링은 '요청'을 '디스패치 서블릿', '핸들러 매핑', '컨트롤러, ' 리졸버' 등으로 이동시킴

골드버그 게임

 

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

- 웹브라우저에서 링크 클릭 혹은 폼을 서브밋할 때, 요청을 처리하기 위한 작업이 수행됨. 

요청 처리 정보

1. 요청이 브라우저에서 떠나면서  사용자의 요구 내용 전달 -> DispatcherServelet

2. DispatcherServlet에서, 다음 요청이 가야할 곳을 찾기위해 핸들러 매핑에게 도움 요청 -> 컨트롤러 선택

3. 선택된 컨트롤러에, DispatcherServlet가 요청을 보냄 -> 요청은 페이로드로 떨굼 -> 이후 컨트롤러의 처리 시간동안 대기 -> 브라우저용 정보로 반환(모델) -> 이 정보들을, 사용자가 보기 편한 형태로 바꾸는 뷰가 필요(Like JSP)

4. 모델과 뷰의 이름을 확인한 후, 함께 DispatcherServelet으로 요청을 반환

5. DispatcherServlet 뷰리졸버에게 전달 받은, 뷰의 이름(논리적으로 주어진?....)과 실제로 구현된 뷰를 매핑해줄 것을  요청 -> 렌더링을 위한 뷰가 어떤 것인지 알게 됨

6. 일반적인 JSP로 모델 데이터를 전달해주는 뷰의 구현

7. 모델데이터로 렌더링 후, 응답 객체로 전달

 

스프링 MVC 설정

DispatcherServelet 설정

* 스프링 MVC의 핵심, 과거에는 web.xml을 사용했으나, 이제는 Java로 설정 가능

package cafe.config;

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

public class CafeWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer{

    @Override
    // DispatcherServelet을 '/'에 매핑
    // 매핑 되기 위한, 하나 혹은 여러개의 패스를 지정한다, '/'은 애플리케이션으로 오는 모든 요청을 처리
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }

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

    @Override
    // 설정 클래스를 명시
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { WebCofig.class };
    }


}

 

DispatcherServlet과 ContextLoaderLisnter란?

위의 DispatcherServletgetRootConfigClasses()와 getServletConfigClasses() 메소드를 이해하려면, 서블릿 리스너 사이의 관계에 대한 이해가 필요함

* DispatcherServlet은 컨트롤러, 뷰리졸버, 핸들러 매핑 등 웹 컴포넌트가 포함된 빈을 로딩할 것으로 생각

* 반면, ContextLoaderListner는 애플리케이 내의 그 외 빈을 로딩할 것으로 생각(중간계층, 데이터 계층 컴포넌트)

 

AbstractAnnotationConfigDispatcherServletInitializerDispatcherServletContextLoaderListner를 생성...  ==> web.xml의 대안.. 아파치 톰캣7, 서블릿 3.0 이상에서만 지원

* getServletConfigClasses()에 리턴된 @Configuration 클래스는 DispatcherServlet에 매핑

* getRootConfigClasses()에 리턴된 @Configuration 클래스는 ContextLoaderListner에 매핑

 

스프링 MVC 활성화 하기

package cafe.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration
@EnableWebMvc   // Spring MVC 활성화
@ComponentScan("cafe.web")  // 컴포넌트 스캐닝 활성화
public class WebConfig extends WebMvcConfigurerAdapter {

    // 뷰 리졸버 생성
    public ViewResolver viewResolver(){
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        // 기본 뷰 리졸버 = JSP를 뷰로 사용할 때 사용

        resolver.setPrefix("/WEB-INF/views/"); // 프리픽스 지정
        resolver.setSuffix(".jsp"); // 서픽스 지정
        resolver.setExposeContextBeansAsAttributes(true); // 스프링 빈을, ApplicationContext에서 접근 가능하게 설정 ${..} 플레이스 홀더 사용 가능
        return resolver;
    }

    @Override
    // 정적 콘텐츠 처리  설정
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable(); // 고정적인 리소스에 대한 요청을 직접 처리하지 않고, 서블릿 컨테이너의 디폴트 서블릿 전달 요청
    }
}

* WebConfig : 컴포넌트 스캔을 통해, 컨트롤러를 가져올 수 있다, 이로인해 내부에 컨트롤러를 선언할 필요가 없음, 

// 컴포넌트 스캐닝을 하는 기본 RootConfig
@Configuration
@ComponentScan(basePackages = {"spittr"},
    excludeFilters = {
            @ComponentScan.Filter(type= FilterType.ANNOTATION, value = EnableWebMvc.class)
    })
public class RootConfig {
}

* RootConfig : 해당 클래스는 컴포넌트 스캐닝을  한다는 점 이외에는, 다음에 다뤄보기로 함.

 

간단한 컨트롤러  작성하기

@RequestMapping : 컨트롤러가 처리할, 요청의 종류를 정의하는 애너테이션 ( value로 요청 패스를, method로 요청 메소드를..)

@Controller // @Component에 기반을 둔, 정형화된 애너테이션, 컴포넌트 스캔 가능
public class HomeController {

    @RequestMapping(value = "/", method = RequestMethod.GET)
    // 요청패스 "/"에 관하여, Http Get 요청을 함
    public String home() {
        return "home"; // 뷰의 이름이 home
        // view는, webConfig에서 /WEB-INF/views 프리픽스, .jsp 서픽스를 지정했으므로, /WEB-INF/views/home.jsp 가 해당 뷰
    }
}


==> 혹은 이렇게 클래스 레벨 요청으로 처리 가능
@Controller
@RequestMapping({"/", "/hompage"}) // "/"와 "/hompage"에 관하여 클레스 레벨로 처리
public class HomeController {

    @RequestMapping(method = RequestMethod.GET)
    public String home() {
        return "home"; // 뷰의 이름이 home
      }
}

 

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

==> /WEB-INF/views/home.jsp

간단한 home화면

 

 

 

컨트롤러 테스팅

일반적인, POJO 테스트로는, 리턴되는 home이 뷰에관한 리턴이 아닌, 단순히 String인 "home"이 리턴된다.

따라서, MVC를 위한 컨트롤러 테스팅이 필요함

public class HomeControllerTest {

    @Test
    public void testHomePage() throws Exception{
        HomeController controller = new HomeController();
        MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
		// mockMvc 관련 셋업... 
        
        mockMvc.perform(get("/")).andExpect(view().name("home"));
        // "/"요청에 관한 get을 수행하는 뷰의 이름이, "home"인지 체크
    }
}

 

뷰에 모델 데이터 전달하기

* 데이터에 엑세스를 위한 저장소를 정의해주어야 함. 실제 구현은 나중에

public interface CoffeeRepository {
    List<Coffee> findCoffees(int min, int max);
}

-> 커피를 찾기 위한, 저장소 인터페이스( 최소가격, 최대가격)

public class Coffee {
    private final String name;
    private final int  price;
    ... 
    생성자, getter 생략
}

==> Coffee 클래스, 커피의 id번호, 이름, 샷, 물, 우유의 양 저장

Coffee 데이터를 처리할  수 있는 컨트롤러 작성

@Controller
@RequestMapping("/coffees")
public class CoffeeController {

    private CoffeeRepository coffeeRepository;

    @Autowired // coffeeReposi
    public CoffeeController(CoffeeRepository coffeeRepository){
        this.coffeeRepository = coffeeRepository;
    }

    @RequestMapping(method = RequestMethod.GET)
    public String coffees(Model model){
        model.addAllAttributes(
                coffeeRespository.findCoffees(1000, 5000)
        );
        // 해당 List<Coffee>를 모델에 추가함
        return "coffees"; 
        // 뷰 : /WEB-INF/views/coffees.jsp
    }
}

==> Model에 직접, 뷰로 전달할 데이터를 넣어줄 수 있다 - 위 코드에선, 1000~5000원 사이의 커피 목록

테스팅

 @Test
    public void testCoffee() throws Exception {
        List<Coffee> expectedCoffees = createCoffeeList(20);

        // mock 저장소
        CoffeeRepository mockRepository =
                mock(CoffeeRepository.class);
        when(mockRepository.findCoffees(1000, 5000)).thenReturn(expectedCoffees);

        CoffeeController controller = new CoffeeController(mockRepository);

        // mock 스프링 MVC
        MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller)
                .setSingleView(new InternalResourceView("/WEB-INF/views/coffees.jsp"))
                .build();

        mockMvc.perform(get("/coffees"))
                .andExpect(view().name("coffees"))
                .andExpect(model().attributeExists("coffeeList"))
                .andExpect(model().attribute("coffeeList",
                        hasItems(expectedCoffees.toArray())));
                        // List<Coffee>이므로 coffeeList로 인식함.. 
    }

    private List<Coffee> createCoffeeList(int count) {
        List<Coffee> coffees = new ArrayList<Coffee>();

        for(int i = 0; i < count; i++){
            coffees.add(new Coffee("Coffee" + i, 2500));
        }

        return  coffees;
    }

* mock 저장소를 만들고, 이를 mock 스프링 MVC에서 가져온 모델이 일치하는지 테스트

coffees.jsp

<body>
    <c:forEach items="${coffeeList}" var="coffee" >
        <li id ="coffee_<c:out value="coffee.name"/>">
            <div class="coffeePrice">
                <c:out value="${coffee.price}"/>
            </div>
        </li>
    </c:forEach>
</body>

==> 모델로 받은 데이터 coffeeList를 간단하게 사용하는 jsp파일 // coffeeList를 그대로 사용 가능(플레이스 홀더)