S3객체는 개인 정보가 포함될 수 있으니 비공개로 두고, 필요한 순간에만 임시 접근 권한을 주는 방식이 더 안전함
그 임시 출입증이 Presigned URL임
S3 Presigned URL
- AWS가 서명해서 만들어주는 유효기간이 있는 임시 URL
- S3 비공개 이미지: 잠긴 창고 안의 물건
- 일반 사용자: 창고 열쇠가 없음
- Presigned URL: 50분 동안만 사용할 수 있는 임시 출입증
https://bucket.s3.amazonaws.com/uploads/image.png
?X-Amz-Algorithm=...
&X-Amz-Credential=...
&X-Amz-Date=...
&X-Amz-Expires=3000
&X-Amz-Signature=...
<흐름>
사용자 이미지
-> Presinged PUT URL로 업로드
-> AWS S3 원본 이미지 저장
-> Presinged GET URL로 다운로드
-> SPRING BOOT 백엔드
-> SPRING BOOT에서 이미지를 BASE64로 변환해서 cloudinary에 업로드
-> cloudinary에서 공개 https 이미지 url을 spring boot로 반환
-> springboot 백엔드가 image_url로 qwen-vl에 전달

Qwen-vl은 이미지 어떻게 읽어?
-> 이미지 url 읽을 수 있음
{
"type": "image_url",
"image_url": {
"url": "https://example.com/problem.png"
}
}
- qwen 서버가 해당 url에 직접 접속해서 이미지를 다운로드 함
즉, 우리 백엔드가 qwen에 url 전달
-> qwen 서버가 그 url에 http 요청
-> 이미지를 다운로드해서 분석
Cloudinary
- 이미지와 동영상을 저장하고 전달하는 서비스
- 이미지를 cloudinary에 업로드하면 아래와 같은 https url을 돌려줌
=> https://res.cloudinary.com/cloud-name/image/upload/qwen_123.png
- 해당 주소를 브라우저나 qwen에 전달하면 이미지를 바로 가져갈 수 있음
S3랑 Cloudinary의 다른 점은?
- S3는 범용적인 비공개 원본 파일 저장
- Cloudinary는 이미지 업로드, 변환, 전달에 특화
즉, 원본은 S3, QWEN 전달용 복사본은 Cloudinary
Cloudinary Relay
- relay : 중계
<과정>
S3 이미지
↓
Spring Boot가 다운로드
↓
Cloudinary에 다시 업로드
↓
Cloudinary URL을 Qwen에 전달
즉, cloudinary가 S3 URL을 자동으로 연결한 것이 아니라,
spring boot 백엔드가 중간에서 직접 다운로드하고 다시 업로드한 것
=> spring boot가 s3 이미지와 cloudinary 사이에서 relay 역할을 수행함
Base64
- 이미지는 원래 바이너리 데이터임
- Base64는 바이너리 데이터를 일반 문자열로 바꾸는 인코딩 방식
- 이렇게 바꾸면 데이터 크기가 일반적으로 약 33% 증가함
< 실제 흐름 >
1. 프론트엔드가 api 호출
GET /s3/presigned?count=2
2. 백엔드가 문제 이미지와 학생 풀이 이미지에 대해 반환
{
"uploadUrls": [
"Presigned PUT URL 1",
"Presigned PUT URL 2"
],
"downloadUrls": [
"Presigned GET URL 1",
"Presigned GET URL 2"
],
"s3Key": "uploads/uuid.png"
}
3. 프론트엔드가 PUT URL로 이미지를 S3에 업로드 함
- 이미지 파일이 서버를 거치지 않으므로 백엔드의 업로드 트래픽을 줄일 수 있음
4. 프론트엔드는 백엔드의 AI 작업을 시작하는 API에 다운로드 URL과 S3 KEY를 전달함
{
"downloadUrls": [
"Presigned GET URL 1",
"Presigned GET URL 2"
],
"s3Key": "uploads/uuid.png"
}
5. SPRING BOOT가 이미지를 다운로드
new URL(presignedUrl).openStream()
6. SPRING BOOT가 CLOUDINARY에 업로드
이미지 바이트
→ Base64
→ data:image/png;base64,...
→ Cloudinary 업로드
7. Cloudinary가 url 반환
8. url을 qwen에 전달
근데 왜 S3에서 Cloudinary로 한 번 더 올림?
- Qwen에 복잡한 서명이 붙은 Presigned URL 대신에 이미지 전달에 특화된 일반 HTTPS URL을 제공하기 위해서임
- 외부 모델과의 호환성을 확보하기 쉬워서
공개 HTTPS URL?
- 로그인이나 AWS 권한 없이 인터넷에 접근할 수 있고, HTTLS로 암호화돼서 전송되는 이미지 주소
사용자가 S3에 올림 -> 백엔드가 S3에서 읽음 -> Cloudinary에 복사 -> Cloudinary 주소를 qwen에 줌
S3PresignedURL을 Qwen2.5-VL에 직접 전달했을 때 외부 AI모델이 이미지에 안정적으로 접근하지 못하는 문제?
- 기존 방식
S3 Presigned URL
https://s3.../image.png
?X-Amz-Credential=...
&X-Amz-Expires=...
&X-Amz-Signature=...
↓ Qwen에 직접 전달
Qwen 서버가 URL을 다시 요청
↓
서명 URL Fetch 호환성 문제
↓
이미지 분석 실패
=> S3PresignedURL을 그대로 qwen에 직접 전달했을 때 S3PresignedURL에 긴 서명 파라미터와 만료 시간이 포함되어 있어서 이것을 qwen이 처리하는 과정에서 호환성 문제가 있었음.
=> 즉, qwen의 서버 측 이미지 fetch 모듈이 서명 파라미터와 만료시간이 포함된 임시 url을 안정적으로 처리하지 못했음
그럼 왜 백엔드 proxy를 안 만들었나?
- 백엔드 proxy 만들면 qwen의 이미지 요청 트래픽을 서버가 계속 받아야하기 때문
'프로젝트 develop' 카테고리의 다른 글
| 비동기 처리 구조와 @Async (0) | 2026.07.19 |
|---|---|
| 동기 vs 비동기 (0) | 2026.07.13 |
| [develop]develop plan (0) | 2026.07.02 |