1. 스프링 Web MVC의 특징

  • 스프링 Web MVC는 웹 MVC 패턴으로 구현된 구조
  • 기본적인 흐름과 Controller, View, Model 등의 용어도 그대로 사용
  • 스프링 MVC가 기존 구조와 다른 부분
    • Front-Controller패턴을 이용해서 모든 흐름의 사전 / 사후 처리 가능
    • 어노테이션을 적극 활용, 최소한의 코드로 많은 처리 가능
    • HttpServletRequest / HttpServletResponse 이용하지 않아도 될 만큼 추상화된 방식으로 개발 가능
  • 스프링 MVC의 전체 흐름

 

 1) DispatcherServlet과 Front Controller

  • 스프링 MVC의 모든 요청은 반드시 DispatcherServlet이라는 존재를 통해서 실행됨
  • Front-Controller 패턴을 이용하면 모든 요청이 반드시 하나의 객체를 지나서 처리되어 모든 공통적인 처리를 Front-Controller에서 처리 가능
  • 스프링 MVC에서 DispatcherServlet이라는 객체가 Front-Controller 역할 수행
  • Front-Controller가 사전 / 사후에 대한 처리를 하게 되면 중간에 매번 다른 처리를 하는 부분만 별도로 처리하는 구조를 만들게 됨(이 부분이 Controller이고 @Controller를 이용해서 처리)

 


실습

 1) 스프링 MVC 사용하기

  - 프로젝트의 webapp 폴더 > resources 폴더 생성: 이미지나 html 파일 같은 정적인 파일을 서비스하기 위한 경로

  - webapp 폴더 > WEB-INF > servlet-context.xml 생성

<!-- servlet-context.xml -->
<?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 설정을 어노테이션 기반으로 처리한다는 의미, 스프링 MVC의 여러 객체들을 자동을 ㅗ스프링의 Bean으로 등록하게 하는 기능 -->
    <mvc:annotation-driven></mvc:annotation-driven>

    <!-- 이미지나 html 파일 같은 정적인 파일 경로 지정 -->
    <!-- "/resources" 경로로 들어오는 요청은 정적인 파일을 요구하는 것으로 판단하고 스프링 MVC에서 처리하지 않는다는 의미 -->
    <mvc:resources mapping = "/resources/**" location = "/resources/"></mvc:resources>

    <!-- InternalResourceViewResolver는 스프링 MVC에서 제공하는 View를 어떻게 결정하는지에 대한 설정 담당 -->
    <bean class = "org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name = "prefix" value = "/WEB-INF/views/"></property>
        <property name = "suffix" value = ".jsp"></property>
    </bean>

</beans>

 

 2) web.xml의 DispatcherServlet 설정

  • 스프링 MVC 실행을 위해 Front-Controller 역할을 하는 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">
    
    ...
    
    <!-- DispatcherServlet 로딩 시 servlet-context.xml을 이용하도록 설정 -->
    <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>

        <!-- Tomcat 로딩 시 클래스를 미리 로딩해두기 위한 설정 -->
        <load-on-startup>1</load-on-startup>
    </servlet>

    <!-- DispatcherServlet이 모든 경로의 요청에 대한 처리를 담당하기 때문에 '/'fh wlwjd -->
    <servlet-mapping>
        <servlet-name>appServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    
</web-app>

 


실습

 2) 스프링 MVC Controller

  - 스프링 MVC Controller의 다른 점

  • 상속이나 인터페이스를 구현하는 방식을 사용하지 않고 어노테이션만으로 처리 가능
  • 오버라이드 없이 필요한 메서드 정의
  • 메서드의 파라미터를 기본 자료형이나 객체 자료형을 마음대로 지정
  • 메서드의 리턴타입도 void, String, 객체 등 다양한 타입 사용 가능

  - org.zerock.springex 프로젝트 내에 controller 패키지 추가 > SampleController 클래스 추가

// SampleController
package org.zerock.springex.controller;

import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

// 해당 클래스가 MVC에서 Controller 역할을 한다는 의미, 스프링의 Bean으로 처리되기 위해 사용
@Controller
@Log4j2
public class SampleController {
  
