Security
여기어때 신입 개발자의 암호화 서비스 개선기
Snow Park여기어때
2024년 11월 5일
원문에서 보기 ↗안녕하세요. 여기어때컴퍼니 공통플랫폼개발팀의 스노우입니다.
공통플랫폼개발팀에서 백엔드 개발자로 첫 걸음을 내디딘 저는, 사회 초년생이자 신입 개발자로서 첫 프로젝트로 암호화 서비스 개선 작업을 맡게 되었습니다. 오늘은 그동안 진행해온 개선 작업의 과정을 여러분과 함께 나누고자 합니다.
암호화와 키 관리의 중요성
여기어때의 암호화 서비스는 회원들의 개인정보와 같은 민감한 데이터를 안전하게 암호화하고 복호화하는 역할 을 담당합니다. 이를 통해 데이터는 외부 위협으로부터 보호받고 안전하게 관리됩니다. 그러나 데이터 암호화만으로는 완벽한 보안을 보장할 수 없습니다. 데이터 보안에서 또 하나 중요한 요소는 바로 암호화 키의 관리입니다.
암호화 키가 노출되면 암호화된 데이터는 의미를 잃고, 쉽게 노출될 위험에 처하게 됩니다. 따라서 암호화 키는 매우 높은 보안 수준에서 철저하게 관리되어야 합니다. 키의 생명주기를 관리하는 것도 그만큼 중요한데, 키의 생성부터 저장, 사용, 회수, 삭제에 이르기까지 모든 단계에서 강력한 보안 조치가 적용되어야만 데이터 보안을 완벽히 유지할 수 있습니다.
그렇다면 암호화 키를 어떻게 안전하게 관리할 수 있을까요? 🤔
바로 키 관리 서비스를 활용하는 것입니다. 많은 클라우드 플랫폼에서 키 관리 서비스를 제공하고 있으며, 이러한 서비스는 암호화 키를 보다 효율적으로 관리할 수 있게 해줍니다. 많이 알려진 서비스로는 다음과 같은 것들이 있습니다.
- AWS Key Management Service: 암호화 키 생성 및 관리를 지원하며, AWS의 다양한 서비스와 통합됩니다. 특히 AWS IAM과 연동하여 키에 대한 세밀한 접근 제어가 가능합니다.
- Google Cloud KMS: Google Cloud 플랫폼 내에서 리소스에 대한 키 관리 기능을 제공하며, 사용이 간편하고 통합이 용이합니다.
- Azure Key Vault: Microsoft Azure에서 암호화 키와 중요한 자격 증명을 안전하게 관리할 수 있는 솔루션입니다.
- IBM Cloud Key Protect: IBM 클라우드 환경에서 안전한 키 관리 솔루션을 제공합니다.
- Oracle Cloud Infrastructure Vault: Oracle 클라우드 환경에서 암호화 키를 안전하게 관리할 수 있는 도구입니다.
또한, 특정 클라우드 제공사에 종속되지 않는 키 관리 솔루션도 존재합니다. 이를 통해 여러 클라우드 환경에서 일관되게 키를 관리할 수 있습니다.
- HashiCorp Vault: 여러 클라우드 플랫폼에서 암호화 키와 비밀 정보를 중앙에서 안전하게 관리할 수 있는 도구입니다.
- Thales CipherTrust Cloud Key Manager: 다양한 클라우드 환경에서 암호화 키를 일관되게 관리할 수 있는 솔루션입니다.
여기어때는 AWS KMS를 사용하고 있으며 AWS 클라우드 서비스와의 호환성과 통합성을 강화했습니다. 이를 통해 키 관리의 복잡성을 줄이면서 AWS IAM과의 연동으로 권한 기반 보안을 강화할 수 있었습니다. 이러한 접근 방식은 데이터 보호 수준을 높이고 보안 관리의 부담을 최소화하여, 효율적이고 강력한 보안 구조를 구축하는 데 기여합니다.
이제 AWS KMS로 구성된 암호화 서비스를 왜 개선하게 되었고, 어떻게 개선되었는지에 대해 설명드리겠습니다.
기존 암호화 서비스의 개선점
현재 사용 중인 암호화 서비스는 수년 전에 구축된 시스템으로, 트래픽 처리의 성능 문제와 코드 최신화 문제 등 개선의 필요성이 점점 부각되었습니다. 이를 개선하기 위한 첫 걸음은 기존 시스템을 명확히 파악하는 것이었습니다.
기존의 암호화 서비스는 API Gateway와 AWS Lambda를 기반으로 한 서버리스 아키텍처로 구성되어 있었습니다.

