Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#153] 실시간 알람 연결, (크루 알람,게임 알람) 생성, 수정, 삭제, 미확인 알람 확인 구현 #190

Merged
merged 78 commits into from
Nov 26, 2023

Conversation

jay-so
Copy link
Member

@jay-so jay-so commented Nov 22, 2023

👨‍💻 작업 사항

📑 PR 개요


✅ 작업 목록

  • 사용자 SSE 연결 설정
  • 크루 관련 알람 도메인
  • 게임 관련 알람 도메인
  • 크루 알람 사용자 구분(크루장, 크루원)
  • 게임 알람 사용자 구분(호스트, 게스트)
  • 사용자 미 알람 여부 구현
  • 크루 관련 알람 수정 구현(크루장 가입 신청 시 감지 알람, 크루원 가입 승락 시 알람, 크루원 가입 거절 시 알람)
  • 게임 관련 알람 수정 구현(호스트 가입 신청 시 감지 알람, 게스트 가입 승락 시 알람, 게스트 가입 거절 시 알람)
  • 크루,게임 관련 알람 삭제 구현
  • 실시간 알람(크루, 게임) 로직 구현
  • 비동기 로직을 통한 성능 개선
  • 이벤트 핸들러를 통한 크루 알람
  • 이벤트 캐시 이용하여, 로그아웃,SSE 연결이 끊기는 동안 발생한 알람, 로그인 시 확인

🙏 리뷰어에게

  • SSE 연결 부분은 @Transactional 어노테이션을 붙이면 안되기 때문에, 붙이지 않았습니다.
  • 각 이벤트 핸들러 및 사용자 실시간 알람에 대해서 구현하였습니다.
  • 현재 프론트가 구현되어 있지 않아, 알람을 보내는 이벤트가 있더라도 콘솔창에는 나타나지 않습니다.
  • 알람 생성 후 DB에서 저장 로직을 구현하였기 때문에, 알람 생성된 후 알람은 DB 저장 내용에서 보면 됩니다.
  • 특정 역할을 수행하는 사용자(크루장, 호스트)가 가입 신청 시 알람을 받고, 이를 승락과 거절을 당하면 해당 지원자에게 알람이 가도록 구현하였습니다.
  • 비동기 로직을 구현하였습니다. 초기 비동기 로직 시 setter를 통해 설정하는 부분이 있습니다. 팀 규칙으로 Builder나 정적 팩토리 패턴을 이용하지만, 해당 부분에서는 초기 설정을 위해서는 setter가 필요한 것으로 알고 있습니다.
  • 각 크루 가입, 승락 및 거절 시 알람이 생성되는 것을 로컬에서 확인하였습니다.
  • 특정 사용자에 대한 SSE 연결 확인 및 해당 사용자의 로그아웃 이후 재 접속 시 온 알람의 판단 여부를 위해 사용자 미 알람 여부를 구현하였습니다.
  • Resposne의 AlarmProfile은 이후의 PR에서, 페이지 네이션를 구현 및 이후에 사용되는 클래스입니다. (삭제 완료)

@jay-so jay-so added the 신규 기능 [feat] 새로운 기능을 추가한다. label Nov 22, 2023
@jay-so jay-so added this to the 5차 스프린트 milestone Nov 22, 2023
@jay-so jay-so requested review from hanjo8813 and a team November 22, 2023 16:23
@jay-so jay-so self-assigned this Nov 22, 2023
@jay-so jay-so requested review from charlesuu and Hchanghyeon and removed request for a team November 22, 2023 16:23
Copy link
Member

@Hchanghyeon Hchanghyeon left a comment

Choose a reason for hiding this comment

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

재훈님 작성하시느라 고생 많으셨습니다~!

우선 컨벤션 부분만 보았으니 확인 부탁드려요! 👍 👍

Copy link
Collaborator

@hanjo8813 hanjo8813 left a comment

Choose a reason for hiding this comment

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

재훈님 수고하셨습니다~
처음 시도해보는 기술이라 어려웠을텐데 그래도 잘 구현하셨네요!
다만 구조적으로 수정해야하는 부분이 꽤 많아서, 최대한 빨리 피드백 반영해주셔야할 것 같습니다~

src/main/java/kr/pickple/back/alarm/config/AsynConfig.java Outdated Show resolved Hide resolved
Comment on lines +31 to +36
return ResponseEntity.status(OK)
.header("X-Accel-Buffering", "no")
.body(emitter);
Copy link
Collaborator

Choose a reason for hiding this comment

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

nginx 설정 중 프록시 버퍼링 옵션을 off로 바꾸면되지 않나요?

Copy link
Member Author

@jay-so jay-so Nov 23, 2023

Choose a reason for hiding this comment

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

해당 부분을 정말 정말 많이 고민했었습니다.

  • SSE 통신에서 서버에서 동적으로 생성된 컨텐츠를 보내주기 때문에 때문에 본문의 크기를 미리 알 수 없기에 기본적으로 응답에 Transfer-Encoding: chunked를 사용하는데요.
  • Nginx는 서버의 응답을 버퍼에 저장해두었다가 버퍼가 차거나 서버가 응답 데이터를 모두 보내면 클라이언트로 전송하게 됩니다. - 이에 버퍼링 기능을 활성화하면 SSE 통신 시 원하는대로 동작하지 않거나 실시간성이 떨어지게 되어 SSE 응답에 대해서는 proxy buffering 설정을 비활성화 해주는 것이 좋지만 다음의 문제가 발생합니다.

해당 방식으로 인한 발생하는 또다른 문제: Nginx의 설정 파일에서 버퍼링을 비활성화하면 다른 모든 API 응답에 대해서도 버퍼링을 하지 않기 때문에 비효율적이기 때문이라는 문제점이 발생됩니다.

  • 따라서 nginx의 X-accel 기능을 활용하면 좋다고 합니다.
  • 백엔드의 응답 헤더에 X-accel로 시작하는 헤더가 있으면 Nginx가 이 정보를 이용해 내부적인 처리를 따로 하도록 만들 수 있도록 하는 방식이 SSE 응답만 버퍼링을 하지 않도록 설정할 수 있다고 합니다.
  • 따라서 헤더에다가 X-accel 기능을 사용하였습니다.

참고자료: 우테코 SSE 연결 (https://tecoble.techcourse.co.kr/post/2022-10-11-server-sent-events/)

Copy link
Member Author

@jay-so jay-so left a comment

Choose a reason for hiding this comment

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

@hanjo8813 @Hchanghyeon
피드백 남긴 부분에 대하여 수정을 진행했습니다~
확인해주시면 감사하겠습니다!

src/main/http/alram/alarm.http Outdated Show resolved Hide resolved
Comment on lines +31 to +36
return ResponseEntity.status(OK)
.header("X-Accel-Buffering", "no")
.body(emitter);
Copy link
Member Author

@jay-so jay-so Nov 23, 2023

Choose a reason for hiding this comment

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

해당 부분을 정말 정말 많이 고민했었습니다.

  • SSE 통신에서 서버에서 동적으로 생성된 컨텐츠를 보내주기 때문에 때문에 본문의 크기를 미리 알 수 없기에 기본적으로 응답에 Transfer-Encoding: chunked를 사용하는데요.
  • Nginx는 서버의 응답을 버퍼에 저장해두었다가 버퍼가 차거나 서버가 응답 데이터를 모두 보내면 클라이언트로 전송하게 됩니다. - 이에 버퍼링 기능을 활성화하면 SSE 통신 시 원하는대로 동작하지 않거나 실시간성이 떨어지게 되어 SSE 응답에 대해서는 proxy buffering 설정을 비활성화 해주는 것이 좋지만 다음의 문제가 발생합니다.

해당 방식으로 인한 발생하는 또다른 문제: Nginx의 설정 파일에서 버퍼링을 비활성화하면 다른 모든 API 응답에 대해서도 버퍼링을 하지 않기 때문에 비효율적이기 때문이라는 문제점이 발생됩니다.

  • 따라서 nginx의 X-accel 기능을 활용하면 좋다고 합니다.
  • 백엔드의 응답 헤더에 X-accel로 시작하는 헤더가 있으면 Nginx가 이 정보를 이용해 내부적인 처리를 따로 하도록 만들 수 있도록 하는 방식이 SSE 응답만 버퍼링을 하지 않도록 설정할 수 있다고 합니다.
  • 따라서 헤더에다가 X-accel 기능을 사용하였습니다.

참고자료: 우테코 SSE 연결 (https://tecoble.techcourse.co.kr/post/2022-10-11-server-sent-events/)

src/main/java/kr/pickple/back/alarm/util/SseEmitters.java Outdated Show resolved Hide resolved
Comment on lines 15 to 18
public class SseEmitters {

private final ConcurrentMap<Long, SseEmitter> emitters = new ConcurrentHashMap<>();

Copy link
Member Author

@jay-so jay-so Nov 24, 2023

Choose a reason for hiding this comment

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

넵!

  • 기존의 SseEmitters를 제거하고, SseEmitterRepositorySseEmitterRespositoryImpl(SseEmitterRepository구현체)로 대체하였습니다.
  • 다만 기존 AlarmService는 크루 알람과 게임 알람 중 미 확인 알람 로직, 삭제 로직이 있고 추후 커서 페이징을 위해 해당 사용자의 모든 알람을 조회하는 로직이 추가될 예정입니다.
  • 따라서 별도의 Sse연결 로직과 관련된 SseService를 만들어 Sse연결 및 notify( )메소드 등이 담긴 클래스로 별도로 만드는 것이 더 좋다고 생각이 들어 SseService 클래스를 생성하였습니다.
  • 해당 부분에서는 아래의 포스팅글을 참고하여 작성하였습니다~
    [SSE 연결 참고 자료]: (https://velog.io/@answlsdud98/SSE%EB%A1%9C-%EC%9B%B9-%ED%91%B8%EC%8B%9C-%EC%95%8C%EB%A6%BC-%EA%B0%9C%EB%B0%9C)

반영 커밋: 6970b6d

@jay-so jay-so requested review from hanjo8813, Hchanghyeon and kylekim2123 and removed request for kylekim2123 November 24, 2023 18:01
Copy link
Member Author

@jay-so jay-so left a comment

Choose a reason for hiding this comment

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

@hanjo8813
남겨 주신 코멘트에 대해서 수정을 완료하였습니다! 확인해주시면 감사하겠습니다~

@jay-so jay-so requested a review from hanjo8813 November 25, 2023 15:59
public class SseEmitterLocalRepository implements SseEmitterRepository {

private final Map<Long, SseEmitter> emitters = new ConcurrentHashMap<>();
private final Map<Long, Object> eventCache = new ConcurrentHashMap<>();
Copy link
Collaborator

Choose a reason for hiding this comment

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

eventCache의 의도가 무엇인가요?

Copy link
Member Author

@jay-so jay-so Nov 25, 2023

Choose a reason for hiding this comment

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

알람 이벤트가 실패했을 때 해당 이벤트를 임시로 저장하는 공간으로 알고 있습니다. 혹여 SSE 연결이 끊기고, 알람 이벤트를 전송 실패 시를 대비하여 evnetCache 에 저장 후, 클라이언트에서 다시 SSE 연결을 맺을 때, 해당 알람을 보내도록 했습니다.

  • 사용자가 SSE 연결이 끊기고 재 연결을 하러 SSE 연결 API를 호출할 경우를 대비하여, 저는 필요하다고 생각을 합니다.
  • 해당 eventCache 변수의 네이밍을 fallbackEmitters로 변경하도록 수정하였습니다.
    반영 커밋: 7b60bf2

Comment on lines 25 to 52
public SseEmitter subscribeToSse(final Long loggedInMemberId) {
final Member member = findMemberById(loggedInMemberId);
final SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);

try {
emitter.send(SseEmitter.event()
.name("AlarmSseConnect")
.data("사용자에 대한 알람 SSE 연결이 정상적으로 처리되었습니다."));
} catch (IOException e) {
sseEmitterRepository.deleteById(loggedInMemberId);
emitter.completeWithError(e);
}

sseEmitterRepository.save(String.valueOf(loggedInMemberId), emitter);
return emitter;
}

public void notify(final Long loggedInMemberId, final Object event) {
try {
SseEmitter emitter = subscribeToSse(loggedInMemberId);
emitter.send(SseEmitter.event().name("AlarmEvent").data(event));
} catch (Exception e) {
sseEmitterRepository.saveEventCache(String.valueOf(loggedInMemberId), event);
sseEmitterRepository.deleteById(loggedInMemberId);
log.error("알림 전송 중 오류가 발생했습니다.", e);
}
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

해당 메소드들에서 오류 발생시 emitter 정보를 삭제하는 코드가 반복되고 있는데,
추후 리팩토링에서 AOP를 적용해보면 좋을것 같습니다.

Copy link
Member Author

Choose a reason for hiding this comment

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

넵! 추후 리팩토링을 진행하면서 AOP를 적용하도록 하겠습니다~

Comment on lines +16 to +20
.corePoolSize(30)
.maxPoolSize(50)
.queueCapacity(70)
.build();
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

현재 매직넘버 사용 지양 원칙을 위반합니다.
이런 설정값은 보통 application.yml에 작성하고, property 객체로 받아오는게 일반적입니다.

Copy link
Member Author

@jay-so jay-so Nov 26, 2023

Choose a reason for hiding this comment

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

해당 문제에 착각을 했었습니다! 매직넘버가 있어, 해당 부분을 설정 파일로 빼었을때, 기존 크루 거절, 게임 거절의 경우 해당 알람이 발생되지 않았었기에 해당 PR에서는 수정하지 못했었습니다.
살펴보니 제 기존 로직에 대해서 다음과 같은 문제가 있었습니다.

현재 PR의 크루 거절, 게임 멤버 거절 알람

            deleteCrewMember(crewMember);

            eventPublisher.publishEvent(CrewMemberRejectedEvent.builder()
                    .crewId(crewId)
                    .memberId(memberId)
                    .build());
            return;
        }

먼저 크루원 테이블, 게임 멤버 테이블에서 거절 시 하드 delete가 되어 알람 대상자가 미리 삭제되어 알람 발송이 불가하였습니다. 따라서 먼저 거절 시 알람을 보내고, 크루원 및 게임 멤버 테이블에 삭제되어야 했었네요.

이후의 PR에서 리팩토링한 부분

       eventPublisher.publishEvent(CrewMemberRejectedEvent.builder()
                    .crewId(crewId)
                    .memberId(memberId)
                    .build());

            deleteCrewMember(crewMember);
            return;

해당 부분을 놓치고 있었네요! 다음 PR에서 바로 수정을 했습니다!

@jay-so jay-so requested a review from hanjo8813 November 25, 2023 16:23
…트 캐시에 저장된 이벤트 재전송 및 전송 이후 캐시에서 해당 이벤트를 삭제 로직 추가)
Copy link
Member Author

@jay-so jay-so left a comment

Choose a reason for hiding this comment

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

@hanjo8813
안녕하세요 재원님! @hanjo8813 @Hchanghyeon
피드백 남긴 부분에 대하여 수정을 진행했습니다~
확인해주시면 감사하겠습니다!

public class SseEmitterLocalRepository implements SseEmitterRepository {

private final Map<Long, SseEmitter> emitters = new ConcurrentHashMap<>();
private final Map<Long, Object> eventCache = new ConcurrentHashMap<>();
Copy link
Member Author

@jay-so jay-so Nov 25, 2023

Choose a reason for hiding this comment

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

알람 이벤트가 실패했을 때 해당 이벤트를 임시로 저장하는 공간으로 알고 있습니다. 혹여 SSE 연결이 끊기고, 알람 이벤트를 전송 실패 시를 대비하여 evnetCache 에 저장 후, 클라이언트에서 다시 SSE 연결을 맺을 때, 해당 알람을 보내도록 했습니다.

  • 사용자가 SSE 연결이 끊기고 재 연결을 하러 SSE 연결 API를 호출할 경우를 대비하여, 저는 필요하다고 생각을 합니다.
  • 해당 eventCache 변수의 네이밍을 fallbackEmitters로 변경하도록 수정하였습니다.
    반영 커밋: 7b60bf2

Comment on lines 25 to 52
public SseEmitter subscribeToSse(final Long loggedInMemberId) {
final Member member = findMemberById(loggedInMemberId);
final SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);

try {
emitter.send(SseEmitter.event()
.name("AlarmSseConnect")
.data("사용자에 대한 알람 SSE 연결이 정상적으로 처리되었습니다."));
} catch (IOException e) {
sseEmitterRepository.deleteById(loggedInMemberId);
emitter.completeWithError(e);
}

sseEmitterRepository.save(String.valueOf(loggedInMemberId), emitter);
return emitter;
}

public void notify(final Long loggedInMemberId, final Object event) {
try {
SseEmitter emitter = subscribeToSse(loggedInMemberId);
emitter.send(SseEmitter.event().name("AlarmEvent").data(event));
} catch (Exception e) {
sseEmitterRepository.saveEventCache(String.valueOf(loggedInMemberId), event);
sseEmitterRepository.deleteById(loggedInMemberId);
log.error("알림 전송 중 오류가 발생했습니다.", e);
}
}

Copy link
Member Author

Choose a reason for hiding this comment

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

넵! 추후 리팩토링을 진행하면서 AOP를 적용하도록 하겠습니다~

Copy link

Kudos, SonarCloud Quality Gate passed!    Quality Gate passed

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 15 Code Smells

18.0% 18.0% Coverage
0.0% 0.0% Duplication

@jay-so jay-so merged commit bad5b73 into dev Nov 26, 2023
@jay-so jay-so deleted the feature/#153 branch November 26, 2023 08:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
신규 기능 [feat] 새로운 기능을 추가한다.
Projects
None yet
4 participants