  // GET 방식으로 들어오는 요청을 처리하기 위해 사용("/hello"라는 경로를 호출할 때 동작)
  @GetMapping("/hello")
  public void hello() {
    log.info("hello........" );
  }
  
}

 

 3) servlet-context.xml의 component-scan

  • controller 패키지에 존재하는 Controller 클래스들을 스프링으로 인식하기 위해 @Controller 어노테이션이 추가된 클래스들의 객체들이 스프링의 Bean으로 설정되게 만들어야 함
  • servelt-context.xml의 component-scan을 다음과 같이 적용
<!-- servlet-context.xml -->
<?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"
       xmlns:context = "http://www.springframework.org/schema/context"
       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
                http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

   ...
   
   <context:component-scan base-package = "org.zerock.springex.controller" />

</beans>
  • "/hello" 경로의 화면을 간단하게 작성(webapp > WEB-INF > views 폴더 생성 > hello.jsp 파일 생성)
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <h1>Hello JSP</h1>
</body>
</html>

 

 4) @RequestMapping과 파생 어노테이션들

  • @RequestMapping은 특정 경로의 요청을 지정하기 위해 사용
  • 클래스 선언부에도 사용할 수 있고, Controller의 메서드에도 사용할 수 있음
  • Servlet 중심의 MVC에서는 Servlet을 상속받아서 doGet() / doPost()와 같은 제한적인 메서드를 오버라이드해서 사용했지만, 스프링 MVC의 경우 하나의 Controller를 이용해서 여러 경로의 호출 처리 가능
  • org.zerock.springex > controller 패키지 > TodoController 작성
// TodoController
package org.zerock.springex.controller;

import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

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

  @RequestMapping("/list")
  public void list() {
    log.info("tood list..........");
  }

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

/todo/list 경로의 화면을 만들지 않아서 화면을 띄우는 것은 오류가 나지만, 로그에 list() 메서드가 실행되어 메세지가 뜬 것을 확인

  • @RequestMapping을 이용하는 것만으로 여러 Controller를 하나의 클래스로 묶을 수 있고, 각 기능마다 메서드 단위로 설계할 수 있게 되어 많은 양의 코드 줄일 수 있음

 

  • 스프링 4버전 이후에는 @GetMapping / @PostMapping 어노테이션으로 GET/ POST 방식 구분해서 처리 가능
  • 예를 들어, "/todo/register"는 GET 방식으로 화면을 보여주고, POST 방식으로 처리하므로 다음과 같이 설계
// TodoController
package org.zerock.springex.controller;

import ...

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

  // 클래스 선언부에서 RequestMapping의 value가 "/todo"이고 list() 메서드에서 ReuestMapping의 value가 "/list"이므로
  // 최종 경로는 "/todo/list"가 됨
  @RequestMapping("/list")
  public void list() {
    log.info("tood list..........");
  }

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

 

 

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

  • 파라미터 자동 수집은 DTO, VO 등을 메서드의 파라미터로 설정하면 자동으로 전달되는 HttpServletRequest의 파라미터들을 수집해주는 기능
  • 단순 문자열만이 아니라 숫자, 배열, 리스트, 첨부 파일도 가능
  • 파라미터 수집 동작 기준
    • 기본 자료형의 경우 자동으로 형 변환처리 가능
    • 객체 자료형의 경루 setXXX()를 통해 처리
    • 객체 자료형의 경우 생성자가 없거나 파라미터가 없는 생성자가 필요(Bean)

 


실습

 3) 단순 파라미터의 자동 수집

  - SampleController에서의 예시

// SampleController
package org.zerock.springex.controller;

import ...

// 해당 클래스가 MVC에서 Controller 역할을 한다는 의미, 스프링의 Bean으로 처리되기 위해 사용
@Controller
@Log4j2
public class SampleController {

