API 응답은 보통 아래와 같은 형태를 가진다.
{
isSuccess : Boolean
code : String // HTTP 상태 코드 외에 더 세부적인 결과를 알려줌
message : String // code에 추가적으로 어떤 결과인지 알려줌
result : {응답으로 필요한 또 다른 json} // 실패한 경우에는 null을 줌
}
응답의 경우 Code라는 이름의 enum으로 형태를 관리한다.
이때 성공 응답과 실패 응답을 하나의 enum으로 관리할 수도 있고, 분리할 수도 있다.
아래 실습에서는 하나로 관리하는 것을 진행하는데,
마지막에 미션을 진행할 때에는 분리해 볼 것이다.
패키지를 만들고(apiPayload로 이름 지음)
그 안에, code 패키지와 ApiResponse 클래스를 만들어주었다.

다음으로 ApiResponse를 만들어주겠다.
package umc.spring.apiPayload;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
@JsonPropertyOrder({"isSuccess", "code", "message", "result"})
public class ApiResponse<T> {
@JsonProperty("isSuccess")
private final Boolean isSuccess;
private final String code;
private final String message;
@JsonInclude(JsonInclude.Include.NON_NULL)
private T result;
// 성공한 경우 응답 생성
// public static <T> ApiResponse<T> onSuccess(T result){
// return new ApiResponse<>(true, SuccessStatus._OK.getCode() , SuccessStatus._OK.getMessage(), result);
// }
//
// public static <T> ApiResponse<T> of(BaseCode code, T result){
// return new ApiResponse<>(true, code.getReasonHttpStatus().getCode() , code.getReasonHttpStatus().getMessage(), result);
// }
// 실패한 경우 응답 생성
public static <T> ApiResponse<T> onFailure(String code, String message, T data){
return new ApiResponse<>(true, code, message, data);
}
}
result는 어떤 형태의 값이 올지 모르기 때문에 Generic으로 만들어주었다.
onSuccess 함수의 경우, 주석처리가 되어 있는데, 응답 내부에 들어갈 code를 아직 만들지 않아서 그렇다.
아래와 같은 응답을 줄 예정인데,
{
"isSuccess ": true,
"code" : "2000",
"message" : "OK",
"result" :
{
"testString" : "This is test!"
}
}
이 code와 message의 형식을 만들어보겠다.

