From d98d88af6b4a580cea1b85f4dff1f143f32237bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=82=98=EC=9A=A9=EC=A4=80?= <141994188+youngJun99@users.noreply.github.com> Date: Sun, 1 Feb 2026 20:48:28 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20OCI=EB=A1=9C=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cloth/service/ClothAiServiceImpl.java | 12 +- .../cloth/service/ClothServiceImpl.java | 6 +- .../history/service/HistoryServiceImpl.java | 6 +- .../image/event/ImageEventListener.java | 8 +- .../src/main/resources/application-dev.yml | 17 +- .../src/main/resources/application-local.yml | 17 +- .../src/main/resources/application-prod.yml | 17 +- .../cloth/service/ClothServiceTest.java | 8 +- .../service/HistoryServiceImplTest.java | 8 +- clokey-infrastructure/build.gradle | 2 +- .../main/java/org/clokey/config/S3Config.java | 39 ++- .../org/clokey/properties/AwsProperties.java | 6 - .../org/clokey/properties/OciProperties.java | 12 + .../clokey/properties/PropertiesConfig.java | 4 +- .../org/clokey/properties/S3Properties.java | 6 - .../clokey/properties/StorageProperties.java | 6 + .../src/main/java/org/clokey/util/S3Util.java | 173 ------------ .../java/org/clokey/util/StorageUtil.java | 262 ++++++++++++++++++ 18 files changed, 356 insertions(+), 253 deletions(-) delete mode 100644 clokey-infrastructure/src/main/java/org/clokey/properties/AwsProperties.java create mode 100644 clokey-infrastructure/src/main/java/org/clokey/properties/OciProperties.java delete mode 100644 clokey-infrastructure/src/main/java/org/clokey/properties/S3Properties.java create mode 100644 clokey-infrastructure/src/main/java/org/clokey/properties/StorageProperties.java delete mode 100644 clokey-infrastructure/src/main/java/org/clokey/util/S3Util.java create mode 100644 clokey-infrastructure/src/main/java/org/clokey/util/StorageUtil.java diff --git a/clokey-api/src/main/java/org/clokey/domain/cloth/service/ClothAiServiceImpl.java b/clokey-api/src/main/java/org/clokey/domain/cloth/service/ClothAiServiceImpl.java index 4b3b799b..dd98ee4e 100644 --- a/clokey-api/src/main/java/org/clokey/domain/cloth/service/ClothAiServiceImpl.java +++ b/clokey-api/src/main/java/org/clokey/domain/cloth/service/ClothAiServiceImpl.java @@ -34,7 +34,7 @@ import org.clokey.global.util.MemberUtil; import org.clokey.member.entity.Member; import org.clokey.properties.WebClientProperties; -import org.clokey.util.S3Util; +import org.clokey.util.StorageUtil; import org.clokey.util.WebClientUtil; import org.springframework.stereotype.Service; @@ -47,7 +47,7 @@ public class ClothAiServiceImpl implements ClothAiService { private final MemberUtil memberUtil; private final CategoryRepository categoryRepository; - private final S3Util s3Util; + private final StorageUtil storageUtil; private final WebClientUtil webClientUtil; private final WebClientProperties webClientProperties; @@ -61,7 +61,7 @@ public ClothImagesPresignedUrlResponse getClothUploadPresignedUrls( request.payloads().stream() .map( req -> - s3Util.createPresignedUrl( + storageUtil.createPresignedUrl( ImageType.CLOTH_IMAGE, currentMember.getId(), req.fileExtension(), @@ -262,13 +262,13 @@ public ClothDetectResponse detectClothes(ClothDetectRequest request) { } private void validateImageUrls(List imageUrls) { - if (!s3Util.doAllFilesExistByUrls(imageUrls)) { + if (!storageUtil.doAllFilesExistByUrls(imageUrls)) { throw new BaseCustomException(ClothErrorCode.ClOTH_NOT_FOUND); } } private void validateImageUrl(String imageUrl) { - if (!s3Util.doesFileExistByUrl(imageUrl)) { + if (!storageUtil.doesFileExistByUrl(imageUrl)) { throw new BaseCustomException(HistoryErrorCode.HISTORY_IMAGE_NOT_FOUND); } } @@ -295,7 +295,7 @@ private List createPresignedUrls(Long memberId, int count) { return java.util.stream.IntStream.range(0, count) .mapToObj( i -> - s3Util.createPresignedUrlWithoutMd5( + storageUtil.createPresignedUrlWithoutMd5( ImageType.CLOTH_IMAGE, memberId, FileExtension.JPEG)) .toList(); } diff --git a/clokey-api/src/main/java/org/clokey/domain/cloth/service/ClothServiceImpl.java b/clokey-api/src/main/java/org/clokey/domain/cloth/service/ClothServiceImpl.java index 903ac5e2..383cc9b8 100644 --- a/clokey-api/src/main/java/org/clokey/domain/cloth/service/ClothServiceImpl.java +++ b/clokey-api/src/main/java/org/clokey/domain/cloth/service/ClothServiceImpl.java @@ -26,7 +26,7 @@ import org.clokey.global.util.MemberUtil; import org.clokey.member.entity.Member; import org.clokey.response.SliceResponse; -import org.clokey.util.S3Util; +import org.clokey.util.StorageUtil; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; @@ -44,7 +44,7 @@ public class ClothServiceImpl implements ClothService { private final ApplicationEventPublisher eventPublisher; private final CoordinateClothRepository coordinateClothRepository; - private final S3Util s3Util; + private final StorageUtil storageUtil; @Override @Transactional @@ -80,7 +80,7 @@ public ClothCreateResponse createClothes(ClothCreateRequests request) { List clothImageUrls = request.content().stream().map(ClothCreateRequest::clothImageUrl).toList(); // 모든 선택된 url들을 확정하는 로직. - s3Util.updateTagsToCompleteByUrls(clothImageUrls); + storageUtil.updateTagsToCompleteByUrls(clothImageUrls); return ClothCreateResponse.from(clothes); } diff --git a/clokey-api/src/main/java/org/clokey/domain/history/service/HistoryServiceImpl.java b/clokey-api/src/main/java/org/clokey/domain/history/service/HistoryServiceImpl.java index e1816919..fff0f752 100644 --- a/clokey-api/src/main/java/org/clokey/domain/history/service/HistoryServiceImpl.java +++ b/clokey-api/src/main/java/org/clokey/domain/history/service/HistoryServiceImpl.java @@ -38,7 +38,7 @@ import org.clokey.member.entity.Member; import org.clokey.member.enums.Visibility; import org.clokey.report.enums.TargetType; -import org.clokey.util.S3Util; +import org.clokey.util.StorageUtil; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -65,7 +65,7 @@ public class HistoryServiceImpl implements HistoryService { private final ReportRepository reportRepository; private final MemberRepository memberRepository; private final ApplicationEventPublisher eventPublisher; - private final S3Util s3Util; + private final StorageUtil storageUtil; @Override @Transactional @@ -384,7 +384,7 @@ public HistoryImagesPresignedUrlResponse getHistoryUploadPresignedUrls( request.payloads().stream() .map( payload -> - s3Util.createPresignedUrl( + storageUtil.createPresignedUrl( ImageType.HISTORY_IMAGE, currentMember.getId(), payload.fileExtension(), diff --git a/clokey-api/src/main/java/org/clokey/domain/image/event/ImageEventListener.java b/clokey-api/src/main/java/org/clokey/domain/image/event/ImageEventListener.java index ad83e781..d91f03b1 100644 --- a/clokey-api/src/main/java/org/clokey/domain/image/event/ImageEventListener.java +++ b/clokey-api/src/main/java/org/clokey/domain/image/event/ImageEventListener.java @@ -1,7 +1,7 @@ package org.clokey.domain.image.event; import lombok.RequiredArgsConstructor; -import org.clokey.util.S3Util; +import org.clokey.util.StorageUtil; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import org.springframework.transaction.event.TransactionPhase; @@ -11,17 +11,17 @@ @RequiredArgsConstructor public class ImageEventListener { - private final S3Util s3Util; + private final StorageUtil storageUtil; @Async @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void handleImagesDeleteEvent(ImagesDeleteEvent event) { - s3Util.deleteAllByUrls(event.imageUrls()); + storageUtil.deleteAllByUrls(event.imageUrls()); } @Async @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void handleImageDeleteEvent(ImageDeleteEvent event) { - s3Util.deleteByUrl(event.imageUrl()); + storageUtil.deleteByUrl(event.imageUrl()); } } diff --git a/clokey-api/src/main/resources/application-dev.yml b/clokey-api/src/main/resources/application-dev.yml index 27ff94b4..26e0e733 100644 --- a/clokey-api/src/main/resources/application-dev.yml +++ b/clokey-api/src/main/resources/application-dev.yml @@ -71,13 +71,16 @@ jwt: refresh-token-expiration-time: ${JWT_REFRESH_TOKEN_EXPIRATION_TIME} issuer: ${JWT_ISSUER} -aws: - access-key-id: ${DEV_AWS_ACCESS_KEY_ID} - secret-access-key: ${DEV_AWS_SECRET_ACCESS_KEY} - region: ${AWS_REGION} - s3: - bucket: ${DEV_S3_BUCKET} - endpoint: ${DEV_S3_ENDPOINT:https://s3.ap-northeast-2.amazonaws.com} +oci: + tenancy-id: ${OCI_TENANCY_ID} + user-id: ${OCI_USER_ID} + fingerprint: ${OCI_FINGERPRINT} + private-key: ${OCI_PRIVATE_KEY} + region: ${OCI_REGION} + passphrase: ${OCI_PASSPHRASE:} + objectstorage: + namespace: ${OCI_OBJECTSTORAGE_NAMESPACE} + bucket: ${OCI_OBJECTSTORAGE_BUCKET} external: api: diff --git a/clokey-api/src/main/resources/application-local.yml b/clokey-api/src/main/resources/application-local.yml index d3a25664..c3465277 100644 --- a/clokey-api/src/main/resources/application-local.yml +++ b/clokey-api/src/main/resources/application-local.yml @@ -71,13 +71,16 @@ jwt: refresh-token-expiration-time: ${JWT_REFRESH_TOKEN_EXPIRATION_TIME} issuer: ${JWT_ISSUER} -aws: - access-key-id: ${AWS_ACCESS_KEY_ID} - secret-access-key: ${AWS_SECRET_ACCESS_KEY} - region: ${AWS_REGION} - s3: - bucket: ${S3_BUCKET} - endpoint: ${S3_ENDPOINT:https://s3.ap-northeast-2.amazonaws.com} +oci: + tenancy-id: ${OCI_TENANCY_ID} + user-id: ${OCI_USER_ID} + fingerprint: ${OCI_FINGERPRINT} + private-key: ${OCI_PRIVATE_KEY} + region: ${OCI_REGION} + passphrase: ${OCI_PASSPHRASE:} + objectstorage: + namespace: ${OCI_OBJECTSTORAGE_NAMESPACE} + bucket: ${OCI_OBJECTSTORAGE_BUCKET} external: api: diff --git a/clokey-api/src/main/resources/application-prod.yml b/clokey-api/src/main/resources/application-prod.yml index e6a4b270..ca95030c 100644 --- a/clokey-api/src/main/resources/application-prod.yml +++ b/clokey-api/src/main/resources/application-prod.yml @@ -71,13 +71,16 @@ jwt: refresh-token-expiration-time: ${JWT_REFRESH_TOKEN_EXPIRATION_TIME} issuer: ${JWT_ISSUER} -aws: - access-key-id: ${PROD_AWS_ACCESS_KEY_ID} - secret-access-key: ${PROD_AWS_SECRET_ACCESS_KEY} - region: ${AWS_REGION} - s3: - bucket: ${PROD_S3_BUCKET} - endpoint: ${PROD_S3_ENDPOINT:https://s3.ap-northeast-2.amazonaws.com} +oci: + tenancy-id: ${OCI_TENANCY_ID} + user-id: ${OCI_USER_ID} + fingerprint: ${OCI_FINGERPRINT} + private-key: ${OCI_PRIVATE_KEY} + region: ${OCI_REGION} + passphrase: ${OCI_PASSPHRASE:} + objectstorage: + namespace: ${OCI_OBJECTSTORAGE_NAMESPACE} + bucket: ${OCI_OBJECTSTORAGE_BUCKET} firebase: credentials-path: ${FIREBASE_CREDENTIALS_PATH} diff --git a/clokey-api/src/test/java/org/clokey/domain/cloth/service/ClothServiceTest.java b/clokey-api/src/test/java/org/clokey/domain/cloth/service/ClothServiceTest.java index 8f54c553..4ce4df22 100644 --- a/clokey-api/src/test/java/org/clokey/domain/cloth/service/ClothServiceTest.java +++ b/clokey-api/src/test/java/org/clokey/domain/cloth/service/ClothServiceTest.java @@ -48,7 +48,7 @@ import org.clokey.member.entity.OauthInfo; import org.clokey.member.enums.OauthProvider; import org.clokey.response.SliceResponse; -import org.clokey.util.S3Util; +import org.clokey.util.StorageUtil; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -80,7 +80,7 @@ class ClothServiceTest extends IntegrationTest { @Autowired private SituationRepository situationRepository; @MockitoBean private MemberUtil memberUtil; - @MockitoBean private S3Util s3Util; + @MockitoBean private StorageUtil storageUtil; @Autowired private ApplicationEvents applicationEvents; @Nested @@ -109,10 +109,10 @@ void setUp() { FileExtension.JPEG, "testMd5Hash1"), new ClothImagesUploadRequest.Payload( FileExtension.JPEG, "testMd5Hash2"))); - given(s3Util.createPresignedUrl(any(), anyLong(), any(), eq("testMd5Hash1"))) + given(storageUtil.createPresignedUrl(any(), anyLong(), any(), eq("testMd5Hash1"))) .willReturn("testUrl1"); - given(s3Util.createPresignedUrl(any(), anyLong(), any(), eq("testMd5Hash2"))) + given(storageUtil.createPresignedUrl(any(), anyLong(), any(), eq("testMd5Hash2"))) .willReturn("testUrl2"); // when diff --git a/clokey-api/src/test/java/org/clokey/domain/history/service/HistoryServiceImplTest.java b/clokey-api/src/test/java/org/clokey/domain/history/service/HistoryServiceImplTest.java index d77f89c6..7e01d525 100644 --- a/clokey-api/src/test/java/org/clokey/domain/history/service/HistoryServiceImplTest.java +++ b/clokey-api/src/test/java/org/clokey/domain/history/service/HistoryServiceImplTest.java @@ -48,7 +48,7 @@ import org.clokey.member.entity.Member; import org.clokey.member.entity.OauthInfo; import org.clokey.member.enums.OauthProvider; -import org.clokey.util.S3Util; +import org.clokey.util.StorageUtil; import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.bean.override.mockito.MockitoBean; @@ -77,7 +77,7 @@ class HistoryServiceImplTest extends IntegrationTest { @Autowired private BlockRepository blockRepository; @MockitoBean private MemberUtil memberUtil; - @MockitoBean private S3Util s3Util; + @MockitoBean private StorageUtil storageUtil; @Autowired private ApplicationEvents applicationEvents; @Nested @@ -105,9 +105,9 @@ void setUp() { new HistoryImagesUploadRequest.Payload( FileExtension.PNG, "testMd5Hash2"))); - given(s3Util.createPresignedUrl(any(), anyLong(), any(), eq("testMd5Hash1"))) + given(storageUtil.createPresignedUrl(any(), anyLong(), any(), eq("testMd5Hash1"))) .willReturn("testUrl1"); - given(s3Util.createPresignedUrl(any(), anyLong(), any(), eq("testMd5Hash2"))) + given(storageUtil.createPresignedUrl(any(), anyLong(), any(), eq("testMd5Hash2"))) .willReturn("testUrl2"); // when diff --git a/clokey-infrastructure/build.gradle b/clokey-infrastructure/build.gradle index 0b072959..311bd530 100644 --- a/clokey-infrastructure/build.gradle +++ b/clokey-infrastructure/build.gradle @@ -14,7 +14,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-redis' - implementation 'io.awspring.cloud:spring-cloud-starter-aws:2.4.4' + implementation 'com.oracle.oci.sdk:oci-java-sdk-objectstorage:3.47.0' api 'com.google.firebase:firebase-admin:9.7.0' implementation 'org.springframework.boot:spring-boot-starter-webflux' implementation 'io.vanslog:spring-data-meilisearch:0.7.3' diff --git a/clokey-infrastructure/src/main/java/org/clokey/config/S3Config.java b/clokey-infrastructure/src/main/java/org/clokey/config/S3Config.java index 6ec82256..f9c4dfec 100644 --- a/clokey-infrastructure/src/main/java/org/clokey/config/S3Config.java +++ b/clokey-infrastructure/src/main/java/org/clokey/config/S3Config.java @@ -1,13 +1,10 @@ package org.clokey.config; -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.BasicAWSCredentials; -import com.amazonaws.client.builder.AwsClientBuilder; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider; +import com.oracle.bmc.objectstorage.ObjectStorageClient; +import java.io.ByteArrayInputStream; import lombok.RequiredArgsConstructor; -import org.clokey.properties.AwsProperties; -import org.clokey.properties.S3Properties; +import org.clokey.properties.OciProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -15,21 +12,23 @@ @RequiredArgsConstructor public class S3Config { - private final AwsProperties awsProperties; - private final S3Properties s3Properties; + private final OciProperties ociProperties; @Bean - public AmazonS3 s3Client() { - BasicAWSCredentials credentials = - new BasicAWSCredentials( - awsProperties.accessKeyId(), awsProperties.secretAccessKey()); - AwsClientBuilder.EndpointConfiguration endpointConfiguration = - new AwsClientBuilder.EndpointConfiguration( - s3Properties.endpoint(), awsProperties.region()); + public ObjectStorageClient objectStorageClient() { + String privateKey = ociProperties.privateKey(); + String passphrase = ociProperties.passphrase(); - return AmazonS3ClientBuilder.standard() - .withEndpointConfiguration(endpointConfiguration) - .withCredentials(new AWSStaticCredentialsProvider(credentials)) - .build(); + SimpleAuthenticationDetailsProvider provider = + SimpleAuthenticationDetailsProvider.builder() + .tenantId(ociProperties.tenancyId()) + .userId(ociProperties.userId()) + .fingerprint(ociProperties.fingerprint()) + .privateKeySupplier(() -> new ByteArrayInputStream(privateKey.getBytes())) + .passPhrase(passphrase != null && !passphrase.isEmpty() ? passphrase : null) + .region(com.oracle.bmc.Region.fromRegionId(ociProperties.region())) + .build(); + + return new ObjectStorageClient(provider); } } diff --git a/clokey-infrastructure/src/main/java/org/clokey/properties/AwsProperties.java b/clokey-infrastructure/src/main/java/org/clokey/properties/AwsProperties.java deleted file mode 100644 index d96917bd..00000000 --- a/clokey-infrastructure/src/main/java/org/clokey/properties/AwsProperties.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.clokey.properties; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -@ConfigurationProperties("aws") -public record AwsProperties(String accessKeyId, String secretAccessKey, String region) {} diff --git a/clokey-infrastructure/src/main/java/org/clokey/properties/OciProperties.java b/clokey-infrastructure/src/main/java/org/clokey/properties/OciProperties.java new file mode 100644 index 00000000..bc7fe247 --- /dev/null +++ b/clokey-infrastructure/src/main/java/org/clokey/properties/OciProperties.java @@ -0,0 +1,12 @@ +package org.clokey.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("oci") +public record OciProperties( + String tenancyId, + String userId, + String fingerprint, + String privateKey, + String region, + String passphrase) {} diff --git a/clokey-infrastructure/src/main/java/org/clokey/properties/PropertiesConfig.java b/clokey-infrastructure/src/main/java/org/clokey/properties/PropertiesConfig.java index 52ef9a03..fe30c904 100644 --- a/clokey-infrastructure/src/main/java/org/clokey/properties/PropertiesConfig.java +++ b/clokey-infrastructure/src/main/java/org/clokey/properties/PropertiesConfig.java @@ -7,8 +7,8 @@ @EnableConfigurationProperties({ RedisProperties.class, JwtProperties.class, - S3Properties.class, - AwsProperties.class, + StorageProperties.class, + OciProperties.class, WebClientProperties.class, FirebaseProperties.class, MeilisearchProperties.class diff --git a/clokey-infrastructure/src/main/java/org/clokey/properties/S3Properties.java b/clokey-infrastructure/src/main/java/org/clokey/properties/S3Properties.java deleted file mode 100644 index ff021da2..00000000 --- a/clokey-infrastructure/src/main/java/org/clokey/properties/S3Properties.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.clokey.properties; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -@ConfigurationProperties("aws.s3") -public record S3Properties(String bucket, String endpoint) {} diff --git a/clokey-infrastructure/src/main/java/org/clokey/properties/StorageProperties.java b/clokey-infrastructure/src/main/java/org/clokey/properties/StorageProperties.java new file mode 100644 index 00000000..62b83554 --- /dev/null +++ b/clokey-infrastructure/src/main/java/org/clokey/properties/StorageProperties.java @@ -0,0 +1,6 @@ +package org.clokey.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("oci.objectstorage") +public record StorageProperties(String namespace, String bucket) {} diff --git a/clokey-infrastructure/src/main/java/org/clokey/util/S3Util.java b/clokey-infrastructure/src/main/java/org/clokey/util/S3Util.java deleted file mode 100644 index 76146573..00000000 --- a/clokey-infrastructure/src/main/java/org/clokey/util/S3Util.java +++ /dev/null @@ -1,173 +0,0 @@ -package org.clokey.util; - -import com.amazonaws.HttpMethod; -import com.amazonaws.SdkClientException; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.model.*; -import java.util.Date; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.TimeUnit; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.clokey.enums.FileExtension; -import org.clokey.enums.ImageType; -import org.clokey.properties.S3Properties; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -@Slf4j -public class S3Util { - - private final AmazonS3 amazonS3; - private final S3Properties s3Properties; - - public String createPresignedUrl( - ImageType imageType, Long memberId, FileExtension fileExtension, String md5Hash) { - String imageKey = UUID.randomUUID().toString(); - String fileName = createFileName(imageType, memberId, imageKey, fileExtension); - String bucket = s3Properties.bucket(); - - GeneratePresignedUrlRequest generatePresignedUrlRequest = - generatePresignedUrlRequest( - bucket, fileName, fileExtension.getExtension(), md5Hash); - - return amazonS3.generatePresignedUrl(generatePresignedUrlRequest).toString(); - } - - public String createPresignedUrlWithoutMd5( - ImageType imageType, Long memberId, FileExtension fileExtension) { - String imageKey = UUID.randomUUID().toString(); - String fileName = createFileName(imageType, memberId, imageKey, fileExtension); - String bucket = s3Properties.bucket(); - - GeneratePresignedUrlRequest generatePresignedUrlRequest = - generatePresignedUrlRequestWithoutMd5( - bucket, fileName, fileExtension.getExtension()); - - return amazonS3.generatePresignedUrl(generatePresignedUrlRequest).toString(); - } - - private String createFileName( - ImageType imageType, Long memberId, String imageKey, FileExtension fileExtension) { - return memberId - + "/" - + imageType.getType() - + "/" - + imageKey - + "." - + fileExtension.getExtension(); - } - - private GeneratePresignedUrlRequest generatePresignedUrlRequest( - String bucket, String fileName, String imageFileExtension, String md5Hash) { - GeneratePresignedUrlRequest generatePresignedUrlRequest = - new GeneratePresignedUrlRequest(bucket, fileName, HttpMethod.PUT) - .withKey(fileName) - .withContentType("image/" + imageFileExtension) - .withExpiration(getPresignedUrlExpiration()); - - generatePresignedUrlRequest.addRequestParameter("x-amz-tagging", "status=pending"); - - generatePresignedUrlRequest.setContentMd5(md5Hash); - - return generatePresignedUrlRequest; - } - - private GeneratePresignedUrlRequest generatePresignedUrlRequestWithoutMd5( - String bucket, String fileName, String imageFileExtension) { - GeneratePresignedUrlRequest generatePresignedUrlRequest = - new GeneratePresignedUrlRequest(bucket, fileName, HttpMethod.PUT) - .withKey(fileName) - .withContentType("image/" + imageFileExtension) - .withExpiration(getPresignedUrlExpiration()); - - generatePresignedUrlRequest.addRequestParameter("x-amz-tagging", "status=pending"); - - return generatePresignedUrlRequest; - } - - public void deleteAllByUrls(List urls) { - if (urls == null || urls.isEmpty()) { - log.info("deleteAllByUrls skipped: received null or empty urls"); - return; - } - String bucket = s3Properties.bucket(); - - List keys = - urls.stream() - .map(this::extractObjectKey) - .map(DeleteObjectsRequest.KeyVersion::new) - .toList(); - - DeleteObjectsRequest request = new DeleteObjectsRequest(bucket).withKeys(keys); - amazonS3.deleteObjects(request); - } - - public void deleteByUrl(String url) { - String bucket = s3Properties.bucket(); - String objectKey = extractObjectKey(url); - amazonS3.deleteObject(bucket, objectKey); - } - - private String extractObjectKey(String url) { - int comIndex = url.indexOf(".com/"); - return url.substring(comIndex + 5); - } - - private Date getPresignedUrlExpiration() { - Date expiration = new Date(); - long expTimeMillis = expiration.getTime(); - expTimeMillis += TimeUnit.HOURS.toMillis(1); - expiration.setTime(expTimeMillis); - - return expiration; - } - - public void updateTagToCompleteByUrl(String url) { - String bucket = s3Properties.bucket(); - String key = extractObjectKey(url); - - List tags = List.of(new Tag("status", "complete")); - ObjectTagging tagging = new ObjectTagging(tags); - - SetObjectTaggingRequest request = new SetObjectTaggingRequest(bucket, key, tagging); - amazonS3.setObjectTagging(request); - } - - public void updateTagsToCompleteByUrls(List urls) { - for (String url : urls) { - updateTagToCompleteByUrl(url); - } - } - - public boolean doesFileExistByUrl(String url) { - String bucket = s3Properties.bucket(); - String key = extractObjectKey(url); - try { - return amazonS3.doesObjectExist(bucket, key); - } catch (AmazonS3Exception e) { - if (e.getStatusCode() == 403) { - log.warn("Access denied for key={}, treating as non-existent", key); - return false; - } - log.error("S3 error while checking existence: {}", e.getErrorMessage(), e); - return false; - } catch (SdkClientException e) { - log.error("Network error while connecting to S3: {}", e.getMessage()); - return false; - } - } - - public boolean doAllFilesExistByUrls(List urls) { - for (String url : urls) { - if (!doesFileExistByUrl(url)) { - log.warn("File not found or inaccessible: {}", url); - return false; - } - } - - return true; - } -} diff --git a/clokey-infrastructure/src/main/java/org/clokey/util/StorageUtil.java b/clokey-infrastructure/src/main/java/org/clokey/util/StorageUtil.java new file mode 100644 index 00000000..819a6157 --- /dev/null +++ b/clokey-infrastructure/src/main/java/org/clokey/util/StorageUtil.java @@ -0,0 +1,262 @@ +package org.clokey.util; + +import com.oracle.bmc.objectstorage.ObjectStorageClient; +import com.oracle.bmc.objectstorage.model.CreatePreauthenticatedRequestDetails; +import com.oracle.bmc.objectstorage.requests.*; +import com.oracle.bmc.objectstorage.responses.CreatePreauthenticatedRequestResponse; +import com.oracle.bmc.objectstorage.responses.HeadObjectResponse; +import java.time.Instant; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.clokey.enums.FileExtension; +import org.clokey.enums.ImageType; +import org.clokey.properties.StorageProperties; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +public class StorageUtil { + + private final ObjectStorageClient objectStorageClient; + private final StorageProperties storageProperties; + + // TODO : S3 -> OCI Storage로 이관하면서 MD5 해시를 사용하지 않게 되었습니다. 추후에 삭제할 필요가 있습니다. + public String createPresignedUrl( + ImageType imageType, Long memberId, FileExtension fileExtension, String md5Hash) { + String imageKey = UUID.randomUUID().toString(); + String fileName = createFileName(imageType, memberId, imageKey, fileExtension); + + CreatePreauthenticatedRequestDetails details = + CreatePreauthenticatedRequestDetails.builder() + .name("presigned-url-" + imageKey) + .objectName(fileName) + .accessType(CreatePreauthenticatedRequestDetails.AccessType.ObjectWrite) + .timeExpires(getPresignedUrlExpiration()) + .build(); + + CreatePreauthenticatedRequestRequest request = + CreatePreauthenticatedRequestRequest.builder() + .namespaceName(storageProperties.namespace()) + .bucketName(storageProperties.bucket()) + .createPreauthenticatedRequestDetails(details) + .build(); + + CreatePreauthenticatedRequestResponse response = + objectStorageClient.createPreauthenticatedRequest(request); + + String presignedUrl = + objectStorageClient.getEndpoint() + + response.getPreauthenticatedRequest().getAccessUri(); + + return presignedUrl; + } + + public String createPresignedUrlWithoutMd5( + ImageType imageType, Long memberId, FileExtension fileExtension) { + String imageKey = UUID.randomUUID().toString(); + String fileName = createFileName(imageType, memberId, imageKey, fileExtension); + + CreatePreauthenticatedRequestDetails details = + CreatePreauthenticatedRequestDetails.builder() + .name("presigned-url-" + imageKey) + .objectName(fileName) + .accessType(CreatePreauthenticatedRequestDetails.AccessType.ObjectWrite) + .timeExpires(getPresignedUrlExpiration()) + .build(); + + CreatePreauthenticatedRequestRequest request = + CreatePreauthenticatedRequestRequest.builder() + .namespaceName(storageProperties.namespace()) + .bucketName(storageProperties.bucket()) + .createPreauthenticatedRequestDetails(details) + .build(); + + CreatePreauthenticatedRequestResponse response = + objectStorageClient.createPreauthenticatedRequest(request); + + return objectStorageClient.getEndpoint() + + response.getPreauthenticatedRequest().getAccessUri(); + } + + private String createFileName( + ImageType imageType, Long memberId, String imageKey, FileExtension fileExtension) { + return memberId + + "/" + + imageType.getType() + + "/" + + imageKey + + "." + + fileExtension.getExtension(); + } + + public void deleteAllByUrls(List urls) { + if (urls == null || urls.isEmpty()) { + log.info("deleteAllByUrls skipped: received null or empty urls"); + return; + } + + String namespace = storageProperties.namespace(); + String bucket = storageProperties.bucket(); + + for (String url : urls) { + try { + String objectKey = extractObjectKey(url); + DeleteObjectRequest request = + DeleteObjectRequest.builder() + .namespaceName(namespace) + .bucketName(bucket) + .objectName(objectKey) + .build(); + objectStorageClient.deleteObject(request); + } catch (Exception e) { + log.error("Failed to delete object: {}", url, e); + } + } + } + + public void deleteByUrl(String url) { + String namespace = storageProperties.namespace(); + String bucket = storageProperties.bucket(); + String objectKey = extractObjectKey(url); + + DeleteObjectRequest request = + DeleteObjectRequest.builder() + .namespaceName(namespace) + .bucketName(bucket) + .objectName(objectKey) + .build(); + + objectStorageClient.deleteObject(request); + } + + private String extractObjectKey(String url) { + // OCI Object Storage URL 형식에 맞게 파싱 + // 예: https://objectstorage.{region}.oraclecloud.com/n/{namespace}/b/{bucket}/o/{object} + int oIndex = url.indexOf("/o/"); + if (oIndex != -1) { + return url.substring(oIndex + 3); + } + int comIndex = url.indexOf(".com/"); + if (comIndex != -1) { + String afterCom = url.substring(comIndex + 5); + int firstSlash = afterCom.indexOf("/"); + if (firstSlash != -1) { + return afterCom.substring(firstSlash + 1); + } + } + throw new IllegalArgumentException("Invalid URL format: " + url); + } + + private Date getPresignedUrlExpiration() { + return Date.from(Instant.now().plusSeconds(3600)); // 1시간 후 + } + + public void updateTagToCompleteByUrl(String url) { + String namespace = storageProperties.namespace(); + String bucket = storageProperties.bucket(); + String objectKey = extractObjectKey(url); + + try { + HeadObjectRequest headRequest = + HeadObjectRequest.builder() + .namespaceName(namespace) + .bucketName(bucket) + .objectName(objectKey) + .build(); + + HeadObjectResponse headResponse = objectStorageClient.headObject(headRequest); + + Map metadata = new HashMap<>(); + if (headResponse.getOpcMeta() != null) { + metadata.putAll(headResponse.getOpcMeta()); + } + + metadata.put("status", "complete"); + + com.oracle.bmc.objectstorage.model.CopyObjectDetails copyDetails = + com.oracle.bmc.objectstorage.model.CopyObjectDetails.builder() + .sourceObjectName(objectKey) + .destinationBucket(bucket) + .destinationObjectName(objectKey) + .build(); + + CopyObjectRequest copyRequest = + CopyObjectRequest.builder() + .namespaceName(namespace) + .bucketName(bucket) + .copyObjectDetails(copyDetails) + .build(); + + objectStorageClient.copyObject(copyRequest); + + log.info( + "Object copied for tag update (metadata update may require manual intervention): {}", + objectKey); + } catch (com.oracle.bmc.model.BmcException e) { + log.error( + "Failed to update tag for object: {}, status: {}, message: {}", + objectKey, + e.getStatusCode(), + e.getMessage(), + e); + log.warn("Tag update failed but continuing: {}", objectKey); + } catch (Exception e) { + log.error("Failed to update tag for object: {}", objectKey, e); + log.warn("Tag update failed but continuing: {}", objectKey); + } + } + + public void updateTagsToCompleteByUrls(List urls) { + for (String url : urls) { + updateTagToCompleteByUrl(url); + } + } + + public boolean doesFileExistByUrl(String url) { + String namespace = storageProperties.namespace(); + String bucket = storageProperties.bucket(); + String objectKey = extractObjectKey(url); + + try { + HeadObjectRequest request = + HeadObjectRequest.builder() + .namespaceName(namespace) + .bucketName(bucket) + .objectName(objectKey) + .build(); + + HeadObjectResponse response = objectStorageClient.headObject(request); + return response.get__httpStatusCode__() == 200; + } catch (com.oracle.bmc.model.BmcException e) { + if (e.getStatusCode() == 404) { + return false; + } + if (e.getStatusCode() == 403) { + log.warn("Access denied for key={}, treating as non-existent", objectKey); + return false; + } + log.error("OCI error while checking existence: {}", e.getMessage(), e); + return false; + } catch (Exception e) { + log.error("Network error while connecting to OCI: {}", e.getMessage()); + return false; + } + } + + public boolean doAllFilesExistByUrls(List urls) { + for (String url : urls) { + if (!doesFileExistByUrl(url)) { + log.warn("File not found or inaccessible: {}", url); + return false; + } + } + + return true; + } +} From 052777a70b54dcb18607036475e91dae48f4114d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=82=98=EC=9A=A9=EC=A4=80?= <141994188+youngJun99@users.noreply.github.com> Date: Sun, 1 Feb 2026 21:27:05 +0900 Subject: [PATCH 2/2] =?UTF-8?q?chore:=20AWS=20->=20OCI=EB=A1=9C=20export?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20Test=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=20=ED=8A=B8=EB=A6=AC=EA=B1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/dev-cd.yml | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/.github/workflows/dev-cd.yml b/.github/workflows/dev-cd.yml index 469aceb2..6c481719 100644 --- a/.github/workflows/dev-cd.yml +++ b/.github/workflows/dev-cd.yml @@ -1,8 +1,14 @@ name: Clokey-Dev CD on: - push: - branches: [ develop ] + pull_request: + branches: [ "main", "develop" ] + paths: + - "clokey-api/**" + - "clokey-domain/**" + - "clokey-common/**" + - "clokey-common-web/**" + - ".github/workflows/ci.yml" jobs: dev-cd: @@ -63,7 +69,7 @@ jobs: username: ubuntu host: ${{ secrets.DEV_EC2_HOST }} key: ${{ secrets.DEV_EC2_SSH_KEY }} - envs: DOCKERHUB_USERNAME,DEV_MYSQL_HOST,MYSQL_PORT,DB_NAME,DB_USERNAME,DB_PASSWORD,REDIS_HOST,REDIS_PORT,REDIS_PASSWORD,DEV_KAKAO_CLIENT_ID,DEV_KAKAO_CLIENT_SECRET,DEV_APPLE_CLIENT_ID,DEV_APPLE_CLIENT_SECRET,JWT_ACCESS_TOKEN_SECRET,JWT_REFRESH_TOKEN_SECRET,JWT_ACCESS_TOKEN_EXPIRATION_TIME,JWT_REFRESH_TOKEN_EXPIRATION_TIME,JWT_ISSUER,DEV_AWS_ACCESS_KEY_ID,DEV_AWS_SECRET_ACCESS_KEY,AWS_REGION,DEV_S3_BUCKET,DEV_S3_ENDPOINT,SWAGGER_USERNAME,SWAGGER_PASSWORD,FIREBASE_SA_JSON_B64,AI_SERVER_IP,CLOTH_INFERENCE_PATH,STYLE_INFERENCE_PATH,CLOTH_DETECT_PATH,MEILISEARCH_ENDPOINT,MEILISEARCH_KEY + envs: DOCKERHUB_USERNAME,DEV_MYSQL_HOST,MYSQL_PORT,DB_NAME,DB_USERNAME,DB_PASSWORD,REDIS_HOST,REDIS_PORT,REDIS_PASSWORD,DEV_KAKAO_CLIENT_ID,DEV_KAKAO_CLIENT_SECRET,DEV_APPLE_CLIENT_ID,DEV_APPLE_CLIENT_SECRET,JWT_ACCESS_TOKEN_SECRET,JWT_REFRESH_TOKEN_SECRET,JWT_ACCESS_TOKEN_EXPIRATION_TIME,JWT_REFRESH_TOKEN_EXPIRATION_TIME,JWT_ISSUER,OCI_TENANCY_ID,OCI_USER_ID,OCI_FINGERPRINT,OCI_PRIVATE_KEY,OCI_REGION,OCI_PASSPHRASE,OCI_OBJECTSTORAGE_NAMESPACE,OCI_OBJECTSTORAGE_BUCKET,SWAGGER_USERNAME,SWAGGER_PASSWORD,FIREBASE_SA_JSON_B64,AI_SERVER_IP,CLOTH_INFERENCE_PATH,STYLE_INFERENCE_PATH,CLOTH_DETECT_PATH,MEILISEARCH_ENDPOINT,MEILISEARCH_KEY script: | export DOCKERHUB_NAME=${{ secrets.DOCKERHUB_USERNAME }} export DOCKER_TAG=dev-app @@ -90,11 +96,14 @@ jobs: export JWT_REFRESH_TOKEN_EXPIRATION_TIME=${{ secrets.JWT_REFRESH_TOKEN_EXPIRATION_TIME }} export JWT_ISSUER=${{ secrets.JWT_ISSUER }} - export DEV_AWS_ACCESS_KEY_ID=${{ secrets.DEV_AWS_ACCESS_KEY_ID }} - export DEV_AWS_SECRET_ACCESS_KEY=${{ secrets.DEV_AWS_SECRET_ACCESS_KEY }} - export AWS_REGION=${{ secrets.AWS_REGION }} - export DEV_S3_BUCKET=${{ secrets.DEV_S3_BUCKET }} - export DEV_S3_ENDPOINT=${{ secrets.DEV_S3_ENDPOINT }} + export OCI_TENANCY_ID=${{ secrets.DEV_OCI_TENANCY_ID }} + export OCI_USER_ID=${{ secrets.DEV_CI_USER_ID }} + export OCI_FINGERPRINT=${{ secrets.DEV_OCI_FINGERPRINT }} + export OCI_PRIVATE_KEY=${{ secrets.DEV_OCI_PRIVATE_KEY }} + export OCI_REGION=${{ secrets.DEV_OCI_REGION }} + export OCI_PASSPHRASE=${{ secrets.DEV_OCI_PASSPHRASE }} + export OCI_OBJECTSTORAGE_NAMESPACE=${{ secrets.DEV_OCI_OBJECTSTORAGE_NAMESPACE }} + export OCI_OBJECTSTORAGE_BUCKET=${{ secrets.DEV_OCI_OBJECTSTORAGE_BUCKET }} export SWAGGER_USERNAME=${{ secrets.SWAGGER_USERNAME }} export SWAGGER_PASSWORD=${{ secrets.SWAGGER_PASSWORD }}