From 4cf8d12cf2f055a5767739e50a68984ddc598158 Mon Sep 17 00:00:00 2001 From: Baejinho4028 Date: Fri, 30 Oct 2026 12:29:11 +0900 Subject: [PATCH 1/2] =?UTF-8?q?chore:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AWS SDK v1 전체 묶음을 s3, ses 모듈로 교체 - 사용하지 않는 h2, opencsv, rest-assured와 중복 선언된 mysql-connector-j 제거 - opencsv 제거로 빠진 commons-collections 대신 Spring CollectionUtils 사용 --- build.gradle | 7 ++----- .../shop/repository/shop/ShopSearchQueryRepository.java | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index 26db9863c8..982c714490 100644 --- a/build.gradle +++ b/build.gradle @@ -38,9 +38,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-aop' // db - implementation 'com.mysql:mysql-connector-j' runtimeOnly 'com.mysql:mysql-connector-j' - runtimeOnly 'com.h2database:h2' implementation 'org.flywaydb:flyway-mysql' // security @@ -50,7 +48,8 @@ dependencies { runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3' // infra (aws, cloud, fcm) - implementation 'com.amazonaws:aws-java-sdk:1.12.672' + implementation 'com.amazonaws:aws-java-sdk-s3:1.12.672' + implementation 'com.amazonaws:aws-java-sdk-ses:1.12.672' implementation platform('software.amazon.awssdk:bom:2.20.56') implementation 'software.amazon.awssdk:s3' implementation 'software.amazon.awssdk:cloudfront' @@ -71,7 +70,6 @@ dependencies { implementation group: 'org.json', name: 'json', version: '20231013' implementation 'com.github.ua-parser:uap-java:1.4.4' implementation 'org.apache.poi:poi-ooxml:5.2.5' - implementation 'com.opencsv:opencsv:5.9' implementation 'net.lingala.zip4j:zip4j:2.6.1' implementation 'com.github.f4b6a3:tsid-creator:5.2.6' @@ -94,7 +92,6 @@ dependencies { testImplementation 'org.testcontainers:junit-jupiter:1.19.3' testImplementation 'org.testcontainers:mysql' testImplementation 'org.testcontainers:localstack' - testImplementation 'io.rest-assured:rest-assured:5.3.2' } clean { diff --git a/src/main/java/in/koreatech/koin/domain/shop/repository/shop/ShopSearchQueryRepository.java b/src/main/java/in/koreatech/koin/domain/shop/repository/shop/ShopSearchQueryRepository.java index 492f4bb1b0..7e60f96aa4 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/repository/shop/ShopSearchQueryRepository.java +++ b/src/main/java/in/koreatech/koin/domain/shop/repository/shop/ShopSearchQueryRepository.java @@ -5,8 +5,8 @@ import java.util.List; -import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Repository; +import org.springframework.util.CollectionUtils; import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.BooleanExpression; From 2e611b66bd0b07f9169450955ee95a55a5143870 Mon Sep 17 00:00:00 2001 From: Baejinho4028 Date: Fri, 30 Oct 2026 23:29:31 +0900 Subject: [PATCH 2/2] =?UTF-8?q?refactor:=20AWS=20SDK=20v1=EC=9D=84=20v2?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - S3, SES 클라이언트를 SDK v2로 교체하고 v1 의존성 제거 - v2 S3Client와 이름이 겹쳐 기존 S3Client를 S3ClientWrapper로 변경 - v1을 통해 들어오던 joda-time 대신 java.time 사용 --- build.gradle | 3 +- .../article/service/ArticleService.java | 4 +- .../summary/ArticleSummarySourceReader.java | 4 +- .../koin/domain/coop/service/CoopService.java | 4 +- .../dining/service/DiningReportService.java | 9 +- .../service/StudentValidationService.java | 2 +- .../infrastructure/config/AwsSesConfig.java | 19 ++-- .../koin/infrastructure/config/S3Config.java | 22 ++--- .../email/client/SesMailSender.java | 13 +-- .../email/service/EmailService.java | 20 ++-- .../{S3Client.java => S3ClientWrapper.java} | 95 ++++++++++++------- .../s3/eventlistener/S3EventListener.java | 4 +- .../s3/service/UploadService.java | 4 +- .../domain/DiningSoldOutReportApiTest.java | 4 +- .../DiningSoldOutReportConcurrencyTest.java | 4 +- .../acceptance/domain/UploadServiceTest.java | 25 ++--- .../ArticleSummarySourceReaderTest.java | 24 ++--- .../DiningReportImageValidationTest.java | 30 +++--- ...ientTest.java => S3ClientWrapperTest.java} | 13 ++- 19 files changed, 152 insertions(+), 151 deletions(-) rename src/main/java/in/koreatech/koin/infrastructure/s3/client/{S3Client.java => S3ClientWrapper.java} (74%) rename src/test/java/in/koreatech/koin/unit/infrastructure/s3/client/{S3ClientTest.java => S3ClientWrapperTest.java} (91%) diff --git a/build.gradle b/build.gradle index 982c714490..79dd01e011 100644 --- a/build.gradle +++ b/build.gradle @@ -48,10 +48,9 @@ dependencies { runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3' // infra (aws, cloud, fcm) - implementation 'com.amazonaws:aws-java-sdk-s3:1.12.672' - implementation 'com.amazonaws:aws-java-sdk-ses:1.12.672' implementation platform('software.amazon.awssdk:bom:2.20.56') implementation 'software.amazon.awssdk:s3' + implementation 'software.amazon.awssdk:ses' implementation 'software.amazon.awssdk:cloudfront' implementation 'com.google.firebase:firebase-admin:9.2.0' implementation 'org.redisson:redisson-spring-boot-starter:3.35.0' diff --git a/src/main/java/in/koreatech/koin/domain/community/article/service/ArticleService.java b/src/main/java/in/koreatech/koin/domain/community/article/service/ArticleService.java index a7ec667105..fba27aa3cc 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/service/ArticleService.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/service/ArticleService.java @@ -34,7 +34,7 @@ import in.koreatech.koin.domain.community.article.service.summary.ArticleAiSummaryService; import in.koreatech.koin.domain.community.article.service.summary.ArticleSummaryView; import in.koreatech.koin.global.exception.custom.KoinIllegalArgumentException; -import in.koreatech.koin.infrastructure.s3.client.S3Client; +import in.koreatech.koin.infrastructure.s3.client.S3ClientWrapper; import lombok.RequiredArgsConstructor; @Service @@ -53,7 +53,7 @@ public class ArticleService { private final HotArticleRepository hotArticleRepository; private final ArticleHitUserRepository articleHitUserRepository; private final Clock clock; - private final S3Client s3Client; + private final S3ClientWrapper s3Client; private final PopularKeywordTracker popularKeywordTracker; private final KeywordRankingManager keywordRankingManager; private final ArticleAiSummaryService articleAiSummaryService; diff --git a/src/main/java/in/koreatech/koin/domain/community/article/service/summary/ArticleSummarySourceReader.java b/src/main/java/in/koreatech/koin/domain/community/article/service/summary/ArticleSummarySourceReader.java index 02be808997..a76cf5d429 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/service/summary/ArticleSummarySourceReader.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/service/summary/ArticleSummarySourceReader.java @@ -23,7 +23,7 @@ import org.springframework.util.StringUtils; import in.koreatech.koin.global.exception.custom.KoinIllegalStateException; -import in.koreatech.koin.infrastructure.s3.client.S3Client; +import in.koreatech.koin.infrastructure.s3.client.S3ClientWrapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -43,7 +43,7 @@ public class ArticleSummarySourceReader { private final ArticleDocumentParseClient documentParseClient; private final ArticleAiSummaryProperties properties; - private final S3Client s3Client; + private final S3ClientWrapper s3Client; public String createFingerprint(ArticleSummarySourceSeed seed) { return createFingerprint(seed, resolveContent(seed.content())); diff --git a/src/main/java/in/koreatech/koin/domain/coop/service/CoopService.java b/src/main/java/in/koreatech/koin/domain/coop/service/CoopService.java index d41d9a38a3..72f4431b1a 100644 --- a/src/main/java/in/koreatech/koin/domain/coop/service/CoopService.java +++ b/src/main/java/in/koreatech/koin/domain/coop/service/CoopService.java @@ -73,7 +73,7 @@ import in.koreatech.koin.domain.user.service.RefreshTokenService; import in.koreatech.koin.global.auth.JwtProvider; import in.koreatech.koin.global.exception.custom.KoinIllegalStateException; -import in.koreatech.koin.infrastructure.s3.client.S3Client; +import in.koreatech.koin.infrastructure.s3.client.S3ClientWrapper; import lombok.RequiredArgsConstructor; @Service @@ -93,7 +93,7 @@ public class CoopService { private final CoopShopService coopShopService; private final PasswordEncoder passwordEncoder; private final JwtProvider jwtProvider; - private final S3Client s3Client; + private final S3ClientWrapper s3Client; private final List placeFilters = Arrays.asList("A코너", "B코너", "C코너"); private final List cafeteriaPlaceFilters = Arrays.asList("A코너", "B코너", "C코너"); private final List allPlaceFilters = Arrays.asList("A코너", "B코너", "C코너", "능수관", "2캠퍼스"); diff --git a/src/main/java/in/koreatech/koin/domain/dining/service/DiningReportService.java b/src/main/java/in/koreatech/koin/domain/dining/service/DiningReportService.java index 90c55a344b..2293f122ca 100644 --- a/src/main/java/in/koreatech/koin/domain/dining/service/DiningReportService.java +++ b/src/main/java/in/koreatech/koin/domain/dining/service/DiningReportService.java @@ -24,8 +24,6 @@ import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; -import com.amazonaws.AmazonClientException; - import in.koreatech.koin.common.event.DiningSoldOutEvent; import in.koreatech.koin.domain.coop.exception.MenuNotFoundException; import in.koreatech.koin.domain.coop.repository.DiningSoldOutCacheRepository; @@ -44,8 +42,9 @@ import in.koreatech.koin.domain.dining.repository.DiningRepository; import in.koreatech.koin.global.exception.CustomException; import in.koreatech.koin.global.exception.custom.KoinIllegalStateException; -import in.koreatech.koin.infrastructure.s3.client.S3Client; +import in.koreatech.koin.infrastructure.s3.client.S3ClientWrapper; import lombok.RequiredArgsConstructor; +import software.amazon.awssdk.core.exception.SdkException; @Service @Transactional(isolation = Isolation.READ_COMMITTED) @@ -60,7 +59,7 @@ public class DiningReportService { private final DiningSoldOutCacheRepository soldOutCacheRepository; private final CoopShopService coopShopService; private final ApplicationEventPublisher eventPublisher; - private final S3Client s3Client; + private final S3ClientWrapper s3Client; private final Clock clock; public DiningReportCreateResponse create(Integer reporterId, Integer diningId, DiningReportCreateRequest request) { @@ -186,7 +185,7 @@ private void validateImage(String imageUrl) { if (!s3Client.doesFileExist(key)) { throw CustomException.of(INVALID_REPORT_IMAGE); } - } catch (AmazonClientException exception) { + } catch (SdkException exception) { throw CustomException.of(IMAGE_STORAGE_UNAVAILABLE); } } diff --git a/src/main/java/in/koreatech/koin/domain/student/service/StudentValidationService.java b/src/main/java/in/koreatech/koin/domain/student/service/StudentValidationService.java index 7e9a312a8e..ac444ea143 100644 --- a/src/main/java/in/koreatech/koin/domain/student/service/StudentValidationService.java +++ b/src/main/java/in/koreatech/koin/domain/student/service/StudentValidationService.java @@ -1,9 +1,9 @@ package in.koreatech.koin.domain.student.service; +import java.time.LocalDateTime; import java.util.List; import java.util.Objects; -import org.joda.time.LocalDateTime; import org.springframework.stereotype.Service; import in.koreatech.koin.global.exception.CustomException; diff --git a/src/main/java/in/koreatech/koin/infrastructure/config/AwsSesConfig.java b/src/main/java/in/koreatech/koin/infrastructure/config/AwsSesConfig.java index 7d5e0491b6..6f4e1a57b9 100644 --- a/src/main/java/in/koreatech/koin/infrastructure/config/AwsSesConfig.java +++ b/src/main/java/in/koreatech/koin/infrastructure/config/AwsSesConfig.java @@ -4,11 +4,10 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.BasicAWSCredentials; -import com.amazonaws.regions.Regions; -import com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceAsync; -import com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceAsyncClient; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.ses.SesClient; @Configuration public class AwsSesConfig { @@ -25,12 +24,12 @@ public AwsSesConfig( } @Bean - public AmazonSimpleEmailServiceAsync amazonSimpleEmailServiceAsync() { - BasicAWSCredentials basicAWSCredentials = new BasicAWSCredentials(accessKey, secretKey); + public SesClient sesClient() { + AwsBasicCredentials awsBasicCredentials = AwsBasicCredentials.create(accessKey, secretKey); - return AmazonSimpleEmailServiceAsyncClient.asyncBuilder() - .withCredentials(new AWSStaticCredentialsProvider(basicAWSCredentials)) - .withRegion(Regions.US_WEST_2) + return SesClient.builder() + .credentialsProvider(StaticCredentialsProvider.create(awsBasicCredentials)) + .region(Region.US_WEST_2) .build(); } } diff --git a/src/main/java/in/koreatech/koin/infrastructure/config/S3Config.java b/src/main/java/in/koreatech/koin/infrastructure/config/S3Config.java index d16533e410..7b8bd43bc4 100644 --- a/src/main/java/in/koreatech/koin/infrastructure/config/S3Config.java +++ b/src/main/java/in/koreatech/koin/infrastructure/config/S3Config.java @@ -5,18 +5,12 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import com.amazonaws.ClientConfiguration; -import com.amazonaws.Protocol; -import com.amazonaws.auth.EC2ContainerCredentialsProviderWrapper; -import com.amazonaws.regions.Regions; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3ClientBuilder; - import software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider; +import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.presigner.S3Presigner; /** - * Key 기반 인증 방식은 취약하므로 IAM Role 기반 인증 방식을 사용한다. (EC2ContainerCredentialsProviderWrapper) + * Key 기반 인증 방식은 취약하므로 IAM Role 기반 인증 방식을 사용한다. (InstanceProfileCredentialsProvider) * EC2 인스턴스에 부여된 "EC2_to_S3" IAM Role을 통해 인증을 수행한다. */ @Configuration @@ -33,14 +27,10 @@ public S3Presigner.Builder s3PresignerBuilder() { } @Bean - public AmazonS3 amazonS3ClientBuilder() { - ClientConfiguration clientConfig = new ClientConfiguration(); - clientConfig.setProtocol(Protocol.HTTPS); - - return AmazonS3ClientBuilder.standard() - .withRegion(Regions.AP_NORTHEAST_2) - .withClientConfiguration(clientConfig) - .withCredentials(new EC2ContainerCredentialsProviderWrapper()) // IAM Role 사용 + public S3Client s3Client() { + return S3Client.builder() + .credentialsProvider(InstanceProfileCredentialsProvider.create()) + .region(AP_NORTHEAST_2) .build(); } } diff --git a/src/main/java/in/koreatech/koin/infrastructure/email/client/SesMailSender.java b/src/main/java/in/koreatech/koin/infrastructure/email/client/SesMailSender.java index 6e0a5b91cd..440a87e0ce 100644 --- a/src/main/java/in/koreatech/koin/infrastructure/email/client/SesMailSender.java +++ b/src/main/java/in/koreatech/koin/infrastructure/email/client/SesMailSender.java @@ -4,26 +4,21 @@ import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Component; -import com.amazonaws.services.simpleemail.AmazonSimpleEmailService; -import com.amazonaws.services.simpleemail.model.Body; -import com.amazonaws.services.simpleemail.model.Content; -import com.amazonaws.services.simpleemail.model.Destination; -import com.amazonaws.services.simpleemail.model.Message; -import com.amazonaws.services.simpleemail.model.SendEmailRequest; - import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import software.amazon.awssdk.services.ses.SesClient; +import software.amazon.awssdk.services.ses.model.SendEmailRequest; @Slf4j @Component @RequiredArgsConstructor public class SesMailSender { - private final AmazonSimpleEmailService amazonSimpleEmailService; + private final SesClient sesClient; @Retryable public void sendMail(SendEmailRequest request) { - amazonSimpleEmailService.sendEmail(request); + sesClient.sendEmail(request); } @Recover diff --git a/src/main/java/in/koreatech/koin/infrastructure/email/service/EmailService.java b/src/main/java/in/koreatech/koin/infrastructure/email/service/EmailService.java index aa7c1d14f0..3a6260eed7 100644 --- a/src/main/java/in/koreatech/koin/infrastructure/email/service/EmailService.java +++ b/src/main/java/in/koreatech/koin/infrastructure/email/service/EmailService.java @@ -6,15 +6,10 @@ import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; -import com.amazonaws.services.simpleemail.model.Body; -import com.amazonaws.services.simpleemail.model.Content; -import com.amazonaws.services.simpleemail.model.Destination; -import com.amazonaws.services.simpleemail.model.Message; -import com.amazonaws.services.simpleemail.model.SendEmailRequest; - import in.koreatech.koin.infrastructure.email.form.EmailForm; import in.koreatech.koin.infrastructure.email.client.SesMailSender; import lombok.RequiredArgsConstructor; +import software.amazon.awssdk.services.ses.model.SendEmailRequest; @Service @RequiredArgsConstructor @@ -37,11 +32,12 @@ private SendEmailRequest createEmailRequest(String targetEmail, EmailForm emailF String htmlBody = templateEngine.process(emailForm.getFilePath(), context); - return new SendEmailRequest() - .withDestination(new Destination().withToAddresses(targetEmail)) - .withSource(NO_REPLY_EMAIL_ADDRESS) - .withMessage(new Message() - .withBody(new Body().withHtml(new Content().withCharset("UTF-8").withData(htmlBody))) - .withSubject(new Content().withCharset("UTF-8").withData(emailForm.getSubject()))); + return SendEmailRequest.builder() + .destination(destination -> destination.toAddresses(targetEmail)) + .source(NO_REPLY_EMAIL_ADDRESS) + .message(message -> message + .body(body -> body.html(content -> content.charset("UTF-8").data(htmlBody))) + .subject(content -> content.charset("UTF-8").data(emailForm.getSubject()))) + .build(); } } diff --git a/src/main/java/in/koreatech/koin/infrastructure/s3/client/S3Client.java b/src/main/java/in/koreatech/koin/infrastructure/s3/client/S3ClientWrapper.java similarity index 74% rename from src/main/java/in/koreatech/koin/infrastructure/s3/client/S3Client.java rename to src/main/java/in/koreatech/koin/infrastructure/s3/client/S3ClientWrapper.java index 88d0a073f2..1da79ecd79 100644 --- a/src/main/java/in/koreatech/koin/infrastructure/s3/client/S3Client.java +++ b/src/main/java/in/koreatech/koin/infrastructure/s3/client/S3ClientWrapper.java @@ -1,8 +1,5 @@ package in.koreatech.koin.infrastructure.s3.client; -import static com.amazonaws.services.s3.model.DeleteObjectsRequest.KeyVersion; - -import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -20,23 +17,28 @@ import org.springframework.util.StringUtils; import org.springframework.web.reactive.function.client.WebClient; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.model.CannedAccessControlList; -import com.amazonaws.services.s3.model.DeleteObjectsRequest; -import com.amazonaws.services.s3.model.MultiObjectDeleteException; -import com.amazonaws.services.s3.model.ObjectMetadata; -import com.amazonaws.services.s3.model.PutObjectRequest; -import com.amazonaws.services.s3.model.S3Object; - import in.koreatech.koin.infrastructure.s3.dto.UploadFileResponse; import in.koreatech.koin.infrastructure.s3.dto.UploadUrlResponse; import in.koreatech.koin.global.exception.custom.KoinIllegalStateException; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.Delete; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest; +import software.amazon.awssdk.services.s3.model.DeleteObjectsResponse; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.HeadObjectRequest; +import software.amazon.awssdk.services.s3.model.ObjectCannedACL; +import software.amazon.awssdk.services.s3.model.ObjectIdentifier; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.S3Error; +import software.amazon.awssdk.services.s3.model.S3Exception; import software.amazon.awssdk.services.s3.presigner.S3Presigner; import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest; import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest; @Component -public class S3Client { +public class S3ClientWrapper { private static final int URL_EXPIRATION_MINUTE = 10; @@ -44,22 +46,22 @@ public class S3Client { private final String domainUrlPrefix; private final URI customDomainUri; private final S3Presigner.Builder presignerBuilder; - private final AmazonS3 amazonS3; + private final S3Client awsS3Client; private final Clock clock; private final WebClient webClient; - public S3Client( + public S3ClientWrapper( @Value("${s3.bucket}") String bucketName, @Value("${s3.custom_domain}") String domainUrlPrefix, S3Presigner.Builder presignerBuilder, - AmazonS3 amazonS3, + S3Client awsS3Client, Clock clock ) { this.bucketName = bucketName; this.domainUrlPrefix = domainUrlPrefix; this.customDomainUri = parseCustomDomainUri(domainUrlPrefix); this.presignerBuilder = presignerBuilder; - this.amazonS3 = amazonS3; + this.awsS3Client = awsS3Client; this.clock = clock; this.webClient = WebClient.builder().build(); } @@ -181,42 +183,55 @@ private static String normalizePath(String path) { } public UploadFileResponse uploadFile(String uploadFilePath, byte[] fileData) { - ObjectMetadata metaData = new ObjectMetadata(); - metaData.setContentLength(fileData.length); - amazonS3.putObject( - new PutObjectRequest(bucketName, uploadFilePath, new ByteArrayInputStream(fileData), metaData) - .withCannedAcl(CannedAccessControlList.PublicRead)); + PutObjectRequest request = PutObjectRequest.builder() + .bucket(bucketName) + .key(uploadFilePath) + .acl(ObjectCannedACL.PUBLIC_READ) + .build(); + awsS3Client.putObject(request, RequestBody.fromBytes(fileData)); return new UploadFileResponse(domainUrlPrefix + uploadFilePath); } public void deleteFile(String s3Key) { try { - amazonS3.deleteObject(bucketName, s3Key); + awsS3Client.deleteObject(DeleteObjectRequest.builder() + .bucket(bucketName) + .key(s3Key) + .build()); } catch (Exception e) { throw new KoinIllegalStateException("S3 객체 삭제 중 문제가 발생했습니다. " + e.getMessage()); } } public void deleteFiles(List s3Keys) { + DeleteObjectsResponse response; try { - List keys = s3Keys.stream() - .map(KeyVersion::new) - .toList(); - DeleteObjectsRequest request = new DeleteObjectsRequest(bucketName).withKeys(keys); - amazonS3.deleteObjects(request); - } catch (MultiObjectDeleteException e) { - List failedKeys = e.getErrors().stream() - .map(MultiObjectDeleteException.DeleteError::getKey) + List keys = s3Keys.stream() + .map(key -> ObjectIdentifier.builder().key(key).build()) .toList(); - throw new KoinIllegalStateException("일부 S3 객체의 삭제가 실패했습니다. : " + failedKeys); + DeleteObjectsRequest request = DeleteObjectsRequest.builder() + .bucket(bucketName) + .delete(Delete.builder().objects(keys).build()) + .build(); + response = awsS3Client.deleteObjects(request); } catch (Exception e) { throw new KoinIllegalStateException("S3 객체 일괄 삭제 중 문제가 발생했습니다. : " + e.getMessage()); } + // v2는 일부 객체 삭제 실패를 예외 대신 응답의 errors로 돌려준다. + if (response.hasErrors() && !response.errors().isEmpty()) { + List failedKeys = response.errors().stream() + .map(S3Error::key) + .toList(); + throw new KoinIllegalStateException("일부 S3 객체의 삭제가 실패했습니다. : " + failedKeys); + } } public void downloadS3Object(String bucketName, String s3Key, File localFile) { - try (S3Object s3Object = amazonS3.getObject(bucketName, s3Key); - InputStream inputStream = s3Object.getObjectContent(); + GetObjectRequest request = GetObjectRequest.builder() + .bucket(bucketName) + .key(s3Key) + .build(); + try (InputStream inputStream = awsS3Client.getObject(request); OutputStream outputStream = new FileOutputStream(localFile)) { byte[] buffer = new byte[1024]; int bytesRead; @@ -236,7 +251,19 @@ public String extractKeyFromUrl(String url) { } public boolean doesFileExist(String key) { - return amazonS3.doesObjectExist(bucketName, key); + try { + awsS3Client.headObject(HeadObjectRequest.builder() + .bucket(bucketName) + .key(key) + .build()); + return true; + } catch (S3Exception e) { + // v1 doesObjectExist와 같이 404만 "없음"으로 보고, 그 외 오류는 그대로 던진다. + if (e.statusCode() == 404) { + return false; + } + throw e; + } } public List extractKeysFromUrls(List urls) { diff --git a/src/main/java/in/koreatech/koin/infrastructure/s3/eventlistener/S3EventListener.java b/src/main/java/in/koreatech/koin/infrastructure/s3/eventlistener/S3EventListener.java index 514c0fd815..c14d80e888 100644 --- a/src/main/java/in/koreatech/koin/infrastructure/s3/eventlistener/S3EventListener.java +++ b/src/main/java/in/koreatech/koin/infrastructure/s3/eventlistener/S3EventListener.java @@ -12,7 +12,7 @@ import in.koreatech.koin.common.event.ImagesDeletedEvent; import in.koreatech.koin.common.event.ImagesSensitiveDeletedEvent; import in.koreatech.koin.infrastructure.s3.client.CloudFrontClientWrapper; -import in.koreatech.koin.infrastructure.s3.client.S3Client; +import in.koreatech.koin.infrastructure.s3.client.S3ClientWrapper; import lombok.RequiredArgsConstructor; @Component @@ -20,7 +20,7 @@ @Profile("!test") public class S3EventListener { - private final S3Client s3Client; + private final S3ClientWrapper s3Client; private final CloudFrontClientWrapper cloudFrontClientWrapper; @Async diff --git a/src/main/java/in/koreatech/koin/infrastructure/s3/service/UploadService.java b/src/main/java/in/koreatech/koin/infrastructure/s3/service/UploadService.java index 3b53feca49..04c3f080a6 100644 --- a/src/main/java/in/koreatech/koin/infrastructure/s3/service/UploadService.java +++ b/src/main/java/in/koreatech/koin/infrastructure/s3/service/UploadService.java @@ -18,7 +18,7 @@ import in.koreatech.koin.infrastructure.s3.dto.UploadUrlResponse; import in.koreatech.koin.infrastructure.s3.model.ImageUploadDomain; import in.koreatech.koin.global.exception.custom.KoinIllegalArgumentException; -import in.koreatech.koin.infrastructure.s3.client.S3Client; +import in.koreatech.koin.infrastructure.s3.client.S3ClientWrapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -28,7 +28,7 @@ @Transactional(readOnly = true) public class UploadService { - private final S3Client s3Client; + private final S3ClientWrapper s3Client; private final Clock clock; public UploadUrlResponse getPresignedUrl(ImageUploadDomain domain, UploadUrlRequest request) { diff --git a/src/test/java/in/koreatech/koin/acceptance/domain/DiningSoldOutReportApiTest.java b/src/test/java/in/koreatech/koin/acceptance/domain/DiningSoldOutReportApiTest.java index c04b295db0..1f9e51987c 100644 --- a/src/test/java/in/koreatech/koin/acceptance/domain/DiningSoldOutReportApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/domain/DiningSoldOutReportApiTest.java @@ -37,7 +37,7 @@ import in.koreatech.koin.domain.user.model.User; import in.koreatech.koin.domain.user.model.UserType; import in.koreatech.koin.domain.user.repository.UserRepository; -import in.koreatech.koin.infrastructure.s3.client.S3Client; +import in.koreatech.koin.infrastructure.s3.client.S3ClientWrapper; @TestPropertySource(properties = { "dining.report.bot-token=test-dining-report-bot-token" @@ -74,7 +74,7 @@ class DiningSoldOutReportApiTest extends AcceptanceTest { private ObjectMapper objectMapper; @MockBean - private S3Client s3Client; + private S3ClientWrapper s3Client; private User student; private String studentToken; diff --git a/src/test/java/in/koreatech/koin/acceptance/domain/DiningSoldOutReportConcurrencyTest.java b/src/test/java/in/koreatech/koin/acceptance/domain/DiningSoldOutReportConcurrencyTest.java index 89e1fb5047..f5124f3b27 100644 --- a/src/test/java/in/koreatech/koin/acceptance/domain/DiningSoldOutReportConcurrencyTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/domain/DiningSoldOutReportConcurrencyTest.java @@ -66,7 +66,7 @@ import in.koreatech.koin.domain.user.model.User; import in.koreatech.koin.infrastructure.fcm.FcmClient; import in.koreatech.koin.infrastructure.fcm.FcmSendResponse; -import in.koreatech.koin.infrastructure.s3.client.S3Client; +import in.koreatech.koin.infrastructure.s3.client.S3ClientWrapper; @Transactional(propagation = Propagation.NOT_SUPPORTED) @TestPropertySource(properties = { @@ -118,7 +118,7 @@ class DiningSoldOutReportConcurrencyTest extends AcceptanceTest { private PlatformTransactionManager transactionManager; @MockBean - private S3Client s3Client; + private S3ClientWrapper s3Client; @MockBean private FcmClient fcmClient; diff --git a/src/test/java/in/koreatech/koin/acceptance/domain/UploadServiceTest.java b/src/test/java/in/koreatech/koin/acceptance/domain/UploadServiceTest.java index 0a3bbbddb4..1b5de95fba 100644 --- a/src/test/java/in/koreatech/koin/acceptance/domain/UploadServiceTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/domain/UploadServiceTest.java @@ -13,25 +13,21 @@ import org.testcontainers.junit.jupiter.Container; import org.testcontainers.utility.DockerImageName; -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.BasicAWSCredentials; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3ClientBuilder; - import in.koreatech.koin.acceptance.AcceptanceTest; import in.koreatech.koin.infrastructure.s3.dto.UploadUrlRequest; import in.koreatech.koin.infrastructure.s3.service.UploadService; -import in.koreatech.koin.infrastructure.s3.client.S3Client; +import in.koreatech.koin.infrastructure.s3.client.S3ClientWrapper; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.presigner.S3Presigner; import software.amazon.awssdk.services.s3.presigner.S3Presigner.Builder; @Disabled class UploadServiceTest extends AcceptanceTest { - private AmazonS3 amazonS3; + private S3Client awsS3Client; private Builder presigner; @Container @@ -45,14 +41,9 @@ void setUp() { localStackContainer.getAccessKey(), localStackContainer.getSecretKey() ); - amazonS3 = AmazonS3ClientBuilder.standard() - .withRegion(localStackContainer.getRegion()) - .withCredentials(new AWSStaticCredentialsProvider( - new BasicAWSCredentials( - localStackContainer.getAccessKey(), - localStackContainer.getSecretKey() - )) - ) + awsS3Client = S3Client.builder() + .credentialsProvider(StaticCredentialsProvider.create(awsBasicCredentials)) + .region(Region.of(localStackContainer.getRegion())) .build(); presigner = S3Presigner.builder() @@ -63,11 +54,11 @@ void setUp() { @Test void 이미지_확장자를_받아_이미지_이름을_UUID로_생성_후_Presigned_URL을_생성하여_반환한다() { // given - S3Client utils = new S3Client( + S3ClientWrapper utils = new S3ClientWrapper( "test-bucket", "https://test-image.koreatech.in/", presigner, - amazonS3, + awsS3Client, Clock.systemDefaultZone() ); diff --git a/src/test/java/in/koreatech/koin/unit/domain/community/article/service/summary/ArticleSummarySourceReaderTest.java b/src/test/java/in/koreatech/koin/unit/domain/community/article/service/summary/ArticleSummarySourceReaderTest.java index d68be44ec2..790d681c06 100644 --- a/src/test/java/in/koreatech/koin/unit/domain/community/article/service/summary/ArticleSummarySourceReaderTest.java +++ b/src/test/java/in/koreatech/koin/unit/domain/community/article/service/summary/ArticleSummarySourceReaderTest.java @@ -23,7 +23,7 @@ import in.koreatech.koin.domain.community.article.service.summary.ArticleSummarySourceReader; import in.koreatech.koin.domain.community.article.service.summary.ArticleSummarySourceSeed; import in.koreatech.koin.domain.community.article.service.summary.DocumentParseRequest; -import in.koreatech.koin.infrastructure.s3.client.S3Client; +import in.koreatech.koin.infrastructure.s3.client.S3ClientWrapper; class ArticleSummarySourceReaderTest { @@ -32,7 +32,7 @@ class ArticleSummarySourceReaderTest { FakeDocumentParseClient parseClient = new FakeDocumentParseClient(); ArticleAiSummaryProperties properties = new ArticleAiSummaryProperties(); properties.setMaxDocumentsPerArticle(3); - S3Client s3Client = mock(S3Client.class); + S3ClientWrapper s3Client = mock(S3ClientWrapper.class); when(s3Client.getDomainUrlPrefix()).thenReturn("https://static.koreatech.in/"); ArticleSummarySourceReader reader = new ArticleSummarySourceReader(parseClient, properties, s3Client); ArticleSummarySourceSeed seed = new ArticleSummarySourceSeed( @@ -73,7 +73,7 @@ class ArticleSummarySourceReaderTest { FakeDocumentParseClient parseClient = new FakeDocumentParseClient(); ArticleAiSummaryProperties properties = new ArticleAiSummaryProperties(); properties.setMaxDocumentsPerArticle(3); - S3Client s3Client = mock(S3Client.class); + S3ClientWrapper s3Client = mock(S3ClientWrapper.class); when(s3Client.getDomainUrlPrefix()).thenReturn("https://static.koreatech.in/"); ArticleSummarySourceReader reader = new ArticleSummarySourceReader(parseClient, properties, s3Client); ArticleSummarySourceSeed seed = new ArticleSummarySourceSeed( @@ -103,7 +103,7 @@ class ArticleSummarySourceReaderTest { FakeDocumentParseClient parseClient = new FakeDocumentParseClient(); ArticleAiSummaryProperties properties = new ArticleAiSummaryProperties(); properties.setMaxDocumentsPerArticle(3); - S3Client s3Client = mock(S3Client.class); + S3ClientWrapper s3Client = mock(S3ClientWrapper.class); when(s3Client.getDomainUrlPrefix()).thenReturn("https://static.koreatech.in/"); ArticleSummarySourceReader reader = new ArticleSummarySourceReader(parseClient, properties, s3Client); ArticleSummarySourceSeed seed = new ArticleSummarySourceSeed( @@ -136,7 +136,7 @@ class ArticleSummarySourceReaderTest { FakeDocumentParseClient parseClient = new FakeDocumentParseClient(); ArticleAiSummaryProperties properties = new ArticleAiSummaryProperties(); properties.setMaxDocumentsPerArticle(3); - S3Client s3Client = mock(S3Client.class); + S3ClientWrapper s3Client = mock(S3ClientWrapper.class); when(s3Client.getDomainUrlPrefix()).thenReturn("https://static.koreatech.in/"); ArticleSummarySourceReader reader = new ArticleSummarySourceReader(parseClient, properties, s3Client); ArticleSummarySourceSeed seed = new ArticleSummarySourceSeed( @@ -172,7 +172,7 @@ class ArticleSummarySourceReaderTest { }; ArticleAiSummaryProperties properties = new ArticleAiSummaryProperties(); properties.setMaxDocumentsPerArticle(3); - S3Client s3Client = mock(S3Client.class); + S3ClientWrapper s3Client = mock(S3ClientWrapper.class); when(s3Client.getDomainUrlPrefix()).thenReturn("https://static.koreatech.in/"); ArticleSummarySourceReader reader = new ArticleSummarySourceReader(parseClient, properties, s3Client); ArticleSummarySourceSeed seed = new ArticleSummarySourceSeed( @@ -206,7 +206,7 @@ class ArticleSummarySourceReaderTest { }; ArticleAiSummaryProperties properties = new ArticleAiSummaryProperties(); properties.setMaxDocumentsPerArticle(3); - S3Client s3Client = mock(S3Client.class); + S3ClientWrapper s3Client = mock(S3ClientWrapper.class); when(s3Client.getDomainUrlPrefix()).thenReturn("https://static.koreatech.in/"); ArticleSummarySourceReader reader = new ArticleSummarySourceReader(parseClient, properties, s3Client); ArticleSummarySourceSeed seed = new ArticleSummarySourceSeed( @@ -240,7 +240,7 @@ class ArticleSummarySourceReaderTest { }; ArticleAiSummaryProperties properties = new ArticleAiSummaryProperties(); properties.setMaxDocumentsPerArticle(3); - S3Client s3Client = mock(S3Client.class); + S3ClientWrapper s3Client = mock(S3ClientWrapper.class); when(s3Client.getDomainUrlPrefix()).thenReturn("https://static.koreatech.in/"); ArticleSummarySourceReader reader = new ArticleSummarySourceReader(parseClient, properties, s3Client); ArticleSummarySourceSeed seed = new ArticleSummarySourceSeed( @@ -269,7 +269,7 @@ class ArticleSummarySourceReaderTest { void 허용된_koreatech_하위_도메인_본문_URL이면_URL_내용을_조회해_요약_입력으로_사용한다() { FakeDocumentParseClient parseClient = new FakeDocumentParseClient(); ArticleAiSummaryProperties properties = new ArticleAiSummaryProperties(); - S3Client s3Client = mock(S3Client.class); + S3ClientWrapper s3Client = mock(S3ClientWrapper.class); when(s3Client.getDomainUrlPrefix()).thenReturn("https://static.koreatech.in/"); when(s3Client.getContentFromUrl("https://stage-static.koreatech.in/articles/content/notice.txt")) .thenReturn("

