[프로젝트 생성]
1. java, jdk, spring boot 버전 확인
2. 초기라이브러리
- spring-web
- thymleaf
- lombok
[MTV pattern]
1. 의존성
1) @Component 와 @Autowired
스프링 bean 으로 등록하기 위해선 @Component 어노테이션을 달아주어야 한다. 만약 해당 객체 (Java는 모든것이 객체) 의 생성자를 만들때 의존성 주입이 필요하다면 @Autowired로 빌드 시 자동으로 주입되도록 한다.
생성자를 만드는 규칙은 3가지 이다
• setter 주입 (수정자 주입)
• field 주입
• 일반 메소드 주입
• 생성자 주입
권장되는 방법은 이중 "생성자 주입" 방식이다. 만약 생성자가 단 1개라면 @Autowired의 생략이 가능하다.
@Controller
@RequestMapping("/basic/items")
public class BasicItemController {
private final ItemRepository itemRepository;
@Autowired // 생성자가 단 한개이므로 생략 가능하다
public BasicItemController(ItemRepository itemRepository) {
this.itemRepository = itemRepository;
}
}
이때 required = False 처리를 하게 되면 주입 대상이 없을 경우 컴파일 에러가 발생하지 않지만 실제 주입 대상이 존재하지 않으면 호출되지 않는다.
@Controller
@RequestMapping("/basic/items")
public class BasicItemController {
private final ItemRepository itemRepository;
@Autowired(required=false)
public BasicItemController(ItemRepository itemRepository) {
this.itemRepository = itemRepository;
}
}
2. 생성자
1) @RequiredArgsConstructor
Lombok의 어노테이션을 사용하면 final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다.
이때 해당 객체는 생성자 주입시 필수 객체로 인지된다.
@Controller
@RequestMapping("/basic/items")
@RequiredArgsConstructor
public class BasicItemController {
private final ItemRepository itemRepository;
}
2) @AllArgsConstructor
모든 값을 필요로 하는 생성자를 만들어준다.
@Data
@AllArgsConstructor // 해당 객체를 만들려면 모든 값을 넘겨야 한다는 뜻
static class UpdateMemberResponse {
private Long id;
private String name;
}
위 객체를 만들기 위해서는 아래와 같이 생성해야 한다.
UpdateMemberResponse res = new UpdateMemberResponse(findMember.getId(), findMember.getName());
3) @NoArgsConstructor
기본생성자를 생성한다.
3. Controller
1) @Controller
스프링 빈으로 등록하기 위해 Component로 등록해야 하지만 Controller 어노테이션에 Component 어노테이션이 부착되어 있으므로 Contoller로 등록한다.
2) @RestController
@Controller 는 응답값이 html 파일로 전송되게 한다. @ResponseBody를 쓰면 되긴 하지만...
일반적으로 rest api 개발시 해당 동작은 불필요 하므로 간편하게 @RestController를 사용한다. (아래 예시)
- Controller사용과 메소드에 적용
@Controller
public class RequestBodyJsonController {
private ObjectMapper objectMapper = new ObjectMapper();
@ResponseBody
@PostMapping("/hello")
public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException {
HelloData data = objectMapper.readValue(messageBody, HelloData.class);
log.info("username={}, age={}", data.getUsername(), data.getAge());
return "ok";
}
- RestController 사용
@RestController
public class RequestBodyJsonController {
private ObjectMapper objectMapper = new ObjectMapper();
@PostMapping("/hello")
public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException {
HelloData data = objectMapper.readValue(messageBody, HelloData.class);
log.info("username={}, age={}", data.getUsername(), data.getAge());
return "ok";
}
3) @RequestBody
HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
개별 메소드에 적용할 수 있고, RestController를 사용함으로써 생략할 수도 있다.
4) @ResponseBody
- 모든 메서드에 @ResponseBody 적용
- 메시지 바디 정보 직접 반환(view 조회X)
- HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
[Bean 등록]
1) @Configuration
직접 스프링 빈을 등록하기 위해서는 클래스를 구성정보/설정정보로 나타내주고 하위에 등록한다.
@Configuration // 구성정보, 설정정보
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
}
위 예제에서 memberService를 스프링 빈으로 등록하려는데 memberRepository에 대한 의존성을 갖고있다. 따라서 이것도 스프링 빈으로 등록해 주어야 한다.
@Configuration // 구성정보, 설정정보
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
[Validation]
값에 대한 검증은 필수이다. 모든걸 비즈니스 로직으로 처리하면 공수가 크고 유지보수가 어려우므로 스프링에서 제공하는 @Validated나 자바 표준 모듈인 @Valid를 사용한다. 둘중 어느걸 사용해도 상관 없다고 한다. 스프링 이라는 틀은 변하지 않을 것 같으므로 @Validated를 사용한다.
1) 직접 검증기 생성하기 (Validator 구현체 생성)
public interface Validator {
boolean supports(Class<?> clazz);
void validate(Object target, Errors errors);
}
위 인터페이스를 구현하는 나만의 Validator를 구현한다.
package hello.itemservice.web.validation;
import hello.itemservice.domain.item.Item;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
@Component
public class ItemValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Item.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
Item item = (Item) target;
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "itemName", "required");
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
errors.rejectValue("price", "range", new Object[]{1000, 1000000}, null);
}
if (item.getQuantity() == null || item.getQuantity() > 10000) {
errors.rejectValue("quantity", "max", new Object[]{9999}, null);
}
//특정 필드 예외가 아닌 전체 예외
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
errors.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
}
}
}
- 검증기 등록하기
@Component
public class ItemValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Item.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
}
}
#1 특정 컨트롤러에 적용하기
// 적용할 클래스 안에서
@InitBinder
public void init(WebDataBinder dataBinder) {
log.info("init binder {}", dataBinder);
dataBinder.addValidators(itemValidator);
}
#2 모든 컨트롤러에 적용하기
@SpringBootApplication
public class ItemServiceApplication implements WebMvcConfigurer {
public static void main(String[] args) {
SpringApplication.run(ItemServiceApplication.class, args);
}
@Override
public Validator getValidator() {
return new ItemValidator();
}
}
위 방법을 사용하게 될 경우 클래스 내의 InitBinder 어노테이션을 제거하여도 검증기로 등록되어 support 하는 클래스의 Validatior를 실행한다.
하지만 2번처럼 글로벌 설정을 하면 아래의 BeanValidator가 자동 등록되지 않고, 실제로 글로벌 설정을 직접 사용하는 경우는 드물다.
2) Bean Validation
해당 기능을 사용하기 위해선 의존관계 추가가 필요하다
implementation 'org.springframework.boot:spring-boot-starter-validation'
Bean Validation을 구현한 기술중에 일반적으로 사용하는 구현체는 하이버네이트 Validator이다.
Jakarta Bean Validation
- jakarta.validation-api : Bean Validation 인터페이스
- hibernate-validator : 구현체
package hello.itemservice.domain.item;
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Data
public class Item {
private Long id;
@NotBlank(message = "공백은 입력할 수 없습니다.")
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
@NotNull
@Max(9999)
private Integer quantity;
public Item() {}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
검증 애노테이션
- @NotBlank : 빈값 + 공백만 있는 경우를 허용하지 않는다.
- @NotNull : `null` 을 허용하지 않는다.
- @Range(min = 1000, max = 1000000) : 범위 안의 값이어야 한다.
- @Max(9999) : 최대 9999까지만 허용한다.
- @Valid
javax.validation.@Valid` 를 사용하려면 `build.gradle` 의존관계 추가가 필요다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
- @Validated
@Validated` 는 검증기를 실행하라는 애노테이션이다.
이 애노테이션이 붙으면 앞서 `WebDataBinder` 에 등록한 검증기를 찾아서 실행한다. 그런데 여러 검증기를 등록한다 면 그 중에 어떤 검증기가 실행되어야 할지 구분이 필요하다. 이때 `supports()` 가 사용된다. 여기서는 `supports(Item.class)` 호출되고, 결과가 `true` 이므로 `ItemValidator` 의 `validate()` 가 호출된다.
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
log.info("errors={}", bindingResult);
return "validation/v3/addForm";
}
//성공 로직
Item savedItem = itemRepository.save(item); redirectAttributes.addAttribute("itemId", savedItem.getId()); redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v3/items/{itemId}";
}
이때 @ModelAttribute에서도 바인딩에 대한 검증이 이뤄지는데 @ModelAttribute각각의 필드 타입 변환을 시도하고, 변환에성공한필드만BeanValidation적용된다.
- @ModelAttribute vs @RequestBody
- @ModelAttribute 는 필드 단위로 정교하게 바인딩이 적용된다. 특정 필드가 바인딩 되지 않아도 나머지 필드 는 정상 바인딩 되고, Validator를 사용한 검증도 적용할 수 있다.
- @RequestBody 는 HttpMessageConverter 단계에서 JSON 데이터를 객체로 변경하지 못하면 이후 단계 자 체가 진행되지 않고 예외가 발생한다. 컨트롤러도 호출되지 않고, Validator도 적용할 수 없다.
[Filter]
1) Filter의 흐름
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
2) Filter의 생성
package hello.login.web.filter;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("log filter init");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
String uuid = UUID.randomUUID().toString();
try {
log.info("REQUEST [{}][{}]", uuid, requestURI);
chain.doFilter(request, response);
} catch (Exception e) {
throw e;
} finally {
log.info("RESPONSE [{}][{}]", uuid, requestURI);
}
}
}
Filter 인터페스를 구체화하여 나만의 Filter를 생성한다. 이때 Filter 인터페이스의 init과 doFilter 메소드를 오버라이딩 하여 로직을 구현한다.
주의해야 할 점은 doFilter 로직 안에서 필터 로직을 진행 한후 doFilter를 호출해야 한다는 것이다. 이때 다음 필터가 있다면 그 필터가 실행되고, 필터가 없다면 서블릿을 호출한다.
chain.doFilter(request, response);
3) Filter 등록
만든 Filter를 스프링이 인식하여 실제 동작하게 하기 위해선 Config에 등록해야 한다.
package hello.login;
import hello.login.web.filter.LogFilter;
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 logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
}
Filter를 등록할때에는 FilterRegistrationBean 클래스를 사용한다.
setOrder를 통해 실행될 필터의 우선순위를 설정하고, addUrlPatterns를 통해 어떤 경로 호출시 필터를 실행시킬지 결정한다.
4) Filter 특징
chain.doFilter를 통해 request와 response를 넘길때 해당 객체의 타입을 조작할 수 있다. Filter인터페이스의 기본 형은 Servlet~ 으로 사용할수 있는 메소드가 한정적이어서 doFilter 메소드 내에서 다운그레이드 하여 Http 객체로 변경처리하였다. 변경된 이 상태를 넘길 수 있다.
5) Filter와 에러처리
1. WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
2. WAS `/error-page/500` 다시 요청 -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러(/error-page/500) -> View
스프링MVC에서 오류 발생시 WAS로 에러가 전달되고, 해당 에러를 처리하기 위해 다시 Filter -> MVC의 흐름이 진행된다. 이럴때 정상 호출시에만 Filter만 호출되게 하고, 에러처리로 인한 실행에는 필터가 동작하지 않게 명시적으로 지정 할 수 있다.
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
return filterRegistrationBean;
}
Filter를 Bean으로 등록시 dispatcherType을 명시하지 않으면 default 값으로 REQUEST가 적용된다.
> DispatcherType 종류
- REQUEST : 클라이언트 요청
- ERROR : 오류 요청
- FORWARD : MVC에서 배웠던 서블릿에서 다른 서블릿이나 JSP를 호출할 때 RequestDispatcher.forward(request, response)
- INCLUDE : 서블릿에서 다른 서블릿이나 JSP의 결과를 포함할 때 RequestDispatcher.include(request, response)
- ASYNC : 서블릿 비동기 호출
[Interceptor]
1) 인터셉터의 흐름
HTTP 요청 ->WAS-> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
2) 인터셉터의 생성
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
}
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
}
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
}
}
인터셉트를 구현하기 위해선 HandlerInterceptor 인터페이스를 구체화 한다.
- preHandle
: 컨트롤러 호출 전에 호출된다. (더 정확히는 핸들러 어댑터 호출 전에 호출된다.) 응답값이 "true" 이면 다음으로 진행하고, "false" 이면 더는 진행하지 않는다. "false" 인 경우 나머지 인터셉터는 물론이고, 핸들러 어댑터도 호출되지 않는다. 그림에서 1번에서 끝이 나버린다.
- postHandle
: 컨트롤러 호출 후에 호출된다. (더 정확히는 핸들러 어댑터 호출 후에 호출된다.) 이떄 컨트롤러 내부에서 에러가 발생했을 경우에는 호출되지 않는다.
- afterCompletion
: 컨트롤러의 에러 여부에 상관없이 뷰가 렌더링 된 이후에 호출된다.
3) 인터셉터의 등록
만든 인터셉터를 실행시키기 위해선 Config로 등록해야 한다. "WebMvcConfigurer" 가 제공하는 addInterceptors() 를 사용해서 인터셉터를 등록할 수 있다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "/*.ico", "/error");
}
}
- order() : 인터셉터의 호출 순서를 지정한다. 낮을 수록 먼저 호출된다.
- addPathPatterns() : 인터셉터를 적용할 URL 패턴을 지정한다.
- excludePathPatterns() : 인터셉터에서 제외할 패턴을 지정한다.
[ArgumentResolver]
Filter와 Interceptor를 통해서도 인증을 체크할수 있지만 어노테이션을 활용하면 더욱 집약적으로 원하는 메소드에만 적용 할수 있다. 이때 ArgumentResolver를 활용한다.
인증된 유저만 접근할수 있는 컨트롤러를 예시로 들면 아래와 같다.
1) 사용 예시 : @Login
@GetMapping("/")
public String homeLoginV3ArgumentResolver(@Login Member loginMember, Model model) { //세션에 회원 데이터가 없으면 home
if (loginMember == null) {
return "home";
}
//세션이 유지되면 로그인으로 이동
model.addAttribute("member", loginMember); return "loginHome";
}
위에 적용된 @Login 어노테이션이 정상적으로 동작할 수있도록 구현하면 아래와 같다.
2) 어노테이션 구현
package hello.login.web.argumentresolver;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {}
- @Target(ElementType.PARAMETER) : 파라미터에만 사용
- @Retention(RetentionPolicy.RUNTIME) : 리플렉션 등을 활용할 수 있도록 런타임까지 애노테이션 정보가 남아있음
3) 어노테이션을 실행시킬 resolver 구현
이제 이것이 실행될 수 있도록 resolver를 구현한다
package hello.login.web.argumentresolver;
import hello.login.domain.member.Member;
import hello.login.web.SessionConst;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
@Slf4j
public class LoginMemberArgumentResolver implements
HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
log.info("supportsParameter 실행");
boolean hasLoginAnnotation =
parameter.hasParameterAnnotation(Login.class);
boolean hasMemberType =
Member.class.isAssignableFrom(parameter.getParameterType());
return hasLoginAnnotation && hasMemberType;
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
log.info("resolveArgument 실행");
HttpServletRequest request = (HttpServletRequest)
webRequest.getNativeRequest();
HttpSession session = request.getSession(false);
if (session == null) {
return null;
}
return session.getAttribute(SessionConst.LOGIN_MEMBER);
}
}
- supportsParameter() : @Login 애노테이션이 있으면서 Member 타입이면 해당 ArgumentResolver 가 사용된다
- resolveArgument() : 컨트롤러 호출 직전에 호출 되어서 필요한 파라미터 정보를 생성해준다. 여기서는 세션 에 있는 로그인 회원 정보인 member 객체를 찾아서 반환해준다. 이후 스프링MVC는 컨트롤러의 메서드를 호출 하면서 여기에서 반환된 member 객체를 파라미터에 전달해준다.
4) 스프링 컨테이너에 등록
마지막으로 생성한 LoginMemberArgumentResolver를 빈으로 등록한다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver());
}
}
[Exception]
REST API를 활용할때 효율적으로 에러처리 하는 방법위주
예외가 발생해 서블릿을 넘어 WAS 까지 전달되면 http status code는 500으로 처리된다.
1) 스프링부트 기본 오류 처리
스프링 부트의 기본 설정은 오류 발생시 `/error`를 오류 페이지로 요청하고, `BasicErrorController`는 이 경로를 기본으로 받는다.
이 기본경로는 .applications 파일에서 `server.erorr.path` 로 변경 가능하다.
스프링 부트는 `BasicErrorController`가 제공하는 기본 정보들을 활용하여 오류 API를 생성해준다. 노출되는 오류 메세지는 조절 가능하다.
server.error.include-binding-errors=always
server.error.include-exception=true
server.error.include-message=always
server.error.include-stacktrace=always
보안상 가릴껀 가리도록 한다!
2) API 예외 처리 : HandlerExceptionResolver
스프링 MVC는 컨트롤러(핸들러) 밖으로 예외가 던져진 경우 예외를 해결하고, 동작을 새로 정의할 수 있는 방법을 제공한다. 컨트롤러 밖으로 던져진 예외를 해결하고, 동작 방식을 변경하고 싶으면 `HandlerExceptionResolver` 를 사용하면 된다. 줄여서 `ExceptionResolver` 라 한다.
이때 ExceptionResolver로 적절한 에러처리를 하였어도 Interceptor의 postHandle()은 호출되지 않는다.
> 사용자 정의 예외 추가하기
- 나만의 Exception 종류를 추가한다.
package hello.exception.exception;
public class UserException extends RuntimeException {
public UserException() {
super();
}
public UserException(String message) {
super(message);
}
public UserException(String message, Throwable cause) {
super(message, cause);
}
public UserException(Throwable cause) {
super(cause);
}
protected UserException(String message, Throwable cause, boolean
enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
> HandlerExceptionResolver 구현하기
- 나만의 Resolver 만들고 에러종류에 따라 status code 지정하기
package hello.exception.resolver;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request,HttpServletResponse response, Object handler, Exception ex) {
try {
if (ex instanceof UserException) {
log.info("UserException resolver to 400");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
if ("application/json".equals(acceptHeader)) {
Map<String, Object> errorResult = new HashMap<>();
errorResult.put("ex", ex.getClass());
errorResult.put("message", ex.getMessage());
String result = objectMapper.writeValueAsString(errorResult);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().write(result);
return new ModelAndView();
} else {
//TEXT/HTML
return new ModelAndView("error/400");
}
}
} catch (IOException e) {
log.error("resolver ex", e);
}
return null;
}
}
발생한 에러 종류가 `IllegalArgumentException` 라면 status code 를 400으로 처리하게 한다.
- HandlerExceptionResolver의 return 값
해당 메소드에서 return 값의 종류에 따라 DispatcherServlet의 동작 방식이 달라진다.
- 빈 ModelAndView :`new ModelAndView()` 처럼 빈 `ModelAndView` 를 반환하면 뷰를 렌더링 하지 않고, 정상 흐름으로 서블릿이 리턴된다.
- ModelAndView 지정 : `ModelAndView` 에 `View` , `Model` 등의 정보를 지정해서 반환하면 뷰를 렌더링 한다.
- null : `null` 을 반환하면, 다음 `ExceptionResolver` 를 찾아서 실행한다. 만약 처리할 수 있는 `ExceptionResolver` 가 없으면 예외 처리가 안되고, 기존에 발생한 예외를 서블릿 밖으로 던진다.
- 구현한 Resolver 등록하기
WebMvcConfigurer를 통해 아래와 같이 등록한다.
/**
* 기본 설정을 유지하면서 추가 */
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
}
3) API 예외 처리 - @ExceptionHandler : 클래스 처리
위에서 언급한 HandlerExceptionResolver의 리턴값은 ModelAndView 인데 REST API 프로젝트에서는 해당 반환값을 필요로 하지 않는다. 또한 특정 컨트롤러에서만 발생하는 예외를 별도로 처리하기도 어려웠는데. 이러한 단점을 보완하는 `@ExceptionHandler` 가 있다.
> 클래스 내에서 사용하는 @ExceptionHandler
@Slf4j
@RestController
public class ApiExceptionV2Controller {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
log.error("[exceptionHandle] ex", e);
return new ErrorResult("BAD", e.getMessage());
}
@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandle(UserException e) {
log.error("[exceptionHandle] ex", e);
ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
}
@GetMapping("/api2/members/{id}")
public MemberDto getMember(@PathVariable("id") String id) {
if (id.equals("user-ex")) {
throw new UserException("사용자 오류");
}
}
- 에러 캐치
Get 메소드로 호출된 컨트롤러 안에서 UserException 발생 시 @ExceptionHandler에 의해 캐치되어 적절하게 에러처리가 이뤄진다. 해당 에러처리 Handle 에서 status code는 400 이 되고 ResponseEntity 로 return 되어 정상 리턴이 이뤄진다.
- 에러 처리 우선순위
@ExcpetionHandler 메소드를 여러개 생성할 수 있는데 캐치하는 에러의 상하관계에 따라 자식클래스가 우선순위를 가지며 에러처리가 이뤄진다. @ExcpeitonHandler의 매개변수로 해당 메서드를 통해 처리될 에러class를 정의할 수 있는데 이곳에 복수개의 등록이 가능하다.
@ExceptionHandler({AException.class, BException.class})
만약 해당 부분을 생략 할 경우 메서드의 파라미터의 예외 종류가 지정되어 처리된다.
@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandle(UserException e) {
> @ExceptionHandler의 실행 흐름
- 컨트롤러를 호출한 결과 `IllegalArgumentException` 예외가 컨트롤러 밖으로 던져진다.
- 예외가 발생했으로 `ExceptionResolver` 가 작동한다. 가장 우선순위가 높은 `ExceptionHandlerExceptionResolver` 가 실행된다.
- `ExceptionHandlerExceptionResolver` 는 해당 컨트롤러에 `IllegalArgumentException` 을 처리 할 수 있는 `@ExceptionHandler` 가 있는지 확인한다.
- `illegalExHandle()` 를 실행한다. `@RestController` 이므로 `illegalExHandle()` 에도 `@ResponseBody` 가 적용된다. 따라서 HTTP 컨버터가 사용되고, 응답이 다음과 같은 JSON으로 반환된다.
- `@ResponseStatus(HttpStatus.BAD_REQUEST)` 를 지정했으므로 HTTP 상태 코드 400으로 응답한다.
4) API 예외 처리 - @ControllerAdvice : 모든 클래스
`@ExceptionHandler` 를 사용해서 예외를 깔끔하게 처리할 수 있게 되었지만, 정상 코드와 예외 처리 코드가 하나의
컨트롤러에 섞여 있다. `@ControllerAdvice` 또는 `@RestControllerAdvice` 를 사용하면 둘을 분리할 수 있다.
#1 컨트롤러에서 예외 처리 코드 삭제
package hello.exception.exhandler;
import hello.exception.exception.UserException;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
public class ApiExceptionV2Controller {
@GetMapping("/api2/members/{id}")
public MemberDto getMember(@PathVariable("id") String id) {
if (id.equals("ex")) {
throw new RuntimeException("잘못된 사용자");
}
if (id.equals("bad")) {
throw new IllegalArgumentException("잘못된 입력 값");
}
if (id.equals("user-ex")) {
throw new UserException("사용자 오류");
}
return new MemberDto(id, "hello " + id);
}
@Data
@AllArgsConstructor
static class MemberDto {
private String memberId;
private String name;
}
}
#2 ControllerAdvice 처리 코드 작성
@Slf4j
@RestControllerAdvice(basePackages = "hello.exception.api")
public class ExControllerAdvice {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandler(IllegalArgumentException e) {
log.error("[exceptionHandler] ex", e);
return new ErrorResult("BAD", e.getMessage());
}
@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandler(UserException e) {
log.error("[exceptionHandler] ex", e);
ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
return new ResponseEntity(errorResult, HttpStatus.BAD_REQUEST);
}
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
public ErrorResult exHandler(Exception e) {
log.error("[exceptionHandler] ex", e);
return new ErrorResult("EX", "내부 오류");
}
}
RestControllerAdvice의 basePackage 파라미터를 통해 적용할 범위를 지정해 준다. 해당 범위 내에서 에러 발생시 위 파일내에서 처리 가능한 에러 종류라면 적절하게 처리되어진다.
> @ControllerAdvice
- `@ControllerAdvice` 는 대상으로 지정한 여러 컨트롤러에 `@ExceptionHandler` , `@InitBinder` 기능
을 부여해주는 역할을 한다.
- `@ControllerAdvice` 에 대상을 지정하지 않으면 모든 컨트롤러에 적용된다. (글로벌 적용)
- `@RestControllerAdvice` 는 `@ControllerAdvice` 와 같고, `@ResponseBody` 가 추가되어 있다. `@Controller` , `@RestController` 의 차이와 같다.
'DEV-ing log > Spring' 카테고리의 다른 글
[Kotlin / Spring] Spring Rest Docs 1탄 - 시작하기 : application/json (0) | 2024.09.12 |
---|---|
[Spring | H2] h2 테이블 생성 안되는 오류 ⚒️ : 예약어 문제 (0) | 2024.04.30 |
[IntelliJ] Live Template 에 커스텀 단축키 생성하기 (1) | 2024.04.28 |
[스프링 핵심원리 - 기본편] 메모 (0) | 2024.04.22 |
[스프링 입문] 메모 (0) | 2024.04.22 |