개발/JAVA

[자바웹개발워크북] 4-2. 스프링 Web MVC

독코더 2023. 1. 14. 11:13
반응형

/*

 *

 * 자바웹개발워크북의 내용을 정리하기 위한 포스팅입니다.

 * 스프링 Web MVC, 예외처리

 */

 

1. 스프링 Web MVC(이하 스프링MVC)의 특징

스프링MVC는 이름에서 알 수 있듯이 Web MVC패턴으로 구현된 구조입니다.

따라서 기본적인 흐름은 이전에 다루었던것과 같고 컨트롤러, 뷰, 모델 등의 용어들 역시 그대로 사용됩니다.

스프링MVC가 기존 구조에 약간의 변화를 주는 부분은 다음과 같습니다.

■ Front-Controller패턴을 이용해서 모든 흐름의 사전/사후처리를 가능하도록 설계된점

■ 어노테이션을 적극적으로 활용해서 최소한의 코드로 많은 처리가 가능하도록 설계된점

■ HttpServletRequest/HttpServletResponse 를 이용하지 않아도 될 만큼 추상화된 방식으로 개발가능

 

스프링MVC의 전체 흐름은 다음과 같습니다. 

 

2. DispatcherServlet과 FrontController

스프링MVC에서 중요한 점은 모든 요청이 반드시 DispatcherServlet이라는 존재를 통해서 실행된다는 사실입니다.

(객체지향에선 모든흐름이 하나의 객체를 통해 진행되는패턴을 퍼사드패턴이라고하고, 웹구조에서는 FrontController패턴이라고 합니다.)

스프링MVC에서는 DispatcherServlet라는 객체가 프론트컨트롤러의 역할을 수행합니다.

프론트컨트롤러가 사전/사후에 대한 처리를 하게 되면 중간에 매번 다른 처리를 하는 부분만 별도로 처리하는 구조를 만들게 됩니다.

이 부분을 컨트롤러라고 하는데 @Controller를 이용해서 처리합니다.

 

3. 스프링MVC 사용하기

앞에서 'spring-webmvc'라이브러리는 이미 추가 되었으므로 스프링MVC관련 설정만 추가하겠습니다.

 

프로젝트 내 WEB-INF폴더에 servlet-context.xml파일을 생성합니다.

기존의 root-context.xml을 이용할수도있지만 일반적으로 3티어구조를 분리하듯이 별도의 설정파일을 이용하는것이 일반적입니다.

 

프로젝트 내 webapp폴더아래 resources라는 폴더를 생성해 둡니다.

여기엔 정적파일들(html, css, js, 이미지 등)을 서비스하기 위한 경로입니다.

 

servlet-context.xml의 설정

작성한 servlet-context.xml은 <mvc:..라는 접두어로 설정에 활용해서 다음과 같이 작성합니다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
    
    <mvc:annotation-driven></mvc:annotation-driven>
    
    <mvc:resources mapping="/resources/**" location="/resources/"></mvc:resources>
    
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/views/"></property>
        <property name="suffix" value=".jsp"></property>
    </bean>

</beans>

 

<mvc:annotaion-driven>는 스프링 MVC설정을 어노테이션 기반으로 처리한다는 의미와 스프링MVC의 여러객체들을 자동으로 스프링의 빈으로 등록하게 하는 기능을 합니다.

<mvc:resources>는 이미지나 html파일같은 정적인 파일의 경로를 지정합니다. '/resources'경로로 들어오는 요청은 정적파일을 요구하는 것이라고 여겨 스프링MVC에서 처리하지 않는다는 의미입니다.

location속성값은 webapp폴더에 만들어둔 폴더를 의미합니다.

servlet-context.xml에는 InternalResourceViewResolver라는 이름의 클래스로 빈이 설정되었습니다.

 InternalResourceViewResolver는 스프링MVC에서 제공하는 뷰를 어떻게 결정하는지에 대한 설정을 담당합니다.

