경력기술서 · 2022.12 – 2023.04 · LG CNS / 퓨렌스

농협 AI기반 금융상담 시스템 구축

풀스택 개발 및 녹취 엔진 개발 · Netty · JPA · Jenkins

서버 이중화와 L4 VIP 구성은 우리은행 불완전판매방지 솔루션과 같습니다. 그 위에서 Netty 녹취 엔진, Tibero 전환에 맞춘 JPA 재구현, Jenkins · Shell CI/CD를 개발했습니다.

Netty Spring (JPA) Tibero PostgreSQL REST Kafka Jenkins Shell
00

같은 인프라, 다른 구현

설계 관점

이중화와 VIP를 프로젝트마다 다시 그리면 녹취와 WAS의 분배 규칙이 은행마다 달라집니다. 농협의 서버 1·2, 녹취 Hash, WAS Round Robin은 우리은행과 같은 구성입니다. 그래서 인프라는 그대로 두고, 그 위에서 녹취 수신, DB 로직, 개발 배포만 새로 만들었습니다.

서버 1·2 이중화 · L4 VIP 녹취 Hash · WAS Round Robin
상담 · 연동
녹취
배포
Client
은행 CS상담 업무 프로그램
REC Agent단말 녹취
Jenkins개발망 빌드
L4 VIP
REST
VIPRound Robin
RTP
VIPHash
Server
WAS 1Spring · JPA
WAS 2Spring · JPA
녹취 1Netty 엔진
녹취 2Netty 엔진
Shell서버별 배포 스크립트
Data
DB VIP
Tibero 1Master
Tibero 2Slave

PostgreSQL 함수 · 프로시저 → JPA

녹취 저장Netty 엔진이 수신한 음성
담당한 구현 Netty녹취 엔진JPATibero 전환Jenkins · Shell
L4 VIP, 서버 1·2 이중화, 녹취 Hash, WAS Round Robin은 우리은행 솔루션과 같은 구성입니다. 농협에서는 녹취 수신을 Netty로 만들고, DB 로직을 JPA로 옮기고, 개발망에 Jenkins와 Shell 배포를 구축했습니다. 운영망은 PCMS 결재 후 수동 배포입니다.
01

규모와 역할

LG CNS 연계농협 금융상담 시스템
5개월2022.12 – 2023.04
풀스택 · 녹취 엔진화면 연동과 수신 서버
개발망 CI/CDJenkins · Shell
02

Netty 녹취 엔진과 연동 API

설계 관점

고객 RX와 상담원 TX는 같은 통화여도 패킷이 따로 들어옵니다. 음원마다 채널을 열면 라이선스가 두 배로 잡히고, 두 스트림을 다른 스레드가 고치면 순번과 종료 시점이 어긋납니다. 그래서 통화 키로 채널 하나에 모으고 라이선스는 그 채널만 세며, Hash VIP로 한 엔진의 이벤트 루프에 고정했습니다.

녹취는 RTP로 받습니다. RTP는 음성을 잘게 나눈 패킷이라, 순번과 시간이 붙어 있습니다. 한 통화 안에서도 고객이 말한 소리와 상담원이 말한 소리는 섞여 오지 않습니다. 고객 마이크는 RX, 상담원 마이크는 TX로 각각 들어옵니다.

엔진은 이 두 스트림을 같은 통화 키로 찾아 채널 하나에 넣습니다. 채널 1개가 동시 녹취 1건이고, 라이선스도 그 채널을 1개로 셉니다. 음원이 둘이라고 라이선스가 2개가 되지는 않습니다. 저장할 때는 RX와 TX의 순번을 맞춰 한 통화 파일로 남깁니다.

