-
Notifications
You must be signed in to change notification settings - Fork 0
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
Conversation
There was a problem hiding this 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/dto/response/AlarmResponse.java
Outdated
Show resolved
Hide resolved
src/main/java/kr/pickple/back/alarm/dto/response/CrewAlarmResponse.java
Outdated
Show resolved
Hide resolved
src/main/java/kr/pickple/back/alarm/dto/response/GameAlarmResponse.java
Outdated
Show resolved
Hide resolved
src/main/java/kr/pickple/back/alarm/event/game/GameMemberJoinedEvent.java
Outdated
Show resolved
Hide resolved
src/main/java/kr/pickple/back/crew/service/CrewMemberService.java
Outdated
Show resolved
Hide resolved
src/main/java/kr/pickple/back/alarm/exception/AlarmExceptionCode.java
Outdated
Show resolved
Hide resolved
src/main/java/kr/pickple/back/alarm/exception/AlarmExceptionCode.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this 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/controller/CrewAlarmController.java
Outdated
Show resolved
Hide resolved
return ResponseEntity.status(OK) | ||
.header("X-Accel-Buffering", "no") | ||
.body(emitter); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nginx 설정 중 프록시 버퍼링 옵션을 off로 바꾸면되지 않나요?
There was a problem hiding this comment.
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/handler/CrewAlarmEventHandler.java
Outdated
Show resolved
Hide resolved
src/main/java/kr/pickple/back/crew/service/CrewMemberService.java
Outdated
Show resolved
Hide resolved
src/main/java/kr/pickple/back/alarm/event/crew/CrewJoinRequestNotificationEvent.java
Show resolved
Hide resolved
src/main/java/kr/pickple/back/alarm/service/CrewAlarmService.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@hanjo8813 @Hchanghyeon
피드백 남긴 부분에 대하여 수정을 진행했습니다~
확인해주시면 감사하겠습니다!
return ResponseEntity.status(OK) | ||
.header("X-Accel-Buffering", "no") | ||
.body(emitter); |
There was a problem hiding this comment.
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/controller/CrewAlarmController.java
Outdated
Show resolved
Hide resolved
src/main/java/kr/pickple/back/alarm/handler/CrewAlarmEventHandler.java
Outdated
Show resolved
Hide resolved
public class SseEmitters { | ||
|
||
private final ConcurrentMap<Long, SseEmitter> emitters = new ConcurrentHashMap<>(); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
넵!
- 기존의
SseEmitters
를 제거하고,SseEmitterRepository
와SseEmitterRespositoryImpl(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
src/main/java/kr/pickple/back/alarm/service/CrewAlarmService.java
Outdated
Show resolved
Hide resolved
…구현, AlarmService의 SSE 연결 설정 및 관련 내용 SseEmitterService로 분리
…추가 및 sseEmitter 연결 로직 리팩토링
…mitterService 클래스에 분리하여 리팩토링
src/main/java/kr/pickple/back/alarm/repository/SseEmitterRepositoryImpl.java
Outdated
Show resolved
Hide resolved
src/main/java/kr/pickple/back/alarm/repository/SseEmitterRepositoryImpl.java
Outdated
Show resolved
Hide resolved
src/main/java/kr/pickple/back/alarm/service/SseEmitterService.java
Outdated
Show resolved
Hide resolved
src/main/java/kr/pickple/back/alarm/service/SseEmitterService.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@hanjo8813
남겨 주신 코멘트에 대해서 수정을 완료하였습니다! 확인해주시면 감사하겠습니다~
src/main/java/kr/pickple/back/alarm/repository/SseEmitterRepositoryImpl.java
Outdated
Show resolved
Hide resolved
src/main/java/kr/pickple/back/alarm/repository/SseEmitterRepositoryImpl.java
Outdated
Show resolved
Hide resolved
src/main/java/kr/pickple/back/alarm/service/SseEmitterService.java
Outdated
Show resolved
Hide resolved
src/main/java/kr/pickple/back/alarm/service/SseEmitterService.java
Outdated
Show resolved
Hide resolved
src/main/java/kr/pickple/back/alarm/service/SseEmitterService.java
Outdated
Show resolved
Hide resolved
src/main/java/kr/pickple/back/alarm/service/SseEmitterService.java
Outdated
Show resolved
Hide resolved
src/main/java/kr/pickple/back/alarm/service/SseEmitterService.java
Outdated
Show resolved
Hide resolved
src/main/java/kr/pickple/back/alarm/service/SseEmitterService.java
Outdated
Show resolved
Hide resolved
public class SseEmitterLocalRepository implements SseEmitterRepository { | ||
|
||
private final Map<Long, SseEmitter> emitters = new ConcurrentHashMap<>(); | ||
private final Map<Long, Object> eventCache = new ConcurrentHashMap<>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
eventCache의 의도가 무엇인가요?
There was a problem hiding this comment.
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
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); | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
해당 메소드들에서 오류 발생시 emitter 정보를 삭제하는 코드가 반복되고 있는데,
추후 리팩토링에서 AOP를 적용해보면 좋을것 같습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
넵! 추후 리팩토링을 진행하면서 AOP를 적용하도록 하겠습니다~
.corePoolSize(30) | ||
.maxPoolSize(50) | ||
.queueCapacity(70) | ||
.build(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
현재 매직넘버 사용 지양 원칙을 위반합니다.
이런 설정값은 보통 application.yml에 작성하고, property 객체로 받아오는게 일반적입니다.
There was a problem hiding this comment.
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에서 바로 수정을 했습니다!
…iterById, deleteEventCache 메소드 추가 및 구현)
…트 캐시에 저장된 이벤트 재전송 및 전송 이후 캐시에서 해당 이벤트를 삭제 로직 추가)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@hanjo8813
안녕하세요 재원님! @hanjo8813 @Hchanghyeon
피드백 남긴 부분에 대하여 수정을 진행했습니다~
확인해주시면 감사하겠습니다!
src/main/java/kr/pickple/back/alarm/service/SseEmitterService.java
Outdated
Show resolved
Hide resolved
src/main/java/kr/pickple/back/alarm/service/SseEmitterService.java
Outdated
Show resolved
Hide resolved
public class SseEmitterLocalRepository implements SseEmitterRepository { | ||
|
||
private final Map<Long, SseEmitter> emitters = new ConcurrentHashMap<>(); | ||
private final Map<Long, Object> eventCache = new ConcurrentHashMap<>(); |
There was a problem hiding this comment.
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
src/main/java/kr/pickple/back/alarm/service/SseEmitterService.java
Outdated
Show resolved
Hide resolved
src/main/java/kr/pickple/back/alarm/service/SseEmitterService.java
Outdated
Show resolved
Hide resolved
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); | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
넵! 추후 리팩토링을 진행하면서 AOP를 적용하도록 하겠습니다~
Kudos, SonarCloud Quality Gate passed! |
👨💻 작업 사항
📑 PR 개요
✅ 작업 목록
🙏 리뷰어에게
@Transactional
어노테이션을 붙이면 안되기 때문에, 붙이지 않았습니다.