Skip to content

Repository files navigation

Common

404-Factory 애플리케이션에서 공통으로 사용하는 Java 라이브러리입니다.

이 저장소는 표준 API 응답 형식, 예외 추상화, Spring MVC 전역 응답/예외 처리 기능뿐만 아니라, 이벤트 주도 아키텍처(Event-Driven Architecture)를 위한 공통 이벤트 봉투 스키마, Kafka 연동 지원, 그리고 Transactional Outbox 및 Inbox 패턴을 구현한 자바 모듈들을 제공합니다.
Gradle 멀티 모듈 프로젝트로 구성되어 있습니다.


Submodules

모듈 설명
core ApiResponse, ErrorResponse, ValidationException, ErrorCode, BaseException 등 공통 DTO와 예외 계약을 제공합니다.
contract Spring MVC 환경에서 응답 래핑과 전역 예외 처리를 담당하는 GlobalControllerAdvice를 자동 설정으로 제공합니다. core 모듈에 의존합니다.
event 도메인 이벤트 및 Kafka 이벤트 메시지 발행을 위한 표준 스키마(Event, EventEnvelope, EventPayload, EventType 등)와 EventEnvelopeFactory, IdempotencyKeyGenerator 등의 인프라 인터페이스를 제공합니다.
kafka Kafka 이벤트 메시지 발행 및 소비를 위한 EventPublisher, CommonKafkaConsumer, EventDispatcher, EventHandler를 제공합니다. event 모듈에 의존합니다.
outbox-core Transactional Outbox 패턴을 구현하기 위한 도메인 모델(OutboxEvent), 저장소 인터페이스(OutboxRepository) 및 공통 서비스(OutboxService)를 제공합니다. event 모듈에 의존합니다.
outbox-jpa JPA 기반의 Outbox 영속성 레이어(OutboxEntity, JpaOutboxRepository)와 로컬 Spring 이벤트를 가로채 Outbox 테이블에 자동으로 저장하는 OutboxEventListener를 제공합니다. outbox-corekafka 모듈에 의존합니다.
inbox-core Idempotent Consumer (Inbox 패턴)를 위한 도메인 모델(ProcessedEvent), 이력 저장소 인터페이스(ProcessedEventRepository) 및 서비스(InboxService)를 제공합니다. event 모듈에 의존합니다.
inbox-jpa JPA 기반의 Inbox 이력 관리 및 @InboxProcessed 어노테이션 기반 AOP(InboxProcessedAspect)를 제공하여 손쉽게 중복 이벤트 처리를 방지합니다. inbox-core 모듈에 의존합니다.

Tech Stack

  • Java 17
  • Gradle 8.14 (Kotlin DSL)
  • Spring Boot 3.5.14

Core 모듈

core 모듈은 여러 서비스에서 공통으로 사용할 API 응답 DTO와 예외 관련 인터페이스/클래스를 제공합니다.

ApiResponse<T>

성공 응답과 실패 응답을 동일한 형식으로 표현하기 위한 표준 API 응답 래퍼입니다.

성공 응답 예시:

{ 
    "success": true,
    "status": 200,
    "message": "success",
    "data": {
        "id": 1,
        "name": "example"
    },
    "timestamp": "2026-05-21T10:15:30" 
}

실패 응답 예시:

{
  "success": false,
  "status": 400,
  "message": "Invalid input value.",
  "error": {
    "code": "CA002",
    "details": [{
      "target": "name",
      "message": "must not be blank"
    }]
  },
  "timestamp": "2026-05-21T10:15:30"
}

ErrorResponse

ApiResponseerror 필드에 들어가는 에러 상세 정보입니다.

포함하는 값은 다음과 같습니다.

  • code: 애플리케이션에서 정의한 에러 코드
  • details: 유효성 검증 실패 상세 목록

ValidationException

Jakarta Bean Validation 과정에서 발생한 단일 검증 실패 정보를 표현합니다.

포함하는 값은 다음과 같습니다.

  • target: 검증에 실패한 필드, 파라미터 또는 객체 이름
  • message: 검증 실패 메시지

ErrorCode

서비스별 에러 코드를 표준화하기 위한 인터페이스입니다.

구현체는 다음 값을 제공합니다.

  • HTTP 상태 코드
  • 에러 코드
  • 사용자 또는 클라이언트에게 전달할 메시지

예시:

public enum UserErrorCode implements ErrorCode {
    USER_NOT_FOUND(404, "USER001", "User not found.");
    private final int status;
    private final String code;
    private final String message;

    UserErrorCode(int status, String code, String message) {
        this.status = status;
        this.code = code;
        this.message = message;
    }

    @Override
    public int getStatus() {
        return status;
    }

    @Override
    public String getCode() {
        return code;
    }