고객 RXRTP
상담원 TXRTP
한 통화로 매칭
채널 1개라이선스 1개 · RX + TX
여러 채널을 적은 스레드가 처리
이벤트 루프 1채널 1 · 4 · 7
이벤트 루프 2채널 2 · 5 · 8
이벤트 루프 3채널 3 · 6 · 9
고객 RX
상담원 TX
녹취 VIP
Netty
채널
녹취 저장
1. 두 음원이 따로 들어옴
① 고객 RTP
② 상담원 RTP
2. 같은 엔진 · 같은 채널
③ Hash로 한 엔진에 고정
④ 통화 키로 매칭, 라이선스 +1
3. 저장과 반환
⑤ 시퀀스로 맞춰 한 파일로 저장
⑥ 양쪽 종료 후 라이선스 -1
03

PostgreSQL 함수 · 프로시저 약 20개를 JPA 서비스로 전환

설계 관점

PostgreSQL 프로시저는 안쪽 호출이 전부 바깥 트랜잭션 안에서 커밋됩니다. 자바에서 단계마다 트랜잭션을 열면 중간 성공이 남고, 같은 클래스에서 자기 메서드를 부르면 그 설정이 적용되지 않습니다. 그래서 경계는 진입점 하나에만 두고, 하위 프로시저는 별도 빈의 REQUIRED로 참여시켰습니다. 본 작업과 무관하게 남아야 하는 처리만 REQUIRES_NEW로 뺐습니다.

운영 DB가 Tibero로 바뀌면서 PostgreSQL 함수 · 프로시저를 그대로 쓸 수 없었습니다. 워크플로우를 이루는 약 20개를 JPA 서비스 코드로 하나씩 옮겼습니다.

어려웠던 부분은 프로시저가 다른 프로시저 · 함수를 호출하는 구조였습니다. DB 안에서는 안쪽 호출이 모두 바깥 프로시저의 트랜잭션 안에서 실행되지만, 자바로 옮기면 트랜잭션 범위를 코드에서 직접 정해야 합니다.

전환 전 · DB 안에서 실행

트랜잭션 1개 · 경계 최상위 프로시저 여기서 시작하고 여기서 커밋
같은 트랜잭션 프로시저 A 바깥 트랜잭션 안에서 실행
같은 트랜잭션 함수 B 조회 · 계산
같은 트랜잭션 프로시저 C 바깥 트랜잭션 안에서 실행

하나라도 실패하면 전체가 롤백됩니다.

전환 후 · 경계는 진입점 한 곳

@Transactional · 경계 WorkflowService.run() 최상위 프로시저에 대응 · 여기서 시작하고 여기서 커밋
REQUIRED · 참여 AService 구 프로시저 A · 바깥 트랜잭션에 합류
트랜잭션 없음 BCalculator 구 함수 B · 조회 · 계산만
REQUIRED · 참여 CService 구 프로시저 C · 바깥 트랜잭션에 합류
REQUIRES_NEW · 필요한 곳만 실패와 무관하게 남아야 하는 처리 본 작업이 롤백돼도 따로 커밋
01 · 경계는 진입점 한 곳

최상위 프로시저에 대응하는 서비스 메서드 하나에만 @Transactional을 둡니다. 하위 단계는 기본 전파(REQUIRED)로 같은 트랜잭션에 참여합니다.

그래서 원래 프로시저처럼 워크플로우 전체가 한 번에 커밋되거나 한 번에 롤백됩니다.

02 · 프로시저 단위로 빈 분리

같은 클래스 안에서 자기 메서드를 부르면 Spring 프록시를 거치지 않아 트랜잭션 설정이 적용되지 않습니다.

그래서 프로시저 하나를 서비스 빈 하나로 나눴고, 조회 · 계산만 하던 함수는 트랜잭션 없는 컴포넌트로 뺐습니다.

03 · 예외와 롤백 규칙

프로시저의 RAISE EXCEPTION은 자바 예외로 옮겼습니다. Spring은 기본적으로 런타임 예외에서만 롤백하므로, 체크 예외는 rollbackFor로 롤백 대상을 명시합니다.

