반응형

과거부터 개발하면서 꿈이 있었다면, 에러가 발생하더라도 서버에서 로그를 확인하지 않고도 바로 알도록 만드는 것이었다.

이런 시스템을 만들어두면 에러가 발생하자마자 바로바로 조치가 가능하기 때문이었다.

 

웹훅으로 만드려고 했으며, 가장 쉬운 슬랙을 사용했다.

 

우선 슬랙에서 다음과 같은 앱을 추가하자

 

이 Incoming Webhooks를 사용해서 채널에 알림을 보낼것이다.

 

여기서 webhook url을 생성하고, 이 과정은 생략하도록 하겠다.

 

에러를 보고하는 서비스의 추상화를 만든다.

interface ErrorReporter {

    fun reportError(content: String, localDateTime: LocalDateTime)
}

 

현재는 슬랙으로 하고 있지만, 구현체가 변경될 수도 있으니 추상화로 만들어둔다.

 

@Service
class SlackErrorReporter(
    @Value("\${slack.webhook_url.error}")
    private val slackWebHookUrl: String
): ErrorReporter {

    override fun reportError(content: String, localDateTime: LocalDateTime) {

        val payload = mapOf("text" to "${localDateTime}에 $content")

        WebClient.create()
            .post().uri(slackWebHookUrl)
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(payload)
            .retrieve()
            .bodyToMono(Void::class.java)
            .block()

    }
}

 

당연히 webhook의 주소는 깃에 노출되면 안되고 변경이 될 수 있기에 환경변수로 주입받는다.

 

http로 요청을 해야하기에 WebClient를 사용했다.

 

안에 들어가는 json 형식은 다음과 같다.

{
	"text": "에러 내용"
}

그렇기에 나는 여기에 시간과 에러의 내용을 보고하도록 만들었다.

 

그리고 이제 이 reporter를 사용해보자.

당연히 전역의 ExceptionHandler를 사용했다.

 

현재 멀티모듈을 사용하기에 container 역할을 하는 container 모듈에 다음과 같은 ExceptionHandler를 추가했다.

 

@ControllerAdvice
class GlobalExceptionHandler(
    private val errorReporter: ErrorReporter
) {

    @ExceptionHandler(Exception::class)
    fun handleException(e: Exception): ResponseEntity<ErrorResponse> {

        val sw = StringWriter()
        e.printStackTrace(PrintWriter(sw))
        errorReporter.reportError(sw.toString(), LocalDateTime.now())

        return ResponseEntity.internalServerError().build()
    }
}

 

errorReporter를 주입받고, 해당 에러에 대한 stackTrace를 errorReporter로 넘기게 된다.

클라이언트에게는 500에러를 그대로 반환한다.

 

이러면 서버에서 해결하지 못했던 모든 500에러가 슬랙을 통해 개발자에게 보고된다.

 

테스트를 해보도록 하자.

그냥 다짜고짜 컨트롤러에서 에러를 던져보았다.

throw IllegalStateException()

 

컨트롤러는 해당 에러를 처리할 수 없기에 클라이언트로 넘기려고 할거고, 그러면 전역 핸들러에 걸리게 될 것이다.

 

일단 Swagger에는 500에러가 나오게 된다.

 

그리고 슬랙에서도 컨트롤러에서 IllegalStateException이 발생했다고 알림이 오는 것을 볼 수 있다.

 

반응형

당연히 예외에 대해 처리를 할 수 있도록 만들어야 한다.

이런 부분들은 보통 Service단에 try-catch로 정의를 했지만, 항상 보며 느끼는 부분은 코드가 지저분해지고 가독성이 많이 떨어진다.

 

어떻게 할지 고민하던 중, 실습 기간 대리님께서 RestControllerAdvice를 써보라고 하셔서 이번 프로젝트에는 이것을 이용해 예외 처리를 최대한 깔끔하게 해보려고 한다.

 

그냥 사용하려면 방법은 어렵지 않다.

@RestControllerAdvice

그냥 이런 annotation을 가지고 있는 class를 정의해준다.

 

그리고 안에

@ExceptionHandler(Exception.class)
    public ResponseEntity<String> GlobalHandler() {
        return new ResponseEntity<>("에러, 자세한 상황을 보고해주세요", HttpStatus.BAD_REQUEST);
    }

이렇게 어떤 에러를 어떻게 처리할 것인지 작성을 해준다.

 

물론 이렇게 작성해버리면 모든 에러가 여기에서 잡혀버리기 때문에 Exception.class는 안 쓸 거 같다.

 

근데 이걸 사용하려니 한 가지 걸리는 부분이 있었다.

만약 SQLException이 발생했다고 하자, 하지만 이 에러가 어느 컨트롤러에서 발생했는 지에 따라 다른 오류일텐데 Global로 처리를 해버리면 똑같이 처리 과정을 거치게 된다.

 

그래서 나는 각 Controller에 다른 handler를 사용하기 위해 

@RestControllerAdvice(basePackageClasses = ???.class)

이렇게 어떤 컨드롤러인지 basePackageClasses에 명시해서 해당 컨트롤러만 사용하도록 만들었다.

 

그러면 또 여기서 문제가 생긴다.

공통적으로 발생하는 예외들은 다 복붙을 해서 적어야 하나...

솔직히 얼마 안 걸리지만, 중복되는 코드들을 계속 적는 부분이 마음에 걸렸다.

 

그렇기에 Global 부분에

package cobo.blog.global.Config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;

import java.sql.SQLException;
import java.sql.SQLTimeoutException;

@Slf4j
public class GlobalExceptionHandler {

    /**
     * DB 관련 에러
     */

    @ExceptionHandler(SQLTimeoutException.class)
    public ResponseEntity<String> GlobalSQLTimeoutHandler(){
        log.info("GlobalSQLTimeoutHandler: {}", this.getClass());
        return new ResponseEntity<>("데이터베이스 접근 시간 초과", HttpStatus.REQUEST_TIMEOUT);
    }

    @ExceptionHandler(SQLException.class)
    public ResponseEntity<String> GlobalSQLHandler(){
        log.info("GlobalSQLHandler: {}", this.getClass());
        return new ResponseEntity<>("데이터 베이스 관련 에러, 자세한 상황을 보고해주세요", HttpStatus.BAD_REQUEST);
    }
}

이런 식으로 공통부분은 구현을 하며, 각 세부 컨트롤러에서 상속을 받아 더 세부적으로 작업할 수 있도록 하였다.

오랫동안 생각해 본 결과 이 방법이 지금 내가 할 수 있는 최선의 방법이라고 생각했고, 후에는 아마 다른 방법도 사용하지 않을까?

'블로그 개발 프로젝트' 카테고리의 다른 글

Nginx에 페이지 연결하기  (0) 2023.08.07
EC2에 Nginx 초기 설정  (0) 2023.08.05
Swagger @ApiModelProperty에 example List  (0) 2023.07.28
Swagger Response  (0) 2023.07.28
CORS Error  (0) 2023.07.28

+ Recent posts