MedOps 프로젝트 개요
MedOps는 사용자에게 편리한 온라인 예약 경험을 제공하고 병원 관리자에게는 효율적인 운영 관리를 위해 백오피스를 제공하는 시스템입니다. 이 프로젝트는 Java, Spring Boot 등 엔터프라이즈 환경에서 널리 사용되는 새로운 기술 스택을 학습하고, 헥사고날 아키텍처와 이벤트 소싱, CQRS 패턴을 실제 프로젝트에 적용하여 아키텍처 설계 역량을 강화하는 것을 목표로 했습니다. 또한 예약 도메인을 분석하고 도메인 주도 설계 관점에서 견고한 모델을 구축하는 경험을 쌓고자 했습니다.
프로젝트 구조

헥사고날 아키텍처
복잡한 의료 도메인 로직을 외부 기술 변경으로부터 보호하고, 테스트 가능한 설계를 만들기 위해 헥사고날 아키텍처를 선택했습니다. 기존 레이어드 아키텍처에서는 서비스 레이어가 특정 기술에 의존하게 되어 테스트와 유지보수가 어려운 경우가 있었습니다.
// 레이어드 아키텍처 - 서비스가 외부 기능에 직접적으로 의존하게 됨
@Service
public class ReservationService {
@Autowired private ReservationRepository repository;
@Autowired private PaymentGateway paymentGateway; // 결제 시스템
@Autowired private NotificationService notificationService; // 알림 시스템
@Autowired private HospitalSystem hospitalSystem; // 병원 내부 시스템
public void createReservation(ReservationDto dto) {
// 결제 처리 - 구체적인 게이트웨이에 의존
PaymentResult result = paymentGateway.charge(dto.getAmount());
Reservation reservation = new Reservation(dto);
reservation.confirm();
repository.save(reservation);
// 다양한 알림 서비스 직접 호출 - 강한 결합
notificationService.send(dto.getPhone(), dto.getEmail(), "예약 확정");
hospitalSystem.updateSchedule(reservation);
}
}
헥사고날 아키텍처에서는 도메인 서비스가 인터페이스만 의존하여 외부 기술과 분리됩니다.
// 헥사고날 - 서비스가 구현체 대신 인터페이스를 의존하게 됨
@Service
@RequiredArgsConstructor
public class ReservationService {
private final SaveReservationPort saveReservationPort;
private final PaymentPort paymentPort; // 결제 인터페이스
private final NotificationPort notificationPort; // 알림 인터페이스
private final HospitalPort hospitalPort; // 병원 내부 시스템 인터페이스
public void createReservation(CreateReservationCommand command) {
// 결제 처리 - 인터페이스에만 의존
PaymentResult result = paymentPort.processPayment(command.getAmount());
Reservation reservation = Reservation.create(command);
reservation.confirm(); // 도메인 로직
saveReservationPort.save(reservation);
// 알림 발송 - 포트로 추상화 (이메일/SMS 구현체는 어댑터에서)
notificationPort.send(reservation);
hospitalPort.updateSchedule(reservation);
}
}
레이어드 아키텍처에서 `Service`를 직접 참조하면 해당 구현체에 강하게 결합되지만, 헥사고날에서 `Port` 인터페이스를 참조하면 이메일, SMS, 푸시 알림 등 어떤 구현체로도 비교적 자유롭게 교체할 수 있습니다. 이런 구조로 MongoDB를 MySQL로 교체하는 상황에서도 어댑터만 바꾸면 됩니다
@Component
@RequiredArgsConstructor
public class ReservationPersistenceAdapter implements LoadReservationPort {
// private final ReservationDocumentRepository repository;
private final ReservationJpaRepository repository; // JPA로 변경
private final ReservationConverter converter;
@Override
public List<Reservation> loadByDoctorId(String doctorId) {
return repository.findAllByDoctorId(doctorId).stream()
.map(converter::toDomain).toList();
}
}
- 첫째, 도메인 로직이 프레임워크와 구현체에 독립적이어서 변경에 유연하고,
- 둘째, 인터페이스 기반으로 테스트용 Mock 구현체를 쉽게 만들 수 있어 빠른 단위 테스트가 가능하며,
- 셋째, 새로운 요구사항(알림 방식 변경, DB 교체 등)에 기존 비즈니스 로직 수정 없이 어댑터만 추가하면 됩니다.
이벤트 소싱 및 CQRS
이 프로젝트에서는 이벤트 소싱 패턴의 학습과 실습을 주요 목적으로 의료 예약 시스템에 적용해보았습니다.
전통적인 CRUD + 로그 방식으로도 충분히 구현 가능하지만, 이벤트 소싱만의 고유한 특성을 경험해보고 싶었습니다.
실제로 구현하면서 느낀 이벤트 소싱의 차별점은, 시스템의 현재 상태가 과거 이벤트들의 '결과'로만 존재한다는 점입니다.
전통적인 방식에서는 현재 상태를 저장하는 테이블이 '정답'이고 로그는 참고용 정보였다면,
이벤트 소싱에서는 이벤트 기록 자체가 '정답'이 되어 현재 상태를 언제든 다시 계산해낼 수 있습니다.
이는 단순한 구현 차이를 넘어서 데이터에 대한 근본적인 사고방식의 전환을 요구했고,
특히 과거 임의 시점의 정확한 상태 재현이나 새로운 프로젝션(읽기 모델) 생성이 매우 자연스럽게 이루어집니다.
이벤트 소싱은 애플리케이션의 상태 변경을 단순히 덮어쓰는 대신, 상태를 변화시키는 모든 행위를 이벤트로 순차적으로 기록하는 아키텍처 패턴입니다. 의료 도메인에서는 법적으로도 모든 의료 행위와 변경 사항에 대한 완전한 기록 보존이 요구되므로, 이벤트 소싱의 완전한 감사 추적(Audit Trail) 기능이 이러한 요구사항을 자연스럽게 만족시킵니다.
실제로 이벤트 소싱을 적용한 결과, 환자 문의 시 "담당의사 응급수술로 인한 불가피한 예약 변경(2024.03.15 14:30, 간호사 김○○ 처리)"과 같이 완전한 맥락 정보를 제공할 수 있게 되어 환자 만족도와 신뢰도가 크게 향상되었습니다. 또한 의료진 간에도 예약 변경 이력을 통해 더 원활한 소통이 가능해졌고, 과거 특정 시점의 상태를 정확히 재현할 수 있어 시스템 문제 발생 시 신속한 원인 분석과 복구가 가능해졌습니다.
나아가 명령(Command)과 조회(Query)의 책임을 분리하는 CQRS 패턴과 결합하면, 이벤트 구독을 통해 다양한 목적의 읽기 전용 모델을 유연하게 생성할 수 있습니다. 이는 앞으로 복잡해질 데이터 조회 요구사항에도 효과적으로 대응할 수 있는 확장성 높은 구조를 만들어 준다고 판단했습니다. 시간여행이 가능한 시스템 아키텍처 참고하여 작성했습니다.

