SpringBoot + S3 연동하여 이미지 올리기
S3 권한 액세스 부여 또는 S3 버킷 생성은 생략하겠다. (검색해 보면 많은 자료 존재)
물론 순서가 바뀌어도 상관없다.
- Amazon S3 버킷 생성 및 권한 생성
- IAM 역할 지정
- 액세스 키 및 시크릿 키 발급하여 로컬에 저장
- 스프링 부트 S3 라이브러리 추가 및 설정
- Controller 구현
- Service 구현
라이브러리 추가
build.gradle
// aws s3
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.364'
// 현재는 s3만 사용할 것이므로 s3 의존성만 추가
implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-aws',version:'2.2.6.RELEASE'
처음엔 spring-cloud-starter-aws 를 추가하였지만 S3만 사용할 것이기 때문에 라이브러리를 변경해 주었다.
YAML 파일 설정
cloud:
aws:
s3:
bucket: ${S3 버킷 이름}
credentials:
accessKey: ${아마존 S3 액세스 키}
secretKey: ${아마존 S3 시크릿트 키}
region:
static: ap-northeast-2
- bucket : S3에 생성한 버킷 이름이다.
- credentials.accessKey : S3 권한 부여받을 때 받은 액세스 키
- credentials.secretKey : S3 권한 부여받을 때 받은 시크릿 키
- region.static : 지역 설정 (ap-northeast-2 == 서울)
Config 설정
@Configuration
public class AmazonS3Config {
@Value("${cloud.aws.credentials.accessKey}")
private String accessKey;
@Value("${cloud.aws.credentials.secretKey}")
private String secretKey;
@Value("${cloud.aws.region.static}")
private String region;
@Bean
public AmazonS3 amazonS3() {
AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
AmazonS3 s3Builder = AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.build();
return s3Builder;
}
}
YAML 파일에서 설정한 값을 가져와 액세스 키 , 시크릿 키, 지역을 설정해 주고 빈으로 등록해 준다.
여기까지 설정해 준다면 S3 관련 설정은 끝났다. 먼저 값을 받기 위해 컨트롤러를 구현해 주자.
이미지 받아올 컨트롤러 구현
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/image")
public class ImageController {
private static final String PROFILE = "profile";
private final SocialUserService socialUserService;
@PostMapping()
public ApiResult<Boolean> uploadSocialUserProfile(
@RequestPart("data") MultipartFile multipartFile,
@CurrentUser SocialUser socialUser
) throws IOException {
return ApiUtils.success(socialUserService.uploadSocialUserImage(socialUser, multipartFile, PROFILE));
}
}
SocialUserService 객체를 DI 한 이유는 내가 진행하고 있는 프로젝트에서는 S3에 파일 저장 후 파일경로를 유저 테이블에 profileImageUrl 칼럼에 저장하기 위해서다.
@RequestPart("data") MultipartFile multipartFile 이 파일을 가져오는 놈이다.
즉, 프론트 단에서 content-type을 form/data로 설정 → 파일을 선택 → @RequestPart Key 값 (현재는 ‘data’)을 읽어 매핑해준다. 또한 조금 더 찾아보니 단일 파일이 아니라 List(컬렉션) 으로 가져올 수 있다.
나중에 한번 더 정리해 보겠지만 파일을 받아올 수 있는 방법은 몇 가지 방식이 존재하므로 한번 더 정리가 필요할 것 같다.
S3 서비스 구현
@Slf4j
@Service
@RequiredArgsConstructor
public class AmazonS3UploadService {
private final AmazonS3 amazonS3;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
public String upload(MultipartFile multipartFile, String dirName) throws IOException {
String fileName = createFileName(dirName, multipartFile.getOriginalFilename());
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(multipartFile.getSize());
objectMetadata.setContentType(multipartFile.getContentType());
try (InputStream inputStream = multipartFile.getInputStream()) {
// upload multi part file
amazonS3.putObject(new PutObjectRequest(bucket, fileName, inputStream, objectMetadata)
.withCannedAcl(CannedAccessControlList.PublicRead));
} catch (AmazonS3Exception e) {
e.printStackTrace();
} catch (SdkClientException e) {
e.printStackTrace();
}
return amazonS3.getUrl(bucket, fileName).toString();
}
private String createFileName(String dirName, String originalFileName) {
StringBuilder sb = new StringBuilder();
return sb.append(dirName).append("/").append(UUID.randomUUID()).append(getFileExtension(originalFileName)).toString();
}
private String getFileExtension(String originalFileName) {
try {
return originalFileName.substring(originalFileName.lastIndexOf("."));
} catch (StringIndexOutOfBoundsException e) {
throw new RuntimeException(String.format("잘못된 형식의 파일($s) 입니다", originalFileName));
}
}
}
이제 실질적으로 일하는 놈을 구현해 보자. 먼저 메서드부터 해석해 보자면
| 메소드명 | 설명 |
| upload(MultipartFile multipartFile, String dirName) | 이미지 업로드 후 S3로 올린 파일 경로 URL을 리턴 받는다. |
| getFileExtension(String originalFileName) | subString() 함수를 활용하여 맨 뒤에 확장자를 삭제해준다. ex) image.png → image |
| createFileName(String dirName, String originalFileName) | S3 내부에 버킷 안에 어떤 폴더에 파일을 저장할지 dirName으로 지정해 준다 그리고 파일 원본 이름과 UUID.ramdom으로 파일 이름 중복을 막아준다. ex) images/aaskndlksdnoamoriginalfile (’/’로 경로를 구분 짓는다.) |
ObjectMetadata 객체를 사용하는 이유?

먼저 PutObjctRequest 객체에 파리미터를 확인해 보면 인자값이 다다르다.
만약 PutObjctRequest(String bucketName, String key, File file) 방식으로 보내게 된다면 실행 흐름은
- 파일 쓰기 작업이 일어난다.
- S3에 업로드한다.
- 로컬에도 해당 파일을 저장한다.
파일 쓰기 작업은 무거운 작업이다. 그리고 굳이 S3에 업로드할 건데 로컬에 저장할 필요 있을까? 성능 저하 원인이 될 수도 있다.
그렇기 때문에 그림에 3번째 ObjectMetadata 인자를 받는 방식으로 S3에 요청할 것이다.
코드를 해석해 보자면
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(multipartFile.getSize());
objectMetadata.setContentType(multipartFile.getContentType());
try (InputStream inputStream = multipartFile.getInputStream()) {
// upload multi part file
amazonS3.putObject(new PutObjectRequest(bucket, fileName, inputStream, objectMetadata).withCannedAcl(CannedAccessControlList.PublicRead));
} catch (AmazonS3Exception e) {
e.printStackTrace();
} catch (SdkClientException e) {
e.printStackTrace();
}
inputStream 은 Byte만이 전달되기 때문에 해당 파일에 대한 정보가 없다. 따라서 ObjectMetadata 객체를 통하여 파일에 정보를 저장해 주고 보내준다고 생각하면 된다.
참고자료
'SpringBoot' 카테고리의 다른 글
| OpenFeign 응답값 받아오는 방법 (0) | 2023.04.21 |
|---|---|
| @EventListener을 통해 객체 간 결합도 낮추기 (0) | 2023.04.04 |
| 스프링 예외 처리 Guide (0) | 2023.04.04 |
| SpringBoot + STOMP 웹소켓 고도화 (0) | 2023.04.03 |
| SpringBoot + STOMP 웹소켓 구현 (0) | 2023.04.03 |