위 사진처럼 code 패키지 밑에 있는 파일들을 만들어주었다.
code 패키지 안에 있는
BaseCode와 BaseErrorCode의 역할 : Status에서 두 개의 메소드를 반드시 Override할 것을 강제한다!
ErrorStatus
package umc.spring.apiPayload.code.status;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;
import umc.spring.apiPayload.code.BaseErrorCode;
import umc.spring.apiPayload.code.ErrorReasonDTO;
@Getter
@AllArgsConstructor
public enum ErrorStatus implements BaseErrorCode {
// 가장 일반적인 응답
_INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."),
_BAD_REQUEST(HttpStatus.BAD_REQUEST,"COMMON400","잘못된 요청입니다."),
_UNAUTHORIZED(HttpStatus.UNAUTHORIZED,"COMMON401","인증이 필요합니다."),
_FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."),
;
// 멤버 관련 응답
// ~~~ 관련 응답 ....
private final HttpStatus httpStatus;
private final String code;
private final String message;
@Override
public ErrorReasonDTO getReason() {
return ErrorReasonDTO.builder()
.message(message)
.code(code)
.isSuccess(false)
.build();
}
@Override
public ErrorReasonDTO getReasonHttpStatus() {
return ErrorReasonDTO.builder()
.message(message)
.code(code)
.isSuccess(false)
.httpStatus(httpStatus)
.build()
;
}
}
SuccessStatus
package umc.spring.apiPayload.code.status;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;
import umc.spring.apiPayload.code.BaseErrorCode;
import umc.spring.apiPayload.code.ErrorReasonDTO;
@Getter
@AllArgsConstructor
public enum SuccessStatus implements BaseErrorCode {
// 가장 일반적인 응답
_OK(HttpStatus.OK, "COMMON200", "요청에 성공했습니다."),
;
// 멤버 관련 응답
// ~~~ 관련 응답 ....
private final HttpStatus httpStatus;
private final String code;
private final String message;
@Override
public ErrorReasonDTO getReason() {
return ErrorReasonDTO.builder()
.message(message)
.code(code)
.isSuccess(false)
.build();
}
@Override
public ErrorReasonDTO getReasonHttpStatus() {
return ErrorReasonDTO.builder()
.message(message)
.code(code)
.isSuccess(false)
.httpStatus(httpStatus)
.build()
;
}
}
BaseCode
package umc.spring.apiPayload.code;
public interface BaseCode {
public ReasonDTO getReason();
public ReasonDTO getReasonHttpStatus();
}
BaseErrorCode
package umc.spring.apiPayload.code;
public interface BaseErrorCode {
public ErrorReasonDTO getReason();
public ErrorReasonDTO getReasonHttpStatus();
}
ErrorReasonDTO
package umc.spring.apiPayload.code;
import lombok.Builder;
import lombok.Getter;
import org.springframework.http.HttpStatus;
@Builder
@Getter
public class ErrorReasonDTO {
private final HttpStatus httpStatus;
private final String code;
private final String message;
private final boolean isSuccess;
}
ReasonDTO
package umc.spring.apiPayload.code;
import lombok.Builder;
import lombok.Getter;
import org.springframework.http.HttpStatus;
@Builder
@Getter
public class ReasonDTO {
private final HttpStatus httpStatus;
private final String code;
private final String message;
private final boolean isSuccess;
}
예시 API는 다음과 같고, 다음 API를 만들어주겠다.

먼저 아래와 같이 DTO를 만들고

RequestBody에 담겨오는 값은 없으므로, TempResponse만 작성해주겠다.
package umc.spring.web.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
public class TempResponse {
@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class TempTestDTO{
String testString;
}
}
+) public static class로 쓰는 이유
+) DTO에서도 빌더 패턴을 쓴다.
우리가 만드는 인스턴스들은 모두 빌더 패턴을 사용한다고 생각하면 된다.
(참고로, RequestDTO는 우리가 만드는 것이 아닌, 프론트엔드에서 만든 객체를 그저 받기에 RequestDTO는 빌더 패턴을 적용할 필요가 없다. 백엔드는 그저 받기만 하면 되니까!)
Converter 패키지에 TempConverter 클래스를 만들어준다.

package umc.spring.converter;
import umc.spring.web.dto.TempResponse;
public class TempConverter {
public static TempResponse.TempTestDTO toTempTestDTO(){
return TempResponse.TempTestDTO.builder()
.testString("This is Test!")
.build();
}
}
이제 Controller를 완성해주겠다.
이 예제에서는 비즈니스 로직이나 DB 접근이 없기 때문에 Service, Repository는 작성하지 않겠다.
이때! ApiResponse 클래스에 onSuccess 함수 주석처리 되어있는 것을 주석 해제해주자!

Controller를 작성한다.

package umc.spring.web.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import umc.spring.apiPayload.ApiResponse;
import umc.spring.converter.TempConverter;
import umc.spring.web.dto.TempResponse;
@RestController
@RequestMapping("/temp")
@RequiredArgsConstructor
public class TempRestController {
@GetMapping("/test")
public ApiResponse<TempResponse.TempTestDTO> testAPI(){
return ApiResponse.onSuccess(TempConverter.toTempTestDTO());
}
}
실행하고 API가 동작하는 것을 확인해보면,
다음과 같이 잘 뜬다!