    @Override
    public String getMessage() {
        return message;
    }
}

BaseException

도메인별 커스텀 예외의 기반이 되는 추상 런타임 예외 클래스입니다.

BaseException을 상속하면 GlobalControllerAdvice에서 일관된 형식의 에러 응답으로 처리할 수 있습니다.

예시:

public class UserException extends BaseException {
    public UserException(ErrorCode errorCode) {
        super(errorCode);
    }

    public UserException(ErrorCode errorCode, Throwable cause) {
        super(errorCode, cause);
    }
}

Contract 모듈

contract 모듈은 Spring MVC 기반 애플리케이션에서 공통 응답 형식과 전역 예외 처리를 제공합니다.

주요 기능은 다음과 같습니다.

  • 성공 응답 자동 래핑
  • 비즈니스 예외 처리
  • 유효성 검증 예외 처리
  • Spring MVC 예외 처리

Auto Configuration

contract 모듈은 Spring Boot의 Auto Configuration을 지원합니다.

ContractAutoConfiguration은 다음 조건을 만족할 때 GlobalControllerAdvice Bean을 등록합니다.

  • Servlet 기반 Web Application인 경우
  • ObjectMapper Bean이 존재하는 경우
  • 사용자가 직접 등록한 GlobalControllerAdvice Bean이 없는 경우

응답 자동 래핑

컨트롤러가 반환하는 성공 응답은 자동으로 ApiResponse 형식으로 감싸집니다.

컨트롤러 예시:

@GetMapping("/users/{id}")
public UserResponse getUser(@PathVariable Long id) {
    return userService.getUser(id);
}

응답 예시:

{
  "success": true,
  "status": 200,
  "message": "success",
  "data": { 
    "id": 1, 
    "name": "example"
  }, 
  "timestamp": "2026-05-21T10:15:30"
}

이미 ApiResponse 타입으로 반환한 응답은 다시 감싸지 않습니다.

응답 래핑 제외 대상

다음 경우에는 응답을 ApiResponse로 감싸지 않습니다.

  • 응답 본문이 이미 ApiResponse인 경우
  • HTTP 메서드가 HEAD, OPTIONS, TRACE인 경우
  • Servlet 기반 응답이 아닌 경우
  • 응답 본문을 가지지 않는 HTTP 상태 코드인 경우
    • 1xx
    • 3xx
    • 204 No Content
  • org.springdoc 패키지에 속한 클래스의 응답인 경우

org.springdoc 패키지는 OpenAPI / Swagger 문서 생성에 영향을 주지 않기 위해 제외됩니다.

String 응답 처리

컨트롤러가 String을 반환하는 경우에도 JSON 형식의 ApiResponse로 응답하기 위해 별도 직렬화 처리를 수행합니다.

컨트롤러 예시:

@GetMapping("/hello") public String hello() { 
    return "hello"; 
}

응답 예시:

{ 
  "success": true, 
  "status": 200, 
  "message": "success", 
  "data": "hello", 
  "timestamp": "2026-05-21T10:15:30"
}

예외 처리

비즈니스 예외 처리

BaseException을 상속한 예외는 GlobalControllerAdvice에서 처리됩니다.

예시:

@GetMapping("/users/{id}")
public UserResponse getUser(@PathVariable Long id) {
    if (id == null) {
        throw new UserException(UserErrorCode.USER_NOT_FOUND);
    }
    return userService.getUser(id);
}

응답 예시:

{ 
  "success": false, 
  "status": 404, 
  "message": "User not found.", 
  "error": { 
    "code": "USER001"
  }, 
  "timestamp": "2026-05-21T10:15:30" }

요청 DTO 검증 실패 처리

@Valid 또는 Jakarta Validation을 통해 요청 값 검증에 실패하면 표준 에러 응답으로 변환됩니다.

요청 DTO 예시:

public class CreateUserRequest {
    @NotBlank
    private String name;

    @Email
    private String email;

    // getter/setter
}

컨트롤러 예시:

@PostMapping("/users")
  public UserResponse createUser(@Valid @RequestBody CreateUserRequest request) { 
    return userService.createUser(request); 
}

응답 예시:

{ 
  "success": false, 
  "status": 400, 
  "message": "Invalid input value.", 
  "error": { 
    "code": "CA002", 
    "details": "must not be blank"
  }, 
  "timestamp": "2026-05-21T10:15:30"
}

메서드 파라미터 검증 실패 처리

컨트롤러 메서드의 @RequestParam, @PathVariable, @RequestHeader, @RequestBody 등에서 발생하는 검증 실패도 처리합니다.

입력값 검증 실패는 400 Bad Request로 처리되며, 반환값 검증 실패는 500 Internal Server Error로 처리됩니다.

기타 Spring MVC 예외 처리

