Skip to content

Conversation

@B1uffer
Copy link

@B1uffer B1uffer commented Oct 28, 2025

요구사항

기본

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

  • 디스코드잇은 BinaryContent의 메타 데이터(DB)와 바이너리 데이터(FileSystem/S3)를 분리해 저장합니다.
  • 만약 지금처럼 두 로직이 하나의 트랜잭션으로 묶인 경우 트랜잭션을 과도하게 오래 점유할 수 있는 문제가 있습니다.
    • 바이너리 데이터 저장 연산은 오래 걸릴 수 있는 연산이며, 해당 연산이 끝날 때까지 트랜잭션이 대기해야합니다.
  • 따라서 Spring Event를 활용해 메타 데이터 저장 트랜잭션으로부터 바이너리 데이터 저장 로직을 분리하여, 메타데이터 저장 트랜잭션이 종료되면 바이너리 데이터를 저장하도록 변경합니다.
  • BinaryContentStorage.put을 직접 호출하는 대신 BinaryContentCreatedEvent를 발행하세요.
    • BinaryContentCreatedEvent를 정의하세요.
      • BinaryContent 메타 정보가 DB에 잘 저장되었다는 사실을 의미하는 이벤트입니다.
    • 다음의 메소드에서 BinaryContentStorage를 호출하는 대신 BinaryContentCreatedEvent를 발행하세요.
      • UserService.create/update
      • MessageService.create
      • BinaryContentService.create
    • ApplicationEventPublisher를 활용하세요.
  • 이벤트를 받아 실제 바이너리 데이터를 저장하는 리스너를 구현하세요.
    • 이벤트를 발행한 메인 서비스의 트랜잭션이 커밋되었을 때 리스너가 실행되도록 설정하세요.
    • BinaryContentStorage를 통해 바이너리 데이터를 저장하세요.
  • 바이너리 데이터 저장 성공 여부를 알 수 있도록 메타 데이터를 리팩토링하세요.
    • BinaryContent에 바이너리 데이터 업로드 상태 속성(status)을 추가하세요.
      • PROCESSING: 업로드 중
        • 기본값입니다.
      • SUCCESS: 업로드 완료
      • FAIL: 업로드 실패
        `-- schema.sql
        CREATE TABLE binary_contents
        (
        id uuid PRIMARY KEY,
        created_at timestamp with time zone NOT NULL,
        updated_at timestamp with time zone,
        file_name varchar(255) NOT NULL,
        size bigint NOT NULL,
        content_type varchar(100) NOT NULL,
        status varchar(20) NOT NULL
        );

-- ALTER TABLE binary_contents
-- ADD COLUMN updated_at timestamp with time zone;
-- ALTER TABLE binary_contents
`

  • BinaryContent의 상태를 업데이트하는 메소드를 정의하세요.
    • 트랜잭션 전파 범위에 유의하세요.
  • 바이너리 데이터 저장 성공 여부를 메타 데이터에 반영하세요.
    • 성공 시 BinaryContent의 status를 SUCCESS로 업데이트하세요.
    • 실패 시 BinaryContent의 status를 FAIL로 업데이트하세요.

Spring Event - 알림 기능 추가하기

  • 1)채널에 새로운 메시지가 등록되거나 2)권한이 변경된 경우 이벤트를 발행해 알림을 받을 수 있도록 구현합니다.
  • 채널에 새로운 메시지가 등록된 경우 알림을 받을 수 있도록 리팩토링하세요.
    • MessageCreatedEvent를 정의하고 새로운 메시지가 등록되면 이벤트를 발행하세요.
    • 사용자 별로 관심있는 채널의 알림만 받을 수 있도록 ReadStatus 엔티티에 채널 알림 여부 속성(notificationEnabled)을 추가하세요.
      • PRIVATE 채널은 알림 여부를 true로 초기화합니다.
      • PUBLIC 채널은 알림 여부를 false로 초기화합니다.

`-- schema.sql
CREATE TABLE read_statuses
(
id uuid PRIMARY KEY,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone,
user_id uuid NOT NULL,
channel_id uuid NOT NULL,
last_read_at timestamp with time zone NOT NULL,
notification_enabled boolean NOT NULL,
UNIQUE (user_id, channel_id)
);

-- ALTER TABLE read_statuses
-- ADD COLUMN notification_enabled boolean NOT NULL;
`

  • 알림 여부를 수정할 수 있게 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
  • 알림이 필요한 이벤트가 발행되었을 때 알림을 생성하세요.

    • 이벤트를 처리할 리스너를 구현하세요.

`public class NotificationRequiredEventListener {

@TransactionalEventListener
public void on(MessageCreatedEvent event) {...}

@TransactionalEventListener
public void on(RoleUpdatedEvent event) {...}
}`

  • on(MessageCreatedEvent)
    • 해당 채널의 알림 여부를 활성화한 ReadStatus를 조회합니다.

    • 해당 ReadStatus의 사용자들에게 알림을 생성합니다.

      • 알림 예시
        • title: "보낸 사람 (#채널명)"
        • content: "메시지 내용"
    • 단, 해당 메시지를 보낸 사람은 알림 대상에서 제외합니다.

    • on(RoleUpdatedEvent)

      • 권한이 변경된 당사자에게 알림을 생성합니다.
        • 알림 예시
          • title: "권한이 변경되었습니다."
          • content: "USER -> CHANNEL_MANAGER"

심화

  • 심화 항목 1
  • 심화 항목 2

주요 변경사항

스크린샷

image

멘토에게

  • 셀프 코드 리뷰를 통해 질문 이어가겠습니다.

BinaryContent의 상태를 업데이트하는 메서드를 정의하는 것까지 했습니다.
BinaryContentCreatedEventListener의 이벤트 리스너에 시험삼아 condition으로 조건을 걸어봤습니다.
채널에 새로운 메시지가 등록된 경우 알림을 받을 수 있도록 리팩토링하세요.
 - MessageCreatedEvent를 정의하고 새로운 메시지가 등록되면 이벤트를 발행하세요.
 - 사용자 별로 관심있는 채널의 알림만 받을 수 있도록 ReadStatus 엔티티에 채널 알림 여부 속성(notificationEnabled)을 추가하세요.
    - PRIVATE 채널은 알림 여부를 true로 초기화합니다.
    - PUBLIC 채널은 알림 여부를 false로 초기화합니다.
 - 알림 여부를 수정할 수 있게 ReadStatusUpdateRequest를 수정하세요.
[ ] 사용자의 권한(Role)이 변경된 경우 알림을 받을 수 있도록 리팩토링하세요.

[ ] 알림 API를 구현하세요.
* 요청자 본인의 알림에 대해서만 수행할 수 있습니다. << fix 해야함

[ ] 알림이 필요한 이벤트가 발행되었을 때 알림을 생성하세요.
* TaskDecorator 수정 필요
* 재시도가 모두 실패했을 때 대응전략 수정 필요
}

// 트랜잭션이 commit되었을 때 실행
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
Copy link
Collaborator

Choose a reason for hiding this comment

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

TransactionPhase.AFTER_COMMIT 가 default 설정인데 이렇게 해두신것은 의도를 표현하기 위함인가요?


@Transactional
@Override
public UserDto updateRoleInternal(RoleUpdateRequest request) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

해당 로직에서도 이벤트 발행이 필요해보이는데 누락된것 같습니다 !

@Bean(name = "taskExecutor")
public Executor taskExecutor() {
// TaskDecorator는 인터페이스다
CustomTaskDecorator decorator = new CustomTaskDecorator();
Copy link
Collaborator

Choose a reason for hiding this comment

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

파라미터로 필요한 정보 추가로 넣어주시면 좋을것 같습니다 !


@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
Copy link
Collaborator

Choose a reason for hiding this comment

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

설정은 잘하셨는데 어노테이션으로 적용하는 부분이 없는것 같습니다. 요구사항에 맞춰 어노테이션을 적용해보시면 좋을것 같습니다.

import org.springframework.stereotype.Component;

@Component
@EnableRetry
Copy link
Collaborator

Choose a reason for hiding this comment

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

RetryConfig 보다는 AppConfig라고 명명하고 추후 앱관련 공통설정은 여기다 몰아넣는게 좋은것 같아요!

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