다크 모드
에러 처리
truloop-core의 에러 처리 전략을 설명합니다. BusinessException 계층 구조, StatusPages 매핑, HTTP 에러 응답 형식을 다룹니다.
에러 처리 흐름
UseCase throws Exception → StatusPages catches → Maps to HTTP response with ErrorDetailBusinessException 계층
BusinessException은 예상된 비즈니스 실패를 나타내는 sealed class입니다. Use Case에서 비즈니스 규칙 위반 시 발생시킵니다.
- 패키지:
co.butbeautiful.truloop.application.shared.exception - 모듈:
:application
kotlin
sealed class BusinessException(
message: String,
val errorCode: String,
val metadata: Map<String, String> = emptyMap(),
cause: Throwable? = null,
val displayType: String? = null,
) : RuntimeException(message, cause)하위 Exception 클래스
| Exception 클래스 | HTTP 상태 | 기본 에러 코드 | 용도 |
|---|---|---|---|
NotFoundException | 404 | ERROR_TYPE_NOT_FOUND | 리소스를 찾을 수 없음 |
ForbiddenException | 403 | ERROR_TYPE_FORBIDDEN | 접근 금지 |
BadRequestException | 400 | ERROR_TYPE_BAD_REQUEST | 잘못된 요청 |
ConflictException | 409 | ERROR_TYPE_CONFLICT | 리소스 충돌 |
UnauthorizedException | 401 | ERROR_TYPE_UNAUTHENTICATED | 인증 실패 |
ServiceUnavailableException | 503 | ERROR_TYPE_SERVICE_UNAVAILABLE | 서비스 일시 불가 |
InternalException | 500 | ERROR_TYPE_INTERNAL_SERVER_ERROR | 내부 서버 오류 |
기능별 Exception 정의
각 도메인 기능은 application/{feature}/exception/ 디렉터리에 자체 Exception 클래스를 정의합니다:
kotlin
// application/loop/exception/LoopExceptions.kt
object LoopExceptions {
class LoopNotFound(val loopEid: String) : BusinessException.NotFoundException(
message = "Loop not found: $loopEid",
errorCode = "ERROR_TYPE_LOOP_NOT_FOUND",
metadata = mapOf("loop_eid" to loopEid),
)
class NotLoopParticipant(loopEid: String) : BusinessException.ForbiddenException(
message = "User is not a participant of loop: $loopEid",
errorCode = "ERROR_TYPE_FORBIDDEN",
metadata = mapOf("loop_eid" to loopEid),
)
}주요 기능별 Exception 그룹:
| Exception 그룹 | 예시 |
|---|---|
LoopExceptions | LoopNotFound, NotLoopParticipant, NotLoopHost, LoopAlreadyExists, DeadlineInPast, DeadlineAfterProposedDates, InvalidProposedDatesFormat |
AuthExceptions | InvalidPhoneNumberFormat, InvalidPhoneVerification, RefreshTokenExpired |
UserExceptions | UserNotFound, InvalidUsernameFormat, UsernameUnavailable |
ChatExceptions | ChatUserNotFound, SelfChatNotAllowed, ChatServiceUnavailable |
StoryExceptions (내부명: Story) | StoryNotFound, StoryAlreadyExists, NotStoryOwner |
LoopMediaExceptions | 룹 미디어 관련 에러 |
LoopParticipantExceptions | 참여자 관련 에러 |
LoopInvitationExceptions | 초대 관련 에러 |
GuestParticipantExceptions | 비회원 참여자 관련 에러 |
SecretaryExceptions | AI 비서 관련 에러 |
MissionExceptions | 미션 관련 에러 |
EtaExceptions | ETA 관련 에러 |
CommentExceptions | 룹 댓글 관련 에러 |
ContactExceptions | 연락처 관련 에러 |
VerificationExceptions | 인증 관련 에러 |
ReportExceptions | 신고 관련 에러 |
BlockExceptions | 차단 관련 에러 |
DeviceExceptions | 디바이스 관련 에러 |
CoverTemplateExceptions | 커버 템플릿 관련 에러 |
CoverSelectionExceptions | 커버 선택 관련 에러 |
LiveActivityExceptions | Live Activity 관련 에러 |
AppVersionExceptions | 앱 버전 관련 에러 |
NotificationExceptions | 알림 관련 에러 |
InfrastructureException 계층
InfrastructureException은 외부 시스템 장애를 나타냅니다. Adapter 계층에서 발생시킵니다.
- 패키지:
co.butbeautiful.truloop.common.exception - 모듈:
:common
| Exception 클래스 | HTTP 상태 | 주요 속성 | 용도 |
|---|---|---|---|
DatabaseException | 500 | operation, cause | DB 연결/쿼리 실패 |
ExternalServiceException | 503 | service, operation, statusCode | 서드파티 API 장애 |
NetworkException | 503 | service, cause | 네트워크 연결 문제 |
ResourceUnavailableException | 503 | resource | 리소스 일시 불가 |
ConfigurationException | 500 | component, reason | 설정 누락/오류 |
StatusPages 매핑
Ktor의 StatusPages 플러그인이 Exception을 HTTP 응답으로 변환합니다. 설정은 adapter:web의 StatusPagesConfig.kt(co.butbeautiful.truloop.adapter.web.error 패키지)에 집중되어 있으며, Bootstrap의 StatusPages.kt에서 위임 호출합니다.
매핑 테이블
| Exception | HTTP 상태 코드 | 처리 |
|---|---|---|
NotFoundException | 404 Not Found | 에러 응답 반환 |
ForbiddenException | 403 Forbidden | 에러 응답 반환 |
BadRequestException | 400 Bad Request | 에러 응답 반환 |
ConflictException | 409 Conflict | 에러 응답 반환 |
UnauthorizedException | 401 Unauthorized | 에러 응답 반환 |
ServiceUnavailableException | 503 Service Unavailable | 로깅 + 에러 응답 반환 |
InternalException | 500 Internal Server Error | 에러 응답 + Sentry 보고 |
ExternalServiceException | 503 Service Unavailable | 에러 응답 + Sentry 보고 |
DatabaseException | 500 Internal Server Error | 에러 응답 + Sentry 보고 |
NetworkException | 503 Service Unavailable | 에러 응답 + Sentry 보고 |
ResourceUnavailableException | 503 Service Unavailable | 에러 응답 + Sentry 보고 |
ConfigurationException | 500 Internal Server Error | 에러 응답 + Sentry 보고 |
MissingRequestParameterException | 400 Bad Request | 파라미터명 포함 에러 응답 |
BadRequestException (Ktor) | 400 Bad Request | 에러 응답 반환 |
RequestValidationException | 400 Bad Request | 필드별 검증 오류 상세 반환 |
SerializationException | 400 Bad Request | 잘못된 요청 형식 에러 응답 |
기타 Throwable | 500 Internal Server Error | 에러 응답 + Sentry 보고 |
정보
i18n 지원: 모든 에러 응답은 요청 헤더의 locale 정보를 추출하여 MessageSourcePort를 통해 다국어 메시지를 생성합니다. errorCode가 i18n 메시지 키로 사용됩니다.
에러 응답 형식
ErrorDetail (JSON 응답 구조)
ErrorDetail은 adapter:web의 co.butbeautiful.truloop.adapter.web.shared.dto 패키지에 정의됩니다.
json
{
"type": "ERROR_TYPE_LOOP_NOT_FOUND",
"display_type": "DISPLAY_TYPE_SNACKBAR",
"message": {
"locale": "ko-KR",
"text": "룹을 찾을 수 없습니다."
},
"metadata_proto_json": "{\"loop_eid\":\"0HJKQ3N8XZ4M8\"}"
}| 필드 | 타입 | 설명 |
|---|---|---|
type | String | 머신 리더블 에러 코드. 클라이언트가 이 코드로 에러 유형을 식별 |
display_type | String | 클라이언트 UI 표시 방식 힌트 |
message | LocalizedMessage | 다국어 메시지 (locale + text) |
metadata_proto_json | String? | 추가 컨텍스트 정보 (JSON 문자열, 선택적) |
Display Type
| 값 | 의미 |
|---|---|
DISPLAY_TYPE_UNSPECIFIED | 미지정 |
DISPLAY_TYPE_NONE | UI 표시 없음 (조용히 처리) |
DISPLAY_TYPE_SNACKBAR | 스낵바/토스트로 표시 (기본값) |
DISPLAY_TYPE_DIALOG | 다이얼로그로 표시 |
주의
BusinessException의 displayType이 null인 경우 StatusPages에서 DISPLAY_TYPE_SNACKBAR를 기본값으로 사용합니다.
사용 가이드
Use Case에서 에러 발생
kotlin
class GetLoopUseCase {
suspend fun execute(eid: String): Loop {
return loopRepository.findByEid(eid)
?: throw LoopExceptions.LoopNotFound(eid)
}
}Adapter에서 인프라 에러 발생
kotlin
class SendbirdChatAdapter : ChatPort {
override suspend fun createChannel(...) {
try {
sendbirdClient.createGroupChannel(...)
} catch (e: Exception) {
throw InfrastructureException.ExternalServiceException(
service = "Sendbird",
operation = "createGroupChannel",
statusCode = 500,
cause = e,
)
}
}
}정보
핵심 원칙:
BusinessException은 Use Case에서 발생 (예상된 비즈니스 규칙 위반)InfrastructureException은 Adapter에서 발생 (외부 시스템 장애)- 호출자가 에러 타입에 따라 분기해야 하는 경우
sealed Result사용 - 에러 복구가 불필요한 경우에만 Exception 사용
변경 이력
| 날짜 | 내용 |
|---|---|
| 2026-03-11 | LoopExceptions에 DeadlineInPast, DeadlineAfterProposedDates, InvalidProposedDatesFormat 추가 |
| 2026-03-10 | 에러 응답 형식을 실제 ErrorDetail DTO에 맞게 수정 (type, display_type, message.locale/text, metadata_proto_json). Display Type에 DISPLAY_TYPE_UNSPECIFIED, DISPLAY_TYPE_DIALOG 추가. StatusPages 매핑 테이블을 Infrastructure Exception별 개별 매핑으로 세분화. 기능별 Exception 그룹 목록을 실제 23개 그룹에 맞게 확장. Adapter 예시를 ExternalServiceException의 실제 생성자(service, operation, statusCode)에 맞게 수정. LoopExceptions 예시를 실제 코드 기반으로 갱신. |