Java/Spring

Exception을 활용하여 클린코드 작성하기

체리필터 2021. 3. 11. 10:11
728x90
반응형

클린코드 책을 읽다 보면 '오류코드 보다는 예외를 사용하라'라는 말이 나온다.

코드로 분기를 치면서 특정 상황에 특정 처리를 해야 하는 코드보다 예외를 던져서 처리하는 것이 한 눈에 볼 수있도록 코드 가독성을 올려준다는 말이다.

따라서 현재 실무에서 처리하고 있는 코드를 예를 들어 보여 줌으로 어떻게 깔끔한 코드를 사용할 수 있는지 살펴보자.

현재 사용하고 있는 코드에서는 다음과 같은 interface를 우선 선언하였다.

package com.kst.macaront.common.lib.exception;

import org.springframework.http.HttpStatus;

public interface ApiException {
    HttpStatus getHttpStatus();
    String getResponseMessage();
    Integer getResponseCode();
}

api project이기 때문에 이름은 ApiException이라 정의 하였고 응답 HttpStatus 정의와 기본적인 message, code를 구현하도록 정의 하였다.

이렇게 정의 된 인터페이스를 각 프로젝트별로 구현해 주면 되는데 일반적인 내용은 CommonException으로, 각 MSA에서 사용할 Exception은 프로젝트에 맞게 생성해 주면 된다.

아래는 하나의 예시이다.

package com.kst.macaront.common.lib.exception;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;

@Slf4j
public class CommonException extends RuntimeException implements ApiException {
    CommonError commonError;

    public CommonException(CommonError commonError) {
        super(commonError.getMessage());
        this.commonError = commonError;
        log.info("ERROR MESSAGE : {} {}", this.commonError.getHttpStatus(), this.commonError.getMessage());
    }

    @Override
    public HttpStatus getHttpStatus() {
        return this.commonError.getHttpStatus();
    }

    @Override
    public String getResponseMessage() {
        return this.commonError.getMessage();
    }

    @Override
    public Integer getResponseCode() {
        return this.commonError.getCode();
    }
}
package com.kst.macaront.common.lib.exception;

import org.springframework.http.HttpStatus;

public enum CommonError {

    SYSTEM_ERROR(HttpStatus.SERVICE_UNAVAILABLE, "시스템 에러 발생.", 0),
    EXTERNAL_LINKAGE_ERROR(HttpStatus.SERVICE_UNAVAILABLE, "외부 연동 에러 발생."),
    INVALID_PARAMETER(HttpStatus.INTERNAL_SERVER_ERROR, "유효하지 않은 전달값입니다.", 9999);

    HttpStatus httpStatus;
    String message;
    Integer code;

    CommonError(HttpStatus httpStatus, String message) {
        this.httpStatus = httpStatus;
        this.message = message;
    }

    CommonError(HttpStatus httpStatus, String message, Integer code) {
        this.httpStatus = httpStatus;
        this.message = message;
        this.code = code;
    }

    public HttpStatus getHttpStatus() {
        return this.httpStatus;
    }
    public String getMessage() {
        return this.message;
    }
    public Integer getCode() {
        return this.code;
    }
}

CommonError에서는 정말로 일반적인 에러에 대한 메시지만 정의 하였으며 프로젝트별로 특화된 비즈니스 로직 또는 그에 맞는 에러 메시지는 따로 정의하여 사용하면 된다.

물론 그에 맞는 Handler도 ApiException을 상속받아 구현해야 한다.

가령 현재 진행하고 있는 Chauffeur 개선 프로젝트의 경우 아래와 같이 ExceptionHandler를 구현하면 된다.

package com.kst.macaront.chauffeur.api.common.exception;

import com.kst.macaront.common.lib.exception.ApiException;
import com.kst.macaront.common.lib.exception.ApiExceptionHandler;
import com.kst.macaront.common.lib.exception.CommonException;
import com.kst.macaront.common.lib.response.ResponseRepresentation;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ChauffeurExceptionHandler extends ApiExceptionHandler {

    @ExceptionHandler({CommonException.class, ChauffeurException.class})
    protected ResponseEntity<ResponseRepresentation> handleApiException(ApiException e) {
        ResponseRepresentation responseRepresentation = new ResponseRepresentation(
                e.getHttpStatus().value(),
                e.getResponseMessage(),
                e.getResponseCode()
        );
        return new ResponseEntity(responseRepresentation, e.getHttpStatus());
    }
}

@ExceptionHandler annotation에 CommonException과 ChauffeurException을 둘다 정의하고 사용하면 된다.

이렇게 정의한 후 사용을 할 때는 아래와 같이 필요한 곳에 사용하면 코드에 의한 분기를 막을 수 있다.

