GroupFlow CTI 제작기 — 레거시 소프트폰을 리버스 엔지니어링해 웹으로

2026.09.15

·

20년 된 Windows 전용 CTI 소프트폰을, 사내 그룹웨어 안에서 동작하는 웹 네이티브 전화 기능으로 옮긴 과정을 정리했습니다. 프로토콜을 패킷부터 뜯어보고, “암호화”의 정체를 확인하고, 결국 아키텍처를 다시 그린 이야기입니다.

상담팀 PC마다 오래된 소프트폰 하나가 떠 있었습니다. 전화가 오면 발신자 정보를 띄우고, 클릭으로 전화를 걸고, 통화 이력을 보여주는 — 이른바 CTI(Computer Telephony Integration) 클라이언트입니다. Delphi로 만들어진 Windows 전용 ActiveX 덩어리였고, 소스는 없었고, 만든 곳과는 연이 끊긴 지 오래였습니다.

문제는 이게 블랙박스라는 점이었습니다. 그룹웨어는 웹으로 옮겨가는데 전화만 낡은 데스크톱 앱에 묶여 있었고, “받은 전화 화면에 띄우기” 같은 걸 그룹웨어에서 하려면 이 앱이 대체 교환기와 무슨 대화를 나누는지부터 알아야 했습니다. 문서는 일부 있었지만 실제 동작과 맞지 않는 곳이 많았습니다.

Sponsored

그래서 뜯어보기로 했습니다.


진실은 문서가 아니라 패킷에 있다

소프트폰은 사내에 있는 교환기(IP-PBX)의 이벤트 포트로 TCP 연결을 맺고 있었습니다. 평문 로그인을 시도해 보니 바로 거부당했습니다 — 뭔가 인코딩/암호화가 걸려 있다는 신호였죠.

가장 확실한 건 실제 트래픽입니다. 소프트폰이 도는 (심지어 tcpdump도 없는 구형) Windows PC에서 raw 캡처 도구로 패킷을 떴습니다. 그리고 바이트를 노려봤습니다.

프레임 구조는 생각보다 정직했습니다. [2바이트 타입][2바이트 길이][본문] — 리틀엔디언 헤더에 본문이 붙는 전형적인 TLV였습니다. 타입 값 몇 개를 분류하니 클라이언트 명령, 서버 응답, 그리고 주기적으로 오가는 평문 keepalive(“서버 살아있니”)가 구분됐습니다. 여기까지는 순조로웠습니다.

문제는 본문이었습니다. 로그인 명령의 본문이 무의미한 바이트 뭉치로 보였거든요.


“SEED”라고 적혀 있었지만, SEED가 아니었다

라이브러리 문자열과 상수 덤프를 보면 힌트가 나옵니다. 처음엔 Feistel 구조의 블록암호처럼 보여 Blowfish를 의심했고, 상수를 더 파보니 국산 표준 블록암호(SEED)의 흔적이 잡혔습니다. “아, SEED구나” 하고 Node로 SEED를 재구현해 RFC 시험 벡터까지 통과시켜 놨습니다.

그런데 그 SEED로 실제 캡처를 풀면 — 안 풀렸습니다.

여기서 배운 게 있습니다. 코드에 어떤 암호 루틴이 들어 있다는 것과, 그 경로가 실제로 쓰인다는 건 다른 얘기입니다. 라이브러리에는 SEED 경로도 있었지만, 이 소프트폰이 켜 둔 “암호화 모드”는 그 경로가 아니었습니다.

캡처를 붙잡고 인코더 관점에서 거꾸로 맞춰보니, 실제로 켜진 건 암호화라 부르기 민망한 수준의 가역적 난독화였습니다. 표준 블록암호라는 겉포장과 달리, 알고 나면 몇 줄로 인코더/디코더를 재현할 수 있는 방식이었죠. 여기서 정확한 방법을 적지는 않겠습니다 — 상용 제품의 취약점을 그대로 공개하는 건 결이 다른 문제라, 우리는 상호운용을 위한 최소한만 재현했습니다.

교훈 하나. “국산 표준 암호 적용”이라는 라벨이 실제 보안을 보장하진 않는다. 둘. 리버스 엔지니어링에서 신뢰할 유일한 원천은 라이브러리 문자열이 아니라 실제로 흘러간 바이트다. 문서와 상수는 우리를 두 번이나 엉뚱한 데로 데려갔고, 매번 우리를 되돌린 건 캡처 파일이었다.

