
{
"Id": "Policy1718072787670",
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Stmt1718072786495",
"Action": "s3:*",
"Effect": "Allow",
"Resource": "arn:aws:s3:::chukahaeyo/*",
"Principal": "*"
}
]
}만약 다음과 같은 에러가 뜬다면

퍼블릭 액세스 차단을 모두 비활성 해 주면 된다. 이 문제를 해결하기 위하여 IAM에 사용자를 추가하고, 역할 부여도 하는 식으로 많이 헤맸었다 ㅠㅠ IAM 사용자에 s3의 모든 권한을 추가하였는데도 정책이 추가되지 않았고, 퍼블릭 액세스 차단 설정과 충돌한다는 에러 메세지를 보고 혹시나 하는 마음에 모든 퍼블릭 액세스 차단을 '비활성'화 하였더니 해결되었다.
내가 만든 정책은 '모든 사용자가 접근 가능하도록 함'이라는 액세스 수준이었고, 초반에 버킷을 생성하였을 때의 버킷 정책은 '퍼블릭 액세스 차단'을 해 두었기 때문에 두개가 충돌나는 문제였다.
아래의 사진처럼 퍼블릭 액세스 차단을 비활성 하니 정책이 잘 추가되었다.

버킷을 생성하고, 정책을 모두 설정해 주었으면 파일을 업로드 해 주면 된다.

이제 이 버킷이 컴퓨터로 따지면 '폴더'가 되는 것이다. 버킷에서의 주소값을 이용하여 사진을 불러 다운받아 사용할 수 있는 것이다.

객체 URL 복사 버튼을 누르면 다음과 같은 URL이 보이는데, 이걸 인터넷으로 접속하면 내가 업로드 한 사진이 제대로 뜨는 것을 알 수 있다.
https://chukahaeyo-bucket.s3.ap-northeast-2.amazonaws.com/chicken_icon.png

이제 spring을 통해서 S3에 접근해 사진을 사용할 것이다. 그러려면 key가 있어야 하므로, 'IAM > 사용자'에 들어가 사용자 생성을 해 준다.
이후 이름을 입력하고, 직접 정책 연결을 눌러 S3의 모든 권한을 부여해준다.

그 다음 AmazonS3FullAccess로 모든 권한을 부여해준다.

마지막으로 사용자 생성을 눌러준 후 다시 사용자에 들어가면 내가 만든 사용자가 뜬 것을 확인할 수 있다. 나는 팀 명이 '축하해요'이므로 사용자의 이름도 'chukahaeyo'로 설정해 주었다.

이 때 발급받은 key(AWS IAM 사용자 액세스 키 & 비밀 액세스 키)는 창을 닫으면 볼 수 없으므로 바로 어딘가에 복사해 두는 것이 좋다.
jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<body>
<h1>Chicken Icon</h1>
<img src="https://chukahaeyo-bucket.s3.amazonaws.com/chicken_icon.png" alt="Chicken Icon" />
</body>
</html>
Config
package com.choikang.chukahaeyo.s3;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@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 AmazonS3Client amazonS3Client(){
BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey,secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder.standard()
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.withRegion(region)
.build();
}
}
Controller
package com.choikang.chukahaeyo.s3;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class S3Controller {
@GetMapping("/s3")
public String s3Test(){
return "/s3/s3Test";
}
}
이렇게 코드를 작성한 후 localhost~/s3로 접속하면 치킨 사진이 나와야 한다.
아래 실행 결과를 보면 s3에 잘 접속되어 사진을 불러오는 것을 알 수 있다.

