Post

평범한 개발자를 넘어: 대용량 시스템 설계를 위한 4가지 핵심 인사이트

평범한 개발자를 넘어: 대용량 시스템 설계를 위한 4가지 핵심 인사이트

많은 개발자가 기능적으로 동작하는 코드를 작성하는 단계를 넘어, 실제 대규모 트래픽을 감당할 수 있는 견고하고 확장 가능한 시스템을 설계하는 단계로 나아가는 데 어려움을 겪습니다. 매일 작성하는 코드 속 작은 아키텍처 결정 하나가 시스템 전체의 성능과 안정성에 얼마나 큰 영향을 미치는지 생각해 본 적 있으신가요? 예를 들어, 데이터베이스 트랜잭션을 어떻게 관리하는지에 따라 시스템은 수많은 요청을 거뜬히 처리할 수도, 혹은 속수무책으로 무너질 수도 있습니다.

이 글은 대용량 트래픽 처리에 대한 전문가 수준의 강의 자료에서 네 가지 핵심적인 인사이트를 추출하여 공유하고자 합니다. 이 여정은 구체적인 구현 기술에서 시작해 거시적인 아키텍처 패턴으로 나아가며, 우리가 당연하게 여겼던 관행에 의문을 제기하고 더 나은 시스템 설계를 위한 실질적인 관점을 제시할 것입니다.

1. Redis, ‘단순 캐시’라는 착각을 버려라

대부분의 개발자는 Redis를 데이터베이스 I/O를 줄여주는 간단한 캐싱 솔루션으로 처음 접합니다. 물론 캐싱은 Redis의 매우 효과적인 활용법이지만, 이러한 관점은 Redis의 잠재력을 극히 일부만 보는 것입니다. 대용량 트래픽 환경에서 Redis의 진정한 힘은 그 다재다능함에 있습니다.

단순 캐시를 넘어 Redis는 다음과 같은 핵심적인 역할을 수행하며 시스템의 안정성을 책임지는 완충 장치가 될 수 있습니다.

  • 분산 락 (Distributed Locks): 여러 요청이 동시에 특정 데이터에 접근하고 수정할 때 발생하는 동시성 이슈를 해결합니다. 데이터베이스의 비관적/낙관적 락은 DB 커넥션을 필요로 하지만, Redis 분산 락은 요청이 값비싼 DB 커넥션을 소모하기 전에 동시성을 제어합니다. 이는 대용량 트래픽이 데이터베이스로 몰리는 것을 막는 훌륭한 완충장치 역할을 합니다.
  • 대기열 시스템 (Waiting Queues): 콘서트 예매 서비스처럼 수많은 사용자가 한꺼번에 몰릴 때, Redis를 대기열로 구현하여 일정 수의 요청만 핵심 서비스로 진입시키고 나머지는 대기시킬 수 있습니다. 이를 통해 데이터베이스 트래픽을 최소화하고 시스템을 보호할 수 있습니다.
  • 다양한 자료구조 (Versatile Data Structures): String, List, Set, Sorted Set, Hash 등 다양한 자료구조를 지원하여 단순한 키-값 저장소를 넘어 정교한 데이터 관리를 가능하게 합니다.

대용량 트래픽에 관련된 면접에서 가장 많이 질문하는 서비스가 바로 이 Redis입니다. 앞으로는 Redis를 단순하게 캐시 정도로만 활용하지 말고, 대용량 트래픽을 처리하는 다양한 방법으로 고도화해서 활용하는 것이 좋습니다.

2. ‘통 큰’ 트랜잭션이 독이 되는 이유

비즈니스 로직의 원자성을 보장하기 위해 전체 프로세스를 하나의 거대한 데이터베이스 트랜잭션으로 묶는 것은 개발자의 흔한 본능입니다. 하지만 이는 대용량 시스템에서 매우 위험한 안티패턴이 될 수 있습니다.

느린 작업으로 인한 성능 저하

하나의 트랜잭션 내에 느린 조회 쿼리나 오래 걸리는 작업이 포함되어 있다면, 해당 작업이 끝날 때까지 데이터베이스 커넥션과 락(Lock)을 계속 점유하게 됩니다. 이는 다른 요청들의 대기 시간을 늘리고, 최악의 경우 데드락을 유발하여 시스템 전체를 마비시킬 수 있습니다.

외부 시스템 호출의 위험

트랜잭션 범위 내에서 외부 API를 호출하는 것은 더 큰 문제를 야기합니다. 예를 들어, 결제 성공 후 주문 정보를 외부 데이터 플랫폼으로 전송하는 로직이 트랜잭션 안에 포함되어 있다고 가정해 봅시다. 만약 외부 API의 응답이 느리거나 실패하면, 이미 성공한 핵심 비즈니스 로직까지 전부 롤백되는 문제가 발생합니다. 심지어 우리 시스템에서는 타임아웃으로 실패 처리했지만, 외부 시스템에서는 정상적으로 데이터가 처리되었을 경우 데이터 불일치 문제로 이어질 수도 있습니다.

이커머스 결제 시나리오를 예로 들면, 핵심 로직(“유저 포인트 차감”, “결제 정보 저장”, “주문 상태 변경”)과 부가 로직(“주문 정보 외부 전파”)을 하나의 트랜잭션에 묶는 것은, 부가 로직의 실패가 핵심 로직의 성공을 위협하는 취약한 구조를 만드는 것과 같습니다. 이처럼 깨지기 쉬운 구조는 반드시 개선되어야 합니다. 그렇다면 우리는 이 문제를 어떻게 해결해야 할까요?

