Skip to content

Commit b4d6ec2

Browse files
authored
refactor: s3 버전 업그레이드 및 로직 수정 (#608)
* refactor: s3 sdk 버전 업그레이드 - 의존성 수정 - 버전 업그레이드에 따른 코드 수정 * refactor: 이미지 이외의 파일 관리를 위해 ImgType 의미 명확하도록 수정 - ImgType에서 UploadType으로 변경 - 해당되는 파일 모두 수정 * refactor: s3 테스트 코드 추가 * fix: s3 access-key, secret-key 최신화, 버킷 명칭 올바르게 수정 * fix: ChatService Test 변경점 반영, S3ServiceTest 단위 테스트로 변경 - images->files로 디렉토리 경로 수정 * fix: 이중 비동기 실행문제 해결 - @async에 전적으로 위임 * refactor: S3Service error 메시지 NPE 가능성 제거 * refactor: 수정사항 반영 - UploadType -> UploadPath로 명칭변경 - 컨벤션 수정(미사용 변수 삭제, 들여쓰기, 명칭변경) * fix: 테스트 코드 오류 수정 - 내부 로직에서 사용하는 fileUploadService 정의 * refactor: 수정사항 반영 - 파일 확장자 상수화 - 확장자 확인로직, 채팅이면 모든 파일 허용, 이미지 확인까지 모두 enum에서 관리 - MultipartFile이 비동기 과정에서 유실되지 않도록 byte로 변환해서 전달 - UrlPrefixResponse PascalCase로 변경 * refactor: 컨벤션 수정 - 사용하지 않는 import문 삭제
1 parent db0287f commit b4d6ec2

File tree

22 files changed

+332
-163
lines changed

22 files changed

+332
-163
lines changed

build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,9 @@ dependencies {
6565
testImplementation 'org.awaitility:awaitility:4.2.0'
6666

6767
// Etc
68+
implementation platform('software.amazon.awssdk:bom:2.41.4')
69+
implementation 'software.amazon.awssdk:s3'
6870
implementation 'org.hibernate.validator:hibernate-validator'
69-
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.782'
7071
implementation 'org.springframework.boot:spring-boot-starter-websocket'
7172

7273
// Database Proxy

src/main/java/com/example/solidconnection/chat/service/ChatService.java

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import com.example.solidconnection.mentor.repository.MentorRepository;
2828
import com.example.solidconnection.siteuser.domain.SiteUser;
2929
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
30+
import java.util.Arrays;
3031
import java.util.Collections;
3132
import java.util.List;
3233
import java.util.Map;
@@ -39,6 +40,7 @@
3940
import org.springframework.messaging.simp.SimpMessageSendingOperations;
4041
import org.springframework.stereotype.Service;
4142
import org.springframework.transaction.annotation.Transactional;
43+
import org.springframework.util.StringUtils;
4244

4345
@Service
4446
public class ChatService {
@@ -240,16 +242,19 @@ public void sendChatImage(ChatImageSendRequest chatImageSendRequest, long siteUs
240242
ChatRoom chatRoom = chatRoomRepository.findById(roomId)
241243
.orElseThrow(() -> new CustomException(INVALID_CHAT_ROOM_STATE));
242244

243-
ChatMessage chatMessage = new ChatMessage(
244-
"",
245-
senderId,
246-
chatRoom
247-
);
245+
ChatMessage chatMessage = new ChatMessage("", senderId, chatRoom);
246+
247+
// 이미지 판별을 위한 확장자 리스트
248+
List<String> imageExtensions = Arrays.asList("jpg", "jpeg", "png", "webp");
248249

249250
for (String imageUrl : chatImageSendRequest.imageUrls()) {
250-
String thumbnailUrl = generateThumbnailUrl(imageUrl);
251+
String extension = StringUtils.getFilenameExtension(imageUrl);
251252

252-
ChatAttachment attachment = new ChatAttachment(true, imageUrl, thumbnailUrl, null);
253+
boolean isImage = extension != null && imageExtensions.contains(extension.toLowerCase());
254+
255+
String thumbnailUrl = isImage ? generateThumbnailUrl(imageUrl) : null;
256+
257+
ChatAttachment attachment = new ChatAttachment(isImage, imageUrl, thumbnailUrl, null);
253258
chatMessage.addAttachment(attachment);
254259
}
255260

@@ -268,11 +273,9 @@ private String generateThumbnailUrl(String originalUrl) {
268273

269274
String thumbnailFileName = nameWithoutExt + "_thumb" + extension;
270275

271-
String thumbnailUrl = originalUrl.replace("chat/images/", "chat/thumbnails/")
276+
return originalUrl.replace("chat/files/", "chat/thumbnails/")
272277
.replace(fileName, thumbnailFileName);
273278

274-
return thumbnailUrl;
275-
276279
} catch (Exception e) {
277280
return originalUrl;
278281
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.example.solidconnection.common.constant;
2+
3+
import java.util.List;
4+
import java.util.stream.Stream;
5+
6+
public final class FileConstants {
7+
private FileConstants() {}
8+
9+
public static final List<String> IMAGE_EXTENSIONS = List.of(
10+
"jpg", "jpeg", "png", "webp", "avif", "heic", "heif", "tiff"
11+
);
12+
13+
public static final List<String> DOCUMENT_EXTENSIONS = List.of(
14+
"doc", "docx", "xls", "xlsx", "ppt", "pptx", "hwp", "hwpx", "pdf", "txt"
15+
);
16+
17+
public static final List<String> ARCHIVE_EXTENSIONS = List.of(
18+
"zip", "7z", "rar"
19+
);
20+
21+
public static final List<String> ALL_ALLOWED_EXTENSIONS = Stream.of(
22+
IMAGE_EXTENSIONS, DOCUMENT_EXTENSIONS, ARCHIVE_EXTENSIONS)
23+
.flatMap(List::stream)
24+
.toList();
25+
}

src/main/java/com/example/solidconnection/community/post/service/PostCommandService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import com.example.solidconnection.community.post.dto.PostUpdateRequest;
1919
import com.example.solidconnection.community.post.dto.PostUpdateResponse;
2020
import com.example.solidconnection.community.post.repository.PostRepository;
21-
import com.example.solidconnection.s3.domain.ImgType;
21+
import com.example.solidconnection.s3.domain.UploadPath;
2222
import com.example.solidconnection.s3.dto.UploadedFileUrlResponse;
2323
import com.example.solidconnection.s3.service.S3Service;
2424
import com.example.solidconnection.siteuser.domain.SiteUser;
@@ -88,7 +88,7 @@ private void savePostImages(List<MultipartFile> imageFile, Post post) {
8888
if (imageFile.isEmpty()) {
8989
return;
9090
}
91-
List<UploadedFileUrlResponse> uploadedFileUrlResponseList = s3Service.uploadFiles(imageFile, ImgType.COMMUNITY);
91+
List<UploadedFileUrlResponse> uploadedFileUrlResponseList = s3Service.uploadFiles(imageFile, UploadPath.COMMUNITY);
9292
for (UploadedFileUrlResponse uploadedFileUrlResponse : uploadedFileUrlResponseList) {
9393
PostImage postImage = new PostImage(uploadedFileUrlResponse.fileUrl());
9494
postImage.setPost(post);

src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import com.example.solidconnection.mentor.domain.MentorApplicationStatus;
66
import com.example.solidconnection.mentor.dto.MentorApplicationRequest;
77
import com.example.solidconnection.mentor.repository.MentorApplicationRepository;
8-
import com.example.solidconnection.s3.domain.ImgType;
8+
import com.example.solidconnection.s3.domain.UploadPath;
99
import com.example.solidconnection.s3.dto.UploadedFileUrlResponse;
1010
import com.example.solidconnection.s3.service.S3Service;
1111
import com.example.solidconnection.siteuser.domain.SiteUser;
@@ -45,7 +45,7 @@ public void submitMentorApplication(
4545
.orElseThrow(() -> new CustomException(USER_NOT_FOUND));
4646
Term term = termRepository.findByName(mentorApplicationRequest.term())
4747
.orElseThrow(() -> new CustomException(TERM_NOT_FOUND));
48-
UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(file, ImgType.MENTOR_PROOF);
48+
UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(file, UploadPath.MENTOR_PROOF);
4949
MentorApplication mentorApplication = new MentorApplication(
5050
siteUser.getId(),
5151
mentorApplicationRequest.country(),

src/main/java/com/example/solidconnection/news/service/NewsCommandService.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import com.example.solidconnection.news.dto.NewsCreateRequest;
1212
import com.example.solidconnection.news.dto.NewsUpdateRequest;
1313
import com.example.solidconnection.news.repository.NewsRepository;
14-
import com.example.solidconnection.s3.domain.ImgType;
14+
import com.example.solidconnection.s3.domain.UploadPath;
1515
import com.example.solidconnection.s3.dto.UploadedFileUrlResponse;
1616
import com.example.solidconnection.s3.service.S3Service;
1717
import com.example.solidconnection.siteuser.domain.Role;
@@ -41,7 +41,7 @@ public NewsCommandResponse createNews(long siteUserId, NewsCreateRequest newsCre
4141

4242
private String getImageUrl(MultipartFile imageFile) {
4343
if (imageFile != null && !imageFile.isEmpty()) {
44-
UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(imageFile, ImgType.NEWS);
44+
UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(imageFile, UploadPath.NEWS);
4545
return uploadedFile.fileUrl();
4646
}
4747
return newsProperties.defaultThumbnailUrl();
@@ -73,7 +73,7 @@ private void updateThumbnail(News news, MultipartFile imageFile, Boolean resetTo
7373
deleteCustomImage(news.getThumbnailUrl());
7474
news.updateThumbnailUrl(newsProperties.defaultThumbnailUrl());
7575
} else if (imageFile != null && !imageFile.isEmpty()) {
76-
UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(imageFile, ImgType.NEWS);
76+
UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(imageFile, UploadPath.NEWS);
7777
deleteCustomImage(news.getThumbnailUrl());
7878
news.updateThumbnailUrl(uploadedFile.fileUrl());
7979
}
Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
package com.example.solidconnection.s3.config;
22

3-
import com.amazonaws.auth.AWSStaticCredentialsProvider;
4-
import com.amazonaws.auth.BasicAWSCredentials;
5-
import com.amazonaws.services.s3.AmazonS3Client;
6-
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
73
import org.springframework.beans.factory.annotation.Value;
84
import org.springframework.context.annotation.Bean;
95
import org.springframework.context.annotation.Configuration;
6+
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
7+
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
8+
import software.amazon.awssdk.regions.Region;
9+
import software.amazon.awssdk.services.s3.S3Client;
1010

1111
@Configuration
1212
public class AmazonS3Config {
@@ -21,12 +21,12 @@ public class AmazonS3Config {
2121
private String region;
2222

2323
@Bean
24-
public AmazonS3Client amazonS3Client() {
25-
BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
26-
return (AmazonS3Client) AmazonS3ClientBuilder
27-
.standard()
28-
.withRegion(region)
29-
.withCredentials(new AWSStaticCredentialsProvider(credentials))
24+
public S3Client s3Client() {
25+
AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKey, secretKey);
26+
27+
return S3Client.builder()
28+
.region(Region.of(region))
29+
.credentialsProvider(StaticCredentialsProvider.create(credentials))
3030
.build();
3131
}
3232
}

src/main/java/com/example/solidconnection/s3/controller/S3Controller.java

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package com.example.solidconnection.s3.controller;
22

33
import com.example.solidconnection.common.resolver.AuthorizedUser;
4-
import com.example.solidconnection.s3.domain.ImgType;
4+
import com.example.solidconnection.s3.domain.UploadPath;
55
import com.example.solidconnection.s3.dto.UploadedFileUrlResponse;
6-
import com.example.solidconnection.s3.dto.urlPrefixResponse;
6+
import com.example.solidconnection.s3.dto.UrlPrefixResponse;
77
import com.example.solidconnection.s3.service.S3Service;
88
import java.util.List;
99
import lombok.RequiredArgsConstructor;
@@ -39,7 +39,7 @@ public class S3Controller {
3939
public ResponseEntity<UploadedFileUrlResponse> uploadPreProfileImage(
4040
@RequestParam("file") MultipartFile imageFile
4141
) {
42-
UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, ImgType.PROFILE);
42+
UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, UploadPath.PROFILE);
4343
return ResponseEntity.ok(profileImageUrl);
4444
}
4545

@@ -48,7 +48,7 @@ public ResponseEntity<UploadedFileUrlResponse> uploadPostProfileImage(
4848
@AuthorizedUser long siteUserId,
4949
@RequestParam("file") MultipartFile imageFile
5050
) {
51-
UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, ImgType.PROFILE);
51+
UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, UploadPath.PROFILE);
5252
s3Service.deleteExProfile(siteUserId);
5353
return ResponseEntity.ok(profileImageUrl);
5454
}
@@ -57,28 +57,28 @@ public ResponseEntity<UploadedFileUrlResponse> uploadPostProfileImage(
5757
public ResponseEntity<UploadedFileUrlResponse> uploadGpaImage(
5858
@RequestParam("file") MultipartFile imageFile
5959
) {
60-
UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, ImgType.GPA);
60+
UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, UploadPath.GPA);
6161
return ResponseEntity.ok(profileImageUrl);
6262
}
6363

6464
@PostMapping("/language-test")
6565
public ResponseEntity<UploadedFileUrlResponse> uploadLanguageImage(
6666
@RequestParam("file") MultipartFile imageFile
6767
) {
68-
UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, ImgType.LANGUAGE_TEST);
68+
UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, UploadPath.LANGUAGE_TEST);
6969
return ResponseEntity.ok(profileImageUrl);
7070
}
7171

7272
@PostMapping("/chat")
73-
public ResponseEntity<List<UploadedFileUrlResponse>> uploadChatImage(
74-
@RequestParam("files") List<MultipartFile> imageFiles
73+
public ResponseEntity<List<UploadedFileUrlResponse>> uploadChatFile(
74+
@RequestParam("files") List<MultipartFile> files
7575
) {
76-
List<UploadedFileUrlResponse> chatImageUrls = s3Service.uploadFiles(imageFiles, ImgType.CHAT);
76+
List<UploadedFileUrlResponse> chatImageUrls = s3Service.uploadFiles(files, UploadPath.CHAT);
7777
return ResponseEntity.ok(chatImageUrls);
7878
}
7979

8080
@GetMapping("/s3-url-prefix")
81-
public ResponseEntity<urlPrefixResponse> getS3UrlPrefix() {
82-
return ResponseEntity.ok(new urlPrefixResponse(s3Default, s3Uploaded, cloudFrontDefault, cloudFrontUploaded));
81+
public ResponseEntity<UrlPrefixResponse> getS3UrlPrefix() {
82+
return ResponseEntity.ok(new UrlPrefixResponse(s3Default, s3Uploaded, cloudFrontDefault, cloudFrontUploaded));
8383
}
8484
}

src/main/java/com/example/solidconnection/s3/domain/ImgType.java

Lines changed: 0 additions & 21 deletions
This file was deleted.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.example.solidconnection.s3.domain;
2+
3+
import com.example.solidconnection.common.constant.FileConstants;
4+
import com.example.solidconnection.common.exception.CustomException;
5+
import com.example.solidconnection.common.exception.ErrorCode;
6+
import lombok.Getter;
7+
8+
@Getter
9+
public enum UploadPath {
10+
PROFILE("profile"),
11+
GPA("gpa"),
12+
LANGUAGE_TEST("language"),
13+
COMMUNITY("community"),
14+
NEWS("news"),
15+
CHAT("chat/files"),
16+
MENTOR_PROOF("mentor-proof"),
17+
;
18+
19+
private final String type;
20+
21+
UploadPath(String type) {
22+
this.type = type;
23+
}
24+
25+
public boolean isResizable(long fileSize, String extension, long maxSizeBytes) {
26+
if (!isImage(extension)) return false;
27+
28+
if (this == CHAT) return false;
29+
30+
return fileSize >= maxSizeBytes;
31+
}
32+
public void validateExtension(String extension) {
33+
if (extension == null || !FileConstants.ALL_ALLOWED_EXTENSIONS.contains(extension.toLowerCase())) {
34+
throw new CustomException(ErrorCode.NOT_ALLOWED_FILE_EXTENSIONS,
35+
"허용된 형식: " + getAllowedExtensionsMessage());
36+
}
37+
}
38+
39+
public boolean isImage(String extension) {
40+
return extension != null && FileConstants.IMAGE_EXTENSIONS.contains(extension.toLowerCase());
41+
}
42+
43+
public String getAllowedExtensionsMessage() {
44+
return String.join(", ", FileConstants.ALL_ALLOWED_EXTENSIONS);
45+
}
46+
}

0 commit comments

Comments
 (0)