# 클라우드 아키텍처 패턴 적용 방안

## 1. 문서 개요

### 1.1 목적
회의록 작성 및 공유 개선 서비스의 마이크로서비스 아키텍처에 적용할 클라우드 디자인 패턴을 정의합니다.

### 1.2 범위
본 문서는 다음 6개의 핵심 클라우드 디자인 패턴의 적용 방안을 다룹니다:
- API Gateway
- Queue-Based Load Leveling
- Cache-Aside
- Publisher-Subscriber
- Asynchronous Request-Reply
- Health Endpoint Monitoring

### 1.3 참조 문서
- [유저스토리](../userstory.md)
- [클라우드 디자인 패턴 요약표](../../claude/cloud-design-patterns.md)
- [논리 아키텍처](../backend/logical/)

---

## 2. 적용 패턴 개요

### 2.1 패턴 분류

| 카테고리 | 패턴 | 적용 우선순위 | 주요 목적 |
|---------|------|--------------|----------|
| Design & Implementation | API Gateway | 높음 | 단일 진입점, 라우팅, 인증 |
| Messaging | Queue-Based Load Leveling | 높음 | 부하 분산, 비동기 처리 |
| Data Management | Cache-Aside | 높음 | 성능 최적화, DB 부하 감소 |
| Messaging | Publisher-Subscriber | 중간 | 이벤트 기반 통신 |
| Messaging | Asynchronous Request-Reply | 중간 | 장시간 작업 비동기 처리 |
| Management & Monitoring | Health Endpoint Monitoring | 높음 | 서비스 상태 모니터링 |

### 2.2 서비스별 패턴 적용 매트릭스

| 서비스 | API Gateway | Queue-Based | Cache-Aside | Pub-Sub | Async Request | Health Monitor |
|--------|-------------|-------------|-------------|---------|---------------|----------------|
| User Service | ✅ | - | ✅ | ✅ | - | ✅ |
| Meeting Service | ✅ | ✅ | ✅ | ✅ | - | ✅ |
| Transcript Service | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| AI Service | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Notification Service | ✅ | ✅ | - | ✅ | - | ✅ |
| Todo Service | ✅ | - | ✅ | ✅ | - | ✅ |

---

## 3. 패턴별 상세 적용 방안

### 3.1 API Gateway

#### 3.1.1 패턴 개요
**문제**: 클라이언트가 여러 마이크로서비스와 직접 통신하면 복잡도가 증가하고 보안 관리가 어려움

**해결**: 모든 클라이언트 요청을 처리하는 단일 진입점을 제공하여 라우팅, 인증, 로깅 등을 중앙화

#### 3.1.2 적용 방안

**구현 기술**
- Kong Gateway 또는 Spring Cloud Gateway
- JWT 기반 인증/인가
- Rate Limiting 및 Throttling

**주요 기능**
```yaml
API Gateway 역할:
  라우팅:
    - /api/users/* → User Service
    - /api/meetings/* → Meeting Service
    - /api/transcripts/* → Transcript Service
    - /api/ai/* → AI Service
    - /api/notifications/* → Notification Service
    - /api/todos/* → Todo Service

  인증/인가:
    - JWT 토큰 검증
    - 사용자 권한 확인
    - 회의 참여자 검증

  부가 기능:
    - Request/Response 로깅
    - Rate Limiting (사용자당 요청 제한)
    - 요청/응답 변환
    - CORS 처리
```

**적용 시나리오**
```
사용자 로그인 플로우:
1. Frontend → API Gateway: POST /api/users/login
2. API Gateway → User Service: 요청 라우팅
3. User Service → API Gateway: JWT 토큰 반환
4. API Gateway → Frontend: 토큰 전달 + 로깅

회의 생성 플로우:
1. Frontend → API Gateway: POST /api/meetings (with JWT)
2. API Gateway: JWT 검증
3. API Gateway → Meeting Service: 요청 라우팅
4. Meeting Service → API Gateway: 회의 정보 반환
5. API Gateway → Frontend: 응답 전달
```

#### 3.1.3 구현 예시

**Kong Gateway 설정 (YAML)**
```yaml
services:
  - name: user-service
    url: http://user-service:8080
    routes:
      - name: user-routes
        paths:
          - /api/users
        methods:
          - GET
          - POST
          - PUT
          - DELETE
    plugins:
      - name: jwt
      - name: rate-limiting
        config:
          minute: 100
          hour: 1000

  - name: meeting-service
    url: http://meeting-service:8080
    routes:
      - name: meeting-routes
        paths:
          - /api/meetings
        methods:
          - GET
          - POST
          - PUT
          - DELETE
    plugins:
      - name: jwt
      - name: rate-limiting
        config:
          minute: 200
          hour: 2000
```

**Spring Cloud Gateway 설정 (application.yml)**
```yaml
spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://USER-SERVICE
          predicates:
            - Path=/api/users/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10
                redis-rate-limiter.burstCapacity: 20

        - id: meeting-service
          uri: lb://MEETING-SERVICE
          predicates:
            - Path=/api/meetings/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 20
                redis-rate-limiter.burstCapacity: 40
```

#### 3.1.4 주의사항
- API Gateway가 SPOF(Single Point of Failure)가 되지 않도록 HA 구성 필요
- Gateway 자체의 성능이 병목이 되지 않도록 수평 확장 가능한 구조 유지
- 과도한 비즈니스 로직 포함 금지 (단순 라우팅 및 공통 기능만 처리)

---

### 3.2 Queue-Based Load Leveling

#### 3.2.1 패턴 개요
**문제**: 트래픽이 급증할 때 서비스가 과부하 상태가 되어 응답 지연 또는 실패 발생

**해결**: 메시지 큐를 사용하여 요청을 버퍼링하고, 서비스가 처리 가능한 속도로 소비

#### 3.2.2 적용 방안

**구현 기술**
- RabbitMQ 또는 Apache Kafka
- Spring AMQP / Spring Kafka

**적용 대상 서비스**
```yaml
적용 서비스:
  Meeting Service:
    - 대량 회의 생성 요청
    - 회의 종료 후 후처리 작업

  Transcript Service:
    - 회의록 생성 요청 (STT 처리)
    - 회의록 섹션별 검증 요청

  AI Service:
    - AI 일정 생성 요청
    - AI 요약 생성 요청
    - 다량의 AI 처리 요청

  Notification Service:
    - 알림 발송 요청 (이메일, SMS)
    - 대량 알림 발송
```

