Study/Spring

[Spring 5 Recipes] Spring 5 Recipes 3장 정리 #2

꼽냥이 2021. 9. 30. 22:46
반응형

스프링 MVC

2. @RequestMapping 으로 요청 매핑

DispatcherServlet 은 사용자로부터 웹 요청을 받는 즉시 @Controller 가 달린 적합한 컨트롤러 클래스에 요청의 처리를 위임한다. 처리를 위임하는 과정은 컨트롤러 클래스의 핸들러 메소드에 선언된 다양한 @RequestMapping 설정 내용에 따라 좌우된다.

 

핸들러는 컨텍스트 경로 (웹 어플리케이션 컨텍스트의 배포 경로) 에 대한 상대 경로 및 서블릿 경로 (디스패처 서블릿 매핑 경로) 에 맞게 연결된다. 예를 들어 "http://localhost:8080/court/welcome" URL 로 요청이 들어오면, 컨텍스트 경로는 /court, 서블릿 경로는 없으니 (CourtWebApplicationInitializer 에서 서블릿 경로는 "/" 로 선언했다) /welcome 에 맞는 핸들러를 찾는다.

 

@RequestMapping 을 가장 쉽게 사용하는 방법은 핸들러 메소드에 직접 어노테이션을 붙여 URL 패턴을 기재하는 것이다. 디스패처 서블릿은 이 어노테이션에 기재한 요청 URL 과 가장 맞는 핸들러에 처리를 위임하게 된다.

@Controller
public class MemberController {
	
    private Memberservice memberService;
    
    // Constructor
    
    @RequestMapping("/member/add")
    public String addMember(Model model) {
    	model.addAttribute("member", new Member());
        model.addAttribute("guests", memberService.list());
        return "memberList";
    }
    
    @RequestMapping(value = {"/member/remove", "/member/delete"}, 
        method = RequestMethod.GET)
    public String removeMember(@RequestParam("memberName") String memberName) {
    	memberService.remove(memberName);
        return "redirect:";
    }
}

removeMember() 메소드의 @RequestMapping value 속성 값처럼 URL 을 여럿 할당하게 되면, /member/remove, /member/delete 둘 다 이 메소드에 매핑된다. 별다른 설정이 없으면 @RequestMapping 으로 명시한 메소드들은 모두 HTTP GET 방식의 요청에 매핑되는 것으로 간주한다.

 

클래스에 따른 요청 매핑

@RequestMapping 은 핸들러 메소드 뿐 아니라 컨트롤러 클래스 자체에도 붙일 수 있다. 4장에서 조금 더 자세히 설명하겠지만, @RequestMapping 을 클래스 레벨에 붙이게 되면 그 클래스에 속한 전체 메소드에 어노테이션을 일일이 붙이지 않아도 된다. 또 메소드마다 나름의 @RequestMapping 을 적용함으로써 URL 을 좀 더 세세하게 적용할 수 있다. URL 을 폭넓게 매치하려면 @RequestMapping 에 와일드카드(*) 를 사용한다.

 

다음 예제 코드에서는 URL 와일드카드를 어떻게 쓰는지, 핸들러 메소드에서 URL 을 세세하게 매치하기 위해 설정하는 방법 등을 볼 수 있다.

@Controller
@RequestMapping("/member/*")
public class MemberController {
	
    private final MemberService memberService;
    
    // Constructor
    
    @ReqeustMapping("add")
    public String addMember(Model model) { ... }
    
    @RequestMapping(value = {"remove", "delete"}, method = RequestMethod.GET)
    public String removeMember(@RequestParam("memberName") String memberName) { ... }
    
