2025 게으른 개발자 컨퍼런스 제 2회 후기

게으르다면서 세상 부지런한 개발자 컨퍼런스에 다녀왔다 🏃‍♀️

2025 게으른 개발자 컨퍼런스는 테크살롱에서 진행되었다. 몰랐는데 우아한형제들이 만든 곳이라고.
후원사가 한빛미디어와 넥스트스텝이었다. 백엔드 컨퍼런스가 많지 않아서인지(?) 티켓팅 경쟁이 꽤나 치열했는데, 운 좋게 아는 분에게 양도 받아서 다녀왔다.

컨퍼런스 순서는 아래와 같다.

  1. 24/7 끊기지 않는 잔고 서비스 개발기 (황보성우님)

  2. Go 서버 메모리 누수 버그 개선 (김환석님)

  3. 실시간 광고 사용자 ID 매핑 시스템 구축 (김소현님)

24/7 끊기지 않는 잔고 서비스 개발기

개인적으로 가장 인상 깊었던 세션이다.

개요

  • 데이터 정합성과 트랜잭션 무결성 고성능 처리가 필요한 상황에서의 잔고 서비스 아키텍처 구축을 다룬다.

  • 과거 서비스 → 단일 인스턴스, 레코드 락으로 인한 병렬 처리 불가 → 확장 불가능한 구조

  • 목표 → 전체 성능 향상 (TPS 개선), Kafka를 이용한 비동기 메세지 처리 도입, 병렬 처리 추가, 레코드 락 제거 및 배치 처리 적용

기술 스택

  • Java21 / Spring Boot(Kafka, JPA) / Protobuf
    → 여기서 Avro를 안쓰고 Protobuf를 쓴 것이 특이했다. 질문 시간에 질문하려 했는데 다른 분이 먼저 질문을 해주셨음

  • Protobuf → 데이터 사이즈 최적화, 일관된 메세지 포맷 유지, 스키마 기반 데이터 구조라는 특징을 가짐. Avro와 비슷하지만 Avro의 스키마 관리법이라던지 기타 등이 맞지 않아서 Protobuf를 채택했다고 한다.

  • Kafka → 카프카의 여러 특징들 (서비스간 결합도 낮춤, 이벤트 기반, 비동기 처리 등) 때문에 카프카를 채택했다고.

Kafka

  • 사용자 ID를 기반으로 파티셔닝해서 컨슈머 그룹 내 각 컨슈머는 고유한 사용자만 처리하는 구조로 만들었다. 여기까지는 카프카에 관심이 있다면 대부분 아는 사실.

  • 메세지 실패 전략 → Protobuf를 통해 입력 메세지 타입을 엄격이 검사하고 잘못된 데이터 입력을 방지함. 처리 실패시 제한적 재시도 후 즉시 중단, 잔고 반영 순서가 중요해 DLQ 사용 하지않음.

  • Kafka 컨슈머 최적화를 위해 수동 ACK 설정과 FETCH_MIN_BYTES_CONFIG, MAX_POLL_RECORDS_CONFIG 설정으로 배치 처리 성능 향상과 크기 조정을 했다.
    → 수동 ACK의 경우 권장하지 않는다고 어디서 봤던 것 같은데 (정확하지 않음) 수동 ACK로 컨트롤하고 있어서 신기했고 어떤 식으로 최적화해 나갔는지 궁금해졌다. 이 부분은 질문 못함
    → ACK를 분실하는 경우에 대해 질문 → 어짜피 재처리를 하면서 멱등성 테이블(뒤에 나옴)에서 처리되었기 때문에 방어가 가능하다. 사실 멱등성 키를 확인하는 구조를 얼마나 잘 짜느냐가 중요한 것.

  • 각 컨슈머들은 사용자 ID 별로 병렬 스레드를 돌려서 메모리에서 처리해 성능을 좀 더 높혔다. Parallel Consumer와 유사한 방식이라고 함.
    → 병렬 스레드를 돌리는데 어떻게 순서가 유지되나 의문이 들었는데 사용자 ID 별로 돌린다고 하니 이해가 되었다.

  • 요청 처리 만큼 응답 메세지도 중요해서 응답 메세지가 유실되면 결과 반영 실패 처리

  • Outbox Pattern을 이용해서 트랜잭션과 Kafka 전송을 완전히 분리해 안정성을 확보했다.

  • 전체 요청을 무조건 잔고 서비스에서 받도록 해 레코드 락을 제거할 수 있었고, 비관적 락을 적용하지 않고 바로 업데이트 하도록 변경

  • 단 건 요청 당 하나의 트랜잭션을 사용했던 것에서, Kafka로 들어온 메세지를 배치로 묶어 한번에 처리해 요청 당 부하가 감소함.

  • JDBC 배치 설정을 통해 대량의 INSERT 성능 최적화를 함

