HTTP status code는 3자리의 코드와 설명으로 표현됩니다.
하지만 실무에서는 더 많고 다양한 종류의 에러 표현을 위해 name, code, reason을 활용하곤 합니다.
그리고 간결함과 생산성을 위해 프로젝트 내에서도 code와 reason이 직관적으로 연결되기를 기대합니다.
STEP 1. Enum으로 관리하기
@Getter
@RequiredArgsConstructor
public enum Errorcode {
NOT_EXIST_PRODUCT("1001", "대상 상품 없음"),
OUT_OF_STOCK("1200", "상품 재고 없음"),
UNKNOWN("9999", "알 수 없는 에러"),
;
private final String code;
private final String reason;
}
@Entity
public class Product {
...
public void order(final Integer quantity) {
if (this.quantity < quantity) {
throw new ProductException(Errorcode.OUT_OF_STOCK);
}
this.quantity -= quantity;
}
}
@RestControllerAdvice
public class ControllerAdvice {
@ExceptionHandler(ProductException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
protected ErrorResponse handleBusinessException(final ProductException ex) {
return new ErrorResponse(ex.getErrorcode());
}
}
장점
- 작은 프로젝트에 적합합니다.
단점
- name, code 변경 시 배포가 필요합니다.
- admin 개발이 필요한 경우 코드 중복이 발생합니다.
- 프로젝트 밖에서 전체 에러 목록을 알기 힘듭니다.
- 메시지 국제화가 어렵습니다.
구현 방법이 아주 쉬워 사이드 프로젝트를 할 때 많이 썼던 관리 방법이지만 단점이 꽤 있습니다.
STEP 2. MessageSource로 관리하기
@Getter
@RequiredArgsConstructor
public enum Errorcode {
NOT_EXIST_PRODUCT("1001", "product.not_exist_product"),
OUT_OF_STOCK("1200", "product.out_of_stock"),
UNKNOWN("9999", "product.unknown"),
;
private final String code;
private final String propertiesCode;
private String reason;
@RequiredArgsConstructor
@Component
public static class ErrorreasonInjector {
private final MessageSource messageSource;
@Value("${application.locale}")
private String locale;
@PostConstruct
public void postConstruct() {
Arrays.stream(Errorcode.values())
.forEach(errorcode -> errorcode.reason = messageSource.getMessage(errorcode.getPropertiesCode(), null, Locale.forLanguageTag(this.locale)));
}
}
}
// messages.properties
product.not_exist_product=대상 상품 없음
product.out_of_stock=상품 재고 없음
product.unknown=알 수 없는 에러
장점
- 작은 프로젝트에 적합합니다.
- 메시지 국제화가 용이합니다.
단점
- name과 code 변경 시 배포가 필요합니다.
- admin 개발이 필요한 경우 코드 중복이 발생합니다.
- 프로젝트 밖에서 전체 에러 목록을 알기 힘듭니다.
- MessageSource는 Map 자료구조와 같이 key, value 한 쌍만을 지원합니다.
실무 코드와 인프런의 김영한님 스프링 MVC 2편 - 메시지, 국제화를 보고 구현했습니다.
하지만 여전히 STEP 1의 단점을 가지고 있으며, 에러 표현에 필요한 name, code, reason의 값을 보여주기 힘듭니다.
STEP 3. DB에서 가져오기
@Getter
@RequiredArgsConstructor
public enum Errorcode {
NOT_EXIST_PRODUCT,
OUT_OF_STOCK,
UNKNOWN;
private String code;
private String reason;
@RequiredArgsConstructor
@Component
public static class ErrorreasonInjector {
private final ErrorcodeDataRepository repository;
@Value("${application.locale}")
private String locale;
@PostConstruct
public void postConstruct() {
Arrays.stream(Errorcode.values())
.forEach(errorcode ->
repository.findByNameAndLocale(errorcode.name(), locale)
.ifPresent(e -> {
errorcode.code = e.getCode();
errorcode.reason = e.getReason();
}
)
);
}
}
}
// data.sql
insert into errorcode_data(locale, name, code, reason)
values ('ko', 'NOT_EXIST_PRODUCT', '1001', '대상 상품 없음'),
('ko', 'OUT_OF_STOCK', '1200', '상품 재고 없음'),
('ko', 'UNKNOWN', '9999', '알 수 없는 에러');
장점
- name, code, reason을 모두 표현할 수 있습니다.
- 메시지 국제화가 용이합니다.
- 프로젝트 밖에서 전체 에러 목록을 알기 쉽습니다.
단점
- name 변경시 배포가 필요합니다.
- DB 관리가 필요합니다.
앞에서 문제로 있던 것들을 조합하여 DB를 통해 구현했습니다.
name은 의도적으로 프로젝트 내에 두어 직관성을 높였습니다.
코드로 구현하지는 않았지만 더 생각해볼 것도 있습니다.
- EnumMap을 활용할 수 있습니다.
- DB 검색을 하는 STEP 3에서는 캐시를 활용할 수 있습니다.
- 빠른 변경 반영을 위해 evict 정책, 백도어 API, 또는 pub/sub를 통한 갱신을 도입할 수 있습니다.
- 에러 메시지를 동적으로 관리하고 싶다면
- 메시지를 String.format() 형태로 만들고 CustomException을 만들 때 매개변수를 받을 수 있습니다.
- 또는 CustomException 생성 시 별도의 필드를 활용할 수도 있습니다.
- 완벽한 국제화는 BCP47 등 고려해야될 점이 더 많습니다.
- 백엔드는 code만 관리하고 사용자에게 노출되는 reason은 프론트엔드가 관리하게 하는 방법도 있습니다.
마치며
지금의 회사는 조금 다른 형태로 사용하고 있습니다.
// 에러코드만으로 예외를 throw
throw new CustomException("4012");
// Supplier를 통해 네이밍은 했지만 일괄 검색은 힘듬
public static final Supplier<CustomException> ERROR_SLIP_DTL_NOT_FOUND = CustomException.supply("4012");
String의 4자리 숫자 code를 MessageSource의 find code 값으로 활용하고 있습니다.
MessageSource 기술을 활용하고 있긴 애매한 부분이 있다고 생각합니다.
- 지금의 회사는 국제화를 고려하는 단계가 아닙니다.
- code가 String이기에 type safe하지 않습니다.
- 프로젝트 내에서 직관적인 code - reason 매핑이 힘듭니다.
- 매핑을 위해서는 별도 관리되는 엑셀이나 DB를 직접 검색해야 합니다.
제가 선호하는 방식은 STEP 2와 STEP 3입니다.
에러 코드는 일반적인 도메인과 다른 성격을 가지고 있습니다.
- 보통 한번 만들어진 code는 변경되지 않습니다.
- 새로운 에러 유형이 생기면 새로운 code를 생성합니다.
- 반면 reason은 좀 더 구체화되거나 에러를 나누는 등 변경될 수 있습니다.
이 관점에서 볼 때 STEP 2의 방법은
- reason만 messages.properties로 관리됩니다.
- properties 파일을 공유하는 방식으로 프로젝트 외부에서도 접근할 수 있습니다.
반면 STEP 3의 방법은 DB 관리가 필요하긴 하지만 간결함과 생산성을 모두 가져갈 수도 있습니다.
'Series > 내가 해본' 카테고리의 다른 글
git branch 전략 + commit 기록 방법 선택하기 (3) | 2023.10.09 |
---|---|
현재 환경에 맞는 값을 가져오기 with enum (0) | 2022.05.18 |
DB 값을 enum 으로 표현해보자 (0) | 2021.11.20 |
Elastic Beanstalk 구성 삽질기 - 글쑤시개 (0) | 2021.08.14 |
Slack Bot 으로 채널에 글쓰기 (0) | 2021.08.04 |