직관팟 생성 시 직관팟의 썸네일은, MultipartFile이나 base64로 이미지를 인코딩해 보내는 것이 아닌, Presigned URL을 통해 보내기로 했다.

이는 전자의 방식으로 이미지를 서버에 보내고, 서버에서 Object Storage에 저장하는 방식은 서버 과부하가 예상되므로, 프론트가 서버에서 Presigned URL을 받으면 PUT으로 프론트 측에서 직접 저장하는 방식이다.
현재는 On-Premise 환경에서 애플리케이션을 운영하므로 S3 등 타 클라우드의 Object Storage가 아닌, Minio를 띄워서 거기에 저장할 계획이다.
build.gradle에 Minio 사용 위한 dependency를 작성한다. Minio는 AWS API를 지원하기에, AWS API를 통해 작성해보겠다.
// Object Storage
implementation 'software.amazon.awssdk:s3:2.20.45'
cf> MinioClient로 관리하고 싶으면 https://min.io/docs/minio/linux/developers/java/API.html#getPresignedObjectUrl 서 볼 수 있다.
yaml 파일에 접근을 위한 ACCESS_KEY와 SECRET_KEY, 그리고 bucket과 endpoint를 작성해줘야 한다.
cloud:
aws:
credentials:
access-key: ${AWS_S3_ACCESS_KEY}
secret-key: ${AWS_S3_SECRET_KEY}
s3:
bucket : ${AWS_S3_BUCKET_NAME}
endpoint : ${AWS_S3_ENDPOINT}
엔드포인트의 경우, minio를 띄운 ip와 포트를 작성해주도록 하자.
ACCESS_KEY와 SECRET_KEY 는 minio 접속 후 좌상단 두 번째 항목에서 생성할 수 있다.

버킷은 Admin - Buckets 에서 만들 수 있다.

S3 연결을 위한 S3Config 파일을 작성해주자. 아래 s3Presigner() 를 bean으로 등록해 URL 생성 시 사용할 수 있도록 했다.
@Configuration
public class S3Config {
@Value("${cloud.aws.credentials.access-key}")
private String accessKey;
@Value("${cloud.aws.credentials.secret-key}")
private String secretKey;
@Value("${cloud.aws.s3.endpoint}")
private String endpoint;
@Bean // Minio, kakao cloud 대응
public S3Presigner s3Presigner() {
return S3Presigner.builder()
.endpointOverride(URI.create(endpoint))
.credentialsProvider(
StaticCredentialsProvider.create(
AwsBasicCredentials.create(accessKey, secretKey)
)
)
.serviceConfiguration(S3Configuration.builder()
.pathStyleAccessEnabled(true)
.build())
.region(Region.of("kr-central-2"))
.build();
}
}
위에서 bean으로 등록한 S3Presigner를 이용해 프론트가 PUT을 통해 이미지를 업로드할 수 있는 Presigned URL을 생성한다. (아래의 10은 10분간 유효하다는 의미이다. 즉 10분이 지나면 그 URL로는 업로드가 안 된다)
@Slf4j
@Component
@RequiredArgsConstructor
public class S3Service {
@Value("${cloud.aws.s3.bucket}")
private String bucketName;
private final S3Client s3Client;
private final S3Presigner s3Presigner;
public String generatePresignedUrl(String imageFileName) {
PutObjectRequest objectRequest = createPutObjectRequest(imageFileName);
PutObjectPresignRequest presignRequest = createPresignedRequest(objectRequest, 10);
return s3Presigner.presignPutObject(presignRequest).url().toString();
}
private PutObjectRequest createPutObjectRequest(String imageFileName) {
return PutObjectRequest.builder()
.bucket(bucketName)
.key(imageFileName)
.build();
}
private static PutObjectPresignRequest createPresignedRequest(PutObjectRequest objectRequest, int minutes) {
return PutObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(minutes))
.putObjectRequest(objectRequest)
.build();
}
}
이제 위 generator를 service 단에 적용시키겠다.
@Service
@RequiredArgsConstructor
public class PartyService {
private final S3Service s3Service;
public PresignedUrlForSaveImageResponse generatePresignedUrlForSaveImage(PresignedUrlForSaveImageRequest request) {
return new PresignedUrlForSaveImageResponse(s3Service.generatePresignedUrl(request.imageFileName()));
}
}
PostMan으로 잘 나오는 것도 확인했으니, 안심하고 PR을 올리도록 하자.

그리고 실패했다...

로그를 확인해보니 application-test.yaml 에서 S3 위한 값을 넣지 않아줘서 발생해 S3Config가 만들어지지 않아 발생한 모양이다.

따라서 아래처럼 @Profile("!test") 를 넣고, @MockitoBean을 통해 Presigner를 Mocking 해 테스트를 완료하였다!
@Profile("!test")
@Configuration
public class S3Config {
@Value("${cloud.aws.credentials.access-key}")
private String accessKey;
@Value("${cloud.aws.credentials.secret-key}")
private String secretKey;
@Value("${cloud.aws.s3.endpoint}")
private String endpoint;
@Bean
public S3Presigner s3Presigner() {
return S3Presigner.builder()
.endpointOverride(URI.create(endpoint))
.credentialsProvider(
StaticCredentialsProvider.create(
AwsBasicCredentials.create(accessKey, secretKey)
)
)
.region(Region.US_EAST_1)
.build();
}
}
class PartyServiceTest extends IntegrationTestSupport {
@Autowired
private PartyService partyService;
@MockitoBean
private S3PresignedUrlGenerator s3PresignedUrlGenerator;
@Autowired
private PartyRepository partyRepository;
@Autowired
private PartyJoinRepository partyJoinRepository;
@Autowired
private PartyAgeRepository partyAgeRepository;
@Autowired
private PartyThumbnailUrlRepository partyThumbnailUrlRepository;
@Autowired
private ChatRoomRepository chatRoomRepository;
@Autowired
private ChatPartRepository chatPartRepository;
@AfterEach
void tearDown() {
partyThumbnailUrlRepository.deleteAllInBatch();
partyAgeRepository.deleteAllInBatch();
partyJoinRepository.deleteAllInBatch();
partyRepository.deleteAllInBatch();
chatRoomRepository.deleteAll();
chatPartRepository.deleteAll();
}
@DisplayName("이미지 저장 위한 Presigned URL 을 발급받을 수 있다.")
@Test
void generatePresignedUrlForSaveImage() {
// given
String responseUrl = "http://presigned-url.com";
PresignedUrlForSaveImageRequest request = new PresignedUrlForSaveImageRequest("image.jpg");
given(s3PresignedUrlGenerator.generatePresignedUrl(request.imageFileName())).willReturn(responseUrl);
// when
PresignedUrlForSaveImageResponse result = partyService.generatePresignedUrlForSaveImage(request);
// then
assertThat(result.presignedUrl()).isEqualTo(responseUrl);
}
}'프로젝트 > PlayUs' 카테고리의 다른 글
| 6. Github Actions CI 에서의 영어 출력으로 인한 테스트 실패 (0) | 2025.05.14 |
|---|---|
| 5. Spring Data MongoDB에서의 Aggregation (join) (0) | 2025.05.14 |
| 4. TroubleShooting- HV000151 발생 (0) | 2025.05.10 |
| 2. TroubleShooting - CI 환경에서 MySQL TestContainer 실행 실패 (0) | 2025.05.05 |
| 1. Enum Validation (0) | 2025.05.05 |