3. 이벤트: 비동기 처리를 넘어 ‘관심사 분리’의 미학

앞서 언급한 ‘통 큰’ 트랜잭션 문제를 해결하기 위해 많은 개발자는 먼저 비동기 처리를 떠올립니다. 가령, 핵심 로직은 트랜잭션으로 묶어 동기적으로 처리하고, 부가 로직인 ‘주문 정보 전파’는 별도의 스레드에서 비동기(Async)로 실행하는 것입니다. 이 코드는 원하는 대로 “정확하게” 동작합니다. 부가 로직의 실패가 더는 핵심 로직에 영향을 주지 않기 때문입니다.

하지만 가독성 관점에서 이는 좋은 코드가 아닙니다. 결제 서비스라는 핵심 로직 내부에 부가 로직을 처리하기 위한 예외 처리, 비동기 블록 등이 뒤섞여 있어 오히려 핵심 로직을 이해하기 어렵게 만듭니다. 여기에 “결제 완료 알림톡 발송” 같은 또 다른 부가 로직이 추가된다면 코드는 더욱 복잡해질 것입니다.

이 문제를 우아하게 해결하는 방법이 바로 애플리케이션 이벤트를 활용하는 것입니다. 이벤트는 단순히 작업을 비동기로 처리하는 도구가 아닙니다. 그 본질적인 가치는 ‘관심사 분리(Separation of Concerns)’를 가능하게 하는 데 있습니다.

  • 이전 코드: 결제 서비스는 핵심 결제 로직과 알림 발송, 데이터 전파 등 온갖 부가 로직이 뒤섞여 있어 코드를 이해하고 유지보수하기 어렵습니다.
  • 개선된 코드: 결제 서비스는 오직 핵심 결제 로직에만 집중합니다. 결제가 성공적으로 완료되면, ‘결제 완료 이벤트’를 발행하고 자신의 책임을 다합니다. 이후의 부가적인 작업들은 해당 이벤트를 구독하는 별도의 이벤트 리스너(EventListener)들이 독립적으로 처리합니다.

이러한 리팩토링은 강력한 결과를 가져옵니다. “이벤트”를 통해 핵심 로직과 부가 로직을 나누어 트랜잭션을 분리할 뿐만 아니라, 관심사를 분리하여 코드의 가독성도 향상시켰습니다. 결과적으로 핵심 비즈니스 로직은 부가 로직의 성공 여부에 영향을 받지 않게 되어 훨씬 더 견고하고 안정적으로 동작할 수 있습니다.

4. MSA의 함정: 이벤트 기반 아키텍처의 빛과 그림자

이벤트의 개념은 단일 애플리케이션을 넘어 마이크로서비스 아키텍처(MSA)로 확장됩니다. MSA 환경에서 이벤트는 각 서비스가 서로 강하게 결합하지 않고 협력할 수 있게 하는 핵심적인 통신 메커니즘입니다. 하지만 이는 양날의 검과 같습니다.

빛 (Light)

이벤트 기반 통신은 서비스 간의 의존성을 낮춰 시스템 전체의 유연성을 극대화합니다. 예를 들어, OrderService가 “주문 완료” 이벤트를 발행하면, InventoryService는 재고를 차감하고 NotificationService는 사용자에게 알림을 보냅니다. 각 서비스는 독립적으로 개발되고 배포될 수 있습니다. 주문 팀과 알림 팀은 서로의 작업 진행에 영향을 받지 않고 병렬로 개발할 수 있으며, 각자 도메인에 가장 적합한 기술 스택을 선택할 수도 있습니다. 트래픽이 몰리는 특정 서비스만 독립적으로 확장하는 것도 가능해집니다.

그림자 (Shadow)

반면, 이벤트 기반 아키텍처는 새로운 복잡성을 야기합니다.

  • 흐름 파악의 어려움: 비즈니스 로직이 여러 서비스에 걸쳐 비동기적으로 실행되기 때문에, 전체 프로세스를 한눈에 파악하고 추적하기가 어렵습니다.
  • 실패 처리의 복잡성: 이벤트 발행 이후에 이를 구독한 다른 서비스에서 작업이 실패할 경우, 데이터 정합성을 맞추기 위해 ‘보상 트랜잭션(Compensation Transaction)’이나 사가(SAGA) 패턴과 같은 복잡한 처리 방식이 필요합니다.
  • 테스트의 난이도 증가: 여러 서비스에 걸쳐 동작하는 기능을 종단간(End-to-End) 테스트하는 것이 훨씬 더 복잡해집니다.

결론

우리는 Redis라는 구체적인 도구의 역할에서 시작해 트랜잭션 범위, 관심사 분리, 그리고 이벤트 기반 아키텍처라는 거시적인 설계 원칙까지 살펴보았습니다. 확장 가능한 대용량 시스템을 구축하는 여정은 단순히 새로운 기술을 도입하는 것을 넘어, 내가 작성하는 코드의 책임과 경계를 비판적으로 사고하는 것에서 시작됩니다.

당신의 코드를 다시 한번 돌아보며 이 질문에 답해보시기 바랍니다.

“당신의 코드 속 가장 큰 트랜잭션은 지금 어떤 책임들을 짊어지고 있나요?”

This post is licensed under CC BY 4.0 by the author.