2026. 4. 16. 21:42ㆍ대우개발원 수업 내용/spring boot, framework
1. HTTP 기반 파일 업로드 및 MultipartFile 인터페이스 (UploadFileDTO.java, UpDownController.java)
필수 폼 속성: POST 방식, enctype="multipart/form-data", type="file" 적용
MultipartFile: 스프링의 바이너리 파일 데이터 처리 인터페이스
중복 방지: UUID와 원본 파일명을 결합하여 고유명 생성 및 서버 저장
2. 환경 설정 주입 (application.properties, UpDownController.java)
경로 주입: application.properties의 저장 경로(file.upload.path)를 @Value로 컨트롤러 변수에 할당
보안 및 관리: 하드코딩 방지로 민감 정보 노출 예방 및 배포 환경 유지보수성 향상
3. Thumbnailator 기반 썸네일 자동 생성 및 DTO 반환 (build.gradle, UploadResultDTO.java)
라이브러리 적용: 고품질 이미지 리사이징 처리가 가능한 Thumbnailator 도입 (build.gradle 설정)
이미지 선별 및 생성: MIME 타입 검사(image/*) 후 이미지에만 "s_" 접두사를 붙여 썸네일 저장
데이터 반환: UploadResultDTO를 통해 UUID, 파일명, 이미지 여부(img) 상태 결과 전달
4. 파일 제어 REST API 모듈화 (UpDownController.java)
업로드(POST): 서버 경로에 원본 및 썸네일 저장 후 결과 데이터 리스트 반환
조회(GET): 파라미터(fileName)로 파일을 읽어 Content-Type 헤더와 함께 브라우저 출력
삭제(DELETE): 서버의 원본 파일 삭제 진행 및 이미지 타입인 경우 썸네일(s_) 파일 동시 삭제
5. JPA @OneToMany 조인 테이블 이슈 및 mappedBy 최적화 (Board.java, BoardImage.java)
연관관계: Board(1)와 BoardImage(N) 엔티티 간 1:N 양방향 매핑 설정
문제점 식별: mappedBy 생략 시 JPA가 조인 테이블(board_image_set)을 자동 생성하여 성능 저하 유발
구조 최적화: Board.java의 @OneToMany(mappedBy = "board") 설정으로 연관관계 주인을 BoardImage로 위임
결과: 중간 테이블 생성 없이 외래키(FK)만을 활용한 효율적인 DB 매핑 완성
파일 업로드
웹에서는 이 클라이언트/서버 간 요청/응답을 HTTP 프로토콜로 진행
HTTP에서는 파일도 지원
파일업로드 : 클라이언트가 요청에 파일을 포함하고 서버가 요청받은 파일을 처리하는 과정의 일환
클라이언트 : "서버야, 나 Request보낼 때 파일도 포함시켜 보낼게. 이거 서버에 저장해줘“
서버 : "OK. 어디보자. Request에 파일 있군. 알았어 잘 처리했어."의 과정
물론 위의 대화를 HTTP프로토콜에서 처리해야 되는데 이게 생각보다 어려움
우선 파일업로드를 위해선 다음의 3가지 규칙을 꼭 지켜줘야 함
<input type="file" >
<form> 태그 method는 POST
<form> 태그 enctype=multipart/form-data
스프링 파일 업로드(MultipartFile)
스프링 프레임워크에서 파일 업로드는 MultipartFile라는 클라이언트로부터 전송된 파일을 나타내는 인터페이스를 사용.
이 인터페이스는 파일 업로드와 관련된 작업을 수행하며, 주로 웹 애플리케이션에서 HTML 폼을 통해
전송된 파일을 처리하는 데에 쓰임
MultipartFile docs
• multipart인 이유?
일반 양식의 데이터 파트(일반 텍스트 형식)
파일 데이터(바이너리 데이터) 파트
두가지로 분류되기 때문에 멀티파트
MultipartFile 인터페이스
getName() : 넘어온 파라미터 명
getOriginalFilename() : 업로드 파일명
getContentType : 파일의 ContentType
isEmpty() : 업로드된 파일이 비어있는지 확인
getSize() : 파일의 바이트 사이즈
getBytes() : 바이트 배열로 저장된 파일의 내용
getInputStream() : 파일의 내용을 읽기 위한 InputStream 반환
transferTo() : 파일 저장
@Value
설정파일(.properties, .yml)에 설정한 내용을 주입시켜주는 어노테이션.
DB 연결에 필요한 정보(계정 정보)나 노출되기 민감한 값들을 하드 코딩하게 된다면, 여러 가지 이슈에 휘말릴 수 있다.
(깃허브 같이 공유 레퍼지토리에 그대로 코드와 함께 유출될 것이다.)
또한 개발 시엔 로컬에 맞는 환경으로 세팅을 했지만, 클라우드 서버에 올린다거나 배포 환경으로 전환될 때,
직접 해당 코드를 수정해야 하는 번거로움이 있다.
이러한 이슈들을 막기 위해 민감한 정보나, 메타정보들은 파일로 따로 빼두어 관리하게 된다. (수정과 관리가 용이하기 때문)
이러한 이유로 따로 빼둔 설정 파일을 필요한 곳에 주입시켜주는 어노테이션이 @value 다.
@value를 쓰는 방법은 간단하다. @value 안에 파라미터로 EL형식의 키 값을 넣어주면, 불러올 수 있다.
@value("${프로퍼티 키값}")의 형태로 입력하게 되면, 해당 변수에 값이 주입하게 된다.
Thumbnailator
• Java에서 간단하고 편리하게 이미지 썸네일을 생성할 수 있는 라이브러리. 사용하기 쉬운 API를 제공하여 이미지 크기 조정,
회전, 워터마크 추가 등 다양한 이미지 처리 기능을 쉽게 구현할 수 있음.
특히, 고품질 이미지 리사이징 알고리즘을 내장하고 있어, 썸네일 이미지의 품질을 유지하면서 빠르게 처리 가능.
• Thumbnailator의 주요 기능
• 이미지 리사이징 : 원하는 크기로 이미지의 크기를 조절할 수 있음
• 이미지 회전 및 뒤집기 : 이미지를 원하는 각도로 회전시키거나 수평/수직으로 뒤집을 수 있음
• 워터마크 추가 : 이미지 워트마크 (텍스트 또는 이미지)를 추가하여 저작권을 표시할 가능.
• 이미지 형식 변환 : JPEG, PNG 등 다양한 이미지 형식으로 변환 가능.
if(Files.probeContentType(savePath).startsWith("image")){
File thumbFile = new File(uploadPath, "s_" + uuid+"_"+originalName);
Thumbnailator.createThumbnail(savePath.toFile(), thumbFile, 200,200);
}
@OneToMany
- 1:N 관계에서 "1" 쪽에 사용하는 어노테이션
- Collection 타입(List, Map 등)에만 사용이 가능
- @OneToMany(mappedBy="xx") 속성을 사용해 연관관계의 주인을 N(다) 쪽으로 지정해줌
- mappedBy의 값에는 연관된 엔티티의 필드명을 작성
- 마찬가지로 fetchType을 지정해 줄 수 있으며, 기본값은 LAZY
- cascade 옵션을 지정하여, 부모 엔티티의 작업이 자식 엔티티에게 영향을 미치도록 할 수 있음
- orphanRemoval 옵션으로 부모엔티티와의 관계가 끊어진 자식 엔티티를 자동으로 삭제할 수 있음
public class Item {
...
@OneToMany(mapppedBy = "item", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ItemType> itemType = new ArrayList<>();
}
파일 업로드 처리를 실습하기 위해 기존에 있던 b01Rest 파일을 복사해서
b01Upload라는 파일로 만든다
복사 후 가동이 잘되는지 가동 테스트 부터 해줌
application.properties 파일에 내용을 추가
C드라이브에 upload 폴더 생성
업로드 처리를 위해 DTO를 구성
DTO 패키지안에 upload 패키지를 만들고 UploadFileDTO 클래스 파일 생성
Controller패키지에 UpDownController 클래스 생성

package com.example.b01.controller;
import com.example.b01.dto.upload.UploadFileDTO;
import io.swagger.v3.oas.annotations.Operation;
import lombok.extern.log4j.Log4j2;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Log4j2
public class UpDownController {
@Operation(description = "POST 방식으로 파일 업로드")
@PostMapping(value = "/upload",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String upload(UploadFileDTO uploadFileDTO){
log.info(uploadFileDTO);
return null;
}
}
실행해서 Swagger에 접속하면
파일 첨부하는 창이 뜬다
UpDownController
코드를 수정
package com.example.b01.controller;
import com.example.b01.dto.upload.UploadFileDTO;
import io.swagger.v3.oas.annotations.Operation;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Log4j2
public class UpDownController {
@Value("${file.upload.path}") //import 시에 springframework로 시작하는 Value
private String uploadPath;
@Operation(description = "POST 방식으로 파일 업로드")
@PostMapping(value = "/upload",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String upload(UploadFileDTO uploadFileDTO){
log.info(uploadFileDTO);
if(uploadFileDTO.getFiles() != null) {
uploadFileDTO.getFiles().forEach(multipartFile -> {
log.info(multipartFile.getOriginalFilename());
});
}
return null;
}
}
실행하면
로그에도 정상적으로 찍힌다
하지만 실제로 서버에 저장은 안되기 때문에 저장되는것을 구현한다
주의해야할점은 파일이름이 같을 경우 주의해야 한다
그래서 uuid를 발생시켜서 저장을 해야한다
사진 저장
UpDownController
코드를 수정
String originalName = multipartFile.getOriginalFilename();
log.info(originalName);
String uuid = UUID.randomUUID().toString();
Path savePath = Paths.get(uploadPath, uuid+"_"+originalName);
try {
multipartFile.transferTo(savePath); // 실제 파일 저장
} catch (IOException e) {
e.printStackTrace();
}
// log.info(multipartFile.getOriginalFilename());
실행하면
upload폴더에 사진이 저장되고
파일이름이 로그에 정상적으로 뜬다.
썸네일을 표기하기 위해 Thumbnailator 라는 라이브러리 사용
s_가 붙은 썸네일 파일도 생성
우선 bulid.gradle 에 아래 코드 추가
implementation 'net.coobird:thumbnailator:0.4.16'
UpDownController 에 아래 구문 추가
// 이미지 파일의 종류라면 MIME 타입이 "image/*"
// (예: "image/png", "image/jpeg", "image/gif")로 시작하는 경우에만 실행
if(Files.probeContentType(savePath).startsWith("image")){
File thumbFile = new File(uploadPath, "s_" + uuid+"_"+originalName);
Thumbnailator.createThumbnail(savePath.toFile(), thumbFile, 200,200);
}
실행하면
업로드가 정상적으로 되고 s_가 붙은 썸네일 파일도 생성된다.
이미지 파일에만 썸네일 파일이 추가되도록
UploadResultDTO추가

package com.example.b01.dto.upload;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UploadResultDTO {
private String uuid;
private String fileName;
private boolean img;
public String getLink() {
if(img) {
return "s_"+ uuid + "_" + fileName; //이미지인 경우 썸네일
} else {
return uuid + "_" +fileName;
}
}
}
l
이미지 파일에만 썸네일 파일이 추가되도록
UpDownController 코드 수정
package com.example.b01.controller;
import com.example.b01.dto.upload.UploadFileDTO;
import com.example.b01.dto.upload.UploadResultDTO;
import io.swagger.v3.oas.annotations.Operation;
import lombok.extern.log4j.Log4j2;
import net.coobird.thumbnailator.Thumbnailator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@RestController
@Log4j2
public class UpDownController {
@Value("${file.upload.path}") //import 시에 springframework로 시작하는 Value
private String uploadPath;
@Operation(description = "POST 방식으로 파일 업로드")
@PostMapping(value = "/upload",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
// public String upload(UploadFileDTO uploadFileDTO){
public List<UploadResultDTO> upload(UploadFileDTO uploadFileDTO) {
log.info(uploadFileDTO);
if(uploadFileDTO.getFiles() != null) {
final List<UploadResultDTO> list = new ArrayList<>();
uploadFileDTO.getFiles().forEach(multipartFile -> {
//forEach를 쓰는 이유 자료가 몇개일지 알 수 없어서
String originalName = multipartFile.getOriginalFilename();
log.info(originalName);
String uuid = UUID.randomUUID().toString();
Path savePath = Paths.get(uploadPath, uuid+"_"+originalName);
boolean image = false;
try {
multipartFile.transferTo(savePath); // 실제 파일 저장
// 이미지 파일의 종류라면 MIME 타입이 "image/*"
// (예: "image/png", "image/jpeg", "image/gif")로 시작하는 경우에만 실행
if(Files.probeContentType(savePath).startsWith("image")){
image = true;
File thumbFile = new File(uploadPath, "s_" + uuid+"_"+originalName);
Thumbnailator.createThumbnail(savePath.toFile(), thumbFile, 200,200);
}
} catch (IOException e) {
e.printStackTrace();
}
list.add(UploadResultDTO.builder()
.uuid(uuid)
.fileName(originalName)
.img(image)
.build());
// log.info(multipartFile.getOriginalFilename());
}); // end each
return list;
} // end if
return null;
}
}
실행하면

[
{
"uuid": "223dc5d8-0670-4188-b5ae-0fb61226fe36",
"fileName": "썸네일 테스트2.png",
"img": true,
"link": "s_223dc5d8-0670-4188-b5ae-0fb61226fe36_썸네일 테스트2.png"
},
{
"uuid": "a6223e60-9e23-497e-8bee-3c184796a6ed",
"fileName": "썸네일 테스트3(문서).txt",
"img": false,
"link": "a6223e60-9e23-497e-8bee-3c184796a6ed_썸네일 테스트3(문서).txt"
}
]


"img": true / "img": false (img 인지 아닌지 구분함)
이미지 파일만 s_ 로 시작하는 썸네일 파일이 생성되고 문서 파일은 생성되지 않는다
업로드된 파일 조회
UpDownController 에 코드 추가
@Operation(description = "GET방식으로 업로드된 파일 조회")
@GetMapping("/view/{fileName}")
public ResponseEntity<Resource> viewFileGET(@PathVariable String fileName) {
// 파일 시스템에 있는 실제 파일을 Resource 객체로 생성
Resource resource = new FileSystemResource(uploadPath+File.separator+fileName);
String resourceName = resource.getFilename();
HttpHeaders headers = new HttpHeaders();
try { // 파일의 MIME 타입(Content-Type)을 자동으로 감지해서 헤더에 추가
headers.add("Content-Type", Files.probeContentType(resource.getFile().toPath()));
} catch (Exception e) { // 파일 타입을 읽는 중 오류 발생 시 500 에러 발생
return ResponseEntity.internalServerError().build();
}
return ResponseEntity.ok().headers(headers).body(resource);
}
실행하면
fileName이 생기고
해당하는 사진이 보인다
URL로 웹사이트에서 켜봐도 그대로 사진이 뜬다
업로드된 파일 삭제
@Operation(description = "DELETE 방식으로 파일 삭제")
@DeleteMapping("/remove/{fileName}")
public Map<String,Boolean> removeFile(@PathVariable String fileName) {
Resource resource = new FileSystemResource(uploadPath+File.separator+fileName);
log.info(resource);
String resourceName = resource.getFilename();
// 결과를 담을 Map (삭제 성공 여부)
Map<String, Boolean> resultMap = new HashMap<>();
boolean removed = false;
try{
String contentType = Files.probeContentType(resource.getFile().toPath());
log.info(resource.getFile().toPath());
removed = resource.getFile().delete();
// 썸네일이 존재한다면
if(contentType.startsWith("image")) {
File thumbnailFile = new File(uploadPath+File.separator+"s_"+fileName);
thumbnailFile.delete();
}
} catch (Exception e) {
log.error(e);
} // end catch
resultMap.put("result", removed);
return resultMap;
}
실행하면
파일이 삭제된것을 확인할 수 있다
@OneToMany
domain 패키지에 BoardImage 클래스 생성

package com.example.b01.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import lombok.*;
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString(exclude = "board")
public class BoardImage implements Comparable<BoardImage> {
@Id
private String uuid;
private String fileName;
private int ord;
@ManyToOne
private Board board;
@Override
public int compareTo(BoardImage other) { return this.ord - other.ord; }
public void changeBoard(Board board) { this.board = board; }
}
Board.java에 아래 코드 추가
HeidiSQL에 접속
application.properties를 수정
spring.application.name=b01
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://localhost:3306/webdbplus
spring.datasource.username=webuser
spring.datasource.password=1234
logging.level.org.springframework=info
logging.level.com.example=debug
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.show-sql=true
spring.servlet.multipart.enabled=true
spring.servlet.multipart.location=C:\\upload
spring.servlet.multipart.max-request-size=30MB
spring.servlet.multipart.max-file-size=10MB
file.upload.path=C:\\upload
데이터 베이스 연결
기존 접속은 해제 하고



지금은 webdbplus 안에 테이블이 없지만
엔티티로 만들었기 때문에
프로젝트를 실행하면 테이블을 만든다

양 방향 통신에서 board와 board_image의 중간에 있는 테이블이 만들어지는데 이건
OneToMany를 처리하기 위해 매핑테이블인 board_image_set테이블이 자동으로 생성된다

하지만 이게 database를 과하게 사용할 수 있다는 단점이 있기 때문에
단방향을 사용하거나
mappedBY속성을 사용할 수 있다.
정리
JPA에서 @OneToMany만 단독으로 사용하면, 객체 세상에는 "게시글(1)이 이미지(N)를 가진다"는 개념이 있지만,
관계형 데이터베이스(DB) 세상에서는 이미지(N) 테이블이 게시글(1)의 ID(FK)를 들고 있는 것이 정석
이 간극을 메우기 위해 JPA는 별도의 지시가 없으면 두 테이블을 연결하는 '조인 테이블(Join Table)'(예: board_image_set)을 자동으로 만들어 관리하려고 합니다.
❌ 조인 테이블 방식의 단점
- 관리 포인트 증가: 테이블이 하나 더 생기므로 관리할 대상이 늘어납니다.
- 성능 저하: 데이터를 넣거나 조회할 때 중간 테이블을 한 번 더 거쳐야 하므로(Join 발생) DB 자원을 추가로 소모
- 직관성 부족: DB 설계 관점에서 굳이 필요 없는 테이블이 생겨 구조가 복잡해 보임
✅ 해결 방법: 연관관계의 주인 설정 (mappedBy)
mappedBy를 사용하면 이 문제를 깔끔하게 해결가능
1. 양방향 매핑과 mappedBy 사용
- 핵심: "이 연관관계의 주인은 N(다) 쪽인 BoardImage다!"라고 선언
- 효과: Board 엔티티의 @OneToMany에 mappedBy를 설정하면,
JPA는 별도의 중간 테이블을 만들지 않고 **BoardImage 테이블에 있는 외래키(FK)**를 사용해 관계를 관리
2. 단방향 매핑 활용
- 만약 Board에서 BoardImage를 참조할 필요가 없다면,
BoardImage 쪽에만 @ManyToOne을 두는 단방향 매핑을 사용
이 경우에도 중간 테이블 없이 깔끔하게 FK로만 관리
테이블들을 DROP 시키고
board.java에 mappedBY 속성을 적용시킨다
이렇게 하고 테이블을 다시 가동하면
board_image_set 테이블이 만들어지지 않는다.
'대우개발원 수업 내용 > spring boot, framework' 카테고리의 다른 글
| 자바 스프링 부트 11일차 b01Upload /첨부파일 이미지와 댓글 조회 및 처리 (0) | 2026.04.21 |
|---|---|
| 자바 스프링 부트 10일차 b01Upload (2) | 2026.04.20 |
| 자바 스프링 부트 8일차 b01Rest 댓글 페이징 및 CRUD 프론트엔드 연동 구현 (1) | 2026.04.15 |
| 자바 스프링 부트 7일차 b01Rest REST API 서버 구축부터 Axios를 활용한 프론트엔드 통신 준비 (0) | 2026.04.14 |
| 자바 스프링 부트 6일차 b01Rest / Ajax 와 JSON (1) | 2026.04.10 |

