코덱을 확보하자 나머지는 빠르게 풀렸습니다. 로그인 → 핸드셰이크(평문 keepalive 한 방 + 상태 구독 명령) → 그 후부터 수신 이벤트가 실시간으로 밀려오기 시작했습니다. “누가(발신번호) 어느 내선으로 전화를 걸었다”가 이벤트 한 줄로 도착했습니다. 이 한 줄이 우리가 원하던 전부였습니다 — 화면에 팝업을 띄우기 위한.


두 개의 채널, 서로 다른 성격

이 교환기는 LG U+의 기업 인터넷전화 플랫폼 DCS였습니다. DCS와 대화하는 길은 둘이었습니다 — 하나는 앞서 뜯어낸 이벤트 소켓(문서에 없던 비공개 프로토콜), 다른 하나는 DCS가 문서로 제공하는 REST API였습니다.

  • 이벤트 소켓: 실시간. 전화가 울리는 바로 그 순간을 알려줍니다. 대신 과거를 묻지는 못합니다.
  • REST API: 요청-응답. 통화 이력, 녹취 목록, 전화 걸기, 문자, 착신전환 — “지금 상태”와 “과거”를 조회/조작합니다.

처음엔 욕심을 냈습니다. 이벤트 소켓으로 들어오는 통화를 우리 DB에 차곡차곡 적재해 자체 통화 이력을 만들자고. 그런데 이 방식은 세 군데서 새고 있었습니다. 서버가 붙어 있는 동안만 기록되고, 우리가 감시하는 내선만 잡히고, 과거 백필이 안 됐습니다. 실제로 테스트 통화 몇 건이 “울리는 중” 상태로 멈춰버리는 걸 보고 마음을 접었습니다.

그때 팀에서 나온 한마디가 방향을 바꿨습니다. “교환기에 이미 다 있는 걸, 우리가 왜 또 쌓아요?”

맞는 말이었습니다. 교환기는 모든 통화를 이미 자기 원장에 기록합니다 — 그게 원천(source of truth)입니다. 우리가 만들 부분 복사본은 언제나 원천보다 불완전할 수밖에 없었습니다. 그래서 통화 이력은 적재를 버리고 조회로 전환했습니다. 이벤트 소켓은 “실시간 팝업”이라는, 조회로는 절대 할 수 없는 일 하나에만 집중시켰습니다.

통합 시스템을 만들 때 가장 흔한 실수는, 이미 원천이 있는 데이터를 굳이 자기 DB에 복제하는 것입니다. 복사본은 반드시 드리프트합니다.


발신자가 누구인지 아는 법

전화가 울릴 때 화면에 뜨는 건 번호가 아니라 사람이어야 합니다. 그래서 발신번호를 두 축으로 해석했습니다. 내부 번호(내선)는 사내 디렉터리(LDAP)의 직원으로, 외부 번호는 고객 마스터로. 여기서도 원칙은 같았습니다 — 번호·내선은 교환기가 원천, 이름·부서는 디렉터리가 원천, 둘을 사번(uid)으로 조인.

수신 통화 상세 패널 — 녹취 재생·전사와 LDAP 담당자·부서 매칭 (개인정보는 모자이크 처리)
발신번호가 사람으로 해석되어 뜬다. 담당자·부서는 LDAP에서, 녹취는 교환기에서 — 각자의 원천에서 끌어와 한 화면에 합친다. (실명·전화번호는 모자이크 처리)

이 “원천을 나눠서 조인” 원칙은 나중에 재미있는 걸 잡아냅니다. 어떤 직원의 이름이 교환기엔 한 글자 다르게(오타로) 들어가 있었던 거죠. 디렉터리와 대조하는 정합 점검을 붙였더니 바로 튀어나왔습니다.


느린 교환기와 싸우기

여기서 현실이 끼어듭니다. 교환기의 REST가 느렸습니다. 요청 하나에 수 초, 그것도 들쭉날쭉. 통화 이력 화면은 “기간 × (수신+발신+녹취)”만큼 요청을 날리니, 며칠치만 봐도 로딩이 답답했습니다.

통화 이력 화면 — 기간 필터와 수신·발신·부재중 목록 (개인정보는 모자이크 처리)
통화 이력은 “기간 × 여러 종류”의 조합이라 요청 수가 금세 불어난다. 느린 REST 위에서 이걸 어떻게 빠르게 보여줄 것인가가 과제였다.

