요새 토이 프로젝트 하나씩 올리는게 나름 재밌어져 간다. 내가 만들고 싶었던걸 만드는 기쁨이란 이런것일까..? 이게 바로 개발자의 삶인가..? 나름 잘 맞는 것 같다. 아무쪼록 이번엔 파일 업로드에 대해 다루어 보겠다. 기본적인 요구사항은 다음과 같다.
0. multipart/form-data 또는 multipart 헤더를 이용하여 파일 송수신을 한다.
1. 이미지 자체는 aws의 S3에 저장한다.
2. file 이름이 중복되면 안되므로 file 마다 고유의 무작위 이름이 정해진다.
3. aws에는 S3 bucket의 /test2 디렉터리 밑에 저장하고 데이터 베이스에는 파일 고유의 무작위 이름을 저장한다.
4. 이미지 조회를 요청하면 file 위치로 redirect 한다.
먼저 aws에 이미지를 업로드 하는 법을 보도록 하자!
1. Configuration
스프링은 amazon 클래스들을 제공해 주어서 정말 쉽게 aws 서비스를 굉장히 편리하게 이용할 수 있다. 먼저 build.gradle에 아래의 dependency를 추가해야 한다.
//amazon-s3
compileOnly 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
이후 AWS 서비스의 어떤 bucket에 접근할 건지 명시해 주어야 하는데 설정 정보 국룰은 application.properties에 적어두고 @Value로 끌어다 쓰는 것이니 아래와 같이 정보를 저장해 두었다.
cloud.aws.region.static=ap-northeast-2 #서비스 지역
cloud.aws.stack.auto=false
cloud.aws.s3.accessKey={#bucket 생성 시 고유 식별 번호}
cloud.aws.s3.secretKey={#bucket 생성 시 할당 받는 비밀번호}
cloud.aws.s3.region=ap-northeast-2 #bucekt 지역
cloud.aws.s3.bucket=#{bucket 이름 + 저장 하고 픈 파일 위치}
아무튼 이러한 설정을 하는 이유는 AmazonS3Client 빈을 생성하기 위해서이다.
@Configuration
@Slf4j
public class AWSConfig {
@Value("${cloud.aws.s3.accessKey}")
private String iamAccessKey;
@Value("${cloud.aws.s3.secretKey}")
private String iamSecretKey;
@Value("${cloud.aws.s3.region}")
private String region;
@Bean
public AmazonS3Client amazonS3Client(){
BasicAWSCredentials basicAWSCredentials = new BasicAWSCredentials(iamAccessKey, iamSecretKey);
return (AmazonS3Client) AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(basicAWSCredentials))
.build();
}
}
위와 같이 필요한 설정 정보들을 기반으로 AmazonS3Client를 생성하면 된다. 이 빈으로 우리는 이미지 업로드 조회가 가능하다!
2. S3에 저장 및 조회
이제 이미지에 저장하는 FileUploadService를 보자.
@Service
@RequiredArgsConstructor
public class FileUploadService {
private final AmazonS3Client amazonS3Client;
@Value("${cloud.aws.s3.bucket}")
private String S3Bucket;
public void uploadCloud(MultipartFile multipartFile, String storeFileName){
long size = multipartFile.getSize();
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(size);
objectMetadata.setContentType(multipartFile.getContentType());
putObjectInAWS(multipartFile, storeFileName, objectMetadata);
}
private void putObjectInAWS(MultipartFile multipartFile, String storeFileName, ObjectMetadata objectMetadata) {
try {
amazonS3Client.putObject(
new PutObjectRequest(S3Bucket, storeFileName,
multipartFile.getInputStream(), objectMetadata)
.withCannedAcl(CannedAccessControlList.PublicRead)
);
}catch(IOException e){
throw new IllegalStateException();
}
}
public String getFileUrl(String storeFileName){
return amazonS3Client.getUrl(S3Bucket, storeFileName).toString();
}
}
코드를 솔직히 꽤나 직관적으로 구현한 거 같아서 설명을 해야할까 싶지만,, 좀 해보자면 위와 같이 bucket 정보와 AmazonS3Client가 필요하다. 이후 uploadCloud에 보면 ObjectMetadata로 파일의 기본 파일의 정보를 만들고 해당 파일 고유의 저장 이름을 putObjectInAws에 전달한다. IOException은 체크 예외라서 일단 언체크 예외로 바꾸어 던졌다. 토이 프로젝트니깐 하핫.
이미지의 url을 갖고 싶다면 amazonS3Client에 저장된 이름으로 요청하면 된다. 이러면 끝!
3. 서비스 제공
controller 코드를 보아야 하는데 왜냐하면 받는 타입은 MultipartFile이라는 타입으로 받는다. 아래와 같이.
@Controller
@RequestMapping("/upload")
@RequiredArgsConstructor
public class UploadController {
private final FileStoreService fileStoreService;
@ResponseBody
@PostMapping
public Long uploadImage(@RequestBody MultipartFile bodyImage){
return fileStoreService.saveItem(bodyImage, bodyImage.getOriginalFilename());
}
@GetMapping("/show")
public String getFile(@RequestParam Long itemId){
return "redirect:"+fileStoreService.getFileUrl(itemId);
}
}
받는 것의 경우 굳이 Request Body 부분 뿐만 아니라 @RequestParam으로 보내기도 한다. 이제 FIleStoreService의 두 메서드를 살펴 보아야 할 것 같다.
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class FileStoreService {
private final ItemRepository itemRepository;
private final FileUploadService fileUploadService;
public Long saveItem(MultipartFile multipartFile, String uploadFileName){
String storeFileName = createStoreFileName(uploadFileName);
Item item = Item.createItem(uploadFileName, storeFileName);
fileUploadService.uploadCloud(multipartFile, storeFileName);
return itemRepository.save(item).getId();
}
private String createStoreFileName(String uploadFileName) {
String ext = extractExt(uploadFileName);
String uuId = UUID.randomUUID().toString();
log.info("url={}.{}",uuId,ext);
return uuId+"."+ext;
}
private String extractExt(String uploadFileName) {
int pos = uploadFileName.lastIndexOf('.');
return uploadFileName.substring(pos+1);
}
public String getFileUrl(Long itemId){
UploadFile fileInfo = getFileInfo(itemId);
return fileUploadService.getFileUrl(fileInfo.getStoreFileName());
}
public UploadFile getFileInfo(Long itemId){
return itemRepository.findById(itemId).get().getAttachFile();
}
}
FileStore Service에서는 FileUploadService를 이용하기만 하면 되고 주 역할은 확장자 이름은 가져다 쓰고 file 고유의 이름을 만들어서 file마다 <file Id, 사용자가 지정한 파일 이름, file의 고유 이름> 쌍을 DB에 저장하는 로직이 전부이다.
또한 결과적으로 test.img 파일이 S3에는 2awek2aw9r@1!@!#.img와 같이 저장될 것이다.
4. 콰긴!
postman을 이용해서 파일을 저장하면 DB와 S3가 어떤 모습인지 필요할 것인데 아래와 같다.
이제 localhost/upload/show?itemId=1 을 진행시키면 아래와 같은 사진이 나온다.
파일이 있는 위치로 redirect를 한것일 뿐이다. 주소는 아래와 같다. 아마 내가 aws free tier가 끝나면 해당 링크로 가도 위 사진은 볼 수 없을 것이얏!
깃허브 링크: https://github.com/peterysh/imageUploadAWSS3
peterysh/imageUploadAWSS3
Contribute to peterysh/imageUploadAWSS3 development by creating an account on GitHub.
github.com
'백앤드(스프링)' 카테고리의 다른 글
제너릭과 DTO, connection pool을 활용하여 Spring RestTemplate 이용하기 (0) | 2022.12.22 |
---|---|
스프링 포인트 거래 시스템 (1) | 2022.12.21 |
스프링(TDD) 테스트 코드 작성 (0) | 2022.11.07 |
AOP 2편[내부 호출과 프록시 기술의 한계, 마무리] (0) | 2022.10.30 |
AOP 2편 [포인트 컷 분리, 어드바이스 활용, 포인트컷 지시자] (0) | 2022.10.30 |