[Spring Boot3] ๐Ÿ“ค S3 Presigned URL๋กœ ์„œ๋ฒ„ ๋ถ€๋‹ด ์—†์ด ํŒŒ์ผ ์—…๋กœ๋“œํ•˜๊ธฐ (๊ตฌํ˜„๋ถ€ํ„ฐ ํ™œ์šฉ๊นŒ์ง€)

hhhยท2025๋…„ 4์›” 30์ผ
4

์Šคํ”„๋ง๋ถ€ํŠธ๐ŸŒฑ

๋ชฉ๋ก ๋ณด๊ธฐ
3/13
post-thumbnail

๐Ÿ“ฆ ๋Œ€์šฉ๋Ÿ‰ ํŒŒ์ผ ์—…๋กœ๋“œ, ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ•˜์ง€?

๋Œ€์šฉ๋Ÿ‰ ํŒŒ์ผ ์—…๋กœ๋“œ ๊ธฐ๋Šฅ์„ ๋งก์œผ๋ฉด ๋ฐฑ์—”๋“œ๋Š” ๊ณ ๋ฏผ์— ๋น ์ง„๋‹ค.
โ€œ์ด๊ฑธ ๋‚ด๊ฐ€ ์ง์ ‘ ๋ฐ›์•„์„œ S3์— ๋‹ค์‹œ ์˜ฌ๋ ค์•ผ ํ•˜๋‚˜?โ€
โ€œ๋ฉ”๋ชจ๋ฆฌ๋Š” ๊ดœ์ฐฎ์„๊นŒ?โ€
โ€œํŠธ๋ž˜ํ”ฝ, ์‹œ๊ฐ„ ์ดˆ๊ณผ๋Š”?โ€


๊ทธ๋ž˜์„œ ๋‚˜๋Š” ์„ ํƒํ–ˆ๋‹ค.
Presigned URL, ์—…๋กœ๋“œ๋Š” ํ”„๋ก ํŠธ์—๊ฒŒ ๋„˜๊ธฐ๊ธฐ๋กœ.

๐Ÿ›ก๏ธ Presigned URL์„ ์•„์‹œ๋‚˜์š”

์‚ฌ์‹ค ์ด์œ ๋Š” ๋ช…ํ™•ํ•˜๋‹ค.
๋ฐฑ์—”๋“œ๊ฐ€ ์ง์ ‘ ํŒŒ์ผ์„ ๋ฐ›์„ ํ•„์š”๊ฐ€ ์—†๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

Presigned URL์€ AWS S3์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ์œผ๋กœ,
ํด๋ผ์ด์–ธํŠธ(ํ”„๋ก ํŠธ์—”๋“œ)๊ฐ€ ๋ฐฑ์—”๋“œ๋ฅผ ๊ฑฐ์น˜์ง€ ์•Š๊ณ  ๋ฐ”๋กœ S3์— ์—…๋กœ๋“œํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š”
์ž„์‹œ ์ ‘๊ทผ ๊ถŒํ•œ URL์ด๋‹ค.

์ด ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•˜๋ฉด, ๋ฐฑ์—”๋“œ๋Š” ์—…๋กœ๋“œ์™€ ๊ด€๋ จ๋œ ๋ฌด๊ฑฐ์šด ์ž‘์—…๋“ค(ํŒŒ์ผ ์ €์žฅ, ์ „์†ก, ๋ฆฌ์†Œ์Šค ๊ด€๋ฆฌ ๋“ฑ)์„ ํ”„๋ก ํŠธ์—๊ฒŒ ๋„˜๊ธฐ๊ณ  ๋น ์งˆ ์ˆ˜ ์žˆ๋‹ค.

ํ•œ๋งˆ๋””๋กœ, ํŒŒ์ผ์€ ๋„ค๊ฐ€ ์˜ฌ๋ คโ€ฆ ๋‚˜๋Š” URL๋งŒ ์ค„๊ฒŒ ์ „๋žต์ธ ์…ˆ์ด๋‹ค.

โšก Presigned URL์˜ ์ฃผ์š” ์žฅ์ 

โœ… ์„œ๋ฒ„ ๋ฆฌ์†Œ์Šค ์ ˆ์•ฝ
๋ฐฑ์—”๋“œ๊ฐ€ ์ง์ ‘ ํŒŒ์ผ์„ ๋ฐ›์ง€ ์•Š์œผ๋ฏ€๋กœ, ๋ฉ”๋ชจ๋ฆฌ๋‚˜ ๋””์Šคํฌ I/O์— ๋ถ€๋‹ด์ด ์—†๋‹ค.

โœ… ํŠธ๋ž˜ํ”ฝ ์ ˆ๊ฐ
ํŒŒ์ผ์ด ๋ฐฑ์—”๋“œ๋ฅผ ๊ฑฐ์น˜์ง€ ์•Š๊ณ  ๋ฐ”๋กœ S3๋กœ ์ „์†ก๋˜๊ธฐ ๋•Œ๋ฌธ์— ํŠธ๋ž˜ํ”ฝ์ด ์ ˆ๋ฐ˜์œผ๋กœ ์ค„์–ด๋“ ๋‹ค.

โœ… ์งํ†ต ๊ตฌ์กฐ
ํ”„๋ก ํŠธ์—”๋“œ โ†’ S3๋กœ ๋ฐ”๋กœ PUT ์š”์ฒญ. ๋ฐฑ์—”๋“œ๋Š” URL๋งŒ ๋ฐœ๊ธ‰ํ•˜๊ณ  ๋น ์ง„๋‹ค.

โœ… ๋ณด์•ˆ์„ฑ ํ™•๋ณด
Presigned URL์—๋Š” ๋งŒ๋ฃŒ ์‹œ๊ฐ„์ด ์„ค์ •๋˜์–ด ์žˆ๊ณ , ๊ทธ ์‹œ๊ฐ„์ด ์ง€๋‚˜๋ฉด ํ•ด๋‹น URL์€ ๋ฌดํšจ๊ฐ€ ๋œ๋‹ค. ์ฆ‰, URL์ด ์œ ์ถœ๋˜์–ด๋„ ์ผ์ • ์‹œ๊ฐ„ ์ดํ›„์—๋Š” ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์— ๋ณด์•ˆ์„ฑ๋„ ์ฑ™๊ธธ ์ˆ˜ ์žˆ๋‹ค.

๐Ÿ” Presigned URL ์ „์ฒด ํ๋ฆ„

์ฒ˜์Œ ์‚ฌ์šฉํ•  ๋•Œ ํ”„๋ก ํŠธ/๋ฐฑ์˜ ์—ญํ• ์ด ๊ต‰์žฅํžˆ ํ—ท๊ฐˆ๋ ธ๋˜ ๊ธฐ์–ต์ด ์žˆ์–ด์„œ ๊ตฌํ˜„ ํ๋ฆ„์„ ์ •๋ฆฌํ•ด๋ณด์•˜๋‹ค.

ํ”„๋ก ํŠธ
1. ์—…๋กœ๋“œํ•  ํŒŒ์ผ ์ •๋ณด(์˜ˆ: ํŒŒ์ผ๋ช…, ๋””๋ ‰ํ† ๋ฆฌ)๋ฅผ ๋ฐฑ์—”๋“œ์— ์ „๋‹ฌ

๋ฐฑ์—”๋“œ
2. ์š”์ฒญ ์ •๋ณด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ Presigned URL ์ƒ์„ฑ ํ›„ ํ”„๋ก ํŠธ์— ์‘๋‹ต

ํ”„๋ก ํŠธ
3. ๋ฐ›์€ Presigned URL๋กœ ์ง์ ‘ AWS S3์— PUT ์š”์ฒญํ•˜์—ฌ ํŒŒ์ผ ์—…๋กœ๋“œ

ํ”„๋ก ํŠธ
4. S3 ๊ฐ์ฒด URL(=ํŒŒ์ผ ๊ณ ์œ  ์ฃผ์†Œ)์„ ์ถ”์ถœํ•˜์—ฌ ๋ฐฑ์—”๋“œ์— ์ „์†ก

๋ฐฑ์—”๋“œ
5. ๊ฐ์ฒด URL์„ DB ๋“ฑ์— ์ €์žฅ

๐Ÿ› ๏ธ Presigned URL ์ƒ์„ฑ API ๊ตฌํ˜„ (Spring Boot)

์ด์ œ (S3์— ๋Œ€ํ•œ IAM, ๋ฒ„ํ‚ท ์„ค์ •์ด ์™„๋ฃŒ๋˜์–ด ์žˆ๋‹ค๋Š” ์ „์ œ ํ•˜์—) ์Šคํ”„๋ง๋ถ€ํŠธ๋กœ Presigned URL์„ ์ƒ์„ฑํ•˜๋Š” API๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์•Œ์•„๋ณด์ž

(1) Controller

/api/s3/presigned-url๋กœ POST ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉด presigned URL์„ ์‘๋‹ตํ•ด์ฃผ๋Š” ์ปจํŠธ๋กค๋Ÿฌ์ด๋‹ค.

@RequiredArgsConstructor
@RequestMapping("/api/s3")
@RestController
public class S3Controller{

    private final S3Service s3Service;

