Spring Projcect/계좌 관리 시스템 프로젝트

Chapter 06. 스프링 MVC(Model-View-Controller)

계란💕 2022. 7. 27. 09:58

6.1 스프링 MVC(Model-View-Controller) 전체 구조 소개

  6.1-1 스프링 MVC 소개

    - 스프링 MVC는 현 시점 국내 IT 서비스 실무에서 가장 많이 활용되는 웹 개발 기술이다.

    - 웹 개발 기술 변화: HTML(HyperText Markup Language) -> CGI(Common Gatewat Interface) -> Servlet -> Spring MVC -> (미래에는 무엇이 쓰일까?)

    - 스프링 MVC는 오랫동안 사용되고 있다. 

    - MVC란 소프트웨어 디자인 패턴을 뜻한다.

    - MVC의 등장: 스파게티처럼 엉켜있는 코드가 MVC, SRP(단일 책임 원칙)과 함께 깔끔하게 정리된다.
    - MVC를 성공적으로 사용하면 사용자 인터페이스로부터 비즈니스 로직을 분리하여 애플리케이션의 시각적 요소나 그 이면에서 실행되는 비즈니스 로직을 서로 영향 없이 쉽게 고칠 수 있는 애플리케이션을 만들 수 있다.

    cf) Servlet(서블릿): 자바를 사용하여 웹 페이지를 동적으로 생성하는 서버 측 프로그램 혹은 그 사양을 말한다.

 

 

    Spring MVC 구조

  • Dispatcher Servlet은 모든 요청을 받고 응답을 준다. 수문장 역할이라 어디로 보낼지는 HandlerMapping에게 물어본다.
  • HandlerMapping 안에 서로(Request - Request별 스프링 MVC는담당자)가 매핑되어 있다. 요청에 따라 담당자(Controller)를 찾아서 보내준다.
  • Controller는 요청을 잘 처리한다.
  • Dispatcher Servlet가 View Name를 주면 View Resolver는 해당 View를 선택해준다. (HandlerMapping 과 비슷)
  • Dispatcher Servlet는 해당 View의 View Response를 만들어준다.
  • View의 ex) json, HTML
  • Model(이동하고 있는 데이터), View(화면 역할), Controller(비즈니스 로직을 처리하고 모델과 뷰를 응답으로 준다.)
  • 결국 MVC는 현재는 웹 개발 내에서 FE, BE, DBA, DevOps, SRE 등 다양하게 분화되어 있다. 업무가 명확해지고 전문성이 올라가는 것과 관련있다.

 

 

 

6.2 스프링 MVC - HTTP 요청 응답 (1)

    6.2-1 스프링 MVC 프로젝트 생성

  Ex) 

    - 설정: Gradle, Openjdk17, Java11, dependency(Spring Web, Lombok)

    (1) 새 프로젝트를 만든다. 스프링 부트: 2.6.xx

      - lombok은 boilerplate code를 줄여주는 어노테이션 기반 라이브러리이다.

        -> boilerplate code: 최소한의 변경으로 여러 곳에서 재사용되며 반복적으로 비슷한 형태를 띄는 코드를 말한다.

      - Spring Web는 MVC 기반, 가장 대중적 기술

      - Spring Reactive Web: non-blocking, 비동기 기반 

      - Spring Web Services

  

  Note) 실행 결과

    - 결과를 보면 톰캣이라는 WAS(Web Application Server, 와스)를 통해 어플리케이션이 동작했고  8080포트에 떠있다는 뜻이다.

    - 일반적으로 어플리케이션은 자동으로 종료한다.

    - 그런데 웹 어플리케이션은 서버의 목적으로 쓰기 때문에 종료되면 안 된다.

    - 외부의 요청을 반복적으로 확인해야한다. 

 

    - 로컬 페이지 들어간 화면 (스프링의 기본 에러 페이지)

      -> 404: 리소스를 찾지 못했다. 

 

 

6.3 스프링 MVC - HTTP 요청 응답 (2)

6.3-1 스프링 MVC 기본 HTTP 요청 매핑

  - 컨트롤러를 만들기 위해 해당 클래스 위에 어노테이션 @Controller를 이용한다. 요청을 받아서 해당 컨트롤러로 보내준다.

  - 컨트롤러가 외부의 요청을 처리한다.

  - 과정: 웹 브라우저가 url에 접근 => 해당 url 서버의 해당 포트에서 스프링 어플리케이션이 떠있다. =>  요청을 다 처리한다. html을 준다. =>  브라우저가 html을 해석해서 전체 그림을 그려준다.

 

 

  @Controller와 @RestController의 차이

  • @Controller: 메서드의 반환값으로 기본적으로 HTML 파일을 주도록 되어 있다. (요즘은 많이 안 쓰는 추세이다.)
  • @RestController: 반환값으로 Rest API 요청에 대한 응답 (주로, JSON)을 주도록 되어 있다. (순수 백엔드 / 앞으로 주로 쓸 예정)

 

  매핑 어노테이션

  • @RequeseMapping: GET, POST 등 요청 방식을 직접 지정한다. 가장 기본적인 매핑 어노테이션, 명확하고 명시적이다.
  • 그림: 1번 주문에 대한 내용을 받아준다.

  Ex) 매핑 어노테이션

package com.example.websample.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

// 웹 요청을 받아주는 컨트롤러

@Slf4j
@RestController
public class SampleController {

