Skip to content

Conversation

@Oh-Myeongjae
Copy link
Collaborator

@Oh-Myeongjae Oh-Myeongjae commented Oct 27, 2025

기본 요구사항

Spring Event - 파일 업로드 로직 분리하기

  • BinaryContentStorage.put을 직접 호출하는 대신 BinaryContentCreatedEvent를 발행
  • BinaryContentCreatedEvent를 정의
  • BinaryContent 메타 정보가 DB에 잘 저장되었다는 사실을 의미하는 이벤트
  • 다음의 메소드에서 BinaryContentStorage를 호출하는 대신 BinaryContentCreatedEvent를 발행
    • UserService.create/update
    • MessageService.create
    • BinaryContentService.create
  • ApplicationEventPublisher를 활용
  • 이벤트를 받아 실제 바이너리 데이터를 저장하는 리스너를 구현
  • 이벤트를 발행한 메인 서비스의 트랜잭션이 커밋되었을 때 리스너가 실행되도록 설정
  • BinaryContentStorage를 통해 바이너리 데이터를 저장
  • 바이너리 데이터 저장 성공 여부를 알 수 있도록 메타 데이터를 리팩토링
  • BinaryContent에 바이너리 데이터 업로드 상태 속성(status)을 추가
  • BinaryContent의 상태를 업데이트하는 메소드 정의
  • 트랜잭션 전파 범위에 유의
  • 바이너리 데이터 저장 성공 여부를 메타 데이터에 반영
    • 성공 시 BinaryContent의 status를 SUCCESS로 업데이트
    • 실패 시 BinaryContent의 status를 FAIL로 업데이트

Spring Event - 알림 기능 추가하기

  • 채널에 새로운 메시지가 등록되거나 권한이 변경된 경우 이벤트를 발행해 알림을 받을 수 있도록 구현
  • 채널에 새로운 메시지가 등록된 경우 알림을 받을 수 있도록 리팩토링
  • MessageCreatedEvent를 정의하고 새로운 메시지가 등록되면 이벤트를 발행
  • 사용자 별로 관심있는 채널의 알림만 받을 수 있도록 ReadStatus 엔티티에 채널 알림 여부 속성(notificationEnabled) 추가
    • PRIVATE 채널은 알림 여부를 true로 초기화
    • PUBLIC 채널은 알림 여부를 false로 초기화
  • 알림 여부를 수정할 수 있게 ReadStatusUpdateRequest 수정
    • 알림이 활성화 되어 있는 경우
    • 알림이 활성화 되어 있지 않은 경우
  • 사용자의 권한(Role)이 변경된 경우 알림을 받을 수 있도록 리팩토링
  • RoleUpdatedEvent를 정의하고 권한이 변경되면 이벤트를 발행
  • 알림 API를 구현
    • NotificationDto를 정의
      • receiverId: 알림을 수신할 User의 id
    • 알림 조회
      • 엔드포인트: GET /api/notifications
      • 요청: 헤더: 엑세스 토큰
      • 응답: 200 List
      • 인증되지 않은 요청: 401 ErrorResponse
    • 알림 확인
      • 엔드포인트: DELETE /api/notifications/{notificationId}
      • 요청: 헤더: 엑세스 토큰
      • 응답: 204 Void
      • 인증되지 않은 요청: 401 ErrorResponse
      • 인가되지 않은 요청: 403 ErrorResponse
      • 요청자 본인의 알림에 대해서만 수행 가능
      • 알림이 없는 경우: 404 ErrorResponse
  • 알림이 필요한 이벤트가 발행되었을 때 알림 생성
  • 이벤트를 처리할 리스너 구현
    • on(MessageCreatedEvent)
      • 해당 채널의 알림 여부를 활성화한 ReadStatus 조회
      • 해당 ReadStatus의 사용자들에게 알림 생성
      • 해당 메시지를 보낸 사람은 알림 대상에서 제외
    • on(RoleUpdatedEvent)
      • 권한이 변경된 당사자에게 알림 생성

비동기 적용하기

  • 비동기를 적용하기 위한 설정(AsyncConfig) 클래스 구현
  • @EnableAsync 어노테이션 활용
  • TaskExecutor를 Bean으로 등록
  • TaskDecorator를 활용해 MDC의 Request ID, SecurityContext의 인증 정보가 비동기 스레드에서도 유지되도록 구현
  • 앞서 구현한 Event Listener를 비동기적으로 처리
  • @async 어노테이션 활용
  • 동기 처리와 비동기 처리 간 성능 차이를 비교
    • 파일 업로드 로직에 의도적인 지연(Thread.sleep) 발생
    • 메시지 생성 API의 실행 시간을 측정
    • @timed 어노테이션을 메소드에 추가
    • Actuator 설정 추가
      • /actuator/metrics/message.create.async 에서 측정된 시간 확인
      • @EnableAsync를 활성화 / 비활성화해 동기 / 비동기 처리 간 응답 속도 차이 확인

