Backend/JAVA

ํŒŒ์ผ ์—…๋กœ๋“œ, ๋‹ค์šด๋กœ๋“œ (MultipartFile)

dddzr 2025. 11. 8. 18:15

๐Ÿ“Œ 1. MultipartFile์ด๋ž€?

MultipartFile์€ Spring์ด ์ œ๊ณตํ•˜๋Š” ํŒŒ์ผ ์—…๋กœ๋“œ๋ฅผ ์œ„ํ•œ ๋‚ด์žฅ ์ธํ„ฐํŽ˜์ด์Šค.
multipart/form-data ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๊ณ , ํŒŒ์ผ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ๋‹ค.

โœ… 1-1. MultipartFile ์ฃผ์š” ํŠน์ง•

  • ์Šคํ”„๋ง์ด ์ž๋™์œผ๋กœ ํŒŒ์ผ์„ ๋ฐ”์ธ๋”ฉํ•ด ์คŒ.
  • ๋‹จ์ผ ํŒŒ์ผ & ๋‹ค์ค‘ ํŒŒ์ผ ์—…๋กœ๋“œ ๊ฐ€๋Šฅ
  • @ModelAttribute, @RequestParam๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉ
  • ํŒŒ์ผ ์ด๋ฆ„, ํฌ๊ธฐ, ํ™•์žฅ์ž, ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ๊ฐ€๋Šฅ

 

โœ… 1-2.  ์ฃผ์š” ๋ฉ”์„œ๋“œ

๋ฉ”์„œ๋“œ ์„ค๋ช…
getOriginalFilename() ์—…๋กœ๋“œ๋œ ํŒŒ์ผ์˜ ์›๋ณธ ์ด๋ฆ„ ๊ฐ€์ ธ์˜ค๊ธฐ
getSize() ํŒŒ์ผ ํฌ๊ธฐ (๋ฐ”์ดํŠธ ๋‹จ์œ„)
getContentType() ํŒŒ์ผ์˜ MIME ํƒ€์ž… ํ™•์ธ
getBytes() ํŒŒ์ผ์„ byte ๋ฐฐ์—ด๋กœ ๋ณ€ํ™˜
getInputStream() ํŒŒ์ผ์„ InputStream์œผ๋กœ ๊ฐ€์ ธ์˜ค๊ธฐ
transferTo(File) ์ง€์ •๋œ ์œ„์น˜์— ํŒŒ์ผ ์ €์žฅ (ํŒŒ์ผ ์ €์žฅ ๊ฐ€๋Šฅ)

 

โœ… 1-3. MultipartFile ์˜ˆ์ œ

1๏ธโƒฃ ๋‹จ์ผ ํŒŒ์ผ ์—…๋กœ๋“œ

์š”์ฒญ ์˜ˆ์‹œ (form-data): file: example.jpg

@PostMapping("/upload")
public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) {
    if (file.isEmpty()) {
        return ResponseEntity.badRequest().body("ํŒŒ์ผ์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”.");
    }
    String fileName = file.getOriginalFilename();
    long fileSize = file.getSize();
    return ResponseEntity.ok("ํŒŒ์ผ ์—…๋กœ๋“œ ์„ฑ๊ณต! ํŒŒ์ผ๋ช…: " + fileName + ", ํฌ๊ธฐ: " + fileSize + " bytes");
}

 

 

2๏ธโƒฃ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ํŒŒ์ผ ์—…๋กœ๋“œ

์š”์ฒญ ์˜ˆ์‹œ (form-data): files: file1.jpg, file2.png, file3.pdf

@PostMapping("/uploadMultiple")
public ResponseEntity<String> uploadMultipleFiles(@RequestParam("files") MultipartFile[] files) {
    for (MultipartFile file : files) {
        if (!file.isEmpty()) {
            System.out.println("ํŒŒ์ผ๋ช…: " + file.getOriginalFilename() + " | ํฌ๊ธฐ: " + file.getSize());
        }
    }
    return ResponseEntity.ok("ํŒŒ์ผ ์—…๋กœ๋“œ ์„ฑ๊ณต!");
}

 