prefix와 suffix의 내용을 보면 MVC에서 사용했던 WEB-INF경로와 .jsp확장자를 지정했는데, 이를 통해서 '/WEB-INF/...jsp'와 같은 경로 설정을 생략할 수 있게됩니다.

 

web.xml의 DispatcherServlet설정

스프링MVC를 실행하려면 프론트컨트롤러 역할을 하는 DispatcherServlet을 설정해야합니다.

이작업은 web.xml을 이용해서 다음과 같이 처리합니다.

<?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">
    
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/root-context.xml</param-value>
    </context-param>
    
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    
    <servlet>
        <servlet-name>appServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/servlet-context.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>appServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

web.xml에 <servlet>, <servlet-mapping>설정이 추가되었습니다.

<servlet>은 DispatcherServlet을 등록하는데 DispatcherServlet이 로딩할때 servlet-context.xml을 이용하도록 설정합니다.

load-on-startup설정의 경우 톰캣 로딩시에 클래스를 미리 로딩해 두기 위한 설정입니다.

<servlet-mapping>은 DispatcherServlet이 모든 경로의 요청에 대한 처리를 담당하기 때문에 '/'로 지정합니다.

 

스프링과 스프링MVC의 설정은 이것이 전부입니다.

반복사용으로 익숙해지는것이 중요합니다.

 

4. 스프링MVC 컨트롤러

스프링MVC 컨트롤러는 전통적인 자바의 클래스 구현방식과 여로모로 상당히 다릅니다.

과거의 많은 프레임워크들은 상속이나 인터페이스를 기반으로 구현되는 방식을 선호했다면 스프링MVC의 컨트롤러는 이런 점이 다릅니다.

■ 상속이나 인터페이스를 구현하는 방식을 사용하지 않고 어노테이션만으로 처리가 가능

■ 오버라이드 없이 필요한 메소드들을 정의

■ 메소드의 파라미터를 기본 자료형이나 객체자료형을 마음대로 지정

■ 메소드의 리턴타입도 void, String, 객체 등 다양한 타입을 사용할 수 있음

 

스프링MVC 컨트롤러 예제를 만들어 보겠습니다.

@Controller
@Log4j2
public class SampleController {

    @GetMapping("/hello")
    public void hello() {
        log.info("hello ...");
    }
}

@Controller는 해당 클래스가 스프링MVC에서 컨트롤러 역할을 한다는것을 의미하고 스프링의 빈으로 처리되기 위해 사용합니다.

@GetMapping은 Get방식으로 들어오는 요청을 처리하기위해 사용합니다.

위의 코드의 경우 '/hello'라는 경로를 호출할때 동작하게 됩니다.

 

servlet-context.xml의 component-scan

controller패키지에 존재하는 컨트롤러 클래스들을 스프링으로 인식하기 위해서는 해당 패키지를 스캔해서 @Controller 어노테이션이 추가된 클래스들의 객채들을 스프링의 빈으로 설정되게 만들어야 합니다.

servlet-context.xml에 component-scan을 다음과 같이 작성합니다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
    
    <mvc:annotation-driven></mvc:annotation-driven>
    
    <mvc:resources mapping="/resources/**" location="/resources/"></mvc:resources>
    
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/views/"></property>
        <property name="suffix" value=".jsp"></property>
    </bean>
    
    <context:component-scan base-package="org.zerock.springex.controller"/>

</beans>

 

@RequestMapping와 파생 어노테이션

스프링 컨트롤러에서 가장 많이 사용하는 어노테이션은 @RequestMapping입니다.

@RequestMapping은 말그대로 '특정한 경로의 요청을 지정'하기 위해서 사용합니다.

이 어노테이션은 컨트롤러 클래스의 선언부에도 사용할 수 있고, 컨트롤러의 메소드에도 사용할 수 있습니다.