기존 암호화 서비스 아키텍처
이 아키텍처는 초기에는 빠르고 간편하게 시스템을 구축할 수 있는 장점이 있었지만, 시간이 지나면서 몇 가지 한계점이 드러났습니다.
API Gateway의 역할과 한계
API Gateway는 초기 서비스 설계에서 Lambda 함수를 호출하는 중계 역할을 맡아 필수적으로 아키텍처에 포함되었지만, 현재는 Lambda의 Functional URL 기능이 도입되어 더 이상 API Gateway를 경유하지 않고 Lambda를 직접 호출할 수 있게 되었습니다. 이를 통해 API Gateway를 거치면서 발생하던 불필요한 네트워크 비용과 운영 비용을 줄일 수 있는 기회가 생겼으며, 나아가 아키텍처를 개선할 가능성도 고려할 수 있게 되었습니다.
Lambda의 비효율적 다건 처리 방식
Lambda 함수는 KMS 동기 클라이언트를 사용하여 암복호화를 처리하고 있습니다. 다건 데이터의 경우, 아래와 같이 순차적으로 데이터를 처리하는 방식을 채택하고 있었습니다.
import boto3
# 동기식 KMS 클라이언트 생성
kmsClient = boto3.client('kms')
# 다건 데이터 순차 처리
for text in plainTexts:
# 암호화(또는 복호화) 처리 로직
이 방식은 각 데이터가 처리될 때까지 대기하는 구조로, 데이터 양이 많아질수록 전체 응답 시간이 길어지는 문제가 발생합니다. 이러한 문제를 해결하기 위해서는 병렬 처리 방식 도입이 필요했습니다.
환경 분리의 필요성
기존 시스템에서는 개발, 스테이징, 상용 환경이 분리되지 않고 통합된 상태로 운영되었습니다. 이로 인해 특정 환경에서 발생한 문제나 트래픽 급증이 다른 환경, 특히 상용 환경에 직접적인 영향을 미칠 수 있었습니다. 예를 들어, 개발 또는 스테이징 환경에서 성능 테스트나 대용량 데이터의 암복호화 작업을 수행할 때, 상용 환경의 성능에 영향을 줄 가능성이 있었습니다. 이러한 문제를 해결하기 위해, 환경별로 서버를 분리하여 각 환경이 독립적으로 운영되도록 개선해야 했습니다.
형상 관리 및 모니터링의 어려움
현재 시스템은 소스 코드에 대한 형상 관리가 부족하고, 모니터링이 어려운 상황이었습니다. 이는 시스템의 안정성을 높이기 위해 반드시 개선해야 했습니다.
개선 작업
환경별 EKS 구성
개선 작업의 첫 단계로, 기존의 API Gateway와 Lambda 기반 아키텍처를 환경별 AWS EKS(Elastic Kubernetes Service)로 재설계하였습니다. 이 아키텍처 전환을 통해 각 환경을 명확하게 분리하고, 불필요한 API Gateway를 제거하여 API 호출 경로를 단순화였습니다. 또한, 각 환경을 독립적으로 운영할 수 있도록 구조를 변경하여 상용 환경에 미치는 영향을 최소화했으며, 개발과 배포 과정에서 발생할 수 있는 리스크를 줄였습니다. 이러한 개선을 통해 더 안정적이고 효율적인 서비스 운영이 가능해졌습니다.

신규 암호화 서비스 아키텍처
Java 및 비동기 클라이언트 사용
팀 기술 스택과의 호환성을 고려하여, 기존의 Python 기반 코드를 Java로 전환하였습니다. 이 과정에서 AWS SDK for Java 2.x가 제공하는 KmsAsyncClient를 사용하여 암호화 및 복호화 요청을 비동기적으로 처리할 수 있게 되었습니다. KmsAsyncClient는 비동기 작업을 지원하며, 암복호화를 수행하는 `encrypt()`, `decrypt()` 메서드는 각각 `CompletableFuture<EncryptResponse>` 또는 `CompletableFuture<DecryptResponse>` 형태로 결과를 반환합니다.