3๏ธโƒฃ ํŒŒ์ผ ์ €์žฅํ•˜๊ธฐ (transferTo ์‚ฌ์šฉ)

@PostMapping("/saveFile")
public ResponseEntity<String> saveFile(@RequestParam("file") MultipartFile file) throws IOException {
    String savePath = "C:/upload/" + file.getOriginalFilename();
    file.transferTo(new File(savePath)); // ํŒŒ์ผ ์ €์žฅ
    return ResponseEntity.ok("ํŒŒ์ผ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์ €์žฅ๋จ: " + savePath);
}

 

 

๐Ÿ“Œ 2. Spring์—์„œ ํŒŒ์ผ + ๋ฐ์ดํ„ฐ ์ „์†ก ๋ฐฉ๋ฒ•

๐Ÿšจ @RequestBody๋Š” application/json์„ ์‚ฌ์šฉํ•ด์•ผ ํ•˜์ง€๋งŒ,
๐Ÿšจ ํŒŒ์ผ ์—…๋กœ๋“œ๋Š” multipart/form-data ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ด์„œ ๋™์‹œ์— ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋‹ค!

๐Ÿ”ฅ JSON๊ณผ ํŒŒ์ผ์„ ๊ฐ™์ด ๋ณด๋‚ด๋ ค๋ฉด → @RequestPart ๋˜๋Š” @ModelAttribute ์‚ฌ์šฉ!

<form action="/upload" method="post" enctype="multipart/form-data">
    <input type="text" name="username">
    <%-- multiple ์†์„ฑ์œผ๋กœ ๋‹ค์ค‘ ํŒŒ์ผ ์„ ํƒ ๊ฐ€๋Šฅ, ํ—ˆ์šฉํ™•์žฅ์ž๋Š” ๋ฐฑ์—”๋“œ์—์„œ๋„ ๊ด€๋ฆฌ --%>
     <input type="file" id="attachFiles" name="attachFiles" accept=".jpg, .jpeg, .png, .gif, .pdf, .zip, .doc, .docx, .xls, .xlsx, .txt" multiple>
    <button type="submit">Upload</button>
</form>

 

โœ… 2-1. @ModelAttribute ์‚ฌ์šฉ (ํผ ๋ฐ์ดํ„ฐ ๋ฐฉ์‹)

@ModelAttribute๋Š” ํผ ๋ฐ์ดํ„ฐ ํ˜•์‹ (multipart/form-data) ์„ ์ด์šฉํ•ด์„œ ๊ฐ์ฒด๋ฅผ ์ž๋™ ๋งคํ•‘ํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค.

โœ” ํผ ํ•„๋“œ (title, content ๋“ฑ)์™€ ํŒŒ์ผ์„ ํ•จ๊ป˜ ๋ณด๋‚ด์•ผ ํ•  ๋•Œ ์‚ฌ์šฉ
โœ” Postman์—์„œ Form-Data ํ˜•์‹์œผ๋กœ ์š”์ฒญํ•ด์•ผ ํ•จ

 

๐Ÿ”น ๋ฐฑ์—”๋“œ ์ฝ”๋“œ (Spring Controller)

@PostMapping("/insert")
public ResponseEntity<Note> insertNote(
        @ModelAttribute Note note,  
        @RequestParam(value = "attachFiles", required = false) MultipartFile[] attachFiles) {

    Note insertedNote = noteService.insertNote(note, attachFiles);
    return new ResponseEntity<>(insertedNote, HttpStatus.OK);
}

โœ”  @ModelAttribute Note note → ํผ ๋ฐ์ดํ„ฐ๋ฅผ Note ๊ฐ์ฒด๋กœ ์ž๋™ ๋งคํ•‘
โœ”  @RequestParam MultipartFile[] attachFiles → ํŒŒ์ผ์€ ๋”ฐ๋กœ ์ฒ˜๋ฆฌ

 