하위 단계 예외를 바깥에서 잡고 넘기면 트랜잭션이 롤백 전용으로 표시돼 커밋 시점에 실패하므로, 예외는 삼키지 않고 경계까지 올립니다.

04

개발망 Jenkins · Shell CI/CD

설계 관점

개발 서버마다 손으로 올리면 산출물이 갈라지고, 그 자동화를 운영까지 이으면 결재 전 빌드가 운영에 들어갑니다. 그래서 Jenkins와 Shell은 개발망에서 WAS와 녹취 엔진을 같은 산출물로 반영하는 데만 쓰고, 운영은 PCMS 결재 뒤 수동 배포로 남겼습니다.

CI/CD는 개발망에 구축했습니다. 개발 서버 안에서 Jenkins로 빌드하고, 서버마다 둔 Shell로 반영합니다. 운영망은 자동화하지 않았습니다.

개발망 · Step 01 / 빌드 · 담당
Jenkins로 개발 서버 안에서 빌드
개발망 서버에 Jenkins를 두고, 같은 기준으로 빌드가 나오게 했습니다.
개발망 · Step 02 / 배포 · 담당
서버별 Shell 스크립트 실행
개발 서버마다 배포 Shell을 두고, Jenkins가 그 스크립트를 호출해 서버별로 반영했습니다.
개발망 · Step 03 / 반영
WAS · 녹취 엔진 반영
JPA가 올라간 WAS와 Netty 녹취 엔진이 개발 서버에 같은 산출물로 반영됩니다.
운영망 · 수동
농협 내부 PCMS 결재 후 수동 배포
운영망은 우리은행과 같이 농협 내부 PCMS로 결재라인만 탄 뒤, 승인된 건을 수동으로 배포했습니다. Jenkins 파이프라인은 타지 않습니다.
개발망에서 난 효과

개발 반영을 서버마다 손으로 복사하지 않아도 됩니다. 빌드 기준이 하나라 개발 서버 간 산출물이 달라지지 않고, 녹취 엔진과 WAS를 같은 흐름으로 올릴 수 있습니다.

운영망은 분리

운영 반영은 결재가 끝난 뒤에만 진행됩니다. 개발 자동 배포와 운영 수동 배포가 나뉘어, 개발 중 배포가 운영 서버로 바로 들어가지 않습니다.

05

트러블슈팅

01RX · TX가 다른 이벤트 루프에서 같은 통화 상태를 동시에 수정

문제 · 동기화되지 않은 공유 상태

고객 RX
상담원 TX
루프 1
루프 2
통화 상태
① RX를 루프 1에서 수신
② 루프 1이 통화 상태 수정
③ TX를 루프 2에서 수신
④ 루프 2도 동시에 수정

RX와 TX는 별도 RTP 스트림이라 서로 다른 Netty 채널로 받고, 두 채널이 다른 이벤트 루프에 배정될 수 있습니다. 두 루프가 같은 통화 객체(시퀀스 · 버퍼)와 라이선스 카운터를 동기화 없이 고치면 음원 순서가 섞이거나 라이선스 수가 어긋납니다. 그렇다고 이벤트 루프에서 락을 잡으면, 그 루프가 맡은 다른 통화의 수신까지 멈춰 병목이 됩니다.

수정 · 통화 상태는 담당 루프 한 곳, 라이선스는 원자 연산

고객 RX
상담원 TX
루프 1 · 담당
루프 2
통화 상태
① RX를 담당 루프 1에서 수신
② TX를 루프 2에서 수신
③ 담당 루프 1로 작업 전달
④ 루프 1만 통화 상태 수정

통화가 시작될 때 담당 이벤트 루프를 정하고, 다른 루프에서 받은 패킷은 eventLoop().execute()로 담당 루프에 넘겨 통화 상태를 한 스레드만 고치게 했습니다. 라이선스 카운터는 서버의 모든 통화가 함께 쓰는 값이라 루프에 묶지 않고 원자 연산(AtomicInteger)으로 증감하고, 통화 종료 때 반환이 두 번 일어나지 않게 종료 여부도 한 번만 바뀌도록 했습니다.