+) 위 사진과 같이 JSON 형태로 뜨는 것을 확인해보고 싶다면
https://chrome.google.com/webstore/detail/json-formatter/bcjindcccaagfpapjjmafapmmgkkhgoa/related?hl=ko
에서 크롬에 추가를 하면 된다!
+) 지금은 localhost지만, 만약 원격 서버에 nginx와 같이 Springboot 인프라를 구축했다면, 먼저 nginx에 도달한 후 내부적인 리버스 프록시를 통해 Springboot로 요청이 간다!
아래와 같이 에러 핸들러 exception 패키지를 만들어주었다.
이 예시의 경우, Code를 성공과 실패 모두 공유하기에 이렇게 만들어주었다.

에러에 대한 규칙을 다음과 같이 적용하면 좋다고 한다. (출처 : UMC 8주차 워크북)
1. common 에러는 COMMON000 으로 둔다. <- 잘 안쓰지만 마땅하지 않을 때 사용
2. 관련된 경우마다 code에 명시적으로 표현한다.
- 예를 들어 멤버 관련이면 MEMBER001 이런 식으로
3. 2번에 이어서 4000번대를 붙인다. 서버측 잘못은 그냥 COMMON 에러의 서버 에러를 쓰면 됨.
- MEMBER400_1 아니면 MEMBER4001 이런 식으로
-> 이런 식으로 Code enum을 관리하면 확장성 측면에서 매우 좋다고 한다!
위의 규칙을 적용해 예시로 Code enum을 몇 개 추가해보겠다.
ErrorStatus에다가 아래 3가지 enum을 추가해주었다.
// Member Error
MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4001", "사용자가 없습니다."),
NICKNAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "MEMBER4002", "닉네임은 필수 입니다."),
// Article Error
ARTICLE_NOT_FOUND(HttpStatus.NOT_FOUND, "ARTICLE4001", "게시글이 없습니다.");
ErrorStatus 현재 전체 코드
package umc.spring.apiPayload.code.status;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;
import umc.spring.apiPayload.code.BaseErrorCode;
import umc.spring.apiPayload.code.ErrorReasonDTO;
@Getter
@AllArgsConstructor
public enum ErrorStatus implements BaseErrorCode {
// 가장 일반적인 응답
_INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."),
_BAD_REQUEST(HttpStatus.BAD_REQUEST,"COMMON400","잘못된 요청입니다."),
_UNAUTHORIZED(HttpStatus.UNAUTHORIZED,"COMMON401","인증이 필요합니다."),
_FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."),
// 멤버 관련 응답
MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4001", "사용자가 없습니다."),
NICKNAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "MEMBER4002", "닉네임은 필수 입니다."),
// Article Error
ARTICLE_NOT_FOUND(HttpStatus.NOT_FOUND, "ARTICLE4001", "게시글이 없습니다.");
// ~~~ 관련 응답 ....
;
private final HttpStatus httpStatus;
private final String code;
private final String message;
@Override
public ErrorReasonDTO getReason() {
return ErrorReasonDTO.builder()
.message(message)
.code(code)
.isSuccess(false)
.build();
}
@Override
public ErrorReasonDTO getReasonHttpStatus() {
return ErrorReasonDTO.builder()
.message(message)
.code(code)
.isSuccess(false)
.httpStatus(httpStatus)
.build()
;
}
}
GeneralException에는 다음 코드를 작성했다.
package umc.spring.apiPayload.exception;
import lombok.AllArgsConstructor;
import lombok.Getter;
import umc.spring.apiPayload.code.BaseErrorCode;
import umc.spring.apiPayload.code.ErrorReasonDTO;
@Getter
@AllArgsConstructor
public class GeneralException extends RuntimeException {
private BaseErrorCode code;
public ErrorReasonDTO getErrorReason() {
return this.code.getReason();
}
public ErrorReasonDTO getErrorReasonHttpStatus(){
return this.code.getReasonHttpStatus();
}
}
다음으로 에러 핸들러를 만들어야 하는데,
에러 핸들러를 만들기 전에 스프링에서 제공해주는 validation과 관련된 기능을 사용하기 위한 의존성 추가가 필요하다.
build.gradle에 추가해주자.
implementation 'org.springframework.boot:spring-boot-starter-validation'