๐Ÿ”น ํ”„๋ก ํŠธ์—”๋“œ (JavaScript - FormData ์‚ฌ์šฉ)

import axios from "axios";

const handleSubmit = async () => {
    const formData = new FormData();
    formData.append("noteTitl", "ํ…Œ์ŠคํŠธ ์ œ๋ชฉ");
    formData.append("regtId", 1);
    formData.append("noteCont", "๋‚ด์šฉ์ž…๋‹ˆ๋‹ค.");
   
    // ํŒŒ์ผ ์ถ”๊ฐ€
    const fileInput = document.getElementById("fileUpload");
    if (fileInput.files.length > 0) {
        formData.append("attachFiles", fileInput.files[0]);
    }

    try {
        const response = await axios.post("http://localhost:8080/insert", formData, {
            headers: { "Content-Type": "multipart/form-data" },
        });
        console.log("์‘๋‹ต:", response.data);

    } catch (error) {
        console.error("์—๋Ÿฌ ๋ฐœ์ƒ:", error);

    }
};



๐Ÿ”น Postman ์š”์ฒญ ์„ค์ • (Form-Data ๋ฐฉ์‹)

  1. Method: POST
  2. Headers: Content-Type: multipart/form-data
  3. Body → form-data ์„ ํƒ
  4. Key ๊ฐ’ ์ž…๋ ฅ (name์€ ํ•„๋“œ๋ช…๊ณผ ๋™์ผํ•ด์•ผ ํ•จ)
Key Value Type
noteTitl test1 Text
regtId 1 Text
noteCont ๋‚ด์šฉ ์ž…๋ ฅ Text
attachFiles (ํŒŒ์ผ ์„ ํƒ) File

 

 

โœ… 2-2. @RequestPart ์‚ฌ์šฉ (JSON + ํŒŒ์ผ ์—…๋กœ๋“œ)

โœ” JSON ๋ฐ์ดํ„ฐ์™€ ํŒŒ์ผ์„ ํ•จ๊ป˜ ์ „์†กํ•ด์•ผ ํ•  ๋•Œ ์‚ฌ์šฉ
โœ” Postman ๋˜๋Š” ํ”„๋ก ํŠธ์—์„œ FormData๋ฅผ ์‚ฌ์šฉํ•ด์„œ JSON + ํŒŒ์ผ์„ ๊ฐ™์ด ์ „์†ก
โœ” @RequestBody ๋Œ€์‹  @RequestPart ์‚ฌ์šฉํ•ด์•ผ ํ•จ (JSON์„ multipart/form-data๋กœ ์ฒ˜๋ฆฌ)

 

๐Ÿ”น ๋ฐฑ์—”๋“œ ์ฝ”๋“œ (Spring Controller)

@PostMapping(value = "/insert", consumes = { MediaType.MULTIPART_FORM_DATA_VALUE })
public ResponseEntity<Note> insertNote(
        @RequestPart("note") Note note,  // JSON ๋ฐ์ดํ„ฐ ๋ฐ›์Œ
        @RequestPart(value = "attachFiles", required = false) MultipartFile[] attachFiles) {

    Note insertedNote = noteService.insertNote(note, attachFiles);
    return new ResponseEntity<>(insertedNote, HttpStatus.OK);
}

 

 

๐Ÿ”น ํ”„๋ก ํŠธ์—”๋“œ

import axios from "axios";