(서블릿중심의 MVC의 경우 servlet을 상속받아서 doGet()/doPost()와 같은 제한적인 메소드를 오버라이드해서 사용했지만,

스프링MVC의 경우 하나의 컨트롤러에서 여러 경로의 호출을 모두 처리할 수 있습니다.)

예를들어 '/todo/list'와 '/todo/register'를 하나의 컨트롤러로 작성해본다면 아래와 같이 사용할 수 있습니다.

@Controller
@RequestMapping(value = "/todo")
@Log4j2
public class TodoController {

    @RequestMapping(value = "/list")
    public void list() {
        log.info("todo list...");
    }

    @RequestMapping(value = "/register", method = RequestMethod.GET)
    public void registerGET() {
        log.info("GET todo register...");
    }

}

@RequestMapping에는 method라는 속성을 이용해서 GET/POST방식을 구분해서 처리했지만,

스프링4버전 이후에는 @GetMapping, @PostMapping 어노테이션이 추가되면서 GET/POST방식을 처리할 수 있게됐습니다.

 

예를들어 todo등록 시 GET방식으로 '/todo/register'를 이용해 입력화면을 보여주고, POST방식으로 처리를 할 경우라면,

@Controller
@RequestMapping(value = "/todo")
@Log4j2
public class TodoController {

    @RequestMapping(value = "/list")
    public void list() {
        log.info("todo list...");
    }

    //@RequestMapping(value = "/register", method = RequestMethod.GET)
    @GetMapping("/register")
    public void registerGET() {
        log.info("todo register...");
    }

    @PostMapping("/register")
    public void registerPOST() {
        log.info("POST todo register...");
    }

}

 

5. 파라미터 자동 수집과 변환

파라미터 자동수집은 간단히 말해서 DTO나 VO등을 메소드의 파라미터로 설정하면 자동으로 전달되는 HttpServletRequest의 파라미터들을 수집해주는 기능입니다.

단순히 문자열만이 아니라 숫자, 배열, 리스트, 첨부파일도 가능합니다.

파라미터 수집은 다음과 같은 기준으로 동작합니다.

■ 기본 자료형의 경우는 자동으로 형 변환처리가 가능합니다.

■ 객체 자료형의 경우는 setXXX()의 동작을 통해서 처리됩니다.

■ 객체 자료형의 경우는 생성자가 없거나 파라미터가 없는 생성자가 필요(java Beans)

 

■ 단순 파라미터의 자동수집

@GetMapping("/ex1")
public void ex1(String name, int age) {
    log.info("ex1....");
    log.info("Name: " + name);
    log.info("age: " + age);
}

ex1()에는 문자열 name과 int 타입의 age를 선언했습니다. 

브라우저에 'ex1?name=AAA&age=22'로 호출되면 자동으로 처리되는것을 확인할 수 있습니다.

 

■ @RequestParam

스프링MVC의 파라미터는 기본적으로 요청에 전달된 파라미터 이름을 기준으로 동작하지만 간혹 파라미터가 전달되지 않으면 문제가 발생할 수 있습니다. 이런 경우 @RequestParam 어노테이션을 고려해 볼 수 있습니다.

@RequestMapping("/ex2")
public void ex2(@RequestParam(name = "name", defaultValue = "AAA")String name,
                @RequestParam(name = "age", defaultValue = "20")int age) {
    log.info("ex2...");
    log.info("Name: " + name);
    log.info("age: " + age);

}

@RequestParam은 defaultValue라는 속성이 있어서 기본값을 지정할 수 있습니다.

 

■ Formatter를 이용한 파라미터의 커스텀 처리

기본적으로 HTTP는 문자열로 데이터를 전달하기 때문에 컨트롤러는 문자열을 기준으로 특정한 클랫의 객체로 처리하는 작업이 진행됩니다. 이때 문제가 되는 타입이 바로 날짜 관련 타입입니다.

브라우저에서 '2023-01-14'같은 형태의 문자열을 Date나 LocalDate, LocalDateTime등으로 변환하는 작업에서 에러가 발생합니다.