처음에는 프로젝트를 배포하면 프로젝트에 필요한 모든 이미지들을 전부 S3 버킷에 올리고, 거기서 불러와서 사용해야 하는 줄 알았다. 현재는 저장된 경로에서 이미지를 불러오고, 이 경로는 로컬에서 불러와진다고 생각하였기 때문이다. 하지만, Intellij 파일 자체에 이미지들을 넣어놓고 이 이미지 자체를 배포해버리면 굳이 S3에 저장할 필요가 없다. 대신, 배포 후 사용자가 이미지를 업로드 하는 것을 S3에 저장해야 하는 것이다.
이제 사용자가 이미지를 업로드하면, 이것을 S3에 저장하고, 저장된 URL을 DB에 넣는 로직을 작성할 것이다.
1. 프론트에서 업로드 된 파일의 이름 받아오기
우선 S3만 따로 기능을 빼서 test하는 것이므로 html에 버튼을 만들어 준다.
s3Test.jsp
<form action="upload" method="post" enctype="multipart/form-data">
Select image to upload:
<input type="file" name="file" id="file">
<input type="submit" value="Upload Image" name="submit">
</form>
이후 파일이 업로드되면 파일 명이 들어가고, 이 파일 명이 백엔드로 전송되어지는 과정을 확인하기 위하여 controller를 작성해 주었다.
S3Controller
@PostMapping("/upload")
public void fileUpload(@RequestParam("file") MultipartFile file, RedirectAttributes redirectAttributes) {
System.out.println("업로드된 파일 이름: " + file.getOriginalFilename());
}
하지만, 다음과 같은 에러가 발생하였다.
org.springframework.web.servlet.DispatcherServlet - Failed to complete request: org.springframework.web.multipart.MultipartException: Failed to parse multipart servlet request; nested exception is java.lang.IllegalStateException: 어떤 multi-part 설정도 제공되지 않았기 때문에, part들을 처리할 수 없습니다.
multi-part 설정을 처음에는 property에 파일 크기만 설정해 주면 된다고 생각하고, 그렇게 하였지만 여전히 같은 에러가 발생하였다. mvn repostiory에서 검색해보니 의존성을 따로 추가해 주는 것도 아닌 것 같았다. Bean을 추가 해 주면 될 것 같아 MvcConfig에서 Bean을 추가 해 줌과 동시에 파일의 최대 용량을 지정해 주었더니 실행이 되었고, 서버로 정보가 넘어오는 것을 볼 수 있었다.