const handleUpload = async () => {
    const formData = new FormData();

    // JSON ๋ฐ์ดํ„ฐ๋ฅผ Blob์œผ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ถ”๊ฐ€
    const noteData = {
        "noteTitl": "test1",
        "regtId": 1,
        "noteCont": "123",
        "recipients": [ { "recvUserId": "user1", "recvCcGbn": "CC" }, { "recvUserId": "user2", "recvCcGbn": "BCC" } ]
    };
    
    const noteBlob = new Blob([JSON.stringify(noteData)], { type: "application/json" });
    formData.append("note", noteBlob);

    // ํŒŒ์ผ ์ถ”๊ฐ€
    const fileInput = document.getElementById("fileUpload");
    if (fileInput.files.length > 0) {
        formData.append("attachFiles", fileInput.files[0]);
    }

    try {
        const response = await axios.post("/insert", formData, {
            headers: {
                "Content-Type": "multipart/form-data"
            }
        });

        console.log("์—…๋กœ๋“œ ์„ฑ๊ณต:", response.data);
    } catch (error) {
        console.error("์—…๋กœ๋“œ ์‹คํŒจ:", error);
    }
};


โœ”  JSON ๋ฐ์ดํ„ฐ๋ฅผ Blob์œผ๋กœ ๋ณ€ํ™˜ ํ›„ FormData์— ์ถ”๊ฐ€ํ•ด์•ผ JSON + ํŒŒ์ผ์„ ๊ฐ™์ด ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋‹ค!
โœ”  Content-Type์„ ๋”ฐ๋กœ ์ง€์ •ํ•˜์ง€ ์•Š์•„๋„ ์ž๋™์œผ๋กœ multipart/form-data๋กœ ์ฒ˜๋ฆฌ๋จ

 

๐Ÿ“Œ3. ์ •๋ฆฌ (์–ด๋–ค ๋ฐฉ์‹์ด ์ ์ ˆํ• ๊นŒ?)

๋ฐฉ์‹ @RequestBody @ModelAttribute @RequestPart
JSON ๋ฐ์ดํ„ฐ๋งŒ โœ… ๊ฐ€๋Šฅ โŒ ์•ˆ๋จ โœ… ๊ฐ€๋Šฅ
ํผ ๋ฐ์ดํ„ฐ๋งŒ โŒ ์•ˆ๋จ โœ… ๊ฐ€๋Šฅ โœ… ๊ฐ€๋Šฅ
JSON + ํŒŒ์ผ ์—…๋กœ๋“œ โŒ ์•ˆ๋จ โŒ ์•ˆ๋จ โœ… ๊ฐ€๋Šฅ
ํผ ๋ฐ์ดํ„ฐ + ํŒŒ์ผ ์—…๋กœ๋“œ โŒ ์•ˆ๋จ โœ… ๊ฐ€๋Šฅ โœ… ๊ฐ€๋Šฅ

โœ”  @ModelAttribute → Form-Data ๋ฐฉ์‹์œผ๋กœ ํผ ๋ฐ์ดํ„ฐ + ํŒŒ์ผ์„ ์ฒ˜๋ฆฌ
โœ”  @RequestPart → JSON + ํŒŒ์ผ ์—…๋กœ๋“œ๋ฅผ ์ฒ˜๋ฆฌ (ํ”„๋ก ํŠธ์—์„œ FormData ์‚ฌ์šฉ)
โœ”  @RequestBody + @RequestParam(MultipartFile) → โŒ ํ•จ๊ป˜ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Œ

 

๐ŸŽฏ4. ๊ฒฐ๋ก  & ์ถ”์ฒœ

1๏ธโƒฃ ํผ ๋ฐ์ดํ„ฐ ๋ฐฉ์‹ (title, content์™€ ํŒŒ์ผ์„ ๊ฐ™์ด ๋ณด๋‚ผ ๋•Œ)
@ModelAttribute ์‚ฌ์šฉ (ํผ ๋ฐ์ดํ„ฐ ์ž๋™ ๋งคํ•‘)

2๏ธโƒฃ JSON + ํŒŒ์ผ์„ ๊ฐ™์ด ๋ณด๋‚ผ ๋•Œ
@RequestPart ์‚ฌ์šฉ (ํ”„๋ก ํŠธ์—์„œ FormData๋กœ JSON + ํŒŒ์ผ ์ „์†ก)