**적용 시나리오**
```
회의록 생성 플로우:
1. Frontend → Meeting Service: 회의 종료 요청
2. Meeting Service → Queue: 회의록 생성 메시지 발행
3. Transcript Service: Queue에서 메시지 소비 (속도 제어)
4. Transcript Service: STT 처리 및 회의록 생성
5. Transcript Service → Meeting Service: 완료 알림

AI 일정 생성 플로우:
1. Frontend → Meeting Service: AI 일정 생성 요청
2. Meeting Service → Queue: AI 처리 메시지 발행
3. AI Service: Queue에서 메시지 소비 (처리 속도 제어)
4. AI Service: AI 분석 및 일정 생성
5. AI Service → Notification Service: 완료 알림 발송
```

#### 3.2.3 구현 예시

**RabbitMQ 큐 정의**
```java
@Configuration
public class QueueConfig {

    // 회의록 생성 큐
    @Bean
    public Queue transcriptQueue() {
        return QueueBuilder.durable("transcript.creation.queue")
            .withArgument("x-max-length", 1000)  // 최대 메시지 수
            .withArgument("x-message-ttl", 3600000)  // 1시간 TTL
            .build();
    }

    // AI 처리 큐
    @Bean
    public Queue aiProcessingQueue() {
        return QueueBuilder.durable("ai.processing.queue")
            .withArgument("x-max-length", 500)
            .withArgument("x-message-ttl", 7200000)  // 2시간 TTL
            .build();
    }

    // 알림 발송 큐
    @Bean
    public Queue notificationQueue() {
        return QueueBuilder.durable("notification.queue")
            .withArgument("x-max-length", 2000)
            .withArgument("x-message-ttl", 1800000)  // 30분 TTL
            .build();
    }
}
```

**Producer 예시 (Meeting Service)**
```java
@Service
@RequiredArgsConstructor
public class TranscriptQueueProducer {

    private final RabbitTemplate rabbitTemplate;

    public void sendTranscriptCreationRequest(TranscriptCreationMessage message) {
        rabbitTemplate.convertAndSend(
            "transcript.creation.queue",
            message,
            msg -> {
                msg.getMessageProperties().setPriority(message.getPriority());
                msg.getMessageProperties().setExpiration("3600000"); // 1시간
                return msg;
            }
        );
        log.info("Transcript creation request sent: meetingId={}", message.getMeetingId());
    }
}
```

**Consumer 예시 (Transcript Service)**
```java
@Service
@RequiredArgsConstructor
public class TranscriptQueueConsumer {

    private final TranscriptService transcriptService;

    @RabbitListener(
        queues = "transcript.creation.queue",
        concurrency = "3-10"  // 동시 처리 워커 수 (최소 3, 최대 10)
    )
    public void handleTranscriptCreation(TranscriptCreationMessage message) {
        try {
            log.info("Processing transcript creation: meetingId={}", message.getMeetingId());
            transcriptService.createTranscript(message);
            log.info("Transcript creation completed: meetingId={}", message.getMeetingId());
        } catch (Exception e) {
            log.error("Transcript creation failed: meetingId={}", message.getMeetingId(), e);
            throw new AmqpRejectAndDontRequeueException("Failed to process transcript", e);
        }
    }
}
```

#### 3.2.4 주의사항
- 메시지 큐가 가득 찰 경우의 처리 전략 정의 필요 (거부, 대기, 우선순위 기반 처리)
- Dead Letter Queue 구성으로 실패 메시지 별도 처리
- Consumer의 동시 처리 수(concurrency) 적절히 설정하여 리소스 효율 극대화

---

### 3.3 Cache-Aside

#### 3.3.1 패턴 개요
**문제**: 데이터베이스 반복 조회로 인한 성능 저하 및 DB 부하 증가

**해결**: 애플리케이션이 캐시를 먼저 확인하고, 캐시 미스 시 DB에서 조회 후 캐시에 저장

#### 3.3.2 적용 방안

**구현 기술**
- Redis (분산 캐시)
- Spring Cache Abstraction
- Caffeine (로컬 캐시 - 옵션)

**적용 대상 데이터**
```yaml
캐싱 대상:
  User Service:
    - 사용자 프로필 정보 (TTL: 30분)
    - 사용자 권한 정보 (TTL: 1시간)

  Meeting Service:
    - 회의 기본 정보 (TTL: 10분)
    - 회의 참여자 목록 (TTL: 10분)
    - 회의 템플릿 목록 (TTL: 1시간)

  Transcript Service:
    - 회의록 조회 (TTL: 30분)
    - 회의록 섹션 정보 (TTL: 30분)

  Todo Service:
    - 사용자별 Todo 목록 (TTL: 5분)
    - Todo 진행 상태 통계 (TTL: 5분)
```

**캐시 전략**
```yaml
캐시 정책:
  읽기 집중 데이터:
    - 패턴: Cache-Aside
    - 전략: Lazy Loading
    - 예: 사용자 프로필, 회의 정보 조회

  쓰기 빈도 높은 데이터:
    - 패턴: Write-Through + Cache-Aside
    - 전략: 업데이트 시 캐시 무효화
    - 예: Todo 상태 변경, 회의 정보 수정

  캐시 무효화:
    - 데이터 변경 시 즉시 무효화
    - TTL 기반 자동 만료
    - 명시적 삭제 API 제공
```

**적용 시나리오**
```
사용자 프로필 조회:
1. User Service: Redis에서 사용자 프로필 조회
2. Cache Hit → 캐시 데이터 반환
3. Cache Miss → DB 조회 → Redis 저장 (TTL: 30분) → 데이터 반환

회의 정보 수정:
1. Meeting Service: 회의 정보 업데이트 (DB)
2. Meeting Service: Redis에서 해당 회의 캐시 삭제
3. 다음 조회 시 새로운 데이터로 캐시 재생성
```

#### 3.3.3 구현 예시

**Redis 설정 (application.yml)**
```yaml
spring:
  data:
    redis:
      host: localhost
      port: 6379
      password: ${REDIS_PASSWORD}
      lettuce:
        pool:
          max-active: 10
          max-idle: 5
          min-idle: 2

  cache:
    type: redis
    redis:
      time-to-live: 1800000  # 기본 TTL: 30분
      cache-null-values: false
      use-key-prefix: true
```

**Cache 설정**
```java
@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public RedisCacheConfiguration cacheConfiguration() {
        return RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(30))
            .serializeKeysWith(
                RedisSerializationContext.SerializationPair.fromSerializer(
                    new StringRedisSerializer()
                )
            )
            .serializeValuesWith(
                RedisSerializationContext.SerializationPair.fromSerializer(
                    new GenericJackson2JsonRedisSerializer()
                )
            );
    }

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();

        // 사용자 프로필 캐시 (TTL: 30분)
        cacheConfigurations.put("userProfile",
            RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(30)));

        // 회의 정보 캐시 (TTL: 10분)
        cacheConfigurations.put("meetingInfo",
            RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(10)));

        // Todo 목록 캐시 (TTL: 5분)
        cacheConfigurations.put("todoList",
            RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(5)));

        return RedisCacheManager.builder(connectionFactory)
            .cacheDefaults(cacheConfiguration())
            .withInitialCacheConfigurations(cacheConfigurations)
            .build();
    }
}
```