  ...

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

}

  - 주소를 "http://localhost:8080/ex1?name=AAA&age=16"로 설정하면 자동으로 name은 문자열 AAA로, age는 숫자 16으로 파라미터를 수집해와서 로그에 출력

 

  - @RequestParam

  - 요청에 전달된 파라미터 이름을 기준으로 동작하지만, 간혹 파라미터가 전달되지 않으면 문제 발생할 수 있음

  - 이 때 @RequestParam이라는 어노테이션 고려

  - @RequestParam은 defaultValue라는 속성이 있어서 '기본값'을 지정할 수 있음

package org.zerock.springex.controller;

import ...

// 해당 클래스가 MVC에서 Controller 역할을 한다는 의미, 스프링의 Bean으로 처리되기 위해 사용
@Controller
@Log4j2
public class SampleController {

  ...

  @GetMapping("/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);
  }

}

  - 주소에 "http://localhost:8080/ex2"만 입력하고 파라미터를 주지 않아도 기본값으로 파라미터를 받아서 로그로 출력함

 

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

  - 기본적으로 HTTP는 문자열로 데이터를 전달하기 때문에 Controller는 문자열을 기준으로 특정 클래스의 객체로 처리하는 작업이 진행

  - 이때 날짜 등 특정 타입을 처리하는 Formatter 이용 가능

  - Formatter는 문자열을 포맷을 이용해서 특정 객체로 변환하는 경우 사용

  - controller 패키지 > formatter 패키지 작성 > LocalDateFormatter 클래스 작성

  - Formatter에는 parse()와 print() 메서드 존재

// LocalDateFormatter
package org.zerock.springex.controller.formatter;

import org.springframework.format.Formatter;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;

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 객체를 스프링의 Bean으로 등록하고 이 안에 작성한 LocalDateFormatter를 추가해야 함

<!-- servlet-context.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<beans ...>

    <!-- conversionService라는 Bean을 등록한 후에 스프링 MVC를 처리할 때 <mvc:annotation-driven에 이를 이용한다는 것을 명시해야 함 -->
    <mvc:annotation-driven conversion-service = "conversionService"></mvc:annotation-driven>
    
    ...
    
    <bean id = "conversionService" class = "org.springframework.format.support.FormattingConversionServiceFactoryBean">
        <property name = "Formatters">
            <set>
                <bean class = "org.zerock.springex.controller.formatter.LocalDateFormatter"/>
            </set>
        </property>
    </bean>

</beans>

  - ex3 경로에 날짜를 파라미터로 받는 메서드 추가후 날짜 형변환 정상 작동 여부 확인

// SampleController
package org.zerock.springex.controller;

import ...

@Controller
@Log4j2
public class SampleController {

 ...

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

}

  - 주소로 "http://localhost:8080/ex3?dueDate=2020-10-10"를 작성하면 날짜형태로 잘 받아서 로그에 출력함을 확인


 

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

  • 기본 자료형과 달리 객체 자료형을 파라미터로 처리하기 위해서는 객체가 생성되고 setXXX()을 이용해서 처리
  • Lombok을 활용하면 @Setter나 @Data를 이용하는 것이 간단
  • 프로젝트에 dto 패키지 추가 > TodoDTO 클래스 추가
// TodoDTO
package org.zerock.springex.dto;

import lombok.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.time.LocalDate;

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

  private Long tno;
  private String title;
  private LocalDate dueDate;
  private boolean finished;
  private String writer;

}

 

※ javax.validation을 사용하려면 build.gradle에 다음을 의존성으로 추가해야함

implementation group: 'javax.validation', name: 'validation-api', version: '2.0.1.Final'

 

  • TodoController의 '/todo/register'를 POST 방식으로 처리하는 메서드에 TodoDTO를 파라미터로 적용
package org.zerock.springex.controller;

import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.zerock.springex.dto.TodoDTO;

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

  @RequestMapping("/list")
  public void list() {
    log.info("tood list..........");
  }

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

  // TodoDTO를 파라미터로 적용
  // 자동으로 형변환되기 때문에 다양한 타입의 멤버 변수들의 처리가 자동으로 이루어짐
  @PostMapping("/register")
  public void registerPOST(TodoDTO todoDTO) {
    log.info("POST todo register...............");
    log.info(todoDTO);
  }

}

 

  • WEB-INF/views에 todo 폴더 생성 > register.jsp 파일 추가
