개요
Smart Factory 계열의 소프트웨어 회사로 입사한지 대략 1년하고도 반의 시간이 지났습니다. 블로그에 글을 적을 시간이나 여유가 아예 없었던 것은 아니지만, 부끄럽게도 개발과 관련해서 이렇다 하고 내세울 활동이나 자기개발 내용이 거의 없었던 것 같습니다. 그래도 바빴던 시기가 지나고 어느 정도 여유를 되찾았기 때문에, 그 간 진행해온 개발 작업과 관련하여 포스트를 작성합니다.
1년 반동안 맡았던 프로젝트들 중 직접 코딩을 맡았던 프로젝트가 2건, 소프트웨어 활용 프로젝트가 5건이었습니다. 그 중에 본격적으로 개발 작업을 진행했던 프로젝트에 대해서 회고록 형태로 이하에 짧게 정리를 해보고자 합니다.
Apache Kafka Consumer 기반 Interface 어플리케이션 개발하기
모 대기업 고객사에서 Apach Kafka Broker 내의 Mirror Maker 토픽에 발행되는 데이터를 실시간으로 수신 및 가공해서 자사의 소프트웨어로 전송하는 인터페이스를 개발했습니다(Java Spring 기반). 초기에는 단일 서비스로 개발했으나, 고객사에서 요청하는 기능들이 추가됨에 따라 현재는 사업소당 각기 8개의 앱으로 구성된 MSA 구조입니다. 처음에는 2인 팀으로 진행될 예정이었으나, 팀 개발자분이 네트워크 보안 기술 개발에 착수하시게 되어 혼자서 진행하게 되었습니다.
여러가지 여건들로 인해 프로젝트 착수 일정이 이전의 1/3 가량으로 촉박해졌기 때문에 초기에는 엉성하게 만들어서 배포했으나(지금도 많이 부족하다고 생각됩니다만) 이후 여러 차례의 수정 및 보완 작업을 거쳤습니다.
환경적인 여건에서의 에러 사항들
- 폐쇠망 환경이었기 때문에, 사용 가능한 모든 컴퓨터에서 웹 브라우저를 통한 접근 이외의 API Response 수신이 막혀 있었습니다. 따라서 어플리케이션 개발에 필요한 repository 파일들은 최대한 바깥에서 직접 반입해서 작업을 진행했는데, 사용해야 할 고객사의 Java 라이브러리가 Nexus에 업로드되어 있고 공유된 주소가 HTTP 주소였기 때문에 자동으로 받아지지 않아 수동으로 필요한 패키지를 Nexus에서 직접 파일 단위로 클릭해 다운받아야 했습니다(보안 규정 때문에 외부로 반출할 수 없었습니다..).
- Kafka Broker 서버와 서비스 배포 서버 사이의 방화벽 통신이 막혀 있었습니다. 보통은 방화벽 규칙을 신청하면 평균적으로 2시간 내로 반영된다고 안내 받았으나, 실질적인 적용은 약 2.5일 가량이 소요되었습니다. 고객사에서 작업이 늦어지는 부분에 대해 크게 압박을 가하지 않아서 다행이라고 생각했었죠.
- 제가 Eclipse를 잘 다루지 못해서 발생하는 문제로 생각되지만, 외부에서 반입한 repository 의존성을 추가하면 모든 패키지가 호출 실패로 바뀌는 현상이 있었습니다. 폐쇠망 안에서만 발생하는 문제였기 때문에 적절히 대응하지 못했고, m2 settings 등을 포함한 여러가지 조치를 시도했으나 해결되지 않았습니다. 따라서 꼭 필요한 서비스만 사내망에서 개발하고, 사외 개발이 가능한 서비스들은 전부 바깥에서 준비해 반입했습니다.
개발 단계에서의 에러 사항들
- 64개의 파티션으로 구성된 단일 토픽에 초당 백만개 단위의 데이터가 입력되고 있었습니다. 데이터들은 n ~ n * 1000 개 단위의 작은 그룹으로 묶여 있고, 입력 순서가 보장되지 않기 때문에 Thread Pool 만으로는 병목과 작업 유실 및 가비지 이슈를 해결하기 어려웠습니다. 따라서 1)LMAX Disruptor 객체를 배열로 관리해 기존의 Thread Pool 구조를 Lock-Free 스레드의 완전 재사용 구조로 개편하고, Consumer 앱에는 2)각 파티션 별로 별도의 @Listener를 할당하여 병목을 해소했습니다(개인의 무지에서 비롯된 것이지만, Spring Kafka Consumer 스레드 수가 Concurrency 값이 아닌 @Listener 선언 수라는 것을 당시에는 몰랐습니다..).
- 자사 소프트웨어가 C#(.NET) 기반이기 때문에 Java 어플리케이션과의 TCP 통신 기능 구현에 어려움이 있었습니다. 초기에는 GRPC 및 Apache 범용 패키지 기반의 TCP 통신을 준비했으나, CPU 및 메모리 자원을 지나치게 많이 사용해 현장에 배포하기에는 다소 어려움이 있었습니다. 따라서 초기 배포본까지는 (급하게 배포를 마무리해야 했기에) UDP 통신을 채용했으나, 지난 주를 기점으로 Netty + Kryo + Protobuf 기반의 TCP 통신 아키텍처 개발 및 테스트를 완료하였으며 다음 업데이트 시에 적용할 예정입니다.
- 특정 데이터들을 대상으로, 값이 변경된 것이 감지될 때 해당 값으로 현재값을 갱신하고 데이터베이스에 INSERT 하는 서비스를 구현해야 했습니다. 현재값을 보관하는 ConcurrentHashMap 을 메모리 상에서 관리하는 구조인데, 데이터 입력 횟수가 빈번하기 때문에 현재값 갱신 이전에 새로운 데이터가 들어와 INSERT 작업이 중복으로 발생하는 이슈가 있었습니다. 따라서 데이터베이스로 바로 INSERT 작업을 수행하는 대신, 특정 폴더에 csv 파일로 데이터를 기록하고 별도의 서비스가 이를 읽어 스케줄 단위로 중복 제거 및 INSERT 작업을 수행하도록 구성했습니다.
향후 고려해야 할 사항들
- 소프트웨어 계약 범위로 인해, 현재는 백만개 단위의 수신 데이터들 중 극히 일부만(n * 10000 개) 처리 및 전송하고 있습니다. 따라서 현재 시점에는 8개의 앱 중 Consumer 서비스 1개만 Disruptor 이벤트 처리를 사용합니다. 그런데 계약 확장과 함께 데이터의 수량이 지속적으로 늘어난다면, 시스템 병목 및 데이터 유실 방지를 위해 모든 서비스에 Disruptor 이벤트 처리를 적용해야 합니다. 이는 상당한 양의 메모리 자원을 소모하기 때문에 종국적으로는 객체/스레드 수량과 성능 간의 상관관계를 파악하고 최적화 작업을 진행해야 합니다.
- 반복적으로 재사용되는 객체는 Apache Pool2 기반의 객체 풀을 구성 관리하는데, 풀은 Lock 기반 구조이기 때문에 Lock-Free 환경에서 사용 시에 운영 안정성이 저하되고 성능이 나빠질 우려가 있습니다. 현재는 데이터 직렬화 및 역직렬화를 Kryo 풀을 구성해 수행하는데, 현재로서는 특별한 성능 저하 포인트가 없으나 개선 방안을 검토 및 연구해야 합니다.
교훈, 느낀점 그리고 반성
고객사, 그것도 대기업이라는 환경에서 진행해 본 첫 번째 프로젝트였기 때문에 많은 교훈들을 배울 수 있었습니다. 융통성 없이 달려왔던 지난 날들의 일정을 돌이켜 보며 핵심적인 교훈들을 정리하면 아래와 같이 이야기할 수 있을 것 같군요.
- 서비스는 처음부터 MSA 기반으로 설계하자. 이미 배포 끝난 걸 다시 쪼개는 것은 더욱 어렵다.
- 2번 이상 사용할 기능은 (아무리 작은 기능이더라도) 반드시 패키징해서 재사용하자.
- 서비스 수정본은 최소 2주 전에는 배포 환경에서 테스트를 진행하자.
- 차후 문제가 될 것이라 예상되는 부분은 (고객사의 요청이 없어도) 자진 출석해서 패치하자.
- 시스템 리소스 중 네트워크 인/아웃바운드 트래픽 사용량을 필히 확인하며 개발하자.
프로젝트를 마무리하는 감정은 늘 복잡했던 것 같습니다. 계약 금액이 큰 프로젝트였기에 한 편으로는 팀과 회사에 도움이 될 수 있어서 기뻤고, 또 한 편으로는 코더라고 칭하기에도 민망한 스스로의 능력과 태도에 자괴감을 느끼기도 했습니다. 특히 앞서 언급했던 기술적인 내용들(개발 에러 사항 및 향후 고려 사항)은 개발 단계에서 절대 발생해서는 안 되는 수준의 기초적인 내용들인데, 아직까지도 유연하게 대처하지 못하는 부분에 대해 부끄러움을 느끼고 있습니다. 그래도 이런 반성과 교훈으로 인해 앞으로도 팀과 회사에 도움이 되는 인재가 되고자 한다면, 자기 개발과 공부를 게을리하지 않고 최선을 다해야 한다는 생각을 품게 되는 것 같습니다.
마치며
두서 없이 포스팅을 적다 보니 내용이 잘 정리되지 못한 느낌이네요. 앞으로는 종종 방문해서 그간 개발한 패키지나 학습한 내용들에 대한 포스팅을 조금씩이라도 꾸준히 적도록 하겠습니다.
읽어주셔서 감사합니다!
발자취를 로그처럼 남기고자 하는 초보 개발자의 블로그