비동기 실패 처리하기

  • S3를 활용해 바이너리 데이터 저장 시 자동 재시도 매커니즘 구축
  • Spring Retry 환경 구성
    • org.springframework.retry:spring-retry 의존성 추가
    • @EnableRetry 어노테이션 활용해 Spring Retry 활성화
  • 바이너리 데이터를 저장하는 메소드에 @retryable 어노테이션 사용, 재시도 정책(횟수, 대기 시간 등) 설정
  • 재시도가 모두 실패했을 때 대응 전략 구축
    • @recover 어노테이션 활용
    • 실패 정보를 관리자에게 통지
      • 실패한 작업 이름
      • MDC의 Request ID
      • 실패 이유(예외 메시지)

캐시 적용하기

  • Caffeine 캐시 환경 구성
    • org.springframework.boot:spring-boot-starter-cache 의존성 추가
    • com.github.ben-manes.caffeine:caffeine 의존성 추가
    • application.yaml 설정 또는 Bean 통해 Caffeine 캐시 설정
  • @Cacheable 어노테이션 활용해 캐시가 필요한 메소드에 적용
    • 사용자별 채널 목록 조회
    • 사용자별 알림 목록 조회
    • 사용자 목록 조회
  • 데이터 변경 시 캐시 갱신 또는 무효화
    • @CacheEvict, @cACHEpUT, CacheManager 활용
    • 예시
      • 새로운 채널 추가/수정/삭제 → 채널 목록 캐시 무효화
      • 알림 추가/삭제 → 알림 목록 캐시 무효화
      • 사용자 추가/로그인/로그아웃 → 사용자 목록 캐시 무효화
  • 캐시 적용 전후 차이 비교
    • 로그 통해 SQL 실행 여부 확인
    • Spring Actuator 활용해 캐시 관련 통계 지표 확인
    • Caffeine Spec에 recordStats 옵션 추가
    • /actuator/caches, /actuator/metrics/cache.* 통해 캐시 관련 데이터 확인

심화 요구사항

Spring Kafka 도입하기

  • Kafka 환경 구성
    • Docker Compose 활용해 Kafka 구동
    • Spring Kafka 의존성 추가, application.yml에 Kafka 설정 추가
  • Spring Event를 Kafka로 발행하는 리스너 구현
    • NotificationRequiredEventListener 비활성화
    • KafkaProduceRequiredEventListener 구현
    • Spring Event를 Kafka 메시지로 변환해 전송하는 중계 구조 구현
  • Kafka Console 통해 Kafka 이벤트 발행 확인
  • Kafka 토픽 구독해 알림 생성 리스너 구현

Redis Cache 도입하기

  • Redis 환경 구성
    • Docker Compose 활용해 Redis 구동
    • Redis 의존성 추가, application.yml에 Redis 설정 추가
    • 직렬화 설정을 위해 Bean 선언
  • DataGrip 통해 Redis에 저장된 캐시 정보 조회

@Oh-Myeongjae Oh-Myeongjae changed the title sprint11 과제제출 [오명재] Sprint11 Oct 27, 2025
Comment on lines +32 to +53
@Async("customTaskExecutor")
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onBinaryContentCreated(BinaryContentCreatedEvent event) {

log.debug("[BinaryContentCreatedEventListener] onBinaryContent id: {}, fileName: {}",
event.binaryContent().getId(),
event.binaryContent().getFileName());

UUID id = event.binaryContent().getId();
byte[] data = event.data();

// 바이너리 데이터 저장
try {
storage.put(id, event.data());
binaryContentService.updateStatus(id, BinaryContentStatus.SUCCESS);

log.info("[BinaryContentCreatedEventListener] 바이너리 데이터 저장 성공: {}", id);
} catch (Exception e) {
log.error("[BinaryContentCreatedEventListener] 바이너리 데이터 저장 성공: {}, 실패 이유: {}", id,
e.getMessage(), e);

binaryContentService.updateStatus(id, BinaryContentStatus.FAIL);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이벤트 소비

@RequestMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, method = RequestMethod.POST)
public ResponseEntity<Message> create(
@RequestPart("messageCreateRequest") MessageCreateRequest messageCreateRequest,
@Timed("message.create.async")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

시간 계산

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants