https://dev-leeyjstar.tistory.com/entry/Spring-boot-AWS-S3-Presigned-URL%EB%A1%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EA%B4%80%EB%A6%AC-1-S3-%EC%84%A4%EC%A0%95
[Spring boot] AWS S3 + Presigned URL로 이미지 관리 (1) - S3 설정
1. 구현 이유 현재 사이드 프로젝트에서 백엔드 spring boot jpa 개발을 맡고 있다. 이미지 crud를 구현하는 것을 목표로 s3를 활용해서 이미지를 관리하고자 한다. 전에는 naver cloud platform에서 제공해
dev-leeyjstar.tistory.com
1. 구현 이유
위에 1편에 이어서 작성한다. 나는 일단 s3를 만들기 위해 버킷을 생성했고 나는 IAM Role 방식으로 ec2 인스턴스에 role을 부여해 자동인증을 함으로서 ec2에 접근이 가능하도록 하는 방식으로 구현을 하려고 세팅을 했다.
하지만 실제 개발을 하고 로컬에서 테스트를 하기 위해서는 결국 테스트용 Iam 을 만들고 access key를 발급받아서 테스트를 해야한다.
테스트가 다 되면 다시 원래 했던 방식으로 돌릴것이다.
즉
로컬 : Access Key 방식 : 로컬에서만 키를 설정 (application-local.yml)
ec2 : iam role 방식 : 키 생략 (application-prod.yml)
아무튼 그 과정을 기록한다..
2. Iam 사용자를 생성
1. iAM 콘솔에서 사용자 추가를 한다.
2. 사용자 이름은 s3-local-test 이렇게 했다.
3. 권한은 AmazonS3FullAccess 로 했다
4. access key 발급 (설명테그는 생략)
5. 키 다운한다. 한번만 보여지기 때문에 꼭 저장 잘 해야함
이 키를 env 안에 넣어주면 된다.
3. profile 환경 분리
자.. 현재 나는 application.yml 에 설정들을 하면서 이걸 배포용, 개발용 다 공통으로 사용하고 있었다.
하지만 지금은 s3 를 설정하면서 로컬에서는 Access Key를 등록해서 인증하는 방식을 사용할 것이고 실제 배포되었을 때는 ec2에 자동 등록이 되어 있는 IAM ROLE 을 사용할 것이다. 이런 분리된 환경에서의 설정을 해보자
일단
credentials? aws 인증 정보(자격증명 )
cloud:
aws:
credentials:
access-key: AKIAxxxxxxxx
secret-key: wJalrxxxxxxxx
aws 리소스를 사용할 수 있는 비밀번호 같은 것
1. 기존에 사용하던 application.yml 에 항상 공통되는 설정을 해준다. 버킷명, 공통 도메인 등등 (region, bucket)
2. application-local.yml 에는 s3 secret key만 설정해준다 (credentials 설정을 넣어줌)
3. application-prod.yml 에는 iam role이 자동 인증 작동되도록 키가 없는 버전으로 설정을 해주면 된다. (credentials 설정을 빼줌)
사실 일반적으로는 prod 파일을 만들어줘야한다. 환경에 따라 오버라이딩이 필요한 설정이 있다면 만들어줘야 한다.
지금 내 상황에서는 cloud.aws.region, bucket 같은 설정이 공통 yml 에 설정이 되어 있고
로컬 버전에만 credentials 설정을 해주면 되고 운영 버전에는 credentials를 빼면 되기 때문에 굳이 파일이 없어도 되는것.
하지만 일반적으로 application-prod.yml을 두는 이유는 나중에 운영에서만 바꾸고 싶은 설정이 있을 수 있기 때문이다.
예를 들어 db url, 로그 레벨을 운영에서는 info로 하고 로컬에서는 debug로 할수 있다. api 키 같은 것도 달라질 수 있어서 운영용 application-prod.yml을 두는게 일반적이다. 하지만 나는 사이드프로젝트이고 지금은 생각해봐도 달라질 설정이 없을 거 같다.
그리고 그떄 필요하면 파일을 만들면 되지 않을까 싶다 굳이 지금 만들 필요가 있을까...
그래서 안만든다.
3. 1. application.yml : 프로파일, aws 버킷과 리전을 설정했다.
spring:
application:
name: recipe
profiles:
active: ${SPRING_PROFILES_ACTIVE:local} # 기본적으로 로컬 프로파일을 활성화
sql:
init:
mode: ${SQL_INIT_MODE:always}
cloud:
aws:
region:
static: ap-northeast-2
s3:
bucket: recipic-bucket
3. 2. application-local.yml : 키 설정
cloud:
aws:
credentials:
access-key: ${AWS_ACCESS_KEY}
secret-key: ${AWS_SECRET_KEY}
3.3. git action 워크플로우 수정
로컬에는 위에처럼 application.yml에 있는 local이 적용되서 사용되지만 실제 배포를 할 떄는 워크플로우의 .env 파일 기반의 실행하면 자동으로 spring 이 application-prod.yml 또는 default로 설정을 사용하게 된다.
spring은 appilcation.yml에 적인 active: local보다도 환경변수로 전달된 spring_profile_active 값을 우선 적용한다.
- name: 📝 Create .env file
run: |
echo "SPRING_PROFILES_ACTIVE=prod" >> .env
echo "APP_DOMAIN=${{ secrets.APP_DOMAIN }}" >> .env
echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env
...
4. 코드
4.1. sdk 추가 (build.gradle)
implementation 'software.amazon.awssdk:s3'
4.2. s3Config.java
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
@Configuration
public class S3Config {
@Value("${cloud.aws.region.static}")
private String region;
@Value("${cloud.aws.s3.access-key:}")
private String accessKey;
@Value("${cloud.aws.s3.secret-key:}")
private String secretKey;
@Bean
public S3Presigner s3Presigner() {
S3Presigner.Builder builder = S3Presigner.builder()
.region(Region.of(region));
if (!accessKey.isEmpty() && !secretKey.isEmpty()) {
// 로컬 환경: Access Key와 Secret Key를 사용
builder.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create(accessKey, secretKey)));
} else {
// EC2 환경: IAM Role을 자동 감지
builder.credentialsProvider(DefaultCredentialsProvider.create());
}
return builder.build();
}
}
- S3Presinger는 aws sdk에서 제공하는 클래스 중 하나로 presigned url을 생성하는 데 사용된다.
4.3. service
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.*;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.*;
import java.net.URL;
import java.time.Duration;
import java.util.UUID;
@Service
@RequiredArgsConstructor
public class S3Service {
private final S3Presigner s3Presigner;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
// S3에 이미지 업로드용 Presigned URL 발급 (PUT 방식)
public String generateUploadPresignedUrl(String folder, String originalFileName) {
// 고유한 파일 경로 생성
String key = folder + "/" + UUID.randomUUID() + "-" + originalFileName;
// S3에 업로드할 파일 요청 정보
PutObjectRequest objectRequest = PutObjectRequest.builder()
.bucket(bucket)
.key(key)
.contentType("image/jpeg")
.build();
// Presigned URL 발급 요청
PresignedPutObjectRequest presignedRequest = s3Presigner.presignPutObject(
r -> r.putObjectRequest(objectRequest)
.signatureDuration(Duration.ofMinutes(5))
);
return presignedRequest.url().toString(); // 프론트가 이 URL로 업로드하게 됨
}
// S3에 있는 이미지 조회용 Presigned URL 발급 (GET 방식)
public String generateViewPresignedUrl(String key) {
// 조회 요청 정보 생성
GetObjectRequest getObjectRequest = GetObjectRequest.builder()
.bucket(bucket)
.key(key)
.build();
// Presigned GET URL 발급
GetObjectPresignRequest getObjectPresignRequest = GetObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(10))
.getObjectRequest(getObjectRequest)
.build();
PresignedGetObjectRequest presignedGetObjectRequest =
s3Presigner.presignGetObject(getObjectPresignRequest);
return presignedGetObjectRequest.url().toString();
}
// S3에서 이미지 삭제
public void deleteFile(String imageUrl) {
String key = extractKeyFromUrl(imageUrl);
S3Client s3 = S3Client.builder()
.region(Region.AP_NORTHEAST_2)
.credentialsProvider(DefaultCredentialsProvider.create())
.build();
s3.deleteObject(DeleteObjectRequest.builder()
.bucket(bucket)
.key(key)
.build());
}
/**
* S3 URL에서 Key 추출
* ex: https://bucket.s3.amazonaws.com/folder/abc.jpg → folder/abc.jpg
*/
private String extractKeyFromUrl(String url) {
int idx = url.indexOf(".amazonaws.com/") + ".amazonaws.com/".length();
return url.substring(idx);
}
}
4.4. controller
import lombok.RequiredArgsConstructor;
import njb.recipe.service.S3Service;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequiredArgsConstructor
@RequestMapping("/s3")
public class S3Controller {
private final S3Service s3Service;
/**
* 업로드용 Presigned URL 발급
* 프론트에서 파일 업로드 전에 요청
*/
@GetMapping("/presigned-upload")
public ResponseEntity<String> getPresignedUploadUrl(
@RequestParam String folder,
@RequestParam String fileName
) {
String presignedUrl = s3Service.generateUploadPresignedUrl(folder, fileName);
return ResponseEntity.ok(presignedUrl);
}
/**
* 조회용 Presigned URL 발급
* 프론트에서 비공개 이미지 조회 시 요청
*/
@GetMapping("/presigned-view")
public ResponseEntity<String> getPresignedViewUrl(
@RequestParam String key // 예: ingredient/uuid-filename.jpg
) {
String viewUrl = s3Service.generateViewPresignedUrl(key);
return ResponseEntity.ok(viewUrl);
}
/**
* 이미지 삭제 (서버가 직접 삭제 요청)
*/
@DeleteMapping("/file")
public ResponseEntity<Void> deleteImage(@RequestParam String imageUrl) {
s3Service.deleteFile(imageUrl);
return ResponseEntity.noContent().build();
}
}
- 조회용 url 발급 ,,, s3 버킷에 저장할 수 있는 presigned url을 발급받아서 저장을 했다. 하지만 굳이 비공개로 해서 조회용 presigned url까지 발급할 필요가 있을까라는 의문점이 들었다.
- 보안적인 측면을 생각하면 비공개로 하는게 맞을 거 같아서 코드상의 구현을 했다. 하지만 일단 s3 버킷은 퍼블릭 읽기 허용으로 바꿔서 접근 가능 할 수 있게 하는게 좋겠다. 나중에 뭐 바꾸면 되니까.. 지금은 이게 더 편리할 거 같다.
5. 테스트
서버 실행 후
5.1. s3에 업로드용 presigned url이 잘 발급되는지 보자
- param에 값을 잘 넣어준다. 이때 저렇게 폴더 이름을 넣어주면 s3 버킷 내에 폴더 아래로 이미지가 저장이 된다.
뭐 이런식으로 presigned.url이 날라온다.
5.2. presignedURL로 s3에게 이미지 저장 요청하기 (put)
- 요청 url을 아까 받은 presignedURL로 저장하고 싶은 이미지를 put 요청을 하면 200응답을 확인할 수 있다.
내 버킷 아래에 아까 param으로 넣었던 폴더 경로 밑에 이미지가 저장된 것이 보인다.
저장 요청이 됐으면 이미지를 프론트에서는 <img src={imageUrl}> 바로 이미지를 띄울 수 있는 것이 좋다.
그래서 s3에 저장된 이미지를 바로 받을 수 있게 s3를 퍼블릭으로 열어두면 된다.
여기서 생겼던 트러블 슈팅은 따로 기록.... 하겠다..
.
.
.
일단 파일 업로드를 위한 presigned URL을 발급을 받았을 때 이것은 서명된 url로 "?" 물음표 뒤에 붙어 있는 값들은 aws에서 발급한 access key, signature 같은 것들이다.
그래서 실제로 프로트에서 img 테그를 통해 이미지를 조회할 때는 split('?')[0] 를 사용해서 쿼리 스트링을 제거하고 수순한 이미지 주소만 가져다가 사용하면 된다.
그래서 백엔드 디비 서버에 이미지 주소 경로를 저장할때도 순수 이미지 주소만 저장해주면 된다.
이거는 굳이 넣지 않겠다. 그냥 디비 crud니까..
그래서 https://recipic-bucket.s3.ap-northeast-2.amazonaws.com/ingredint/9aeabㅇㄹㄴㅇㄹㄴㅇㄹㄴㅇㄹ.testing.jpg?X-Aㄴㅇㅇㄹㄴㅇㄹㄴㅇㄹㄴㅇ
이렇게 presigned URL 응답으로 준 값에 ?만 제거하고 검색을 하면 이미지에 퍼블릭 접근이 가능해서 조회를 할 수 있게 된다.
아무 화면 캡처에서 저장했더니 ㅎㅎ 이렇다..ㅎ
'Bakend > Spring' 카테고리의 다른 글
[Spring boot] AWS S3 + Presigned URL로 이미지 관리 (1) - S3 설정 (1) | 2025.04.17 |
---|---|
[트러블 슈팅] 파이어베이스 FCM , Spring boot 알림 구현 (0) | 2025.04.06 |
[트러블 슈팅] ncp Object Storage (OOS) 이미지 올리기, 수정하기 (0) | 2024.11.22 |
[트러블 슈팅] 공공데이터포털 SERVICE_KEY_IS_NOT_REGISTERED_ERROR (1) | 2024.09.22 |