
HeartBeat
알림 기능을 구현은 했지만, 이슈가 있었다.
1. 클라이언트와 연결되는 커넥션 시간이 45초를 넘으면 반드시 죽었다.
2. SSE객체의 생명주기를 조정해도(45초보다 짧게 해 의도적으로 객체가 죽도록 설정해도) 해당 결과는 변함이 없었다.
이는 첫 연결과 관련이 있다. 클라이언트는 subscribe라는 동작을 통해서 서버에게 정보를 수신할 수 있는 상태를 마련한다. 처음 이렇게 서버와 연결이 되면 서버는 커넥션이 subcribe 직후에 종료되지 않도록 dummy데이터를 보내주도록 설정했다.
문제는 초기에 이 dummy데이터를 보내주어도 위와 같은 문제가 반복되는 것이었다. 그래서 내게 된 결론은 subscribe직후 뿐만이 아니라 주기적으로 이 connection이 살아있음을 알려주는 신호를 보내는 과정이 필요했다.
그래서 이렇게, 45초 간격으로 살아있음을 알려주는 heartBeat를 보내서 연결을 유지했다. 물룐이 연결은 SseEmitter 객체의 timeout시간이 되면 소멸한다. 하지만 곧바로 재요청을 보내도록 되어있어 다시 알림을 수신할 수 있다.
비동기 처리
이벤트 정보가 중요한지를 생각해봐야 한다.
실제로 댓글이 등록되는 로직은 중요하지만, 이벤트가 발생하는(알림을 전송하는) 과정은 상대적으로 중요하지 않다. 그래서 동작의 속도를 비교해보아도, 알림은 조금 느리게 동작해도 괜찮을만한 요구사항이다.
또한, 예외과정도 생각해봐야 한다. 댓글이 생성되지 못하는 예외가 발생하면, 알림도 전송되어선 안된다.
반대로 알림에서 문제가 발생해도 댓글 생성에는 전혀 영향을 미치지 않도록 설계하는게 가장 바람직 하다.
그래서 이부분은 @TransactionalEventListener, @Async키워드를 적용해 해결했다.
@TransactionalEventListener
트랜잭션이 끝난(커밋된) 이후에 동작하게끔 설정한다.
부모 메서드가 커밋되고나서 이벤트 로직이 시작하기 때문에 부모 메서드(댓글 등록) 에서 예외가 발생해도 자식(알림) 메서드 자체를 호출하지 않고 처리하게 된다.
반대로 부모에서 예외가 발생하지 않고 자식에서 예외가 발생하면, 트랜잭션 레벨을 REQUIRED_NEW로 예외가 전파되지 않도록 설정해야 하나 고민했다. 하지만 TransactionalEventListener 에서는 이를 걱정하지 않아도 된다. 부모 메서드는 이미 커밋되었기 때문에 자식과는 별개로 동작한다.
다만, 사용의 순서에 따른 로직의 불일치 문제는 생각해봐야 한다.
예를 들어, 비즈니스로직 -> 이벤트 -> 비즈니스 로직 으로 코드가 진행되고 이벤트 이후 예외가 발생하면, 이벤트는 정상 발행된다. (독립된 동작) 반면 첫번쨰 세번쨰 비즈니스 로직은 롤백된다.
그러기 때문에 이벤트가 실행되는 순서와 옵션을 잘 조정해서 사용하는 것이 바람직해보인다.
@Async
스프링에서 비동기로 메서드를 호출하면, 호출하는 메서드와, 호출을 받은 메서드는 각각 다른 스레드에서 작동한다. 그래서 트랜잭션을 서로 공유하지 않게 된다.(ThreadLocal) 그렇기 떄문에 트랜잭션 전파 레벨을 고민할 필요는 없어진다.
(두 키워드 모두 향후 테스트를 통해서 확인해기)
옵저버 패턴
기존 코드같은 경우는 구현에 집중하며 어떻게 리팩토링 해야할 지도 계획에 조차 없었다. 하지만 이를 '관리'와 '행위'로 나누어 보면, 더 많은 것들이 보였다.
일반적으로는 관찰 대상자, 관찰자라고 이야기를 하니 그 관점에 맞춰서 이야기해보도록 하겠다.
유의할 점 : 일반적으로는 Subject -> Observer라는 표현을 사용하지만, 여기서는 Manager -> Subject로 명칭을 사용했다.
관찰자 : 행위자
우선 관찰자가 존재한다. 어떤 사건을 인지하면 그에 맞는 적절한 행동을 한다. 나의 비즈니스 로직으로 치면, '댓글 등록'이 되면 작성자에게 알림이 가도록 설정했으니 이 역할을 관찰자가 한다.
관찰 대상자 : 관리자
관찰자들을 관리한다. 실제로 관리하는 것은 아니고, 관찰자를 대신해서 정보를 읽고, 관찰자에게 어떤 행동을 하도록 지시하는 것이다.
다시 말하면, 관리자가 가장 앞단에서 알림에 대한 비즈니스 처리를 접수한다. 그리고 실제 관찰자에게 행동을 위임해 관찰자들이 실제 행동을 하도록 명령한다.
예를 들면 다음과 같다. 관리자가 알림을 보내라는 명령을 한다. 그러면 관리자가 실제로 알림을 보내는 것이 아니라, 적절한 관찰자를 찾고, 그 구현체가 알림을 보내는 구조이다.
이 개념으로 리팩토링을 해보면 다음과 같은 코드를 구현할 수 있다.
AlarmManager.java
우선 관리자 인터페이스는 다음과 같이,
AlarmManagerImpl.java
인터페이스로 관찰자를 등록하는 메소드, 관찰자에게 알림을 전송하라는 의미의 메소드를 사용했다.
눈여겨볼점은 Map형태로 AlarmSubject인터페이스를 가지고 있다는 점이다. 저기에서 해당하는 관찰자를 찾고, 그 관찰자가 알림을 전송하도록 역할을 위임하는 점이 옵저버 디자인 패턴의 핵심이다.
AlarmSubject.java
실제로 행동을 하는 역할인 관찰자다. SseEmitter를 생성하고, 알림에 대한 정보를 전송하는 실질적인 역할을 맡고 있다.
실제 구현 내용은 생략한다.
'프로젝트 일기' 카테고리의 다른 글
[Bucket - Back] 프로젝트 트러블 슈팅 (1) | 2024.02.12 |
---|---|
Prometheus, Loki, Grafana 구조 이해하기 (0) | 2024.02.12 |
[와디즈 클론코딩 리팩토링] - 1. 동적인 처리(RequestBodyAdvice) (1) | 2024.02.02 |
[LIME 리팩토링] - 연관관계 제거 (2) | 2024.01.27 |
[빅데이터] 도시 물류 혁신 계획 (0) | 2022.11.22 |