원인을 실측으로 좁혔습니다. 순차로 6번 요청하면 20초 가까이 걸리는데, 연결을 재사용하면 같은 6번이 1초 미만이었습니다. 범인은 매 요청마다 새로 맺는 TLS 핸드셰이크였습니다. 클라이언트가 연결을 매번 새로 열고 있었던 거죠. keepAlive를 켜 연결 풀을 재사용하게 하니 즉시 개선됐습니다.

그다음은 캐시였습니다. 핵심 통찰은 단순합니다 — 지난 날짜의 통화는 변하지 않는다. 어제의 통화 이력이 오늘 바뀔 리 없으니, 과거 날짜는 무기한 캐시해도 안전합니다. 오늘만 짧게, 과거는 길게. 여기에 캐시를 메모리가 아니라 DB에 영속시켜서, 서버를 재시작해도 과거 이력은 즉시 뜨게 했습니다. 마지막으로 “오늘분”은 백그라운드에서 주기적으로 데워, 사용자가 열 때는 이미 준비되어 있게 했습니다.

결과적으로 30일치 조회가 수십 초에서 0.2초 아래로 떨어졌습니다. 재시작 후 콜드 로딩도 사라졌고요. 화려한 최적화는 없습니다. “변하지 않는 것은 다시 묻지 않는다”는 원칙 하나였습니다.


사람이 스스로 등록하게

전화 기능을 전 상담원으로 확대하는 단계에서 운영 문제가 하나 있었습니다. 관리자가 모든 사람의 교환기 비밀번호를 알 수는 없다는 것. 그리고 우리 서버가 어떤 내선의 세션을 점유하면, 그 사람이 쓰던 기존 소프트폰이 튕긴다는 것.

전화(CTI) 설정 화면 — 내 전화 등록·수신 팝업·착신전환·내선 현황 (개인정보는 모자이크 처리)
각 직원이 자기 내선과 비밀번호를 직접 등록한다. 비밀번호는 내선별로 암호화 보관하고, 소켓·REST 모두 그 내선의 자격증명을 쓴다. (실명·직통번호는 모자이크 처리)

답은 자가 등록이었습니다. 각 직원이 그룹웨어 설정에서 자기 내선번호와 비밀번호를 넣으면, 서버가 교환기로 검증한 뒤 등록합니다. 비밀번호는 암호화해 내선별로 보관하고, 소켓이든 REST든 그 내선의 자격증명을 쓰게 했습니다. 덕분에 “비밀번호가 다른 내선도 동작”하고, “기존 소프트폰이 끊기는 것도 본인이 선택”하게 됐습니다. 확대는 관리자의 일괄 스위치가 아니라, 준비된 사람부터 스스로 켜는 방식으로 자연스러워졌습니다.

이 과정에서 값진 교훈도 하나. 한 내선이 등록 후 수신이 안 됐는데, 우리 시스템을 완전히 떼어내도 여전히 안 울렸습니다 — 알고 보니 그 사이 바뀐 비밀번호 때문에 책상 위 하드폰의 SIP 등록이 풀린 거였죠. 전화기 재부팅으로 끝. 통합을 만들다 보면 우리 코드 밖의 물리 세계가 원인인 경우가 반드시 나옵니다. “우리를 빼도 재현되는가”를 물어보는 게 가장 빠른 분리 진단이었습니다.


그리고 열린 것들

원천을 조회로 삼는 구조가 자리를 잡자, 그 위에 기능이 빠르게 쌓였습니다.

  • 실시간 수신 팝업: 이벤트 소켓 → 서버 → SSE로 각자 화면에.
  • 녹취 재생: 교환기의 녹취 파일을 백엔드가 프록시해 그대로 재생·다운로드.
  • 녹취 전사: 녹취 음성을 STT(Whisper)로 텍스트화. 한국어 8kHz 전화음성인데도 정확도가 쓸 만했습니다. 이제 통화 내용으로 검색이 됩니다.
  • 통화 태그·부재중 후속: “처리완료/콜백필요”를 통화에 붙여, 놓친 전화를 후속 관리.
  • 대시보드 위젯: 오늘 수신/발신/부재중/후속필요를 한눈에.
