실습 로그 / 산학 실시간 심전도 처리 서버

산학 2단계 · 서버 연결 방식 설계

생성기에서 나간 심전도가 서버까지 어떻게 갈지, 어떤 형식으로 보낼지 수신부 담당 팀원과 정한다.

진행 과정은 실습 내용을 AI와 함께 정리했고, 배운 점은 그걸 보고 제가 이해한 대로 직접 썼습니다. 배운 점 바로가기 ↓

네트워크

목표

생성기에서 나간 심전도가 서버까지 어떻게 갈지, 어떤 형식으로 보낼지 수신부 담당 팀원과 정한다. (10.5)

구성

생성기 → TCP 8000 (NDJSON) → 수신부 → Redis Streams → 워커 1개 (채널별 10초 모음) → R파 검출 → AI 판정
  • 수신부: 상자를 받아 검사하고 큐에 넣는 접수 창구
  • Redis Streams: 상자를 줄 세워두는 컨베이어(큐). 몰려도 서버가 안 죽게 해줌
  • 워커: 큐에서 상자를 꺼내 분석하는 일꾼
  • 채널: 환자 한 명의 심전도 줄. 채널 = 환자로 봐도 됨
  • R파: 심전도에서 가장 뛰어오른 뾰족한 봉우리. 박동 하나에 하나

A. 전송 방식: TCP

  • 연결을 한 번 열어두고 계속 보냄. 순서가 지켜지고 중간에 빠지면 다시 보내줌
    • TCP: 컴퓨터끼리 데이터를 주고받는 규칙 중 하나. 도착 순서와 누락 재전송을 보장함. 반대로 UDP는 빠르지만 빠져도 모름
    • 8000: 포트 번호. 건물(컴퓨터) 안의 문 번호라고 보면 됨
  • 여러 환자 메시지를 한 연결로 이어서 보내기 때문에 메시지 사이 경계가 필요 → NDJSON

자세히 보기

비유는 “심전도 택배 물류센터”로 통일함.

1. 연결 방식을 왜 먼저 정해야 하나

  • 보내는 쪽(내 생성기)과 받는 쪽(수신부)을 다른 사람이 만듦. 도로와 송장 양식을 안 정하면 각자 만들고 나서 안 맞음
  • 정해두면 상대가 끝날 때까지 기다리지 않고 동시에 만들 수 있음 (실제로 10.8에 따로 만든 두 코드가 바로 붙음)

2. TCP = 도로

  • 한 번 길을 뚫어두면(연결) 상자가 보낸 순서대로, 빠짐없이 도착함. 중간에 사라지면 TCP가 알아서 다시 보냄
  • 8000 = 문 번호(포트). 수신부가 8000번 문 앞에서 기다리고, 생성기가 그 문으로 찾아감
  • 환자마다 도로를 따로 내지 않고 도로 하나에 전원 상자를 실음
  • 응답(“잘 받았어”)을 안 받기로 한 이유: 한 건마다 답을 기다리면 왕복 시간이 매번 붙음. 1ms만 잡아도 1초에 1,000건이 한계인데 환자 100명이 딱 1초에 1,000건이라 여유가 0. 빠진 건 송장 번호로 셈

B. NDJSON

  • 한 줄에 JSON 하나, 줄바꿈(\n)이 메시지 끝 표시
    • JSON: {"이름": 값} 모양으로 데이터를 적는 글자 형식. 사람도 읽을 수 있음
    • NDJSON: Newline Delimited JSON. 줄바꿈으로 구분한 JSON
  • 받는 쪽은 줄 단위로 자르기만 하면 됨

자세히 보기

3. NDJSON = 상자 사이 칸막이

  • 도로 하나에 상자가 줄줄이 붙어서 오니까 어디서 끊을지 약속이 필요함
  • “줄바꿈 = 상자 하나 끝”. 받는 쪽은 줄 단위로 자르기만 하면 됨
  • 덕분에 wc -l(줄 수 세기)만 해도 몇 개 왔는지 알 수 있음 (3단계)

C. 메시지 형식 (임시안)

{"channel_id": 0, "seq": 0, "t_start": 0.0, "sent_at": 1760000000.0, "fs": 250, "duration": 0.1, "leads": ["V4", "V3", "II"], "samples": [[...25개], [...], [...]]}
항목 뜻 쓰는 곳
channel_id 환자(채널) 번호 누구 것인지, 워커가 채널별로 모을 때
seq 채널별 순번 빠진 번호 = 유실
t_start 이 조각의 측정 시작 시각 순서 맞추기
sent_at 보낸 시각 지연 계산
fs, duration 샘플링 속도, 조각 길이 받는 쪽에서 검사
leads, samples 리드 이름과 값 R파 검출 입력
  • 리드: 전극 조합으로 본 심장 방향. 3리드면 세 방향에서 본 그래프 3줄
  • samples: 리드별 숫자 25개씩. [[25개], [25개], [25개]] = 3줄
  • 임시안: 기업 장비 문서가 아직 없어서 우리끼리 정한 것. 문서 오면 바뀔 수 있음

