μ 체 μμ€μ½λ : Github
Spring Initializr μ¬μ©
Dependencies
κ°λ¨νκ² μνμ μ΄λ¦, μλ, μ΄λ―Έμ§λ₯Ό μ λ ₯λ°λ νΌμ λ§λ€κ³ DBμ μ μ₯κΉμ§ νλ νλ‘μ νΈμ λλ€.
μ΄λ―Έμ§λ λ‘컬μ μ μ₯νκ³ ν΄λΉ κ²½λ‘λ₯Ό DBμ μ μ₯νλ νμμΌλ‘ μ§νν κ²μ΄κ³ 1μ°¨μ μΌλ‘ μ μ₯νλ κΈ°λ₯λ§ κ΅¬ν ν μ‘°ν, κ²μ¦, μμΈμ²λ¦¬ λ±μ μΆκ°ν μμ μ λλ€.
μ€ν€λ§ ꡬ쑰 λ±μ λμ€μ 보μ ν μμ μ΄λ λ무 νμ ν΄λ λ΄μ£ΌμΈμ γ
μΌλ¨ Item
μν°ν°λ ItemName
μνλͺ
, quantity
μλ, fileId
File μν°ν°μ PKκ°μ νλλ‘ κ°μ΅λλ€.
Fileμ κ΄κ³λ‘ νμ΄λ³΄λ € νμλλ° κ΅³μ΄ κ΄κ³κΉμ§ μ€μ ν΄μ£Όμ§ μμλ λ κ² κ°μ μ΄λ° λ°©μμ μ±ννμ΅λλ€.
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Item {
@Id @GeneratedValue
private Long id;
private String itemName;
private Integer quantity;
@Column(nullable = true)
private Long fileId;
@Builder
public Item(String itemName, Integer quantity, Long fileId) {
this.itemName = itemName;
this.quantity = quantity;
this.fileId = fileId;
}
}
originFileName
μ νμ₯μλ₯Ό ν¬ν¨ν νμΌλͺ
μ μν νλμ
λλ€.
test.jpeg
μ΄λ° μμΌλ‘ μ μ₯λ κ²μ
λλ€.
fullPath
νλλ μ μ₯λ κ²½λ‘μ νμΌλͺ
μ ν¬ν¨ν©λλ€. νμΌμ μ‘°ννμ¬ λ³΄μ¬μ£Όκ±°λ λ€μ΄λ‘λ ν λ μ΄μ©λ νλμ
λλ€.
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
public class File {
@Id @GeneratedValue
private Long id;
@Column(nullable = false)
private String originFileName;
@Column(nullable = false)
private String fullPath;
@Builder
public File(Long id, String originFileName, String fullPath) {
this.id = id;
this.originFileName = originFileName;
this.fullPath = fullPath;
}
}
Controller
μ Service
λ μ΄μ΄ κ° λ°μ΄ν° μ μ‘μ μν ν΄λμ€μ
λλ€. μ΅λν Entity
λ₯Ό μ§μ μ μΌλ‘ λ
ΈμΆμν€μ§ μλλ€λ λλ¦λλ‘μ μμΉμ μν΄ μμ±νμμ΅λλ€.
νμ¬λ Entity
μ λΆνμνλ€ μκ°λλ νλκ° μκΈ° λλ¬Έμ Entity
μ κ±°μ κ°κ² μ€κ³λμμ΅λλ€.
κ° DTOλ Service
λ μ΄μ΄μμ μν°ν° λ³νμ μν νΈμ λ©μλλ₯Ό κ°μ΅λλ€.
( ToEntity
)
[ ItemDTO ]
@NoArgsConstructor
@Data
public class FileDto {
private Long id;
private String originFileName;
private String fullPath;
public File toEntity() {
return File.builder()
.id(this.id)
.originFileName(this.originFileName)
.fullPath(this.fullPath)
.build();
}
@Builder
public FileDto(Long id, String originFileName, String fullPath) {
this.id = id;
this.originFileName = originFileName;
this.fullPath = fullPath;
}
}
[ FileDTO ]
@NoArgsConstructor
@Data
public class FileDto {
private Long id;
private String originFileName;
private String fullPath;
public File toEntity() {
return File.builder()
.id(this.id)
.originFileName(this.originFileName)
.fullPath(this.fullPath)
.build();
}
@Builder
public FileDto(Long id, String originFileName, String fullPath) {
this.id = id;
this.originFileName = originFileName;
this.fullPath = fullPath;
}
}
Repository
μ κ²½μ° Spring Data JPA
λ₯Ό μ¬μ©νκ³ νΉλ³ν μ‘°ν κΈ°λ₯μ΄ νμ¬λ μκΈ° λλ¬Έμ CRUD
λ§ κ·Έλλ‘ μ¬μ©ν©λλ€.
[ ItemRepository ]
public interface ItemRepository extends JpaRepository<Item, Long> {}
[ FileRepository ]
public interface FileRepository extends JpaRepository<File, Long> {}
μΌλ¨ μ μ₯κΈ°λ₯λ§ κ΅¬ννκΈ°λ‘ νμΌλ―λ‘ λ©μλλ νλμ
λλ€.
컨νΈλ‘€λ¬
μμ λ³νλμ΄ λμ΄μ€λ DTO
λ₯Ό Entity
λ‘ λ³ννμ¬ μμνν©λλ€.
@RequiredArgsConstructor
@Service
public class ItemService {
private final ItemRepository itemRepository;
@Transactional
public Long save(ItemDto itemDto) {
return itemRepository.save(itemDto.toEntity()).getId();
}
}
FileService
λν λ§μ°¬κ°μ§λ‘ μ μ₯κΈ°λ₯λ§μ μνν©λλ€.
μ¬μ€ service
κ³μΈ΅μ λμ§ μμλ λκ² μ§λ§ μ΄λ° μν€ν
μ³μ μ΅μν΄μ§κΈ° μν΄ μλΉμ€ κ³μΈ΅μ λμμ΅λλ€. π
@RequiredArgsConstructor
@Service
public class FileService {
private final FileRepository fileRepository;
@Transactional
public Long save(FileDto fileDto) {
return fileRepository.save(fileDto.toEntity()).getId();
}
}
νΌμ λλλ§ μν λ©μλμ λλ€.
@GetMapping("/form")
public String homeView(Model model) {
model.addAttribute("item", new ItemRequest());
return "home";
}
form
μΌλ‘λΆν° λ°κ³ μ νλ λ°μ΄ν°λ₯Ό κ°μ²΄λ‘ μ΄κΈ°ννμ¬ μ λ¬νμμ΅λλ€.
μ΄λ κ² νλ©΄ Thyemeaf
μ¬μ©μ th:object
, th:field
λ±μ κΈ°λ₯μΌλ‘ νΈνκ² νΌ λ°μ΄ν°λ₯Ό νΈλ€λ§ ν μ μμ΅λλ€.
μμ² κ°μ²΄μ
λλ€. ItemRequest
νΌμΌλ‘λΆν° λ°μ λ°μ΄ν°λ₯Ό νλλ‘ ν©λλ€. Bean Validation
μ μ΄ν μΆκ°ν μμ μ
λλ€. (μΌλ¨ νλ €λ κ²μ μ§μ€!)
@Getter @Setter
public class ItemRequest {
private String itemName;
private Integer qty;
private MultipartFile file;
}
μμ² κ°μ²΄λ₯Ό λ°μ μ²λ¦¬νλ λ©μλμ λλ€. (κ°μ₯ μ€μ)
μΌλ¨ νΌμμ λ°μ λ°μ΄ν°λ₯Ό νμ±ν©λλ€.
μ°μ Item
μν°ν°λ₯Ό μν λ°μ΄ν°λ₯Ό νμ±νμ΅λλ€. File
μ nullμΌ μ μκΈ° λλ¬Έμ
λλ€.
νμ±ν μνλͺ
κ³Ό μλμ DTO
λ‘ λ³νν©λλ€. λ³ν νμ λ°λ‘ μλΉμ€ κ³μΈ΅μ save λ©μλλ₯Ό νΈμΆνμ§ μμ΅λλ€.
String itemName = itemRequest.getItemName();
Integer qty = itemRequest.getQty();
ItemDto itemDto = ItemDto.builder()
.itemName(itemName)
.qty(qty)
.build();
λ§μ½ νμΌμ΄ μλ κ²½μ° νμΌμ ID
λ₯Ό ItemDto
μ μ±μμ€ νμ μ μ₯ν΄μ£Όμ΄μΌ νκΈ° λλ¬Έμ
λλ€.
file
μ΄ μλμ§ λ μ²΄ν¬ νμ file
μ λν νμ±μ μμν©λλ€.
νμν λ°μ΄ν°λ νμΌλͺ
κ³Ό νμ₯μλ₯Ό λ΄κ³ μλ originalFilename
κ³Ό μ μ₯λ ν΄λμ κ²½λ‘κΉμ§ ν¬ν¨νλ fullPath
μ
λλ€.
μ°μ originalFilename
μ MultipartFile
κ°μ²΄κ° μ 곡νλ getOriginalFilename
λ©μλλ₯Ό μ΄μ©ν΄ μ»μ΄μ΅λλ€.
MultipartFile file = itemRequest.getFile();
μ 체 κ²½λ‘λ₯Ό μν νμΌ μ
λ‘λ ν΄λ κ²½λ‘λ νκ²½λ³μλ₯Ό μ¬μ©νμ΅λλ€.
(application.yml λλ application.propertiesμ μμ±)
image:
path: /Users/jeonhyeji/Documents/etc/file/
@Value
μ΄λ
Έν
μ΄μ
κ³Ό SpringEL
λ¬Έλ²μ μ΄μ©ν΄ νκ²½λ³μλ₯Ό κ°μ Έμ΅λλ€.
@Value("${image.path}")
private String uploadDir;
μ΄μ μ 체 κ²½λ‘(fullpath)λ₯Ό μ»μ΄μ¬ μ μμ΅λλ€.
String fullPath = uploadDir + file.getOriginalFilename();
μ 체 κ²½λ‘λ₯Ό μ»μμΌλ―λ‘ μ
λ‘λλ νμΌμ μλ²μ μ μ₯ν©λλ€. μ΄λ MultipartFile
μ transferTo
λ©μλλ₯Ό μ΄μ©ν©λλ€.
file.transferTo(new File(fullPath));
μ΄μ μλ²μ μ μ₯νμμΌλ DB
μ μ μ₯ν μ°¨λ‘μ
λλ€. μλΉμ€ κ³μΈ΅μΌλ‘ File
μ μ λ¬νκΈ° μν΄ FileDto
λ‘ λ³νν©λλ€.
FileDto fileDto = FileDto.builder()
.originFileName(file.getOriginalFilename())
.fullPath(uploadDir + file.getOriginalFilename())
.build();
λ³νλ FileDto
λ₯Ό μμ ꡬνν FileService
μ save
λ©μλμ νλΌλ―Έν°λ‘ λ£μ΄μ€λλ€. μ΄ λ Service
-> Repository
λ₯Ό κ±°μ³ DBμ μ μ₯λκ³ PK
λ₯Ό 리ν΄ν©λλ€.
Long savedFileId = fileService.save(fileDto);
μ΄μ Item
μ FileId
λ₯Ό ν¬ν¨μμΌ μ μ₯μν€κΈ° μν΄ ItemDto
μ FileId
λ₯Ό μΈν
ν΄μ€λλ€.
itemDto.setFileId(savedFileId);
μ΄μ νμΌμ΄ μμλ€λ©΄ ItemDto
μ FileId
κ° μΈν
λμμ κ²μ΄κ³ μμλ€λ©΄ null
μΈ μνμΌ κ²μ
λλ€. null
μ νμ©νλ―λ‘ μμ μ²λ¦¬λ₯Ό μλ£νμλ€λ©΄ Item
κΉμ§ μ μ₯μμΌμ€λλ€.
itemService.save(itemDto);
μ 체 μ½λμ λλ€.
@PostMapping("/form")
public String saveFormRequests(@ModelAttribute("item") ItemRequest itemRequest) throws IOException {
String itemName = itemRequest.getItemName();
Integer qty = itemRequest.getQty();
ItemDto itemDto = ItemDto.builder()
.itemName(itemName)
.qty(qty)
.build();
if (itemRequest.getFile() != null) {
MultipartFile file = itemRequest.getFile();
String fullPath = uploadDir + file.getOriginalFilename();
file.transferTo(new File(fullPath));
log.info("file.getOriginalFilename = {}", file.getOriginalFilename());
log.info("fullPath = {}", fullPath);
FileDto fileDto = FileDto.builder()
.originFileName(file.getOriginalFilename())
.fullPath(uploadDir + file.getOriginalFilename())
.build();
Long savedFileId = fileService.save(fileDto);
itemDto.setFileId(savedFileId);
}
itemService.save(itemDto);
return "redirect:/form";
}
Thymeleaf
μ Bootstrap
μ μ΄μ©ν κ°λ¨ν νΌμ
λλ€.
Content-Type
μ Multipart/form-data
λ‘ ν΄μ£Όμ΄μΌ νκΈ° λλ¬Έμform
νκ·Έμ enctype="multipart/form-data"
μ λ°λμ μ€μ ν΄μ£Όμ΄μΌ ν©λλ€.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>File Upload</title>
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="text-center my-4">
<h2>νμΌ μ
λ‘λ νΌ</h2>
</div>
<form th:action th:object="${item}" method="post" enctype="multipart/form-data">
<div>
<label for="itemName">μνλͺ
</label>
<input type="text" th:field="*{itemName}" class="form-control" id="itemName">
</div>
<hr class="my-4">
<div>
<label for="qty">μνκ°μ</label>
<input type="text" th:field="*{qty}" class="form-control" id="qty">
</div>
<hr class="my-4">
<div>
<input type="file" th:field="*{file}" class="form-control">
</div>
<br/>
<button class="btn-primary" type="submit">
μ μ‘
</button>
</form>
</div>
</body>
</html>
μ λ yml
νμμΌλ‘ μμ±νμκ³ MySQL
μ Docker
λ‘ κ΅¬λνμμ΅λλ€.
image:
path: /Users/jeonhyeji/Documents/etc/file/
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mydb
username: root
password:
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
open-in-view: false
hibernate:
ddl-auto: create
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
properties:
hibernate:
format_sql: true
logging:
level:
org.hibernate.SQL: debug
org.hibernate.type: trace
νΉμ λͺ°λΌ Docker νκ²½μΌλ‘ MySQL ꡬλλ²κΉμ§ μ μ΄λ³Όκ»μ γ
μ΄λ―Έμ§ λΉλ λ° μ»¨ν
μ΄λ μ€ν
μλ λͺ
λ Ήμ΄λ₯Ό μ
λ ₯νλ©΄ 컨ν
μ΄λIDλ₯Ό μΆλ ₯νκ³ λ°λ‘ μ’
λ£λ©λλ€.
-d
μ΅μ
μ μ€μ λ°±κ·ΈλΌμ΄λλ‘ μ λμκ°κ³ μμ κ±°μμ γ
docker run -d -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=true βname mysql mysql:5.7
μ΄μ mysql
μ bash
μλ‘ μ κ·Όν©λλ€.
Docker exec -it mysql bash
bash μμ μ κ·Όλμλ€λ©΄ μ΄μ mysqlμ μ μν©λλ€.
μ΄λ―Έμ§λ₯Ό λ§λ€ λ -e MYSQL_ALLOW_EMPTY_PASSWORD=true
νκ²½λ³μλ₯Ό μ£Όμ΄μ ν¨μ€μλλ νμ μμ΅λλ€.
mysql -uroot
μλ κ°μ νλ©΄μ΄ λμ€λ©΄ λ©λλ€.
λ€μμλ μ‘°ν κΈ°λ₯κ³Ό multiple
νμΌ μ
λ‘λλ₯Ό ν΄λ³Όκ»μ
κ°μ¬ν©λλ€ ! π
λμμ΄ λ§μ΄ λλ κ±° κ°μ΅λλ€.
κ°μ¬ν©λλ€~^^