🏕️기존 상황
이미 S3 버킷은 만들어져 있는 상황이었고 모든 권한을 가진 iam 유저도 1명 있는 상태였다.
그 상황에서 더 추가적으로 필요한 것들을 추가해서 개발하였다.
🔺S3연동을 위해 해당 권한만 별도로 가진 유저를 생성
- 노란색 박스처럼 S3 권한을 가진 전용 iam 유저를 [highWay_S3_user]라는 이름으로 생성.
- 핑크색 박스의 권한을 부여하여 유저를 생성. (S3에 대한 전체 권한을 허용하는 것이기 때문에 추후 더 보안에 신경 쓸 수 있는 권한을 도입예정)
🔺build.gradle
//aws
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
🔺application.properties
# S3
cloud.aws.credentials.accessKey= **개별정보**
cloud.aws.credentials.secretKey=**개별정보**
cloud.aws.s3.bucket=**개별정보**
cloud.aws.region.static=ap-northeast-2
cloud.aws.stack.auto-=false
🔺Image
@Getter
@Setter
@ToString
@NoArgsConstructor
@Entity(name = "image_TB")
public class Image {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // Image 객체 pk
@Column
private String imageUrl;
public Image(String imageUrl) {
this.imageUrl = imageUrl;
}
}
🔺ImageServiceImpl
@Service
public class ImageServiceImpl {
// 의존성 주입
private final JpaDiaryRepository diaryRepository;
@Autowired
private S3Uploader s3Uploader;
public ImageServiceImpl(JpaDiaryRepository diaryRepository) {
this.diaryRepository = diaryRepository;
}
@Transactional
public Long keepImage(MultipartFile image, Image entity) throws IOException {
if (!image.isEmpty()) {
try {
String storedFileName = s3Uploader.upload(image, "images");
entity.setImageUrl(storedFileName);
} catch (IOException e) {
// 업로드 오류 처리
e.printStackTrace();
// 또는 로그로 기록: log.error("S3 파일 업로드에 실패했습니다. {}", e.getMessage());
throw new IllegalStateException("S3 파일 업로드에 실패했습니다.");
}
}
System.out.println("-------------"+entity.getImageUrl()+"---------------");
Image savedImage = diaryRepository.save(entity);
return savedImage.getId();
}
}
🔺S3Config
@Configuration
public class S3Config {
@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 amazonS3Client() {
AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
return AmazonS3ClientBuilder
.standard()
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.withRegion(region)
.build();
}
}
🔺S3Controller
@Controller
public class S3Controller {
private final ImageServiceImpl service;
public S3Controller(ImageServiceImpl service) {
this.service = service;
}
@ResponseBody
@PostMapping(value="/image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Long saveDiary(HttpServletRequest request, @ModelAttribute Image diary, @RequestParam(value="image") MultipartFile image) throws IOException {
Long ImageId = service.keepImage(image, diary);
return ImageId;
}
}
🔺S3Uploader
@Slf4j
@RequiredArgsConstructor // final 멤버변수가 있으면 생성자 항목에 포함시킴
@Component
@Service
public class S3Uploader {
private final AmazonS3Client amazonS3Client;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
// MultipartFile을 전달받아 File로 전환한 후 S3에 업로드
public String upload(MultipartFile multipartFile, String dirName) throws IOException {
// 메타데이터 설정
File uploadFile = convert(multipartFile)
.orElseThrow(() -> new IllegalArgumentException("MultipartFile -> File 전환 실패"));
System.out.println("변환된 이미지 파일 이름: " + uploadFile.getName());
return upload(uploadFile, dirName);
}
private String upload(File uploadFile, String dirName) {
String fileName = dirName + "/" + UUID.randomUUID() + "." + uploadFile.getName();
String uploadImageUrl = putS3(uploadFile, fileName);
removeNewFile(uploadFile); // 로컬에 생성된 File 삭제 (MultipartFile -> File 전환 하며 로컬에 파일 생성됨)
return uploadImageUrl; // 업로드된 파일의 S3 URL 주소 반환
}
private String putS3(File uploadFile, String fileName) {
amazonS3Client.putObject(
new PutObjectRequest(bucket, fileName, uploadFile)
.withCannedAcl(CannedAccessControlList.PublicRead) // PublicRead 권한으로 업로드 됨
);
return amazonS3Client.getUrl(bucket, fileName).toString();
}
private void removeNewFile(File targetFile) {
if(targetFile.delete()) {
log.info("파일이 삭제되었습니다.");
}else {
log.info("파일이 삭제되지 못했습니다.");
}
}
private Optional<File> convert(MultipartFile file) {
File convertFile = new File(file.getOriginalFilename());
try (FileOutputStream fos = new FileOutputStream(convertFile)) {
fos.write(file.getBytes());
} catch (IOException e) {
// 변환에 실패하면 예외 처리
convertFile.delete();
// 예외 메시지와 스택 트레이스를 로그로 출력
log.error("MultipartFile 변환에 실패했습니다: {}", e.getMessage(), e);
return Optional.empty();
}
return Optional.of(convertFile);
}
}
🔺성공화면
💚후기
- 내가 일일이 작성한 코드는 아니고 블로그에 검색해서 나온 코드를 내 필요에 맞게 변경한 것이다.
(근데 흥미로운 것은 대부분 코드가 다 비슷비슷하다는 것이다. ) - 그렇게 복잡한 코드는 아닐지라도 성공하는데 거의 2~3일은 소모한 거 같다...
- 중간에 멘토님께도 질문하고, 성공하지 못하는 다양한 원인을 생각해 보고 그에 상응하는 해결책들을 검색해 보느라 오래 걸리기도 했다.
- 오류가 중간에 좀 있었는데 해결하느라 정리는 하나도 못했다!!!
- 그렇게 복잡하지 않은 코드들로 인터넷에 로컬에 있는 이미지가 올라가는 게 신기하다! (더 공부해야지...)
🧡앞으로 할 일들
- 자세한 코드 분석과 작동원리 공부하기
- 일단 성공시키느라 보안에 대해선 크게 신경 쓰지 못했기 때문에 권한이나 접근에 대해 더 고민하여 S3, iam을 변경하기
- 업로드 이외에 읽기/삭제 기능 구현하기
- 게시판 코드와 도킹(?)하기 ㅎㅎ
- 다중 업로드에 대해서 추가 기능 개발하기

'개발일지 > 2023_한이음' 카테고리의 다른 글
🚨ERROR - com.amazonaws.SdkClientException: Failed to connect to service endpoint: (0) | 2023.07.28 |
---|---|
[개발]🚨ERROR - com.amazonaws.services.s3.model.AmazonS3Exception: The bucket does not allow ACLs (0) | 2023.07.27 |
[개발] S3_accessKey, secretKey 발급받기기(문제/해결) (0) | 2023.07.26 |
[개발] Querydsl을 사용하여 학교 좋아요 리스트 반환하기 (0) | 2023.07.19 |
[개발] Querydsl를 사용하여 원하는 정보 조회/반환하기(Refactoring) (0) | 2023.07.18 |