이제 ExceptionAdvice를 작성해보겠다.
package umc.spring.apiPayload.exception;
import lombok.extern.slf4j.Slf4j;
import javax.validation.ConstraintViolationException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import umc.spring.apiPayload.ApiResponse;
import umc.spring.apiPayload.code.ErrorReasonDTO;
import umc.spring.apiPayload.code.status.ErrorStatus;
import javax.servlet.http.HttpServletRequest;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
@Slf4j
@RestControllerAdvice(annotations = {RestController.class})
public class ExceptionAdvice extends ResponseEntityExceptionHandler {
@org.springframework.web.bind.annotation.ExceptionHandler
public ResponseEntity<Object> validation(ConstraintViolationException e, WebRequest request) {
String errorMessage = e.getConstraintViolations().stream()
.map(constraintViolation -> constraintViolation.getMessage())
.findFirst()
.orElseThrow(() -> new RuntimeException("ConstraintViolationException 추출 도중 에러 발생"));
return handleExceptionInternalConstraint(e, ErrorStatus.valueOf(errorMessage), HttpHeaders.EMPTY,request);
}
@Override
public ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException e, HttpHeaders headers, HttpStatus status, WebRequest request) {
Map<String, String> errors = new LinkedHashMap<>();
e.getBindingResult().getFieldErrors().stream()
.forEach(fieldError -> {
String fieldName = fieldError.getField();
String errorMessage = Optional.ofNullable(fieldError.getDefaultMessage()).orElse("");
errors.merge(fieldName, errorMessage, (existingErrorMessage, newErrorMessage) -> existingErrorMessage + ", " + newErrorMessage);
});
return handleExceptionInternalArgs(e,HttpHeaders.EMPTY,ErrorStatus.valueOf("_BAD_REQUEST"),request,errors);
}
@org.springframework.web.bind.annotation.ExceptionHandler
public ResponseEntity<Object> exception(Exception e, WebRequest request) {
e.printStackTrace();
return handleExceptionInternalFalse(e, ErrorStatus._INTERNAL_SERVER_ERROR, HttpHeaders.EMPTY, ErrorStatus._INTERNAL_SERVER_ERROR.getHttpStatus(),request, e.getMessage());
}
@ExceptionHandler(value = GeneralException.class)
public ResponseEntity onThrowException(GeneralException generalException, HttpServletRequest request) {
ErrorReasonDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus();
return handleExceptionInternal(generalException,errorReasonHttpStatus,null,request);
}
private ResponseEntity<Object> handleExceptionInternal(Exception e, ErrorReasonDTO reason,
HttpHeaders headers, HttpServletRequest request) {
ApiResponse<Object> body = ApiResponse.onFailure(reason.getCode(),reason.getMessage(),null);
// e.printStackTrace();
WebRequest webRequest = new ServletWebRequest(request);
return super.handleExceptionInternal(
e,
body,
headers,
reason.getHttpStatus(),
webRequest
);
}
private ResponseEntity<Object> handleExceptionInternalFalse(Exception e, ErrorStatus errorCommonStatus,
HttpHeaders headers, HttpStatus status, WebRequest request, String errorPoint) {
ApiResponse<Object> body = ApiResponse.onFailure(errorCommonStatus.getCode(),errorCommonStatus.getMessage(),errorPoint);
return super.handleExceptionInternal(
e,
body,
headers,
status,
request
);
}
private ResponseEntity<Object> handleExceptionInternalArgs(Exception e, HttpHeaders headers, ErrorStatus errorCommonStatus,
WebRequest request, Map<String, String> errorArgs) {
ApiResponse<Object> body = ApiResponse.onFailure(errorCommonStatus.getCode(),errorCommonStatus.getMessage(),errorArgs);
return super.handleExceptionInternal(
e,
body,
headers,
errorCommonStatus.getHttpStatus(),
request
);
}
private ResponseEntity<Object> handleExceptionInternalConstraint(Exception e, ErrorStatus errorCommonStatus,
HttpHeaders headers, WebRequest request) {
ApiResponse<Object> body = ApiResponse.onFailure(errorCommonStatus.getCode(), errorCommonStatus.getMessage(), null);
return super.handleExceptionInternal(
e,
body,
headers,
errorCommonStatus.getHttpStatus(),
request
);
}
}
이때!
import org.hibernate.exception.ConstraintViolationException;이 아니라
import javax.validation.ConstraintViolationException;에 대해 import를 해야 한다!! 그래야 오류 없게 잘 나옴!

