저희 팀은 이전까지 에러 이슈와 관련된 문제들을 모니터링 및 수집하기 위해서, 디스코드에 채널방을 만들어서 어떠한 에러가 발생하고 있는지 작성해주고 있었습니다.
그러나, 모든 에러를 디스코드에 공유하기는 어려웠고 실제 프로젝트를 사용자들에게 배포해서 운영할 때에는 에러를 따로 모니터링하기 힘들다고 판단하였습니다.
따라서 저희는 에러 모니터링 시스템을 도입하기로 하였고, 이중에서 무료로 사용할 수 있는 Sentry를 사용하기로 하였습니다.
Sentry의 계정을 만들고, Sentry에 프로젝트를 생성한 뒤에 백엔드 코드에 설정 파일를 작성하고 에러를 수집하고 싶은 곳에 에러 수집 코드만 작성하면 되어서 하루면 쉽게 에러 모니터링 시스템 Sentry를 도입할 수 있습니다!!
저희 팀은 Spring Boot 2.7을 사용하고 있습니다.
https://docs.sentry.io/platforms/java/guides/spring-boot/
implementation 'io.sentry:sentry-spring-boot-starter:6.28.0'
sentry:
dsn: 복사한 DSN 키
enable-tracing: true
sentry:
dsn: 복사한 DSN 키
enable-tracing: true
environment: development
Sentry.captureException(e)
함수를 사용하여, Sentry에 에러를 전송할 수 있습니다.@RestControllerAdvice
로 공통 예외 처리를 진행하고 있어서, 해당 ControllerExceptionAdvice 클래스에 에러 전송하는 코드를 추가하였습니다.@RequiredArgsConstructor
@RestControllerAdvice
public class ControllerExceptionAdvice {
/**
* 400 Bad Request (잘못된 요청, Validation Exception)
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(BindException.class)
private ApiResponse<Object> handleValidationError(BindException e) {
String errorMessage = e.getBindingResult().getFieldErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(joining("\n"));
Sentry.captureException(e);
return ApiResponse.error(ErrorCode.INVALID, errorMessage);
}
/**
* 400 Bad Request (잘못된 요청)
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(InvalidException.class)
private ApiResponse<Object> handleBadRequest(InvalidException e) {
Sentry.captureException(e);
return ApiResponse.error(e.getErrorCode());
}
/**
* 404 Not Found (존재하지 않는 리소스)
*/
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(NotFoundException.class)
private ApiResponse<Object> handleNotFound(NotFoundException e) {
Sentry.captureException(e);
return ApiResponse.error(e.getErrorCode());
}
/**
* 500 Internal Server Exception (서버 내부 에러)
*/
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(InternalServerException.class)
private ApiResponse<Object> handleInternalServerException(InternalServerException e) {
Sentry.captureException(e);
return ApiResponse.error(e.getErrorCode());
}
}
여기까지 하면 Sentry를 모두 다 설정하였습니다!!
따라서, 실제로 에러를 발생시키고 Sentry로 에러 관련 정보가 잘 전송되었는지 확인해보겠습니다!
2023-09-08 00:48:52.703 WARN 7838 --- [nio-8080-exec-4] .m.m.a.ExceptionHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.forever.dadamda.dto.ApiResponse<com.forever.dadamda.dto.scrap.CreateScrapResponse> com.forever.dadamda.controller.scrap.ScrapController.addScraps(com.forever.dadamda.dto.scrap.CreateScrapRequest,org.springframework.security.core.Authentication) throws net.minidev.json.parser.ParseException: [Field error in object 'createScrapRequest' on field 'pageUrl': rejected value []; codes [NotBlank.createScrapRequest.pageUrl,NotBlank.pageUrl,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [createScrapRequest.pageUrl,pageUrl]; arguments []; default message [pageUrl]]; default message [url을 입력해주세요.]] ]
Sentry를 사용하면서 주의할 점에 대해서 이야기해보겠습니다!
사진 출처 : https://sentry.io/pricing/
여기에서 무료 계정은 Developer 계정입니다.
따라서, 제한된 모니터링 가능한 에러 개수, 1명만 사용 가능 등 여러 제한 사항이 있지만 무료로 사용할 수 있다는 가장 큰 메리트가 있어서 Developer 계정으로 진행하였습니다.
그러나, 회원가입하고 나서 가격을 선택하는 페이지가 나올 줄 알았는데😢 바로 Team 계정으로 생성되었고 Setting에서도 Developer 계정으로 변경하기는 어려웠습니다.
따라서 계정을 탈퇴하고 나서, 다음의 페이지에서 https://sentry.io/pricing/ Developer Pricing의 GET STARTED 버튼을 눌러서 계정을 새롭게 만들었습니다.
따라서 설정으로 이동하면 Developer Plan임을 확인할 수 있습니다.
다른 Team, Business 계정은 돈이 청구되는 만큼 현재 프로젝트에 알맞는 plan인지 확인해보는 것도 중요합니다!!
현재 무료 계정을 사용하고 있기 때문에 사용 가능한 에러의 개수가 제한되어 있습니다.
local에서는 Sentry를 사용하지 않고 dev, prod와 같이 실제 에러를 수집해야 하는 환경에만 설정하는 것이 좋다고 판단하였습니다.
하지만, Sentry를 local의 환경 설정 파일(yml)에 설정해주지 않으면 에러가 발생합니다.
따라서 검색해서 찾아본 결과, 아래의 글에서 추천한 방법으로 진행하였습니다.
https://github.com/getsentry/sentry-symfony/issues/38
@RequiredArgsConstructor
@RestControllerAdvice
public class ControllerExceptionAdvice {
/**
* 400 Bad Request (잘못된 요청, Validation Exception)
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(BindException.class)
private ApiResponse<Object> handleValidationError(BindException e) {
String errorMessage = e.getBindingResult().getFieldErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(joining("\n"));
Sentry.captureException(e);
return ApiResponse.error(ErrorCode.INVALID, errorMessage);
}
}