대부분의 웹 서비스에서 "비밀번호 불일치", "작성자가 아님" 과 같은 비즈니스적 예외가 요청마다 자주 발생한다.
위와 같은 비즈니스 예외마다 Stacktrace를 생성하거나 로깅하면:
- 스택 캡처 오버헤드
- 로그 출력 오버헤드
등이 누적되어 애플리케이션 처리량에 직접적인 영향을 줄 수 있다.
정상적인 비즈니스의 흐름으로 볼 수 있는 비즈니스적 예외에서도 동일한 비용을 지불하며 사용하는 것이 맞을까?
Kotlin에서 예외를 처리할 수 있는 3가지 방법인:
- RuntimeException
- Stacktrace를 억제한 SuppressedException
- Either<Error, Value> 함수형 예외 모델
각 전략에 대해 싱글톤 재사용 여부에 따른 할당 비용 차이도 함께 측정하여 사용 전략 구축.
| 항목 | 사양 |
|---|---|
| JVM | Java 21, Kotlin 1.9.23 |
| Arrow-core | 1.2.4 |
| 테스트 도구 | JMH 1.37 |
| CPU | Apple M1 Pro 10‑core |
| 메모리 | 16 GB |
| OS | macOS 15.5 (Sequoia) |
-
오류율(errorProb): 1% / 10% / 30%
-
측정 지표: Throughput (ops/s)
-
JMH 설정:
jmh { fork = 3 // 측정용 JVM 3회 warmupForks = 1 // 워밍업 전용 JVM 1회 iterations = 5 // 측정 5회 warmupIterations = 2 // 워밍업 2회 } -
EitherError과 에 사용된SuppressedException클래스:// EitherError 클래스 sealed interface EitherError { val message: String // 비싱글톤 open class Instance( override val message: String, ) : EitherError // 싱글톤 data object SingletonInstance : Instance(FAILED_MESSAGE) { private fun readResolve(): Any = SingletonInstance } } // SuppressedException 클래스 sealed class SuppressedException( message: String, ) : RuntimeException(message, null, false, false) { // 비싱글톤 open class Instance( override val message: String, ) : SuppressedException(message) // 싱글톤 data object SingletonInstance : Instance(FAILED_MESSAGE) { private fun readResolve(): Any = SingletonInstance } }
전체 테스트 코드는 ErrorScenarioBenchmark.kt 참고
| 에러율 1% (ops/s) | 에러율 10% (ops/s) | 에러율 30% (ops/s) | |
|---|---|---|---|
| baseline | 215,204,867 (100%) |
215,468,070 (100%) |
214,801,889 (100%) |
| runtimeException | 79,907,851 (37.13%) |
13,655,039 (6.34%) |
4,846,679 (2.26%) |
| suppressedException | 162,859,646 (75.68%) |
145,488,242 (67.52%) |
114,031,863 (53.10%) |
| suppressedExceptionWithSingleton | 211,190,290 (98.13%) |
207,712,218 (96.40%) |
182,137,598 (84.80%) |
| either | 145,141,856 (67.44%) |
136,870,922 (63.52%) |
118,797,754 (55.31%) |
| eitherWithSingleton | 148,588,796 (69.05%) |
142,431,633 (66.10%) |
129,384,834 (60.23%) |
테스트 결과 파일은 results.txt 참고
-
RuntimeException
fillInStackTrace()호출로 스택 프레임 순회 비용이 가장 큼- 즉, 예외 발생시 매번 예외 객체를 생성하여 Stacktrace 캡처 수행으로 인해 가장 큰 손실을 보임
-
SuppressedException
writableStackTrace=false설정으로 Stacktrace 오버헤드 제거- RuntimeException 대비 10 ~ 30배 좋은 처리율을 보임
- 예외 클래스를 재사용(싱글톤)할 경우 baseline 대비 85% ~ 98%로 가장 높은 처리율을 보임
-
Either
Throwable을 상속하지 않아 Stacktrace 비용이 없음- 하지만, 매 호출마다
Left/Right래퍼 객체 오버헤드가 있음 - 오류 객체를 재사용(싱글톤)해도 크게 처리율이 개선되지 않음 (약 3% ~ 9% 정도)
- 빈번히 발생하는 비즈니스적 예외에서 Stacktrace를 포함한 예외 발생은 지양
- 발생 빈도가 매우 낮은 시스템 장애 혹은 예상치 못한 오류에 한에 사용하는 것이 바람직
- 비즈니스적 예외에서는
SuppressedException혹은Either와 같은 경량 예외 객체를 사용 - 성공과 실패에 따른 비즈니스 로직이 여러 단계에 걸쳐있을 경우 성공과 실패를 모두 반환하는
Either사용 권장
(Either 에 대한 usecase 아티클 - 코틀린 함수형 프로그래밍의 길을 찾아서)