RMRT 이미지 관리 시스템은 AWS S3 기반의 안전하고 확장 가능한 이미지 업로드/조회/삭제 기능을 제공합니다. Presigned URL 방식을 사용하여 서버 부하를 최소화하고, 헥사고날 아키텍처를 통해 높은 유지보수성을 확보했습니다.
- AWS S3 Presigned PUT URL 방식 사용
- 클라이언트가 S3에 직접 업로드하여 서버 부하 최소화
- 업로드 완료 후 데이터베이스에 메타데이터 저장
- 실시간 업로드 진행률 표시
- Presigned GET URL을 통한 안전한 이미지 접근
- 이미지 캐러셀 UI 지원
- 사용자별 이미지 목록 조회
- 소프트 삭제 방식 (is_deleted 플래그)
- 사용자 권한 검증
- 게시글 연동 이미지 관리
┌─────────────────────────────────────────────────────────────┐
│ Adapter Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ ImageApi │ │ ImageView │ │ S3PresignedUrl │ │
│ │ (REST API) │ │ (Thymeleaf) │ │ Generator │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Application Layer │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Provided Ports (Inbound) │ │
│ │ - ImageUploadRequester │ │
│ │ - ImageUploadTracker │ │
│ │ - ImageReader │ │
│ │ - ImageDeleter │ │
│ │ - ImageKeyGenerator │ │
│ │ - ImageUploadValidator │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Required Ports (Outbound) │ │
│ │ - ImageRepository │ │
│ │ - PresignedUrlGenerator │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Domain Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Image │ │ ImageType │ │ FileKey │ │
│ │ (Entity) │ │ (Enum) │ │ (Value Object) │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Image: 이미지 도메인 엔티티 (업로드자, 이미지 타입, 삭제 여부 관리)ImageType: 이미지 타입 (PROFILE_IMAGE, POST_IMAGE, THUMBNAIL)FileKey: S3 파일 키 값 객체 (경로 안전성 검증)
Provided Ports (애플리케이션이 제공하는 기능)
ImageUploadRequester: Presigned URL 요청ImageUploadTracker: 업로드 완료 추적 및 메타데이터 저장ImageReader: 이미지 조회 및 URL 생성ImageDeleter: 이미지 삭제ImageKeyGenerator: 안전한 파일 키 생성ImageUploadValidator: 업로드 요청 검증
Required Ports (애플리케이션이 필요로 하는 기능)
ImageRepository: 이미지 영속성 관리PresignedUrlGenerator: AWS S3 Presigned URL 생성
ImageApi: REST API 엔드포인트ImageView: Thymeleaf 뷰 모델S3PresignedUrlGenerator: AWS S3 연동 구현체
sequenceDiagram
participant Client
participant Backend
participant S3 as AWS S3
Note over Client, S3: 1. Presigned URL 요청 단계
Client ->> Backend: POST /api/images/upload-request
Note right of Backend: 1. 사용자 검증<br/>2. 업로드 제한 확인<br/>3. 이미지 메타데이터 검증<br/>4. 파일 키 생성
Backend ->> S3: Presigned PUT URL 생성 요청
S3 -->> Backend: Presigned PUT URL
Backend -->> Client: PresignedPutResponse<br/>- uploadUrl<br/>- key<br/>- expiresAt<br/>- metadata
Note over Client, S3: 2. 이미지 업로드 단계
Client ->> S3: PUT {uploadUrl}<br/>with metadata headers
Note right of S3: 파일 저장
S3 -->> Client: 200 OK
Note over Client, S3: 3. 업로드 확인 단계
Client ->> Backend: POST /api/images/upload-confirm<br/>key={fileKey}
Note right of Backend: 1. Image 엔티티 생성<br/>2. DB 저장
Backend -->> Client: ImageUploadResult<br/>- success: true<br/>- imageId: 123
sequenceDiagram
participant Client
participant Backend
participant S3 as AWS S3
Client ->> Backend: POST /api/images/upload-request
activate Backend
Note right of Backend: 사용자 검증
Note right of Backend: 업로드 제한 확인
Note right of Backend: 이미지 메타데이터 검증
Note right of Backend: 파일 키 생성
Backend ->> S3: Presigned PUT URL 생성 요청
activate S3
S3 -->> Backend: Presigned PUT URL
deactivate S3
Backend -->> Client: PresignedPutResponse<br/>- uploadUrl<br/>- key<br/>- expiresAt<br/>- metadata
deactivate Backend
sequenceDiagram
participant Client
participant S3 as AWS S3
Client ->> S3: PUT {uploadUrl}<br/>with metadata headers
activate S3
Note right of S3: 파일 저장
S3 -->> Client: 200 OK
deactivate S3
sequenceDiagram
participant Client
participant Backend
participant DB as Database
Client ->> Backend: POST /api/images/upload-confirm<br/>key={fileKey}
activate Backend
Backend ->> Backend: Image 엔티티 생성
Backend ->> DB: 이미지 메타데이터 저장
activate DB
DB -->> Backend: 저장 완료
deactivate DB
Backend -->> Client: ImageUploadResult<br/>- success: true<br/>- imageId: 123
deactivate Backend
CREATE TABLE images
(
id BIGINT AUTO_INCREMENT PRIMARY KEY,
file_key VARCHAR(512) NOT NULL UNIQUE COMMENT 'S3 파일 키',
uploaded_by BIGINT NOT NULL COMMENT '업로드한 사용자 ID',
image_type VARCHAR(20) NOT NULL COMMENT '이미지 타입 (PROFILE_IMAGE, POST_IMAGE, THUMBNAIL)',
is_deleted BOOLEAN NOT NULL DEFAULT FALSE COMMENT '삭제 여부',
created_at DATETIME(6) NOT NULL COMMENT '생성 일시',
updated_at DATETIME(6) NOT NULL COMMENT '수정 일시',
INDEX idx_uploaded_by (uploaded_by),
INDEX idx_image_type (image_type),
FOREIGN KEY (uploaded_by) REFERENCES members (id)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci;idx_uploaded_by: 사용자별 이미지 조회 성능 최적화idx_image_type: 이미지 타입별 필터링 성능 최적화
Endpoint: POST /api/images/upload-request
Request Body:
{
"fileName": "photo.jpg",
"contentType": "image/jpeg",
"fileSize": 1024000,
"imageType": "POST_IMAGE",
"width": 1920,
"height": 1080
}Response:
{
"uploadUrl": "https://bucket.s3.region.amazonaws.com/path/to/file?X-Amz-...",
"key": "posts/123/uuid-photo.jpg",
"expiresAt": "2025-11-30T12:00:00Z",
"metadata": {
"original-name": "photo.jpg",
"content-type": "image/jpeg",
"file-size": "1024000",
"width": "1920",
"height": "1080"
}
}Endpoint: POST /api/images/upload-confirm?key={fileKey}
Response:
{
"success": true,
"imageId": 123
}Endpoint: GET /api/images/{imageId}/url
Response:
{
"url": "https://bucket.s3.region.amazonaws.com/path/to/file?X-Amz-..."
}Endpoint: GET /api/images/my-images
Response:
[
{
"imageId": 123,
"fileKey": "posts/123/uuid-photo.jpg",
"imageType": "POST_IMAGE",
"createdAt": "2025-11-30T10:00:00Z"
}
]Endpoint: DELETE /api/images/{imageId}
Response:
{
"message": "이미지가 성공적으로 삭제되었습니다"
}- 일일 업로드 횟수 제한 (기본값: 100개)
- 파일 크기 제한 (최대 5MB)
- 지원 이미지 형식: JPEG, PNG, GIF, WebP
- UUID 기반 고유 파일명 생성
- 경로 탐색 공격 방지 (
..검증) - 사용자별 디렉토리 분리
- 인증된 사용자만 업로드 가능
- 업로드한 사용자만 삭제 가능
- Presigned URL 유효 시간 제한 (기본값: 15분)
- 민감정보 로깅 제거
- S3 메타데이터 헤더를 통한 파일 정보 전송
- CSRF 보호 적용
- Testcontainers + LocalStack: 로컬 S3 환경 구축
- CORS 설정: 테스트 환경에서 클라이언트 업로드 지원
- 격리성: 테스트별 독립적인 S3 버킷 사용
- 도메인 로직 테스트 (Image, FileKey, ImageType)
- 애플리케이션 서비스 테스트 (업로드, 조회, 삭제)
- API 통합 테스트 (MockMvc)
- S3 연동 테스트 (LocalStack)
// 도메인 테스트
-Image 생성 및 권한 검증
-FileKey 경로 안전성 검증
-이미지 타입별 기능 테스트
// 애플리케이션 테스트
-Presigned URL 생성
-업로드 완료 추적
-일일 업로드 제한 검증
-이미지 삭제 권한 검증
// API 테스트
-REST API 엔드포인트 테스트
-예외 처리 테스트
-보안 검증 테스트aws:
s3:
access-key: ${AWS_ACCESS_KEY_ID}
secret-key: ${AWS_SECRET_ACCESS_KEY}
region: ${AWS_REGION:ap-northeast-2}
bucket-name: ${S3_BUCKET_NAME}
image:
upload:
expiration-minutes: 15 # Presigned PUT URL 유효 시간
get:
expiration-minutes: 15 # Presigned GET URL 유효 시간AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
AWS_REGION=ap-northeast-2
S3_BUCKET_NAME=your-bucket-name<!-- image-upload.html 프래그먼트 -->
<div th:fragment="image-upload">
<!-- 파일 선택 -->
<input type="file" multiple accept="image/*"/>
<!-- 업로드 진행률 -->
<div class="progress">
<div class="progress-bar" role="progressbar"></div>
</div>
<!-- 미리보기 -->
<div class="preview-container"></div>
</div><!-- image-carousel.html 프래그먼트 -->
<div th:fragment="carousel" class="carousel slide">
<div class="carousel-inner">
<div
th:each="imageUrl, iterStat : ${imageUrls}"
th:classappend="${iterStat.first} ? 'active'"
class="carousel-item"
>
<img th:src="${imageUrl}" class="d-block w-100"/>
</div>
</div>
<!-- 컨트롤 버튼 -->
<button class="carousel-control-prev" type="button"></button>
<button class="carousel-control-next" type="button"></button>
</div>- 버전 관리된 스키마 변경 추적
- CI/CD 파이프라인 통합
- 롤백 지원
-- 초기 스키마에 images 테이블 포함
-- 인덱스 및 외래키 제약조건 자동 생성- 클라이언트가 S3에 직접 업로드
- 서버는 URL 생성과 메타데이터 관리만 담당
- 인덱스 활용한 빠른 조회
- N+1 문제 방지 (findByIds 배치 조회)
- S3 Presigned GET URL을 CloudFront와 연동 가능
- 이미지 캐싱으로 응답 속도 개선
- 썸네일 자동 생성 (Lambda)
- 이미지 리사이징
- WebP 자동 변환
- CloudFront CDN 연동
- 이미지 캐싱 전략
- 레이지 로딩
- 이미지 편집 기능
- 이미지 태깅
- 중복 이미지 감지