💡 프로젝트 환경
- spring boot 3.2.2
- spring-cloud-starter-aws:2.2.6.RELEASE
1-1. AWS S3 버킷 만들기
1) aws 콘솔에서 회원가입 후 S3 검색해 버킷을 만듦
https://aws.amazon.com/ko/console/
실무에서는 보통 액세스 차단을 한다고 함, 테스트를 위해서 퍼블릭 상태로 둠
2) 버킷 정책 생성
권한 - 버킷 정책 편집 - 정책 생성기 클릭
Actions에는 GetObject 선택해주면 됨
현재 이미 정책을 생성해놓아서 유효하지 않은 ARN으로 보이긴 하지만, 실제로는 버튼이 활성화될 것이다.
버튼을 클릭하면 json 형태로 생성된 정책을 확인할 수 있으며, 이를 정책편집기에 붙여넣기 하고 변경사항을 저장해주면 된다.
1-2. 사용자 생성
root 사용자로 이용해도 되지만 IAM 사용자를 생성해서 이용하도록 한다.
오른쪽 상단 계정정보에서 보안자격 증명 - 액세스 관리 - 사용자로 이동하여 사용자 생성 버튼 클릭
권한 설정 시 기존 정책 직접 연결 - 정책 필터: AmazonS3FullAccess 체크
생성 후 액세스 키 ID와 비밀 액세스 키가 발급 되는데 스프링 부트 연동 시 필요하므로 반드시 따로 기록하여 관리해야 한다.
2. 스프링부트에 AWS 설정 세팅
1) application.yml
연결을 위해 yml 파일에 버킷과 사용자 정보를 넣어줘야 한다.
반드시 gitignore을 이용해서 외부에 노출되지 않도록 조심 또 조심!!
accessKey와 secretKey는 IAM 사용자 생성 시 발급받은 키로 넣어주면 된다.
cloud:
aws:
stack:
auto: false
credentials:
access-key: {accessKey}
secret-key: {secretKey}
region:
static: ap-northeast-2
s3:
bucket: {버킷 이름}
2) config
@Configuration
public class S3Config {
@Value("${cloud.aws.credentials.access-key}")
private String accessKey;
@Value("${cloud.aws.credentials.secret-key}")
private String secretKey;
@Value("${cloud.aws.region.static}")
private String region;
@Bean
public AmazonS3Client amazonS3Client() {
BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCreds))
.build();
}
}
3) controller -> service로 변경(필요한 controller에서 꺼내쓸 수 있게)
처음에 controller로 api를 만들어 테스트 해보았더니 잘 되었다!
나는 버킷에서 폴더를 또 분리했기 때문에 url에 폴더명까지 붙여줬고,
같은 파일명이 s3에 올라가게 되면 중복으로 인식해 하나만 남기 때문에 중복 방지를 위해 uuid를 생성하여 파일명에 같이 붙여주었다.
@RestController
@RequiredArgsConstructor
public class FileController {
private final AmazonS3Client amazonS3Client;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
@PostMapping("/upload")
public List<String> uploadFiles(@RequestPart("files") List<MultipartFile> files) {
List<String> uploadedFileUrls = new ArrayList<>();
try {
for (MultipartFile file : files) {
String fileName = file.getOriginalFilename();
String uuid = UUID.randomUUID().toString();
String fileUrl= "https://" + bucket + ".s3.ap-northeast-2.amazonaws.com/post/" + uuid + "_" + fileName;
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(file.getContentType());
metadata.setContentLength(file.getSize());
amazonS3Client.putObject(bucket, "post/"+ uuid + "_" + fileName, file.getInputStream(), metadata);
uploadedFileUrls.add(fileUrl);
}
return uploadedFileUrls;
} catch (IOException e) {
throw new HttpException(false, "이미지 등록에 실패하였습니다.", HttpStatus.BAD_REQUEST);
}
}
}
하지만 프로필 이미지, 포스트에 쓸거라 service단으로 커스텀하여 옮겨놓고 작업했음!
@Service
@RequiredArgsConstructor
@Slf4j
public class FileUploadService {
private final AmazonS3Client amazonS3Client;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
// 유저 프로필 이미지 업로드
public String uploadProfile(MultipartFile file) {
try {
String fileName=file.getOriginalFilename();
String uuid = UUID.randomUUID().toString();
ObjectMetadata metadata= new ObjectMetadata();
metadata.setContentType(file.getContentType());
metadata.setContentLength(file.getSize());
amazonS3Client.putObject(bucket,"profile/"+ uuid + "_"+fileName,file.getInputStream(),metadata);
return amazonS3Client.getUrl(bucket, "profile/"+ uuid + "_"+fileName).toString();
} catch (IOException e) {
throw new HttpException(false, "프로필 이미지 등록에 실패하였습니다.", HttpStatus.BAD_REQUEST); }
}
// 게시글 멀티미디어 저장
public List<String> uploadFiles(List<MultipartFile> files) {
List<String> uploadedFileUrls = new ArrayList<>();
try {
for (MultipartFile file : files) {
String fileName = file.getOriginalFilename();
String uuid = UUID.randomUUID().toString();
String fileUrl= "https://" + bucket + ".s3.ap-northeast-2.amazonaws.com/post/" + uuid + "_" + fileName;
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(file.getContentType());
metadata.setContentLength(file.getSize());
amazonS3Client.putObject(bucket, "post/"+ uuid + "_" + fileName, file.getInputStream(), metadata);
uploadedFileUrls.add(fileUrl);
}
return uploadedFileUrls;
} catch (IOException e) {
throw new HttpException(false, "이미지 등록에 실패하였습니다.", HttpStatus.BAD_REQUEST);
}
}
}
3. controller에서 필요한 로직 작성
1) RequestBody로 받는 updateDto 와 RequestParam으로 받는 file 별도 로직 처리
// 프로필 이미지 수정
@PatchMapping("/{userId}/profileImg")
public ResponseEntity<UserResponseDto> uploadProfileImg(@PathVariable Long userId, @RequestParam("file") MultipartFile file) {
String profileUrl = fileUploadService.uploadProfile(file);
UserUpdateDto updateDto = new UserUpdateDto(userId, profileUrl);
mypageService.updateProfileImg(userId, updateDto);
UserResponseDto response = UserResponseDto.res(HttpStatus.OK.value(), "프로필 이미지 수정 완료");
return ResponseEntity.status(HttpStatus.OK).body(response);
}
// 기본정보 수정
@PatchMapping("/{userId}/profile")
public ResponseEntity<UserResponseDto> updateProfile(@PathVariable Long userId,
@RequestBody @Valid UserUpdateDto userUpdateDto){
mypageService.updateProfile(userId, userUpdateDto);
UserResponseDto response = UserResponseDto.res(HttpStatus.OK.value(), "프로필 수정 완료");
return ResponseEntity.status(HttpStatus.OK).body(response);
};
👉 프로필 이미지의 경우 분리해도 괜찮지만 게시글에 다중 이미지 처리할 땐 하나의 메소드로 합쳐져야 한다고 판단해서 하나의 컨트롤러로 합침
2) RequestPart로 동시에 파라미터값 받아서 db값 수정 + S3 업로드 한번에 처리
@PatchMapping("/{userId}/profile")
public ResponseEntity<UserResponseDto> updateProfileTest(@PathVariable Long userId,
@RequestPart(value = "data") @Valid UserUpdateDto userUpdateDto,
@RequestPart(name = "files") MultipartFile file){
String profileUrl = fileUploadService.uploadProfile(file);
userUpdateDto.setProfileImg(profileUrl);
mypageService.updateProfile(userId, userUpdateDto);
UserResponseDto response = UserResponseDto.res(HttpStatus.OK.value(), "프로필 수정 완료");
return ResponseEntity.status(HttpStatus.OK).body(response);
};
4. post맨 테스트
프로필 수정(닉네임, 소개, 프로필 이미지) 할 때
아래와 같이 data에 json형태의 객체 그대로를 넣고 files에 파일 업로드해서 한번에 보내면
db에도 유저 정보가 수정되면서 s3에 들어가면 파일이 잘 들어와있다!
+++ 나의 프로젝트에서는 멀티미디어 파일 첨부하는 것이 선택사항이기 때문에
반드시 하나의 api로 관리할 필요가 없다고 느껴져서 멀티미디어와 게시글 관련 로직을 모두 분리하였다.
만약 쇼핑몰처럼 반드시 첨부가 되어야 할 상황에서는 같은 api로 작성하는 것도 좋을 것 같다!
'프로젝트 > 뉴스피드' 카테고리의 다른 글
[통합 테스트/MockMvc] 어쩌다 회원 가입만 290번 (0) | 2024.02.23 |
---|---|
[Spring boot + thymeleaf] 화면단 연결 고군분투기 (0) | 2024.02.18 |
[회고][프로젝트 5~12일차] 다시 1일차로 돌아간 이야기 (0) | 2024.02.04 |
[회고][프로젝트 2~4일차] 회원가입, 로그인 기능 구현 (0) | 2024.01.27 |
[spring security] 로그아웃 기능 구현 (0) | 2024.01.26 |