**서비스 레이어 적용**
```java
@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;

    // Cache-Aside 패턴: 조회
    @Cacheable(value = "userProfile", key = "#userId")
    public UserProfileDto getUserProfile(Long userId) {
        log.info("Cache miss - Loading user profile from DB: userId={}", userId);
        User user = userRepository.findById(userId)
            .orElseThrow(() -> new UserNotFoundException(userId));
        return UserProfileDto.from(user);
    }

    // 캐시 무효화: 업데이트
    @CacheEvict(value = "userProfile", key = "#userId")
    public UserProfileDto updateUserProfile(Long userId, UpdateUserProfileRequest request) {
        log.info("Updating user profile and evicting cache: userId={}", userId);
        User user = userRepository.findById(userId)
            .orElseThrow(() -> new UserNotFoundException(userId));
        user.updateProfile(request);
        userRepository.save(user);
        return UserProfileDto.from(user);
    }

    // 캐시 무효화: 삭제
    @CacheEvict(value = "userProfile", key = "#userId")
    public void deleteUser(Long userId) {
        log.info("Deleting user and evicting cache: userId={}", userId);
        userRepository.deleteById(userId);
    }
}
```

**회의 정보 캐싱**
```java
@Service
@RequiredArgsConstructor
public class MeetingService {

    private final MeetingRepository meetingRepository;

    @Cacheable(value = "meetingInfo", key = "#meetingId")
    public MeetingDto getMeeting(Long meetingId) {
        log.info("Cache miss - Loading meeting from DB: meetingId={}", meetingId);
        Meeting meeting = meetingRepository.findById(meetingId)
            .orElseThrow(() -> new MeetingNotFoundException(meetingId));
        return MeetingDto.from(meeting);
    }

    @CacheEvict(value = "meetingInfo", key = "#meetingId")
    public MeetingDto updateMeeting(Long meetingId, UpdateMeetingRequest request) {
        log.info("Updating meeting and evicting cache: meetingId={}", meetingId);
        Meeting meeting = meetingRepository.findById(meetingId)
            .orElseThrow(() -> new MeetingNotFoundException(meetingId));
        meeting.update(request);
        meetingRepository.save(meeting);
        return MeetingDto.from(meeting);
    }
}
```

#### 3.3.4 주의사항
- 캐시와 DB 간 데이터 정합성 유지 전략 필요
- 캐시 크기 제한 및 메모리 관리 (Eviction Policy 설정)
- Hot Key 문제 대응 (특정 키에 대한 과도한 접근)
- Cache Stampede 방지 (동시 다발적 Cache Miss 시 DB 부하)

---

### 3.4 Publisher-Subscriber (Pub-Sub)

#### 3.4.1 패턴 개요
**문제**: 서비스 간 강한 결합으로 인한 확장성 저하 및 이벤트 기반 통신의 어려움

**해결**: 메시지 브로커를 통해 이벤트를 발행하고, 관심 있는 서비스가 구독하여 처리

#### 3.4.2 적용 방안

**구현 기술**
- RabbitMQ (Exchange/Topic 기반) 또는 Apache Kafka (Topic 기반)
- Spring Cloud Stream

**적용 이벤트**
```yaml
도메인 이벤트:
  User Events:
    - UserCreated: 사용자 생성 이벤트
    - UserUpdated: 사용자 정보 변경 이벤트
    - UserDeleted: 사용자 삭제 이벤트

  Meeting Events:
    - MeetingCreated: 회의 생성 이벤트
    - MeetingStarted: 회의 시작 이벤트
    - MeetingEnded: 회의 종료 이벤트
    - MeetingCancelled: 회의 취소 이벤트

  Transcript Events:
    - TranscriptCreated: 회의록 생성 완료 이벤트
    - TranscriptVerified: 회의록 검증 완료 이벤트
    - TranscriptShared: 회의록 공유 이벤트

  Todo Events:
    - TodoCreated: Todo 생성 이벤트
    - TodoStatusChanged: Todo 상태 변경 이벤트
    - TodoCompleted: Todo 완료 이벤트
```

**구독자 매트릭스**
```yaml
이벤트 구독:
  MeetingCreated:
    - Notification Service: 참여자에게 알림 발송
    - AI Service: 회의 일정 분석 준비

  MeetingEnded:
    - Transcript Service: 회의록 생성 시작
    - Notification Service: 회의 종료 알림
    - Todo Service: 회의 기반 Todo 추출

  TranscriptCreated:
    - Notification Service: 회의록 생성 완료 알림
    - Meeting Service: 회의 상태 업데이트
    - AI Service: AI 분석 시작

  TodoCreated:
    - Notification Service: 담당자에게 알림

  TodoCompleted:
    - Notification Service: 완료 알림
    - Meeting Service: 회의 진행률 업데이트
```

**적용 시나리오**
```
회의 종료 후 플로우:
1. Meeting Service: MeetingEnded 이벤트 발행
2. Transcript Service: 이벤트 구독 → 회의록 생성 시작
3. Notification Service: 이벤트 구독 → 참여자에게 종료 알림
4. Todo Service: 이벤트 구독 → 회의 내용에서 Todo 추출
5. AI Service: 이벤트 구독 → AI 분석 준비

회의록 생성 완료 플로우:
1. Transcript Service: TranscriptCreated 이벤트 발행
2. Notification Service: 이벤트 구독 → 회의록 생성 완료 알림
3. Meeting Service: 이벤트 구독 → 회의 상태 '회의록 생성 완료'로 업데이트
4. AI Service: 이벤트 구독 → 회의록 AI 분석 시작
```

#### 3.4.3 구현 예시