    @RequestMapping(value = "/order/1", method = RequestMethod.GET)
    public String getOrder(){
        log.info("Get some order");
        return "orderId: 1, orderAmount: 1000";
    }
}

      - 새 패키지 만든다. com.examplem.websample.controller 패키지 안에 자바 클래스를 만든다.SampleController

      - @RestController안에는 @Controller, @ResponseBody 가 있다.

      - @Controller에는 @Component가 있다. 따라서, @Component의 변경된 형태이다.

      -  스프링에서 빈으로 등록해놓고 외부에 요청이 들어왔을 때, 처리할 수 있도록 처리 목록에 담아둔다.

      - 롬복에 있는 @Slf4j 를 작성

        -> Slf4j(Simple Logging Facade for Java)는 단순한 퍼사드 패턴을 수단으로 하는 자바 로깅 API를 제공한다.  

      - 외부에서 요청이 들어오면 컨트롤러에 등록된 RequestMapping, url과 메서드는  getOrder()이 연결되어 있다.

 

 

  축약형 매핑 어노테이션 (Spring 4 .xx 중반 부터 생겼다. 한 눈에 파악 가능)

  • @GetMapping: 데이터 가져오기 (위 예제의 RequestMethod.GET과 같다.)
  • @PostMapping: 데이터 전송
  • @PutMapping: 전체 수정
  • @PatchMapping: 일부 수정
  • @DeleteMapping: 삭제

 

  Ex) 축약형 매핑 어노테이션

    - log.info()는 클래스 위치, 메시지, 어느 스레드에 있는지 다양한 정보를 보여준다.

    - Post는 어떻게 테스트할까? (order로는 안 된다.)

      -> intelliJ test를 활용한다.

package com.example.websample.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

// 웹 요청을 받아주는 컨트롤러

@Slf4j
@RestController
public class SampleController {

    @GetMapping("/order/1")
    public String getOrder(){
        log.info("Get some order");
        return "orderId: 1, orderAmount: 1000";
    }
    @PostMapping("/order")
    public String createOrder(){
        log.info("Create order");
        return "order created -> orderId: 1, orderAmount: 1000";
    }
}

  Note) 실행 결과

    

  IntelliJ http test (ultimate 버전만 가능)

  •  IntelliJ http test를 활용하면 위에서 만든 HTTP요청을 간단히 요청해볼 수 있다.
  • POSTMAN이나 기타 다른 툴로도 가능하다

  Ex) 매핑

    - 요청 타입은 json 타입이다.

<hide/>
package com.example.websample.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
class SampleController {

    @GetMapping("/order/1")
    public String getOrder(){
        log.info("Get some order");
        return "orderId: 1, orderAmount: 1000";
    }
    @PostMapping("/order")
    public String createOrder(){
        log.info("Create order");
        return "order created -> orderId: 1, orderAmount: 1000";
    }
}
<hide/>
### create Order
POST http://localhost:8080/order
Content-Type: application/json

{"orderId" : "123", "orderAmount":  8500}

<> 2022-07-27T164551.200.txt

  Note) 실행 결과

    - 123, 1000은 실제로 쓰이지 않기 때문에 원래 값인 1, 1000이 나오는 것이 정상이다.

 

 

6.4 스프링 MVC - HTTP 요청 응답 (3)

 

  파라미터를 넘기는 방법(1) - Get, Delete

  • Get, Delete의 경우, PathVariable을 많이 쓴다. 요새는 id를 path에 넣는 것을 선호한다. 이름이 같은 경우 생략 가능하다. 그리고  여러 개 삽입 가능
  •   ex) @PathVariable("id") String identity
  • query-params: 추가적인 정보를 입력한다. 게시판의 검색 필터 페이징에서 많이 사용한다.
  • @RequestParam사용법 ex) PathVariable처럼 이름을 동일하게 하면 생략이 가능하다.
  •  required, dafaultValue 옵션을 넣어줄 수 있다. (dafaultValue의 기본값을 false로 바꿀 수 있다.)
  •  없어도 자동으로 나온다.
  •  Map, MultiValueMap으로 요청받는 방법

 

  Ex) 

    (1) {orderId}라는 pathVariable을 넣어주니까 아래처럼 알림이 뜬다. 아래처럼 orderId를 생성한다

        <에러> 13번 행에 중괄호를 두 번 넣었는데 하나만 넣어야 맞다. '}}' => '}'

<hide/>
package com.example.websample.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
class SampleController {

    @GetMapping("/order/{orderId}")
    public String getOrder(@PathVariable("orderId") String id){
        log.info("Get some order: " + id);  // 받아온 아이디 사용
        return "orderId: " + id+ ", " + "orderAmount: 1000";
    }
    @PutMapping("/order")
    public String createOrder(){
        log.info("Create order");
        return "order created -> orderId: 1, orderAmount: 1000";
    }
}
<hide/>
### create Order
PUT http://localhost:8080/order
Content-Type: application/json

{"orderId" : "123", "orderAmount" : 8500}


### get Order
GET http://localhost:8080/order/987

<> 2022-07-28T125422.200.txt

  Note) put 실행 결과

 

    2) requestParam을 써본다. url에 매개변수로 id, amount를 받아올 수 있다.

<hide/>
package com.example.websample.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
class SampleController {

    @GetMapping("/order/{orderId}")
    public String getOrder(@PathVariable("orderId") String id){
        log.info("Get some order: " + id);  // 받아온 아이디 사용
        return "orderId: " + id+ ", " + "orderAmount: 1000";
    }

    @GetMapping("/order")
    public String getOrderWithRequestParam(
            @RequestParam("orderId") String id,
            @RequestParam("orderAmount") String amount    ){
        log.info("Get order: " + id + ", amount : " + amount);  // 받아온 아이디 사용
        return "orderId: " + id+ ", " + "orderAmount: " + amount;
    }

    @PutMapping("/order")
    public String createOrder(){
        log.info("Create order");
        return "order created -> orderId: 1, orderAmount: 1000";
    }
}
<hide/>
### get Order with request param
GET http://localhost:8080/order?orderId=oioi52&orderAmount=9000

<> 2022-07-28T130153.200.txt

 

  Note)  실행 결과

    - get요청을 할 때 path 경로상에 있는 값을 pathVariable에 받아오고, 여러 개 받아오려면 requestParam을 받아온다.

 

 

    3) @DeleteMapping

<hide/>
package com.example.websample.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
class SampleController {

    @GetMapping("/order/{orderId}")
    public String getOrder(@PathVariable("orderId") String id){
        log.info("Get some order: " + id);  // 받아온 아이디 사용
        return "orderId: " + id+ ", " + "orderAmount: 1000";
    }

