1. 프로젝트 배경 및 기술 선택
설문 서비스는 환자가 직접 보고하는 결과(PRO, Patient-Reported Outcome)를 다루는 의료 설문 플랫폼의 핵심 서비스로, 설문 서식의 정의부터 발송, 응답 수집, 마감에 이르는 설문의 전 생명주기를 책임집니다. 채점·통계·리포트 가공은 별도의 메트릭 서비스가, 알림·채팅·SMS/이메일의 실제 발송은 별도의 커뮤니케이션 서비스가 담당하도록 책임을 명확히 분리하여, 본 서비스는 '설문 데이터의 정의와 수집'이라는 단일 책임에 집중하도록 설계하였습니다.
다수의 내부 마이크로서비스 및 외부 레거시 의료정보 시스템(EMR)과 연동되는 환경에서 데이터 정합성과 확장성을 확보하기 위해, 다음과 같은 기술 스택을 전략적으로 선택하였습니다.
• Java 21 & Spring Boot 3.5.13 : 최신 LTS 런타임과 안정적인 Spring 생태계를 기반으로 서비스의 신뢰성을 확보하였습니다.
• Spring Cloud 2025.0.0 (OpenFeign) : 마이크로서비스 간 동기 통신을 선언적(declarative) 방식으로 처리하여 가독성과 유지보수성을 높였습니다.
• PostgreSQL & JPA/Hibernate & QueryDSL 5.0 : 복잡한 설문 도메인 모델을 객체지향적으로 관리하고, 타입 안전한 동적 조회를 구현하였습니다. JSON 컬럼은 hypersistence utils로 처리하여 가변적인 설문 속성을 유연하게 저장합니다.
• Apache Kafka : 서비스 간 비동기 이벤트 통신으로 결합도를 낮추고 최종 일관성을 보장합니다.
• OAuth2 & Keycloak : 분산 환경의 통합 인증(SSO)·인가를 위해 업계 표준 OAuth2 프로토콜을 적용하였습니다.
• Redis · ShedLock · MapStruct · Jasypt : 캐싱, 분산 스케줄 락, 객체 매핑, 설정 암호화 등 운영 안정성을 위한 보조 기술을 함께 적용하였습니다.
2. 핵심 기술 적용 및 아키텍처 설계
2.1 도메인 중심의 멀티 모듈 및 헥사고날 아키텍처
비즈니스 로직을 기술적 의존성으로부터 보호하기 위해 도메인을 중심에 둔 멀티 모듈 구조를 채택했습니다. 이는 헥사고날(포트-어댑터) 아키텍처 사상을 반영한 것으로, 핵심 도메인 로직은 domain 모듈에 응집시키고, 데이터베이스 영속화(store-jpa)나 외부 서비스 연동(proxy)은 교체 가능한 어댑터로 분리했습니다.
서비스는 책임에 따라 9개 모듈로 구성됩니다: domain(도메인 모델·포트), store-jpa(영속화 어댑터), proxy(외부 연동·이벤트 발행 어댑터), feature(유스케이스), facade(REST·이벤트 수신), client/event(타 서비스가 소비하는 계약·이벤트 아티팩트), scheduler(배치), boot(조립·진입점). 유스케이스 계층은 도메인 인터페이스에만 의존하고 실제 구현체는 boot 모듈이 주입(DIP)하므로, 연동 대상이 바뀌어도 핵심 로직은 수정 없이 유지됩니다.
2.2 CQRS 기반 명령(Command)/조회(Query) 책임 분리
쓰기와 읽기의 요구사항이 상이한 설문 도메인 특성에 맞춰 CQRS 패턴을 적용했습니다. 데이터 변경은 정규화된 명령 모델(cm_* 테이블)에서 수행하고, 조회는 화면에 최적화되어 비정규화된 조회 모델(qm_* 뷰)에서 처리합니다. 두 모델은 이벤트를 통해 동기화되어 결과적 일관성을 유지합니다.
이로써 복잡한 목록·검색 조회가 명령 트랜잭션에 부하를 주지 않으며, 조회 모델을 화면 요구에 맞게 자유롭게 비정규화할 수 있습니다. 명령의 대상 선별은 항상 최신값을 보장하는 명령 모델을 기준으로 수행하여 정합성을 확보했습니다.
2.3 멀티 테넌시와 소프트 삭제를 통한 데이터 격리·보호
여러 의료기관·진료 단위가 하나의 플랫폼을 공유하는 환경을 위해, 공통 베이스 엔티티가 테넌트 식별자(기관/진료부서/단계)를 모든 데이터에 자동으로 주입하도록 설계했습니다. 이를 통해 애플리케이션 코드의 개입 없이 테넌트 단위로 데이터가 격리됩니다.
또한 모든 엔티티는 물리적 삭제 대신 유효 여부 플래그를 이용한 소프트 삭제를 적용하여, 삭제된 데이터도 감사(audit) 추적이 가능하고 의료 데이터의 이력 보존 요건을 충족하도록 했습니다.
3. 주요 기능 구현 및 문제 해결 경험
3.1 설문 서식의 안전한 버전 관리 (Master/Snapshot 패턴)
설문 서식은 운영 중에도 수시로 수정되지만, 이미 발송되어 환자가 응답한 설문은 발송 당시의 서식과 정합성을 유지해야 한다는 까다로운 요구가 있었습니다. 이를 'Master/Snapshot' 패턴으로 해결했습니다.
• 편집(Master) : 작성자는 Master 서식을 자유롭게 편집합니다. Master는 현재 활성 스냅샷을 가리키는 포인터를 보유합니다.
• 공표(Publish) : 공표 시점에 Master의 내용을 복제한 불변(immutable) Snapshot을 생성합니다. 이후 발송되는 설문은 이 스냅샷에 고정됩니다.
• 응답 정합성 보장 : 환자의 응답은 항상 발송 시점의 스냅샷을 참조하므로, 서식이 이후 변경되어도 과거 응답의 의미가 훼손되지 않습니다.
공표 전 검증을 위한 미리보기 발행, 편집본을 Master에 병합하는 흐름 등을 함께 구현하여, 운영 중 무중단 서식 개정과 데이터 무결성을 동시에 달성했습니다.
3.2 설문 발송·배정 및 응답 수집 생명주기
설문 수행은 '배정 → 작업 → 상태'의 단계로 모델링했습니다. 배정(누가 누구에게 어떤 일정으로 보내는가)으로부터 개별 수행 단위인 작업이 생성되고, 작업의 진행 상태(미시작/임시저장/제출/완료 등)는 별도의 상태 컨테이너로 관리됩니다.
응답은 질문 유형에 따라 주관식은 값으로, 객관식은 선택 항목 집합으로 구분 저장하여 후속 채점·통계가 일관되게 처리될 수 있도록 정규화했습니다. 또한 설문 완료/강제 완료/만료 처리를 유스케이스로 명확히 분리하고, 스케줄러가 마감·만료·통신 종료를 자동으로 수행하여 운영 부담을 줄였습니다.
3.3 외부 레거시 의료정보 시스템 연동
기존 의료기관의 레거시 EMR과 연동하되, 마이크로서비스 간에는 물리적 외래키(FK)를 두지 않고 ID 기반의 논리 참조만 사용하는 원칙을 따랐습니다. 외부 시스템 연동은 별도의 어댑터 서비스를 REST(Feign)로 경유하도록 하여, 외부 시스템의 변경이 핵심 도메인에 직접 침투하지 못하도록 부패 방지 계층(ACL)을 형성했습니다.
레거시 시스템이 채번하는 식별번호를 응답 저장 시점에 매핑하여 양 시스템 간 데이터를 정합성 있게 연결했으며, 연동 구현을 전략(Strategy) 패턴으로 추상화하여 향후 다른 기관·시스템을 추가할 때 핵심 로직 수정 없이 확장할 수 있도록 했습니다.
4. 인프라 및 운영 효율화
4.1 Apache Kafka를 통한 서비스 간 결합도 해소
도메인에서 의미 있는 사건이 발생하면(서식 공표, 설문 발송, 응답 완료, 케이스 완료 등) 이를 도메인 이벤트로 발행합니다. 채점·통계 서비스와 알림·채팅 서비스는 필요한 이벤트만 구독하여 각자의 책임을 수행하므로, 서비스 간 직접 호출 의존이 제거되고 한쪽의 장애가 다른 쪽으로 전파되지 않습니다.
반대로 사용자·조직 정보의 변경 이벤트는 본 서비스가 구독하여 조회 모델을 갱신함으로써, 마스터 데이터와의 결과적 일관성을 유지합니다. 표준 의료정보 교환 형식(FHIR) 변환을 위한 이벤트도 함께 발행하여 외부 연계 확장성을 확보했습니다.
4.2 운영 안정성 인프라 (Redis · ShedLock · Outbox)
• Redis 캐싱 : 자주 조회되는 공통 데이터를 캐싱(TTL 적용)하여 조회 성능을 높이고 데이터베이스 부하를 줄였습니다.
• ShedLock : 다중 인스턴스 환경에서 마감·발송 등 스케줄러가 중복 실행되지 않도록 분산 락으로 작업의 유일성을 보장했습니다.
• Outbox 패턴 : 공통 라이브러리의 아웃박스 패턴을 활용해, 데이터 변경과 이벤트 발행을 동일 트랜잭션으로 묶어 이벤트 유실 없는 신뢰성 있는 발행을 구현했습니다.
4.3 보안 및 인증
외부 진입점은 OAuth2 Resource Server로 보호하여 검증된 토큰(JWT)만 허용하고, 역할 기반 접근 제어로 환자·의료진·운영자·관리자 권한을 분리했습니다. 서비스 간 내부 호출은 client_credentials 방식의 서비스 토큰을 Feign 인터셉터가 자동 주입하여, 비즈니스 로직이 인증 코드로부터 분리되도록 설계했습니다. 데이터베이스 접속 정보 등 설정의 민감 값은 암호화(Jasypt)하여 보관하고, 자격증명·내부 엔드포인트는 운영 환경 변수로만 주입하여 소스에 노출되지 않도록 관리합니다.
4.4 계약 기반 모듈 배포 전략
타 서비스가 본 서비스를 호출하기 위한 통신 계약(Feign 인터페이스)과 구독할 이벤트 스키마를 각각 client/event 모듈로 분리하여 사내 아티팩트 저장소에 배포합니다. 소비 측 서비스는 이 아티팩트를 의존성으로 가져와 사용하므로, API 변경이 컴파일 시점에 드러나 연동 안정성이 높아집니다.
5. 결론 및 성과
설문 서비스는 설문의 정의부터 수집까지 전 생명주기를 단일 책임으로 응집하면서도, 채점·알림 등 후속 처리는 이벤트로 분리하여 확장성과 안정성을 동시에 확보한 사례입니다.
• 데이터 무결성 : Master/Snapshot 패턴으로 서식이 변경되어도 과거 응답의 정합성을 보존했습니다.
• 결합도 해소 : Kafka 이벤트 기반 비동기 통신으로 서비스 간 결합도를 낮추고 장애 격리를 달성했습니다.
• 확장성 확보 : 헥사고날 멀티 모듈과 멀티 테넌시 설계로, 신규 연동 대상이나 도입 기관이 늘어나도 핵심 로직 수정 없이 유연하게 대응할 수 있는 기반을 마련했습니다.
본 프로젝트를 통해 확보한 도메인 중심 설계, CQRS, 이벤트 기반 아키텍처 경험은 향후 복잡한 도메인을 다루는 다양한 시스템 설계와 구축에 핵심 자산이 될 것입니다.
conley