**RabbitMQ Exchange/Queue 설정**
```java
@Configuration
public class RabbitMQConfig {

    // Topic Exchange 정의
    @Bean
    public TopicExchange meetingExchange() {
        return new TopicExchange("meeting.events");
    }

    @Bean
    public TopicExchange transcriptExchange() {
        return new TopicExchange("transcript.events");
    }

    @Bean
    public TopicExchange todoExchange() {
        return new TopicExchange("todo.events");
    }

    // Notification Service용 Queue
    @Bean
    public Queue notificationMeetingQueue() {
        return new Queue("notification.meeting.queue", true);
    }

    @Bean
    public Queue notificationTranscriptQueue() {
        return new Queue("notification.transcript.queue", true);
    }

    // Transcript Service용 Queue
    @Bean
    public Queue transcriptMeetingQueue() {
        return new Queue("transcript.meeting.queue", true);
    }

    // Todo Service용 Queue
    @Bean
    public Queue todoMeetingQueue() {
        return new Queue("todo.meeting.queue", true);
    }

    // AI Service용 Queue
    @Bean
    public Queue aiTranscriptQueue() {
        return new Queue("ai.transcript.queue", true);
    }

    // Binding 설정
    @Bean
    public Binding bindingNotificationMeeting() {
        return BindingBuilder
            .bind(notificationMeetingQueue())
            .to(meetingExchange())
            .with("meeting.*");  // meeting.created, meeting.ended 등 모든 이벤트
    }

    @Bean
    public Binding bindingTranscriptMeeting() {
        return BindingBuilder
            .bind(transcriptMeetingQueue())
            .to(meetingExchange())
            .with("meeting.ended");  // 회의 종료 이벤트만 구독
    }

    @Bean
    public Binding bindingTodoMeeting() {
        return BindingBuilder
            .bind(todoMeetingQueue())
            .to(meetingExchange())
            .with("meeting.ended");  // 회의 종료 이벤트만 구독
    }

    @Bean
    public Binding bindingAiTranscript() {
        return BindingBuilder
            .bind(aiTranscriptQueue())
            .to(transcriptExchange())
            .with("transcript.created");  // 회의록 생성 이벤트만 구독
    }
}
```

**Publisher 예시 (Meeting Service)**
```java
@Service
@RequiredArgsConstructor
public class MeetingEventPublisher {

    private final RabbitTemplate rabbitTemplate;

    public void publishMeetingEnded(MeetingEndedEvent event) {
        rabbitTemplate.convertAndSend(
            "meeting.events",
            "meeting.ended",
            event,
            message -> {
                message.getMessageProperties().setContentType("application/json");
                message.getMessageProperties().setTimestamp(new Date());
                return message;
            }
        );
        log.info("Published MeetingEnded event: meetingId={}", event.getMeetingId());
    }

    public void publishMeetingCreated(MeetingCreatedEvent event) {
        rabbitTemplate.convertAndSend(
            "meeting.events",
            "meeting.created",
            event
        );
        log.info("Published MeetingCreated event: meetingId={}", event.getMeetingId());
    }
}

// 이벤트 모델
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class MeetingEndedEvent {
    private Long meetingId;
    private String meetingTitle;
    private LocalDateTime startTime;
    private LocalDateTime endTime;
    private List<Long> participantIds;
    private String audioFileUrl;
    private LocalDateTime occurredAt;
}
```

**Subscriber 예시 (Transcript Service)**
```java
@Service
@RequiredArgsConstructor
public class TranscriptEventSubscriber {

    private final TranscriptService transcriptService;

    @RabbitListener(queues = "transcript.meeting.queue")
    public void handleMeetingEnded(MeetingEndedEvent event) {
        try {
            log.info("Received MeetingEnded event: meetingId={}", event.getMeetingId());

            // 회의록 생성 시작
            transcriptService.createTranscript(
                event.getMeetingId(),
                event.getAudioFileUrl(),
                event.getParticipantIds()
            );

            log.info("Transcript creation started: meetingId={}", event.getMeetingId());
        } catch (Exception e) {
            log.error("Failed to handle MeetingEnded event: meetingId={}",
                event.getMeetingId(), e);
            throw new AmqpRejectAndDontRequeueException("Failed to process event", e);
        }
    }
}
```

**Subscriber 예시 (Notification Service)**
```java
@Service
@RequiredArgsConstructor
public class NotificationEventSubscriber {

    private final NotificationService notificationService;

    @RabbitListener(queues = "notification.meeting.queue")
    public void handleMeetingEvents(Message message) {
        String routingKey = message.getMessageProperties().getReceivedRoutingKey();

        switch (routingKey) {
            case "meeting.created":
                MeetingCreatedEvent createdEvent =
                    (MeetingCreatedEvent) message.getBody();
                handleMeetingCreated(createdEvent);
                break;

            case "meeting.ended":
                MeetingEndedEvent endedEvent =
                    (MeetingEndedEvent) message.getBody();
                handleMeetingEnded(endedEvent);
                break;

            default:
                log.warn("Unknown routing key: {}", routingKey);
        }
    }

    private void handleMeetingCreated(MeetingCreatedEvent event) {
        log.info("Sending meeting creation notification: meetingId={}",
            event.getMeetingId());
        notificationService.notifyMeetingCreated(
            event.getParticipantIds(),
            event.getMeetingTitle(),
            event.getScheduledTime()
        );
    }

    private void handleMeetingEnded(MeetingEndedEvent event) {
        log.info("Sending meeting end notification: meetingId={}",
            event.getMeetingId());
        notificationService.notifyMeetingEnded(
            event.getParticipantIds(),
            event.getMeetingTitle()
        );
    }
}
```

#### 3.4.4 주의사항
- 이벤트 순서 보장이 필요한 경우 Kafka Partition Key 활용
- Idempotent Consumer 패턴으로 중복 처리 방지
- 이벤트 스키마 버전 관리 및 하위 호환성 유지
- 장애 시 이벤트 유실 방지를 위한 Persistent 설정

---

### 3.5 Asynchronous Request-Reply

#### 3.5.1 패턴 개요
**문제**: 장시간 실행되는 작업을 동기 방식으로 처리하면 클라이언트 대기 시간이 길어지고 연결이 끊길 수 있음

**해결**: 요청을 비동기로 처리하고, 클라이언트가 상태를 폴링하거나 콜백으로 결과를 받음

#### 3.5.2 적용 방안

**구현 기술**
- RabbitMQ (Reply-To Queue)
- Redis (상태 저장)
- WebSocket (실시간 알림 - 옵션)

**적용 대상 작업**
```yaml
장시간 작업:
  AI Service:
    - AI 일정 생성 (예상 시간: 30초 ~ 2분)
    - AI 회의록 요약 생성 (예상 시간: 1분 ~ 5분)
    - 대량 회의 분석 (예상 시간: 5분 ~ 30분)

  Transcript Service:
    - 음성 파일 STT 변환 (예상 시간: 1분 ~ 10분)
    - 대용량 회의록 검증 (예상 시간: 30초 ~ 3분)
```