무결성 보장

  1. 각 메세지의 Type, Key를 가지고 미리 저장되어 있는 테이블에 비교. 멱등성 키 테이블에 없는 데이터들만 INSERT 후 사용함
    → 멱등성 키 테이블도 결국 디비 접근인데, 캐시 서버로 이루어진건지 질문했다. 단건이 아니고 배치 처리라서 성능이 괜찮았고 캐시 서버를 두면 결국 네트워크를 또 타야하는데 안정성을 위해 어느정도 희생한 부분이 있다고 답변을 해주심.

  2. 잔고 변동 처리 전에 필요한 데이터를 미리 조회해 트랜잭션 중 추가 조회 없이 메모리에서만 처리 가능하게 했다. DB 접근 적게해 성능 최적화

  3. 엔티티 업데이트. JPA의 Auto Increment로 인해 매번 INSERT 후 UPDATE가 발생해서 AI를 모두 삭제하고 복합기를 유일 키로 설정해서 불필요한 UPDATE문을 줄여 성능 향상을 했다.

배포 전략

  • 기침 이슈로 잠깐 나가있느라 자세한건 못 듣고 k8s에서 Recreate 방식을 사용한다고 함. 다른 방식은 서버가 순차적으로 교체될 때 이전 서버와 현재 서버가 동시에 떠있으면 데이터 정합성이 맞지 않을 수 있어서 순단 현상이 발생하더라도 Recreate 방식을 사용한다고 했다.

카프카 개념을 전반적으로 생각하게 하면서, MSA 설계도 같이 고민하게되는 세션이었다. 제일 흥미로웠고, 사이드 프로젝트에도 관련 개념을 적용해보려고 생각 중이다.

여기부터는 잘 모르는 내용이라 세션 내용을 짧게 적었다.

Go 서버 메모리 누수 버그 개선

개요

  • Go에는 동시성 개발에 사용되는 Gorouine이 있는데 고루틴 내부에는 Channel이라는 MQ와 비슷한 개념이 존재한다.

  • unbuffered channel / buffered channel 로 나뉘는데 un의 경우 consumer가 받을 때 blocking 된다는 특징이 있고 buffered는 비동기 넌블로킹으로 메세지 큐와 흡사하다.

메모리 누수 문제와 분석

  • 메모리 80% 이상인 경우 슬랙 알림이 오도록 설정해놓았는데 특정 인스턴스가 특정 시간에 단기간에 급격히 증가하는 추세를 보였다.

  • 분석을 위해 3가지 방법을 시도 했다.

    1. 서버 동작 구조 시각화

      • 내부 구조도와 요청 흐름을 도식화해 전체 동작 맥락을 파악했다.
    2. 로그 분석 및 proof(go profiler) 활용

      • 특정 로그만 반복적인 패턴이 나타남.

      • 고루틴의 수와 Heap 메모리 사용량을 분석했다

    3. 테스트 코드로 재현 시도

      • 버그 상황을 테스트 코드로 재현해 문제 원인을 검증했다.

