이 글은 기존 운영했던 WordPress 블로그인 PyxisPub: Development Life (pyxispub.uzuki.live) 에서 가져온 글 입니다. 모든 글을 가져오지는 않으며, 작성 시점과 현재 시점에는 차이가 많이 존재합니다.
작성 시점: 2017-08-28
지금 개발하는 앱에서는
이런 구조로 작동해야 되는 것이 많다.
압축 파일을 풀어서 파일 하나하나를 작성해야 하는 만큼, BufferedOutputStream 을 이용할 수 밖에 없는데, 코틀린으로 어떻게 쓸 수 있는지 알아보기 위해서 코드를 몇 개 정도 작성했다.
결과적으론 32줄 (원본) > 20줄 (1차) > 16줄 (2차) > 14줄 (3차) > 9줄 (4차) 정도로 줄어들었다.
ZipFile 등이 안드로이드 API 24 (7.0) 이상부터 사용할 수 있어서, Apache ant를 추가했다.
compile group: 'org.apache.ant', name: 'ant', version: '1.10.1'
public void unzip(String filePath, String targetPath) {
try {
ZipFile zipFile = new ZipFile(filePath, "euc-kr");
Enumeration e = zipFile.getEntries();
while (e.hasMoreElements()) {
ZipEntry entry = (ZipEntry) e.nextElement();
File destinationFilePath = new File(targetPath, entry.getName());
destinationFilePath.getParentFile().mkdirs();
if (entry.isDirectory())
continue;
BufferedInputStream bis = new BufferedInputStream(zipFile.getInputStream(entry));
int b;
byte buffer[] = new byte[1024];
FileOutputStream fos = new FileOutputStream(destinationFilePath);
BufferedOutputStream bos = new BufferedOutputStream(fos, 1024);
while ((b = bis.read(buffer, 0, 1024)) != -1) {
bos.write(buffer, 0, b);
}
bos.flush();
bos.close();
bis.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
32줄. 매우 길다.
파라미터로는 filePath는 압축을 풀을 zip 경로, targetPath는 압축을 풀 경로이다.
우선적으론 코틀린으로 파일을 효율적으로 읽는 방법 에서도 활용한 .use 메소드를 적극 활용해서 줄여보려고 한다.
private fun unzip(zipFile: File, targetPath: String) {
val zip = ZipFile(zipFile, "euc-kr")
val enumeration = zip.entries
while (enumeration.hasMoreElements()) {
val entry = enumeration.nextElement()
val destFilePath = File(targetPath, entry.name)
destFilePath.parentFile.mkdirs()
if (entry.isDirectory)
continue
val bufferedIs = BufferedInputStream(zip.getInputStream(entry))
bufferedIs.use {
destFilePath.outputStream().buffered(1024).use { bos ->
bufferedIs.copyTo(bos)
}
}
}
}
그 외 while 문 등 기본적인 구조는 유지했다.
File.outputStream()
란 확장 메소드로 FileOutputStream 을 꺼내고, FileOutputStream.buffered(size)
로 BufferedOutputStream을 꺼낸 다음 최종적으로 use 메소드를 사용해서 바이트 어레이를 copyTo
하는 것이다.
Enumeration 이란 클래스는 객체들의 집합에서 각각의 객체를 하나에 하나씩 처리할 수 있도록 돕는 인터페이스로, 두 개의 메소드를 제공한다.
package java.util;
public interface Enumeration<E> {
boolean hasMoreElements(); // 다음 객체가 있는지
E nextElement(); // 다음 객체 리턴
}
얼핏 보면 iterator랑 비슷한것 같다.
그리고 코틀린에서는 이 클래스의 확장 메소드로 Enumeration.toList() 를 제공하는데, 열거된 순서로 Enumeration을 가져와 List 형태로 만들어준다.
그러면 언제나와 같이, 확장 메소드를 하나 더 만들어서 기능을 wrap 해보자.
private inline fun <E> Enumeration<E>.forEach(action: (E) -> Unit) {
for (element in this.toList()) action(element)
}
기본적으로 Enumeration.toList() 한 결과값을 for-each 하여 한 객체마다 action을 invoke 하는 코드이다.
성능 이슈를 줄이기 위하여 Inline Function 을 사용했고, Invoke operator를 사용했다.
위 확장 메소드를 사용해 만든 코드가 이쪽이다.
private fun unzip(zipFile: File, targetPath: String) {
val zip = ZipFile(zipFile, "euc-kr")
zip.entries.forEach {
val destFilePath = File(targetPath, it.name)
destFilePath.parentFile.mkdirs()
if (!it.isDirectory) {
val bufferedIs = BufferedInputStream(zip.getInputStream(it))
bufferedIs.use {
destFilePath.outputStream().buffered(1024).use { bos ->
bufferedIs.copyTo(bos)
}
}
}
}
}
forEach를 사용함으로서 implicit parameter (통칭 it) 가 사용이 가능해졌기 때문에, val enumeration = zip.entries
와 val entry = enumeration.nextElement()
를 없애고 it를 사용했다.
bufferedIs 를 굳이 선언하지 않아도 통합이 가능할 것 같다.
private fun unzip(zipFile: File, targetPath: String) {
val zip = ZipFile(zipFile, "euc-kr")
zip.entries.forEach {
val destFilePath = File(targetPath, it.name)
destFilePath.parentFile.mkdirs()
if (!it.isDirectory)
BufferedInputStream(zip.getInputStream(it)).use { bis ->
destFilePath.outputStream().buffered(1024).use { bos ->
bis.copyTo(bos)
}
}
}
}
위 3번의 enumeration 처럼 변수 선언을 없애고 bis 라는 변수를 내부에서 선언함으로서 bis.copyTo(bos) 형태가 되었다.
그런데 잠깐만, 마지막의 buffered(1024).use 도 역시 하나의 파라미터니까 it를 사용할 수 있지 않을까?
... 결과적으로 나온 코드가 이쪽이다.
private fun unzip(zipFile: File, targetPath: String) {
val zip = ZipFile(zipFile, "euc-kr")
zip.entries.forEach {
val destFilePath = File(targetPath, it.name)
destFilePath.parentFile.mkdirs()
if (!it.isDirectory)
BufferedInputStream(zip.getInputStream(it)).use { bis ->
destFilePath.outputStream().buffered(1024).use { bis.copyTo(it) }
}
}
}
폴더가 항상 만들어져 있다는 가정을 하면, destFilePath.parentFile.mkdirs() 를 제거하고 통합할 수 있다.
다행히 다른 클래스에서 경로를 반환할 때 항시 mkdirs를 거치도록 작성해놨기 때문에, 과감하게 제거한다.
private fun unzip(zipFile: File, targetPath: String) {
val zip = ZipFile(zipFile, "euc-kr")
zip.entries.forEach {
if (!it.isDirectory)
BufferedInputStream(zip.getInputStream(it)).use { bis ->
File(targetPath, it.name).outputStream().buffered(1024).use { bis.copyTo(it) }
}
}
}
마지막 나온 코드는 이 it가 이 it인지, 저 it가 저 it인지 구별이 쉽지 않다.
Kotlin 1.1.3 부터는 Semantic highlighting 를 제공하는데, 선택산 색상대로 변수에 색상을 지정하는 것이다.
위 기능으로 it를 쉽게 구분할 수 있게 되었다.
ZipFile도 줄일 수 있으면 더욱 좋았겠지만, 저것 만큼은 어떻게든 줄일 방법이 떠오르지 않았다.
아무튼, 32줄에서 9줄이면 약 72%의 감축을 이뤄낸 셈이 되었다.