๊ฐ๋ฐํ๊ฒฝ
๐ก ์ ์ฒด ์ฝ๋๋ Github๋ฅผ ์ฐธ์กฐํด์ฃผ์ธ์.
- spring boot 3.3.1
- JDK 17
Intro
Spring MVC์์๋ MultipartFile ์ด๋ผ๋ ์ธํฐํ์ด์ค๋ฅผ ํตํด ํ์ผ ์ ๋ก๋ ๊ธฐ๋ฅ์ ์ ๊ณตํ๊ณ ์์ต๋๋ค. ZIP, UnZIP ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ ์ ํธ์ฑ ํด๋์ค๋ฅผ ํจ๊ป ๊ฐ๋ฐํด๋ณด๊ณ , MultipartFile์ ํจ๊ป ์ฌ์ฉํ์ฌ ํ์ผ ์ ๋ก๋, ๊ทธ๋ฆฌ๊ณ HttpServletResponse๋ฅผ ํตํด ํ์ผ์ ํด๋ผ์ด์ธํธ์๊ฒ ์คํธ๋ฆผ์ ํตํด ๋ด๋ ค์ค์ ๋ค์ด๋ก๋ํ ์ ์๋ API๋ฅผ ๊ฐ๋ฐ ๋ฐ ํ ์คํธ ํ๋ ๋ฐฉ๋ฒ์ ๋ํด์ ๋ฐฐ์๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
๋จผ์ Multipartfile์ด ๋ญ๊น์?
๋ณดํต ํ์ผ์ ์ ์กํ ๋, HTTP body๋ฅผ ์ฌ๋ฌ ๋ถ๋ถ(Multipart Data
)์ผ๋ก ๋๋ ์ ๋ณด๋ด๋๋ฐ ์ด๋ฌํ Multipart Data ์ฆ, Multipart ์์ฒญ์ผ๋ก ๋ฐ์ ์
๋ก๋๋ ํ์ผ์ ๋งํฉ๋๋ค. ํด๋น ์ธํฐํ์ด์ค๋ฅผ ํตํด ํ์ผ์ ์ ๋ณด(์ฌ์ด์ฆ, ํ์ผ ์ด๋ฆ ๋ฑ)๋ฅผ ์ฝ๊ฒ ๊ฐ์ ธ์ฌ ์ ์์ต๋๋ค.
์คํ๋ง์์๋ ์์ฒญ์ผ๋ก ๋ฐ์ ํ์ผ ์ ๋ณด๋ฅผ ๋ฉ๋ชจ๋ฆฌ ๋๋ ๋์คํฌ์ ์ผ์์ ์ผ๋ก ์ ์ฅ์์ผ ์ฌ์ฉํ ์ ์๊ฒ ํด์ค๋๋ค. ์์ ๋ฐ์ดํฐ๋ ์์ฒญ ์ฒ๋ฆฌ๊ฐ ๋๋ ๋ ์ ๋ฆฌ๋ฉ๋๋ค.
1. Zip ๊ตฌํ
๋จผ์ ZIP ๊ธฐ๋ฅ์ ๋จผ์ ๊ตฌํํด๋ณด๊ฒ ์ต๋๋ค. API๋ฅผ ์ด๋ป๊ฒ ์ค๊ณํ๋์ ๋ฐ๋ผ ๋ฌ๋ผ์ง๊ฒ ์ง๋ง, ์ํ๋ ๋๋ ํ ๋ฆฌ ๋๋ ํ์ผ์ ์ ๊ณต๋ฐ๊ณ ํด๋น ๋๋ ํ ๋ฆฌ ๋๋ ํ์ผ์ Zippingํ๋ ๊ฐ๋จํ ๊ธฐ๋ฅ์ ๊ตฌํํ์ต๋๋ค.
public static byte[] zipFile(File sourceFile) {
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
try (ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream)) {
addFileToZip(sourceFile, zipOutputStream, null);
}
return byteArrayOutputStream.toByteArray();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
๋จผ์ ZIP์ผ๋ก ์ฐ๊ธฐ ์ํด์๋ ํ์ผ ๋ด๋ถ์ byte๋ก ์์ฑํด์ผ ํฉ๋๋ค. ByteArrayOutputStream๊ณผ ZipOutputStream์ try-with-resource๋ฅผ ํตํด ์ด์ด์ค๋๋ค. ๊ทธ๋ฆฌ๊ณ ๋ addFileToZip ๋ฉ์๋๋ฅผ ํธ์ถํ๋๋ฐ์ ํต์ฌ ๋ก์ง์ ํด๋น ๋ฉ์๋ ๋ด๋ถ์ ์์ต๋๋ค.
private static void addFileToZip(File parentFile, ZipOutputStream zipOutputStream, String parentFolderName) {
String currentPath = hasText(parentFolderName) ? parentFolderName + "/" + parentFile.getName() : parentFile.getName();
if (parentFile.isDirectory()) {
for (File f : Objects.requireNonNull(parentFile.listFiles())) {
addFileToZip(f, zipOutputStream, currentPath);
}
} else {
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
try (FileInputStream fileInputStream = new FileInputStream(parentFile)) {
zipOutputStream.putNextEntry(new ZipEntry(currentPath));
int length;
while ((length = fileInputStream.read(buffer)) > 0) {
zipOutputStream.write(buffer, 0, length);
}
zipOutputStream.closeEntry();
} catch (FileNotFoundException e) {
log.error("File not found: {}, Exception: {}", parentFile, e.getMessage());
throw new RuntimeException(e);
} catch (IOException e) {
log.error("Error while zipping file: {}, Exception: {}", parentFile, e.getMessage());
throw new RuntimeException(e);
}
}
}
ํด๋น ๋ฉ์๋๋ ์ฌ๊ท ํจ์์ฒ๋ผ ์ฌ์ฉ๋ฉ๋๋ค. ZipOutputStream์ ์ ๋ฌ๋ฐ์์ ํ์ฌ ๊ฒฝ๋ก๋ฅผ ๋จผ์ ์์ฑํฉ๋๋ค. (ํด๋๊ฐ ์ฌ๋ฌ depth๋ก ๊ตฌ์ฑ๋์ด์๋ ๊ฒ์ ์ ์งํ๊ธฐ ์ํด)
ํ์ฌ ํ์ผ์ด ๋๋ ํ ๋ฆฌ๋ผ๋ฉด? ์ฌ๊ท ํธ์ถ์ ํ๊ณ , ํ์ผ์ด๋ผ๋ฉด? ํ์ผ ์คํธ๋ฆผ์ ์ด์ด์ ZipEntry์ write ํด์ค๋๋ค. ์ฌ๊ธฐ์ ์ฃผ์ํ ์ ์ ๋ด๋ถ๊ฐ ๋น์ด์๋ค๋ฉด fileInputStream.read(buffer) ์ด ๋ถ๋ถ์์ ์์ธ๊ฐ ๋ฐ์ํฉ๋๋ค. ํด๋น ๋ถ๋ถ์ try-catch๋ฅผ ํตํด ์ก์์ฃผ์๋์ง ์ง๊ธ์ฒ๋ผ ๊ทธ๋ฅ ์์ธ๋ฅผ ๋ฐ์ํ๋์ง ํ์๋ฉด ๋ฉ๋๋ค.
1.1. Zip ๊ธฐ๋ฅ ํ ์คํธ
@Test
void directoryZippingTest() throws IOException {
// given
final Path zipFilePath = Path.of("src/test/resources/test-dir");
final Path testZipPath = Path.of("src/test/resources/test.zip");
Files.deleteIfExists(testZipPath); // delete test-zip
// when
byte[] bytes = ZipUtils.zipFile(zipFilePath.toFile());
Files.write(testZipPath, bytes);
// then
assertThat(testZipPath).exists();
}
@Test
void fileZippingTest() throws IOException {
// given
final Path zipFilePath = Path.of("src/test/resources/ํ๊ธ์ด๋ฆ_ํ
์คํธ.txt");
final Path testZipPath = Path.of("src/test/resources/test2.zip");
Files.deleteIfExists(testZipPath); // delete test-zip
// when
byte[] bytes = ZipUtils.zipFile(zipFilePath.toFile());
Files.write(testZipPath, bytes);
// then
assertThat(testZipPath).exists();
}
ํ ์คํธ ์ฝ๋๋ ๊ฐ๋จํฉ๋๋ค. Zipping ํ ํ์ผ ๋๋ ๋๋ ํ ๋ฆฌ๋ฅผ ๋๊ฒจ์ byte[]๋ฅผ ๋ฐํ๋ฐ์ ๋ค์, ํด๋น byte[]๋ฅผ ํ์ผ๋ก ์จ์ฃผ๋ฉด ๋ฉ๋๋ค.
2. unZip ๊ธฐ๋ฅ ๊ตฌํ
unZip ๊ธฐ๋ฅ์ ๋ค์๊ณผ ๊ฐ์ด ๊ตฌํํ์์ต๋๋ค. Zip ํ์ผ(zipFile) ๋๋ InputStream์ ์์ถ ํด์ ํ ๋๋ ํ ๋ฆฌ(dstPath)๋ก ์ ๋ ฅ๋ฐ์ ์ฒ๋ฆฌํฉ๋๋ค.
public static void unZipFile(String dstPath, File zipFile) throws IOException {
unZipFile(dstPath, new FileInputStream(zipFile), null);
}
public static void unZipFile(String dstPath, InputStream inputStream) throws IOException {
unZipFile(dstPath, inputStream, null);
}
๊ฒฐ๋ก ์ ์ผ๋ก InputStream์ผ๋ก ๋ณํํ์ฌ ํต์ฌ ๋ก์ง์ ์ํํ๋ ๋ฉ์๋๋ก ์ ๋ฌํฉ๋๋ค.
private static void unZipFile(String dstPath, InputStream inputStream, @Nullable Function<Path, Path> renameDuplicatedFilename) throws IOException {
try (ZipInputStream zipInputStream = new ZipInputStream(inputStream, Charset.forName("EUC-KR"))) {
ZipEntry entry;
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
while ((entry = zipInputStream.getNextEntry()) != null) {
final String entryName = entry.getName();
if (entry.isDirectory() || entryName.startsWith("__MACOSX")) continue;
File entryFile;
if (renameDuplicatedFilename == null) {
entryFile = new File(dstPath, entryName);
} else {
Path path = Path.of(dstPath, entryName);
entryFile = renameDuplicatedFilename.apply(path).toFile();
}
Files.createDirectories(entryFile.getParentFile().toPath());
try (BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(entryFile))) {
int bytesRead;
while ((bytesRead = zipInputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
}
zipInputStream.closeEntry();
}
}
}
์ ๋ฌ ๋ฐ์ inputStream์ ์ด์ฉํด์ ZipInputStream์ ์ด์ด์ค๋๋ค. ๊ฐ entry๋ฅผ ์ํํ๋ฉด์ Directory๊ฑฐ๋ __MACOSX๋ก ์์ํ๋ค๋ฉด ๊ฑด๋๋์ด์ค๋๋ค. ์ด๋ ๊ฒ ํ ์ด์ ๋ ๋๋ ํ ๋ฆฌ์ ์ํ ์ํธ๋ฆฌ๋ฅผ ์ฎ๊ธธ ๋ ์ํ ๋๋ ํ ๋ฆฌ๋ฅผ ์์ฑ ์ํฌ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ ๋๋ค. ํน์๋ผ๋ ์์ถ์ ํด์ ํ๋ ๊ณณ์ ๋์ผํ ์ด๋ฆ์ ํ์ผ์ด ์กด์ฌํ๋ค๋ฉด ์์ธ๊ฐ ๋ฐ์ํ ์ ์์ผ๋ renameDuplicatedFilename๋ฅผ ์ด์ฉํด์ rename์ ํ ์ ์๋๋ก ์ค์ ํ ์๋ ์์ต๋๋ค.
2.1. UnZip ๊ธฐ๋ฅ ํ ์คํธ
@Test
void unzipTest() throws IOException {
// given
Path dstPath = Path.of("src/test/resources/unzip-result");
FileUtils.deleteDirectory(dstPath.toFile());
final Path testZipPath = Path.of("src/test/resources/test.zip");
// when
ZipUtils.unZipFile(dstPath.toString(), testZipPath.toFile());
// then
assertThat(dstPath).exists();
assertThat(Path.of(dstPath.toString(), "test-dir", "ํ๊ธ์ด๋ฆ_ํ
์คํธ.txt")).exists();
assertThat(Path.of(dstPath.toString(), "test-dir", "file2.txt")).exists();
assertThat(Path.of(dstPath.toString(), "test-dir", "file3.txt")).exists();
assertThat(Path.of(dstPath.toString(), "test-dir", "test-dir2", "ํ๊ธ์ด๋ฆ_ํ
์คํธ.txt")).exists();
assertThat(Path.of(dstPath.toString(), "test-dir", "test-dir2", "file2.txt")).exists();
assertThat(Path.of(dstPath.toString(), "test-dir", "test-dir2", "test-dir3", "ํ๊ธ์ด๋ฆ_ํ
์คํธ.txt")).exists();
assertThat(Path.of(dstPath.toString(), "test-dir", "test-dir2", "test-dir3", "file2.txt")).exists();
assertThat(Path.of(dstPath.toString(), "test-dir", "test-dir2", "test-dir3", "test-dir4", "ํ๊ธ์ด๋ฆ_ํ
์คํธ.txt")).exists();
assertThat(Path.of(dstPath.toString(), "test-dir", "test-dir2", "test-dir3", "test-dir4", "file2.txt")).exists();
}
Unzip ํ ์คํธ๋ ๊ฐ๋จํฉ๋๋ค. unzipํ๋ ค๋ Zip ํ์ผ๊ณผ, ์์ถ ํด์ ํ ๋๋ ํ ๋ฆฌ๋ฅผ ๋๊ฒจ์ ์์ถํด์ ํ๊ณ ๋ด๋ถ์ ํ์ผ๋ค์ด ์ ์์ ์ผ๋ก ์์ฑ๋๋์ง ๊ฒ์ฆ ํฉ๋๋ค. ํ ์คํธ๋ ์ฑ๊ณต์ ์ผ๋ก ์ํ๋ฉ๋๋ค.
3. Directory ๋ค์ด๋ก๋ API ๊ตฌํ
@GetMapping(value = "/api/v1/file-zipping-and-download")
public void downloadZipFile(
@RequestParam(value = "folderName") String folderName,
HttpServletResponse response
) {
try {
byte[] bytes = ZipUtils.zipFile(Path.of(folderName).toFile());
response.setContentType("application/zip");
response.setHeader("Content-Disposition", "attachment; filename=".concat(UUID.randomUUID().toString()).concat(".zip"));
response.getOutputStream().write(bytes);
} catch (IOException e) {
throw new RuntimeException("Failed to download zip file", e);
}
}
๋ค์ด๋ก๋ ํ๋ ค๋ ๋๋ ํ ๋ฆฌ ๊ฒฝ๋ก๋ฅผ ์ ๋ ฅ๋ฐ์ ์ฐ๋ฆฌ๊ฐ ๊ตฌํํ zipFile ๋ฉ์๋๋ฅผ ํธ์ถํ์ฌ ๋ฐํ ๋ฐ์ byte[]๋ฅผ HttpServletResponse์ outputStream์ ํตํด ์จ์ฃผ๋ฉด ํด๋ผ์ด์ธํธ๋ ์๋์ ์ผ๋ก ๋ค์ด๋ฐ์ ์ ์์ต๋๋ค. ์ค์ํ ์ ์ ContentType๊ณผ Header๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ์ค์ ํด์ค์ผ ํฉ๋๋ค. ํ์ฌ๋ ๊ฐ๋จํ๊ฒ ํ ์คํธ ํ ๋ชฉ์ ์ผ๋ก Controller์ ๋ก์ง์ ์์ฑํ๋๋ฐ ๋ฐ๋ก Service ๋ ์ด์ด๋ก ์ฎ๊ธฐ๋๊ฒ ์์ธ ์ฒ๋ฆฌ ๋ฐ ์ ์ง๋ณด์ํ ๋๋ ํธ๋ฆฌํ ๊ฒ ์ ๋๋ค.
3.1. Directory ๋ค์ด๋ก๋ API ํ ์คํธ
@WebMvcTest(controllers = ZipUtilController.class)
class ZipUtilControllerTest {
@Autowired private MockMvc mockMvc;
@DisplayName("Zip Download Test")
@Test
void downloadZipFileTest() throws Exception {
// given
// when & then
mockMvc.perform(get("/api/v1/file-zipping-and-download")
.param("folderName", "src/test/resources/test-dir"))
.andDo(print())
.andExpect(status().isOk())
;
}
}
MockMvc๋ฅผ ์ด์ฉํด์ zipFile Download ํ ์คํธ๋ฅผ ํ ์ ์์ต๋๋ค.
4. Zip ํ์ผ ์ ๋ก๋ API ๊ตฌํ
@PostMapping(value = "/api/v1/zip-upload", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<String> uploadZipFile(
@RequestParam("dstPath") String dstPath,
@RequestPart("file") MultipartFile file
) {
try {
ZipUtils.unZipFile(dstPath, file.getInputStream());
} catch (IOException e) {
return ResponseEntity.ok("failed to upload zip");
}
return ResponseEntity.ok("success to upload zip");
}
์ ๋ก๋ API๋ Multipart๋ฅผ ์ด์ฉํด์ ์ ๋ก๋๋ฅผ ํ ์ ์์ต๋๋ค. ํ์ฌ API ์ฒ๋ผ ๋จ์ผ MultipartFile๋ก ๊ตฌ์ฑํด๋ ๋๊ณ , List๋ฅผ ํตํด ์ฌ๋ฌ ๊ฐ๋ฅผ ๋ฐ์์ ๋ด๋ถ์์ Zippingํ์ฌ ์ ์ฅํ ์๋ ์์ต๋๋ค.
ํ์ฌ๋ Zip ํ์ผ์ ์ ๋ก๋ ๋ฐ๋๋ค๊ณ ๊ฐ์ ํ๊ณ ZIp ํ์ผ์ ๋ฐ์์ ์ฐ๋ฆฌ๊ฐ ๋ง๋ unZipFile ๋ฉ์๋๋ฅผ ํตํด ์ฌ์ฉ์๋ก๋ถํฐ ์ ๋ ฅ๋ฐ์ ์ ์ฅ ๊ฒฝ๋ก์ ์์ถํด์ ํ ์ ์๋๋ก ๊ตฌํํ์์ต๋๋ค.
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
@DisplayName("Zip Upload Test")
@Test
void uploadZipFileTest() throws Exception {
// given
MockMultipartFile file = new MockMultipartFile("file", "test.zip", MULTIPART_FORM_DATA_VALUE, (byte[]) null);
// when & then
mockMvc.perform(multipart("/api/v1/zip-upload")
.file(file)
.param("dstPath", "test-dir"))
.andDo(print())
.andExpect(status().isOk())
;
}
MultipartFile ์
๋ก๋ API ํ
์คํธ๋ MockMvcRequestBuilders.multipart
๋ฅผ ํตํด์ ํ
์คํธํ ์ ์์ต๋๋ค. MultipartFile์ MockMultipartFile์ ์ด์ฉํด์ Mockingํ์ฌ ํ
์คํธํ๋ฉด ๋ฉ๋๋ค.
5. ํธ๋ฌ๋ธ ์ํ
5.1. MacOS์์ ์์ฑํ zip์ ๋ด๋ถ์ ๋๋ ํ ๋ฆฌ๊ฐ ์จ์ด์๋ค? (__**MAC_OS__**
)
MacOS์์๋ Zip ํ์ผ๋ค์ ๋ฉํ๋ฐ์ดํฐ๋ค์ __MAC_OS__
๋ด๋ถ์ ์ ์ฅํฉ๋๋ค. (stack overflow ์ฐธ์กฐ)
๊ทธ๋์ ํน์ ๋ชจ๋ฅผ Mac ์ ์ ๋ค์ ๋๋นํด ํด๋น ํด๋์ ๋ํด ์์ธ์ฒ๋ฆฌํด์ค์ผ ํฉ๋๋ค.
private static void unZipFile(String dstPath, InputStream inputStream, @Nullable Function<Path, Path> renameDuplicatedFilename) throws IOException {
try (ZipInputStream zipInputStream = new ZipInputStream(inputStream, Charset.forName("EUC-KR"))) {
ZipEntry entry;
...
while ((entry = zipInputStream.getNextEntry()) != null) {
final String entryName = entry.getName();
if (entry.isDirectory() || entryName.startsWith("__MACOSX")) continue;
...
}
}
}
entryName.startsWith("__MACOSX")
๋ฅผ ํตํด ํด๋น ์ด๋ฆ์ผ๋ก ์์ํ๋ ํด๋๋ ํจ์คํ๊ฒ๋ ์์ธ ์ฒ๋ฆฌํด์คฌ์ต๋๋ค.
What's inside the __MACOSX hidden folder in zip files created in Mac OS
[Java] zip ํ์ผ ๋ด ๊ตฌ์ฑ ํ์ธํ๋ ์ฝ๋
5.2. ์๋์ฐ ๋ธ๋ผ์ฐ์ ์์๋ ํ์ฅ์๊ฐ appication/zip์ด ์๋๋ผ application/x-zip-compressed์ด๋ผ๊ณ ?
์๋์ฐ์์๋ ZIP ํ์ผ์ ์ ๋ก๋ํ๋ฉด ๋ธ๋ผ์ฐ์ ์์ application/zip์ด ์๋ application/x-zip-compressed ๊ฐ์ ๋๊ฒจ์ค๋๋ค.
๋ง์ฝ ์ ๋ก๋ ํ์์ ์ ํํ๋ค๋ฉด, ์๋์ฐ ์ฌ์ฉ์๋ฅผ ์ํด application/x-zip-compressed Content-Type๋ ํ์ฉํด์ค์ผ ํฉ๋๋ค.
zip mime types, when to pick which one
5.3. ์๋์ฐ์์ ์ ๋ก๋ํ ํ์ผ์ unZipํ์ ๋, ๋ด๋ถ ํ์ผ๋ช ๋ค์ด ํน์๋ฌธ์๋ก ๋ณด์ผ ๋ (์ธ์ฝ๋ฉ ํ์ ์๋ง์)
์ฐพ์๋ณด๋ ์๋์ฐ์์๋ EUC-KR ๋ญ์๊ธฐ๋ก ์ธ์ฝ๋ฉ์ ํ..๋ค๊ณ ํฉ๋๋ค..
ํ..
private static void unZipFile(String dstPath, InputStream inputStream, @Nullable Function<Path, Path> renameDuplicatedFilename) throws IOException {
try (ZipInputStream zipInputStream = new ZipInputStream(inputStream, Charset.forName("EUC-KR"))) {
...
}
}
์ธ์ฝ๋ฉ๋ง ์ ์ฉํด์ฃผ๋ฉด ๋๊ธฐ ๋๋ฌธ์ ์ด๋ ค์ธ ๊ฒ์ ์์ต๋๋ค. ZipInputStream์ ์์ฑํ ๋, Charset.forName("EUC-KR")
์ธ์ฝ๋ฉ์ ํตํด ์์ฑํด์ฃผ๋ฉด ๋ฉ๋๋ค.
Java char set encoding problem(from UTF8 to cp866)
Java // unzip error :MALFORMED
Outro
Spring MVC์์ ์ ๊ณตํด์ฃผ๋ MultipartFile ์ธํฐํ์ด์ค๋ฅผ ์ด์ฉํ์ฌ API ๊ตฌํ, ๊ทธ๋ฆฌ๊ณ ์ง์ ๊ตฌํํด๋ณธ Zip, UnZip Util ํด๋์ค๋ฅผ ๋ง๋ค์ด๋ณด๊ณ ํ ์คํธ๋ฅผ ํด๋ดค์ต๋๋ค. Zip, UnZip ์ ํธ ํด๋์ค๋ ์ด๋ฏธ ์ ๋ช ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ(ex. apache.common.io ๋ฑ)๊ฐ ๋ง์๋ฐ์, ์ด๋ฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์์กดํ๋ ๊ฒ๋ณด๋จ ๊ธฐ๋ณธ JDK API๋ฅผ ํตํด ๊ตฌํํ๋๊ฒ ๋์ค์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ณด์ ์ทจ์ฝ์ , ์ ๋ฐ์ดํธ์ ๋ถ์ฌ ๋ฑ๋ฑ์ ๋์ฒํ๊ธฐ ์ด๋ ต๊ธฐ ๋๋ฌธ์ ์ง์ ๊ตฌํํ๋๊ฒ ์ข๋ค๊ณ ์๊ฐ์ด ๋ญ๋๋ค.
๊ทธ๋ ๊ฒ ๋๋จํ ๊ธฐ๋ฅ์ ์๋์ง๋ง์..
์์ง API ํ ์คํธ ํ๋ ๋ถ๋ถ์ด ์กฐ๊ธ ๋ฏธํกํ๊ณ ์์ฝ์ต๋๋ค. ์ด ๋ถ๋ถ์ ๋ ์ข์ ๋ฐฉ๋ฒ์ด ์์ผ๋ฉด ์ถํ์ ์ ๋ฐ์ดํธ ํ๋๋ก ํ๊ฒ ์ต๋๋ค.
๊ธด ๊ธ ์ฝ์ด์ฃผ์ ์ ๊ฐ์ฌํฉ๋๋ค!