**처리 플로우**
```yaml
비동기 요청-응답 플로우:
  1. 요청 단계:
     - 클라이언트 → API Gateway → AI Service: AI 일정 생성 요청
     - AI Service: 작업 ID 생성 및 Redis에 상태 저장 (status: PENDING)
     - AI Service → 클라이언트: 작업 ID 즉시 반환 (202 Accepted)

  2. 처리 단계:
     - AI Service: Queue에 작업 메시지 발행
     - AI Worker: Queue에서 메시지 소비 및 처리 시작
     - AI Worker: Redis 상태 업데이트 (status: PROCESSING)
     - AI Worker: AI 처리 완료
     - AI Worker: Redis 상태 업데이트 (status: COMPLETED, result: {...})

  3. 결과 조회:
     방법 1 - 폴링:
       - 클라이언트 → API Gateway → AI Service: GET /api/ai/tasks/{taskId}
       - AI Service: Redis에서 상태 조회 및 반환

     방법 2 - WebSocket (옵션):
       - AI Worker: 완료 시 WebSocket으로 클라이언트에게 Push
```

**적용 시나리오**
```
AI 일정 생성 요청:
1. Frontend → AI Service: POST /api/ai/schedules
   Body: { meetingId: 123, transcriptId: 456 }

2. AI Service:
   - taskId 생성: "ai-schedule-uuid-12345"
   - Redis 저장: { taskId, status: "PENDING", createdAt: "2025-01-20T10:00:00Z" }
   - Queue 발행: { taskId, meetingId, transcriptId }

3. AI Service → Frontend:
   Response: 202 Accepted
   Body: {
     taskId: "ai-schedule-uuid-12345",
     status: "PENDING",
     statusUrl: "/api/ai/tasks/ai-schedule-uuid-12345"
   }

4. AI Worker:
   - Queue에서 메시지 수신
   - Redis 업데이트: { taskId, status: "PROCESSING", startedAt: "2025-01-20T10:00:05Z" }
   - AI 일정 생성 처리 (1분 소요)
   - Redis 업데이트: {
       taskId,
       status: "COMPLETED",
       completedAt: "2025-01-20T10:01:05Z",
       result: { scheduleId: 789, schedules: [...] }
     }

5. Frontend (폴링):
   - 5초마다 GET /api/ai/tasks/ai-schedule-uuid-12345 호출
   - status가 "COMPLETED"일 때 result 획득
```

#### 3.5.3 구현 예시

**작업 상태 모델**
```java
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class AsyncTaskStatus {
    private String taskId;
    private TaskStatus status;  // PENDING, PROCESSING, COMPLETED, FAILED
    private LocalDateTime createdAt;
    private LocalDateTime startedAt;
    private LocalDateTime completedAt;
    private Object result;  // 완료 시 결과
    private String errorMessage;  // 실패 시 오류 메시지

    public enum TaskStatus {
        PENDING,
        PROCESSING,
        COMPLETED,
        FAILED
    }
}
```

**AI Service - 요청 접수 및 작업 ID 반환**
```java
@RestController
@RequestMapping("/api/ai")
@RequiredArgsConstructor
public class AiScheduleController {

    private final AiScheduleService aiScheduleService;

    @PostMapping("/schedules")
    public ResponseEntity<AsyncTaskResponse> createSchedule(
            @RequestBody @Valid CreateScheduleRequest request) {

        String taskId = aiScheduleService.requestScheduleCreation(request);

        AsyncTaskResponse response = AsyncTaskResponse.builder()
            .taskId(taskId)
            .status(TaskStatus.PENDING)
            .statusUrl("/api/ai/tasks/" + taskId)
            .build();

        return ResponseEntity.accepted()
            .location(URI.create("/api/ai/tasks/" + taskId))
            .body(response);
    }

    @GetMapping("/tasks/{taskId}")
    public ResponseEntity<AsyncTaskStatus> getTaskStatus(@PathVariable String taskId) {
        AsyncTaskStatus status = aiScheduleService.getTaskStatus(taskId);

        if (status == null) {
            return ResponseEntity.notFound().build();
        }

        if (status.getStatus() == TaskStatus.COMPLETED) {
            return ResponseEntity.ok(status);
        } else if (status.getStatus() == TaskStatus.FAILED) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(status);
        } else {
            // PENDING or PROCESSING
            return ResponseEntity.status(HttpStatus.ACCEPTED).body(status);
        }
    }
}
```

**AI Service - 작업 요청 처리**
```java
@Service
@RequiredArgsConstructor
public class AiScheduleService {

    private final RedisTemplate<String, AsyncTaskStatus> redisTemplate;
    private final RabbitTemplate rabbitTemplate;

    public String requestScheduleCreation(CreateScheduleRequest request) {
        // 작업 ID 생성
        String taskId = "ai-schedule-" + UUID.randomUUID().toString();

        // Redis에 초기 상태 저장
        AsyncTaskStatus taskStatus = new AsyncTaskStatus(
            taskId,
            TaskStatus.PENDING,
            LocalDateTime.now(),
            null,
            null,
            null,
            null
        );

        redisTemplate.opsForValue().set(
            "task:" + taskId,
            taskStatus,
            Duration.ofHours(24)  // TTL: 24시간
        );

        // Queue에 작업 메시지 발행
        AiScheduleMessage message = new AiScheduleMessage(
            taskId,
            request.getMeetingId(),
            request.getTranscriptId()
        );

        rabbitTemplate.convertAndSend("ai.processing.queue", message);

        log.info("AI schedule creation requested: taskId={}, meetingId={}",
            taskId, request.getMeetingId());

        return taskId;
    }

    public AsyncTaskStatus getTaskStatus(String taskId) {
        return redisTemplate.opsForValue().get("task:" + taskId);
    }
}
```

**AI Worker - 비동기 작업 처리**
```java
@Service
@RequiredArgsConstructor
public class AiScheduleWorker {

    private final RedisTemplate<String, AsyncTaskStatus> redisTemplate;
    private final AiScheduleGenerator aiScheduleGenerator;

    @RabbitListener(queues = "ai.processing.queue", concurrency = "2-5")
    public void processScheduleCreation(AiScheduleMessage message) {
        String taskId = message.getTaskId();

        try {
            // 상태 업데이트: PROCESSING
            updateTaskStatus(taskId, TaskStatus.PROCESSING, null, null);

            log.info("AI schedule processing started: taskId={}, meetingId={}",
                taskId, message.getMeetingId());

            // AI 일정 생성 처리 (장시간 소요)
            AiScheduleResult result = aiScheduleGenerator.generateSchedule(
                message.getMeetingId(),
                message.getTranscriptId()
            );

            log.info("AI schedule processing completed: taskId={}", taskId);

            // 상태 업데이트: COMPLETED
            updateTaskStatus(taskId, TaskStatus.COMPLETED, result, null);

        } catch (Exception e) {
            log.error("AI schedule processing failed: taskId={}", taskId, e);

            // 상태 업데이트: FAILED
            updateTaskStatus(taskId, TaskStatus.FAILED, null, e.getMessage());

            throw new AmqpRejectAndDontRequeueException("Failed to process AI schedule", e);
        }
    }

    private void updateTaskStatus(String taskId, TaskStatus status,
                                   Object result, String errorMessage) {
        AsyncTaskStatus currentStatus = redisTemplate.opsForValue().get("task:" + taskId);

        if (currentStatus != null) {
            AsyncTaskStatus updatedStatus = new AsyncTaskStatus(
                taskId,
                status,
                currentStatus.getCreatedAt(),
                status == TaskStatus.PROCESSING ? LocalDateTime.now() : currentStatus.getStartedAt(),
                status == TaskStatus.COMPLETED || status == TaskStatus.FAILED ?
                    LocalDateTime.now() : null,
                result,
                errorMessage
            );

            redisTemplate.opsForValue().set(
                "task:" + taskId,
                updatedStatus,
                Duration.ofHours(24)
            );
        }
    }
}
```