    @DeleteMapping("/order/{orderId}")
    public String deleteOrder(@PathVariable("orderId") String id){
        log.info("Delete some order: " + id);  // 받아온 아이디 사용
        return "Delete orderId: " + id;
    }

    @GetMapping("/order")
    public String getOrderWithRequestParam(
            @RequestParam("orderId") String id,
            @RequestParam("orderAmount") String amount    ){
        log.info("Get order: " + id + ", amount : " + amount);  // 받아온 아이디 사용
        return "orderId: " + id+ ", " + "orderAmount: " + amount;
    }

    @PutMapping("/order")
    public String createOrder(){
        log.info("Create order");
        return "order created -> orderId: 1, orderAmount: 1000";
    }
}
### delete Order
DELETE http://localhost:8080/order/987

  Note) 실행 결과

 

 

  Ex) orderId의 속성 required를  false로 바꾸기

 

    (1) orderId 설정하지 않고 실행하는 경우

      - required = true가 기본값이지만 false로 바꿀 수 있다.

      - orderId를 생략한 아래 코드를 실행시키면 에러가 난다. 기본적으로 required가 true라서 오류가 난다.

### get Order with request param
GET http://localhost:8080/order?orderAmount=9000

    Note) 실행 결과 

    - 잘못된 요청이라고 에러 난다.

 

    (2) 그 다음 아래와 같이 false로 바꾸고 다시 실행한다.

package com.example.websample.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
class SampleController {

    @GetMapping("/order/{orderId}")
    public String getOrder(@PathVariable("orderId") String id){
        log.info("Get some order: " + id);  // 받아온 아이디 사용
        return "orderId: " + id+ ", " + "orderAmount: 1000";
    }

    @DeleteMapping("/order/{orderId}")
    public String deleteOrder(@PathVariable("orderId") String id){
        log.info("Delete some order: " + id);  // 받아온 아이디 사용
        return "Delete orderId: " + id;
    }

    @GetMapping("/order")
    public String getOrderWithRequestParam(
            @RequestParam(value = "orderId", required = false) String id,
            @RequestParam("orderAmount") String amount){
        log.info("Get order: " + id + ", amount : " + amount);  // 받아온 아이디 사용
        return "orderId: " + id+ ", " + "orderAmount: " + amount;
    }

    @PutMapping("/order")
    public String createOrder(){
        log.info("Create order");
        return "order created -> orderId: 1, orderAmount: 1000";
    }
}

    - 그 다음 아래 코드를 다시 실행한다.

### get Order with request param
GET http://localhost:8080/order?orderAmount=9000

  Note) 실행 결과

 

    (3) orderId에 값이 안 들어오는 경우의 default 값을 설정할 수 있다.

<hide/>
package com.example.websample.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
class SampleController {

    @GetMapping("/order/{orderId}")
    public String getOrder(@PathVariable("orderId") String id){
        log.info("Get some order: " + id);  // 받아온 아이디 사용
        return "orderId: " + id+ ", " + "orderAmount: 1000";
    }

    @DeleteMapping("/order/{orderId}")
    public String deleteOrder(@PathVariable("orderId") String id){
        log.info("Delete some order: " + ida);  // 받아온 아이디 사용
        return "Delete orderId: " + id;
    }

    @GetMapping("/order")
    public String getOrderWithRequestParam(
            @RequestParam(value = "orderId", required = false, defaultValue = "defaultId") String id,
            @RequestParam("orderAmount") String amount){
        log.info("Get order: " + id + ", amount : " + amount);  // 받아온 아이디 사용
        return "orderId: " + id+ ", " + "orderAmount: " + amount;
    }

    @PutMapping("/order")
    public String createOrder(){
        log.info("Create order");
        return "order created -> orderId: 1, orderAmount: 1000";
    }
}

 

    - 그 다음 아래 코드를 실행시키면?

### get Order with request param
GET http://localhost:8080/order?orderAmount=9000

  Note) 실행 결과

 

 

  파라미터를 넘기는 방법(2) - Post, Put, Push

  • @RequestBody: http body정보를 편리하게 받을 수 있다. 주로 사용하는 포맷은 Json(현재 사실상 표준), 양이 많을 때 사용한다.
  • @RequestHeader: http header정보를 편리하게 받을 수 있다. 
  • @RequestHeader는 계정 정보, 인증 정보 같은 메타 정보(데이터를 설명해주는 데이터)를 많이 담는다.

 

    어노테이션 차이점

  • @Data: @Getter + @Setter + @RequiredArgsConstructor + @ToString + @EqualsAndHashCode 
  • @Builder: @AllArgsConstructor와 비슷하게 객체를 생성하고 필드값을 주입해주는데 빌더의 형식을 제공한다.
  • @NoArgsConstructor: 파라미터가 없는 생성자 제공한다. JPA(Java Persistence API) 사용 시 필수 어노테이션
  • @AllArgsConstructor: 모든 변수가 있는 생성자를 제공한다.
  • @RequiredArgsConstructor: final이 붙은 멤버변수는 필수적으로 초기화해야 하는데 이를 위해 스프링이 생성자를 자동으로 만들어준다.

 

 

  Ex) 

    (1) PostMapping, createOrder()를 만든다.

    (2) 클래스 내에 이너 클래스를 만든다. CreateOrderRequest

      - 이너 클래스 위에 @Data 어노테이션을 붙이면 자바 빈 객체로 만들어주고 자동으로 getter, setter을 생성해준다. (lombok의 역할)

      - 아래와 같이 이너 클래스와 createOrder()를 추가한다.

<hide/>
package com.example.websample.controller;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
class SampleController {

    @GetMapping("/order/{orderId}")
    public String getOrder(@PathVariable("orderId") String id){
        log.info("Get some order: " + id);  // 받아온 아이디 사용
        return "orderId: " + id+ ", " + "orderAmount: 1000";
    }

    @DeleteMapping("/order/{orderId}")
    public String deleteOrder(@PathVariable("orderId") String id){
        log.info("Delete some order: " + id);  // 받아온 아이디 사용
        return "Delete orderId: " + id;
    }