    @RequestMapping("display/{member}")
    public String displayMember(@PathVariable("member" String member, Model model) {
    	model.addAttribute("member", memberService.find(member).orElse(null));
        return "member";
    }
    
    @RequestMapping
    public void memberList() {}
    
    public void memberLogic(String memberName) {}
}

컨트롤러 클래스 레벨의 @RequestMapping 에 와일드카드가 포함된 URL 이 있으므로 /member/ 로 시작하는 URL 은 모두 이 컨트롤러의 핸들러 메소드 중 하나로 매핑된다.

 

HTTP GET /member/add 를 요청하면 addMember() 메소드가, HTTP GET /member/remove (/member/delete) 를 요청하면 removeMember() 메소드가 각각 호출된다.

displayMember() 메소드의 @RequestMapping 설정에 쓴 "{PathVariable}" 형태의 독특한 표기법은 URL 안에 포함된 값을 핸들러 메소드의 입력 매개변수값으로 전달한다는 의미이다. 그래서 핸들러 메소드의 인자에도 @PathVariable("user") String user 라고 선언되어 있다. 요청 URL 이 /member/display/jdoe 이면 핸들러 메소드의 인자는 "jdoe" 로 설정될 것이다. 핸들러에서 요청 객체를 살펴볼 필요가 없으니 꽤 편리한 기능이고 특히 RESTful Web Service 를 설계할 때 유용한 기법이다.

 

memberList() 메소드에도 @RequestMapping 이 있지만 특정된 URL 이 없다. 클래스 레벨에 이미 /member/* 라는 와일드카드를 쓴 URL 이 있으므로 다른 메소드에 걸리지 않은 요청은 모두 이 메소드에 매핑된다. 예를 들어, /member/abcdef 또는 /member/random 등 어떤 URL 에 대한 요청도 모두 이 메소드를 거쳐 간다. 반환값은 void 이기 때문에, 앞선 포스팅에서 설명했듯 memberList() 메소드는 자신의 URL 주소와 같은 뷰 (즉, member) 로 제어권을 넘긴다.

memberLogic() 메소드는 @RequestMapping 이 달려있지 않은, 이 클래스 내부에서만 사용되는 유틸리티 메소드이다.

 

HTTP 요청 메소드에 따른 요청 매핑

기본적으로 @RequestMapping 은 모든 HTTP 요청 메소드를 처리할 수 있지만 GET, POST 요청을 하나의 메소드가 받아 처리할 일은 거의 없을 것이다. HTTP 메소드 별로 요청을 따로 처리하려면 @RequestMapping 에 HTTP 요청 메소드를 명시한다.

@RequestMapping(value = "processUser", method = RequestMethod.POST)
public String submitForm() { ... }

HTTP 요청 메소드는 HEAD, GET, POST, PUT, DELETE, PATCH, TRACE, OPTIONS, CONNECT 까지 모두 여덟 가지이다. 그러나 이런 HTTP 요청 메소드는 웹 서버나 요청하는 클라이언트 측에서 모두 지원이 가능해야 하기 때문에 MVC 컨트롤러 영역 밖의 문제이다. HTTP 요청의 대다수가 GET/POST 인 사실을 감안하면 그 밖의 메소드를 지원할 일은 드문 편이다.

 

스프링 MVC 는 널리 사용되는 HTTP 요청 메소드를 위해 다음과 같이 전용 어노테이션을 지원한다.

요청 메소드 어노테이션
POST @PostMapping
GET @GetMapping
DELETE @DeleteMapping
PUT @PutMapping

이러한 편의성 어노테이션은 모두 @RequestMapping 을 특정한 것으로, 핸들러 메소드를 조금 더 간결하게 코딩할 수 있도록 한다.

@PostMapping("processUser")
public String submitForm() { ... }

 

위에서 @RequestMapping 에 지정한 URL 을 살펴보면 .html 이나 .jsp 같은 파일 확장자는 명시되어 있지 않다. 이는 MVC 설계 사상이 충실히 반영된 좋은 모양새라 할 수 있다.

컨트롤러는 HTML, JSP 같은 특정 뷰 구현 기술에 얽매이지 않고 작성이 되어야 한다. 컨트롤러에서 논리 뷰를 반환할 때 URL 에 확장자를 넣지 않은 것도 이 때문이라 할 수 있다.

 

3. 핸들러 인터셉트로 요청 가로채기

서블릿 명세에 정의된 서블릿 필터를 적용하면 웹 요청을 서블릿이 처리하기 전후에 각각 전처리와 후처리를 할 수 있다.

 

스프링 MVC 에서 웹 요청은 핸들러 인터셉터로 가로채 전처리/후처리를 적용할 수 있다. 핸들러 인터셉터는 스프링 웹 어플리케이션 컨텍스트에 구성하기 때문에 컨테이너의 기능을 자유롭게 활용할 수 있고, 내부에 선언된 모든 빈을 참조할 수 있다. 또한 인터셉터는 특정 URL 에 대한 요청에만 적용되도록 매핑할 수도 있다.

 

핸들러 인터셉터는 HandlerInterceptor 인터페이스를 구현해야 하며 preHandle(), postHandle(), afterCompletion() 세 개의 콜백 메소드를 구현한다. preHandle() 과 postHandle() 메소드는 핸들러가 요청을 처리하기 직전과 직후에 각각 호출된다. postHandle() 메소드는 핸들러가 반환한 ModelAndView 객체에 접근할 수 있기 때문에 그 안에 들어있는 모델 속성을 꺼내 조작할 수 있다. afterCompletion() 메소드는 요청 처리가 모두 끝난 (뷰 렌더링까지 완료된) 이후 호출된다.

 

스프링의 커스텀 핸들러 인터셉터는 다음과 같이 작성할 수 있다.

public class MeasurementInterceptor implements HandlerInterceptor {
	
    @Override
    public boolean preHandle(HttpServletRequest request,
        HttpServletResponse response, Object handler) throws Exception {
        long startTime = System.currentTimeMillis();
        request.setAttribute("startTime", startTime);
        return true;
    }
    
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, 
        Object handler, ModelAndView modelAndView) throws Exception {
        long startTime = (Long) request.getAttribute("startTime");
        request.removeAttribute("startTime");
        
        long endTime = System.currentTimeMillis();
        modelAndView.addObject("handlingTime", endTime - startTime);
    }
    
    @Override
    public void afterCompletion(HttpServletReqeust request, HttpServletResponse response,
        Object handler, Exception e) throws Exception {}
}

디스패처 서블릿은 preHandle() 메소드가 true 를 반환해야만 요청 처리를 계속 진행하며 그 외에는 이 메소드 선에서 요청 처리가 끝났다 판단하고 유저에게 곧장 Response 객체를 반환한다. preHandle() 메소드는 요청 처리를 시작한 시각을 재서 요청 속성에 보관한다. postHandle() 메소드는 요청 속성에 보관된 시작 시각을 읽고 현재 시각과 비교해 요청 처리에 소요된 시간을 모델에 추가한 뒤 뷰에 넘긴다. afterCompletion() 메소드는 할 일이 없으니 비워둔다.

 

더보기

NOTE.
자바에서 인터페이스를 구현할 때는 원치 않는 메소드까지 모조리 구현해야 하는 규칙이 있다. 그래서 인터페이스를 구현하는 대신 인터셉터 어댑터 클래스를 상속받아 사용하는 것이 더 나을 수 있다. 인터셉터 어댑터는 인터페이스에 선언된 메소드를 모두 기본 구현한 클래스이므로 필요한 메소드만 오버라이드해서 사용할 수 있다.

 Java 8 부터는 인터페이스 작성 시 메소드에 default 키워드를 붙여 구현 코드를 직접 작성할 수 있다. 이렇게 작성한 메소드는 구현 클래스에서 오버라이드하지 않아도 된다.

 

위의 소스 코드에서 인터셉터 인터페이스를 구현하지 않고 인터셉터 어댑터 클래스를 상속하면 아래와 같이 작성할 수 있다.

public class MeasurementInterceptor extends HandlerInterceptorAdapter {
	
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
        Object handler) throws Exception { ... }
        
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response,
        Object handler, ModelAndView modelAndView) throws Exception { ... }
}

 

위와 같이 작성한 커스텀 인터셉터는 다음과 같이 WebMvcConfigurer 인터페이스를 구현한 구성 클래스에서 addInterceptors() 메소드를 오버라이드함으로써 추가할 수 있다.

@Configuration
public class InterceptorConfiguration implements WebMvcConfigurer {
	
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    	registry.addInterceptor(measuremeantInterceptor());
    }
    
    @Bean
    public MeasurementInterceptor measurementInterceptor() {
    	return new MeasurementInterceptor();
    }
}

 

HandlerInterceptor 는 기본적으로 모든 @Controller 에 적용되지만 원하는 컨트롤러만 선택적으로 적용할 수도 있다. 네임스페이스 및 자바 구성 클래스를 이용해 인터셉트를 특정 URL 에 매핑할 수 있다.

@Configuration
public class InterceptorConfiguration implements WebMvcConfigurer {
	
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    	registry.addInterceptor(measuremeantInterceptor());
        registry.addInterceptor(summaryReportInterceptor())
            .addPathPatterns("/reservationSummary*");
    }
    
    @Bean
    public MeasurementInterceptor measurementInterceptor() {
    	return new MeasurementInterceptor();
    }
    
    @Bean
    public SummaryReportInterceptor summaryReportInterceptor() {
    	return new SummaryReportInterceptor();
    }
}

위의 소스 코드에서 인터셉터 빈 summaryReportInterceptor 를 추가했다. measurementInterceptor 와 비슷하지만 /reservationSummary URL 에 매핑된 컨트롤러에만 인터셉터 로직이 적용되는 점이 다르다. 인터셉터를 등록할 때 매핑 URL 을 지정하면 되는데, 기본적으로 앤트 스타일의 표현식으로 작성하며 addPathPatterns() 메소드의 인자로 전달한다. 역으로 제외할 URL 이 있으면 같은 표현식을 excludePathPatterns() 메소드 인자로 지정한다.

 

#Reference.

 

스프링 5 레시피(4판)

이 책은 스프링 5에 새로 탑재된 기능 및 다양한 구성 옵션 등 업데이트된 프레임워크 전반을 실무에 유용한 해법을 제시하는 형식으로 다룹니다. IoC 컨테이너 같은 스프링 기초부터 스프링 AOP/A

www.hanbit.co.kr