**프론트엔드 - 폴링 방식 구현**
```javascript
// AI 일정 생성 요청
async function requestAiSchedule(meetingId, transcriptId) {
  const response = await fetch('/api/ai/schedules', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ meetingId, transcriptId })
  });

  if (response.status === 202) {
    const { taskId, statusUrl } = await response.json();

    // 폴링 시작
    pollTaskStatus(statusUrl);
  }
}

// 작업 상태 폴링
async function pollTaskStatus(statusUrl) {
  const maxAttempts = 60;  // 최대 5분 (5초 * 60)
  let attempts = 0;

  const intervalId = setInterval(async () => {
    attempts++;

    try {
      const response = await fetch(statusUrl);
      const taskStatus = await response.json();

      if (taskStatus.status === 'COMPLETED') {
        clearInterval(intervalId);
        console.log('AI schedule completed:', taskStatus.result);
        displayScheduleResult(taskStatus.result);
      } else if (taskStatus.status === 'FAILED') {
        clearInterval(intervalId);
        console.error('AI schedule failed:', taskStatus.errorMessage);
        displayError(taskStatus.errorMessage);
      } else if (attempts >= maxAttempts) {
        clearInterval(intervalId);
        console.warn('Polling timeout');
        displayTimeout();
      } else {
        console.log('AI schedule processing... status:', taskStatus.status);
        updateProgress(taskStatus.status);
      }
    } catch (error) {
      console.error('Polling error:', error);
    }
  }, 5000);  // 5초마다 폴링
}
```

#### 3.5.4 주의사항
- 작업 ID는 충돌하지 않도록 UUID 사용
- Redis TTL 설정으로 완료된 작업 상태 자동 삭제
- 폴링 간격과 최대 시도 횟수 적절히 설정 (네트워크 부하 고려)
- 장기 실행 작업의 경우 WebSocket 또는 Server-Sent Events (SSE) 고려

---

### 3.6 Health Endpoint Monitoring

#### 3.6.1 패턴 개요
**문제**: 서비스의 상태를 외부에서 확인할 방법이 없어 장애 발생 시 빠른 대응이 어려움

**해결**: 각 서비스가 Health Check 엔드포인트를 제공하여 상태를 모니터링하고, 장애 시 자동 복구

#### 3.6.2 적용 방안

**구현 기술**
- Spring Boot Actuator
- Kubernetes Liveness/Readiness Probes
- Prometheus + Grafana (메트릭 수집 및 대시보드)

**Health Check 레벨**
```yaml
Health Check 계층:
  1. Liveness Probe (생존 확인):
     - 목적: 서비스가 살아있는지 확인
     - 실패 시: 컨테이너 재시작
     - 엔드포인트: /actuator/health/liveness
     - 체크 항목: 애플리케이션 기본 동작 여부

  2. Readiness Probe (준비 상태 확인):
     - 목적: 서비스가 트래픽을 받을 준비가 되었는지 확인
     - 실패 시: 트래픽 라우팅 중단 (재시작 X)
     - 엔드포인트: /actuator/health/readiness
     - 체크 항목:
       - 데이터베이스 연결
       - Redis 연결
       - RabbitMQ 연결
       - 외부 API 연결

  3. Custom Health Indicator:
     - 서비스별 비즈니스 로직 상태 확인
     - 예: AI Service → AI 모델 로딩 상태
     - 예: Transcript Service → STT 엔진 연결 상태
```

**서비스별 Health Check 구성**
```yaml
모든 서비스 공통:
  Liveness:
    - /actuator/health/liveness
    - initialDelaySeconds: 30
    - periodSeconds: 10
    - timeoutSeconds: 5
    - failureThreshold: 3

  Readiness:
    - /actuator/health/readiness
    - initialDelaySeconds: 10
    - periodSeconds: 5
    - timeoutSeconds: 3
    - failureThreshold: 3

User Service:
  Dependencies:
    - PostgreSQL (user_db)
    - Redis (cache)

Meeting Service:
  Dependencies:
    - PostgreSQL (meeting_db)
    - Redis (cache)
    - RabbitMQ

Transcript Service:
  Dependencies:
    - PostgreSQL (transcript_db)
    - Redis (cache)
    - RabbitMQ
    - STT Engine (외부 API)

AI Service:
  Dependencies:
    - PostgreSQL (ai_db)
    - Redis (cache)
    - RabbitMQ
    - AI Model Server (외부 API)

Notification Service:
  Dependencies:
    - PostgreSQL (notification_db)
    - RabbitMQ
    - Email Service (SMTP)
    - SMS Service (외부 API)

Todo Service:
  Dependencies:
    - PostgreSQL (todo_db)
    - Redis (cache)
    - RabbitMQ
```

#### 3.6.3 구현 예시

**Spring Boot Actuator 설정 (application.yml)**
```yaml
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
      base-path: /actuator

  endpoint:
    health:
      enabled: true
      show-details: always  # 개발: always, 운영: when-authorized
      probes:
        enabled: true  # Liveness/Readiness 활성화

  health:
    livenessState:
      enabled: true
    readinessState:
      enabled: true
    db:
      enabled: true
    redis:
      enabled: true
    rabbit:
      enabled: true

  metrics:
    export:
      prometheus:
        enabled: true
```

**Custom Health Indicator (Meeting Service)**
```java
@Component
public class MeetingServiceHealthIndicator implements HealthIndicator {

    private final MeetingRepository meetingRepository;
    private final RedisTemplate<String, Object> redisTemplate;
    private final RabbitTemplate rabbitTemplate;

    @Override
    public Health health() {
        try {
            // 데이터베이스 연결 확인
            meetingRepository.count();

            // Redis 연결 확인
            redisTemplate.opsForValue().get("health-check");

            // RabbitMQ 연결 확인
            rabbitTemplate.getConnectionFactory().createConnection().isOpen();

            return Health.up()
                .withDetail("database", "Connected")
                .withDetail("redis", "Connected")
                .withDetail("rabbitmq", "Connected")
                .build();

        } catch (Exception e) {
            return Health.down()
                .withDetail("error", e.getMessage())
                .build();
        }
    }
}
```