<!-- register.jsp -->
<%@ taglib prefix ="form" uri = "http://www.springframework.org/tags/form" %>
<%@ 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 = "2023-01-17">
        </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>

  • 'localhost:8080/todo/register'에 들어가면 위와 같은 입력 화면이 나오고, 각 입력창에 데이터를 입력한 뒤, Register 버튼을 누르면 로그에 해당 데이터가 출력됨

 

 2) Model이라는 특별한 파라미터

  • 웹 MVC와 스프링 MVC 모두 Model이라고 부르는 데이터를 JSP에 전달해야함
  • Servlet 방식에서는 request.setAttribute()를 이용해서 데이터를 담아 JSP에 전달
  • 스프링 MVC 방식에서는 Model이라는 객체를 이용해서 처리 가능
  • Model에서 addAttribute() 메서드를 이용해서 View에 전달할 이름과 값을 지정
// SampleController
package org.zerock.springex.controller;

import ...

@Controller
@Log4j2
public class SampleController {

  ...

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

}

 

  • 데이터를 받을 ex4.jsp 파일도 WEB-INF/views 폴더에 생성
<!-- 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을 이용해 별다른 처리없이 사용 가능

 

  • 스프링 MVC의 Controller는 파라미터로 getter / setter를 이용하는 Java Beans의 형식의 사용자 정의 클래스가 파라미터인 경우 자동으로 화면까지 객체 전달
  • 예를 들어, 파라미터로 TodoDTO를 받는 경우, 다음과 같이 작성하고, JSP에서 ${todoDTO}를 통해 이용 가능
@GetMapping("/ex4_1")
public void ex4Extra(TodoDTO todoDTO, Model model) {
	log.info(todoDTO);
}
  • 이때 자동으로 생성된 todoDTO라는 이름 외에 다른 이름을 사용하고 싶으면 @ModelAttribute()를 사용하여 지정가능
@GetMapping("/ex4_1")
public void ex4Extra(@ModelAttribute("dto") TodoDTO todoDTO, Model model) {
	log.info(todoDTO);
}
  • 이렇게 하면 ${dto}로 사용가능

 

 3) RedirectAttributes와 리다이렉션

  • POST 방식으로 어떤 처리를 하고 Redirect 해서 GET 방식으로 특정 페이지 이동하는 PRG 패턴 처리를 위해 스프링 MVC에서는 RedirectAttributes라는 타입 사용
  • RedirectAttributes의 중요 메서드
    • addAttribute(키, 값): Redirect할 때 쿼리 스트링 값 적용
    • addFlashAttribute(키, 값): 일회용으로 데이터만 전달하고 삭제되는 값 지정
  • SampleController에서 ex5()는 RedirectAttributes를 파라미터로 추가하고 addAttribute와 addFlashAttribute를 사용해서 name과 result라는 이름을 가진 값들을 전달
// SampleController
package org.zerock.springex.controller;

import ...

@Controller
@Log4j2
public class SampleController {

  ...
  
  @GetMapping("/ex5")
  public String ex5(RedirectAttributes redirectAttributes) {
    redirectAttributes.addAttribute("name", "ABC");
    redirectAttributes.addFlashAttribute("result", "success");
    
    // redirect를 위해 "redirect:"라는 접두어를 붙여 문자열로 반환
    return "redirect:/ex6";
  }
  
  @GetMapping("/ex6")
  public void ex6() {
    
  }

}
  • ex5에서 넘겨준 값을 받은 ex6의 jsp 화면 작성