Spring MVC 내부에서 발생한 예외는 ApiResponse 에러 형식으로 변환됩니다.

처리되지 않은 예외

별도로 처리되지 않은 예외는 내부 서버 오류로 변환됩니다.

응답 예시:

{ 
  "success": false, 
  "status": 500, 
  "message": "An unexpected internal server error occurred.", 
  "error": { 
    "code": "CA001"
  }, 
  "timestamp": "2026-05-21T10:15:30" }

Contract 에러 코드

코드 HTTP 상태 메시지
CA001 500 An unexpected internal server error occurred.
CA002 400 Invalid input value.
CA003 500 Invalid output value.
CA004 500 Failed to write JSON response.
CA005 XXX MVC-related error occurred.

CA005는 Spring MVC 예외 처리 과정에서 실제 응답 상태 코드를 사용합니다.


Event 모듈

event 모듈은 분산 이벤트 기반 아키텍처 환경에서 메시지의 정합성과 규격을 보장하기 위한 핵심 도메인 모델과 추상화 레이어를 제공합니다.

주요 인터페이스 및 클래스

  1. Event<T extends EventPayload>
    • 모든 도메인 이벤트가 지녀야 할 기본적인 메타데이터 구조를 강제하는 인터페이스입니다.
    • idempotencyKey (멱등 키), eventType (이벤트 논리명), payload (실제 바디), traceId (분산 추적 식별자), timestamp (발생 시각), aggregateType (애그리거트 타입), aggregateId (애그리거트 식별자) 정보에 접근할 수 있는 메서드를 제공합니다.
  2. EventEnvelope<T extends EventPayload>
    • Event 인터페이스의 표준 구현체입니다. Jackson 직렬화/역직렬화 시 상속 구조를 안정적으로 유지하기 위해 @JsonTypeInfo 설정을 탑재하고 있습니다.
  3. EventPayload
    • 이벤트 바디로 사용될 비즈니스 DTO 클래스를 마킹하기 위한 인터페이스입니다.
  4. EventType
    • 이벤트의 성격과 물리적 구분을 위한 이름을 제공하는 인터페이스입니다. 주로 Enum 클래스로 구현하여 사용합니다.
  5. EventEnvelopeFactory
    • IdempotencyKeyGeneratorTraceIdProvider, Clock을 주입받아, 중복되지 않는 고유 멱등성 키와 분산 추적용 traceId가 포함된 완성형 EventEnvelope 인스턴스를 일관되게 생성할 수 있도록 돕는 유틸리티 클래스입니다.

Kafka 모듈

kafka 모듈은 Kafka 브로커로 이벤트를 전송하고, 수신된 이벤트를 적절한 서비스(핸들러)로 라우팅하는 기본 메시징 솔루션을 자동 설정과 함께 제공합니다.

주요 인터페이스 및 클래스

  1. EventPublisher
    • 도메인 이벤트를 외부 메시징 브로커 또는 로컬 버스로 발행하는 기본 인터페이스입니다.
  2. CommonKafkaConsumer
    • 스프링 @KafkaListener 어노테이션을 탑재하고 브로커로부터 레코드를 주기적으로 수집하여 EventDispatcher로 이관해 주는 기본 컨슈머 클래스입니다.
    • sigma.event.consumer.topicsgroupId 설정을 활성화하여 작동합니다.
  3. EventDispatcher
    • 들어온 JSON 메시지에서 eventType 필드를 식별한 뒤, 해당 이벤트를 구독하고 있는 EventHandler<?> 구현체를 스캔 및 매핑하여, payload 타입에 맞춰 역직렬화된 Event<T> 객체를 던져 주는 라우터 역할을 합니다.
  4. EventHandler<T extends EventPayload>
    • microservice 영역에서 특정 eventType의 이벤트를 수신하여 처리하는 비즈니스 리스너 규격입니다.

application.yml 설정 예시

sigma:
  event:
    consumer:
      topics: equipment-events, recipe-events
      groupId: anomaly-detection-service

Outbox 모듈 (outbox-core, outbox-jpa)

데이터베이스 상태 변경(Transaction)과 Kafka 이벤트 메시지 발행 간의 원자성(Atomicity)을 보장하기 위해 Transactional Outbox 패턴을 쉽게 활용할 수 있도록 지원합니다.

작동 메커니즘

  1. 비즈니스 로직을 수행하는 서비스에서 EventPublisher.publish(event)를 호출합니다.
  2. outbox-jpa 모듈은 이 이벤트를 Spring의 로컬 ApplicationEventPublisher로 포워딩합니다.
  3. @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)로 등록된 OutboxEventListener가 트랜잭션이 데이터베이스에 커밋되기 직전에 이를 가로채, outbox 테이블에 엔티티(OutboxEntity) 형태로 저장합니다.
  4. 이로써 비즈니스 데이터의 변경과 이벤트 저장이 하나의 로컬 데이터베이스 트랜잭션으로 묶여 원자성을 보장받게 됩니다.
  5. 저장된 Outbox 데이터는 이후 별도의 CDC 툴(Debezium 등)이나 Outbox Poller 스케줄러를 통해 실제 Kafka 브로커로 발행됩니다.