    @GetMapping("/order")
    public String getOrderWithRequestParam(
            @RequestParam(value = "orderId", required = false, defaultValue = "defaultId") String id,
            @RequestParam("orderAmount") String amount){
        log.info("Get order: " + id + ", amount : " + amount);  // 받아온 아이디 사용
        return "orderId: " + id+ ", " + "orderAmount: " + amount;
    }

    @PostMapping("/order")
    public String createOrder(
            @RequestBody CreateOrderRequest createOrderRequest,
            @RequestHeader String userAccountId){
        log.info("create order: " + createOrderRequest + ", userAccountId : " + userAccountId);  // 받아온 아이디 사용
        return "orderId: " + createOrderRequest.getOrderId()+ ", " +
               "orderAmount: " + createOrderRequest.getOrderAmount();
    }

    @PutMapping("/order")
    public String createOrder(){
        log.info("Create order");
        return "order created -> orderId: 1, orderAmount: 1000";
    }

    @Data
    public static  class CreateOrderRequest{
        private String orderId;
        private Integer orderAmount;
    }
}
<hide/>
###
POST http://localhost:8080/order
Content-Type: application/json
userAccountId: account800

{"orderId" : "123", "orderAmount" : 8500}

  Note) 실행 화면

    - 디버그 창을 확인해보면 아래처럼 계좌 정보가 잘 들어온 걸 볼 수 있다.

 

 

 

6.5 스프링 MVC - 필터,  인터셉터 (1)

 

6.5-1 필터와 인터셉터의 차이

  • 필터와 인터셉터는 공통적으로 주소, URL, 프로토콜로 처리할 수 있는 웹 기반 기술이다.
  • 필터는 크고 인터셉터는 작다. 필터는 대부분의 요청을 처리하고 인터셉터는 일부 요청만 처리한다.
  • 모든 요청이 인터셉터와 AOP를 거치지는 않는다.
  • DispatcherServlet은 핸들러매핑을 통해 Controller에 넘긴다. 
  • DispatcherServlet는 최종 응답을 받고 Client에게 응답을 넘긴다.

 

AOP(Aspect Oriented Programming, 관점 지향 프로그래밍)

  • AOP란 업무 등 특정 책임이 있는 클래스 안에 본질적인 처리(ex) 거래, 주문)만 기술하고 본질적이지 않은 처리는 밖으로 꺼내는 기술이다.
  • 로그 출력, 예외 처리, 보안 인증, 트랜잭션 처리 등 공통화할 수 있는 처리를 Aspect 하나의 단위로 묶어서 어떤 객체가 본질적인 역할만 하도록 하는 기술이다.
  • 핵심 비즈니스 로직과 공통 모듈을 구분하고 핵심 로직에 영향을 주지 않으면서 공통 모듈을 효과적으로 잘 끼워넣을 수 있는 개발 방법을 일컫는다.

 

Filter(필터)

  • 스프링 외부의 서블릿에서 제공하는 공통 처리 기능
  • 스프링 내로 요청이 들어오기 전과 스프링의 요청이 나갈 때 처리 가능
  • 직접 핸들링할 수 있기 때문에 low level 처리가 가능
  • 가장 먼저 필터링해주는 곳
  • ex) 해킹 시도(csrf(Cross-Site Request Forgery, 사이트 간 요청 위조), SQL Injection(sql 삽입 공격))를 막을 수 있다.
  • doFilter()를 오버라이드하고 @Component를 등록하기만 하면 필터를 만들 수 있다.

 

    Interceptor(인터셉터)

  • 스프링에서 제공하는 공통 처리 기능
  • 실제 매핑된 Handler 정보 확인 가능 (어떤 것이 실제 내 요청을 처리하는 지도 확인 가능하다.)
  • 상세한 조건식과 세부적인 스텍(pre, post, after)를 통해 구체적 시점에 구체적인 동작이 가능하다
  • AOP는 인터셉터 보다 구체적인 조건(애너테이션, 파라미터, 주소 등)과 동작 위치(AfterThrowing 등)을 갖기 때문에 상황에 따라서 구체적인 처리가 가능하다.
  • AOP는 URL보다는 패키지, 애너테이션 같은 자바 코드와 관련이 있다면 필터와 인터셉터는 웹에 더 치중한다.(URL, 프로토콜) .. ("AP"를 걸 수 있다??)

 

 

6.6 스프링 MVC - 필터, 인터셉터 (2)

 

  Ex) 필터 만들기 - Bean으로 바로 등록하는 경우

<hide/>
package com.example.websample.config;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import java.io.IOException;

@Slf4j
@Component
public class LogFilter implements Filter {
   
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        // 외부 -> filter -> (-> 처리 ->) filter -> 외부
        // 체인을 이어준다. 괄호 부분을 처리한다.
        log.info("Hello LogFilter : " + Thread.currentThread());
        chain.doFilter(request, response);
        log.info("Bye LogFilter : " + Thread.currentThread());
    }
}

    - 스프링이 스캔해서 자동으로 Bean 등록하는 경우이다.

    - 요청, 응답, 필터 체인을 매개변수로 받아서 chain이 doFilter로 이어져 나간다.

    - 새로운 클래스 LogFilter를 만든다.

 

   Note) 실행 결과

      - order/150 을 순서대로 넣었더니 아래과 같이 기록이 남는다.

      - 그러면 아래와 같이 스레드 정보와 함께 주문 정보가 콘솔에 나온다.

      - @Component(스캐너가  자동으로 빈으로 만들어서 컨테이너에 등록)를 추가하지 않으면 "Hello", "Bye" 같은 부분이 콘솔에 안 나오니 꼭 넣어준다.

 

  Ex) 필터 만들기 - Bean으로 바로 등록하지 않는 경우

<hide/>
package com.example.websample.config;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;

@Configuration
public class WebConfig {