RestControllerAdvice를 이용한 예외 처리는 실제로 사용해봐야 편한 것을 체감할 수 있다고 한다. 이 방식을 사용하지 않으면 코드가 지저분해진다고 한다.
이제 임시로 예외(에러)처리하는 예제를 진행하겠다.
GET /temp/exception에 대해
Query String에 flag를 받아오는데, 해당 flag가 2인 경우 exception을 만드는 경우를 진행해보겠다.
1) ErrorStatus에 새로운 경우를 추가한다.
// For test
TEMP_EXCEPTION(HttpStatus.BAD_REQUEST, "TEMP4001", "이거는 테스트"),
2) handler 패키지에 Temp에 대한 핸들러를 추가한다.

package umc.spring.apiPayload.exception.handler;
import umc.spring.apiPayload.code.BaseErrorCode;
import umc.spring.apiPayload.exception.GeneralException;
public class TempHandler extends GeneralException {
public TempHandler(BaseErrorCode errorCode) {
super(errorCode);
}
}
TempResponse DTO를 추가한다.
package umc.spring.web.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
public class TempResponse {
@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class TempTestDTO{
String testString;
}
@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class TempExceptionDTO{
Integer flag;
}
}
Converter를 작성한다.
컨버터 함수 이름은 to<만들려는 대상> (ex : toTempTestDTO) 형식으로 지어주어야 한다!
package umc.spring.converter;
import umc.spring.web.dto.TempResponse;
public class TempConverter {
public static TempResponse.TempTestDTO toTempTestDTO(){
return TempResponse.TempTestDTO.builder()
.testString("This is Test!")
.build();
}
public static TempResponse.TempExceptionDTO toTempExceptionDTO(Integer flag){
return TempResponse.TempExceptionDTO.builder()
.flag(flag)
.build();
}
}
Controller를 작성한다.
이번 예시에서는 Service를 사용할 것이기 때문에 Controller에서는 return null만 해두겠다.
Springboot의 RestController에서 queryString을 받아오는 방법은 @RequestParam 이다.
package umc.spring.web.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import umc.spring.apiPayload.ApiResponse;
import umc.spring.converter.TempConverter;
import umc.spring.web.dto.TempResponse;
@RestController
@RequestMapping("/temp")
@RequiredArgsConstructor
public class TempRestController {
@GetMapping("/test")
public ApiResponse<TempResponse.TempTestDTO> testAPI(){
return ApiResponse.onSuccess(TempConverter.toTempTestDTO());
}
@GetMapping("/exception")
public ApiResponse<TempResponse.TempExceptionDTO> exceptionAPI(@RequestParam Integer flag){
return null;
}
}
Service를 작성하겠다.
Service를 작성할 때에는 다음과 같은 규칙을 따른다고 한다.
1. GET 요청과 나머지 요청에 대해 아래와 같이 분리한다.
a. GET 요청에 대한 비즈니스 로직을 처리할 경우
TempQueryService 이렇게 만든다.
b. 나머지 요청에 대한 비즈니스 로직을 처리할 경우
TempCommandService 이렇게 만든다.
2. 서비스를 만들 경우 인터페이스를 먼저 두고 이를 구체화 한다.
TempQueryService 인터페이스, TempCommandService 인터페이스를 만들고,
이에 대한 Impl 구체화 클래스를 만든다.
3. 컨트롤러는 인터페이스를 의존하며 실제 인터페이스에 대한 구체화 클래스는
Springboot의 의존성 주입을 이용한다!
이 규칙에 맞게 적용하면 아래와 같다.

