Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,9 @@ dependencies {
testImplementation 'org.awaitility:awaitility:4.2.0'

// Etc
implementation platform('software.amazon.awssdk:bom:2.41.4')
implementation 'software.amazon.awssdk:s3'
implementation 'org.hibernate.validator:hibernate-validator'
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.782'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package com.example.solidconnection.s3.config;

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;

@Configuration
public class AmazonS3Config {
Expand All @@ -21,12 +21,12 @@ public class AmazonS3Config {
private String region;

@Bean
public AmazonS3Client amazonS3Client() {
BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder
.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(credentials))
public S3Client s3Client() {
AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKey, secretKey);

return S3Client.builder()
.region(Region.of(region))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@
import lombok.Getter;

@Getter
public enum ImgType {
public enum UploadType {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저에게는 UploadType이 직관적으로는 업로드한 파일이 어떤 타입인지, 즉 파일 확장자를 구별하는 걸로 느껴집니다..!
해당 enum의 목적이 어떤 기능(도메인)에서 사용되는 기능인지를 구분하고, 저장하는 파일의 basePath를 결정지어야 하는 책임이 있다고 생각해서 다른 네이밍이 좋을 것 같다는 조심스런 의견을 드립니다 ㅎㅎ

(추신 : 제미나이가 추천한 이름은 UploadPath, UploadCategory였습니다..!)

PROFILE("profile"),
GPA("gpa"),
LANGUAGE_TEST("language"),
COMMUNITY("community"),
NEWS("news"),
CHAT("chat"),
CHAT("chat/files"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이미 chat 을 통해 업로드된 파일이 있나요 ? 그렇다면 관련해서 데이터 마이그레이션이 필요한 것 같습니다 !

MENTOR_PROOF("mentor-proof"),
;

private final String type;

ImgType(String type) {
UploadType(String type) {
this.type = type;
}
}
Original file line number Diff line number Diff line change
@@ -1,51 +1,49 @@
package com.example.solidconnection.s3.service;
package com.example.solidconnection.s3.service;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FileUploadService가 전부 한 칸씩 들여쓰기 되었어요 ㅠㅠ


import static com.example.solidconnection.common.exception.ErrorCode.S3_CLIENT_EXCEPTION;
import static com.example.solidconnection.common.exception.ErrorCode.S3_SERVICE_EXCEPTION;
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.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 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.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.ObjectCannedACL;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.model.S3Exception;

@Component
@EnableAsync
@Slf4j
public class FileUploadService {
@Component
@Slf4j
@RequiredArgsConstructor
public class FileUploadService {

private final AmazonS3Client amazonS3;
private final S3Client s3Client;

public FileUploadService(AmazonS3Client amazonS3) {
this.amazonS3 = amazonS3;
}
@Async
public void uploadFile(String bucket, String fileName, MultipartFile multipartFile) {
Comment on lines +27 to +28
Copy link

@coderabbitai coderabbitai bot Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# `@EnableAsync` 설정 확인
rg -n "@EnableAsync" --type java

Repository: solid-connection/solid-connect-server

Length of output: 287


🏁 Script executed:

#!/bin/bash
# 두 파일의 내용 확인 - `@EnableAsync` 주변 컨텍스트
echo "=== UpdateViewCountScheduler.java ===" 
sed -n '1,30p' src/main/java/com/example/solidconnection/scheduler/UpdateViewCountScheduler.java

echo -e "\n=== UpdateViewCountService.java ===" 
sed -n '1,30p' src/main/java/com/example/solidconnection/community/post/service/UpdateViewCountService.java

Repository: solid-connection/solid-connect-server

Length of output: 2450


@EnableAsync는 선언되어 있지만, 설정 위치를 개선하세요.

다음과 같은 사항을 확인했습니다:

  1. 현재 상태

    • @EnableAsync가 코드베이스에 존재하므로 uploadFile 메서드의 @Async 애노테이션은 비동기로 작동합니다.
  2. 개선 필요 사항

    • @EnableAsyncUpdateViewCountScheduler (@Component) 와 UpdateViewCountService (@Service) 등 여러 클래스에 분산되어 있습니다.
    • 모범 사례는 하나의 전용 @Configuration 클래스에만 @EnableAsync를 선언하는 것입니다.
  3. 권장 조치

    • 중앙 집중식 설정 클래스를 만들고 그곳에 @EnableAsync를 옮기세요. (예: AsyncConfig.java)
    • 불필요한 중복 선언을 제거하세요.
🤖 Prompt for AI Agents
In `@src/main/java/com/example/solidconnection/s3/service/FileUploadService.java`
around lines 27 - 28, You currently have `@EnableAsync` scattered across
components (e.g., UpdateViewCountScheduler, UpdateViewCountService) while
methods like uploadFile(String bucket, String fileName, MultipartFile
multipartFile) rely on `@Async`; create a single `@Configuration` class (e.g.,
AsyncConfig) and place `@EnableAsync` there, remove all other `@EnableAsync`
annotations from components/services, ensure Spring picks up the config via
component scanning or `@Import` so `@Async` on uploadFile continues to work.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 나중에 한 번에 바꿔야 할 것 같네

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lsy1307, 알겠습니다! @EnableAsync 설정 통합은 나중에 일괄적으로 정리하는 것이 좋겠네요.

필요하시다면 추적을 위한 GitHub 이슈를 생성해 드릴까요? 이슈에 현재 상황과 개선 방향을 정리해 두면 나중에 작업하실 때 도움이 될 것 같습니다.

try {
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucket)
.key(fileName)
.contentType(multipartFile.getContentType())
.contentLength(multipartFile.getSize())
.build();

@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());
s3Client.putObject(putObjectRequest,
RequestBody.fromInputStream(multipartFile.getInputStream(), 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());
throw new CustomException(S3_SERVICE_EXCEPTION);
} catch (SdkClientException | IOException e) {
log.error("이미지 업로드 중 s3 클라이언트 예외 발생 : {}", e.getMessage());
throw new CustomException(S3_CLIENT_EXCEPTION);
log.info("파일 업로드 정상 완료 thread: {}", Thread.currentThread().getName());
} catch (S3Exception e) {
log.error("S3 서비스 예외 발생 : {}", e.awsErrorDetails().errorMessage());
throw new CustomException(S3_SERVICE_EXCEPTION);
} catch (SdkException | IOException e) {
log.error("S3 클라이언트 또는 IO 예외 발생 : {}", e.getMessage());
throw new CustomException(S3_CLIENT_EXCEPTION);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@
import static com.example.solidconnection.common.exception.ErrorCode.S3_SERVICE_EXCEPTION;
import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND;

import com.amazonaws.AmazonServiceException;
import com.amazonaws.SdkClientException;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.DeleteObjectRequest;
import com.example.solidconnection.common.exception.CustomException;
import com.example.solidconnection.s3.domain.ImgType;
import com.example.solidconnection.s3.dto.UploadedFileUrlResponse;
Expand All @@ -23,21 +19,22 @@
import java.util.Objects;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.core.exception.SdkException;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
import software.amazon.awssdk.services.s3.model.S3Exception;

@Service
@RequiredArgsConstructor
public class S3Service {

private static final Logger log = LoggerFactory.getLogger(S3Service.class);
private static final long MAX_FILE_SIZE_MB = 1024 * 1024 * 5;

private final AmazonS3Client amazonS3;
private final S3Client s3Client;
private final SiteUserRepository siteUserRepository;
private final FileUploadService fileUploadService;
private final ThreadPoolTaskExecutor asyncExecutor;
Expand All @@ -56,22 +53,23 @@ public class S3Service {
* - 5mb 미만의 파일은 바로 업로드한다.
* */
public UploadedFileUrlResponse uploadFile(MultipartFile multipartFile, ImgType imageFile) {
// 파일 검증
validateImgFile(multipartFile);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 PR의 작업 내용은 아니지만, validateImgFile가 이미지만 검증하는 것이 아닌 거 같습니다. validateFile 등과 같이 변경하는 게 좋을 거 같네요 !

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여유 되시면 이 내용도 적용 부탁드립니다

// 파일 이름 생성
UUID randomUUID = UUID.randomUUID();
String fileName = imageFile.getType() + "/" + randomUUID;
// 파일업로드 비동기로 진행
if (multipartFile.getSize() >= MAX_FILE_SIZE_MB) {
asyncExecutor.submit(() -> {
fileUploadService.uploadFile(bucket, "origin/" + fileName, multipartFile);
});
} else {
asyncExecutor.submit(() -> {
fileUploadService.uploadFile(bucket, fileName, multipartFile);
});
}
return new UploadedFileUrlResponse(fileName);
String extension = getFileExtension(Objects.requireNonNull(multipartFile.getOriginalFilename()));
String baseFileName = randomUUID + "." + extension;
String fileName = imageFile.getType() + "/" + baseFileName;
final boolean isLargeFile = multipartFile.getSize() >= MAX_FILE_SIZE_MB && imageFile != ImgType.CHAT;

final String uploadPath = isLargeFile ? "original/" + fileName : fileName;
final String returnPath = isLargeFile
? "resize/" + fileName.substring(0, fileName.lastIndexOf('.')) + ".webp"
: fileName;

asyncExecutor.submit(() -> {
fileUploadService.uploadFile(bucket, uploadPath, multipartFile);
});

return new UploadedFileUrlResponse(returnPath);
}

public List<UploadedFileUrlResponse> uploadFiles(List<MultipartFile> multipartFile, ImgType imageFile) {
Expand Down Expand Up @@ -125,12 +123,14 @@ public void deletePostImage(String url) {

private void deleteFile(String fileName) {
try {
amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName));
} catch (AmazonServiceException e) {
log.error("파일 삭제 중 s3 서비스 예외 발생 : {}", e.getMessage());
DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder()
.bucket(bucket)
.key(fileName)
.build();
s3Client.deleteObject(deleteObjectRequest);
} catch (S3Exception e) {
throw new CustomException(S3_SERVICE_EXCEPTION);
} catch (SdkClientException e) {
log.error("파일 삭제 중 s3 클라이언트 예외 발생 : {}", e.getMessage());
} catch (SdkException e) {
throw new CustomException(S3_CLIENT_EXCEPTION);
}
}
Expand Down