이제 이 파일을 S3에 업로드 해 주면 된다.
단, 이 때 버킷명이 유출되면 요금 폭탄을 맞을 수 있으므로 버킷명도 환경 변수에 넣어 주어야 한다. 현재 S3 에는 버킷에 업로드가 실제로 되지 않고 업로드 시도를 하기만 해도 요금이 부과되는 버그가 있기 때문이다. 그래서 상대방의 버킷명을 알기만 해도 요금이 많이 나오도록 공격 할 수 있기 때문에, 버킷명을 숨겨 주어야 한다.
2. S3에 업로드하기
S3Controller
@PostMapping("/upload")
public void fileUpload(@RequestParam("file") MultipartFile file) {
System.out.println("업로드된 파일 이름: " + file.getOriginalFilename());
if (file.isEmpty()) {
throw new CustomException(ErrorCode.VALIDATION_REQUEST_MISSING_EXCEPTION, "업로드 할 파일이 선택되지 않았습니다.");
}
try {
String fileUrl = s3Service.saveFile(file);
System.out.println("컨트롤러 fileUrl : " + fileUrl);
if(s3Service == null){
System.out.println("S3에서 받아온 주소 값이 null입니다.");
}
System.out.println(fileUrl);
}catch(CustomException e){
throw new CustomException(ErrorCode.INTERNAL_SERVER_ERROR, "S3 bucket에 사진을 저장하는 것을 실패했습니다.");
}
}
S3Service
@Service
@RequiredArgsConstructor
public class S3Service {
@Value("${cloud.aws.s3.bucket}")
private String bucket;
private final AmazonS3 amazonS3;
// 파일 유효성 검사
private String getFileExtension(String fileName) {
if (fileName.length() == 0) {
throw new CustomException(ErrorCode.NOT_FOUND_IMAGE_EXCEPTION, ErrorCode.NOT_FOUND_IMAGE_EXCEPTION.getMessage());
}
ArrayList<String> fileValidate = new ArrayList<>();
fileValidate.add(".jpg");
fileValidate.add(".JPG");
fileValidate.add(".jpeg");
fileValidate.add(".JPEG");
fileValidate.add(".png");
fileValidate.add(".PNG");
fileValidate.add(".webp");
fileValidate.add(".WebP");
fileValidate.add(".heif");
fileValidate.add(".HEIF");
fileValidate.add(".heic");
fileValidate.add(".HEIC");
fileValidate.add(".svg");
fileValidate.add(".SVG");
String idxFileName = fileName.substring(fileName.lastIndexOf("."));
System.out.println("idxFileName : " + idxFileName);
if (!fileValidate.contains(idxFileName)) {
throw new CustomException(ErrorCode.VALIDATION_IMAGE_REQUEST_FAILED, ErrorCode.VALIDATION_IMAGE_REQUEST_FAILED.getMessage());
}
return fileName.substring(fileName.lastIndexOf("."));
}
//파일을 S3 bucket에 업로드
public String saveFile(MultipartFile file){
System.out.println("컨트롤러에서 받아온 file명 : " + file);
String fileName = createFileName(file.getOriginalFilename());
System.out.println("서버에서 생성한 파일 이름 : " + fileName);
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(file.getSize());
metadata.setContentType(file.getContentType());
try{
amazonS3.putObject(bucket, fileName, file.getInputStream(), metadata);
} catch(SdkClientException e){
System.out.println("AWS SDK 클라이언트에서 문제 발생");
throw new CustomException(ErrorCode.NOT_FOUND_IMAGE_EXCEPTION, "AWS SDK 클라이언트에서 문제가 발생하였습니다.");
} catch (IOException e){
System.out.println("파일 업로드 중 문제 발생");
throw new CustomException(ErrorCode.NOT_FOUND_IMAGE_EXCEPTION, "AWS에서 파일 업로드 중 문제가 발생하였습니다.");
}
System.out.println("파일 업로드 성공");
System.out.println("업로드한 파일 이름 : " + fileName);
return amazonS3.getUrl(bucket, fileName).toString(); //S3에 저장된 URL을 갖고 오는 로직
}
//파일 이름 중복 방지를 위한 파일명 생성
private String createFileName(String fileName) {
return UUID.randomUUID().toString().concat(getFileExtension(fileName));
}
}
이 과정도 매우 험난했다 ... 처음에 Controller에서 Service가 호출되지 않는 문제가 발생했었다. Service의 빈이 제대로 주입되지 않는 문제였었다. Service 어노테이션도 붙였는데 말이다. 알고보니 Controller에서 @Autowired를 해 주지 않아서 발생한 문제였다.. 오랜 시간을 투자했는데, 정말 의외의 곳에서 에러를 찾아서 허무했다...
Service에 접근이 가능했지만, 파일을 저장할 수 없는 문제가 발생하였다. print를 찍어 보았는데, 분명 컨트롤러에서는 "pizza.png"와 같은 String값으로 넘겼지만 서버에서 출력 될 때는 이상한 문자열로 출력이 되는 문제였다. 나는 객체를 String으로 변환하기 위하여 toString() 문자열을 썼었는데, 이렇게 되면 원본 url이 출력이 되는 것이 아닌 것이다. file.getOriginalFilename()을 써야 원본 url을 받아올 수 있다. 이후 바꾸어 출력을 해 주니 제대로 받아와짐을 알 수 있었다.

여기서 파일을 선택하고 Upload Image 버튼을 누르면 서버에는 다음과 같이 출력이 된다.

fileUrl을 누르면 의도한 사진이 잘 뜨는 것을 알 수 있다.
https://chukahaeyo-bucket.s3.ap-northeast-2.amazonaws.com/45c065b8-d869-463d-832b-31809be0cab2.png
또한, bucket에도 제대로 올라가 있음을 알 수 있다.

3. S3의 링크를 DB에 저장하기
현재 시점까지의 템플릿이다.

여기서 사진만 S3 bucket에 업로드되고, 이름, 날짜, 문구, 이모티콘은 DB에 저장이 되어야 한다. 현재 DB에 업로드되는 부분은 현재 프론트에서 테스트를 진행하며 수정중이고, 사진 업로드만 먼저 구현이 가능한 상황이었다. 그래서 나는 '장바구니 담기, 결제하기' 버튼을 누르면 다음 두 가지의 동작이 이루어지도록 로직을 작성했었다.
우선 1번 작업부터 수행하던 중, 문득 '결합도가 높다'는 생각이 들었다. 왜냐하면 사진을 저장하려는 각 jsp마다 ajax문을 써 주어야 하고, 컨트롤러를 다시 호출해 주어야 하기 때문인데다가 컨트롤러도 DB 저장용과 S3 저장용 두 개를 호출해야 하기 때문이다.
하나의 컨트롤러를 만들어 프론트에서 받아온 필수 항목들을 전부 받아오고, 이 안에서 S3 service를 호출하여 S3에 저장하는 로직을 따로 호출하고 DB에 저장하는 로직도 따로 호출해야 하나의 컨트롤러만 사용하고, 결합도가 낮아진다고 판단하였다.
다음 과정대로 진행하면 된다.
현재 로직 : saveFile을 수행하면 프론트에 S3에 저장된 파일명을 넘겨줌(서버에서 생성한 중복을 방지하기 위한 파일명) -> 이 파일명이 html에 저장되어 있음. 이 값을 삭제 요청을 할 때 String값으로 넘겨줌
1. 받아온 파일명을 이용하여 삭제 요청을 보냄
public void deleteFile(String fileName){
System.out.println("받아온 삭제할 파일명 : " + fileName);
amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName));
}
@PostMapping("/delete")
public ResponseEntity<String> fileDelete(String fileName) {
if (fileName.isEmpty()) {
throw new CustomException(ErrorCode.VALIDATION_REQUEST_PARAMETER_MISSING_EXCEPTION, ErrorCode.VALIDATION_REQUEST_PARAMETER_MISSING_EXCEPTION.getMessage());
}
try {
s3Service.deleteFile(fileName);
} catch (CustomException e) {
throw new CustomException(ErrorCode.INTERNAL_SERVER_ERROR, "S3 bucket에서 사진을 삭제하는 것을 실패했습니다.");
}
return new ResponseEntity<>(SuccessCode.DELETE_SUCCESS.getMessage(), SuccessCode.DELETE_SUCCESS.getHttpStatus());
}
@Log4j
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {config.MvcConfig.class})
@WebAppConfiguration
@Slf4j
public class S3Test {
@Autowired
S3Service s3Service;
@Test
public void cancelTest(){
s3Service.deleteFile("d26e29fe-fb0b-4807-bb1d-2ea664aa0094.png");
}
}
결제 취소 매개변수를 찾기 위하여 우선 AmazonS3 Interface에 들어가 취소 부분을 보았다.

여기에서 var1과 var2값이 무엇인지를 몰라 AmazonS3Client에 들어가 보았더니 다음과 같이 쓰여져 있었다.

여기서의 bucketName은 s3에서 설정해 준 bucket의 이름 값을 넣으면 되고, 이는 환경 변수로 설정을 한 후 전역변수로 선언해 두었다. String key값은 Bucket에 들어가 보이는 key값인데, 이는 파일명과 동일하므로 파일명을 넘겨주면 된다.

이후 S3 버킷에 들어가 확인해 보면 삭제가 제대로 수행된 것을 알 수 있다.