@GetMapping("/ex3")
public void ex2(LocalDate dueDate) {
    log.info("ex3...");
    log.info("dueDate :" + dueDate);
}

브라우저에 '/ex3?dueDate=2023-01-14'로 호출한다면 에러가 발생합니다.

String타입을 java.time.LocalDate로 변환할 수 없기 때문입니다.

이런 경우에는 특정한 타입을 처리하는 Formatter라는것을 이용할 수 있습니다.

controller패키지에 formatter패키지를 생성하고 LocalDateFormatter 클래스를 작성합니다.

Formatter인터페이스는 parse()와 print()메소드가 존재합니다.

public class LocalDateFormatter implements Formatter<LocalDate> {

    @Override
    public LocalDate parse(String text, Locale locale) {
        return LocalDate.parse(text, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
    }

    @Override
    public String print(LocalDate object, Locale locale) {
        return DateTimeFormatter.ofPattern("yyyy-MM-dd").format(object);
    }
}

Formatter를 servlet-context.xml에 적용하기 위해서는 조금 복잡한 과정이 필요합니다.

FormattingConversionServiceFactoryBean객체를 스프링의 빈으로 등록해야하고 이안에 LocalDateFormatter를 추가해야합니다.

우선, servlet-context.xml에 다음 코드를 추가해줍니다.

<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="formatters">
        <set>
            <bean class="org.zerock.springex.controller.formatter.LocalDateFormatter"/>
        </set>
    </property>
</bean>

conversionService라는 빈을 등록한 후에는 스프링MVC를 처리할 때 <mvc:annotation-driven>에 이를 이용한다고 지정해야 합니다.

<mvc:annotation-driven conversion-service="conversionService" />

설정이 끝났다면 브라우저에 '/ex3?dueDate=2023-01-14' 경로를 호출해도 오류없이 파라미터를 수집할 수 있습니다.

 

■ 객체자료형의 파라미터 수집

기본자료형과 달리 객체자료형을 파라미터로 처리하기 위해서는 객체가 생성되고 setXXX()을 이용해야 합니다.

Lombok을 이용하면 @Setter, @Data를 이용해서 간단히 처리할 수 있습니다.

프로젝트에 dto패키지를 추가하고 TodoDTO클래스를 추가합니다.

@ToString
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class TodoDTO {

    private long tno;

    private String title;
    
    private LocalDate dueDate;
    
    private boolean finished;
    
    private String writer; // 새로 추가됨
}

그다음, TodoController의 '/todo/register'를 POST방식으로 처리하는 메소드에 TodoDTO를 파라미터로 적용해봅니다.

@PostMapping("/register")
public void registerPOST(TodoDTO todoDTO) {
    log.info("POST todo register...");
    log.info(todoDTO);
}

자동으로 형 변환이 되기때문에 TodoDTO와 같이 다양한 타입의 멤버변수들의 처리가 자동으로 이루어지게 됩니다.

WEB-INF/views에는 todo폴더를 생성하고 register.jsp를 추가합니다.

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <form action="/todo/register" method="post">
        <div>
            Title: <input type="text" name="title">
        </div>
        <div>
            DueDate: <input type="date" name="dueDate" value="2022-01-14">
        </div>
        <div>
            writer: <input type="text" name="writer">
        </div>
        <div>
            finished: <input type="checkbox" name="finished">
        </div>
        <div>
            <button type="submit">Register</button>
        </div>
    </form>
</body>
</html>

 

■ Model이라는 특별한 파라미터

스프링MVC는 기본적으로 웹MVC와 동일한 방식이므로 모델이라고 부르는 데이터를 JSP까지 전달할 필요가 있습니다.

순수한 서블릿방식에서는 request.setAttribute()를 이용해서 데이터를 담아 JSP까지 전달했지만 스프링MVC에서는 Model이라는 객체를 이용해서 처리할 수 있습니다.

Model에는 addAttribute()라는 메소드를 이용해서 뷰에 전달할 '이름'과 '값'을 지정할 수 있습니다.

@GetMapping("/ex4")
public void ex4(Model model) {
    log.info("--------------");
    model.addAttribute("message", "hello World");
}

ex4.jsp는 다음과 같이 구성합니다.

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<html>
<head>
    <title>Title</title>
</head>
<body>
 <h1>${message}</h1>
    <h1><c:out value="${message}"></c:out></h1>
</body>
</html>

Model에 담긴 데이터는 내부적으로 HttpServletRequest의 setAttribute()와 동일한 동작을 수행하기 때문에 JSP에서는

EL을 이용해서 별다른 처리없이 바로 사용할 수 있습니다.

 

■ Java Beans와 @ModelAttribute

스프링MVC의 컨트롤러는 특이하게도 파라미터로 getter/setter를 이용하는 JavaBeans의 형식의 사용자 정의 클래스가 파라미터인 경우에는 자동으로 화면까지 객체를 전달합니다. 파라미터로 TodoDTO를 받는경우 다음과 같이 작성할 수 있습니다.

@GetMapping("/ex4_1")
public void ex4Extra(TodoDTO todoDTO, Model model) {
    log.info(todoDTO);
}

이런 경우 JSP에서는 별도의 처리없이 ${todoDTO}를 이용할수 있게됩니다.

만일 자동으로 생성된 변수명 todoDTO라는 이름외에 다른 이름을 사용하고 싶다면 @ModelAttribute()를 지정할 수 있습니다.

위 코드에 @ModelAttribute()를 적용한다면 다음과 같습니다.

@GetMapping("/ex4_1")
public void ex4Extra(@ModelAttribute("dto") TodoDTO todoDTO, Model model) {
    log.info(todoDTO);
}

 

이 경우 JSP에서 ${dto}와 같은 이름의 변수로 처리할 수 있습니다.

 

■ RedirectAttributes와 리다이렉션

POST방식으로 어떤 처리를 하고 Redirect를 해서 GET방식으로 특정한 페이지로 이동하는 PRG패턴을 처리하기 위해서

스프링MVC에서는 RedirectAttributes라는 특별한 타입을 제공합니다.

RedirectAttributes 역시 Model과 마찬가지로 파라미터로 추가해 주면 자동으로 생성되는 방식으로 개발할때 사용할 수 있습니다.

RedirectAttributes에서 중요한 메소드는 다음과 같습니다.

▶ addAttribute(키, 값) : 리다이렉트할 때 쿼리스트링이 되는 값을 지정

▶ addFlashAttribute(키, 값) : 일회용으로만 데이터를 전달하고 삭제되는 값을 지정

addAttribute()로 데이터를 추가하면 리다이렉트할 URL에 쿼리스트링으로 추가되고, addFlashAttribute()를 이용하면 URL에 보이지는 않지만, JSP에서는 일회용으로 사용할 수 있습니다.

@GetMapping("/ex5")
public String ex5(RedirectAttributes redirectAttributes) {
    
    redirectAttributes.addAttribute("name", "ABC");
    redirectAttributes.addFlashAttribute("result", "success");
    
    return "redirect:/ex6";
}

@GetMapping("/ex6")
public void ex6() {
    
}

 

스프링MVC에서 리다이렉트하기 위해서는 'redirect:'라는 접두어를 붙여서 문자열로 반환합니다.

브라우저에서 '/ex5'를 호출하면 서버에서는 다시 '/ex6'경로를 호출하라고 알려주게 됩니다.

 

ex6.jsp파일에서는 addFlashAttribute()로 전달된 'result'라는 이름의 데이터를 확인할 수 있도록 작성해줍니다.

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <h1>ADD FLASH ATTRIBUTE : ${result}</h1>
</body>
</html>

'/ex5'를 호출했을때 최종화면은 '/ex6'를 다시 호출합니다. 이때 addAttribute()로 추가한 name데이터는 쿼리스트링이 됩니다.

반면에 addFlashAttribute()로 추가한 result는 눈에 보이지 않지만 JSP에서 사용한것을 볼 수 있습니다.

result데이터는 일회용이므로 새로고침하면 result변수가 더이상 존재하지 않게됩니다.

 

■ 다양한 리턴타입

스프링MVC는 파라미터를 자유롭게 상황에 맞춰 지정할 수 있듯이, 컨트롤러 내에 선언하는 메소드의 리턴 타입도 다양하게 사용할 수 있습니다. 주로 사용하는 리턴 타입은 다음과 같습니다.

void, 문자열, 객체나 배열, 기본자료형, ResponseEntity

일반적으로 화면이 따로 있는 경우에는 주로 void나 문자열을 이용하고, 나중에 JSON타입을 활용할땐 객체나 ResponseEntity타입을 주로 사용합니다.(뒤에서 REST 방식을 처리할때 사용하겠습니다.)

 

void는 컨트롤러의 @RequestMapping값과 @GetMapping등 메소드에서 선언된 값을 그대로 뷰의 이름으로 사용하게 됩니다.

void는 주로 상황에 관계없이 동일한 화면을 보여주는 경우에 사용합니다.

 

문자열은 상황에 따라서 다른 화면을 보여주는 경우에 사용합니다. 문자열의 경우 특별한 접두어를 사용할 수 있는데,

▶ redirect : 리다이렉션을 이용하는 경우

▶ forward : 브라우저의 URL은 고정하고 내부적으로 다른 URL로 처리하는 경우

특별한 경우가 아니라면 'forward:'를 이용하는 경우는 없고 주로 'redirect:'만을 이용합니다.

 

■ 스프링MVC에서 주로 사용하는 어노테이션들

 

컨트롤러 선언부에 사용하는 어노테이션

@Controller : 스프링 빈의 처리됨을 명시

@RequestController : REST방식의 처리를 위한 컨트롤러임을 명시

@RequestMapping : 특정한 URL패턴에 맞는 컨트롤러인지 명시

 

메소드 선언부에 사용하는 어노테이션

@GetMapping/@PostMapping/@DeleteMapping/@PutMapping... : HTTP전송방식에 따라 해당 메소드를 지정하는 경우에 사용

@RequestMapping : GET/POST방식 모두를 지원하는 경우에 사용

@ResponseBody : REST방식에서 사용

 

메소드의 파라미터에 사용하는 어노테이션

@RequestParam : Request에 있는 특정한 이름의 데이터를 파라미터로 받아서 처리하는 경우에 사용

@PathVariable : URL경로의 일부를 변수로 삼아서 처리하기 위해서 사용

@ModelAttribute : 해당 파라미터는 반드시 Model에 포함되어서 다시 뷰로 전달됨을 명시(주로 기본자료형, Wrapper클래스, 문자열)

기타 : @SessionAttribute, @Vaild, @RequestBody 등

 

6. 스프링MVC의 예외처리

스프링MVC 컨트롤러에서 발생하는 예외를 처리하는 가장 일반적인 방식은 @ControllerAdvice를 이용하는것입니다.

@ControllerAdvice는 컨트롤러에서 발생하는 예외에 맞게 처리할 수 있는 기능을 제공하는데 @ControllerAdvice가 선언된 클래스 역시 스프링의 빈으로 처리됩니다.

controller패키지에 exception패키지를 추가하고 CommonExceptionAdvice클래스를 생성합니다.

@ControllerAdvice
@Log4j2
public class CommonExceptionAdvice {
    
}

 

■ @ExceptionHandler

@ControllerAdvice의 메소드들에는 특별하게 @ExceptionHandler라는 어노테이션을 사용할 수 있습니다.

이를 이용해서 전달되는 Exception객체들을 지정하고 메소드의 파라미터에서는 이를 이용할 수 있습니다.

@ControllerAdvice의 동작을 보기위해 고의로 예외를 발생하는 코드를 추가합니다.

@GetMapping("/ex7")
public void ex7(String p1, int p2) {
    log.info("p1 ............." + p1);
    log.info("p2.............." + p2);
}

브라우저에서 'ex7?p1=AAA&p2=BBB'를 호출하면 NumberFormatException이 발생할것입니다.

이를 해결하기 위해서 CommonExceptionAdvice에 NumberFormatException을 처리하도록 지정해줍니다.

@ControllerAdvice
@Log4j2
public class CommonExceptionAdvice {
    
