안녕하세요,
최근 WeGo 프로젝트에서 다양한 요구사항을 개발하며 늘어나는 모듈과 모듈 사이의 결합도로 인한 생산성 문제가 있었는데요.
이러한 문제를 해결하기 위한 방식으로 도메인 이벤트 패턴(Domain Event Pattern)을 적용하여 결합도를 크게 낮췄던 경험을 공유해보려합니다.
정말 체감이 많이 되었던 방식으로 이번 협업에서 생산성을 크게 키워줬던 것 같습니다.
서비스 구조적 문제?
백엔드팀에서는 각 모듈 별로 개발을 진행하고 있었고, 모듈은 아래와 같습니다.

notification 모듈은 당연하게도 다른 모듈의 비즈니스 로직에 많이 포함되어 있었고, 동일한 트랜잭션을 사용하고 있었습니다.
이로인해 알림의 응답 형식과 같은 변경이 있을 때마다 코드 변경은 어쩔 수 없이 발생했고, 동료가 작성한 코드를 직접 변경하는 일도 많아졌습니다.
알림 모듈의 높은 결합도로 인한 문제는 아래 두가지가 있었습니다.
- 알림 로직의 실패가 기존 비즈니스 로직에 영향을 줌
- notification 모듈의 스펙 변경이 전역 모듈 변경을 일으킴
또한 이외에도 가장 큰 문제는 채팅 서비스가 모임 서비스에 크게 의존한다는 점이였는데요,
저희 서비스 구조는 아래와 같습니다.
- 모임이 생성되는 시점에 그룹 채팅방 생성
- 사용자가 모임 참여 시 그룹 채팅방도 자동 참여
- 모임 퇴장, 추방 시 그룹 채팅방 자동 퇴장
- 모임 종료 시 그룹 채팅방 데이터 제거
구조를 보면 알겠지만, 모임 도메인 요구사항 변경이 있을 때 마다 채팅 스펙도 당연히 변경이 필요하게 되는 구조입니다.
반대 케이스도 당연히 있었구요. (채팅 스펙 변경 -> 모임 로직 수정)
서로 다른 동료의 코드를 건드려야 한다는 점은 굉장히 큰 문제를 야기할 수 있고, 그만큼 개발 속도의 영향을 주는 부분이였습니다.
저희 백엔드팀은 이런 문제에 대한 검토를 진행했고 그 결과 이벤트 방식 도입을 결정하게 되었습니다.
도입 과정
위에서 설명드린 문제를 해결하기 위해서는 먼저 내부 컴포넌트 간 결합을 느슨하게 만들 필요가 있었고, 요구사항 변경 등의 확장에 용이하도록 개선할 필요가 있었습니다.
이를 위해 저희 팀에서는 도메인 이벤트 패턴 전략을 선택했고, Spring Application Event 매커니즘을 통해 구현했습니다.
Spring Application Event는 옵저버 디자인 패턴을 구현해놓은 Spring 구현체이며, Spring 4.2 버전 이후부터는 @EventListener 어노테이션을 통해 쉽게 이벤트 발행 및 로직을 구성할 수 있습니다.
이번 프로젝트는 모놀리식 구조로 이루어져 있으며, 도입 시점 기준으로 확장이 크게 예정되어 있었지 않았기에 도메인이 아닌 애플리케이션 레이어로 이벤트 발행 시점을 정하고 개발을 진행했습니다.
먼저 팔로우 서비스 로직에 적용된 기존 코드를 이벤트 방식으로 변경하는 과정입니다.
private final NotificationRepository notificationRepository;
private final SseEmitterService sseEmitterService;
public void follow(String followNickname, Long followerId) {
//중간 생략
followRepository.save(Follow.builder()
.follower(follower)
.follow(follow)
.build());
Notification notification = Notification.createFollowNotification(follow, follower);
notificationRepository.save(notification);
// SSE 전송
NotificationResponse dto = NotificationResponse.from(notification);
sseEmitterService.sendNotification(follow.getId(), dto);
}
변경전의 기존 코드를 보면,
follow 서비스 레이어에서 notification 도메인에 대한 직접적인 의존성을 가지며, 대부분의 로직을 처리하고 있습니다.
물론 follow뿐만 아니라 알림을 사용하는 전역 도메인에서 위와 비슷한 상황이였습니다.
작성전엔 몰랐지만 아래와 같은 많은 문제점을 포함하고 있는 코드라고 볼 수 있습니다.
- 단일 책임 원칙 위반 : 팔로우 로직, 알림 저장 로직, SSE 전송 로직
- 강한 결합도 : 다른 도메인을 직접 의존 -> 도메인 변경 시 영향
- 트랜잭션 경계 : SSE 실패가 팔로우 로직에도 영향을 끼침
이러한 구조적 문제를 개선하기위해 애플리케이션 레이어에 이벤트 발행을 적용했고, 적용 후 코드는 아래와 같습니다.
public void follow(String followNickname, Long followerId) {
//중간 생략
followRepository.save(Follow.builder()
.follower(follower)
.follow(follow)
.build());
// 알림 이벤트 발행
eventPublisher.publishEvent(new UserFollowEvent(follower.getId(), follow.getId()));
}
UserFollowEvent라는 도메인 이벤트를 발행하는 것으로 서비스 로직을 대체하여 결합도를 크게 낮출 수 있었습니다.
이벤트 객체를 생성할 때 엔티티가 아닌 필요한 데이터(id)로만 구성하여 전달하도록 구성했습니다.
@Component
public class FollowNotificationListener {
// 팔로우 트랜잭션 커밋 후 실행
@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleFollow(FollowEvent event) {
// 알림 저장
// SSE 전송 실패해도 팔로우는 유지됨
}
}
또한 @TransactionalEventListener 어노테이션을 통해 핸들러 로직이 기존 팔로우 로직에 영향을 끼치지 않도록 트랜잭션을 명시적으로 분리하여 구분지었습니다.
팔로우 로직 성공 유무에 영향을 받되, 핸들러 로직(알림) 성공 유무가 기존 로직에 영향을 끼치면 안된다고 판단하였기에 AFTER_COMMIT 속성을 추가해주었습니다.
구조적 결합도가 높았던 모임, 채팅 간 결합도도 아래와 같이 논리적으로 구분지을 수 있었습니다.
/**
* 모임 생성 시 그룹 채팅방 자동 생성
*/
@EventListener
public void handleGroupCreated(GroupCreatedEvent event) {
log.info("모임 생성 이벤트 수신 - groupId: {}, hostUserId: {}",
event.groupId(), event.hostUserId());
try {
chatRoomService.createGroupChatRoomForMeeting(
event.groupId(),
event.hostUserId()
);
log.info("그룹 채팅방 생성 완료 - groupId: {}", event.groupId());
} catch (Exception e) {
log.error("그룹 채팅방 생성 실패 - groupId: {}", event.groupId(), e);
}
}
기존 모임 도메인의 이벤트를 그대로 활용하여 코드를 작성해 굉장히 빠르게 개발했었던 것 같습니다.
"모임 생성 시 그룹 채팅방이 생성되어야 한다." 라는 기능적 요구사항 개발을 이벤트가 아니였다면 다른 동료의 모임쪽 코드를 하나하나 뜯어보며 붙여나가야 했고, 이는 많은 공수가 걸렸을거라 생각이 됩니다.
물론 다른 요구사항들도 마찬가지로 개발을 진행했고, 굉장히 짧은 시간안에 끝낼 수 있었습니다.
도입 후 개선 효과
위와 같이 이벤트를 도입하면서 굉장히 편했던 점은 결합도를 낮춘 덕분에 다른 동료의 코드를 수정할 일이 크게 줄어들었다는 점이고, 이로 인해 각 모듈 개발 진척 속도를 높일 수 있었습니다.
또한 도메인 로직 변경으로 인한 병합 단계에서의 오류율을 크게 줄일 수 있었습니다.
다음으로는 확장성이였는데요,
기능 추가 및 수정사항이 많았던 프로젝트에서 기존에 있었던 이벤트에 핸들러를 추가하는 방향으로 생산성을 향상시킬 수 있었습니다.
마무리를 하며 Event 객체를 어떻게 설계하고 이벤트 발행 시점과 이에 따른 트랜잭션 경계를 제대로 설계하는데는 꽤 많은 시간이 걸릴 것 같은데 그만한 아웃풋이 나오는 개발 방법이지 않나 생각이 들었습니다.
개발 기술 블로그, Dev
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!