TempQueryService
package umc.spring.service.TempService;
public interface TempQueryService {
void CheckFlag(Integer flag);
}
TempQueryServiceImpl
package umc.spring.service.TempService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class TempQueryServiceImpl implements TempQueryService{
@Override
public void CheckFlag(Integer flag) {
if (flag == 1)
throw new TempHandler(ErrorStatus.TEMP_EXCEPTION);
}
}
Controller를 완성해주자.
package umc.spring.web.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import umc.spring.apiPayload.ApiResponse;
import umc.spring.converter.TempConverter;
import umc.spring.service.TempService.TempQueryService;
import umc.spring.web.dto.TempResponse;
@RestController
@RequestMapping("/temp")
@RequiredArgsConstructor
public class TempRestController {
private final TempQueryService tempQueryService;
@GetMapping("/test")
public ApiResponse<TempResponse.TempTestDTO> testAPI(){
return ApiResponse.onSuccess(TempConverter.toTempTestDTO());
}
@GetMapping("/exception")
public ApiResponse<TempResponse.TempExceptionDTO> exceptionAPI(@RequestParam Integer flag){
tempQueryService.CheckFlag(flag);
return ApiResponse.onSuccess(TempConverter.toTempExceptionDTO(flag));
}
}
이제 실행하고 테스트해보자!


잘 뜨는 것을 확인할 수 있다!
-> 에러 상황과, 에러가 아닌 상황 마다 응답이 다르다!
Service에서는 다음과 같이 에러를 만들어서 던지기만 했지, 응답을 만드는 코드는 없다.
@Override
public void CheckFlag(Integer flag) {
if (flag == 1)
throw new TempHandler(ErrorStatus.TEMP_EXCEPTION);
}
응답은 어디에 있을까?
에러 핸들러 코드를 잘 봐야 한다.
Temphandler를 보면, Service에서 해당 핸들러를 만들면(위 코드 참조) GeneralException을 만든다.

그러면 GeneralException 클래스에서 @AllArgsConstructor를 통해 만들어진 생성자가 호출된다.

GeneralException은 다시, RuntimeException을 상속 받은 상태이다.
이는 런타임에 발생한 Exception 으로써 MasterExceptionHandler가 감지하게 된다.
-> ExceptionAdvice가 @RestControllerAdvice를 가지고 있기 때문이다.

ExceptionAdvice의 아래 onThrowException 메서드 코드에서 GeneralException에 대해 다시 한번 오버로딩 된 함수(handleExceptionInternal)를 호출하고 있다

최종적으로 다시 한번 호출된 함수에서는 상속 받은 부모 클래스의 생성자를 호출하고 있다.

이 코드를 보면, 여기에서 응답을 보내는 것을 알 수 있다.

결론적으로는, 아래 Service 구현 코드에서
if 문 내부로 들어가게 되면 (에러 상황)
Service 이후 Controller로 돌아가지 않고 바로 Exception handler에 의해 응답이 보내진다.

RestControllerAdvice는 @RestController가 붙은 대상에서, Exception이 발생하는 것을 감지하는 역할을 한다.
위의 코드들을 보면 Service의 메소드를 @RestController가 붙은 메소드에서 호출을 했기에, 결국 컨트롤러에서 Exception이 발생 한 것으로 판단되는 것이다.
마찬가지로 Converter에서 Exception을 발생시켜도
결국 컨트롤러에서 Converter를 호출하기 때문에 Exception Handler에 잡히게 된다.