    @Bean
    public FilterRegistrationBean loggingFilter(){  // 필터 등록해주기 위한 bean

         FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
         filterRegistrationBean.setFilter(new LogFilter());
         filterRegistrationBean.setOrder(1); // 필터 여러 개 있는 경우, 가장 첫번째가 되도록 한다.
         filterRegistrationBean.addUrlPatterns("/*");   //  와일드 카드

        return filterRegistrationBean;
    }
}

    - 다양한 설정이 필요한 경우에는 위 예제 처럼 @Component로 바로 등록하기 보다는 새로운 자바 설정 파일을 만든다.

    - FilterRegisterationBean을 통해서 필터로 등록해준다.

 

  Note) 실행 결과

    - 아까와 동일하게 실행된다.

 

 

 Ex) 와일드 카드를 payment로 바꿔서 실행

    (1) 아래처럼 코드를 바꾼다.

<hide/>
package com.example.websample.config;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;

@Configuration
public class WebConfig {

    @Bean
    public FilterRegistrationBean loggingFilter(){  // 필터 등록해주기 위한 bean

         FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
         filterRegistrationBean.setFilter(new LogFilter());
         filterRegistrationBean.setOrder(1); // 필터 여러 개 있는 경우, 가장 첫번째가 되도록 한다.
         filterRegistrationBean.addUrlPatterns("/payment");   //  와일드 카드

        return filterRegistrationBean;
    }
}

  Note) 실행 결과

    - 처리만 하고 필터에 걸리지 않는 걸 볼 수 있다.

 

 

    (2) 검색 창에 payment 입력해보면?

    Note) 실행 결과

      - 그러면 반대로 필터는 걸리지만 처리할 내용이 없는 상황이 된다. 

 

 cf) filterRegisterBean을 이용하면 필터 걸어줄 위치를 지정하고 동작 순서도 정할 수 있다.

 

 

 

6.7 스프링 MVC - 필터,  인터셉터 (3)

 

Interceptor 구현 과정

  • HandlerInterceptor 인터페이스를 구현해서 만든다.
  • preHandle: 핸들링 하기 전에 처리 - try같은 느낌, 반환형은 boolean(다음 인터셉터 핸들러에게 요청이 계속 진행되길 바라면 true)
  • postHandle: 핸들링한 다음에 처리 - 정상 처리했을 때, 실행된다.
  • afterCompletion은 위 경우에 예외가 발생했을 때도 실행되는 구문이다. 그래서 Exception까지 정의되어 있다. - finally같은 느낌 (필터보다 구체적인 조건식을 만들 수 있다.)
  • Component로 바로 등록하지 않고 WebConfig에서 인터셉터를 직접 등록해줘야한다. (인터페이스 WebMvcConfigurer을 구현한다.)
  • 요즘은 필터보다 인터셉터를 많이 쓰는 추세이다.

 

  Ex) 인터셉터 실습

<hide/>
package com.example.websample.config;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.servlet.Filter;
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public FilterRegistrationBean loggingFilter(){  // 필터 등록해주기 위한 bean

         FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
         filterRegistrationBean.setFilter(new LogFilter());
         filterRegistrationBean.setOrder(1); // 필터 여러 개 있는 경우, 가장 첫번째가 되도록 한다.
         filterRegistrationBean.addUrlPatterns("/*");   //  와일드 카드
        return filterRegistrationBean;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
                .order(1)
                .addPathPatterns("/**")              // 어디에서 인터셉터를 추가할 지
                .excludePathPatterns("/css/*", "/images/*"); // 인터셉터를 뺄 지 .
       }
}

    1) config 아래에 LogInterceptor를 만든다.

    2) 아래와 같이 구현한다.

    3) addInterceptors을 오버라이드한다.

    4) registry는 등록해두는 등록 책과 같은 역할을 한다.

    5) css, images는 인터셉터가 필요없다. 따라서, 인터셉터를 거치지 않도록 예외를 준다.

 

  Note) 실행 결과

    - 필터에서는 요청을  누가 처리하는지 확인할 수 없었지만 디스패치와 서블릿이 

인터셉터는 가능하도록 한다?

 

 

  Ex) 

    - @GetMapping부분을 아래와 같이 수정한다. (id == 500인지 여부 확인하는 코드만 추가한다.)

<hide/>
package com.example.websample.controller;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
class SampleController {

    @GetMapping("/order/{orderId}")
    public String getOrder(@PathVariable("orderId") String id) throws IllegalAccessException {
        log.info("Get some order: " + id);  // 받아온 아이디 사용

        if (id.equals("500")) {
            throw new IllegalAccessException("500 is not valid orderId");
        }
        return "orderId: " + id + ", " + "orderAmount: 1000";
    }

    @DeleteMapping("/order/{orderId}")
    public String deleteOrder(@PathVariable("orderId") String id) {
        log.info("Delete some order: " + id);  // 받아온 아이디 사용
        return "Delete orderId: " + id;
    }

    @GetMapping("/order")
    public String getOrderWithRequestParam(
            @RequestParam(value = "orderId", required = false, defaultValue = "defaultId") String id,
            @RequestParam("orderAmount") String amount) {
        log.info("Get order: " + id + ", amount : " + amount);  // 받아온 아이디 사용


        return "orderId: " + id + ", " + "orderAmount: " + amount;
    }

    @PostMapping("/order")
    public String createOrder(
            @RequestBody CreateOrderRequest createOrderRequest,
            @RequestHeader String userAccountId) {
        log.info("create order: " + createOrderRequest + ", userAccountId : " + userAccountId);  // 받아온 아이디 사용
        return "orderId: " + createOrderRequest.getOrderId() + ", " +
                "orderAmount: " + createOrderRequest.getOrderAmount();
    }

    @PutMapping("/order")
    public String createOrder() {
        log.info("Create order");
        return "order created -> orderId: 1, orderAmount: 1000";
    }

