1. 개요

MinIO 를 쓰다 SeaweedFS 로 옮기면서 클라이언트를 MinIO SDK 에서 AWS SDK v2 로 바꿨다. S3 호환이니 되겠지 했고, 됐다. 그런데 의존성 정리를 하며 BOM 을 2.21 → 2.35 로 올렸을 때, 버전 업그레이드가 없던 위험을 새로 들였다. 예외도 로그도 없이 업로드된 파일만 깨지는 종류의 위험이다. 세 프로젝트에 같은 설정을 고정하게 된 경위를 적는다.


2. 핵심 내용

2-1. 2.30 부터 바뀐 기본값

AWS SDK for Java 2.30.0 부터 requestChecksumCalculation 의 기본이 WHEN_REQUIRED 에서 WHEN_SUPPORTED 로 바뀌었다. 무슨 뜻인가.

  • WHEN_REQUIRED: API 가 체크섬을 요구하는 작업(예: DeleteObjects)에만 붙인다.
  • WHEN_SUPPORTED: API 가 체크섬을 지원하는 모든 작업에 붙인다. PutObject 포함.

PutObject 에 체크섬을 붙이면 SDK 는 본문을 aws-chunked 인코딩으로 감싸고 마지막에 CRC32 트레일러를 붙인다. 진짜 S3 는 이걸 이해하고 감싼 것을 벗겨 저장한다.

2-2. 게이트웨이가 모르면 어떻게 되나

S3 호환 게이트웨이(MinIO 구버전, SeaweedFS 일부 버전, 기타)가 aws-chunked 트레일러 프레이밍을 모르면 두 가지 중 하나다.

  • 400 을 돌려준다. 이건 낫다. 즉시 드러난다.
  • 감싼 바이트를 그대로 객체로 저장하고 200 을 돌려준다. 청크 길이 헤더와 트레일러가 파일 안에 들어간 채로.

두 번째면 업로드는 성공이고, 로그도 깨끗하고, 다운로드해 보기 전까지 아무도 모른다. 이미지는 열리지 않고, PDF 는 깨져 있다. “언제부터” 를 추적하면 BOM 을 올린 배포 시점이다.

2-3. 설정을 코드 상수로 고정

@Bean
S3Client s3Client(S3Properties p) {
    return S3Client.builder()
        .endpointOverride(URI.create(p.endpoint()))
        .region(Region.of(p.region()))             // SigV4 형식용, 게이트웨이는 무시
        .credentialsProvider(StaticCredentialsProvider.create(
            AwsBasicCredentials.create(p.accessKey(), p.secretKey())))
        .forcePathStyle(true)                       // bucket.host 가 아니라 host/bucket
        .requestChecksumCalculation(RequestChecksumCalculation.WHEN_REQUIRED)
        .responseChecksumValidation(ResponseChecksumValidation.WHEN_REQUIRED)
        .build();
}
  • WHEN_REQUIRED 둘: 요청·응답 모두. 2.30 이전 동작으로 되돌린다.
  • forcePathStyle(true): 가상 호스트 방식(bucket.host)은 와일드카드 DNS 와 TLS 가 필요하다. 홈랩 게이트웨이는 경로 방식이다. 이걸 설정값으로 열어 두면 누가 false 로 바꿔 놓고 “가끔 안 된다” 가 된다. 코드 상수로.
  • region: 게이트웨이는 리전을 안 보지만 SigV4 서명에 형식상 필요하다. 아무 값이나 넣되 비우면 안 된다.

세 프로젝트 모두 이 모양으로 맞췄다.

2-4. “다른 프로젝트는 같은 버전으로 잘 돈다” 는 근거가 아니다

한 프로젝트에서 이 문제를 찾고 나머지 둘을 볼 때 “저기는 2.54 로 잘 돌고 있으니 괜찮겠지” 라고 생각했다. 실측하니 그 프로젝트의 SeaweedFS 는 버전이 달랐고, 그 버전은 aws-chunked 를 처리했다. 같은 SDK 버전이라도 게이트웨이 버전이 다르면 결과가 다르다. 실제로 SeaweedFS 4.45 는 왕복 테스트가 통과했고, 그래서 그 프로젝트에서는 “문제 없음” 으로 보였다.

그럼 왜 거기도 WHEN_REQUIRED 로 고정했나. 게이트웨이를 업그레이드하거나 바꾸는 날 다시 터질 수 있고, 체크섬이 붙어서 얻는 이득이 이 환경에선 없기 때문이다. 없던 위험을 들이지 않는 쪽으로.

2-5. 왕복 테스트는 가드가 못 된다

이 설정을 지키는 테스트를 넣으려 했다. 첫 시도는 왕복이다. 업로드하고 다운로드해 바이트가 같은지. Testcontainers 로 실제 SeaweedFS 를 띄운다.

문제는 2-4 다. 테스트 컨테이너의 SeaweedFS 가 aws-chunked 를 처리하는 버전이면, WHEN_SUPPORTED 로 돌아가도 왕복이 통과한다. 설정을 누가 지워도 초록이다. 가드가 아니다.

그래서 요청 모양을 보는 테스트 를 따로 뒀다. SDK 의 실행 인터셉터로 PutObject 요청을 잡아 Content-Encoding: aws-chunked 나 x-amz-trailer 헤더가 없는지 단언한다. 게이트웨이가 뭘 하든 상관없이 “우리가 보내는 요청에 트레일러가 없다” 를 확인한다. 설정을 WHEN_SUPPORTED 로 바꿔 보고 빨간불이 나는 것을 확인한 뒤 머지했다.

2-6. 테스트 컨테이너는 운영과 같은 것으로

MinIO 를 테스트 컨테이너로 쓰던 저장소가 있었다. 어느 날 Docker Hub 의 minio/minio 이미지가 404 가 됐다(라이선스·배포 정책 변경). 테스트가 통째로 죽었다.

운영이 SeaweedFS 인데 테스트가 MinIO 인 것도 이상했다. 테스트 컨테이너를 운영과 같은 chrislusf/seaweedfs 같은 버전으로 바꿨다. S3 호환은 “대체로 호환” 이지 동일이 아니다. 운영에서 겪을 차이를 테스트에서도 겪는 게 낫다.


3. 마무리

요약

  • AWS SDK v2 2.30+ 는 PutObject 에 aws-chunked 트레일러를 붙인다. 모르는 게이트웨이는 그대로 저장하고 200.
  • requestChecksumCalculation / responseChecksumValidation 을 WHEN_REQUIRED 로 고정. path-style 은 코드 상수.
  • 같은 SDK 버전이라도 게이트웨이 버전이 다르면 결과가 다르다. 다른 프로젝트는 근거가 아니다.
  • 왕복 테스트는 게이트웨이가 착하면 통과한다. 요청 모양을 보는 테스트가 가드다.
  • 테스트 컨테이너는 운영과 같은 것으로.