Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
Tags
- 인프런
- 오블완
- table status
- Less
- 맥
- 순차탐색
- CS스터디
- cs
- MySQL
- 알고리즘
- 이진탐색
- 분할정복 방법
- 오일러 경로
- spring boot
- mysql 표 출력
- 네트워킹데이
- 인프런워밍업클럽
- mycli
- 스터디2기
- zsh
- VI
- 터미널
- oh-my-zsh
- 티스토리챌린지
- zsh theme
- 동적 프로그래밍 방법
- Pager
- 욕심쟁이 방법
- mysql 표
- 데이크스트라
Archives
- Today
- Total
Develop
[Spring Boot] 게시판 - 09. 게시글 이미지 업로드 구현하기 (feat. 이미지 여러개 업로드 & 순서 관리) 본문
백엔드/게시판 만들기
[Spring Boot] 게시판 - 09. 게시글 이미지 업로드 구현하기 (feat. 이미지 여러개 업로드 & 순서 관리)
230801 2025. 11. 30. 23:56안녕하세요 .ᐟ
지난 시간에 파일 업로드 기본 구조를 만들었다면, 오늘은 게시글에 여러 이미지를 첨부하는 기능을 구현해보겠습니다.
실제 서비스처럼 게시글당 최대 10개의 이미지를 업로드하고, 순서를 관리하며, 삭제까지 할 수 있도록 만들어 보겠습니다.
구현할 기능
- 게시글 복수 이미지 업로드 (최대 10개)
- 이미지 순서 관리 (displayOrder)
- 이미지 목록 조회
- 개별 이미지 삭제
- 이미지 파일 브라우저에서 보기
왜 별도 Entity가 필요한가?
처음엔 Post Entity에 String images 필드를 하나 만들어서 쉼표로 구분하면 되지 않을까? 라고 생각했습니다.
// 안 좋은 설계
private String images; // "image1.jpg,image2.jpg,image3.jpg"
하지만 이렇게 하면 아래 문제가 있기 때문에 별도 테이블로 분리했습니다.
- 이미지 순서를 바꾸기 어려움
- 특정 이미지만 삭제하기 복잡
- 이미지별 메타데이터(원본 파일명, 크기) 저장 불가
- 검색/필터링이 비효율적
PostImage Entity
게시글과 이미지는 1:N 관계입니다. 하나의 게시글에 여러 이미지가 달릴 수 있습니다.
- post: 어느 게시글의 이미지인가? (@ManyToOne)
- filePath: 실제 저장된 파일명 (UUID)
- displayOrder: 이미지 표시 순서 (프론트에서 정렬에 사용)
- originalFilename: 사용자가 업로드한 원본 파일명 (다운로드 시 사용)
- fileSize: 용량 표시 및 통계에 활용
Post Entity에 연관관계 추가
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PostImage> images = new ArrayList<>();
Cascade 옵션의 의미
- CascadeType.ALL: Post 저장/삭제 시 PostImage도 함께 처리
- orphanRemoval = true: Post에서 제거된 이미지는 DB에서도 자동 삭제
post.removeImage(image); // 이것만으로 DB에서도 DELETE!
- orphanRemoval = true 덕분에 Post에서 제거한 이미지가 자동으로 DB에서도 삭제되기 때문에 일일이 repository.delete() 호출할 필요가 없습니다.
PostImageRepository
public interface PostImageRepository extends JpaRepository<PostImage, Long> {
// 게시글의 모든 이미지 조회 (순서대로)
List<PostImage> findByPostIdOrderByDisplayOrder(Long postId);
// 게시글의 이미지 개수 카운트
long countByPostId(Long postId);
}
Spring Data JPA의 메서드 이름 규칙을 따르면 쿼리를 자동 생성해줍니다.
SELECT * FROM post_images
WHERE post_id = ?
ORDER BY display_order;
FileService
복수 이미지 업로드
- 게시글이 존재하는지 확인
- 현재 이미지 개수 + 새로 올릴 개수 체크 (최대 10개)
- 서버 용량 보호, 사용자 경험, 로딩시간 관리를 위해 최대 10개로 제한
- displayOrder 자동 증가시키며 저장
long currentImageCount = postImageRepository.countByPostId(postId);
if (currentImageCount + files.size() > 10) {
throw new FileStorageException("게시글당 최대 10개의 이미지만 업로드할 수 있습니다.");
}
int startOrder = (int) currentImageCount;
for (int i = 0; i < files.size(); i++) {
// displayOrder는 startOrder + i로 자동 증가
}
이미지 삭제
- 해당 게시글의 이미지만 삭제하도록 구현
// 이미지가 정말 이 게시글의 것인지 확인!
if (!image.getPost().getId().equals(postId)) {
throw new UnauthorizedException("해당 게시글의 이미지가 아닙니다.");
}
이미지 파일보기
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"...\"")
.body(resource);
Content-Disposition:
- inline: 브라우저에서 바로 보기
- attachment: 다운로드 강제
DTO 전략 (목록 vs 상세)
PostListResponse
- 목록 조회에서 모든 이미지를 보내게 되면 네트워크 트래픽, 로딩시간이 증가되므로 썸네일과 이미지 개수만 보냅니다.
private String thumbnailPath; // 첫 번째 이미지만
private Integer imageCount; // 개수만 표시
PostDetailResponse
private List<PostImageResponse> images; // 모든 이미지 정보
느낀 점
1. Cascade의 편리함
2. 에러 메시지 분기처리하는게 손이 많이 필요하긴해도 마음이 편하다.
3. DTO를 목록과 상세로 나눠서 쓰면서 성능을 고민하는 계기가 되었다.

'백엔드 > 게시판 만들기' 카테고리의 다른 글
| [Spring Boot] 게시판 - 10. QueryDSL로 동적 검색 기능 구현하기 (0) | 2025.12.05 |
|---|---|
| 초보 백엔드가 무중단 배포와 CI/CD를 직접 구축하면서 만난 에러들 (0) | 2025.12.04 |
| [Spring Boot] 게시판 - 08. 파일 업로드 구현하기 (feat. 프로필 이미지 업로드) (0) | 2025.11.28 |
| [Spring Boot] 게시판 - 07. 통합 테스트 작성하기 (MockMvc & Fixture 패턴) (1) | 2025.11.27 |
| [Spring Boot] 게시판 - 06. 단위 테스트 작성하기 (Mockito & AssertJ) (0) | 2025.11.24 |