주요 구성 요소

  • OutboxEvent<T extends EventPayload>: EventEnvelope을 확장하며, 파티셔닝 키로 사용하기 위해 aggregateTypeaggregateId를 필수로 보관합니다.
  • OutboxService: 전해받은 이벤트를 직렬화하여 아웃박스 영속성 레이어에 적재하는 핵심 제어 인터페이스입니다.
  • OutboxEntity: 데이터베이스 outbox 테이블과 1:1 매핑되는 JPA 엔티티입니다.
  • OutboxEventListener: BEFORE_COMMIT 시점에 로컬 이벤트를 가로채 DB 아웃박스 레코드로 기록합니다.

사용 예시 (이벤트 발행 서비스)

@Service
@RequiredArgsConstructor
@Transactional
public class AnomalyDetectionServiceImpl implements AnomalyDetectionService {

    private final EventPublisher eventPublisher;
    private final EventEnvelopeFactory eventEnvelopeFactory;

    public void detectAnomaly(...) {
        // ... 비즈니스 로직 및 엔티티 수정
        
        AnomalyCreatedPayload payload = AnomalyCreatedPayload.builder()
                .equipmentId(equipment.getId())
                .severity("CRITICAL")
                .occurredTime(Instant.now())
                .build();

        // 1. 이벤트 봉투 생성
        EventEnvelope<AnomalyCreatedPayload> eventEnvelope = eventEnvelopeFactory.create(
                AnomalyEventType.ANOMALY_CREATED,
                payload
        );

        // 2. 이벤트 발행 (로컬 트랜잭션과 묶여 outbox 테이블에 저장됨)
        eventPublisher.publish(eventEnvelope);
    }
}

Inbox 모듈 (inbox-core, inbox-jpa)

메시지 브로커(Kafka)로부터 유입되는 중복 메시지나 재처리(Retry) 요청으로 인해 발생하는 비즈니스 오작동을 차단하기 위해 **Idempotent Consumer (Inbox 패턴)**를 제공합니다.

작동 메커니즘

  1. 카프카 토픽으로부터 이벤트를 수신하여 처리하는 핸들러의 process 메서드에 @InboxProcessed 어노테이션을 부착합니다.
  2. AOP 가로채기(InboxProcessedAspect)를 통해, 메서드가 실행되기 전 수신한 Event 객체의 idempotencyKeyeventType을 기반으로 InboxService.isAlreadyProcessed(idempotencyKey)를 조회합니다.
  3. 만약 이미 동일한 키로 처리 완료된 이력이 DB 테이블(processed_events)에 기록되어 있다면, 메서드 실행을 즉시 스킵하고 null을 반환합니다.
  4. 처음 유입된 이벤트인 경우, 메서드가 정상적으로 완료되면 커밋 시점에 성공 이력(ProcessedEventEntity)을 함께 데이터베이스에 영속화하여 중복 방지를 확정 짓습니다.

주요 구성 요소

  • ProcessedEvent: 처리 완료된 메시지 식별 모델입니다.
  • InboxService: 중복 유입 여부 판별 및 완료 이력 작성을 대행하는 코어 인터페이스입니다.
  • @InboxProcessed: 멱등 처리를 보장하고자 하는 타겟 메서드에 선언적으로 부여하는 어노테이션입니다.
  • ProcessedEventEntity: 데이터베이스 processed_events 테이블과 연동되는 JPA 엔티티입니다.

사용 예시 (이벤트 핸들러)

@Component
@RequiredArgsConstructor
public class EquipmentRecipeUpdatedHandler implements EventHandler<EquipmentRecipePayload> {

    private final EquipmentRecipeRepository equipmentRecipeRepository;

    @Override
    public String getEventType() {
        return "EquipmentRecipeUpdated";
    }

    @Override
    @Transactional
    @InboxProcessed // 멱등 처리 AOP 활성화
    public void process(Event<EquipmentRecipePayload> event) {
        // 이미 처리된 event.getIdempotencyKey()가 존재하는 경우 이 아래 코드는 실행되지 않고 자동 스킵됩니다.
        
        EquipmentRecipePayload payload = event.getPayload();
        
        EquipmentRecipe recipe = EquipmentRecipe.builder()
                .id(payload.getEquipmentRecipeId())
                .version(payload.getVersion())
                .build();
                
        equipmentRecipeRepository.save(recipe);
    }
}

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages