| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
- 로그백
- 공통규약
- DDD
- 커뮤니티서버
- model
- MVC
- springsecurity
- 사면초가
- 냄새라도
- 이벤트핸들러등록
- SEQUENCE
- 전전긍긍
- index
- AOP
- Transaction
- JPA
- string
- 문제선택근거
- 설정세팅
- 공통기술
- 디스크i/o
- TDD
- Ajax
- JDBC
- OOP
- 단위테스트
- 클린아키텍처
- 이벤트핸들러this
- SpringLegacy
- MVC요청플로우
- Today
- Total
개발이군고구마
[전전긍긍 설정세팅] 3. 2단계 파일 업로드 (#공통기술) 본문
작년 소위 "그놈의 마이페이지" 라며 여럿 주니어들을 골로 가게 했던 프로젝트에서 나는 1:1 문의 상담 파트를 맡았다.
이미지를 받아오고 메타데이터로 변환하는 지저분한 코드를 나름대로 깔끔하게 정리해서 잘 마무리 했다고 생각했으나...
왠걸 이번 사이드 프로젝트에서 팀원분이 제안한 파일 업로드 시퀀스 다이어그램을 보고 "전혀" 이해하지 못했다.
파일 업로드 시퀀스 다이어그램은 실무에서 사용하고 있는 방식과 거의 유사한 패턴이었는데 내가 했던 방식과 조금 벗어나면 이렇게 생소하게 느껴질 줄이야.
작업 '원리' 를 이해하지 못한 상태에서 개발을 한게 치명적인 문제였다는 것을 깨달았다.
2년이 넘도록 회사 코드에 갇혀있다 전혀 다른 환경에서 사이드 프로젝트를 진행하며 문득 궁금했던 것이 있다.
쓰나미처럼 몰려오는 테크 정보들과 신기술 사이에서 엔지니어들은 어떻게 살아남아야하는 것인가?
죽는 날 그날까지 모든 기술들을 학습하고 섭렵해야만 좋은 엔지니어라고 말할 수 있는가?
답답한 마음에 시니어 아키텍처 분께 상담을 했고, 그 대답이 바로 내가 이번 '파일 업로드 사건' 에서 깨달았던 게 아닌가 싶다.
결국 모든 기술들의 근본 은 똑같습니다.
근본에 대한 제대로된 이해만 있다면 새로 나온 기술들은 거기서 약간 변형을 한 것 뿐이에요.
내가 짜고 있는 이 코드들에 대해 정말로 이해했다고 말할 수 있을까?
레거시 코드, 오래된 기술이라 터부시 하며 무시하고 있었던 것이 아닐까?

하늘을 보고 공부를 하는 것 좋되,
지금 맡은 업무, 작업하고 있는 로직과 코드들부터 분석할 생각부터 하자. 리팽토링을 하는 것도 배우는 것일테니까.

1. 통합 업로드 방식
1) 정의와 사용법
하나의 API 안에 json (body로 담아온 정보)와 file 이미지가 함께 전달되어 저장되는 방식
const formData = new FormData();
formData.append("file", file);
formData.append("title", title);
formData.append("content", content);
fetch("/api/post", { method: "POST", body: formData });
@PostMapping("/post")
public PostResponse createPost(@RequestParam("file") MultipartFile file,
@RequestParam("title") String title,
@RequestParam("content") String content) {
// 1. S3에 파일 업로드
String s3Url = s3Service.uploadFile(file);
// 2. 게시글 DB 저장
Post post = new Post();
post.setTitle(title);
post.setContent(content);
post.setFileUrl(s3Url);
postRepository.save(post);
return new PostResponse(post);
}
현재 우리 사이트의 방식이 바로 통합 업로드 방식이다.
우선 파일 첨부를 클릭하여 파일을 첨부하면
-> 해당 파일은 S3 /temp 폴더에 임시로 저장되고 (일종의 더미데이터)
-> file 객체에서 받아온 메타 데이터를 form 에 담아주고
-> 저장 버튼을 클릭할 때 다시 file + json 을 "함께" 서버에 전달해준후
-> file은 다시 S3 /cont 영구 폴더에 저장
-> 메타데이터 + 일반 데이터는 DB 에 저장되는 방식
2) 장점과 단점
❓ 해당 프로세스의 장점은 무엇일까
- 우선 임시 데이터 관리를 S3 /temp 만 해줘도 된다는 게 크게 이점으로 보인다.
- 2단계 업로드를 하려면 파일 관련 데이터에도 메타데이터가 업로드가 될텐데 (물론 외부 모듈을 사용해야하긴 하다) DB는 따로 관리를 해줄 필요가 없다는 게 장점
❓ 그렇다면 단점은 무엇일까
- S3 버킷을 하나 더 두어야한다는 것이 단점.
- 또한 단일 API에 너무나 큰 책임이 있다는 것도 문제이고 성격이 다른 행동들이 하나의 목적에 너무나 많이 밀집되어있다.
3) 다이어그램