결과

통화 상태를 동시에 고치는 구간이 없어져 음원이 섞이지 않습니다. 락을 쓰지 않아 이벤트 루프가 대기로 멈추지 않고, 라이선스 사용 수는 실제 동시 통화 수와 맞습니다.

02프로시저 내부 호출을 옮기면서 트랜잭션 경계가 어긋남

문제 · 단계마다 따로 커밋되면 반쪽 데이터

WorkflowService
AService
CService
DB
① A 호출
② A가 자기 트랜잭션으로 커밋
③ C 호출
④ C 실패 · C만 롤백

DB 안에서는 프로시저 A · C가 바깥 프로시저의 트랜잭션 안에서 실행돼, C가 실패하면 A도 함께 취소됐습니다. 자바로 옮기면서 단계마다 트랜잭션이 따로 열리면 A만 반영되고 C는 빠진 반쪽 데이터가 남습니다. 같은 클래스 안에서 단계 메서드를 부르면 트랜잭션 설정 자체가 적용되지 않는 문제도 있었습니다.

수정 · 경계를 진입점 한 곳으로, 하위 단계는 참여

WorkflowService
AService
CService
DB
① run()에서 트랜잭션 시작
② A 참여 · 아직 커밋 전
③ C 참여 · 실패하면 예외 전파
④ 경계 전체 롤백 · A도 취소

최상위 프로시저에 대응하는 WorkflowService.run()에만 @Transactional을 두고, 프로시저 단위로 나눈 A · C 빈은 REQUIRED로 같은 트랜잭션에 참여시켰습니다. 하위 단계 예외는 삼키지 않고 경계까지 올려, C가 실패하면 A까지 함께 롤백됩니다. 전환한 약 20개는 같은 입력으로 전환 전 프로시저 결과와 대조한 뒤 반영했습니다.

결과

워크플로우 약 20개가 전환 전과 같은 단위로 커밋 · 롤백되어 반쪽 데이터가 남지 않습니다. 트랜잭션 범위가 코드(@Transactional)에 드러나 어디까지 한 번에 처리되는지 바로 확인할 수 있습니다.

06

핵심성과

녹취 · 트러블슈팅 01 통화 상태 동시 수정 → 담당 루프 1개가 전담
한 일

RX · TX가 다른 이벤트 루프에서 같은 통화 상태를 동시에 고치는 지점을 찾았습니다. 통화 상태는 담당 루프에 넘겨 한 스레드만 수정하고, 공유 라이선스 카운터는 원자 연산으로 증감했습니다.

개선

락 없이 동시 수정 구간이 사라져 이벤트 루프가 대기로 멈추지 않습니다. 음원이 섞이지 않고, 라이선스 사용 수는 실제 동시 통화 수와 맞습니다.

데이터 · 트러블슈팅 02 DB 프로시저 의존 약 20개 → 0개
한 일

워크플로우 함수 · 프로시저 약 20개를 각각 JPA 서비스로 옮겼습니다. 트랜잭션 경계는 진입점 한 곳에 두고, 프로시저 단위로 나눈 하위 빈은 같은 트랜잭션에 참여시켰습니다.

개선

Tibero에서 PostgreSQL 전용 함수 없이 동작합니다. 실패하면 워크플로우 전체가 롤백돼 반쪽 데이터가 남지 않고, 20개 모두 전환 전 결과와 대조했습니다.

배포 개발 반영은 서버 수만큼의 수작업 → 빌드 1회
한 일

개발망은 Jenkins 빌드 1번에 서버별 Shell이 같은 산출물을 올립니다. 운영망은 농협 PCMS 결재 뒤 수동 배포로 분리했습니다.

개선

서버마다 다른 산출물이 올라가 다르게 동작하는 일이 없습니다. 결재 전 개발 산출물이 운영으로 들어가지 않습니다.