    @Data
    public static class CreateOrderRequest {
        private String orderId;
        private Integer orderAmount;
    }
}
<hide/>
package com.example.websample.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Slf4j  // 간단히 로깅해보자
public class LogInterceptor implements HandlerInterceptor{
    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        log.info("preHandle LogInterceptor  : " + Thread.currentThread() ); //현재 스레드 정도
        log.info("preHandle handler : " + handler);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request,
                           HttpServletResponse response,
                           Object handler,
                           ModelAndView modelAndView) throws Exception {
        log.info("postHandle LogInterceptor  : " + Thread.currentThread() ); //현재 스레드 정도
    }

    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler,
                                Exception e) throws Exception {
        log.info("afterCompletion LogInterceptor  : " + Thread.currentThread() ); //현재 스레드 정도
        if(e != null){  // 에러난 경우에만 실행하는 구문
            log.error("afterCompletion exception : " + e.getMessage());
        }
    }
}

  Note) 실행 결과

    - localhost:8080/order/300으로 들어간 경우 (정상)

 

    - localhost:8080/order/500으로 들어간 경우

      -> 아까 300을 입력했을 때는 코드에서 필터를 타고 인터셉터를 타고 들어갔다.

      -> 그런데, 이번에는 GetOrder()다음에 postHandle로 들어가지지 않는다.

      -> GetOrder() 다음에 바로 afterCompletion으로 들어간다.

      -> 500은 valid한 id가 아니라는 log 설명이 나온다. (LogInterceptor의 afterCompletion()메서드에 예외 처리 위한 구문이 있기 때문이다.)

 

 

 

6.8 스프링 MVC - 예외 처리 (1)

6.8-1 예외 처리

  - 예외처리가 없으면 개발에 대한 숙련도가 낮아보인다.

  - 경력자는 수많은 예외 상황을 처리하는 방법을 안다. 

  - 자바에서는 try -catch구문을 이용해서 예외 처리를 해왔다.

  

 

스프링 MVC에서 예외를 처리하는 방법 (REST API 이용)

  • @ExceptionHandler
  • 컨트롤러 기반 예외 처리
  • HTTP Status code를 변경하는 방법: @RespenseStatus(200말고 다른 값을 넣어줄 수 있다.), ResponseEntity 활용
  • 예외 처리 우선 순위: 해당 Exception이 정확히 지정된 Handler => 해당 Exception의 부모 ExceptionHandler => 모든 예외의 부모

 

  Ex) 

    - 아래 코드를 실행하고 나면 

###
GET http://localhost:8080/order/500

 

   Note) 실행 결과

    - 우리가 지정해준 값과 관련없는 코드들이 나온다. 기본으로 지정된 부분

    - 그래서 exceptionHandler를 만들 것이다.

 

 

 

  Ex) 예외 처리   

    - SampleHandler에 코드를 추가한다.

    - Exception 관련 코드만 추가한다.

<hide/>
package com.example.websample.controller;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
class SampleController {

    @GetMapping("/order/{orderId}")
    public String getOrder(@PathVariable("orderId") String id) throws IllegalAccessException {
        log.info("Get some order: " + id);  // 받아온 아이디 사용

        if (id.equals("500")) {
            throw new IllegalAccessException("500 is not valid orderId");
        }
        return "orderId: " + id + ", " + "orderAmount: 1000";
    }
    @ExceptionHandler (IllegalAccessException.class)
    public String handleIllegalAccessException(IllegalAccessException e){
        log.error("IllegalAccessException is occurred." + e);
        return  "INVALID_ACCESS";
    }



    @DeleteMapping("/order/{orderId}")
    public String deleteOrder(@PathVariable("orderId") String id) {
        log.info("Delete some order: " + id);  // 받아온 아이디 사용
        return "Delete orderId: " + id;
    }

    @GetMapping("/order")
    public String getOrderWithRequestParam(
            @RequestParam(value = "orderId", required = false, defaultValue = "defaultId") String id,
            @RequestParam("orderAmount") String amount) {
        log.info("Get order: " + id + ", amount : " + amount);  // 받아온 아이디 사용


        return "orderId: " + id + ", " + "orderAmount: " + amount;
    }

    @PostMapping("/order")
    public String createOrder(
            @RequestBody CreateOrderRequest createOrderRequest,
            @RequestHeader String userAccountId) {
        log.info("create order: " + createOrderRequest + ", userAccountId : " + userAccountId);  // 받아온 아이디 사용
        return "orderId: " + createOrderRequest.getOrderId() + ", " +
                "orderAmount: " + createOrderRequest.getOrderAmount();
    }

    @PutMapping("/order")
    public String createOrder() {
        log.info("Create order");
        return "order created -> orderId: 1, orderAmount: 1000";
    }

    @Data
    public static class CreateOrderRequest {
        private String orderId;
        private Integer orderAmount;
    }
}

 

  - 그 다음에 이 코드를 실행한다.

###
GET http://localhost:8080/order/500

 

  Note) 실행 결과

    - 이전에 넣은 "INVALID_ACCESS"가 잘 실행된다.

    - 그런데, 지금 오류가 생긴 상황에서  정상을 뜻하는 "200"이 떴다. => "Response Status"....HTTP Status Code 라고 한다.

 

  Note) 실행 결과

    - @Exception 애너테이션 바로 위에 아래 코드를 추가한다. 그 다음에 재실행한다. (forbidden은 실패를 뜻한다.) - http 관련한 약속이라고 볼 수 있다.

@ResponseStatus (HttpStatus.FORBIDDEN)

    - 그 다음에 다시 get.. 코드를 실행하고 나면? 

      -> 아까와 다르게 200(정상을 뜻한다 보통)이 아닌 403(에러 코드)이 나오는 것을 볼 수 있다.

 

 

  Ex)

  1) 새로운 클래스 만든다.

     - lombok은 생성자, getter, setter을 쉽게 만들어준다.

<hide/>
package com.example.websample.dto;
import lombok.AllArgsConstructor;
import lombok.Data;

@AllArgsConstructor
@Data
public class ErrorResponse {
    private String errorCode;
    private String message;
}

 

    2) 샘플 컨트롤러 수정한다. 서버 재실행한다.