2. 두단계 업로드 방식
1) 정의와 사용법
업무의 성격에 맞게 API를 사용하고 파일은 파일 / 정보는 정보 나누어서 저장하는 방식
// 1단계: 파일 업로드
const formData = new FormData();
formData.append("file", file);
const **s3Url** = await (await fetch("/api/upload", { method: "POST", body: formData })).text();
// 2단계: S3 경로와 추가 정보 등록
fetch("/api/post", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title, content, fileUrl: s3Url }),
})
@PostMapping("/upload")
public String uploadFile(@RequestParam("file") MultipartFile file) {
return s3Service.uploadFile(file); // S3 URL 반환
}
@PostMapping("/post")
public PostResponse createPost(@RequestBody PostRequest request) {
Post post = new Post();
post.setTitle(request.getTitle());
post.setContent(request.getContent());
post.setFileUrl(request.getFileUrl()); // 이미 S3에 올라간 파일의 경로
postRepository.save(post);
return new PostResponse(post);
}
2) 장점과 단점
❓ 해당 프로세스의 장점은 무엇일까
- 역할을 분리하며 파일은 파일, 정보는 정보로 나누어서 API를 관리할 수 있다는 것이 장점
- 하지만 2step file에 대한 메타데이터 + 모듈 정보 를 전달받을 때, 외부 모듈 (=여기서는 파일일 수도) 를 사용할 수 밖에 없기는 함
- API 가 분리가 되어서 확장성, 수정 등에 유리함
- S3 버킷 자원을 덜 사용할 수 있음
❓ 해당 프로세스의 단점은 무엇일까
- 처음 파일을 등록할 때 DB 에 메타데이터를 함께 저장을 한다. 이때 파일 생명주기를 관리하기 위한 속성 값을 가지고 있는데,
- 실제 저장을 할 때 이 속성 값을 변경해야하고 (이 부분이 단일에서 S3 버킷 /cont 에 저장과 비슷한 맥락) + Disk I/O
- 추후에 생명주기도 DB 까지 함께 관리해줘야 하므로 배치 구현이나 DB 관리를 해야한다는 것이 단점
3) 다이어그램