    @ResponseBody
    @ExceptionHandler(NumberFormatException.class)
    public String exceptNumber(NumberFormatException numberFormatException) {
        log.info("-------------------------------");
        log.error(numberFormatException.getMessage());
        
        return "NUMBER FORMAT EXCEPTION";
    }

}

@ExceptionHandler를 가진 모든 메소드는 해당 타입의 예외를 파라미터로 전달 받을 수 있습니다.

 

exceptNumber()는 @RequestBody를 이용해서 만들어진 문자열을 그대로 브라우저에 전송하는 방식을 이용했습니다.

같은 예외가 발생하더라도 이제 결과는 다음과 같이 달라질것입니다.

 

■ 범용적인 예외처리

개발을 하다보면 어디선가 문제가 발생하고 이를 자세하 메시지로 확인하고 싶은 경우가 많습니다.

이를 위해서 예외처리의 상위 타입인 Exception타입을 처리하도록 구성하면 다음과 같이 작성할 수 있습니다.

@ResponseBody
@ExceptionHandler(Exception.class)
public String exceptionCommon(Exception exception) {
    log.error("--------------------------------");
    log.error(exception.getMessage());
    
    StringBuffer buffer = new StringBuffer("<ul>");
    
    buffer.append("<li>" + exception.getMessage() + "</li>");
    
    Arrays.stream(exception.getStackTrace()).forEach(stackTraceElement -> {
        buffer.append("<li>" + stackTraceElement + "<li>");
    });
    buffer.append("<ul>");
    
    return buffer.toString();
}

exceptCommon()은 Exception타입을 처리하기 때문에 사실상 거의 모든 예외를 처리하는 용도로 사용할 수 있습니다.

개발할때는 예외 메시지가 자세하게 출력되는것이 좋을때가 많기 때문에 디버깅용으로 사용하고,

배포할 때는 별도의 에러 페이지를 만들어서 사용하는것이 좋습니다.

 

404 에러 페이지와 @ResponseStatus

서버내부에서 생긴 문제가 아니라 시작부터 잘못된 URL을 호출하면 404예외가 발생하면서 톰캣이 보내는 메시지를 보게 됩니다.

@ControllerAdvice에 작성하는 메소드에 @ResponseStatus를 이용하면 404상태에 맞는 화면을 별도로 작성할 수 있습니다.

CommonExceptionAdvice에 새로운 메소드를 다음과 같이 추가합니다.

@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public String notFound(){
    return "custom404";
}

WEB-INF/views폴더에는 custom404.jsp를 추가합니다.

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <h1>OOPS! 페이지를 찾을 수 없습니다!!</h1>
</body>
</html>

web.xml에서는 DispatcherServlet의 설정을 조정해야 합니다.

<servlet>태그 안에 <init-param>을 추가하고 throwExceptionIfNoHandlerFound라는 파라미터설정을 추가해야 합니다.

<servlet>
    <servlet-name>appServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/servlet-context.xml</param-value>
    </init-param>
    
    <init-param>
        <param-name>throwExceptionIfNoHandlerFound</param-name>
        <param-value>true</param-value>
    </init-param>
    
    <load-on-startup>1</load-on-startup>
</servlet>

 

반응형