-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* chore: aws, s3, cloudfront 의존성 추가 * feat: AWS S3 설정 추가 * feat: StoreImageProcessor 상수 추가 * feat: S3 이미지 업로드 기능 추가 * test: S3 이미지 업로드 기능 테스트 추가 * feat: S3 이미지 업로드 기능 사용을 제어할 수 있는 설정 추가 * feat: 테스트 환경에서 S3 이미지 업로드 기능 사용을 제어할 수 있는 설정의 설정값 변경 * remove: 테스트용 AwsConfiguration 삭제 * feat: 프로덕션 환경에서만 동작하는 Profile 어노테이션 추가 * test: 누락된 s3 지역 설정값 추가 * feat: 프로덕션 환경에 s3 관련 yml 설정 추가 * refactor: 누락된 final 추가
- Loading branch information
1 parent
07bad46
commit a97b42d
Showing
11 changed files
with
301 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
25 changes: 25 additions & 0 deletions
25
backend/ddang/src/main/java/com/ddang/ddang/auction/configuration/AwsConfiguration.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package com.ddang.ddang.auction.configuration; | ||
|
||
import com.ddang.ddang.configuration.ProductProfile; | ||
import com.google.api.client.util.Value; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider; | ||
import software.amazon.awssdk.regions.Region; | ||
import software.amazon.awssdk.services.s3.S3Client; | ||
|
||
@ProductProfile | ||
@Configuration | ||
public class AwsConfiguration { | ||
|
||
@Value("${aws.s3.region}") | ||
private String s3Region; | ||
|
||
@Bean | ||
public S3Client s3Client() { | ||
return S3Client.builder() | ||
.region(Region.of(s3Region)) | ||
.credentialsProvider(InstanceProfileCredentialsProvider.create()) | ||
.build(); | ||
} | ||
} |
14 changes: 14 additions & 0 deletions
14
backend/ddang/src/main/java/com/ddang/ddang/configuration/ProductProfile.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package com.ddang.ddang.configuration; | ||
|
||
import org.springframework.context.annotation.Profile; | ||
|
||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
|
||
@Target(ElementType.TYPE) | ||
@Retention(RetentionPolicy.RUNTIME) | ||
@Profile("!local && !test") | ||
public @interface ProductProfile { | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
103 changes: 103 additions & 0 deletions
103
...nd/ddang/src/main/java/com/ddang/ddang/image/infrastructure/s3/S3StoreImageProcessor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
package com.ddang.ddang.image.infrastructure.s3; | ||
|
||
import com.ddang.ddang.configuration.ProductProfile; | ||
import com.ddang.ddang.image.domain.StoreImageProcessor; | ||
import com.ddang.ddang.image.domain.dto.StoreImageDto; | ||
import com.ddang.ddang.image.infrastructure.local.exception.EmptyImageException; | ||
import com.ddang.ddang.image.infrastructure.local.exception.StoreImageFailureException; | ||
import com.ddang.ddang.image.infrastructure.local.exception.UnsupportedImageFileExtensionException; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.web.multipart.MultipartFile; | ||
import software.amazon.awssdk.core.exception.SdkException; | ||
import software.amazon.awssdk.core.sync.RequestBody; | ||
import software.amazon.awssdk.services.s3.S3Client; | ||
import software.amazon.awssdk.services.s3.model.PutObjectRequest; | ||
|
||
import java.io.IOException; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.UUID; | ||
|
||
@Component | ||
@ProductProfile | ||
@ConditionalOnProperty(name = "aws.s3.enabled", havingValue = "true") | ||
@RequiredArgsConstructor | ||
public class S3StoreImageProcessor implements StoreImageProcessor { | ||
|
||
@Value("${aws.s3.bucket-name}") | ||
private String bucketName; | ||
|
||
@Value("${aws.s3.image-path}") | ||
private String path; | ||
|
||
private final S3Client s3Client; | ||
|
||
@Override | ||
public List<StoreImageDto> storeImageFiles(final List<MultipartFile> imageFiles) { | ||
final List<StoreImageDto> storeImageDtos = new ArrayList<>(); | ||
|
||
for (final MultipartFile imageFile : imageFiles) { | ||
if (imageFile.isEmpty()) { | ||
throw new EmptyImageException("이미지 파일의 데이터가 비어 있습니다."); | ||
} | ||
|
||
storeImageDtos.add(storeImageFile(imageFile)); | ||
} | ||
|
||
return storeImageDtos; | ||
} | ||
|
||
@Override | ||
public StoreImageDto storeImageFile(final MultipartFile imageFile) { | ||
try { | ||
final String originalImageFileName = imageFile.getOriginalFilename(); | ||
final String storeImageFileName = createStoreImageFileName(originalImageFileName); | ||
final String fullPath = findFullPath(storeImageFileName); | ||
final PutObjectRequest putObjectRequest = PutObjectRequest.builder() | ||
.key(fullPath) | ||
.bucket(bucketName) | ||
.contentType(imageFile.getContentType()) | ||
.build(); | ||
|
||
s3Client.putObject( | ||
putObjectRequest, | ||
RequestBody.fromInputStream(imageFile.getInputStream(), imageFile.getSize()) | ||
); | ||
|
||
return new StoreImageDto(originalImageFileName, storeImageFileName); | ||
} catch (final IOException ex) { | ||
throw new StoreImageFailureException("이미지 저장에 실패했습니다.", ex); | ||
} catch (final SdkException ex) { | ||
throw new StoreImageFailureException("AWS 이미지 저장에 실패했습니다.", ex); | ||
} | ||
} | ||
|
||
private String findFullPath(final String storeImageFileName) { | ||
return path + storeImageFileName; | ||
} | ||
|
||
private String createStoreImageFileName(final String originalFilename) { | ||
final String extension = extractExtension(originalFilename); | ||
|
||
validateImageFileExtension(extension); | ||
|
||
final String uuid = UUID.randomUUID().toString(); | ||
|
||
return uuid + EXTENSION_FILE_CHARACTER + extension; | ||
} | ||
|
||
private String extractExtension(final String originalFilename) { | ||
int position = originalFilename.lastIndexOf(EXTENSION_FILE_CHARACTER); | ||
|
||
return originalFilename.substring(position + 1); | ||
} | ||
|
||
private void validateImageFileExtension(final String extension) { | ||
if (!WHITE_IMAGE_EXTENSION.contains(extension)) { | ||
throw new UnsupportedImageFileExtensionException("지원하지 않는 확장자입니다. : " + extension); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
106 changes: 106 additions & 0 deletions
106
...dang/src/test/java/com/ddang/ddang/image/infrastructure/s3/S3StoreImageProcessorTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
package com.ddang.ddang.image.infrastructure.s3; | ||
|
||
import com.ddang.ddang.image.domain.dto.StoreImageDto; | ||
import com.ddang.ddang.image.infrastructure.local.exception.EmptyImageException; | ||
import com.ddang.ddang.image.infrastructure.local.exception.StoreImageFailureException; | ||
import com.ddang.ddang.image.infrastructure.local.exception.UnsupportedImageFileExtensionException; | ||
import com.ddang.ddang.image.infrastructure.s3.fixture.S3StoreImageProcessorFixture; | ||
import org.assertj.core.api.SoftAssertions; | ||
import org.junit.jupiter.api.DisplayNameGeneration; | ||
import org.junit.jupiter.api.DisplayNameGenerator; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.extension.ExtendWith; | ||
import org.mockito.InjectMocks; | ||
import org.mockito.Mock; | ||
import org.mockito.junit.jupiter.MockitoExtension; | ||
import software.amazon.awssdk.core.exception.SdkException; | ||
import software.amazon.awssdk.core.sync.RequestBody; | ||
import software.amazon.awssdk.services.s3.S3Client; | ||
import software.amazon.awssdk.services.s3.model.PutObjectRequest; | ||
import software.amazon.awssdk.services.s3.model.PutObjectResponse; | ||
|
||
import java.io.ByteArrayInputStream; | ||
import java.io.IOException; | ||
import java.util.List; | ||
|
||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | ||
import static org.mockito.ArgumentMatchers.any; | ||
import static org.mockito.BDDMockito.given; | ||
|
||
@ExtendWith({MockitoExtension.class}) | ||
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) | ||
@SuppressWarnings("NonAsciiCharacters") | ||
class S3StoreImageProcessorTest extends S3StoreImageProcessorFixture { | ||
|
||
@InjectMocks | ||
S3StoreImageProcessor imageProcessor; | ||
|
||
@Mock | ||
S3Client s3Client; | ||
|
||
@Test | ||
void 이미지_파일이_비어_있는_경우_예외가_발생한다() { | ||
// when & then | ||
assertThatThrownBy(() -> imageProcessor.storeImageFiles(List.of(빈_이미지_파일))) | ||
.isInstanceOf(EmptyImageException.class) | ||
.hasMessage("이미지 파일의 데이터가 비어 있습니다."); | ||
} | ||
|
||
@Test | ||
void 허용되지_않은_확장자의_이미지_파일인_경우_예외가_발생한다() { | ||
// given | ||
given(이미지_파일.getOriginalFilename()).willReturn(지원하지_않는_확장자를_가진_이미지_파일명); | ||
|
||
// when & then | ||
assertThatThrownBy(() -> imageProcessor.storeImageFiles(List.of(이미지_파일))) | ||
.isInstanceOf(UnsupportedImageFileExtensionException.class) | ||
.hasMessageContaining("지원하지 않는 확장자입니다."); | ||
} | ||
|
||
@Test | ||
void 이미지_저장에_실패한_경우_예외가_발생한다() throws IOException { | ||
// given | ||
given(이미지_파일.getOriginalFilename()).willReturn(기존_이미지_파일명); | ||
given(이미지_파일.getInputStream()).willThrow(new IOException()); | ||
|
||
// when & then | ||
assertThatThrownBy(() -> imageProcessor.storeImageFiles(List.of(이미지_파일))) | ||
.isInstanceOf(StoreImageFailureException.class) | ||
.hasMessage("이미지 저장에 실패했습니다."); | ||
} | ||
|
||
@Test | ||
void AWS_이미지_저장에_실패한_경우_예외가_발생한다() throws IOException { | ||
// given | ||
final ByteArrayInputStream fakeInputStream = new ByteArrayInputStream("가짜 이미지 데이터".getBytes()); | ||
given(이미지_파일.getOriginalFilename()).willReturn(기존_이미지_파일명); | ||
given(이미지_파일.getInputStream()).willReturn(fakeInputStream); | ||
given(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))) | ||
.willThrow(SdkException.class); | ||
|
||
// when & then | ||
assertThatThrownBy(() -> imageProcessor.storeImageFiles(List.of(이미지_파일))) | ||
.isInstanceOf(StoreImageFailureException.class) | ||
.hasMessage("AWS 이미지 저장에 실패했습니다."); | ||
} | ||
|
||
@Test | ||
void 유효한_이미지_파일인_경우_이미지_파일을_저장한다() throws Exception { | ||
// given | ||
final ByteArrayInputStream fakeInputStream = new ByteArrayInputStream("가짜 이미지 데이터".getBytes()); | ||
given(이미지_파일.getOriginalFilename()).willReturn(기존_이미지_파일명); | ||
given(이미지_파일.getInputStream()).willReturn(fakeInputStream); | ||
given(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))) | ||
.willReturn(PutObjectResponse.builder().build()); | ||
|
||
// when | ||
final List<StoreImageDto> actual = imageProcessor.storeImageFiles(List.of(이미지_파일)); | ||
|
||
// then | ||
SoftAssertions.assertSoftly(softAssertions -> { | ||
softAssertions.assertThat(actual).hasSize(1); | ||
softAssertions.assertThat(actual.get(0).storeName()).isNotBlank(); | ||
softAssertions.assertThat(actual.get(0).uploadName()).isEqualTo(기존_이미지_파일명); | ||
}); | ||
} | ||
} |
14 changes: 14 additions & 0 deletions
14
...st/java/com/ddang/ddang/image/infrastructure/s3/fixture/S3StoreImageProcessorFixture.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package com.ddang.ddang.image.infrastructure.s3.fixture; | ||
|
||
import org.springframework.mock.web.MockMultipartFile; | ||
import org.springframework.web.multipart.MultipartFile; | ||
|
||
import static org.mockito.Mockito.mock; | ||
|
||
public class S3StoreImageProcessorFixture { | ||
|
||
protected MockMultipartFile 빈_이미지_파일 = new MockMultipartFile("image.png", new byte[0]); | ||
protected MultipartFile 이미지_파일 = mock(MultipartFile.class); | ||
protected String 기존_이미지_파일명 = "image.png"; | ||
protected String 지원하지_않는_확장자를_가진_이미지_파일명 = "image.gif"; | ||
} |
Oops, something went wrong.