🌑 ToyProject - 1 SpringBootλ₯Ό μ΄μš©ν•œ 파일 μ—…λ‘œλ“œμ— JPA적용 μ‹œν‚€κΈ°

Kim Dae HyunΒ·2021λ…„ 7μ›” 24일
2

Toy-Project

λͺ©λ‘ 보기
1/4
post-thumbnail

전체 μ†ŒμŠ€μ½”λ“œ : Github

πŸ”Ž ν”„λ‘œμ νŠΈ μ„€μ •

Spring Initializr μ‚¬μš©

Dependencies

  • Spring web
  • Spring Data JPA
  • Thymeleaf
  • Lombok
  • MySQL Driver

κ°„λ‹¨ν•˜κ²Œ μƒν’ˆμ˜ 이름, μˆ˜λŸ‰, 이미지λ₯Ό μž…λ ₯λ°›λŠ” 폼을 λ§Œλ“€κ³  DB에 μ €μž₯κΉŒμ§€ ν•˜λŠ” ν”„λ‘œμ νŠΈμž…λ‹ˆλ‹€.

μ΄λ―Έμ§€λŠ” λ‘œμ»¬μ— μ €μž₯ν•˜κ³  ν•΄λ‹Ή 경둜λ₯Ό DB에 μ €μž₯ν•˜λŠ” ν˜•μ‹μœΌλ‘œ 진행할 것이고 1차적으둜 μ €μž₯ν•˜λŠ” κΈ°λŠ₯만 κ΅¬ν˜„ ν›„ 쑰회, 검증, μ˜ˆμ™Έμ²˜λ¦¬ 등을 μΆ”κ°€ν•  μ˜ˆμ •μž…λ‹ˆλ‹€.


πŸ”Ž Item Entity μž‘μ„±

μŠ€ν‚€λ§ˆ ꡬ쑰 등은 λ‚˜μ€‘μ— 보정할 μ˜ˆμ •μ΄λ‹ˆ λ„ˆλ¬΄ ν—ˆμ „ν•΄λ„ λ΄μ£Όμ„Έμš” γ…Ž

일단 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;
    }
}

πŸ”Ž File Entity μž‘μ„±

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;
    }
}

πŸ”Ž Item, File DTO μž‘μ„±

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 μž‘μ„±

Repository 의 경우 Spring Data JPAλ₯Ό μ‚¬μš©ν•˜κ³  νŠΉλ³„ν•œ 쑰회 κΈ°λŠ₯이 ν˜„μž¬λŠ” μ—†κΈ° λ•Œλ¬Έμ— CRUD만 κ·ΈλŒ€λ‘œ μ‚¬μš©ν•©λ‹ˆλ‹€.

[ ItemRepository ]

public interface ItemRepository extends JpaRepository<Item, Long> {}
[ FileRepository ]

public interface FileRepository extends JpaRepository<File, Long> {}

πŸ”Ž ItemService μž‘μ„±

일단 μ €μž₯κΈ°λŠ₯만 κ΅¬ν˜„ν•˜κΈ°λ‘œ ν–ˆμœΌλ―€λ‘œ λ©”μ„œλ“œλŠ” ν•˜λ‚˜μž…λ‹ˆλ‹€.
μ»¨νŠΈλ‘€λŸ¬μ—μ„œ λ³€ν™˜λ˜μ–΄ λ„˜μ–΄μ˜€λŠ” DTOλ₯Ό Entity둜 λ³€ν™˜ν•˜μ—¬ μ˜μ†ν™”ν•©λ‹ˆλ‹€.

@RequiredArgsConstructor
@Service
public class ItemService {

    private final ItemRepository itemRepository;

    @Transactional
    public Long save(ItemDto itemDto) {
        return itemRepository.save(itemDto.toEntity()).getId();
    }
}

πŸ”Ž FileService μž‘μ„±

FileService λ˜ν•œ λ§ˆμ°¬κ°€μ§€λ‘œ μ €μž₯κΈ°λŠ₯λ§Œμ„ μˆ˜ν–‰ν•©λ‹ˆλ‹€.
사싀 service계측을 두지 μ•Šμ•„λ„ λ˜κ² μ§€λ§Œ 이런 아킀텍쳐에 μ΅μˆ™ν•΄μ§€κΈ° μœ„ν•΄ μ„œλΉ„μŠ€ 계측을 λ‘μ—ˆμŠ΅λ‹ˆλ‹€. 😁

@RequiredArgsConstructor
@Service
public class FileService {

    private final FileRepository fileRepository;

    @Transactional
    public Long save(FileDto fileDto) {
        return fileRepository.save(fileDto.toEntity()).getId();
    }
    
}

πŸ”Ž Controller μž‘μ„±

폼을 λžœλ”λ§ μœ„ν•œ λ©”μ„œλ“œμž…λ‹ˆλ‹€.

@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";
    }

πŸ”Ž HTML μ½”λ“œ

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>

πŸ”Ž DB (MySQL) μ—°κ²° μ„€μ • (application.yml)

μ €λŠ” 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 ꡬ동

ν˜Ήμ‹œ λͺ°λΌ 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 파일 μ—…λ‘œλ“œλ₯Ό ν•΄λ³Όκ»˜μš”

κ°μ‚¬ν•©λ‹ˆλ‹€ ! πŸ˜„

profile
μ’€ 더 천천히 까먹기 μœ„ν•΄ κΈ°λ‘ν•©λ‹ˆλ‹€. 🧐

1개의 λŒ“κΈ€

comment-user-thumbnail
2021λ…„ 7μ›” 24일

도움이 많이 λ˜λŠ” κ±° κ°™μŠ΅λ‹ˆλ‹€.
κ°μ‚¬ν•©λ‹ˆλ‹€~^^

λ‹΅κΈ€ 달기