자세히 보기

4. 상자 하나 뜯어보기

{"channel_id": 3, "seq": 57, "t_start": 5.7, "sent_at": 1791432323.07,
 "fs": 250, "duration": 0.1, "leads": ["V4","V3","II"], "samples": [[25개],[25개],[25개]]}
칸 택배로 치면 왜 필요한가
channel_id 받는 사람(환자 번호) 누구 심전도인지. 워커 배정 기준이기도 함
seq 송장 번호 0, 1, 3이 오면 2번이 빠진 거 → 유실을 셀 수 있음
sent_at 발송 시각 받은(처리 끝난) 시각 − 발송 시각 = 지연
t_start 상자 안 내용물의 날짜 파형상 시각. 파일을 재생하니까 보낸 시각과 다름
fs, duration 내용물 규격 1초에 250번 측정, 이 상자는 0.1초 분량. 받는 쪽에서 검사용
leads, samples 내용물 리드 3개 × 25개 숫자. leads 순서가 samples 줄 순서

D. 측정 방식

  • 지연 = 워커 처리 완료 시각 − sent_at, p95(느린 쪽 5%를 뺀 최대값)로 봄
    • p95: 100개를 빠른 순서로 줄 세웠을 때 95번째. 평균은 아주 느린 몇 개를 가려버려서 p95를 봄
    • p50: 중간값. 절반은 이것보다 빠름
  • 유실 = 채널별 seq가 빈 개수
    • 예: 환자 3번 상자가 0, 1, 2, 4로 오면 3이 빠짐 → 유실 1
  • 개발 중에는 생성기도 같은 VM에서 돌림 → 시계가 같아야 시각 빼기가 맞음

자세히 보기

5. 예상 질문

  • seq, sent_at은 왜 넣었나? → seq는 순번이라 빠진 번호로 유실을 세고, sent_at은 보낸 시각이라 받은 시각에서 빼서 지연을 잼. 둘 다 없으면 부하테스트 자체가 불가능
  • 생성기를 서버랑 같은 VM에서 돌리는 이유? → 시계가 같아야 sent_at 빼기가 맞음. 개발 단계는 같이, 최종 측정만 분리 (조교 피드백)
  • 왜 MQTT 같은 걸 안 썼나? → 기업 장비가 실제로 뭘 쓰는지 문서가 아직 없음. 추측으로 정하지 않고 가장 단순한 TCP로 임시 연결. 문서 오면 수신부의 전송 부분만 교체

배운 점

직접 작성

목표

  • 생성기에서 나간 심전도가 서버까지 어떻게 갈지, 어떤 형식으로 보낼지 수신부 담당 팀원과 협의
    • 보내는 쪽과 받는 쪽이 같은 규칙을 써야 연결가능
    • 나 = 보내는 쪽 (생성기) , 팀원 = 받는 쪽 (수신부)
  • 전체 흐름 : 생성기 → TCP 8000 (NDJSON) → 수신부 → Redis Streams → 워커 → R파 검출 → AI판정
    • 수신부 : 상자를 받아서 검사하고 큐에 넣는 접수 창구
    • redis Stream : 상자를 줄 세워두는 큐
      • 상자가 한 번에 몰려와도 서버가 안 죽게 해줌
    • 워커 : 큐에서 상자를 꺼내 분석해줌
    • 채널 : 환자 한 명의 심전도 줄 (=환자)
    • R파 : 심전도에서 가장 높이 뾰족하게 솟은 봉우리.
      • 박동 하나씩 존재

A. 전송 방식 : TCP

  • 목적 : 생성기와 서버를 잇는 통신 방식 결정
    • 심전도는 중간에 빠지거나 순서가 섞이면 판정이 틀어진다.
      • 기업도 틀리는 것보다 놓치는 걸 더 치명적으로 봤다.
  • 설명 : 연결을 한 번 열어두고 그 위로 계속 보냄. 순서가 지켜지고 중간에 빠지면 다시 보냄
    • TCP : 컴퓨터끼리 데이터를 주고받는 규칙.
      • 도착 순서와 빠진 것을 재전송 보장
    • UDP : TCP의 반대
      • 빠르지만 빠져도 모름 —> 이번 프로젝트와는 어울리지 않음
    • 연결을 열어두는 이유 : 0.1초마다 새로 연결하면 연결하는 데 시간이 계속 듦
      • 연결을 열어둔다는 뜻은 TCP는 데이터를 보내기 전에 요청→응답→확인 과정을 거치는데 그 인사를 모든 상자에서 하지 않고 한 번 연결한 뒤 끊지 않고 계속 사용
    • 포트 8000 : 컴퓨터 안의 문 번호. (우리가 정한 번호)
      • 서버가 8000번 문을 열어두고 기다린다.
    • 문제 : 연결 하나로 여러 환자 상자를 이어보내니깐 상자 사이 경계가 필요함