→ 결론: 무한 블로킹이 원인이었음

  • 블락 되는 케이스가 두가지가 있는데 Channel이 꽉차서 Producer가 blocking 되거나 (MQ와 비슷), Consumer가 사라진 경우 Producer가 사라진 걸 인지하지 못하고 계속 blocking 상태가 된다고 한다.

해결방안

  • MQ 처럼 Pub/Sub 구조로 갈 수 없을지 고민하다가 Watermil 라이브러리 사용

  • P/S 구조를 사용하면서 Go의 Channel을 지원해 Kafka와 같은 별도의 미들웨어를 두지 않고도 해결함.

질문

  • Q. Go가 In-memory 방식인데 데이터가 유실되어도 괜찮은지?
    A. 스푼라디오(음성) 서비스라 데이터 유실이 되어도 문제 없는 서비스라 괜찮았다.

  • Q. Channel을 분리하는 기준이 궁금하다
    A. 아직 필요 없어서 서비스를 분리하지는 않았다.

  • 들으면서 내가 궁금했던 것은, Producer가 Consumer가 사라진 것을 인지하지 못할 때 blocking 된다고 했는데, Consumer가 사라지지 않도록 방지하는 방법에 대해서만 설명하고 Consumer가 실제로 사라지게 되면 (종료되면) 다시 똑같은 일이 발생할 수 있는건지가 궁금했는데, 질문은 하지 않고 넘어갔다. 쓰다보니까 더 궁금하다.

Go에 대해서는 잘 모르지만 MQ 개념이 나와서 이해하는데 어려움은 크게 없었던 세션이었다.

실시간 광고 사용자 ID 매핑 시스템 구축

이 세션은 나한테 좀 어려웠다.

개요

  • 유저 당 발행되는 광고 아이디, 브라우저 단위로 발생하는 아이디 등등 하나의 유저에게 여러개의 아이디가 발행될 수 있는데, 문제는 이 아이디들이 한 사용자라는 것을 모르는 것이다. 그래서 광고 사용자 ID 매핑이 필요하다.

배치에서 스트리밍으로

  • 기존 배치의 경우 아래와 같은 단점이 있었다.

    • 일괄 처리로 사용자 ID 수집

    • 조건문 기반의 ID 매핑 로직

    • 따라서 매핑 결과의 지연 발생

    • 확장성 또한 부족

  • 그래서 Spark와 Kafka를 이용한 구조로 확장/변경했다.

ID 매핑 알고리즘

  • ID 타입의 우선 순위를 정의했다

  • 알고리즘의 트리 구조를 이용함.

  • 잘 모르겠어서 이해가 안가기 시작한다.

시스템 설계

  • 아이디만 필요해서 추출하는 스파크 모듈이 존재한다.

  • 저장시 인메모리에도 저장하고 카프카에도 보낸다.

  • micro batch로 중복을 제거한다.

백엔드의 도메인이 워낙 넓다보니 백엔드 컨퍼런스에 개발, 데이터, Devops 등등이 포함될 수 밖에 없는 상황이다. 그래서 데이터 도메인 쪽 얘기를 듣다보니 이해를 많이 하지 못했다. 다만 한가지 아쉬운 점은 발표자 분의 설명이 좀 더 근본적인 핵심을 얘기하면 좋았을 것 같다는 생각이 들었다. 예를 들면 카프카를 썼어요 / 카프카를 이러이러한 이유로 썼어요는 차이가 있는데 전자에 가까운 설명을 해서 관련 도메인 지식이 없는 상태에서 들으니까 이해가 더 어려웠던 것 같다.

백엔드 컨퍼런스가 귀한데(?) 좋은 기회로 다녀오게 되서 좋았고, 재미도 있었다. 운영진 분들의 사비로 운영되는 컨퍼런스라는데 후원사가 좀 더 붙고 흥했으면 좋겠다. 후기 끝!

0
Subscribe to my newsletter

Read articles from 𝙇𝙮𝙜𝙞𝙖 directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

𝙇𝙮𝙜𝙞𝙖
𝙇𝙮𝙜𝙞𝙖

Back-end Engineer, Clarity in code. Calm in process.