통화 상세에서 바로 문자(SMS/LMS) 발송 (개인정보는 모자이크 처리)
통화라는 이벤트를 사내 데이터·업무 흐름과 엮으면, 문자 발송·녹취 전사·태그 같은 기능이 같은 화면 위에 자연스럽게 붙는다. (수신자·본문은 모자이크 처리)

전화를 “웹으로 옮긴다”는 건 결국 화면 하나를 옮기는 게 아니라, 통화라는 이벤트를 사내 데이터·업무 흐름과 엮는 일이었습니다.


이번엔 LG U+ DCS였지만

이번 교환기는 LG U+ DCS였습니다. 하지만 우리가 실제로 한 일은 “DCS를 다루는 법”이 아니라, 낯선 인터페이스를 관측으로 파악하고, 원천을 조회로 삼아 사내 데이터와 엮는 법이었습니다. 대상이 DCS라서 된 게 아니라, 대상이 무엇이든 되는 절차를 만든 것입니다.

그래서 대상이 바뀌어도 순서는 같습니다. API 문서·엔드포인트·기능 명세만 주어지면 — 다른 통신사의 교환기든, 사내에서 굴러가는 레거시 ERP·그룹웨어·현장 장비든 — 같은 방식으로 붙입니다. 실시간이 필요한 건 이벤트로, 원장이 있는 건 조회로, 사람으로 보여야 하는 건 디렉터리와 조인해서. 그리고 문서가 아예 없다면? 이 글 앞머리에서 했던 것처럼, 패킷부터 뜯으면 됩니다.

연동의 난이도는 “어떤 제품이냐”가 아니라 “인터페이스가 얼마나 드러나 있느냐”에서 갈립니다. 문서가 있으면 빠르게, 없으면 관측으로 — 어느 쪽이든 길은 있습니다.


배운 것 세 가지

  1. 원천을 복제하지 마라. 이미 권위 있는 원장이 있다면, 쌓지 말고 조회하라. 복사본은 드리프트한다.
  2. 신뢰는 문서가 아니라 관측에 둬라. 리버스 엔지니어링이든 성능 튜닝이든, 우리를 매번 구한 건 실제 캡처와 실측이었다. 라벨(“SEED 적용”, “이 함수가 그 일을 한다”)은 두 번이나 우리를 속였다.
  3. 변하지 않는 것은 다시 묻지 마라. 과거 데이터의 불변성을 인정하는 순간, 캐시 전략은 저절로 단순해진다.

낡은 블랙박스 하나를 뜯는 일이, 결국 “우리 시스템은 무엇을 원천으로 삼아야 하는가”라는 아키텍처 질문으로 돌아왔습니다. 좋은 통합은 새 기능을 많이 붙이는 게 아니라, 원천과 복사본의 경계를 정확히 긋는 것에서 시작하더군요.


GUNIQ는 레거시 시스템을 리버스 엔지니어링해 웹으로 옮기고, 사내 데이터와 업무 흐름을 하나로 엮는 통합 개발을 합니다. 비슷한 블랙박스를 안고 계시다면 문의로 이야기 나눠요.


MORE POSTS

다른 글 보기

테크 랩

GroupFlow 콜센터 통합 – 06. 통화 녹취, 금융사만 하는 거 아닙니다: 일반 기업이 녹취를 남겨야 하는 이유

통화 녹취는 금융사만? 일반 기업의 녹취 필요성과, 쌓아만 두지 않고 STT로 검색하는 녹취 관리.
2026.09.18
테크 랩

GroupFlow 콜센터 통합 – 05. 부재중 전화, 그냥 사라지고 있지 않나요: 통화 추적과 후속 관리 자동화

부재중 전화를 놓치지 않는 통화 추적·후속 관리 자동화 — 처리상태 태그·후속 필터·대시보드·문자 후속.
2026.09.17
테크 랩

GroupFlow 콜센터 통합 – 04. 교환기는 있는데 소프트폰만 쓰나요: LG U+ DCS를 그룹웨어에 연동하다

LG U+ DCS 같은 IP-PBX 교환기를 교체하지 않고 그룹웨어에 연동 — 레거시 소프트폰을 무중단으로 대체하는 방법.
2026.09.16

프로젝트 문의 환영합니다

기획부터 개발, 운영까지 함께 만들어 드립니다.

무료 3분 자가진단

우리 회사, 자체 클라우드가 답일까?

AWS vs 자체 인프라 · 11개 항목 3분 체크