<!-- ex6.jsp -->
<%@ 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 호출하면 name과 result라는 이름을 가진 값들이 생성되어 ex6으로 redirect됨

  • ex6의 화면이 나오고 쿼리 스트링으로 준 name의 값 "ABC"가 주소창에 전달되어 있고, 화면에 ${result}로 출력한 result의 값인 "success"가 출력되어 있음

  • addFlashAttribute는 일회용으로 전달하고 사라지므로, 해당 페이지를 새로고침하면, "success"가 사라짐을 확인

 

 4) 다양한 리턴 타입

  • 스프링 MVC에서 Controller내에 선언하는 메서드의 리턴 타입을 다양하게 사용 가능
    • void: 화면이 따로 있는 경우, @RequestMapping값 과 @GetMapping 등 메서드에서 선언된 값을 그대로 View의 이름으로 사용, 주로 상황에 관계없이 동일한 화면을 보여줄 때 사용
    • 문자열: 화면이 따로 있는 경우, 상황에 따라 다른 화면 보여줄 때 사용, 다음과 같은 특별한 접두어 사용가능
      • redirect: 리다이렉션을 이용하는 경우, 주로 forward 대신 redirect 이용
      • forward: 브라우저의 URL은 고정하고 내부적으로 다른 URL로 처리하는 경우
    • 객체나 배열, 기본 자료형: JSON 타입 활용 시
    • ResponseEntity: JSON 타입 활용 시

 

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

  • Controller 선언부에 사용하는 어노테이션
    • @Controller: 스프링 Bean의 처리됨을 명시
    • @RestController: REST 방식의 처리를 위한 Controller임을 명시
    • @RequestMapping: 특정한 URL 패턴에 맞는 Controller인지를 명시
  • 메서드 선언부에 사용하는 어노테이션
    • @GetMapping / @PostMapping / @DeleteMapping / @PutMapping ...: HTTP 전송방식에 따라 해당 메서드를 지정하는 경우 사용, 일반적으로 @GetMapping과 @PostMapping을 주로 사용
    • @RequestMapping: GET / POST 방식 모두 지원하는 경우 사용
    • @ResponseBody: REST 방식에서 사용
  • 메서드의 파라미터에 사용하는 어노테이션
    • @RequestParam: Request에 있는 특정한 이름의 데이터를 파라미터로 받아서 처리하는 경우 사용
    • @PathVariable: URL 경로의 일부를 변수로 삼아서 처리하기 위해 사용
    • @ModelAttribute: 해당 파라미터는 반드시 Model에 포함되어 다시 View로 전달됨을 명시(주로 기본 자료형이나 Wrapper 클래스, 문자열에 사용)
    • 기타: @SessionAttribute, @Valid, @RequestBody 등

 

 

 3. 스프링 MVC의 예외 처리

  • 스프링 MVC에서 예외를 처리하는 가장 일반적인 방법은 @ControllerAdvice를 이용하는 것
  • @ControllerAdvice는 Controller에서 발생하는 예외에 맞게 처리할 수 있는 기능 제공
  • @ControllerAdvice가 선언된 클래스 역시 스프링의 Bean으로 처리됨
  • 실습을 위해 controller 패키지 > exception 패키지 > CommonExceptionAdvice 클래스 작성
// CommonExceptionAdvice
package org.zerock.springex.controller.exception;

import lombok.extern.log4j.Log4j2;
import org.springframework.web.bind.annotation.ControllerAdvice;

@ControllerAdvice
@Log4j2
public class CommonExceptionAdvice {
}

 

 1) @ExceptionHandler

  • @ControllerAdvice의 메서드들에는 특별하게 @ExceptionHandler라는 어노테이션 사용 가능
  • 이를 이용해서 전달되는 Exception 객체들을 지정하고 메서드의 파라미터에서 이를 이용 가능
  • 고의로 예외를 발생시키는 코드 작성하여 실험
// SampleController
package org.zerock.springex.controller;

import ...
@Controller
@Log4j2
public class SampleController {

  ...

  // p1에는 문자열이, p2에는 숫자가 전달되어야 함
  @GetMapping("/ex7")
  public void ex7(String p1, int p2) {
    log.info("p1........" + p1);
    log.info("p2........" + p2);
  }

}
  • 해당 코드를 작성하고, p2에 숫자가 아닌 문자열을 쿼리 스트링으로 전달해주면 예외가 발생
  • "localhost:8080/ex7?p1=AAA&p2=BBB"를 주면 BBB는 int형이 아니므로 에러 코드 400이 발생

  • 해결을 위해 CommonExceptionAdvice에 NumberFormatException을 처리하도록 지정