3๏ธโƒฃ ํŒŒ์ผ ์—…๋กœ๋“œ ๋‹จ๋…์œผ๋กœ ๋ณด๋‚ผ ๋•Œ
@RequestParam MultipartFile ์‚ฌ์šฉ

 

๐Ÿ”ฅ ํŒŒ์ผ + JSON์„ ๊ฐ™์ด ๋ณด๋‚ด์•ผ ํ•œ๋‹ค๋ฉด? @RequestPart ์ถ”์ฒœ! ๐Ÿš€

 

๐Ÿ“Œ 5. ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ ์˜ˆ์‹œ

@Value("${atchfile.upload.path}")
private String ATCHFILE_UPLOAD_PATH;


   // ์ฒจ๋ถ€ ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ
   @PostMapping("/download/attachFile.json")
   public ResponseEntity<Resource> getAttachFile(@RequestBody NoteAttachRequestDTO request,
        @RequestHeader(value = "X-User-Name", required = false) String username) {
    try {
    if(!noteRestService.checkNoteAuth(request.getSno(), username)) {
    return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
    }
    NoteAttach file = noteAttachService.getAttachFile(request);
 
    if(file == null){
               return ResponseEntity.notFound().build();
           }  

            Path filePath = Paths.get(ATCHFILE_UPLOAD_PATH + "/" + file.getAtchSaveNm()).toAbsolutePath().normalize();
            Resource resource = new UrlResource(filePath.toUri());
            if (!resource.exists()) {
                return ResponseEntity.notFound().build();
            }
            String originalFileName = file.getAtchNm();
            String encodedFileName = URLEncoder.encode(originalFileName, "UTF-8").replaceAll("\\+", "%20");
            return ResponseEntity.ok()
                    .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + encodedFileName  + "\"")
                    .body(resource);

        } catch (Exception e) {
            e.printStackTrace();
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
   }
 
   // ์ฒจ๋ถ€ ํŒŒ์ผ ์ „์ฒด ๋‹ค์šด๋กœ๋“œ
   @PostMapping("/download/all/attachFile.json")
   public ResponseEntity<Resource> getAllAttachFile(@RequestBody NoteAttachRequestDTO request,
        @RequestHeader(value = "X-User-Name", required = false) String username) {
       try {
    if(!noteRestService.checkNoteAuth(request.getSno(), username)) {
    return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
    }
        List<NoteAttach> atchList = noteAttachService.getAllAttachFile(request.getSno());
           if (atchList == null || atchList.isEmpty()) {
               return ResponseEntity.notFound().build();
           }
           Path zipFilePath = Files.createTempFile(request.getSno() + "_files", ".zip");
           try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFilePath))) {
               
               for (NoteAttach file : atchList) {
                   Path filePath = Paths.get(ATCHFILE_UPLOAD_PATH + "/" + file.getAtchSaveNm()).toAbsolutePath().normalize();
                 
                   if (!Files.exists(filePath)) {
                       continue; // ํŒŒ์ผ์ด ์—†์œผ๋ฉด ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค.
                   }
                   
                   // ZIP ํŒŒ์ผ์— ์ถ”๊ฐ€
//                    String zipEntryName = file.getAtchNm() + "." + file.getAtchExtn();
                   String zipEntryName = file.getAtchNm();
                   zipOut.putNextEntry(new ZipEntry(zipEntryName));
                   Files.copy(filePath, zipOut);
                   zipOut.closeEntry();

               }
           }
           Resource resource = new UrlResource(zipFilePath.toUri());
           if (!resource.exists()) {
               return ResponseEntity.notFound().build();
           }
           return ResponseEntity.ok()
                   .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + request.getSno() + "_files.zip")
                   .contentType(MediaType.APPLICATION_OCTET_STREAM)
                   .body(resource);

       } catch (Exception e) {
           e.printStackTrace();
           return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
       }
   }