팀원 프로필 사진을 서버 디스크가 아닌 S3에 저장한다. 서버 디스크에 저장하면 서버가 죽을 때 사진도 함께 날아가기 때문이다. 이미지는 Presigned URL을 통해서만 접근 가능하도록 구현한다.
s3:PutObject, s3:GetObject){
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:GetObject"],
"Resource": ["arn:aws:s3:::cloudproject-prod-profile-images/*"]
}
]
}
POST /api/members/{id}/profile-image — MultipartFile로 이미지를 받아 S3에 업로드하고, 저장된 key를 DB에 업데이트GET /api/members/{id}/profile-image — Presigned URL을 생성해 반환 (유효기간 7일)GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder()
.signatureDuration(Duration.ofDays(7)) // 요구사항: 7일
.getObjectRequest(getObjectRequest)
.build();
String presignedUrl = s3Presigner.presignGetObject(presignRequest).url().toString();
DB에는 이미지 URL 전체가 아니라 S3 오브젝트 key만 저장했다. Presigned URL은 매번 새로 서명해야 하는 값이라, URL 자체를 저장해두면 만료됐을 때 의미가 없기 때문이다.
Could not resolve placeholder 'app.s3.bucket-name
spring: 안에 spring:이 중첩되어 있었고, app:이 info: 아래로 잘못 들여쓰기 되어 있었다. YAML은 들여쓰기 한 칸 차이로 완전히 다른 위치의 속성이 되어버린다는 걸 실감했다. 최상위 레벨로 위치를 바로잡아서 해결했다.
Before
info:
team-name: ${team-name}
app: # info 밑에 들어가 있었음
s3:
bucket-name: ...
After
info:
team-name: ${team-name}
app: # 최상위 레벨로 이동
s3:
bucket-name: ...
MissingServletRequestPartException: Required part 'file' is not present
파일 타입은 File로 잘 선택했는데, 정작 Key 이름 입력을 빠뜨림. 컨트롤러의 @RequestParam("file")과 정확히 일치하는 file을 Key 칸에 입력해야 서버가 인식한다는 걸 알게 됬다.
form-data로 이미지 업로드 → 200 OKGET /profile-image 호출 → Presigned URL 발급 확인X-Amz-Expires=604800 확인 → 604800초 = 정확히 7일