저번 포스팅에서는 Spring boot로 로컬에 파일 업로드를 구현하였다.
이번에는 앞에서 구현한 파일 업로드를 로컬이 아닌 AWS의 S3 저장소에 저장할 수 있도록 바꿔보려고 한다.
S3를 사용해야 되는 이유와 S3를 연동하는데 필요한 AWS 준비과정은 저번 포스팅에 모두 정리해두었다.
의존성 추가 및 yml 파일 설정
AWS S3를 이용하기 위해서는 build.gradle에 다음 코드를 추가해주자.
//aws s3
implementation('org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE')
AWS 관련 설정 정보는 yml 파일에 다음과 같이 작성해주자.
src/main/resources/application.yml
# AWS S3
cloud:
aws:
credentials:
accessKey: accessKey 기입
secretKey: secretKey 기입
s3:
bucket: 버킷 이름 기입
region:
static: ap-northeast-2
stack:
auto: false
AwsProperties 객체 만들기
설정한 yml 값을 주입받아 활용하는 방법에는 여러 가지가 있다.
가장 간단하게는 @Value를 사용한 placeholder 방식, SpEL 방식이 있다.
하지만 이러한 @Value 방식에는 불변이 아니고, 타입 안정성이 보장되지 않는 문제가 있다.
다른 방법인 @ConfigurationProperties를 활용하는 방법도 불변이 아니며, Setter가 공개되어 있어 중간에 값이 변경될 위험성이 있다는 단점이 있다.
하지만 여기에 @ConstructorBinding를 함께 활용한다면, final 필드에 대해 값을 주입해주기 때문에 불변성을 보장하고, Setter 없이도 값을 매칭 할 수 있다.
따라서 이번 구현에서는 @ConfigurationProperties와 @ConstructorBinding를 함께 활용하여 AWS yml 설정 값을 AwsProperties 객체에 주입하고자 한다.
전체 코드는 다음과 같다.
@Getter
@ConstructorBinding
@RequiredArgsConstructor
@ConfigurationProperties(prefix = "cloud.aws")
public class AwsProperties {
private final Credentials credentials = new Credentials();
private final S3 s3 = new S3();
private final Region region = new Region();
@Getter
@Setter
public static class Credentials {
private String accessKey;
private String secretKey;
}
@Getter
@Setter
public static class S3 {
private String bucket;
}
@Getter
@Setter
public static class Region {
private String statics;
}
public String getAccessKey() {
return credentials.getAccessKey();
}
public String getSecretKey() {
return credentials.getSecretKey();
}
public String getBucket() {
return s3.getBucket();
}
public String getRegionStatic() {
return region.getStatics();
}
}
여기서 3가지 주의할 점이 있다.
가장 먼저 @ConfigurationProperties를 활용하기 위해서는 다음 의존성을 build.gradle에 추가해줘야 한다.
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
다음으로 현재 이 방식을 사용하면 Properties 클래스에 직접적으로 @Configuration을 이용해서 Spring Bean을 만들어주지 않기 때문에, 아래와 같이 PropertiesConfiguration 클래스에 @EnableConfigurationProperties을 이용해서 생성할 Properties 클래스의 클래스 타입을 명시해줘야 한다.
@EnableConfigurationProperties(value = {AwsProperties.class})
public class WordiApplication {
//.. 생략 ..
}
또 다른 하나는 AwsProperties에 Region Inner class에서 static이 Java 내부의 예약어이기 때문에 변수명을 statics으로 해줘야한다.
따라서, yml 파일 설정 부분에서도 이를 고려하여 다음과 같이 statics를 추가로 기입해주었다.
# AWS S3
cloud:
aws:
credentials:
accessKey: accessKey 기입
secretKey: secretKey 기입
s3:
bucket: 버킷 이름 기입
region:
static: ap-northeast-2
statics: ap-northeast-2
stack:
auto: false
AwsS3Service
이제 본격적으로 AWS S3에 저장하는 비즈니스 로직을 AwsS3Service class를 만들어 구현해보자.
그전에, 확장성을 위해 저장 기능과 관련된 인터페이스를 만들고, 해당 인터페이스를 상속하는 구현체로 AwsS3Service를 만드는 방향으로 구현해주자.
인터페이스 코드는 다음과 같다.
public interface StorageService {
String upload(MultipartFile file, String destLocation);
}
이후, 생성한 인터페이스를 상속하여 AwsS3Service를 구현해보자.
AwsS3Service 코드는 다음과 같다.
@RequiredArgsConstructor
@Service
public class AwsS3Service implements StorageService{
private final AwsProperties awsProperties;
private AmazonS3 s3Client;
@PostConstruct
private void setS3Client() {
AWSCredentials credentials = new BasicAWSCredentials(awsProperties.getAccessKey(),
awsProperties.getSecretKey());
s3Client = AmazonS3ClientBuilder.standard()
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.withRegion(awsProperties.getRegionStatic())
.build();
}
public String uploadBucket(MultipartFile file) {
return upload(file, awsProperties.getBucket());
}
public String upload(MultipartFile file, String bucket) {
String fileName = file.getOriginalFilename();
String convertedFileName = FileNameUtils.fileNameConvert(fileName);
try {
String mimeType = new Tika().detect(file.getInputStream());
ObjectMetadata metadata = new ObjectMetadata();
FileNameUtils.checkMimeType(mimeType);
metadata.setContentType(mimeType);
s3Client.putObject(
new PutObjectRequest(bucket, convertedFileName, file.getInputStream(), metadata)
.withCannedAcl(CannedAccessControlList.PublicRead));
} catch (IOException exception) {
throw new FileRoadFailedException("파일 저장에 실패하였습니다.");
}
return s3Client.getUrl(bucket, convertedFileName).toString();
}
}
가장 먼저 uploadBucket() 메서드 부분을 보자.
awsProperties 객체에 있는 bucket 정보를 파일과 함께 매개변수에 담아 실제 업로드 로직이 있는 upload() 메서드를 호출 및 return 하고 있다.
이처럼 upload() 메서드를 호출하기 전에 bucket 정보를 담는 메서드를 만들어 파일에 따라 버킷 저장소를 선택하여 저장할 수 있게 구현할 수 있다.(현재 예제에서는 버킷을 1개만 생성하였으므로 버킷 1개를 기준으로 작성된 코드이다.)
다음으로 upload() 메서드에 있는 Tika를 보자.
아파치 티카는 csv, pdf 등 다양한 형태의 파일 메타 데이터와 텍스트를 감지하고 추출해주는 라이브러리이다.
해당 라이브러리를 통해 입력받은 파일의 미디어 타입을 추출하여 올바른 파일 형식인지 판단하는 checkMimeType()을 호출해주고, 보낼 metadata에 추출한 미디어 타입을 set 해주자.
metadata에 미디어 타입을 설정하지 않으면, S3 저장소에 파일이 저장될 때 기본 Content-Type인 application/octet-stream으로 저장이 된다.
그렇게 되면 저장 경로를 접근했을 때, 웹에 파일이 출력되는 것이 아닌 파일이 곧바로 다운로드가 돼버린다.
기존 Service 수정
다음으로 로컬에 저장하도록 구현된 기존 Service 로직을 수정해보았다.
수정된 Service 코드는 다음과 같다.
(파일 업로드와 관련된 부분은 프로필 이미지 파일 저장, 멘토 증명서 파일 저장 부분이다.)
@Transactional
public void createMentor(Long userId, CreateRequest createRequest, @Nullable MultipartFile profileImage, MultipartFile certification) {
User user = userRepository.findByIdAndStatus(userId, ACTIVE)
.orElseThrow(() -> new NoExistUserException("접속한 회원 정보와 일치하는 회원 정보가 없습니다."));
if(mentorRepository.countByUser(user)>=1) {
throw new ExistMentorException("이미 가입하신 멘토 정보가 있습니다.");
}
// 프로필 이미지 파일 저장
if (!profileImage.isEmpty()) {
String s3FilePath = awsS3Service.uploadBucket(profileImage);
createRequest.updateImageUrl(s3FilePath);
}
// 멘토 증명서 파일 저장
if (!certification.isEmpty()) {
String s3FilePath = awsS3Service.uploadBucket(certification);
createRequest.updateCertificationUrl(certification.getOriginalFilename(), s3FilePath);
}
// 멘토 정보 저장
Mentor mentor = mentorRepository.save(createRequest.toEntity(user));
// 멘토 키워드 저장
createRequest.getKeywordList().stream()
.forEach(k -> mentorKeywordRepository.save(new MentorKeyword(mentor, k)));
// 멘토 일정 저장
createRequest.getScheduleList().stream()
.forEach(s -> mentorScheduleRepository.save(s.toMentorSchedule(mentor)));
}
확실히 AwsS3Service 로직을 따로 구현하고 있어서 그런지 기존 코드에 비해 코드량이 줄어든 것을 볼 수 있다.
업로드 테스트
구현된 코드가 정상적으로 동작하는지 Postman으로 테스트해보자.
다음과 같이 워디 멘토 등록 데이터와 파일 데이터 2개를 보냈다.
(해당 Request 데이터에 대한 자세한 설명 및 Postman 설정은 이전 파일 업로드 포스팅을 참고하자)
다음과 같이 생성한 S3에 정상적으로 파일이 저장된 것을 볼 수 있다.
참고
https://bottom-to-top.tistory.com/46
https://devlog-wjdrbs96.tistory.com/323
https://victorydntmd.tistory.com/334
https://github.com/f-lab-edu/shoe-auction
'👨💻 개발' 카테고리의 다른 글
SpringBoot에 CloudFront 적용 (0) | 2021.12.07 |
---|---|
AWS CloudFront 배포하기 (0) | 2021.12.07 |
AWS S3 저장소 구축하기 (0) | 2021.12.05 |
SpringBoot 파일 업로드 구현 (0) | 2021.12.03 |
QueryDSL을 활용한 동적 쿼리 구현 (0) | 2021.12.02 |