컬리 공통 암호화 SDK 모듈에서 간헐적으로 발생한 ByteBuffer 버퍼 오류를 추적해 동시성 이슈로 규명하고 해결한 디버깅 기록이다. AWS KMS 봉투 암호화와 Caffeine 캐시 구조에서 왜 thread-safe 문제가 생겼는지 코드 레벨로 파고든다.
핵심 포인트- 개인정보 암호화에 AWS KMS 봉투 암호화를 쓰고, KMS API 호출 비용을 줄이려 Data Key를 Caffeine 캐시에 보관해 재사용한다.
- 1:1 문의 게시판에서 마스킹 전화번호가 빈칸으로 뜨는 newPosition greater than limit Buffer 오류가 일부 요청에만 간헐적으로 발생했다.
- 원인은 여러 스레드가 같은 캐시된 ByteBuffer를 공유하며 position이 오염된 것으로, Buffer는 thread-safe하지 않다.
- synchronized(성능 손해) 대신 duplicate()로 스레드별 지역 버퍼를 만들어 해결했다(asReadOnlyBuffer 권장과 동일 원리).
상세 정리- 구조: 캐시 미사용 시 매 요청마다 KMS 클라이언트를 호출해야 하므로, GenerateDataKey로 받은 Data Key를 일정 시간 Caffeine 캐시에 보관해 API 호출·비용을 줄였다.
- 증상: EncryptService가 캐시에서 Data Key를 꺼내 SecretKey.createKey로 암호화 키를 만드는데, createKey 내부 getByteArray의 b.get(byteArray)에서 오류가 났다.
- 코드 추적: get(byte[])는 get(dst,0,dst.length)를 거쳐 HeapByteBuffer.get으로, 다시 position(pos+length)를 호출한다.
- 예외 지점: Buffer.position은 newPosition이 limit보다 크거나 음수면 createPositionException을 던지는데, pos+length가 간헐적으로 버퍼 limit인 32를 넘겼다.
- 간헐성 분석: 캐시 유지 시간 내 같은 키인데도 여러 번 오류가 났고, 키 교체 시점 가설을 배제하니 동시성 이슈가 의심됐다.
- 재현 테스트: 100개 고정 스레드풀로 동시에 암호화를 수행하며 버퍼의 limit·position·length를 출력했다.
- 근거 확인: clear() 후 position은 0이어야 하는데 일부 스레드에서 32로 찍혀 메소드가 thread-safe하지 않음을 확인했고, Java Buffer 문서도 다중 스레드 동시 사용은 안전하지 않아 동기화가 필요하다고 명시한다.
- 해결책 후보: 첫째 synchronized 추가는 성능을 trade-off해야 하고, 둘째 duplicate()로 단일 스레드용 지역 버퍼를 생성한다.
- 최종: getPlaintext() 자바독이 thread-safe를 위해 asReadOnlyBuffer 사용을 권장하며, duplicate()가 같은 원리라 스레드마다 새 객체를 만들어 동시성 이슈를 해결했다.
왜 읽나캐시된 공유 객체(ByteBuffer)의 동시성 버그를 코드 레벨로 추적·해결하는 과정을 보고 싶은 백엔드 개발자에게.