    // S3 presigned URL ์ƒ์„ฑ API
    @PostMapping("/presigned-url")
    public ResponseEntity<SuccessResponse<?>> generatePresignedUrl(@RequestBody PresignedUrlRequestDTO presignedUrlRequest) {
        PresignedUrlResponseDTO presignedUrl = s3Service.generatePresignedUrl(presignedUrlRequest);
        return SuccessResponse.ok(presignedUrl);
    }
}

(2) DTO

RequestDTO - ์š”์ฒญ์‹œ์—๋Š” Presigned Url ์ƒ์„ฑํ•œ ํ•„์š”ํ•œ ์ •๋ณด๋ฅผ ๋ฐ›์•„์˜ฌ๊ฑด๋ฐ ์—ฌ๊ธฐ์„œ๋Š” ์—…๋กœ๋“œํ•  ํŒŒ์ผ์˜ ๊ฒฝ๋กœ์™€ ์ด๋ฆ„์„ ํฌํ•จํ•œ ์ •๋ณด๋ฅผ ๋ฐ›์•„์™”๋‹ค.

public record PresignedUrlRequestDTO(
        String directory,
        String fileName
) {
}

ResponseDTO - ์ƒ์„ฑ๋œ presigned URL์„ ๊ฐ์‹ธ์„œ ํด๋ผ์ด์–ธํŠธ์— ์‘๋‹ตํ•œ๋‹ค.

public record PresignedUrlResponseDTO(
        String presignedUrl
) {
    public static PresignedUrlResponseDTO of(String presignedUrl) {
        return new PresignedUrlResponseDTO(presignedUrl);
    }
}

(3) Service

@Service
@RequiredArgsConstructor
@Slf4j
public class S3Service {

    private final AmazonS3 amazonS3;

    @Value("${cloud.aws.s3.bucket}")
    private String bucketName;

    // presigned URL ์ƒ์„ฑ
    public PresignedUrlResponseDTO generatePresignedUrl(PresignedUrlRequestDTO presignedUrlRequest) {
        String filePath = presignedUrlRequest.directory() + "/" + presignedUrlRequest.fileName();
        Date expiration = new Date(System.currentTimeMillis() + 1000 * 60 * 15); // 15๋ถ„ ์œ ํšจ

        GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucketName, filePath)
                .withMethod(HttpMethod.PUT)
                .withExpiration(expiration);

        URL url = amazonS3.generatePresignedUrl(generatePresignedUrlRequest);

        return PresignedUrlResponseDTO.of(url.toString());
    }
}

  • ์ „๋‹ฌ๋ฐ›์€ ๋””๋ ‰ํ† ๋ฆฌ์™€ ํŒŒ์ผ๋ช…์„ ๊ธฐ๋ฐ˜์œผ๋กœ presigned URL์„ ์ƒ์„ฑํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
  • ํ•ด๋‹น URL์€ PUT ๋ฐฉ์‹์œผ๋กœ 15๋ถ„๊ฐ„ ์œ ํšจํ•˜๋‹ค.

๐Ÿ” Presigned URL API ์‚ฌ์šฉ๋ฒ•

์ด์ œ ๊ตฌํ˜„ํ•œ Presigned URL API ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•ด๋ณด์ž

(1) API ํ˜ธ์ถœ

POST /api/s3/presigned-url ์š”์ฒญ์„ ํ†ตํ•ด Presigned URL์„ ๋ฐœ๊ธ‰๋ฐ›๋Š”๋‹ค. ์š”์ฒญ ์‹œ์—๋Š” ์—…๋กœ๋“œํ•  ํŒŒ์ผ์˜ ๋””๋ ‰ํ† ๋ฆฌ ๊ฒฝ๋กœ(directory)์™€ ํŒŒ์ผ๋ช…(fileName)์„ ํ•จ๊ป˜ ์ „๋‹ฌํ•œ๋‹ค.

POST /api/s3/presigned-url
{
  "directory": "profile",
  "fileName": "profile_user1"
}

์‘๋‹ต์œผ๋กœ ๋‹ค์Œ๊ณผ ๊ฐ™์€ presigned URL์„ ๋ฐ›๋Š”๋‹ค. ์ด presigned URL์€ PUT ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ์œ ํšจ ์‹œ๊ฐ„ ๋‚ด์— S3์— ์ง์ ‘ ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ค€๋‹ค.

{
  "presignedUrl": "https://[your-bucket].s3.ap-northeast-2.amazonaws.com/profile/profile_user1?X-Amz-Algorithm=..."
}

(2) Presigned URL์„ ์‚ฌ์šฉํ•ด S3์— ํŒŒ์ผ ์—…๋กœ๋“œ

๋ฐœ๊ธ‰๋ฐ›์€ Presigned URL๋กœ PUT ์š”์ฒญ์„ ๋ณด๋‚ด ๊ฐ์ฒด(์ด๋ฏธ์ง€) ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•œ๋‹ค.