신청 기간은 5월 20일까지입니다.

"); @@ -294,7 +294,7 @@ class ArticleSummarySourceReaderTest { void 본문_URL과_조회된_본문은_같은_fingerprint를_사용한다() { FakeDocumentParseClient parseClient = new FakeDocumentParseClient(); ArticleAiSummaryProperties properties = new ArticleAiSummaryProperties(); - S3Client s3Client = mock(S3Client.class); + S3ClientWrapper s3Client = mock(S3ClientWrapper.class); when(s3Client.getDomainUrlPrefix()).thenReturn("https://static.koreatech.in/"); when(s3Client.getContentFromUrl("https://stage-static.koreatech.in/articles/content/notice.txt")) .thenReturn("

신청 기간은 5월 20일까지입니다.

"); @@ -328,7 +328,7 @@ class ArticleSummarySourceReaderTest { void 본문이_URL로_시작하는_일반_텍스트이면_URL_본문으로_간주하지_않는다() { FakeDocumentParseClient parseClient = new FakeDocumentParseClient(); ArticleAiSummaryProperties properties = new ArticleAiSummaryProperties(); - S3Client s3Client = mock(S3Client.class); + S3ClientWrapper s3Client = mock(S3ClientWrapper.class); when(s3Client.getDomainUrlPrefix()).thenReturn("https://static.koreatech.in/"); ArticleSummarySourceReader reader = new ArticleSummarySourceReader(parseClient, properties, s3Client); ArticleSummarySourceSeed seed = new ArticleSummarySourceSeed( @@ -352,7 +352,7 @@ class ArticleSummarySourceReaderTest { void 허용되지_않은_본문_URL은_요약_입력에서_제외한다() { FakeDocumentParseClient parseClient = new FakeDocumentParseClient(); ArticleAiSummaryProperties properties = new ArticleAiSummaryProperties(); - S3Client s3Client = mock(S3Client.class); + S3ClientWrapper s3Client = mock(S3ClientWrapper.class); when(s3Client.getDomainUrlPrefix()).thenReturn("https://static.koreatech.in/"); ArticleSummarySourceReader reader = new ArticleSummarySourceReader(parseClient, properties, s3Client); ArticleSummarySourceSeed seed = new ArticleSummarySourceSeed( diff --git a/src/test/java/in/koreatech/koin/unit/domain/dining/service/DiningReportImageValidationTest.java b/src/test/java/in/koreatech/koin/unit/domain/dining/service/DiningReportImageValidationTest.java index 238e750c73..2e085467ad 100644 --- a/src/test/java/in/koreatech/koin/unit/domain/dining/service/DiningReportImageValidationTest.java +++ b/src/test/java/in/koreatech/koin/unit/domain/dining/service/DiningReportImageValidationTest.java @@ -26,8 +26,6 @@ import org.mockito.ArgumentCaptor; import org.springframework.context.ApplicationEventPublisher; -import com.amazonaws.services.s3.AmazonS3; - import in.koreatech.koin.domain.coop.repository.DiningSoldOutCacheRepository; import in.koreatech.koin.domain.coopshop.service.CoopShopService; import in.koreatech.koin.domain.dining.dto.DiningReportCreateRequest; @@ -42,13 +40,16 @@ import in.koreatech.koin.domain.dining.service.DiningReportService; import in.koreatech.koin.domain.user.repository.UserRepository; import in.koreatech.koin.global.exception.CustomException; -import in.koreatech.koin.infrastructure.s3.client.S3Client; +import in.koreatech.koin.infrastructure.s3.client.S3ClientWrapper; import in.koreatech.koin.infrastructure.s3.dto.UploadUrlRequest; import in.koreatech.koin.infrastructure.s3.model.ImageUploadDomain; import in.koreatech.koin.infrastructure.s3.service.UploadService; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.HeadObjectRequest; +import software.amazon.awssdk.services.s3.model.HeadObjectResponse; import software.amazon.awssdk.services.s3.presigner.S3Presigner; class DiningReportImageValidationTest { @@ -57,19 +58,19 @@ class DiningReportImageValidationTest { private static final String DOMAIN_URL = "https://static.koreatech.in/"; private final Clock clock = Clock.fixed(Instant.parse("2026-10-02T03:00:00Z"), ZoneId.of("Asia/Seoul")); - private AmazonS3 amazonS3; - private S3Client s3Client; + private S3Client awsS3Client; + private S3ClientWrapper s3Client; private DiningRepository diningRepository; private DiningReportRepository reportRepository; private DiningReportService service; @BeforeEach void setUp() { - amazonS3 = mock(AmazonS3.class); - s3Client = new S3Client(BUCKET_NAME, DOMAIN_URL, S3Presigner.builder() + awsS3Client = mock(S3Client.class); + s3Client = new S3ClientWrapper(BUCKET_NAME, DOMAIN_URL, S3Presigner.builder() .credentialsProvider(StaticCredentialsProvider.create( AwsBasicCredentials.create("test-access-key", "test-secret-key"))) - .region(Region.AP_NORTHEAST_2), amazonS3, clock); + .region(Region.AP_NORTHEAST_2), awsS3Client, clock); diningRepository = mock(DiningRepository.class); reportRepository = mock(DiningReportRepository.class); service = new DiningReportService(diningRepository, reportRepository, @@ -95,7 +96,7 @@ void setUp() { new UploadUrlRequest(1, "image/jpeg", fileName)).fileUrl(); String rawKey = imageUrl.substring(DOMAIN_URL.length()); assertThat(imageUrl).startsWith(DOMAIN_URL + "upload/COOP/2026/10/2/").endsWith("/" + fileName); - when(amazonS3.doesObjectExist(BUCKET_NAME, rawKey)).thenReturn(true); + when(awsS3Client.headObject(headObjectRequest(rawKey))).thenReturn(HeadObjectResponse.builder().build()); Integer reporterId = 42; Integer diningId = 1; @@ -110,7 +111,7 @@ void setUp() { verify(reportRepository).saveAndFlush(reportCaptor.capture()); assertThat(response.status()).isEqualTo(DiningReportStatus.PENDING); assertThat(reportCaptor.getValue().getImageUrl()).isEqualTo(imageUrl); - verify(amazonS3).doesObjectExist(BUCKET_NAME, rawKey); + verify(awsS3Client).headObject(headObjectRequest(rawKey)); when(reportRepository.findById(1)).thenReturn(Optional.of(reportCaptor.getValue())); var queryService = new DiningReportQueryService(reportRepository, mock(UserRepository.class)); @@ -132,7 +133,8 @@ void setUp() { mock(CoopShopService.class), mock(ApplicationEventPublisher.class), s3Client, utcClock); String imageUrl = new UploadService(s3Client, clock).getPresignedUrl(ImageUploadDomain.COOP, new UploadUrlRequest(1, "image/jpeg", "soldout.jpg")).fileUrl(); - when(amazonS3.doesObjectExist(BUCKET_NAME, imageUrl.substring(DOMAIN_URL.length()))).thenReturn(true); + when(awsS3Client.headObject(headObjectRequest(imageUrl.substring(DOMAIN_URL.length())))) + .thenReturn(HeadObjectResponse.builder().build()); Dining dining = Dining.builder().date(LocalDate.parse(diningDate)).type(DiningType.LUNCH) .place("A코너").menu("[\"돈까스\"]").build(); when(diningRepository.findByIdForUpdate(1)).thenReturn(Optional.of(dining)); @@ -165,6 +167,10 @@ void setUp() { assertThatThrownBy(() -> service.create(42, 1, new DiningReportCreateRequest(imageUrl))) .isInstanceOf(CustomException.class) .hasMessage(INVALID_REPORT_IMAGE.getMessage()); - verifyNoInteractions(amazonS3, diningRepository); + verifyNoInteractions(awsS3Client, diningRepository); + } + + private HeadObjectRequest headObjectRequest(String key) { + return HeadObjectRequest.builder().bucket(BUCKET_NAME).key(key).build(); } } diff --git a/src/test/java/in/koreatech/koin/unit/infrastructure/s3/client/S3ClientTest.java b/src/test/java/in/koreatech/koin/unit/infrastructure/s3/client/S3ClientWrapperTest.java similarity index 91% rename from src/test/java/in/koreatech/koin/unit/infrastructure/s3/client/S3ClientTest.java rename to src/test/java/in/koreatech/koin/unit/infrastructure/s3/client/S3ClientWrapperTest.java index 3bfaa348ad..ac76cbb371 100644 --- a/src/test/java/in/koreatech/koin/unit/infrastructure/s3/client/S3ClientTest.java +++ b/src/test/java/in/koreatech/koin/unit/infrastructure/s3/client/S3ClientWrapperTest.java @@ -10,23 +10,22 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import com.amazonaws.services.s3.AmazonS3; - import in.koreatech.koin.global.exception.custom.KoinIllegalStateException; -import in.koreatech.koin.infrastructure.s3.client.S3Client; +import in.koreatech.koin.infrastructure.s3.client.S3ClientWrapper; +import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.presigner.S3Presigner; -class S3ClientTest { +class S3ClientWrapperTest { - private S3Client s3Client; + private S3ClientWrapper s3Client; @BeforeEach void setUp() { - s3Client = new S3Client( + s3Client = new S3ClientWrapper( "test-bucket", "https://static.koreatech.in/articles/", mock(S3Presigner.Builder.class), - mock(AmazonS3.class), + mock(S3Client.class), Clock.systemUTC() ); }