ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [스프링 5 레시피] 3장: 스프링 MVC
    Spring 2021. 3. 10. 22:15
    반응형

     

     

    먼저 이 장의 주제인 스프링 MVC와 그의 Front Controller를 담당하고 있는 DispatcherServlet에 대해 알아보겠습니다.

     

    스프링 MVC

     

    MVC 패턴은 웹 개발을 할 때 필수적으로 알아야 할 디자인 패턴인데요, MVC 패턴을 적용하면 애플리케이션에서 모델, 뷰, 컨트롤러의 역할을 각각 분담하고 UI에서 비즈니스 로직을 떼어낼 수 있습니다.

     

    UI에서 비즈니스 로직을 떼어낸다는 말을 이해하려면 예전에 사용하던 모델1 방식과 현재의 MVC 패턴을 따르는 모델2 방식의 차이를 이해해야 합니다.

     

     

    모델1 방식

     

    화면의 기능과 비즈니스 로직 처리를 하나의 JSP 페이지에서 개발하는 방법을 모델1 방식이라고 하는데요, 클라이언트에서 입력값과 함께 요청이 들어오면 JSP 페이지는 자신이 직접 작성한 자바빈을 이용해서 데이터를 처리하고 그에 대한 결과를 클라이언트에 출력합니다.

     

    즉, 하나의 JSP 페이지 내에서 입력과 출력, 데이터 처리가 모두 이루어집니다.

    출처: https://hleee.medium.com/%EB%AA%A8%EB%8D%B8-2-%EB%B0%A9%EC%8B%9D-%EA%B0%9C%EB%B0%9C-df8da43d1ff3

     

    이러한 모델1 방식은 구조가 단순하기 때문에 개발하기 쉽다는 장점이 있지만, 다음과 같은 문제들이 있기 때문에 현재는 거의 사용되지 않는 방법입니다.

     

    단점

    • HTML과 JAVA 코드가 중간중간 섞여있기 때문에 프로그램이 커질수록 복잡성이 증가하고 유지보수하기 힘들다.
    • 프론트엔드 개발과 백엔드 개발이 하나의 파일에서 이루어지기 때문에 분업할 때 효율성이 떨어진다.
    • 모듈화, 재사용성 등 OOP의 장점을 활용하기 힘들다.

     

     

    모델2 방식

     

    모델2는 위의 모델1의 단점을 보완하기 위해 애플리케이션을 개발할 때 화면 기능, 요청/응답 처리, 비즈니스 로직을 각각 분리하여 구현합니다. 

    출처: https://hleee.medium.com/%EB%AA%A8%EB%8D%B8-2-%EB%B0%A9%EC%8B%9D-%EA%B0%9C%EB%B0%9C-df8da43d1ff3

     

    표면적으로는 모델2가 모델1보다 복잡한 구조를 가지고 있어서 처음에는 이해하는데 조금 어려울 수 있지만, 조금만 복잡한 애플리케이션을 개발한다면 모델2의 장점은 명확해집니다.

     

    장점

    • HTML과 JAVA 코드가 완벽히 분리되었기 때문에 프로그램이 커져도 복잡성이 크게 증가하지 않아 디버깅 등 유지보수하기 수월하다.
    • 프론트엔드와 백엔드 개발자는 각자의 공간에서 서로의 코드를 모른 채 작업하기 때문에 분업할 때 효율적이다.
    • 애플리케이션을 구성할 때 기능을 모듈화 하여 OOP의 장점을 활용할 수 있다.

     

     

    MVC 패턴

     

    MVC는 Model, View, Controller의 합성어로 소프트웨어 공학에서 사용되는 디자인 패턴인데요, 각각의 의미를 알아보겠습니다.

    출처: https://hleee.medium.com/%EB%AA%A8%EB%8D%B8-2-%EB%B0%A9%EC%8B%9D-%EA%B0%9C%EB%B0%9C-df8da43d1ff3

     

    Model

    • 데이터베이스와 연동해서 사용자가 입력한 데이터나 사용자에게 출력할 데이터에 대한 비즈니스 로직을 수행합니다.
    • 보통 Service나 Dao 등 비즈니스 로직에 사용되는 클래스를 지칭합니다.

     

    View

    • 모델이 처리한 데이터나 그 작업의 결과를 사용해서 사용자가 보게 될 화면을 생성합니다.
    • JSP, Mustache, Thymeleaf 등을 사용해서 작성할 수 있습니다.

     

    Controller

    • 클라이언트의 요청을 받으면 실제 업무를 처리하는 모델을 호출하는데요, 클라이언트에서 보낸 데이터가 있다면 모델을 호출하기 전에 인수로 전달할 데이터를 적절히 가공하는 역할도 합니다.
    • 모델이 작업을 완료하면 그 결과 데이터로 화면을 생성하도록 뷰를 선택하여 전달합니다.
    • 전체적인 과정의 흐름을 제어합니다.

     

     

    DispatcherServlet

     

    정의

    Servlet Container (Tomcat, Jetty 등..)에서 HTTP 프로토콜을 통해 들어오는 모든 요청을 Presentation Layer의 제일 앞에 둬서 중앙집중식으로 처리해주는 Front Controller

     

    정의에 대해 설명을 하면, 클라이언트에서 보내는 요청은 Tomcat과 같은 Servlet Container가 받아서 처리하는데, 이때 제일 앞에서 서버로 들어오는 모든 요청을 처리하는 프론트 컨트롤러 패턴을 스프링이 DispatcherServlet으로 구현하였습니다. DispatcherServlet은 요청을 분석하고 적절한 Controller로 작업을 위임해줍니다.

     

     

    스프링 MVC의 요청 처리 흐름

    출처: https://www.stechstar.com/user/zbxe/JSPWebProg/53384

     

    1. 클라이언트에서 요청이 들어오면 DispatcherServlet은 HandlerMapping에 전달해서 해당 요청을 처리할 수 있는 핸들러를 찾음.
    2. 찾은 핸들러를 실행시킬 수 있는 HandlerAdapter를 이용해서 Java Reflection으로 핸들러를 실행시킴.
    3. 컨트롤러는 요청 처리 결과와 View 페이지 정보를 담은 ModelAndView 객체를 반환합니다.
    4. DispatcherServlet은 논리적인 View의 이름을 ViewResolver에게 전달하고 그에 맞는 실제 View를 얻어옵니다.
    5. 가져온 View에 컨트롤러로부터 받은 Model 데이터를 렌더링 해서 클라이언트에게 보내줄 응답을 생성합니다.

     

    스프링 MVC 애플리케이션 설정하기

     

    Spring Boot 환경에서 MVC를 이용해 웹 애플리케이션을 개발하려면 spring-boot-starter-web 하나만 추가해도 웹 개발에 필요한 다양한 의존체들이 자동으로 추가됩니다.

    메이븐
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    그레이들
    implementation 'org.springframework.boot:spring-boot-starter-web'

     

    spring-boot-starter-web에 포함된 의존체들

     

     

     

    스프링 MVC 컨트롤러 작성하기

     

    컨트롤러를 작성하기 위해서는 자바 클래스에 @Controller 애너테이션을 붙여서 정의합니다. @RequestMapping은 클래스와 메서드 레벨에 선언할 수 있는데요, 클래스에는 URL 패턴을 정의하고 핸들러 메서드에는 HTTP 메서드를 매핑합니다.

    @Controller
    @RequestMapping("/hello")
    public class HelloController {
    
        @RequestMapping(method = RequestMethod.GET)
        public String hello(Model model) {
            model.addAttribute("today", LocalDateTime.now());
            return "hello";
        }
    
    }
    

     

    해당 핸들러 메서드는 호출된 시점에 LocalDateTime 객체를 생성해서 today라는 이름으로 인수로 받은 Model 객체에 추가하고 View에서 화면에 렌더링 하게 합니다.

     

    Spring Boot 환경에서는 기본적으로 mustache나 thymeleaf 등 서버 사이드 템플릿 엔진을 사용할 경우 resources/templates 디렉토리에서 View를  찾기 때문에 예시와 같이 "hello"만 반환해줘도 hello.mustache를 뷰로 사용합니다.

     

     

    다음은 hello.mustache 파일인데요, 모델에 저장된 today 데이터를 미리 정의된 템플릿에 적절하게 넣어주고, HTML로 만들어서 클라이언트로 응답합니다.

    <!doctype html>
    <html lang="ko">
    <head>
        <meta charset="UTF-8">
        <title>Hello</title>
    </head>
    <body>
    Time: {{today}}
    </body>
    </html>

    결과

     

     

    메서드 레벨에 @RequestMapping의 속성인 method를 정의하는 대신 @GetMapping이나 @PostMapping을 사용해서 코드를 좀 더 간결하게 작성할 수 있습니다.  그리고 반환 타입이 void일 경우는 view가 url에 따라가게 되는데 URL이 /hello 일 경우 리턴 값이 "/hello"와 동일합니다.

     

    @RequestParam은 요청 URL 파라미터를 추출해서 사용하겠다는 선언인데요, 요청 시 URL에 name이라는 파라미터를 넣으면 이름으로 매핑해서 데이터를 바인딩합니다. 위와 마찬가지로 Model 객체에 데이터를 넣으면 View에서 데이터 렌더링 후에 HTML 파일을 만드는 것을 확인할 수 있습니다.

    @GetMapping("/hello")
    public void hello(@RequestParam String name, Model model) {
        model.addAttribute("today", LocalDateTime.now());
        model.addAttribute("name", name);
    }

     

    <!doctype html>
    <html lang="ko">
    <head>
        <meta charset="UTF-8">
        <title>Hello</title>
    </head>
    <body>
    Time: {{today}} <br/>
    Name: {{name}}
    </body>
    </html>

     

     

    결과

     

     


     

    스프링 MVC 애플리케이션에서 웹 요청은 컨트롤러 클래스에 선언된 @RequestMapping에 따라 담당 핸들러로 매핑됩니다.

     

    즉, http://localhost:8080/hello/world 요청이 들어오면 /hello/world에 들어맞는 핸들러가 있어야 합니다.

     

     

    가장 단순한 방법은 핸들러 메서드에 @RequestMapping을 붙여 URL 패턴을 모두 기재하는 것입니다.

    @Controller
    public class HelloController {
    
        @GetMapping("/hello/world")
        public void helloWorld() {
            ...
        }
    }

     

     

    사실 위의 방법보다는 클래스 레벨에 @RequestMapping을 붙여서 해당 컨트롤러에 속한 모든 핸들러를 대상으로 공통 URL을 작성하고 각 핸들러는 자신만의 세부적인 URL을 작성하는 방법을 많이 사용합니다.

    @Controller
    @RequestMapping("/hello")
    public class HelloController {
    
        @GetMapping("") // /hello 매핑
        public void hello(@RequestParam String name, Model model) {
            model.addAttribute("today", LocalDateTime.now());
            model.addAttribute("name", name);
        }
    
        @GetMapping("/world") // /hello/world 매핑
        public void helloWorld() {
            System.out.println("hello world!!");
        }
    
    }

     

     

    다음은 REST형 웹 서비스를 설계할 때 많이 사용하는 @PathVariable에 관한 설명인데요, 중괄호 안에 memberId는 URL에 포함된 값을 핸들러 메서드의 매개 변수로 받겠다는 의미입니다. @PathVariable을 사용하면 쿼리 스트링 없이 URL을 깔끔하게 구성할 수 있습니다.

    @RestController
    @RequestMapping("/members")
    public class MemberController {
    
        @GetMapping("/{memberId}")
        public String member(@PathVariable Long memberId) {
            return "ID: " + memberId;
        }
    
    }
    http://localhost:8080/members/101
    ---
    ID: 101

     

     

    스프링은 @RequestMapping 대신 사용할 수 있는 요청 메서드 별 전용 애너테이션을 다섯 가지 제공합니다. API를 RESTful 하게 설계하기 위해서는 Resource에 대한 CRUD를 아래 다섯 가지 요청 메서드로 적절하게 표현하는 것이 중요합니다.

    요청 메서드 애너테이션  설명
    POST @PostMapping Create : 리소스 생성
    /members
    GET @GetMapping Read : 리소스를 목록 또는 하나 조회
    /members
    /members/{id}
    PUT @PutMapping Update : 리소스 전체 필드 수정
    /members/{id}
    PATCH @PatchMapping Update : 리소스 부분 수정
    /members/{id}
    DELETE @DeleteMapping Delete : 리소스 삭제
    /members/{id}

     

     

     

     

     

    핸들러 인터셉터

     

    스프링 MVC에서 웹 요청은 핸들러 인터셉터로 가로채서 전처리/후처리를 할 수 있습니다.

     

    인터셉터는 HandlerInterceptor 인터페이스를 구현해야 하고, preHandle(), postHandle(), afterCompletion() 세 개의 메서드를 사용할 수 있습니다.

     

    다음 코드는 웹 요청을 처리하는데 걸린 시간을 측정하여 로그로 남기는 인터셉터입니다.

     

    preHandle() 메서드는 handler가 요청을 처리하기 전에 호출이 되고, 요청 처리를 시작한 시각을 요청의 startTime 속성에 담아줍니다. 그리고 true를 반환해줘야 DispatcherServlet이 요청 처리를 계속 진행하며, false를 반환할 경우 핸들러까지 요청이 전달되지 않고 바로 응답을 반환합니다.

     

    postHandle() 메서드는 handler가 요청을 처리한 후에 호출이 되고, 요청이 끝난 시각과 startTime을 비교해서 해당 요청을 처리한 시간을 로그로 남깁니다.

    @Slf4j
    public class MeasurementInterceptor implements HandlerInterceptor {
    
        // handler 요청 처리 전
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            String ip = request.getRemoteAddr();
            log.info(String.format("START URL ==> %-35s TIME ==> %-25s IP ==> %s", request.getRequestURI(), now().format(ofPattern("yyyy-MM-dd HH:mm:ss")), ip));
            request.setAttribute("startTime", System.nanoTime());
    
            return true;
        }
    
        // handler 요청 처리 후
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
            long endTime = System.nanoTime();
            long startTime = (long) request.getAttribute("startTime");
            request.removeAttribute("startTime");
            log.info(String.format("END URL   ==> %-35s {executed in %d msec}", request.getRequestURI(), (endTime - startTime) / 1000000));
        }
    
        // View 렌더링 완료 후
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    
        }
    }

     

    인터셉터는 WebMvcConfigurer 인터페이스를 구현한 자바 구성 클래스에서 addInterceptors() 메서드를 오버라이드하여 추가할 수 있습니다. 인터셉터는 기본적으로 모든 @Controller에 적용되기 때문에 URL 매핑 패턴을 정의해서 선택적으로 사용할 수 있습니다.

    @Configuration
    public class WebConfig implements WebMvcConfigurer {
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(measurementInterceptor())
                    .addPathPatterns("/hello/**"); // URL이 /hello로 시작하는 핸들러만 적용 (default /**)
    
            registry.addInterceptor(exampleInterceptor())
                    .excludePathPatterns("/hello/**"); // URL이 /hello로 시작하는 핸들러는 적용 안됨
        }
    
        @Bean
        public MeasurementInterceptor measurementInterceptor() {
            return new MeasurementInterceptor();
        }
    
        @Bean
        public ExampleInterceptor exampleInterceptor() {
            return new ExampleInterceptor();
        }
    }

     

    /hello/world 호출 시 실행 시간 로깅

     

     

    유저 로케일 해석하기

     

    스프링 MVC 애플리케이션에서 유저의 로케일은 LocaleResolver 인터페이스를 구현한 로케일 리졸버가 식별하는데요, 스프링은 식별 방법에 따라 여러 가지 리졸버를 제공합니다. 

     

    로케일 리졸버는 LocalResolver 타입 빈으로 등록이 되고, DispatcherServlet당 하나만 등록할 수 있습니다.

     

     

     

    HTTP 요청 헤더에 따라 로케일 해석하기

     

    스프링에 등록된 기본 로케일 리졸버는 AcceptHeaderLocaleResolver 구현체인데요, 요청의 Accept-Language 헤더 값에 따라 로케일을 해석합니다. 유저 웹 브라우저는 자신이 실행되고있는 운영체제의 로케일 설정으로 이 헤더 값을 결정합니다.

     

    브라우저에 설정된 Accept-Language 헤더

     

    실제로 요청을 해보면 LocaleResolver에 의해서 해석된 Locale이 핸들러의 매개변수로 들어온 것을 확인할 수 있습니다.

    @Controller
    @RequiredArgsConstructor
    @RequestMapping("/hello")
    public class HelloController {
    
        private final MessageSource messageSource;
        private final LocaleResolver localeResolver;
    
        @GetMapping("/world")
        public void helloWorld(Locale locale) {
            System.out.println("locale = " + locale);
    
            String greeting = messageSource.getMessage("greeting", new String[]{"min"}, locale);
            System.out.println("greeting = " + greeting);
        }
    
    }
    
    
    -- 결과 --
    locale = ko_KR
    greeting = 안녕, min

     

     

     

    세션 속성에 따라 로케일 해석하기

     

    SessionLocaleResolver는 유저 세션에 정의된 속성에 따라 로케일을 해석합니다. 세션이 없으면  Accept-Language 헤더를 사용합니다.

     

    로케일 리졸버를 SessionLocaleResolver로 등록하게 되면 setLocale() 메서드를 사용해서 세션에 로케일 정보를 등록할 수 있는데요, DB에 등록된 로그인한 사용자의 로케일 정보를 얻어서 설정하는 등의 방법으로 사용할 수 있습니다.

    @Bean
    public LocaleResolver localeResolver() {
        SessionLocaleResolver localeResolver = new SessionLocaleResolver();
        
        // 로케일 세션이 없을 경우 대체
        localeResolver.setDefaultLocale(Locale.KOREA);
        return localeResolver;
    }

     

    @Component
    @RequiredArgsConstructor
    public class I18NInterceptor implements HandlerInterceptor {
    
        private final LocaleResolver localeResolver;
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            // 유저 세션에 로케일 정보를 등록
            localeResolver.setLocale(request, response, Locale.US);
            return true;
        }
    }

     

     

     

    쿠키에 따라 로케일 해석하기

     

    CookieLocaleResolver는 유저 브라우저의 쿠키값에 따라 로케일을 해석합니다. 마찬가지로 쿠키가 없을 경우 Accept-Language 헤더를 로케일로 사용합니다.

     

    CookieLocaleResolver는 setLocale() 메서드로 사용자의 쿠키에 로케일을 저장하는데요,

    @Bean
    public LocaleResolver localeResolver() {
        CookieLocaleResolver localeResolver = new CookieLocaleResolver();
        localeResolver.setCookieName("language");
        localeResolver.setCookieMaxAge(3600);
        localeResolver.setDefaultLocale(Locale.KOREA);
        return localeResolver;
    }
    

     

    @Component
    @RequiredArgsConstructor
    public class I18NInterceptor implements HandlerInterceptor {
    
        private final LocaleResolver localeResolver;
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            // 쿠키에 로케일 정보를 등록
            localeResolver.setLocale(request, response, Locale.US);
            return true;
        }
    }

     

    사용자가 요청했을 때 웹 브라우저에 이름이 language인 쿠키가 생성되는 것을 확인할 수 있습니다.

     

     

     

     

    URL 매개변수로 로케일 변경하기

     

    LocaleChangeInterceptor는 URL에 특정한 매개변수를 감지해서 로케일을 변경하는 스프링에서 제공하는 인터셉터인데요, setParamName() 메서드로 감지하려는 매개변수 이름을 설정하고 인터셉터로 등록해줍니다.

    @Configuration
    public class WebConfig implements WebMvcConfigurer {
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(measurementInterceptor());
            registry.addInterceptor(localeChangeInterceptor());
        }
    
        @Bean
        public LocaleChangeInterceptor localeChangeInterceptor() {
            LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
            localeChangeInterceptor.setParamName("language");
            return localeChangeInterceptor;
        }
    }

     

    다음과 같이 요청 URL에 language 매개변수를 이용해서 유저 로케일을 변경할 수 있습니다.

    • http://localhost:8080/hello/world?language=en_US
    • http://localhost:8080/hello/world?language=ko_KR

     

     

     

    뷰 리졸버

     

    DispatcherServlet은 핸들러에서 요청을 처리하고 나서 보내준 뷰의 이름을 ViewResolver에게 보내는데요, ViewResolver는 뷰 이름을 해석해서 적절한 뷰 객체를 생성하고 반환해주는 역할을 합니다.

     

    스프링 MVC에서는 ViewResolver 인터페이스를 구현한 객체가 빈으로 등록되어 있으면 자동으로 감지해서 뷰 이름을 해석하는 데 사용하는데요, 스프링은 기본적으로 여러 가지 구현체를 제공합니다.

     

    개발자가 DispatcherServlet에 별도의 ViewResolver를 등록하지 않는다면 스프링은 기본적으로 등록되어있는 InternalResourceViewResolver를 사용합니다.

     

    InternalResourceViewResolver를 커스텀해서 빈으로 등록하면 prefix/suffix를 이용해서 뷰 이름을 애플리케이션의 특정 디렉토리에 매핑할 수 있습니다. 이 뷰 리볼버는 기본적으로 JSP를 지원하기 때문에 View 타입이 JstlView인데 저는 mustache를 사용하기 때문에 MustacheView 타입으로 변경해주었습니다.

    @Bean
    public InternalResourceViewResolver viewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/templates/");
        viewResolver.setSuffix(".mustache");
        viewResolver.setViewClass(MustacheView.class);
        return viewResolver;
    }

     

    이렇게 뷰 리졸버를 커스텀하게 되면 핸들러에서 뷰 이름을 반환할 때 "/templates/hello.mustache"처럼 풀 네임이 아닌 파일의 이름인 "hello"만 반환하면 뷰 리졸버가 prefix와 suffix를 붙여서 뷰 템플릿을 찾게 됩니다.

     

     

    Spring Boot 사용 시

     

    Spring Boot를 사용한다면 이렇게 일일이 뷰 리졸버를 커스텀할 필요 없이 사용하고자 하는 뷰 템플릿의 의존 라이브러리를 등록하면 자동으로 그에 맞는 뷰 리졸버를 등록합니다.

     

    implementation 'org.springframework.boot:spring-boot-starter-mustache'

     

    mustache 의존체를 등록한 후에 DispatcherServlet을 디버깅해보면 여러 뷰 리졸버 중 MustacheViewResolver가 등록되어 있고, viewClass와 prefix, suffix가 적절하게 설정되어 있는 것을 확인할 수 있습니다.

     

     

     

     

     

     

    예외 매핑하기

     

    웹 애플리케이션을 개발하다 보면 예기치 못한 예외가 발생하거나 개발자가 의도적으로 특정 상황에 예외를 발생시켜서 더 이상 비즈니스 로직을 진행하지 않게 하는 경우가 생기는데요, 이럴 경우에 로직을 중단시키는 것에 끝나는 것이 아니라 클라이언트에게 어떤 문제가 있었는지 전달할 필요가 있습니다.

     

    예를 들면, 사용자가 중복된 아이디를 입력한 경우 회원가입 작업을 중단하고 클라이언트에게 중복된 아이디라는 것을 알려줘야 합니다.

     

    이럴 때 컨트롤러 메서드에 @ExceptionHandler 애너테이션을 붙여서 예외 핸들러를 매핑할 수 있는데요,

     

    join() 핸들러의 로직 처리 도중에 예외가 발생했을 때 해당 타입의 예외를 처리하는 예외 핸들러에 매핑 시켜서 한 곳에서 예외 처리를 할 수 있습니다. 

    @RestController
    @RequiredArgsConstructor
    @RequestMapping("/members")
    public class MemberController {
    
        private final MemberService memberService;
    
        @ExceptionHandler(DuplicateKeyException.class)
        public String handle(DuplicateKeyException e) {
            return e.getMessage();
        }
    
        @PostMapping("")
        public String join(@RequestParam String id) {
            if (memberService.existsId(id))
                throw new DuplicateKeyException(id + "는 이미 존재합니다.");
    
            return "회원가입완료";
        }
    
    }

     

    예외 핸들러에서는 예외의 메시지를 반환하도록 하였고 요청을 보냈을 때 의도적으로 예외를 발생시켰는데요, 아래 요청을 보내서 테스트하면 예상대로 예외 메시지가 출력되는 것을 확인할 수 있습니다.

    POST http://localhost:8080/members?id=min

    min는 이미 존재합니다.

     

    한 가지 문제는 handle() 예외 핸들러 메서드가 MemberController에서 발생한 예외에 대해서만 매핑한다는 점인데요, @ControllerAdvice(또는 @RestControllerAdvice) 애너테이션을 사용해서 전역 컨트롤러에서 발생하는 예외를 모두 매핑할 수 있습니다.

    @RestControllerAdvice
    public class GlobalExceptionController {
    
        @ExceptionHandler(DuplicateKeyException.class)
        public String handle(DuplicateKeyException e) {
            return e.getMessage();
        }    
    }

     

     

     

     

     

    표준 애너테이션(JSR-303)으로 빈 검증하기

     

     

    웹 애플리케이션을 개발할 때 JSR-303 표준 애너테이션을 이용해서 자바 빈의 유효성을 검증할 수 있습니다.

    JSR-303은 자바 빈에 애너테이션을 붙여서 유효성 검증하는 방법을 표준화한 명세입니다.

     

    JSR-303 명세의 목표는 자바 빈 클래스에 직접 애너테이션을 붙여 대상 코드에 직접 규칙을 지정할 수 있습니다. 

     

    Member 클래스의 name 필드에 null을 허용하지 않은 @NotNull 애너테이션과 최소 2자 이상의 문자만 허용하도록 @Size(min = 2)을 붙여주었습니다.

    @Data
    public class Member {
    
        @NotNull
        @Size(min = 2)
        private String name;
    
        @Min(14)
        @Max(150)
        private int age;
    
    }

     

    Member 클래스를 컨트롤러에서 다음과 같이 @Valid 애너테이션을 붙여서 사용할 수 있습니다.

    @PostMapping("")
    public Member join(@RequestBody @Valid Member member) {  
        return member;
    }

     

    Member에 정의된 규칙에 맞지 않는 값이 들어오면 400 Bad Request 응답을 반환해주는데, BindingResult 매개변수를 같이 받으면 규칙에 맞지 않더라도 BindingResult 객체에 에러 내용이 담긴 채로 핸들러를 실행하기 때문에 다양한 로직을 추가할 수 있습니다.

    @PostMapping("")
    public Member join(@RequestBody @Valid Member member, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            List<FieldError> fieldErrors = bindingResult.getFieldErrors();
    
            for (FieldError fieldError : fieldErrors) {
                System.out.println(fieldError.getDefaultMessage());
            }
        }
    
        return member;
    }

     

     

    반응형
Designed by Tistory.