PUT 
https://[your-bucket].s3.ap-northeast-2.amazonaws.com/profile/profile_user1?X-Amz-...

์š”์ฒญ์ด ์„ฑ๊ณตํ•˜๋ฉด ํ•ด๋‹น ์ด๋ฏธ์ง€๊ฐ€ S3 ๋ฒ„ํ‚ท์— ์ •์ƒ ์—…๋กœ๋“œ๋œ๋‹ค.

(3) Presigned URL์—์„œ ๊ฐ์ฒด URL ์ถ”์ถœ

Presigned URL์€ ์ธ์ฆ์šฉ ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ํฌํ•จํ•˜๋ฏ€๋กœ,
์‹ค์ œ ์ด๋ฏธ์ง€ ์ ‘๊ทผ์„ ์œ„ํ•œ ๊ฐ์ฒด URL์€ ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ œ๊ฑฐํ•œ ํ˜•ํƒœ๋กœ ์‚ฌ์šฉํ•œ๋‹ค.

Presigned URL:
https://[your-bucket].s3.ap-northeast-2.amazonaws.com/profile/profile_user1?X-Amz-Algorithm=...


โ†’ ๊ฐ์ฒด URL:
https://[your-bucket].s3.ap-northeast-2.amazonaws.com/profile/profile_user1

  • ์ด ๊ฐ์ฒด URL์€ ํ”„๋ก ํŠธ์—”๋“œ์—์„œ Presigned URL์—์„œ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ œ๊ฑฐํ•ด ์ถ”์ถœํ•˜๊ฑฐ๋‚˜, ๋ฐฑ์—”๋“œ๊ฐ€ ๋ณ„๋„ ์ƒ์„ฑ ๋กœ์ง์„ ๊ตฌํ˜„ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

  • ์ด๋ ‡๊ฒŒ ์ถ”์ถœ๋œ ๊ฐ์ฒด URL์„ ํ”„๋ก ํŠธ์—์„œ ๋ฐฑ์—”๋“œ๋กœ ์ „๋‹ฌํ•˜๋ฉด, ๋ฐฑ์—”๋“œ๋Š” ์ด๋ฅผ DB์— ์ €์žฅํ•˜์—ฌ ์ด๋ฏธ์ง€ ์‹๋ณ„ ๋ฐ ์ ‘๊ทผ ๊ฒฝ๋กœ๋กœ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

(4) ๊ฐ์ฒด URL์„ ํ™œ์šฉํ•œ ํ›„์† API ์š”์ฒญ

์ดํ›„ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ๊ฐ€ ํ•„์š”ํ•œ ๋‹ค๋ฅธ API์—์„œ๋Š”, ์œ„์—์„œ ์ถ”์ถœํ•œ ๊ฐ์ฒด URL ๋ฌธ์ž์—ด์„ ์š”์ฒญ ๋ณธ๋ฌธ์— ํฌํ•จํ•˜์—ฌ ์ „๋‹ฌํ•œ๋‹ค.

POST /api/user/profile

{
  "userId": "user1",
  "profileImageUrl": "https://[your-bucket].s3.ap-northeast-2.amazonaws.com/profile/profile_user1"
}

์ด ๋ฐฉ์‹์œผ๋กœ ๋‹ค์–‘ํ•œ ์—”๋“œํฌ์ธํŠธ์—์„œ ์ด๋ฏธ์ง€ ๋“ฑ์˜ ๊ฐ์ฒด URL์„ ์ผ๊ด€์„ฑ ์žˆ๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

๐Ÿงฉ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

Presigned URL์€ ๋ฐฑ์—”๋“œ ์„œ๋ฒ„๊ฐ€ ์ง์ ‘ ํŒŒ์ผ์„ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š๊ณ ๋„,
์•ˆ์ •์ ์ด๊ณ  ๋ณด์•ˆ์„ฑ ์žˆ๋Š” ์—…๋กœ๋“œ ๊ตฌ์กฐ๋ฅผ ์„ค๊ณ„ํ•  ์ˆ˜ ์žˆ๋Š” ์•„์ฃผ ์ข‹์€ ๋ฐฉ๋ฒ•์ด๋‹ค.

๋”ฐ๋ผ์„œ S3 ์—…๋กœ๋“œ๊ฐ€ ํ•„์š”ํ•œ ํ”„๋กœ์ ํŠธ๋ผ๋ฉด,
"์ง์ ‘ ๋ฐ›์ง€ ๋ง๊ณ , URL๋งŒ ์ฃผ์ž."

profile
๋ฐฑ์—”๋“œ๊ฐœ๋ฐœ์ž์˜ ๊ฐœ๋ฐœ ๊ธฐ๋ก ๋„์ ๋„์ โœ๏ธ

0๊ฐœ์˜ ๋Œ“๊ธ€