B. NDJSON

  • 목적 : 연결 하나로 이어서 보내는 상자들 사이에 경계를 정함

    • TCP는 데이터를 순서대로 흘려보내기만 하고 상자 하나가 어디까진지 설명해주지 않는다.
      • 문제점 : 받는 쪽이 상자를 나누지 못 한다
  • 설명 : 상자 하나를 한 줄로 적고 줄바꿈을 상자의 끝 표시로 사용한다. 수신부에서도 줄바꿈이 나올 때마다 자르면 됨

    • JSON : {”이름”: 값} 모양으로 데이터를 적는 글자 형식
    • NDJSON : Newline Delimited JSON
      • 줄바꿈으로 구분한 JSON
      • 한 줄 = JSON 하나 = 상자 하나 = 환자 한 명의 0.1초치 (환자 1명 전체가 아님)
        • 환자 한 명이 0.1초마다 상자를 하나씩 계속 보냄 —> 1초에 10개
    • 줄바꿈(\n) : 화면엔 보이지 않지만 글자로는 들어가져 있음
  • EX

      {환자0, 0.0~0.1초}\n
      {환자1, 0.0~0.1초}\n
      {환자0, 0.1~0.2초}\n
      {환자1, 0.1~0.2초}\n
    
    • 받는 쪽은 \n 기준으로 잘라서 상자 4개로 나눔 (환자0 거 2개, 환자1 거 2개)
      • 줄바꿈은 상자를 나누고 , 환자는 channel_id로 나눔 (C단계)

C. 메시지 형식 (임시)

  • 목적 : 상자에 붙일 송장 양식을 수신부 팀원과 결정

    • 줄바꿈으로 상자는 나눌 수 있어도, 누구 건지, 몇 번째인지, 언제 보냈는지 확인 불가능
      • 받는 쪽이 분류하고 검사하고 측정할라면 필요
    • 임시안으로 결정. 문서 오고 추후 추가할거나 제거할 거 결정
  • 설명 : 상자 하나 (한 줄)에 정한 값 —> 스키마

    • channel_id : 환자 번호
      • 누구 건지 확인 가능. 워커가 환자별로 모을 때 사용
    • seq : 환자별 순번 0,1,2 …
      • 중간에 빠진 번호 있으면 유실
    • t_start : 이 상자가 심전도의 몇 초 지점부터인지
      • 순서 확인
    • sent_at : 보낸 시각
      • 받은 시각에서 빼면 지연 (D에서 사용)
    • fs, duration : 250Hz, 0.1초
      • 받는 쪽이 개수가 맞는지 검사
    • leads, samples : 리드 이름과 실제 값
      • R파 검출에 들어가는 값
      • 3리드라서 samples = [[25개], [25개], [25개]] = 리드 3개 X 25개
    • 스키마 : 이렇게 칸을 정한 양식을 부르는 공식 용어
      • 스키마 = 빈 양식 / 메시지 = 양식에 값을 채운 것
      • schema_version : 양식 버전 번호. 칸을 추가·삭제하거나 단위를 바꿀 때만 올림 (메시지마다 올라가는 건 seq)
        • 받는 쪽이 버전을 보고 옛날 양식이랑 새 양식을 구분해서 읽으려고 씀
  • EX

    {"channel_id": 0, "seq": 2, "t_start": 0.2, "sent_at": 1760000000.2, "fs": 250, "duration": 0.1, "leads": ["V4", "V3", "II"], "samples": [[25개], [25개], [25개]]}
    
    • 이 코드는 생성기 코드 (send 함수)가 0.1초마다 환자별로 자동으로 만들어서 보낸다.
      • 터미널에선 python stream_multi.py 100 만 작성
      • send 함수는 3단계에서 채움

D. 측정 방식

  • 목적 : 서버가 잘 버티는지 판단할 기준을 미리 정함
    • 기준 없이 돌리면 잘 되는 것처럼 보임. 기준을 정해야 5단계에서 몇 명까지 버티는지 말할 수 있음
  • 설명 : 지연이랑 유실 두 가지를 측정. 둘 다 sent_at, seq으로 계산
    • 지연 : 워커가 처리를 끝낸 시각 빼기 sent_at (보낸 시각) = 보내고 나서 처리까지 걸린 시간
      • ex. 10.00초에 보내고 10.35초에 끝나면 지연 0.35초
    • 유실 : 환자별로 seq가 빈 개수
      • ex. 환자 3번 상자가 0,1,2,4로 도착 —> 3빠졌으므로 유실 1
    • p95 : 100개를 빠른 순서로 줄 세웠을 때 95번째 값.
      • 느린 쪽 5%를 빼고 가장 느린 값
      • 평균을 사용하면 아주 느린 몇 개로 가려버려서 환자 몇 명이 늦게 받아도 평균은 괜찮아 보임
      • 기업도 부하테스트는 p95을 보라고 함
    • p50 : 중간값.
      • 절반은 이것보다 빠름
    • 같은 컴퓨터로 실행
      • 지연은 두 시각을 빼는 거므로 시계가 같아야 정확