1. 도입: 기획 화면을 서버 아키텍처로 번역하다
프로젝트의 시작은 팀원이 그려온 기획 화면(사진)들이었다. 스마트워치(Edge 기기)에서 수집된 피보호자의 생체 데이터(심박수, 호흡수, 낙상 여부)를 실시간으로 모니터링하고, 비정상 수치 발생 시 보호자에게 즉각적인 알림을 보내는 시스템을 구축해야 했다.
단순한 CRUD 게시판이 아니라 1초마다 쏟아지는 센서 데이터를 어떻게 안정적으로 처리하고 화면에 뿌려줄 것인가? 이 질문에서부터 아키텍처 고민이 시작되었다.
2. 데이터 성격에 따른 처리 주체 분리 (Edge vs Server)
가장 먼저 고민한 것은 "긴급 상황을 누가 판단할 것인가?" 였다.
- 낙상 감지 (Edge Computing): 초당 수십 번씩 변하는 자이로스코프의 원시 데이터(Raw Data)를 서버가 다 받을 수는 없다. 따라서 기기가 직접 연산하여 isFallDetected = true라는 '결과값'만 던져주면, 백엔드는 이를 100% 신뢰하고 즉시 알림을 생성하도록 설계했다.
- 심박수/호흡수 이상 (Server-side Rule Engine): 반면, 심박/호흡은 병원 정책이나 환자 상태에 따라 '정상 기준'이 수시로 바뀔 수 있다. 따라서 기기는 '수치'만 던지고, 백엔드에서 60~120이라는 비즈니스 룰을 적용해 이상 여부를 판단하도록 역할을 명확히 분리했다.
3. 객체 지향(OOP)을 활용한 클린 코드: "룰은 엔티티가 스스로 알게 하라"
기능을 구현하다 보니 EmergencyEventService(알림 생성)와 UserProfileService(화면 조회) 양쪽에서 '심박수 60~120 비교 로직'이 중복해서 사용되는 것을 발견했다.
DRY(Don't Repeat Yourself) 원칙을 지키기 위해, 판단 로직을 서비스 클래스에서 빼내어 데이터의 주인인 Vital 엔티티 내부의 메서드(isHeartBreathNormal())로 캡슐화했다. 마찬가지로 "FALL = 낙상이 감지되었습니다" 같은 문자열 변환 로직도 EmergencyEvent 엔티티로 응집시켜, 불필요한 서비스 간의 의존성(순환 참조 위험)을 제거하고 유지보수성을 극대화했다.
4. 실시간 데이터 파이프라인과 Two-Track 저장 전략
센서에서 데이터가 들어올 때의 플로우를 다음과 같이 컨베이어 벨트처럼 4단계 파이프라인으로 설계하고, 이를 지휘하는 VitalProcessingService(총괄 매니저)를 도입했다.
- 안정화 (Filter): 날것의 센서 데이터에서 튀는 값(Noise)을 깎아내는 알고리즘 적용
- 실시간 전송 (SSE): 프론트엔드 그래프 렌더링을 위해 가공된 데이터를 지연 없이 스트리밍
- 긴급 검사: 수신 즉시 룰을 체크하여 긴급 상황이면 DB에 알림(EmergencyEvent) 생성
- 최종 저장: 생체 데이터(Vital) DB 저장
🔥 트래픽 최적화 포인트 (Two-Track 전략): 1초마다 들어오는 모든 정상 생체 데이터를 그대로 DB에 INSERT 하면 부하가 엄청나다. 따라서 긴급 상황은 1초마다 실시간으로 검사 및 알림을 보내되, 정상 데이터는 메모리(Buffer)에 들고 있다가 Spring @Scheduled를 활용해 10초 주기로 한 번에 DB에 밀어 넣는(Batch Insert) 투트랙 아키텍처를 도입해 DB I/O를 획기적으로 줄였다.
5. 트러블슈팅: 404 에러와 DB 페이징의 함정
개발 중 겪었던 두 가지 재미있는(?) 삽질과 해결 과정이다.
- API 404 Not Found의 비밀: Postman으로 긴급 알림 상세 조회 테스트 중 권한(401/403)이나 서버 에러(500)가 아닌 404 에러를 마주했다. 원인은 컨트롤러의 클래스 레벨 @RequestMapping과 메서드 레벨 @GetMapping이 합쳐지는 과정에서 주소 경로 맨 뒤의 /detail을 호출 시 누락했던 단순 오타였다. (덤으로 @GetMapping 내부 문자열 시작 시 안전하게 /를 붙이는 컨벤션도 맞추었다.)
- 사라진 최신 데이터 미스터리: 분명 서버 코드는 OrderByRecordedAtDesc로 최신 데이터를 가져오게 짰는데, DB GUI 툴(Supabase)에서는 최신 데이터가 안 보였다. 알고 보니 서버 버그가 아니라, 브라우저 렌더링 부하를 막기 위해 GUI 툴 자체가 100건 단위로 페이징(Pagination) 처리를 해서 보여주고 있었기 때문이었다. 이는 역으로 나중에 프론트엔드에게 통계 리스트를 내려줄 때 왜 페이징 처리가 필수적인지 뼈저리게 깨닫는 계기가 되었다.
6. 결론: 협업을 위한 읽기/쓰기(Read/Write) 분리
팀원과 업무를 나눌 때, 생체 데이터 처리는 내가 맡고 화면 통계 API는 팀원에게 위임했다. 이것이 가능했던 이유는 데이터를 가공해서 밀어 넣는 파이프라인(Write)과 저장된 데이터를 꺼내보는 통계(Read)의 역할을 완벽하게 분리했기 때문이다. 팀원에게는 "데이터를 자바 메모리로 다 가져오지 말고, 무조건 DB 단에서 JPQL로 AVG, MAX, GROUP BY 계산을 끝내서 응답해라!"라는 성능 최적화 가이드라인도 함께 전달했다.
이제 뼈대는 완벽하게 세워졌다. 다음 목표는 실시간 센서 데이터의 노이즈를 효과적으로 제거할 '데이터 안정화 알고리즘'을 구현하여 파이프라인의 첫 단추를 완성하는 것이다!
'개발 > 백엔드' 카테고리의 다른 글
| 🚀 [Spring Boot] IoT 헬스케어 시스템의 실시간 데이터 파이프라인 구축기 (SSE + 데이터 샘플링) (0) | 2026.05.10 |
|---|---|
| [Spring Boot] IoT 실시간 모니터링: SSE 통신 원리와 Optional 리팩토링 경험기 (1) | 2026.04.16 |
| [JPA/Spring] 엔티티 설계의 정석: @NoArgsConstructor(access = AccessLevel.PROTECTED)와 @Builder의 조화 (0) | 2026.03.11 |
| 멀티캠퍼스 [현대 이지웰] Spring Boot 활용하여 기본 CRUD 기능 구현 (0) | 2025.12.01 |
| 멀티캠퍼스 [현대 이지웰] 인터페이스 기반 프로젝션이란? (0) | 2025.11.24 |