회원로그인, 회원가입 등등 여러 기능에서 사용자에게 예외를 던져주어야 하는 경우가 생긴다.(예를 들면 회원 로그인 아이디로 이미 가입되어 있는 회원인데 중복해서 가입하려는 등의 문제)
이때 무지성으로 예외를 던지게 되면 사용자가 해당 서비스를 이용할 때 많은 어려움을 겪을 것이다. (별다른 처리 없이 예외만 던지면 서비스가 종료되거나 사용자가 서비스 이용에 많은 불편함을 겪게 되기 때문이다.)
그렇기 때문에 사용자의 접근이 비정상적이라고 생각이 들면 개발자인 내가 해당 접근에 대한 예외를 적절하게 던져주고 던진 예외를 ExceptionHandler와 (Rest)ControllerAdvice를 사용해서 사용자에게 보다 더 나은 서비스를 제공하고자 했다.
또한 MSA구조로 이루어진 나의 프로젝트의 경우엔 RestTemplate을 사용한 통신이 빈번했고, RestTemplate을 사용해서 서버끼리 통신을 했을 때 넘어오는 오류가 400번대 에러임에도 불구하고 500번대 서버 에러로 넘어오는 등의 문제가 있어서 이를 핸들링하고 싶어서 예외를 핸들링하기로 했다.
사용자는 개발자가 만든 서비스를 이용할 때 발생할 수 있는 에러를 미리 사전에 예측해서 사용자가 서비스를 이용하는 동안에 불편함이 없도록 유도하기 위해서 에러를 핸들링
하는 과정이 필요하다.
비슷하지만 발생 주체가 다르다. 에러는 컴퓨터가 발생시키는 것을 의미하고 예외는 개발자가 의도적으로 발생시키는 것을 의미한다.
또한 예외와 에러 모두 발생하면 프로그램이 종료된다는 공통점이 있지만 예외는 예외처리를 통해 프로그램의 종료 없이 프로그램을 계속해서 사용자가 사용할 수 있게 할수 있다는 차이점이 있다.
Error?
에러는 언어의 문법과 관련된 에러, 통신 장애로 인한 에러등 컴퓨터가 코드를 실행하는 과정 자체에서 발생하는 것들을 에러라고 일컫는다.
개발자가 의도적으로 발생시키는 예외?
예외는 코드가 실행되는 일에는 큰 문제가 없지만 개발자가 판단하기에 정상적 상황이 아닌 과정이 일어날 때 예외를 발생시키는 것을 개발자가 의도적으로 발생시키는 예외라고 일컫는다.
[예시 상황]
이미 가입된 회원 아이디를 사용해서 가입하려는 회원이 있는 경우 (AlreadyExistExceptin과 같은 400번대 예외를 던져서 해당 경우를 핸들링)
이미 탈퇴환 회원을 찾으려고 할 때 예외를 던져서 해당 경우를 핸들링
@(Rest)ControllerAdvice
를 사용하지 않고 특정 컨트롤러에@ExceptionHandler
만을 적용한 경우
[에러 응답에 사용될 클래스]
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ErrorResponse{
private HttpStatus errorCode;
private String errorMessage;
public static ErrorResponse of(HttpStatus errorCode,String errorMessage) {
return ErrorResponse.builder()
.errorCode(errorCode)
.errorMessage(errorMessage)
.build();
}
}
[RestController와 @ExceptionHandler]
@RestController
public class ExceptionHandlerController {
@GetMppint("/member/{loginId}")
public ResponseEntity<Member> getMemberByLoginId(){
//1. 로그인 아이디를 사용해서 해당 회원을 찾아오는 로직이 있다고 예를 들어 봅시다.
// 2. 그때 여기서 회원이 없어서 NotFoundMemberException이 발생했다고 생각해봅시다
throw new NotFoundMemberException();
}
@ExceptionHandler(NotFoundMemberException.class)
public ResponseEntity exceptionHanlerMethod(Exception e) {
System.err.println(e.getClass());
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ErrorResponse.of(
HttpStatus.NOT_FOUND, "not found member@"));
}
}
ResponseEntity.status(HttpStatus.NOT_FOUND).body(ErrorResponse.of(HttpStatus.NOT_FOUND, "not found member"));
를 실행해서 아래와 같은 json 형태의 응답을 받을 것이다.[해당 오류가 발생했을 때 ExceptionHandler가 적용된 응답처리 결과]
body로 해당 json message가 출력할 수 있게 설정해두었음..
{
errorCode:
errorMessage: "not found member@"
}
@(Rest)ControllerAdvice를 사용하지 않고 특정 컨트롤러에@ExceptionHandler 역시 적용하지 않은 경우
[에러 응답에 사용될 클래스]
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ErrorResponse{
private HttpStatus errorCode;
private String errorMessage;
public static ErrorResponse of(HttpStatus errorCode,String errorMessage) {
return ErrorResponse.builder()
.errorCode(errorCode)
.errorMessage(errorMessage)
.build();
}
}
[RestController만 있는 경우 - ExceptionHandler가 해당 컨트롤러 내에 없을 경우]
@RestController
@RequiredArgsConstructor
class TestRestControllerTwo{
private final QueryMemberService queryMemberService;
@GetMapping("/test/member/{loginId}")
public ResponseEntity<MemberLoginResponse> getMemberByLoginId(@PathVariable(name = "loginId") String loginId){
//1. 로그인 아이디를 사용해서 해당 회원을 찾아오는 로직이 있다고 예를 들어 봅시다.
// 2. 그때 여기서 회원이 없어서 NotFoundMemberException이 발생했다고 생각해봅시다
MemberLoginResponse member = queryMemberService.findMemberByLoginId(loginId);
if(Objects.isNull(member)){
throw new NotFoundMemberException();
}
return ResponseEntity.status(HttpStatus.OK).body(member);
}
[ExceptionHandler 적용 없이 예외 발생할 경우]
package com.yaloostore.shop;
import com.yaloostore.shop.member.controller.MemberLoginHistoryRestController;
import com.yaloostore.shop.member.dto.response.MemberIdResponse;
import com.yaloostore.shop.member.dto.response.MemberLoginResponse;
import com.yaloostore.shop.member.entity.Member;
import com.yaloostore.shop.member.exception.NotFoundMemberException;
import com.yaloostore.shop.member.service.inter.QueryMemberService;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.util.Objects;
import static org.hamcrest.Matchers.equalTo;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.when;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.log;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
@WebMvcTest(TestRestController.class)
@AutoConfigureRestDocs
public class test {
@Autowired
private MockMvc mockMvc;
@MockBean
private QueryMemberService service;
@Test
@WithMockUser
void test() throws Exception {
String loginId = "test";
when(service.findMemberByLoginId(loginId)).thenThrow(NotFoundMemberException.class);
ResultActions perform = mockMvc.perform(get("/member/{loginId}", loginId)
.with(csrf())
.contentType(MediaType.APPLICATION_JSON));
perform.andDo(print())
.andExpect(MockMvcResultMatchers.status().isNotFound())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.errorCode", equalTo(HttpStatus.NOT_FOUND.getReasonPhrase())))
.andExpect(jsonPath("$.errorMessage", equalTo("not found member@")));
}
@Test
@WithMockUser
void test_two() throws Exception {
String loginId = "test";
when(service.findMemberByLoginId(loginId)).thenThrow(NotFoundMemberException.class);
ResultActions perform = mockMvc.perform(get("/test/member/{loginId}", loginId)
.with(csrf())
.contentType(MediaType.APPLICATION_JSON));
perform.andDo(print())
.andExpect(MockMvcResultMatchers.status().isNotFound())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.errorCode", equalTo(HttpStatus.NOT_FOUND.getReasonPhrase())))
.andExpect(jsonPath("$.errorMessage", equalTo("not found member@")));
}
}
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
class ErrorResponse{
private String errorCode;
private String errorMessage;
public static ErrorResponse of(HttpStatus errorCode, String errorMessage){
return ErrorResponse.builder()
.errorCode(errorCode.getReasonPhrase())
.errorMessage(errorMessage)
.build();
}
}
@RestController
@RequiredArgsConstructor
class TestRestController{
private final QueryMemberService queryMemberService;
@GetMapping("/member/{loginId}")
public ResponseEntity<MemberLoginResponse> getMemberByLoginId(@PathVariable(name = "loginId") String loginId){
//1. 로그인 아이디를 사용해서 해당 회원을 찾아오는 로직이 있다고 예를 들어 봅시다.
// 2. 그때 여기서 회원이 없어서 NotFoundMemberException이 발생했다고 생각해봅시다
MemberLoginResponse member = queryMemberService.findMemberByLoginId(loginId);
if(Objects.isNull(member)){
throw new NotFoundMemberException();
}
return ResponseEntity.status(HttpStatus.OK).body(member);
}
@ExceptionHandler(NotFoundMemberException.class)
public ResponseEntity exceptionHandlerMethod(Exception e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ErrorResponse.of(
HttpStatus.NOT_FOUND, "not found member@"));
}
}
@RestController
@RequiredArgsConstructor
class TestRestControllerTwo{
private final QueryMemberService queryMemberService;
@GetMapping("/test/member/{loginId}")
public ResponseEntity<MemberLoginResponse> getMemberByLoginId(@PathVariable(name = "loginId") String loginId){
//1. 로그인 아이디를 사용해서 해당 회원을 찾아오는 로직이 있다고 예를 들어 봅시다.
// 2. 그때 여기서 회원이 없어서 NotFoundMemberException이 발생했다고 생각해봅시다
MemberLoginResponse member = queryMemberService.findMemberByLoginId(loginId);
if(Objects.isNull(member)){
throw new NotFoundMemberException();
}
return ResponseEntity.status(HttpStatus.OK).body(member);
}
}
org.springframework.web.client.HttpServerErrorException$
InternalServerError: 500 :
"{"timestamp":"2023-07-06T01:44:16.253+00:00",
"status":500,
"error":"Internal Server Error",
"trace":"com.yalooStore.common_utils.exception.ClientException: member not found
@ExceptionHandler(ClientException.class)
public ResponseEntity<ResponseDto<Object>> clientExceptionHandler(ClientException e){
ResponseDto<Object> response = ResponseDto.builder()
.success(false)
.status(e.getResponseStatus())
.errorMessages(List.of(e.getDisplayErrorMessage())).build();
return ResponseEntity.status(e.getResponseStatus()).body(response);
}
ClientException
은 custom한 Exception입니다.ResponseDto
역시 custom한 dto객체 입니다.try {
ResponseEntity<ResponseDto<MemberLoginResponse>> memberLoginResponse = restTemplate
.exchange(uri.toUri(),
HttpMethod.GET,
httpEntity,
new ParameterizedTypeReference<>() {
});
MemberLoginResponse data = memberLoginResponse.getBody().getData();
User user = new User(data.getLoginId(), data.getPassword(), getAuthorities(data));
return user;
} catch (HttpClientErrorException e){
throw new UsernameNotFoundException("not found username");
}
-> 에러가 아닌 경우에 한해서 개발자가 사용자의 접근이 옳지 않다고 판단하는 경우를 직접 판단해 해당 경우에 한해 적절한 예외를 던지도록 한다.
-> 던진 예외를 ExceptionHandler를 통해서 특정 컨트롤러에서만 관리할 수도 있고 @(Rest)ControllerAdvice를 사용해서 모든 컨트롤러에 대해서 ExceptionHandler를 적용하여 예외를 관리 할수 있게 된다.
-> 결과적으로 이런 예외 핸들링은 사용자에게 더 높은 서비스를 제공할 수 있으기 때문에 진행한다고 할수 있다.
-> @ExceptionHandler를 특정 메서드에 붙여서 처리할수 있다.
-> 더 넓은 범위에서 예외를 다루고 싶다면 @(Rest)Controller를 사용해서 해당 예외를 다룰 수 있게 한다.
-> 둘은 예외를 다룬다는 점에서 공통점이 있다.
-> 그러나 둘은 범위의 차이가 있다 ExceptionHandler는 특정 메서드에 붙이면 해당 컨트롤러의 메서드에서 발생한 예외에만 동작하지만 @(Rest)ControllerAdvice에 ExceptionHandler를 작성해두면 모든 컨트롤러에서 발생하는 예외를 비교해서 예외를 다룰 수 있게 된다.