throw new ChauffeurException(ChauffeurError.NO_EXIST_COMPANY_BELONG_TYPE);

 

하지만 위의 내용은 서버측 오류 즉 http status code 5XX 대의 경우에만 대응 가능하며 호출하는 Client 쪽의 실수에 의한 Exception은 핸들링 하지 못한다.

이럴 경우에는 아래와 같이 별도로 핸들러를 선언하면 된다.

package com.kst.macaront.common.lib.exception;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import com.kst.macaront.common.lib.response.ResponseRepresentation;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;

@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ApiExceptionHandler {

    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    private ResponseEntity<ResponseRepresentation> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
        ResponseRepresentation responseRepresentation = new ResponseRepresentation(
                HttpStatus.BAD_REQUEST.value(),
                e.getName() + " should be of type "
                        + e.getRequiredType().getSimpleName() + ", Input values {" + e.getValue() + "}"
        );
        return new ResponseEntity(responseRepresentation, HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(HttpMessageNotReadableException.class)
    private ResponseEntity<ResponseRepresentation> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
        String msg;
        Throwable throwable = e.getMostSpecificCause();

        if (throwable instanceof InvalidFormatException) {
            msg = getInvalidFormatExceptionMessage((InvalidFormatException) throwable);
        } else if (throwable instanceof MismatchedInputException) {
            msg = getMismatchedInputExceptionMessage((MismatchedInputException) throwable);
        } else if (throwable instanceof JsonParseException) {
            msg = getJsonParseExceptionMessage((JsonParseException) throwable);
        } else if (throwable instanceof HttpMessageNotReadableException) {
            msg = "Message Not Readable: Required request body is missing";
        } else {
            msg = "Request not readable";
        }

        ResponseRepresentation responseRepresentation = new ResponseRepresentation(
                HttpStatus.BAD_REQUEST.value(),
                msg
        );

        return new ResponseEntity(responseRepresentation, HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(Exception.class)
    private ResponseEntity<ResponseRepresentation> handleException(Exception e) {
        ResponseRepresentation responseRepresentation = new ResponseRepresentation(
                HttpStatus.INTERNAL_SERVER_ERROR.value(),
                e.toString()
        );
        return new ResponseEntity(responseRepresentation, HttpStatus.INTERNAL_SERVER_ERROR);
    }

    private String getInvalidFormatExceptionMessage(InvalidFormatException invalidFormatException) {
        String message = "Can not deserialize value of type "
                + "[" + invalidFormatException.getTargetType().getSimpleName() + "]"
                + " from " + invalidFormatException.getValue().getClass().getSimpleName() + " "
                + "[" + invalidFormatException.getValue() + "]"
                + " at [line: " + invalidFormatException.getLocation().getLineNr()
                + ", column: " + invalidFormatException.getLocation().getColumnNr() + "]";

        if (!ObjectUtils.isEmpty(invalidFormatException.getPath()) && invalidFormatException.getPath().size() > 0) {
            JsonMappingException.Reference ref = invalidFormatException.getPath().get(invalidFormatException.getPath().size()-1);
            message += ", Path: " + ref.getFrom().getClass().getSimpleName()
                    + "[" + ref.getFieldName() + "]";
        }
        return message;
    }

    private String getMismatchedInputExceptionMessage(MismatchedInputException mismatchedInputException) {
        String message = "Can not deserialize value of type "
                + "[" + mismatchedInputException.getTargetType().getSimpleName() + "]"
                + " at [line: " + mismatchedInputException.getLocation().getLineNr()
                + ", column: " + mismatchedInputException.getLocation().getColumnNr() + "]";
        return message;
    }

    private String getJsonParseExceptionMessage(JsonParseException jsonParseException) {
        String message = jsonParseException.getOriginalMessage()
                + " at [line: " + jsonParseException.getLocation().getLineNr()
                + ", column: " + jsonParseException.getLocation().getColumnNr() + "]";
        return message;
    }
}

MethodArgumentTypeMismatchException 나 HttpMessageNotReadableException에 대한 Handler를 별도로 선언해 주게 되면 4XX 대의 오류에 대해서도 깔끔하게 처리 가능하다.

Code의 Else 구문과도 같은 의미로 남은 Exception 마저 다 처리하기 위해 Exception 전체에 대한 Handler도 별도로 선언해 주면 이제 전체적으로 Exception을 처리할 수 있다.

 

기본적인 내용에 더해 추가적인 내용까지 잘 조사하고 정리해준 팀 내 장화평 매니저에게 감사를 드린다 ^^ (허락받고 내용 작성했습니다. ^^)

728x90
반응형