-
신규 예약 신청 (이벤트 발생)
- CommandExecutor: 커맨드 처리 후 이벤트 생성
- EventStore: 생성된 이벤트를 저장
- EventPublisher: 이벤트 발행
- EventListener: 구독 중인 View Model 최신화
-
기존 예약 접수 (상태 변경)
- EventStore: 해당 예약의 과거 이벤트들을 모두 조회
- EventHandler: 조회된 이벤트들을 순차적으로 적용하여 현재 상태(State) 재구성
- CommandExecutor: 커맨드 처리 후 이벤트 생성
- EventStore: 새로운 이벤트를 추가로 저장
- EventListener: View Model 최신화
핵심 기능 및 구현 내용
예약 관리 시스템

이 기능의 핵심은 예약의 생성부터 완료까지 모든 상태 변경을 이벤트로 기록하여 데이터의 변경 과정을 완벽하게 추적하는 이벤트 소싱 기반의 상태 관리입니다. 이를 통해 특정 시점의 상태를 정교하게 복원할 수 있는 안정적인 시스템을 구축했습니다.
실시간 알림 (SSE)

Spring Boot의 SseEmitter를 활용하여 서버에서 클라이언트로 단방향 데이터 푸시가 가능한 실시간 알림 기능을 구현했습니다. WebSocket에 비해 가볍고 구현이 용이한 장점이 있습니다. 여러 관리자가 별도의 새로고침 없이 최신 예약 현황을 동기화할 수 있어, 다중 사용자 환경에서의 운영 효율성과 데이터 일관성을 크게 향상시켰습니다.
관리자 대시보드

관리자 대시보드는 CQRS 패턴을 적용하여 복잡한 조회 요건을 효율적으로 처리하도록 설계했습니다. 쓰기(Command) 모델과 분리된 읽기(Query) 전용 모델을 통해 실시간 예약 현황 및 매출 통계 조회의 성능을 최적화했습니다. 또한, 조회된 통계 데이터는 EChart.js와 연동하여 동적인 차트로 시각화함으로써, 관리자가 비즈니스 현황을 직관적으로 파악하고 데이터 기반의 의사결정을 내릴 수 있도록 지원합니다.