From d86d6843db623867ce078f95aad7d9137b54e33a Mon Sep 17 00:00:00 2001 From: ALbertIM0427 Date: Sun, 30 Nov 2025 00:12:39 +0900 Subject: [PATCH] =?UTF-8?q?refactor(image):=20S3=20Presigned=20PUT=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EC=A0=84=ED=99=98=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A9=94=ED=83=80=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - PresignedPostResponse를 PresignedPutResponse로 명명 변경 - S3PresignedUrlGenerator에서 메타데이터를 별도 필드로 분리 - ImageUploadService, ImageApi 등 관련 클래스 타입 업데이트 - PUT 방식에 맞는 응답 구조로 개선 Frontend: - image-upload.html에서 S3 메타데이터 헤더 직접 설정 - x-amz-meta-* 헤더를 통해 파일 정보 전송 - Content-Type 및 메타데이터 정확한 전달 개선 Tests: - 모든 테스트에서 fields 검증 제거 및 metadata 검증으로 변경 - PUT 방식에 맞는 테스트 케이스로 업데이트 - API 응답 구조 변경에 따른 테스트 코드 수정 Configuration: - application-prod.yml에서 불필요한 Hibernate dialect 설정 제거 - commons-logging 충돌 경고 해결을 위한 의존성 정리 - 테스트 환경에서 Flyway 비활성화로 개발 속도 향상 --- .github/workflows/build.yml | 12 ++++----- .../s3/S3PresignedUrlGenerator.kt | 27 +++++++++---------- .../adapter/webapi/image/ImageApi.kt | 4 +-- ...ostResponse.kt => PresignedPutResponse.kt} | 4 +-- .../image/provided/ImageUploadRequester.kt | 4 +-- .../image/required/PresignedUrlGenerator.kt | 4 +-- .../image/service/ImageUploadService.kt | 4 +-- src/main/resources/application-prod.yml | 1 - .../image/fragments/image-upload.html | 8 +++++- .../adapter/webapi/image/ImageApiTest.kt | 2 +- .../provided/ImageUploadRequesterTest.kt | 7 ----- .../required/PresignedUrlGeneratorTest.kt | 11 -------- src/test/resources/application-devdb.yml | 8 +----- 13 files changed, 38 insertions(+), 58 deletions(-) rename src/main/kotlin/com/albert/realmoneyrealtaste/application/image/dto/{PresignedPostResponse.kt => PresignedPutResponse.kt} (69%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8bf180f1..55e4d6b7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -181,16 +181,16 @@ jobs: TASK_DEFINITION_ARN=$(aws ecs describe-task-definition --task-definition rmrt-task --query 'taskDefinition.taskDefinitionArn' --output text) aws ecs update-service \ - --cluster rmrt-cluster \ - --service rmrt-service-1 \ + --cluster rmrt-cluster-ec2version \ + --service rmrt-task-service-o4qq7b02 \ --task-definition $TASK_DEFINITION_ARN \ --force-new-deployment # 배포 완료 대기 echo "⏳ ECS 배포 완료 대기..." aws ecs wait services-stable \ - --cluster rmrt-cluster \ - --services rmrt-service-1 + --cluster rmrt-cluster-ec2version \ + --services rmrt-task-service-o4qq7b02 echo "✅ ECS 배포 완료!" @@ -198,8 +198,8 @@ jobs: run: | # 서비스 상태 확인 aws ecs describe-services \ - --cluster rmrt-cluster \ - --services rmrt-service-1 \ + --cluster rmrt-cluster-ec2version \ + --services rmrt-task-service-o4qq7b02 \ --query 'services[0].{status: status, runningCount: runningCount, desiredCount: desiredCount}' echo "🌐 실제 도메인: https://rmrt.albert-im.com" diff --git a/src/main/kotlin/com/albert/realmoneyrealtaste/adapter/infrastructure/s3/S3PresignedUrlGenerator.kt b/src/main/kotlin/com/albert/realmoneyrealtaste/adapter/infrastructure/s3/S3PresignedUrlGenerator.kt index d42d1fa3..b96c9cad 100644 --- a/src/main/kotlin/com/albert/realmoneyrealtaste/adapter/infrastructure/s3/S3PresignedUrlGenerator.kt +++ b/src/main/kotlin/com/albert/realmoneyrealtaste/adapter/infrastructure/s3/S3PresignedUrlGenerator.kt @@ -1,7 +1,7 @@ package com.albert.realmoneyrealtaste.adapter.infrastructure.s3 import com.albert.realmoneyrealtaste.application.image.dto.ImageUploadRequest -import com.albert.realmoneyrealtaste.application.image.dto.PresignedPostResponse +import com.albert.realmoneyrealtaste.application.image.dto.PresignedPutResponse import com.albert.realmoneyrealtaste.application.image.required.PresignedUrlGenerator import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value @@ -24,23 +24,22 @@ class S3PresignedUrlGenerator( private val logger = LoggerFactory.getLogger(S3PresignedUrlGenerator::class.java) - override fun generatePresignedPutUrl(imageKey: String, request: ImageUploadRequest): PresignedPostResponse { + override fun generatePresignedPutUrl(imageKey: String, request: ImageUploadRequest): PresignedPutResponse { val expiration = Instant.now().plus(Duration.ofMinutes(s3PutUrlExpirationMinutes)) + val metadata = mapOf( + "original-name" to request.fileName, + "content-type" to request.contentType, + "file-size" to request.fileSize.toString(), + "width" to request.width.toString(), + "height" to request.height.toString() + ) val putObjectRequest = PutObjectRequest.builder() .bucket(s3Config.bucketName) .key(imageKey) .contentType(request.contentType) - .metadata( - mapOf( - "original-name" to request.fileName, - "content-type" to request.contentType, - "file-size" to request.fileSize.toString(), - "width" to request.width.toString(), - "height" to request.height.toString() - ) - ) + .metadata(metadata) .build() val presignRequest = PutObjectPresignRequest.builder() @@ -52,11 +51,11 @@ class S3PresignedUrlGenerator( logger.info("Generated presigned PUT URL for key: $imageKey") - return PresignedPostResponse( + return PresignedPutResponse( uploadUrl = presignedRequest.url().toString(), key = imageKey, - fields = emptyMap(), // PUT 방식에서는 fields가 필요 없음 - expiresAt = expiration + expiresAt = expiration, + metadata = metadata, ) } diff --git a/src/main/kotlin/com/albert/realmoneyrealtaste/adapter/webapi/image/ImageApi.kt b/src/main/kotlin/com/albert/realmoneyrealtaste/adapter/webapi/image/ImageApi.kt index d82acca0..ad376bce 100644 --- a/src/main/kotlin/com/albert/realmoneyrealtaste/adapter/webapi/image/ImageApi.kt +++ b/src/main/kotlin/com/albert/realmoneyrealtaste/adapter/webapi/image/ImageApi.kt @@ -4,7 +4,7 @@ import com.albert.realmoneyrealtaste.adapter.infrastructure.security.MemberPrinc import com.albert.realmoneyrealtaste.application.image.dto.ImageInfo import com.albert.realmoneyrealtaste.application.image.dto.ImageUploadRequest import com.albert.realmoneyrealtaste.application.image.dto.ImageUploadResult -import com.albert.realmoneyrealtaste.application.image.dto.PresignedPostResponse +import com.albert.realmoneyrealtaste.application.image.dto.PresignedPutResponse import com.albert.realmoneyrealtaste.application.image.provided.ImageDeleter import com.albert.realmoneyrealtaste.application.image.provided.ImageReader import com.albert.realmoneyrealtaste.application.image.provided.ImageUploadRequester @@ -38,7 +38,7 @@ class ImageApi( fun requestImageUpload( @RequestBody @Valid request: ImageUploadRequest, @AuthenticationPrincipal member: MemberPrincipal, - ): ResponseEntity { + ): ResponseEntity { val response = imageUploadRequester.generatePresignedPostUrl(request, member.id) return ResponseEntity.ok(response) diff --git a/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/dto/PresignedPostResponse.kt b/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/dto/PresignedPutResponse.kt similarity index 69% rename from src/main/kotlin/com/albert/realmoneyrealtaste/application/image/dto/PresignedPostResponse.kt rename to src/main/kotlin/com/albert/realmoneyrealtaste/application/image/dto/PresignedPutResponse.kt index 98b60924..d06c7578 100644 --- a/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/dto/PresignedPostResponse.kt +++ b/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/dto/PresignedPutResponse.kt @@ -2,9 +2,9 @@ package com.albert.realmoneyrealtaste.application.image.dto import java.time.Instant -data class PresignedPostResponse( +data class PresignedPutResponse( val uploadUrl: String, val key: String, - val fields: Map, val expiresAt: Instant, + val metadata: Map, ) diff --git a/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/provided/ImageUploadRequester.kt b/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/provided/ImageUploadRequester.kt index 4869c295..9e4039f8 100644 --- a/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/provided/ImageUploadRequester.kt +++ b/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/provided/ImageUploadRequester.kt @@ -1,8 +1,8 @@ package com.albert.realmoneyrealtaste.application.image.provided import com.albert.realmoneyrealtaste.application.image.dto.ImageUploadRequest -import com.albert.realmoneyrealtaste.application.image.dto.PresignedPostResponse +import com.albert.realmoneyrealtaste.application.image.dto.PresignedPutResponse fun interface ImageUploadRequester { - fun generatePresignedPostUrl(request: ImageUploadRequest, userId: Long): PresignedPostResponse + fun generatePresignedPostUrl(request: ImageUploadRequest, userId: Long): PresignedPutResponse } diff --git a/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/required/PresignedUrlGenerator.kt b/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/required/PresignedUrlGenerator.kt index 0a40b03f..dc112260 100644 --- a/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/required/PresignedUrlGenerator.kt +++ b/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/required/PresignedUrlGenerator.kt @@ -1,10 +1,10 @@ package com.albert.realmoneyrealtaste.application.image.required import com.albert.realmoneyrealtaste.application.image.dto.ImageUploadRequest -import com.albert.realmoneyrealtaste.application.image.dto.PresignedPostResponse +import com.albert.realmoneyrealtaste.application.image.dto.PresignedPutResponse interface PresignedUrlGenerator { - fun generatePresignedPutUrl(imageKey: String, request: ImageUploadRequest): PresignedPostResponse + fun generatePresignedPutUrl(imageKey: String, request: ImageUploadRequest): PresignedPutResponse fun generatePresignedGetUrl(imageKey: String): String } diff --git a/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/service/ImageUploadService.kt b/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/service/ImageUploadService.kt index 6f41fe05..45770ad9 100644 --- a/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/service/ImageUploadService.kt +++ b/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/service/ImageUploadService.kt @@ -2,7 +2,7 @@ package com.albert.realmoneyrealtaste.application.image.service import com.albert.realmoneyrealtaste.application.image.dto.ImageUploadRequest import com.albert.realmoneyrealtaste.application.image.dto.ImageUploadResult -import com.albert.realmoneyrealtaste.application.image.dto.PresignedPostResponse +import com.albert.realmoneyrealtaste.application.image.dto.PresignedPutResponse import com.albert.realmoneyrealtaste.application.image.exception.ImageConfirmUploadException import com.albert.realmoneyrealtaste.application.image.exception.ImageGenerateException import com.albert.realmoneyrealtaste.application.image.provided.ImageKeyGenerator @@ -32,7 +32,7 @@ class ImageUploadService( private val logger = LoggerFactory.getLogger(ImageUploadService::class.java) - override fun generatePresignedPostUrl(request: ImageUploadRequest, userId: Long): PresignedPostResponse { + override fun generatePresignedPostUrl(request: ImageUploadRequest, userId: Long): PresignedPutResponse { try { // 1. 사용자 검증 val todayUploadCount = imageReader.getTodayUploadCount(userId) diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index b8a2cc65..a1dcb7d0 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -24,7 +24,6 @@ spring: properties: hibernate: format_sql: false - dialect: org.hibernate.dialect.MySQLDialect jdbc: time_zone: Asia/Seoul diff --git a/src/main/resources/templates/image/fragments/image-upload.html b/src/main/resources/templates/image/fragments/image-upload.html index 63d905a3..25092d59 100644 --- a/src/main/resources/templates/image/fragments/image-upload.html +++ b/src/main/resources/templates/image/fragments/image-upload.html @@ -247,10 +247,16 @@
// Presigned PUT URL 방식 사용 console.log(uploadRequest) console.log(uploadRequest.uploadUrl) + let metadata = uploadRequest.metadata const response = await fetch(uploadRequest.uploadUrl, { method: 'PUT', headers: { - 'Content-Type': file.type + 'Content-Type': metadata['content-type'], + 'x-amz-meta-content-type': metadata['content-type'], + 'x-amz-meta-file-size': metadata['file-size'], + 'x-amz-meta-height': metadata['height'], + 'x-amz-meta-original-name': metadata['original-name'], + 'x-amz-meta-width': metadata['width'] }, body: file }); diff --git a/src/test/kotlin/com/albert/realmoneyrealtaste/adapter/webapi/image/ImageApiTest.kt b/src/test/kotlin/com/albert/realmoneyrealtaste/adapter/webapi/image/ImageApiTest.kt index 657de872..fab07d03 100644 --- a/src/test/kotlin/com/albert/realmoneyrealtaste/adapter/webapi/image/ImageApiTest.kt +++ b/src/test/kotlin/com/albert/realmoneyrealtaste/adapter/webapi/image/ImageApiTest.kt @@ -67,7 +67,7 @@ class ImageApiTest : IntegrationTestBase() { .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.uploadUrl").isString) .andExpect(jsonPath("$.key").isString) - .andExpect(jsonPath("$.fields").isMap) + .andExpect(jsonPath("$.metadata").isMap) .andExpect(jsonPath("$.expiresAt").isString) } diff --git a/src/test/kotlin/com/albert/realmoneyrealtaste/application/image/provided/ImageUploadRequesterTest.kt b/src/test/kotlin/com/albert/realmoneyrealtaste/application/image/provided/ImageUploadRequesterTest.kt index f553e626..fdcd4153 100644 --- a/src/test/kotlin/com/albert/realmoneyrealtaste/application/image/provided/ImageUploadRequesterTest.kt +++ b/src/test/kotlin/com/albert/realmoneyrealtaste/application/image/provided/ImageUploadRequesterTest.kt @@ -44,9 +44,6 @@ class ImageUploadRequesterTest : IntegrationTestBase() { assertTrue(response.key.contains("images/")) assertTrue(response.key.endsWith(".jpg")) - assertNotNull(response.fields) - assertTrue(response.fields.isEmpty()) - // 만료 시간 확인 (기본 15분 후) val expectedMinExpiry = Instant.now().plus(Duration.ofMinutes(14)) val expectedMaxExpiry = Instant.now().plus(Duration.ofMinutes(16)) @@ -106,7 +103,6 @@ class ImageUploadRequesterTest : IntegrationTestBase() { assertNotNull(response.uploadUrl) assertNotNull(response.key) - assertTrue(response.fields.isEmpty()) assertTrue(response.expiresAt.isAfter(Instant.now())) } } @@ -266,9 +262,6 @@ class ImageUploadRequesterTest : IntegrationTestBase() { assertNotNull(response.key) assertTrue(response.key.isNotBlank()) - assertNotNull(response.fields) - assertTrue(response.fields.isEmpty()) - // 만료 시간 검증 assertTrue(response.expiresAt.isAfter(Instant.now())) assertTrue(response.expiresAt.isAfter(Instant.now().plus(Duration.ofMinutes(10)))) diff --git a/src/test/kotlin/com/albert/realmoneyrealtaste/application/image/required/PresignedUrlGeneratorTest.kt b/src/test/kotlin/com/albert/realmoneyrealtaste/application/image/required/PresignedUrlGeneratorTest.kt index 517fc295..72402e7a 100644 --- a/src/test/kotlin/com/albert/realmoneyrealtaste/application/image/required/PresignedUrlGeneratorTest.kt +++ b/src/test/kotlin/com/albert/realmoneyrealtaste/application/image/required/PresignedUrlGeneratorTest.kt @@ -38,7 +38,6 @@ class PresignedUrlGeneratorTest( assertTrue(response.uploadUrl.contains(imageKey)) assertEquals(imageKey, response.key) - assertTrue(response.fields.isEmpty()) // PUT 방식에서는 fields가 비어있음 // 만료 시간 확인 val expectedMinExpiry = Instant.now().plus(Duration.ofMinutes(14)) @@ -72,8 +71,6 @@ class PresignedUrlGeneratorTest( assertEquals(key2, response2.key) assertTrue(response1.uploadUrl.contains(key1)) assertTrue(response2.uploadUrl.contains(key2)) - assertTrue(response1.fields.isEmpty()) - assertTrue(response2.fields.isEmpty()) } @Test @@ -102,7 +99,6 @@ class PresignedUrlGeneratorTest( assertNotNull(response.uploadUrl) assertEquals(imageKey, response.key) - assertTrue(response.fields.isEmpty()) assertTrue(response.expiresAt.isAfter(Instant.now())) } } @@ -133,7 +129,6 @@ class PresignedUrlGeneratorTest( assertNotNull(response.uploadUrl) assertEquals(imageKey, response.key) - assertTrue(response.fields.isEmpty()) assertTrue(response.expiresAt.isAfter(Instant.now())) } } @@ -163,7 +158,6 @@ class PresignedUrlGeneratorTest( assertNotNull(response.uploadUrl) assertEquals(imageKey, response.key) assertTrue(response.uploadUrl.contains(imageKey)) - assertTrue(response.fields.isEmpty()) assertTrue(response.expiresAt.isAfter(Instant.now())) } } @@ -188,7 +182,6 @@ class PresignedUrlGeneratorTest( // Then assertNotNull(minResponse.uploadUrl) assertEquals(minImageKey, minResponse.key) - assertTrue(minResponse.fields.isEmpty()) // Given - 최대값 val maxRequest = ImageUploadRequest( @@ -208,7 +201,6 @@ class PresignedUrlGeneratorTest( // Then assertNotNull(maxResponse.uploadUrl) assertEquals(maxImageKey, maxResponse.key) - assertTrue(maxResponse.fields.isEmpty()) } @Test @@ -236,8 +228,6 @@ class PresignedUrlGeneratorTest( assertNotNull(response.key) assertEquals(imageKey, response.key) - assertNotNull(response.fields) - assertTrue(response.fields.isEmpty()) // PUT 방식에서는 fields가 비어있음 assertNotNull(response.expiresAt) assertTrue(response.expiresAt.isAfter(Instant.now())) @@ -255,7 +245,6 @@ class PresignedUrlGeneratorTest( height = 600, imageType = ImageType.POST_IMAGE ) - val expirationMinutes = 30L // When val response1 = presignedUrlGenerator.generatePresignedPutUrl(imageKey, request) diff --git a/src/test/resources/application-devdb.yml b/src/test/resources/application-devdb.yml index e654c0ac..34f9eeeb 100644 --- a/src/test/resources/application-devdb.yml +++ b/src/test/resources/application-devdb.yml @@ -1,10 +1,4 @@ spring: jpa: hibernate: - ddl-auto: validate - # Flyway 설정 - flyway: - enabled: true - baseline-on-migrate: true - baseline-version: 0 - validate-on-migrate: true + ddl-auto: create-drop