// CommonExceptionAdvice
package org.zerock.springex.controller.exception;

import lombok.extern.log4j.Log4j2;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

@ControllerAdvice
@Log4j2
public class CommonExceptionAdvice {

  // 문자열이나 JSON 데이터를 그대로 전송할 때 사용되는 어노테이션
  @ResponseBody
  // @ExceptionHandler를 가진 모든 메서드는 해당 타입의 예외를 파라미터로 전달받을 수 있음
  @ExceptionHandler(NumberFormatException.class)
  // exceptNumber()는 @ResponseBody를 이용해서 만들어진 문자열을 그대로 브라우저에 전송하는 방식 이용
  public String exceptNumber(NumberFormatException numberFormatException) {
    log.error("--------------------------------");
    log.error(numberFormatException.getMessage());

    return "NUMBER FORMAT EXCEPTION";
  }
}

 

 2) 범용적인 예외처리

  • 예외 처리의 상위 타입인 Excpetion 타입을 처리하도록 구성
// CommonExceptionAdvice
package org.zerock.springex.controller.exception;

import lombok.extern.log4j.Log4j2;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.Arrays;

@ControllerAdvice
@Log4j2
public class CommonExceptionAdvice {

  // 문자열이나 JSON 데이터를 그대로 전송할 때 사용되는 어노테이션
  @ResponseBody
  // @ExceptionHandler를 가진 모든 메서드는 해당 타입의 예외를 파라미터로 전달받을 수 있음
  @ExceptionHandler(Exception.class)
  // exceptCommon은 Exception 타입을 처리하여 사실상 거의 모든 예외를 처리하는 용도로 사용 가능
  public String exceptCommon(Exception exception) {
    log.error("--------------------------------");
    log.error(exception.getMessage());

    // <ul>로 시작하는 buffer 문자열 작성
    StringBuffer buffer = new StringBuffer("<ul>");

    // 예외 처리 메세지가 발생할 때마다 <li>와 함께 <ul></ul> 안에 리스트 형태로 해당 메세지를 추가
    buffer.append("<li>" + exception.getMessage() + "</li>");

    // 에러가 났을 때, 현재의 함수나 메서드 명도 같이 출력하여 더 자세히 디버깅할 수 있도록 함 
    Arrays.stream(exception.getStackTrace()).forEach(stackTrackElement -> {
      buffer.append("<li>" + stackTrackElement + "</li>");
    });
    
    // 마지막에는 리스트 형식을 끝내도록 </ul> 추가
    buffer.append("</ul>");

    return buffer.toString();
  }
}

 

 3) 404 에러 페이지와 @ResponseStatus

  • 서버 내부가 아닌 시작부터 잘못된 URL을 호출할 때 404 예외 발생
  • @ControllerAdvice에 작성하는 메서드에 @ResponseStatus를 이용하면 404상태에 맞는 화면을 별도로 작성 가능
// CommonExceptionAdvice
package org.zerock.springex.controller.exception;

import ...

@ControllerAdvice
@Log4j2
public class CommonExceptionAdvice {

  ...

  // 404 에러 대비
  @ExceptionHandler(NoHandlerFoundException.class)
  @ResponseStatus(HttpStatus.NOT_FOUND)
  public String notFound() {
    return "custom404";
  }
}
  • custom404의 페이지를 jsp 파일로 작성
<!-- custom404 -->
<%@ 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라는 파라미터 설정 추가
<!-- web.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>

    ...
    
    <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>

        <!-- 404에러 처리용으로 추가 -->
        <init-param>
            <param-name>throwExceptionIfNoHandlerFound</param-name>
            <param-value>true</param-value>
        </init-param>
		<!-- 여기까지 -->
        
        <load-on-startup>1</load-on-startup>
    </servlet>

    ...
    
    
</web-app>

설정하지 않은 주소를 쳤을 때, 404에러 대신 직접 만든 페이지가 나오는 것을 확인

+ Recent posts