Custom Exception

PPakSSam·2022년 1월 26일
0
post-thumbnail

정답에 집착하지 말고 계속해서 고민하라

1주차 수업에서 들은 말이다.
그냥 그려러니 하고 넘긴 말인데 정리를 하고 공부를 한창하고 있는 요즘에 정말 필요한 말인 것 같다. 하나라도 더 얻으려고 놓치지 않으려고 하다보니 집착이 되었고 괴로움만 늘어나는 것 같다.
맘을 내려놓고 편안한 마음으로 정리를 하려고 한다.

그래 일단 정리해놓고 나중에 다시 정리하자

요즘 마인드를 이렇게 바꾸었다.
미루는 것이 아니라 다시 정리할 시간을 내자 이렇게 맘을 먹는 것이다.
주말이나 쉬는 공휴일, 휴가 이런 시간들에 정리를 하면 된다고 생각하니 한결 맘이 편해진다.


Custom Exception에 대하여

Custom Exception을 어떤 이유로 사용하나요?

회사에서는 Custom Exception을 사용해본 적이 없었다. 그런데 이번 수업에서 보고 들은 것은 있기에 한번 만들어봤고 위의 질문을 했었다.

커스텀 Exception를 활용할 경우 해당 예외가 발생했을 때의 처리를 한 곳에서 묶어서 처리할 수 있는 장점이 있을 것 같아요.

커스텀 Exception 역시 점진적으로 리팩터링 해야할 대상이라고 생각합니다. 처음부터 어떠한 규칙을 만들어 놓고 정책을 정할 경우 이후 유지보수가 매우 어려워지는 경우를 많이 겪었어서 그렇게 생각하는 것 일 수 있어요. 최초 기능구현 할 때는 범용적인 Exception을 사용하여 기능구현을 한 뒤 특정 Exception들을 묶어서 한 곳에서 처리해야는 상황이 생겨서 리팩터링을 할 때 규칙을 정해가면서 이어나가는 전략을 생각해볼 수 도 있을 것 같네요.

리뷰어님의 답변이었다. 이걸 보면서 든 생각은 아 처음부터 Custom Exception을 만들기 보다는 리팩토링 하면서 만들면 되겠구나였다.

그리고 나서 든 생각은 특정 Exeption을 묶어서 한 곳에서 처리해야하는 상황은 무엇일까였다.
다행히도 미션을 계속 진행하면서 그 상황이 발생하였다.

예외 발생 처리를 @ControllerAdvice를 이용해서 처리하곤 하는데 처음의 Exception Controller는 다음과 같은 형태였다.

@RestControllerAdvice
public class ExceptionController {

    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<ExceptionResponse> runtimeExceptionHandler(RuntimeException e) {
        e.printStackTrace();
        return ResponseEntity.internalServerError()
                             .body(new ExceptionResponse(e.getMessage()));
    }
}

뭐가 그냥 없다 ㅋㅋㅋ 그냥 Runtime Exception 발생하면 서버에러 상태코드인 500을 내보내는 단순한 코드이다.

그런데 두둥!!😛😛 다음과 같은 리뷰를 받았다.

예외 응답코드를 internalSeverError 보다는 badRequest로 하는 건 어떨까요?

제가 400 응답코드를 추천하는 근거는 RFC 문서이고 이를 제 관점에서 해석했을 땐 500대 에러의 경우 서버쪽 원인으로 발생한 에러를 의미하고 400대 에러의 경우 클라이언트의 원인으로 야기된 에러를 의미한다고 판단했습니다. (Client Error 4xx, Server Error 5xx) 사실 이 판단은 정답이라곤 할 순 없는데 스펙문서는 다양한 관점으로 해석될 수 있다고 생각하기 때문이에요. 다양한 사람들의 해석을 참고해서 기준을 세워보셔도 좋을 것 같습니다.

와우... 그냥 생각 없이 internalServerError를 썼던 내 자신을 반성하게 되는 계기가 되었다. 응답코드 만으로도 이런 깊이가 있을 줄은 몰랐다. 이 부분에 대해서는 리뷰어님이 추천한 블로그인 https://blog.outsider.ne.kr/1121를 들어가서 읽어보는 것을 추천한다.

그래도 이왕 공부하려고 쓴 글이니 정리를 해보자면

400 Bad Request

요청에서 지켜야 할 문법을 제대로 지키지 못해서 서버가 처리 못하는 것 뿐만 아니라 어떤 오류로 처리하지 않을 때 반환하는 상태 값

만약 중복 처리 예외를 처리할 때 위의 뜻을 생각해보면 사용해도 괜찮을 것 같다는 생각이다.

409 Conflict

리소스의 현재 상태와 충돌해서 요청을 처리할 수 없으므로 클라이언트가 수정해서 요청을 다시 보내야할 때 반환하는 상태 값

블로그에서는 중복 처리 예외가 400보다는 이 409 상태값이 더 적합하지 않냐고 제안을 한다.
물론 동의를 하는 바이다.

403 Fobidden

인가(Authorization)에 실패할 때 반환하는 상태 값

이외에도 여러가지 상태 값이 있지만 일단 이 정도까지만 정리를 하도록 하겠다.
시간이 된다면 http://parker0phil.com/2014/10/16/REST_http_4xx_status_codes_syntax_and_sematics/ 이 글도 보면서 정리를 해봐야겠다는 생각이 들었다.

오호... 중복 처리 예외는 Bad Request를 던져야겠는데??