<hide/>
@ResponseStatus (HttpStatus.FORBIDDEN)
@ExceptionHandler (IllegalAccessException.class)
public ErrorResponse handleIllegalAccessException(IllegalAccessException e){
    log.error("IllegalAccessException is occurred.", e);
    return new ErrorResponse("INVALID_ACCESS", "IllegalAccessException is occurred.");
}

 

  Note) 실행 결과

    - 훨씬 깔끔하게 json형태로 출력된다.

      -> json은 직렬화, 해석하는 입장에서도 수월하다.

 

 

 

6.9 스프링 MVC - 예외 처리 (2)

   Ex) Response Entity 이용하기

    - 샘플 컨트롤러의 코드를 아래와 같이 수정한다.

<hide/>
@ExceptionHandler (IllegalAccessException.class)
public ResponseEntity<ErrorResponse> handleIllegalAccessException(IllegalAccessException e){
    log.error("IllegalAccessException is occurred.", e);
    return ResponseEntity
            .status(HttpStatus.FORBIDDEN)
            .header("newHeader", "Some Value")
            .body(new ErrorResponse("INVALID_ACCESS",
                    "IllegalAccessException is occurred"));
}

 

  Note) 실행 결과

    - forbidden => "403"

    - newHeader도 생겼다.

 

 

  Ex) 

    - exception이라는 새 폴더를 만들고 webSampleException클래스를 만든다.

package com.example.websample.exception;
public class WebSampleException {
    private ErrorCode erroeCode;
    private String message;
}

 

    - 열거형 만든다.

package com.example.websample.exception;
public enum ErrorCode {
    TOO_BIG_ID_ERROR,
    TOO_SMALL_ID_ERROR;
}

 

    - 커스텀 예외 코드와 커스텀 메시지를 담고 있는 예외 클래스를 만든다.

<hide/>
package com.example.websample.dto;
import com.example.websample.exception.ErrorCode;
import lombok.AllArgsConstructor;
import lombok.Data;

@AllArgsConstructor
@Data
public class ErrorResponse {
    private ErrorCode errorCode;
    private String message;
}

 

    - 샘플 컨트롤러 클래스 수정한다.

<hide/>
@ExceptionHandler (IllegalAccessException.class)
public ResponseEntity<ErrorResponse> handleIllegalAccessException(IllegalAccessException e){
    log.error("IllegalAccessException is occurred.", e);
    return ResponseEntity
            .status(HttpStatus.FORBIDDEN)
            .header("newHeader", "Some Value")
            .body(new ErrorResponse(ErrorCode.TOO_BIG_ID_ERROR,
                    "IllegalAccessException is occurred"));
}
<hide/>
@GetMapping("/order/{orderId}")
public String getOrder(@PathVariable("orderId") String id) throws IllegalAccessException {
    log.info("Get some order: " + id);  // 받아온 아이디 사용

    if (id.equals("500")) {
        throw new WebSampleException(
                ErrorCode.TOO_BIG_ID_ERROR,
                "500 is not valid orderId."
        );
    }
    if (id.equals("3")) {
        throw new WebSampleException(
                ErrorCode.TOO_SMALL_ID_ERROR,
                "3 is not valid orderId."
        );
    }
    return "orderId: " + id + ", " + "orderAmount: 1000";
}

 

  Note) 실행 결과 (order/500)

    - 내가 만든 webSamplException이 발생한다.

    - 가장 기본적인 방식으로 에러를 보여준다. json으로 보여주기로 했는데 파싱 에러가발생한다.

      -> Exception Handler를 만든다. 

 

  Ex) 위 예제를 수정한다.

<hide/>
@ExceptionHandler (IllegalAccessException.class)
public ResponseEntity<ErrorResponse> handleIllegalAccessException(
        IllegalAccessException e){
    log.error("IllegalAccessException is occurred.", e);
    return ResponseEntity
            .status(HttpStatus.FORBIDDEN)    // 507 에러
            .header("newHeader", "Some Value")
            .body(new ErrorResponse(
                     ErrorCode.TOO_BIG_ID_ERROR,
                    "IllegalAccessException is occurred"));
}

@ExceptionHandler (WebSampleException.class)
public ResponseEntity<ErrorResponse> handleIllegalAccessException(
        WebSampleException e){
    log.error("WebSampleException is occurred.", e);
    return ResponseEntity
            .status(HttpStatus.INSUFFICIENT_STORAGE)    // 아무거나 넣음. 507 에러
            .body(new ErrorResponse(e.getErrorCode(),
                    "WebSampleException is occurred"));
}

  Note) 실행 결과

  Ex)

<hide/>
package com.example.websample.controller;
import com.example.websample.dto.ErrorResponse;
import com.example.websample.exception.ErrorCode;
import com.example.websample.exception.WebSampleException;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.sql.SQLException;
import java.sql.SQLIntegrityConstraintViolationException;

@Slf4j
@RestController
class SampleController {

    @GetMapping("/order/{orderId}")
    public String getOrder(@PathVariable("orderId") String id) throws IllegalAccessException, SQLIntegrityConstraintViolationException {
        log.info("Get some order: " + id);  // 받아온 아이디 사용

        if (id.equals("500")) {
            throw new WebSampleException(
                    ErrorCode.TOO_BIG_ID_ERROR,
                    "500 is not valid orderId."
            );
        }
        if (id.equals("3")) {
            throw new WebSampleException(
                    ErrorCode.TOO_SMALL_ID_ERROR,
                    "3 is too small orderId."
            );
        }
        if (id.equals("4")) {
            throw new SQLIntegrityConstraintViolationException(
                    //예를 들어, pk에 중복된 데이터를 넣었을 경우에 발생
                    "Duplicated insertion was tried"
            );
        }
        return "orderId: " + id + ", " + "orderAmount: 1000";
    }

    @ExceptionHandler (IllegalAccessException.class)
    public ResponseEntity<ErrorResponse> handleIllegalAccessException(
            IllegalAccessException e){
        log.error("IllegalAccessException is occurred.", e);
        return ResponseEntity
                .status(HttpStatus.FORBIDDEN)    // 507 에러
                .header("newHeader", "Some Value")
                .body(new ErrorResponse(
                         ErrorCode.TOO_BIG_ID_ERROR,
                        "IllegalAccessException is occurred"));
    }

