Skip to content

에러 처리

truloop-core의 에러 처리 전략을 설명합니다. BusinessException 계층 구조, StatusPages 매핑, HTTP 에러 응답 형식을 다룹니다.


에러 처리 흐름

UseCase throws Exception → StatusPages catches → Maps to HTTP response with ErrorDetail

BusinessException 계층

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 상태기본 에러 코드용도
NotFoundException404ERROR_TYPE_NOT_FOUND리소스를 찾을 수 없음
ForbiddenException403ERROR_TYPE_FORBIDDEN접근 금지
BadRequestException400ERROR_TYPE_BAD_REQUEST잘못된 요청
ConflictException409ERROR_TYPE_CONFLICT리소스 충돌
UnauthorizedException401ERROR_TYPE_UNAUTHENTICATED인증 실패
ServiceUnavailableException503ERROR_TYPE_SERVICE_UNAVAILABLE서비스 일시 불가
InternalException500ERROR_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 그룹예시
LoopExceptionsLoopNotFound, NotLoopParticipant, NotLoopHost, LoopAlreadyExists, DeadlineInPast, DeadlineAfterProposedDates, InvalidProposedDatesFormat
AuthExceptionsInvalidPhoneNumberFormat, InvalidPhoneVerification, RefreshTokenExpired
UserExceptionsUserNotFound, InvalidUsernameFormat, UsernameUnavailable
ChatExceptionsChatUserNotFound, SelfChatNotAllowed, ChatServiceUnavailable
StoryExceptions (내부명: Story)StoryNotFound, StoryAlreadyExists, NotStoryOwner
LoopMediaExceptions룹 미디어 관련 에러
LoopParticipantExceptions참여자 관련 에러
LoopInvitationExceptions초대 관련 에러
GuestParticipantExceptions비회원 참여자 관련 에러
SecretaryExceptionsAI 비서 관련 에러
MissionExceptions미션 관련 에러
EtaExceptionsETA 관련 에러
CommentExceptions룹 댓글 관련 에러
ContactExceptions연락처 관련 에러
VerificationExceptions인증 관련 에러
ReportExceptions신고 관련 에러
BlockExceptions차단 관련 에러
DeviceExceptions디바이스 관련 에러
CoverTemplateExceptions커버 템플릿 관련 에러
CoverSelectionExceptions커버 선택 관련 에러
LiveActivityExceptionsLive Activity 관련 에러
AppVersionExceptions앱 버전 관련 에러
NotificationExceptions알림 관련 에러

InfrastructureException 계층

InfrastructureException외부 시스템 장애를 나타냅니다. Adapter 계층에서 발생시킵니다.

  • 패키지: co.butbeautiful.truloop.common.exception
  • 모듈: :common
Exception 클래스HTTP 상태주요 속성용도
DatabaseException500operation, causeDB 연결/쿼리 실패
ExternalServiceException503service, operation, statusCode서드파티 API 장애
NetworkException503service, cause네트워크 연결 문제
ResourceUnavailableException503resource리소스 일시 불가
ConfigurationException500component, reason설정 누락/오류

StatusPages 매핑

Ktor의 StatusPages 플러그인이 Exception을 HTTP 응답으로 변환합니다. 설정은 adapter:webStatusPagesConfig.kt(co.butbeautiful.truloop.adapter.web.error 패키지)에 집중되어 있으며, Bootstrap의 StatusPages.kt에서 위임 호출합니다.

매핑 테이블

ExceptionHTTP 상태 코드처리
NotFoundException404 Not Found에러 응답 반환
ForbiddenException403 Forbidden에러 응답 반환
BadRequestException400 Bad Request에러 응답 반환
ConflictException409 Conflict에러 응답 반환
UnauthorizedException401 Unauthorized에러 응답 반환
ServiceUnavailableException503 Service Unavailable로깅 + 에러 응답 반환
InternalException500 Internal Server Error에러 응답 + Sentry 보고
ExternalServiceException503 Service Unavailable에러 응답 + Sentry 보고
DatabaseException500 Internal Server Error에러 응답 + Sentry 보고
NetworkException503 Service Unavailable에러 응답 + Sentry 보고
ResourceUnavailableException503 Service Unavailable에러 응답 + Sentry 보고
ConfigurationException500 Internal Server Error에러 응답 + Sentry 보고
MissingRequestParameterException400 Bad Request파라미터명 포함 에러 응답
BadRequestException (Ktor)400 Bad Request에러 응답 반환
RequestValidationException400 Bad Request필드별 검증 오류 상세 반환
SerializationException400 Bad Request잘못된 요청 형식 에러 응답
기타 Throwable500 Internal Server Error에러 응답 + Sentry 보고

정보

i18n 지원: 모든 에러 응답은 요청 헤더의 locale 정보를 추출하여 MessageSourcePort를 통해 다국어 메시지를 생성합니다. errorCode가 i18n 메시지 키로 사용됩니다.


에러 응답 형식

ErrorDetail (JSON 응답 구조)

ErrorDetailadapter:webco.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\"}"
}
필드타입설명
typeString머신 리더블 에러 코드. 클라이언트가 이 코드로 에러 유형을 식별
display_typeString클라이언트 UI 표시 방식 힌트
messageLocalizedMessage다국어 메시지 (locale + text)
metadata_proto_jsonString?추가 컨텍스트 정보 (JSON 문자열, 선택적)

Display Type

의미
DISPLAY_TYPE_UNSPECIFIED미지정
DISPLAY_TYPE_NONEUI 표시 없음 (조용히 처리)
DISPLAY_TYPE_SNACKBAR스낵바/토스트로 표시 (기본값)
DISPLAY_TYPE_DIALOG다이얼로그로 표시

주의

BusinessExceptiondisplayTypenull인 경우 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-11LoopExceptions에 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 예시를 실제 코드 기반으로 갱신.