**Custom Health Indicator (AI Service)**
```java
@Component
public class AiServiceHealthIndicator implements HealthIndicator {

    private final AiModelClient aiModelClient;
    private final AiRepository aiRepository;
    private final RedisTemplate<String, Object> redisTemplate;

    @Override
    public Health health() {
        Health.Builder builder = new Health.Builder();

        try {
            // 데이터베이스 연결 확인
            aiRepository.count();
            builder.withDetail("database", "Connected");

            // Redis 연결 확인
            redisTemplate.opsForValue().get("health-check");
            builder.withDetail("redis", "Connected");

            // AI 모델 서버 연결 확인
            boolean aiModelReady = aiModelClient.checkHealth();
            if (aiModelReady) {
                builder.withDetail("ai-model", "Ready");
            } else {
                builder.withDetail("ai-model", "Not Ready");
                return builder.down().build();
            }

            return builder.up().build();

        } catch (Exception e) {
            return builder.down()
                .withDetail("error", e.getMessage())
                .build();
        }
    }
}
```

**Kubernetes Deployment with Probes**
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: meeting-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: meeting-service
  template:
    metadata:
      labels:
        app: meeting-service
    spec:
      containers:
      - name: meeting-service
        image: meeting-service:1.0.0
        ports:
        - containerPort: 8080

        # Liveness Probe: 컨테이너가 살아있는지 확인
        livenessProbe:
          httpGet:
            path: /actuator/health/liveness
            port: 8080
          initialDelaySeconds: 30  # 초기 대기 시간
          periodSeconds: 10        # 체크 간격
          timeoutSeconds: 5        # 응답 대기 시간
          failureThreshold: 3      # 실패 허용 횟수

        # Readiness Probe: 트래픽을 받을 준비가 되었는지 확인
        readinessProbe:
          httpGet:
            path: /actuator/health/readiness
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 5
          timeoutSeconds: 3
          failureThreshold: 3

        resources:
          requests:
            memory: "512Mi"
            cpu: "250m"
          limits:
            memory: "1Gi"
            cpu: "500m"

        env:
        - name: SPRING_PROFILES_ACTIVE
          value: "prod"
        - name: SPRING_DATASOURCE_URL
          valueFrom:
            secretKeyRef:
              name: meeting-db-secret
              key: url
```

**Health Check 응답 예시**
```json
// /actuator/health
{
  "status": "UP",
  "components": {
    "db": {
      "status": "UP",
      "details": {
        "database": "PostgreSQL",
        "validationQuery": "isValid()"
      }
    },
    "redis": {
      "status": "UP",
      "details": {
        "version": "7.0.5"
      }
    },
    "rabbit": {
      "status": "UP",
      "details": {
        "version": "3.11.0"
      }
    },
    "meetingService": {
      "status": "UP",
      "details": {
        "database": "Connected",
        "redis": "Connected",
        "rabbitmq": "Connected"
      }
    },
    "diskSpace": {
      "status": "UP",
      "details": {
        "total": 107374182400,
        "free": 53687091200,
        "threshold": 10485760,
        "exists": true
      }
    }
  }
}

// /actuator/health/liveness
{
  "status": "UP"
}

// /actuator/health/readiness
{
  "status": "UP",
  "components": {
    "db": { "status": "UP" },
    "redis": { "status": "UP" },
    "rabbit": { "status": "UP" }
  }
}
```

**Prometheus 메트릭 수집**
```yaml
# prometheus.yml
scrape_configs:
  - job_name: 'meeting-service'
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['meeting-service:8080']

  - job_name: 'transcript-service'
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['transcript-service:8080']

  - job_name: 'ai-service'
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['ai-service:8080']
```

#### 3.6.4 주의사항
- Health Check 엔드포인트는 가벼워야 함 (복잡한 비즈니스 로직 포함 금지)
- Liveness와 Readiness를 명확히 구분 (잘못 설정 시 무한 재시작 발생 가능)
- initialDelaySeconds는 애플리케이션 시작 시간보다 길게 설정
- failureThreshold를 너무 낮게 설정하면 일시적 네트워크 장애에도 재시작 발생

---

## 4. 패턴 간 통합 시나리오

### 4.1 회의 종료 후 전체 플로우

```
사용자 → Frontend → API Gateway → Meeting Service
                                    ↓
                              1. 회의 종료 처리
                              2. MeetingEnded 이벤트 발행 (Pub-Sub)
                              3. Redis 캐시 무효화 (Cache-Aside)
                                    ↓
                    ┌───────────────┼───────────────┐
                    ↓               ↓               ↓
          Transcript Service  Notification    Todo Service
                 ↓             Service              ↓
          Queue 발행          알림 발송        Queue 발행
       (Queue-Based)                        (Queue-Based)
                 ↓                                  ↓
          STT Worker                         Todo Worker
       비동기 처리                           Todo 추출
    (Async Request-Reply)
                 ↓
       TranscriptCreated 이벤트
              (Pub-Sub)
                 ↓
       ┌────────┼────────┐
       ↓        ↓        ↓
   Meeting   Notification  AI Service
   Service   Service          ↓
      ↓         ↓        Queue 발행
   캐시      완료 알림   (Queue-Based)
   무효화                    ↓
                        AI Worker
                     (Async Request-Reply)
```

**단계별 패턴 적용**

1. **API Gateway**: 사용자 요청 라우팅 및 인증
2. **Cache-Aside**: Meeting Service에서 회의 정보 캐시 무효화
3. **Pub-Sub**: MeetingEnded 이벤트 발행
4. **Queue-Based Load Leveling**: Transcript Service, Todo Service가 Queue를 통해 작업 수신
5. **Asynchronous Request-Reply**: STT 처리 및 AI 분석은 비동기로 처리, 상태 폴링
6. **Health Endpoint Monitoring**: 모든 서비스 상태 모니터링

### 4.2 회의록 조회 플로우

```
사용자 → Frontend → API Gateway → Transcript Service
                                         ↓
                                   1. Redis 캐시 확인 (Cache-Aside)
                                   2. Cache Hit → 캐시 반환
                                   3. Cache Miss → DB 조회 → 캐시 저장
                                         ↓
                                    응답 반환