3. 단일 업로드 방식과 두단계 업로드 방식의 비교
| 통합 업로드 | 두단계 업로드 | |
| API 구조 | 파일과 정보 한 번에 다룸 | 파일 업로드와 DB 등록을 분리 |
| 확장/유지보수 | 단순하지만 유연성 떨어짐 | 여러 파일 처리, 비동기 작업 등 확장 유리 |
| 코드/로직 분리 | 한 군데서 몰아서 처리 | 역할별 분리, 책임 분리(클린 아키텍처 유리) |
4. 이미지를 저장하는 방식
1) 서버가 저장
프론트에서 file을 Multipart 형태로 넘겨주면 서버가 S3 (=이미지 저장소)에 직접 인터페이스 연결을 해서 저장하는 방식을 뜻한다.
2) 프론트가 저장 : presigend url 방식
단순히 서버는 프론트가 S3에 접근할 수 있는 승인된 url을 주는 방식.
이 url을 가지고 프론트에서 S3에 접근하여 파일을 업로드 한다.
(백엔드 서버에서 트래픽 병목 현상이 발생하지 않기 때문에, 대용량 파일 업로드에 적합하다)
1) 프론트 → 백엔드: 업로드용 presigned URL 요청
2) 백엔드 → 프론트: presigned URL 발급
3) 프론트 → S3: presigned URL로 파일 직접 업로드
즉,
[사용자]
|
v
[프론트엔드] --(presigned url 요청)--> [백엔드 서버]
<--(presigned url 반환)--
|
v
[프론트엔드] --(파일 직접 업로드)--> [AWS S3]
@GetMapping("/presigned-url")
public String getPresignedUrl(@RequestParam String fileName) {
GeneratePresignedUrlRequest request =
new GeneratePresignedUrlRequest("bucket-name", fileName)
.withMethod(HttpMethod.PUT)
.withExpiration(Date.from(Instant.now().plus(15, ChronoUnit.MINUTES)));
return amazonS3.generatePresignedUrl(request).toString();
}
// 1. 서버에서 presigned url 받기
const res = await fetch("/presigned-url?fileName=test.jpg");
const url = await res.text();
// 2. presigned url로 S3에 직접 파일 업로드
await fetch(url, { method: "PUT", body: file });
3) 비교
현재 사이트는 각각 이미지 1개씩을 저장하는 요구사항을 가지고 있다.
대용량 파일 기능은 오버스펙이 될 수 있기 때문에, 비교적 코드 구현이 간편한 서버 의존 방식으로 파일을 업로드 하도록 한다.
5. 남아있는 이미지 정리 방법
1) S3 LifeCycle - SDK tagging
SDK : S3와 쉽게 통신하는 AWS 제공 개발 도구(Java용 aws-java-sdk-s3 등)
Tagging : S3 파일에 추가 정보(key-value) 달기
-> 임시파일과 최종파일을 구분해서 관리
-> LifeCycle 정책에서 특정 태그가 붙은 파일만 자동 삭제
// 1. 태그 정보 만들기
ObjectTagging tagging = new ObjectTagging(Arrays.asList(
new Tag("status", "temp"), // 임시파일임을 표시
new Tag("owner", "jeeyon") // 업로더 정보 등
));
// 2. 파일 업로드할 때 태깅 포함해서 요청
PutObjectRequest putObjectRequest = new PutObjectRequest(
bucketName, fileName, fileInputStream, objectMetadata
).withTagging(tagging);
amazonS3.putObject(putObjectRequest); // 파일 업로드 + 태깅
// -> 업로드 성공시 S3의 해당 객체(파일)에 태그가 함께 저장됨!
2) DB 속성 값으로
(배치) DB에 저장된 속성값 중 임시 파일의 여부와 생성 날짜를 비교하여 DB와 S3의 파일을 삭제함

6. S3가 파일을 업로드 하게 되면 반환되는 값이 있을까?
파일 업로드 후에 특별히 반환되는 값은 없음
-> 따라서 메타데이터는 file 객체에서 추출한 속성값을 저장하게 됨
but 업로드가 성공하면 파일의 S3 URL을 알 수 있음
String s3Url = amazonS3.getUrl(bucketName, fileName).toString();
'SERVER > Architecture' 카테고리의 다른 글
| [전전긍긍 설정세팅] 5. Profile별 설정 과 보안 파일 관리 (#공통규약) (0) | 2025.05.22 |
|---|---|
| [전전긍긍 설정세팅] 4. 어노테이션과 BaseEntity/QueryDSL (#공통규약 #공통기술) (5) | 2025.05.22 |
| [전전긍긍 설정세팅] 2. Swagger API 명세 (#공통규약) (0) | 2025.05.19 |
| [사면초가 TDD/OOP] 1. 구현 원칙 확립 (나는 나의 역할 만을 담당한다) (5) | 2025.05.19 |
| [전전긍긍 설정세팅] 1. Rest API 규격 (#공통규약) (6) | 2025.05.18 |