KmsAsyncClient의 enrypt 메서드
각 암복호화 요청의 후속 작업은 `thenApply`와 같은 콜백 메서드를 통해 처리할 수 있습니다. 이를 통해 각 요청이 완료될 때까지 기다리지 않고 다음 로직을 계속 실행할 수 있으며, 요청이 완료되면 지정한 콜백 메서드가 호출되어 후속 작업을 수행하게 됩니다. 또한, `CompletableFuture.allOf`를 사용하여 모든 비동기 작업이 완료될 때까지 대기한 후 각 요청의 결과를 수집하여 처리할 수 있습니다.
비동기 클라이언트의 동작을 더 세밀하게 제어하고자 한다면, NettyNioAsyncHttpClient를 사용해 KmsAsyncClient의 옵션을 커스터마이징할 수 있습니다. 아래는 그 예시입니다.
implementation 'software.amazon.awssdk:netty-nio-client:{version}'
@Bean
public KmsAsyncClient kmsAsyncClient() {
return KmsAsyncClient.builder()
.region(Region.AP_NORTHEAST_2)
.credentialsProvider(DefaultCredentialsProvider.create())
.httpClientBuilder(NettyNioAsyncHttpClient.builder()
.maxConcurrency(50) // 동시 처리 가능한 최대 연결 수
.maxPendingConnectionAcquires(10000) // 대기 중인 연결 요청 수
.connectionTimeout(Duration.ofSeconds(2)) // 연결 타임아웃 설정
)
.overrideConfiguration(o -> o.retryStrategy(b -> b.maxAttempts(3))) // 재시도 전략 설정
.build();
}
자세한 내용은AWS SDK for Java 개발자 가이드에서 확인할 수 있습니다.
형상 관리 및 회사 표준 CI/CD 적용
기존에 체계적으로 관리되지 않던 소스 코드는 이제 GitLab을 통해 형상 관리가 이루어지고 있습니다. 또한, 회사 표준 CI/CD 파이프라인을 적용하여 코드 변경 시 자동으로 빌드와 테스트가 실행되며, 모든 테스트가 성공적으로 통과된 경우에만 상용 환경에 안전하게 배포됩니다. 이로써 코드 품질을 보장하는 동시에 배포 과정의 안정성을 크게 향상시켰습니다.
APM(Application Performance Monitoring) 구성
암호화 서비스에 사내 APM 도구를 적용하여 모니터링 환경을 구축했습니다. APM은 애플리케이션의 주요 메트릭(응답 시간, 트랜잭션 처리 속도, 자원 사용량 등)을 실시간으로 추적하고, 이를 시각화하여 문제를 빠르게 분석할 수 있도록 돕습니다. 또한, 슬랙 알림을 추가해 성능 이슈나 오류가 발생할 경우 팀에 즉시 알림을 보내, 신속한 대응이 가능하도록 개선했습니다. 이를 통해 문제 발생 시 대응 시간을 크게 단축하고, 서비스 운영의 안정성을 한층 더 강화할 수 있었습니다.
이렇게 개선 작업이 마무리 되는 걸까요?
……
…
.
하하… 개발이 이렇게 순탄하게 흘러갈 리가 없죠…🫠
이슈 발생 및 해결: Max Pending Connection
성능 테스트 중, ‘Max Pending Connection’이라는 오류에 직면했습니다. 이 오류는 비동기 클라이언트가 처리할 수 있는 최대 연결 수를 초과하면서 발생한 문제로, 동시에 너무 많은 요청을 보내는 경우 시스템의 처리 한계를 넘어서게 되어 오류가 발생했습니다.
예를 들어, 비동기 클라이언트가 최대 1000개의 연결을 지원한다고 가정했을 때, 20명의 사용자(VUser)가 각각 500개의 다건 데이터 암호화 요청을 동시에 보낸다면 총 10,000개의 요청이 발생하게 됩니다. 이는 비동기 클라이언트의 처리 한계를 크게 초과하므로, 성능 저하나 오류 발생이 불가피했습니다.
이 문제를 해결하기 위한 접근 방식은 비교적 간단했습니다. 모든 요청을 한 번에 보내지 않고, 청크 단위로 나누어 처리하는 전략을 채택했습니다. 요청을 일정한 크기의 청크로 나누어 비동기 요청을 보내고, 각 청크의 응답을 받은 후 다음 청크를 처리하는 방식으로, 한 번에 처리되는 요청 수를 제한함으로써 시스템의 한계를 넘지 않도록 했습니다.
그렇다면 청크 사이즈는 어떻게 설정해야 할까요?
청크 사이즈는 CPU 코어 수를 고려하여 설정하였습니다. 청크 사이즈가 너무 크면 비동기 스레드가 많아지면서 CPU 사용률이 급격하게 증가하는 문제가 있었습니다. 이에 따라 다양한 청크 사이즈로 성능 테스트를 반복 진행했습니다. 그 결과, CPU 사용률이 가장 안정적인 지점인 CPU 코어 수 + 1로 청크 사이즈를 설정하였습니다.
초기에는 안정적인 성능 확보를 위해 다소 보수적으로 설정하되, 실제 트래픽을 관찰하며 CPU 사용률이 충분히 여유롭다면 청크 사이즈를 점진적으로 늘려가며 더 높은 성능을 달성할 수 있습니다. 중요한 점은 무작정 높은 성능을 목표로 청크 사이즈를 크게 잡기보다는, 서버의 성능을 충분히 고려한 후, 다양한 테스트를 통해 최적의 값을 찾는 것입니다.
KMS Throttling과 추가 개선
아직 해결해야 할 중요한 개선 사항이 남아 있습니다. KMS는 기본적으로 동일 리전 및 동일 키 타입에 대해 요청 할당량을 초당 10,000건으로 공유하도록 설정되어 있습니다. 현재 여기어때는 이 기본 할당량을 사용 중인데, 만약 상용 외 환경에서 요청량이 증가하여 초당 요청 할당량을 초과하게 된다면, 상용 환경에서 Throttling 문제가 발생할 위험이 있습니다. 비록 현재는 Throttling 이슈가 발생할 정도의 사용률은 아니지만, 여기어때의 트래픽이 계속해서 증가하고 암복호화 요청이 더 많아지면 문제가 발생할 가능성이 있습니다.
이를 방지하기 위해 처리율 제한 장치를 도입해 각 환경의 KMS 요청을 제어할 계획입니다. 이를 통해 상용 환경에 미치는 영향을 최소화하고, 상용 환경의 안정성을 보장할 수 있습니다. 만약 향후 KMS Throttling 문제가 빈번하게 발생하게 된다면, KMS의 초당 요청 할당량을 늘리는 방안도 고려할 수 있을 것입니다.
결과
개선 작업 완료 후, 기존 암호화 서비스와 새롭게 개선된 암호화 서비스의 성능을 비교하기 위해 nGrinder를 사용하여 성능 테스트를 진행했습니다. 테스트 조건은 현재 암호화 서비스에서 처리 중인 실제 요청량을 기반으로 설정하여, 실무 환경에서의 성능을 보다 정확하게 평가할 수 있도록 했습니다.
테스트 결과, 신규 암호화 서비스는 기존 시스템에 비해 약 3배 향상된 성능을 보여주었습니다. 특히 다량의 데이터를 처리할 때 두 시스템 간의 성능 차이가 더욱 두드러졌습니다.
- 기존 시스템: 동기적 처리 방식을 사용하여 데이터를 순차적으로 처리했기 때문에, 데이터 양이 많아질수록 응답 시간이 급격히 증가하는 문제가 있었습니다. 이로 인해 처리량이 제한되고, 요청이 누적될수록 성능 저하가 발생했습니다.
- 개선된 시스템: 비동기 처리 방식을 도입해 다량의 데이터를 병렬로 처리함으로써 처리 속도가 크게 개선되었습니다. 병렬 처리를 통해 시스템 자원을 효율적으로 활용하여 더 많은 요청을 동시에 처리할 수 있게 되었습니다.
이번 성능 테스트 결과를 바탕으로, 앞으로도 성능 모니터링과 최적화 작업을 지속적으로 추진할 계획입니다. 이를 통해 예상치 못한 트래픽 증가 등 다양한 상황에서도 안정적인 서비스를 제공할 수 있도록 철저히 대비하겠습니다.
마무리
이번 개선 작업을 통해 기존 문제점들을 해결하고 서비스를 더욱 발전시킬 수 있었습니다. 물론 개발 과정에서 비동기 클라이언트 연결 한도 초과와 같은 예상치 못한 문제도 있었지만, 이를 해결하며 시스템을 더욱 안정적이고 견고하게 만들 수 있었습니다. 이번 경험에서 얻은 인사이트는 향후 프로젝트에도 큰 도움이 될 것입니다.
앞으로도 더 나은 개발자가 되기 위해 계속 성장해 나가며, 여기어때의 백엔드 개발자로서 안정적인 서비스를 제공하기 위해 최선을 다하겠습니다. 긴 글을 읽어주셔서 감사합니다!