```

**패턴 적용**
- **API Gateway**: 요청 라우팅 및 인증
- **Cache-Aside**: 회의록 데이터 캐싱으로 성능 최적화

---

## 5. 패턴 적용 로드맵

### 5.1 1단계: 기본 인프라 (Week 1-2)

**목표**: 핵심 인프라 구축 및 기본 패턴 적용

**작업 항목**
1. API Gateway 설정
   - Kong 또는 Spring Cloud Gateway 선택 및 설치
   - 라우팅 규칙 정의
   - JWT 인증 설정

2. Health Endpoint Monitoring 구현
   - Spring Boot Actuator 활성화
   - Liveness/Readiness Probes 설정
   - Kubernetes Deployment 설정

3. 메시지 브로커 설치
   - RabbitMQ 설치 및 설정
   - Exchange/Queue 정의

4. Redis 설치
   - Redis 클러스터 구성
   - 캐시 설정

**완료 기준**
- 모든 서비스가 API Gateway를 통해 접근 가능
- Health Check 엔드포인트가 정상 동작
- RabbitMQ와 Redis 연결 확인

### 5.2 2단계: 성능 최적화 (Week 3-4)

**목표**: 캐싱 및 부하 분산 패턴 적용

**작업 항목**
1. Cache-Aside 패턴 구현
   - User Service: 사용자 프로필 캐싱
   - Meeting Service: 회의 정보 캐싱
   - Todo Service: Todo 목록 캐싱

2. Queue-Based Load Leveling 구현
   - Meeting Service → Transcript Service (회의록 생성)
   - Meeting Service → AI Service (AI 처리)
   - Notification Service (알림 발송)

**완료 기준**
- 캐시 Hit Rate 70% 이상
- Queue를 통한 비동기 처리 정상 동작
- 부하 테스트 결과 성능 개선 확인

### 5.3 3단계: 이벤트 기반 아키텍처 (Week 5-6)

**목표**: 서비스 간 느슨한 결합 및 확장성 향상

**작업 항목**
1. Pub-Sub 패턴 구현
   - MeetingEnded, TranscriptCreated, TodoCreated 이벤트
   - 이벤트 구독자 구현 (Notification, AI, Todo Service)

2. Asynchronous Request-Reply 패턴 구현
   - AI Service: AI 일정 생성 비동기 처리
   - Transcript Service: STT 처리 비동기 처리
   - Redis 기반 상태 관리

**완료 기준**
- 이벤트 발행/구독 정상 동작
- 비동기 작업 상태 조회 가능
- 서비스 간 직접 의존성 제거 확인

### 5.4 4단계: 모니터링 및 안정화 (Week 7-8)

**목표**: 운영 안정성 확보

**작업 항목**
1. Prometheus + Grafana 구축
   - 메트릭 수집 설정
   - 대시보드 구성

2. 알림 설정
   - Health Check 실패 알림
   - Queue 적체 알림
   - 성능 저하 알림

3. 부하 테스트 및 튜닝
   - Queue Consumer Concurrency 조정
   - 캐시 TTL 최적화
   - API Gateway Rate Limiting 조정

**완료 기준**
- Grafana 대시보드에서 모든 서비스 상태 확인 가능
- 알림 시스템 정상 동작
- 목표 성능 달성 (응답 시간, 처리량)

---

## 6. 모니터링 및 운영

### 6.1 주요 모니터링 지표

**API Gateway**
- 요청 수 (RPS)
- 응답 시간 (P50, P95, P99)
- 에러율 (4xx, 5xx)
- Rate Limit 초과 횟수

**Queue-Based Load Leveling**
- Queue 길이
- 메시지 처리 속도
- Dead Letter Queue 메시지 수
- Consumer Lag

**Cache-Aside**
- Cache Hit Rate
- Cache Miss Rate
- 캐시 메모리 사용량
- 캐시 Eviction 수

**Pub-Sub**
- 이벤트 발행 수
- 이벤트 구독 지연 시간
- 이벤트 처리 실패 수

**Asynchronous Request-Reply**
- 비동기 작업 대기 시간
- 작업 완료율
- 작업 실패율

**Health Endpoint Monitoring**
- 서비스 가용성 (Uptime)
- Health Check 응답 시간
- Liveness/Readiness 실패 횟수

### 6.2 알림 규칙

| 지표 | 임계값 | 조치 |
|------|--------|------|
| API Gateway 에러율 | > 5% | 즉시 알림, 로그 분석 |
| Queue 길이 | > 1000 | Consumer 증설 검토 |
| Cache Hit Rate | < 50% | 캐시 전략 재검토 |
| Health Check 실패 | 3회 연속 | 서비스 재시작, 근본 원인 분석 |
| 비동기 작업 실패율 | > 10% | Worker 상태 점검, Queue 확인 |

---

## 7. 장애 대응

### 7.1 API Gateway 장애

**증상**: 모든 서비스 접근 불가

**대응**
1. API Gateway Pod 재시작
2. HA 구성 확인 (최소 2개 이상 인스턴스)
3. 부하 분산 설정 점검

### 7.2 Queue 적체

**증상**: 메시지 처리 지연, Queue 길이 증가

**대응**
1. Consumer 수 증가 (Concurrency 조정)
2. Worker Pod 수평 확장
3. 메시지 우선순위 재조정

### 7.3 Cache 장애

**증상**: 응답 시간 증가, DB 부하 증가

**대응**
1. Redis 연결 확인
2. Cache Fallback 동작 확인 (DB 직접 조회)
3. Redis 재시작 또는 Failover

### 7.4 이벤트 유실

**증상**: 특정 이벤트가 구독자에게 전달되지 않음

**대응**
1. RabbitMQ 연결 상태 확인
2. Queue Binding 설정 점검
3. Dead Letter Queue 확인
4. 필요 시 수동 재발행

---

## 8. 성공 지표

### 8.1 성능 목표

| 지표 | 목표 |
|------|------|
| API 응답 시간 (P95) | < 500ms |
| 회의록 생성 시간 | < 2분 |
| AI 일정 생성 시간 | < 1분 |
| Cache Hit Rate | > 70% |
| 시스템 가용성 | > 99.5% |

### 8.2 확장성 목표

| 항목 | 목표 |
|------|------|
| 동시 사용자 수 | 10,000명 |
| 동시 진행 회의 수 | 1,000개 |
| 일일 처리 회의 수 | 100,000개 |

---

## 9. 참고 자료

- [Microsoft Azure Cloud Design Patterns](https://learn.microsoft.com/en-us/azure/architecture/patterns/)
- [Kong Gateway Documentation](https://docs.konghq.com/)
- [Spring Cloud Gateway Reference](https://spring.io/projects/spring-cloud-gateway)
- [RabbitMQ Patterns](https://www.rabbitmq.com/getstarted.html)
- [Redis Best Practices](https://redis.io/docs/management/optimization/)
- [Kubernetes Probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/)

---

## 10. 문서 이력

| 버전 | 작성일 | 작성자 | 변경 내용 |
|------|--------|--------|----------|
| 1.0 | 2025-01-20 | 길동 | 초안 작성 (6개 핵심 패턴 적용 방안) |
