-
Notifications
You must be signed in to change notification settings - Fork 8
refactor: s3 버전 업그레이드 및 로직 수정 #608
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
8bc53a7
6210cac
4a020d6
e799840
18b9039
a709bdf
1bd38ee
3d01d3b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,6 +27,7 @@ | |
| import com.example.solidconnection.mentor.repository.MentorRepository; | ||
| import com.example.solidconnection.siteuser.domain.SiteUser; | ||
| import com.example.solidconnection.siteuser.repository.SiteUserRepository; | ||
| import java.util.Arrays; | ||
| import java.util.Collections; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
@@ -39,6 +40,7 @@ | |
| import org.springframework.messaging.simp.SimpMessageSendingOperations; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
| import org.springframework.util.StringUtils; | ||
|
|
||
| @Service | ||
| public class ChatService { | ||
|
|
@@ -240,16 +242,19 @@ public void sendChatImage(ChatImageSendRequest chatImageSendRequest, long siteUs | |
| ChatRoom chatRoom = chatRoomRepository.findById(roomId) | ||
| .orElseThrow(() -> new CustomException(INVALID_CHAT_ROOM_STATE)); | ||
|
|
||
| ChatMessage chatMessage = new ChatMessage( | ||
| "", | ||
| senderId, | ||
| chatRoom | ||
| ); | ||
| ChatMessage chatMessage = new ChatMessage("", senderId, chatRoom); | ||
|
|
||
| // 이미지 판별을 위한 확장자 리스트 | ||
| List<String> imageExtensions = Arrays.asList("jpg", "jpeg", "png", "webp"); | ||
Hexeong marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| for (String imageUrl : chatImageSendRequest.imageUrls()) { | ||
| String thumbnailUrl = generateThumbnailUrl(imageUrl); | ||
| String extension = StringUtils.getFilenameExtension(imageUrl); | ||
|
|
||
| ChatAttachment attachment = new ChatAttachment(true, imageUrl, thumbnailUrl, null); | ||
| boolean isImage = extension != null && imageExtensions.contains(extension.toLowerCase()); | ||
|
|
||
| String thumbnailUrl = isImage ? generateThumbnailUrl(imageUrl) : null; | ||
|
|
||
| ChatAttachment attachment = new ChatAttachment(isImage, imageUrl, thumbnailUrl, null); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thumbnailUrl이 null인 경우에 대해 방어 로직이 존재하나요?? 이번 pr 상에서는 안보여서... (개인적으로 gif나 avif 이미지 파일 확장자도 잘 쓰인다고 생각해서 확인 한 번 해주시면 감사드리겠습니다!)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 제 생각에 해당 부분은 프론트에서 null일 경우 대체 이미지로 보여주는 게 맞는 것 같은데 어떻게 생각하시나요.......? |
||
| chatMessage.addAttachment(attachment); | ||
| } | ||
|
|
||
|
|
@@ -268,11 +273,9 @@ private String generateThumbnailUrl(String originalUrl) { | |
|
|
||
| String thumbnailFileName = nameWithoutExt + "_thumb" + extension; | ||
|
|
||
| String thumbnailUrl = originalUrl.replace("chat/images/", "chat/thumbnails/") | ||
| return originalUrl.replace("chat/files/", "chat/thumbnails/") | ||
| .replace(fileName, thumbnailFileName); | ||
|
|
||
| return thumbnailUrl; | ||
|
|
||
| } catch (Exception e) { | ||
| return originalUrl; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,19 +3,19 @@ | |
| import lombok.Getter; | ||
|
|
||
| @Getter | ||
| public enum ImgType { | ||
| public enum UploadPath { | ||
| PROFILE("profile"), | ||
| GPA("gpa"), | ||
| LANGUAGE_TEST("language"), | ||
| COMMUNITY("community"), | ||
| NEWS("news"), | ||
| CHAT("chat"), | ||
| CHAT("chat/files"), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이미
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. s3에 파일이 없는 걸 확인해서 바로 수정했습니다! |
||
| MENTOR_PROOF("mentor-proof"), | ||
| ; | ||
|
|
||
| private final String type; | ||
|
|
||
| ImgType(String type) { | ||
| UploadPath(String type) { | ||
| this.type = type; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,48 +3,48 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import static com.example.solidconnection.common.exception.ErrorCode.S3_CLIENT_EXCEPTION; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import static com.example.solidconnection.common.exception.ErrorCode.S3_SERVICE_EXCEPTION; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.amazonaws.AmazonServiceException; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.amazonaws.SdkClientException; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.amazonaws.services.s3.AmazonS3Client; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.amazonaws.services.s3.model.CannedAccessControlList; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.amazonaws.services.s3.model.ObjectMetadata; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.amazonaws.services.s3.model.PutObjectRequest; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.example.solidconnection.common.exception.CustomException; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.io.IOException; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import lombok.RequiredArgsConstructor; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import lombok.extern.slf4j.Slf4j; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.scheduling.annotation.Async; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.scheduling.annotation.EnableAsync; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 software.amazon.awssdk.services.s3.model.S3Exception; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Component | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @EnableAsync | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Slf4j | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @RequiredArgsConstructor | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public class FileUploadService { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final AmazonS3Client amazonS3; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public FileUploadService(AmazonS3Client amazonS3) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.amazonS3 = amazonS3; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final S3Client s3Client; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Async | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void uploadFile(String bucket, String fileName, MultipartFile multipartFile) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 메타데이터 생성 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String contentType = multipartFile.getContentType(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ObjectMetadata metadata = new ObjectMetadata(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| metadata.setContentType(contentType); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| metadata.setContentLength(multipartFile.getSize()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| amazonS3.putObject(new PutObjectRequest(bucket, fileName, multipartFile.getInputStream(), metadata) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .withCannedAcl(CannedAccessControlList.PublicRead)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.info("이미지 업로드 정상적 완료 thread: {}", Thread.currentThread().getName()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (AmazonServiceException e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.error("이미지 업로드 중 s3 서비스 예외 발생 : {}", e.getMessage()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PutObjectRequest putObjectRequest = PutObjectRequest.builder() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .bucket(bucket) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .key(fileName) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .contentType(multipartFile.getContentType()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .contentLength(multipartFile.getSize()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .build(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s3Client.putObject(putObjectRequest, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| RequestBody.fromInputStream(multipartFile.getInputStream(), multipartFile.getSize())); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
26
to
+38
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find . -type f -name "FileUploadService.java" | head -5Repository: solid-connection/solid-connect-server Length of output: 158 🏁 Script executed: cat -n ./src/main/java/com/example/solidconnection/s3/service/FileUploadService.javaRepository: solid-connection/solid-connect-server Length of output: 2581 🌐 Web query:
💡 Result: Cause
Fixes (pick one)
Minimal example (save to disk then async):
References
🏁 Script executed: # Check if there are any controllers or callers of this uploadFile method
rg "uploadFile" --type java -B 2 -A 2Repository: solid-connection/solid-connect-server Length of output: 28961 🏁 Script executed: cat -n ./src/main/java/com/example/solidconnection/s3/service/S3Service.javaRepository: solid-connection/solid-connect-server Length of output: 6579 🏁 Script executed: # Check if there are any existing mitigations like reading bytes before async
rg "multipartFile\.getBytes|multipartFile\.transferTo" --type java -A 3 -B 3Repository: solid-connection/solid-connect-server Length of output: 63 🏁 Script executed: # Check S3Service.uploadFile to confirm the direct call to async method
sed -n '55,71p' ./src/main/java/com/example/solidconnection/s3/service/S3Service.javaRepository: solid-connection/solid-connect-server Length of output: 999 @async 메서드에서 MultipartFile 수명 이슈 반드시 확인 필요 HTTP 요청이 종료되면 임시 파일이 정리되어 비동기 업로드가 실패하거나 빈 파일이 될 수 있습니다. 권장 해결 방법:
🛠️ 동기 처리로 전환하는 최소 변경 예시- `@Async`
public void uploadFile(String bucket, String fileName, MultipartFile multipartFile) {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.info("파일 업로드 정상 완료 thread: {}", Thread.currentThread().getName()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (S3Exception e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String errorMessage = (e.awsErrorDetails() != null) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? e.awsErrorDetails().errorMessage() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : e.getMessage(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.error("S3 서비스 예외 발생 : {}", errorMessage); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new CustomException(S3_SERVICE_EXCEPTION); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (SdkClientException | IOException e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.error("이미지 업로드 중 s3 클라이언트 예외 발생 : {}", e.getMessage()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (SdkException | IOException e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.error("S3 클라이언트 또는 IO 예외 발생 : {}", e.getMessage()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new CustomException(S3_CLIENT_EXCEPTION); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이건 상수로 빼는 건 어떤가요 ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
사용하는 게 여기 밖에 없어서 상수로 빼는 게 나을까요??