    @ExceptionHandler (WebSampleException.class)
    public ResponseEntity<ErrorResponse> handleIllegalAccessException(
            WebSampleException e){
        log.error("WebSampleException is occurred.", e);
        return ResponseEntity
                .status(HttpStatus.INSUFFICIENT_STORAGE)    // 아무거나 넣음. 507 에러
                .body(new ErrorResponse(e.getErrorCode(),
                        "WebSampleException is occurred"));
    }
    // 위에서 예외를 거르고 거른 다음에 남은 예외들 처리 - 가장 최후의 보루와 같음
    @ExceptionHandler (Exception.class)
    public ResponseEntity<ErrorResponse> handlesException(
            Exception e){
        log.error("WebSampleException is occurred.", e);
        return ResponseEntity
                .status(HttpStatus.INTERNAL_SERVER_ERROR)    // 너무 예외적인 케이스에 내리는 오류
                .body(new ErrorResponse(ErrorCode.INTERNAL_SERVER_ERROR,
                        "Exception is occurred"));
    }

    @DeleteMapping("/order/{orderId}")
    public String deleteOrder(@PathVariable("orderId") String id) {
        log.info("Delete some order: " + id);  // 받아온 아이디 사용
        return "Delete orderId: " + id;
    }

    @GetMapping("/order")
    public String getOrderWithRequestParam(
            @RequestParam(value = "orderId", required = false, defaultValue = "defaultId") String id,
            @RequestParam("orderAmount") String amount) {
        log.info("Get order: " + id + ", amount : " + amount);  // 받아온 아이디 사용
        return "orderId: " + id + ", " + "orderAmount: " + amount;
    }

    @PostMapping("/order")
    public String createOrder(
            @RequestBody CreateOrderRequest createOrderRequest,
            @RequestHeader String userAccountId) {
        log.info("create order: " + createOrderRequest + ", userAccountId : " + userAccountId);  // 받아온 아이디 사용
        return "orderId: " + createOrderRequest.getOrderId() + ", " +
                "orderAmount: " + createOrderRequest.getOrderAmount();
    }

    @PutMapping("/order")
    public String createOrder() {
        log.info("Create order");
        return "order created -> orderId: 1, orderAmount: 1000";
    }

    @Data
    public static class CreateOrderRequest {
        private String orderId;
        private Integer orderAmount;
    }
}

    - 코드를 추가하고 예외를 메서드 시그니처에 추가를 클릭한다. 

    - ExceptionHandler

 

  Note) 실행 결과

 

 

 

6.10 스프링 MVC - 예외 처리 (3)

 

6.10-1 @RestControllerAdvice

  - 어플리케이션의 전역적 예외처리하는 기능이다. 스프링에서 가장 많이 활용되는 기술이다. (일관적인 예외 및 응답 처리)

  - @RestControllerAdvice와 @ControllerAdvice의 차이는? Controller와 RestController의 차이와 동일하다.

    -> @ControllerAdvice: 기본적으로 뷰(view)를 응답하는 방식이다. 화면 내려준다.

    -> @RestControllerAdvice: REST API 객체를 응답하는 방식 (주로, Json으로 만들어서 응답해준다.)

    -> REST(REpresentational State Transfer):자원마다 구분하여 해당 자원의 상태, 정보를 주고 받는 것을 의미한다.

    -> REST API(REpresentational State Transfer Application Transfer Interface): 데이터와 기능의 집합을 제공하여 프로그램 간 상호 작용을 촉진하며 서로 정보를 교환 가능하도록 한다.

 

  Ex)

<hide/>
package com.example.websample.exception;
import com.example.websample.dto.ErrorResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {   // 전체에 넣어주니까 global

    @ExceptionHandler(IllegalAccessException.class)
    public ResponseEntity<ErrorResponse> handleIllegalAccessException(
            IllegalAccessException e){
        log.error("IllegalAccessException is occurred.", e);
        return ResponseEntity
                .status(HttpStatus.FORBIDDEN)    // 507 에러
                .header("newHeader", "Some Value")
                .body(new ErrorResponse(
                        ErrorCode.TOO_BIG_ID_ERROR,
                        "IllegalAccessException is occurred"));
    }

    @ExceptionHandler (WebSampleException.class)
    public ResponseEntity<ErrorResponse> handleIllegalAccessException(
            WebSampleException e){
        log.error("WebSampleException is occurred.", e);
        return ResponseEntity
                .status(HttpStatus.INSUFFICIENT_STORAGE)    // 아무거나 넣음. 507 에러
                .body(new ErrorResponse(e.getErrorCode(),
                        "WebSampleException is occurred"));
    }
    // 위에서 예외를 거르고 거른 다음에 남은 예외들 처리 - 가장 최후의 보루와 같음
    @ExceptionHandler (Exception.class)
    public ResponseEntity<ErrorResponse> handlesException(
            Exception e){
        log.error("WebSampleException is occurred.", e);
        return ResponseEntity
                .status(HttpStatus.INTERNAL_SERVER_ERROR)    // 너무 예외적인 케이스에 내리는 오류
                .body(new ErrorResponse(ErrorCode.INTERNAL_SERVER_ERROR,
                        "Exception is occurred"));
    }
}

    - 클래스를 만든다. GlobalExceptionHandler

    - 기존에 만들었던 ExceptionHandler를 복사해온다.

 

  Note) 실행 결과

    - 그러면 아까와 동일한 결과가 나온다.

    - 컨트롤러를 추가해도 매번 동일한 작업을 할 필요가 없다.

  cf) 커스텀 예외를 만들고 에러 코드를 만들어서 응답 값을 만들 수 있다.

<hide/>
package com.example.websample.exception;
public enum ErrorCode {
    TOO_BIG_ID_ERROR,
    NEW_OLD_UNMATCHED,
    REFUND_AMOUNT_EXCEED_PAYMENT_AMOUNT,
    TOO_SMALL_ID_ERROR,
    INTERNAL_SERVER_ERROR;
}