개요
현재 알림 시스템은 딱 한 유형, '댓글이 달렸을 때' 만을 기준으로 개발되어있다.
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class NotificationProducer {
private final KafkaTemplate<String, NotificationEvent> kafkaTemplate;
private final PostRepository postRepository;
@Around("@annotation(triggerNotification)")
public Object notify(ProceedingJoinPoint joinPoint, TriggerNotification triggerNotification) throws Throwable {
Object result = joinPoint.proceed();
Object[] args = joinPoint.getArgs();
Long toMemberId = extractToMemberId(args);
Long fromMemberId = extractFromMemberId(args);
Long relatedId = extractCommentId(result);
if (toMemberId.equals(fromMemberId)) return result;
NotificationEvent event = NotificationEvent.builder()
.target(triggerNotification.target())
.fromMemberId(fromMemberId)
.toMemberId(toMemberId)
.relatedId(relatedId)
.content("새 댓글이 달렸습니다.")
.build();
kafkaTemplate.send("notification.comment", event);
log.info("댓글 알림 전송: {}", event);
return result;
}
private Long extractToMemberId(Object[] args) {
Long postId = (Long) args[1];
Post post = postRepository.findById(postId).orElse(null);
return Objects.requireNonNull(post).getMember().getId();
}
private Long extractFromMemberId(Object[] args) {
User userDetails = (User) args[0];
return Long.valueOf(userDetails.getUsername());
}
private Long extractCommentId(Object result) {
if (result instanceof Long id) return id;
return null;
}
}
aop기반으로 설계하여서 모든 알림 유형을 같은 로직으로 사용할 수는 없을 것이다. 메서드 마다 추출값도 다르고, 수신자 수도 다르다.
새로운 유형, 로직이 추가되어도 확장성이 좋도록 리팩토링 해보자.
전략 패턴 ? 템플릿 메서드 패턴 ?
템플릿 메서드 패턴은 전략 패턴이랑 비교했을 때 요즘은 잘 손이 안 가는 것 같다.
특히 자바에서 인터페이스에 default 메서드가 가능하다는 걸 알고 나서는, 굳이 상속 구조 만들어서 템플릿 메서드 쓸 이유가 잘 안 느껴진다.
전략 패턴이면 필요한 메서드만 딱 구현해서 넘기면 되고, 의존성도 깔끔하게 DI로 주입해서 쓰면 되는데,
템플릿 메서드는 상위 추상 클래스에 뭔가 의존성이 필요하면 결국 super()로 주입해줘야 해서 오히려 불편하다.
상속 구조도 얽히면 유지보수도 어렵고, 테스트도 귀찮고…
템플릿 메서드가 갖는 이점이 뭐가 있을까? 강제로 공통 흐름을 정의할 수 있다는 정도?
근데 그것도 전략 패턴 + 디폴트 메서드 조합이면 웬만한 흐름은 다 커버 가능해서,,,
아무튼 그래서 전략 패턴으로 여러 알림 유형을 구현할 수 있게 만들어보자
public interface NotificationStrategy {
boolean supports(NotificationTarget target);
NotificationEvent buildEvent(Object[] args, Object result);
}
인터페이스를 만들어서 다양한 알림 전략을 구현하고, Kafka에 사용될 Event객체를 반환하도록 한다
@Component
@RequiredArgsConstructor
public class CommentNotificationStrategy implements NotificationStrategy {
private final PostRepository postRepository;
@Override
public boolean supports(NotificationTarget target) {
return target == NotificationTarget.COMMENT;
}
@Override
public NotificationEvent buildEvent(Object[] args, Object result) {
Long postId = (Long) args[1];
User userDetails = (User) args[0];
Post post = postRepository.findById(postId).orElseThrow();
Long toMemberId = post.getMember().getId();
Long fromMemberId = Long.valueOf(userDetails.getUsername());
Long commentId = (result instanceof Long id) ? id : null;
return NotificationEvent.builder()
.target(NotificationTarget.COMMENT)
.fromMemberId(fromMemberId)
.toMemberId(toMemberId)
.relatedId(commentId)
.content("새 댓글이 달렸습니다.")
.build();
}
}
구현체에서는 이렇게 서비스 메서드에서 추출해온 변수들로 Event객체를 만들고 반환한다.
전략패턴을 사용한 목적이 여기에 있다. 각 전략 마다 추출방법이 다를 것이다
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class NotificationProducer {
private final KafkaTemplate<String, NotificationEvent> kafkaTemplate;
private final List<NotificationStrategy> strategies;
@Around("@annotation(triggerNotification)")
public Object notify(ProceedingJoinPoint joinPoint, TriggerNotification triggerNotification) throws Throwable {
Object result = joinPoint.proceed();
Object[] args = joinPoint.getArgs();
NotificationStrategy strategy = strategies.stream()
.filter(s -> s.supports(triggerNotification.target()))
.findFirst()
.orElseThrow(() -> new InvalidRequestException(ErrorCode.INVALID_NOTIFICATION_TYPE));
NotificationEvent event = strategy.buildEvent(args, result);
//자기 자신에게 보내는 알림은 무시
if (Objects.equals(event.getToMemberId(), event.getFromMemberId())) return result;
kafkaTemplate.send("notification." + event.getTarget().name().toLowerCase(), event);
log.info("알림 전송: {}", event);
return result;
}
}
최종적으로 Producer에서는 strategy에 따라 다른 event를 카프카에 보내는 구조를 가지게 된다!
새로운 알림 서비스가 생기면, NotificationStrategy만 구현하면 된다.
예외처리 & RDB 저장
이제 여기서 새로운 로직을 추가해보자. 새로운 로직 또한 상위클래스에 고정하는것이 아니라, 클래스 마다 조금씩 다르게 적용할 수 있어야 한다.
예를 들어,
1. 댓글 알림 flow
> kafka event 객체 생성
> 예외 처리
> event 객체 토대로 rdb에 저장
> kafka에 send
2. 실시간 매칭 성공 flow
> kafka event 객체 생성
> 예외 처리
> rdb 저장 필요없음: "실시간 매칭 알림" 이기 때문에.. 굳이?
> kafka에 send
이처럼 알림 유형마다 로직이 다르기 때문에, 인터페이스에서 적절히 default메서드를 섞어주자.
public interface NotificationStrategy {
//알림 유형을 bool 형으로 판단
boolean supports(NotificationTarget target);
//Kafka 에 전송할 event 객체 생성
NotificationEvent buildEvent(Object[] args, Object result);
//알림 예외처리: True 이면 Return
boolean validate(NotificationEvent event);
//RDB 에 알림 객체 저장(실시간 알림인 경우 오버라이딩만)
void save(NotificationEvent event);
//Kafka 에 Event 보냄
void publish(NotificationEvent event);
//전체 로직 정의
default void execute(Object[] args, Object result) {
NotificationEvent event = buildEvent(args, result);
if (validate(event)) return;
save(event);
publish(event);
}
}
예외처리, RDB 저장, kafka에 전송하는 메서드를 추가로 정의해주고,
(카프카에 event를 보내는 메서드도 default가 가능하긴 한데.. KafkaTemplate이 필요해서 일단 이렇게 놔뒀다.)
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class NotificationProducer {
private final List<NotificationTemplate> templates;
@Around("@annotation(triggerNotification)")
public Object notify(ProceedingJoinPoint joinPoint, TriggerNotification triggerNotification) throws Throwable {
Object result = joinPoint.proceed();
Object[] args = joinPoint.getArgs();
NotificationStrategy strategy = strategies.stream()
.filter(s -> s.supports(triggerNotification.target()))
.findFirst()
.orElseThrow(() -> new InvalidRequestException(ErrorCode.INVALID_NOTIFICATION_TYPE));
strategy.execute(args, result);
return result;
}
}
최종적으로 Producer에서는, supports로 어떤 template을 사용할지 찾고 execute만 호출하면 된다!
이제 어떠한 알림 전략이 추가되어도 손쉽게 확장할 수 있는 구조가 되었다
리팩토링 하고도 잘 작동한다 뿌듯
회고
전략패턴으로 예전에 삽질했던 것들이 생각이 났다
그때는 쓰고싶어서 썼다면 지금은 필요해서 썼다고 당당히 말할 수 있지 않을까?
재밌는 리팩토링이었다~~
'개인 공부용 > ㅇㅅㅇ' 카테고리의 다른 글
MESI 프로토콜 (0) | 2025.04.18 |
---|---|
알림 구현(2) 웹소켓 세션 다중 클라이언트, 다중 인스턴스 환경 (0) | 2025.04.11 |
알림 구현(1) Kafka + Redis Pub/Sub + WebSocket MVP (0) | 2025.04.07 |