이제 이런 생각이 드니 BadRequestException을 만들어서 400 상태를 반환하는 예외를 따로 만들자라는 결론에 도달하게 되었다.

따라서 중복 처리 부분에 다음과 같이 작성했고

private void validateDuplicateStationName(String name) {
       if(stationRepository.findByName(name) != null) {
           throw new BadRequestException("중복된 역 이름입니다.");
       }
    }

ExceptionController 부분도 다음과 같이 변하게 되었다.

@RestControllerAdvice
public class ExceptionController {
    
    // 서버 에러는 500을 반환하고
    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<ExceptionResponse> runtimeExceptionHandler(RuntimeException e) {
        e.printStackTrace();
        return ResponseEntity.internalServerError().body(new ExceptionResponse(e.getMessage()));
    }
    
    // Bad Request의 경우에는 400을 반환하자
    @ExceptionHandler(BadRequestException.class)
    public ResponseEntity<ExceptionResponse> badRequestExceptionHandler(BadRequestException e) {
        e.printStackTrace();
        return ResponseEntity.badRequest().body(new ExceptionResponse(e.getMessage()));
    }
}

확실히 특정 Exception들을 묶어서 한 곳에서 처리해야는 상황이 생겼고 이 때 Custom Exception을 사용하는구나 느꼈다!


참고

[SectionService]

public SectionResponse saveSection(SectionRequest request, Long lineId) {
    ...
    
    Station upStation = stationRepository.findById(request.getUpStationId())
                .orElseThrow(() -> new BadRequestException("상행역이 존재하지 않습니다.");
    
    Station downStation = stationRepository.findById(request.getDownStationId())
                .orElseThrow(() -> new BadRequestException("하행역이 존재하지 않습니다."));
                
    ...
}

[LineService]

private Section createSection(LineRequest request, Line line) {
    
    Station upStation = stationRepository.findById(request.getUpStationId())
                .orElseThrow(() -> new BadRequestException("상행역이 존재하지 않습니다.");
    
    Station downStation = stationRepository.findById(request.getDownStationId())
                .orElseThrow(() -> new BadRequestException("하행역이 존재하지 않습니다."));    
                
    return Section.builder()
        .line(line)
        .upStation(upStation)
        .downStation(downStation)
        .distance(request.getDistance())
        .build();
}

위의 두 코드처럼 작성하면 만약 유지보수를 할 때 예외 처리 메세지를 LineService에서는 상행역이 존재하지 않습니다.라고 하고 SectionService에서는 상행역으로 존재하지 않는 역을 지정할 수 없습니다. 라고 할 수 있다는 점이다.
왜냐하면 SectionService와 LineService를 둘 다 자신이 짜지 않는 이상 LineService 또는 SectionService에 같은 예외처리 메시지가 있음을 알기 힘들기 때문이다.

이런 경우 또한 Custom Exception으로 해결해보자!!

public class UpStationNotExistException extends BadRequestException{

    public static String MESSAGE = "상행역이 존재하지 않습니다.";

    public UpStationNotExistException() {
        super(MESSAGE);
    }
}

public class DownStationNotExistException extends BadRequestException{
    public static final String MESSAGE = "하행역이 존재하지 않습니다.";
    
    public DownStationNotExistException() {
        super(MESSAGE);
    }
}

위와 같이 커스텀 Exception을 만든 뒤

[SectionService]

public SectionResponse saveSection(SectionRequest request, Long lineId) {
    ...
    
    Station upStation = stationRepository.findById(request.getUpStationId())
                .orElseThrow(UpStationNotExistException::new);
    
    Station downStation = stationRepository.findById(request.getDownStationId())
                .orElseThrow(DownStationNotExistException::new);
                
    ...
}

[LineService]

private Section createSection(LineRequest request, Line line) {
    
    Station upStation = stationRepository.findById(request.getUpStationId())
                .orElseThrow(UpStationNotExistException::new);
    
    Station downStation = stationRepository.findById(request.getDownStationId())
                .orElseThrow(DownStationNotExistException::new);    
                
    return Section.builder()
        .line(line)
        .upStation(upStation)
        .downStation(downStation)
        .distance(request.getDistance())
        .build();
}

이렇게 구현하면 어디서든 똑같은 예외 메세지를 보내게 될 것이다.
이러한 경우에도 Custom Exception을 사용하는 것 같다!

처음에는 StationService에서 findById를 또 구현해서 처리를 하려 했다.

public Station findById(Long id) {
    return stationRepository.findById(id)
            .orElseThrow(() -> new BadRequestException("존재하지 않는 역입니다."));
}

위의 코드처럼 만든 뒤 SectionService나 LineService에서 StaionService를 주입받고
stationService.findById를 사용하게 했다.
그런데...

사견으로는 현재는 단순 조회의 경우 때문이지만,
추후 요구사항에 따라서 서비스간에 의존도가 높아지게 된다면 자연스럽게 결합도도 높아지지 않을까요?
그렇게 된다면 유지보수 측면에서 코드 수정시 봐야하는 코드 수도 많을 수 있다고 생각이 들고,
수정도 어려워 질 수 있을 것 같아요!

이런 리뷰를 받았고, 또한 같이 스터디를 하는 분의 의견을 들어도 서비스끼리 의존하게 된다면 나중에 유지보수할 때 떼어내기가 힘들다는 의견이 있어서 이부분은 과감하게 버리기로 하였다!

profile
성장에 대한 경험을 공유하고픈 자발적 경험주의자

0개의 댓글