Developing Myself Everyday
article thumbnail

파일 I/O 처리 


파일 처리는 Input / Output에 대한 데이터 처리를 말합니다. 이런 데이터가 처리되어 흐르는 `데이터의 흐름`Stream이라고 합니다.

 

자바에서는 이러한 기능을 수행하기 위해 입출력을 위한 기본 패키지인 `java.io`에 InputStreamOutputStream이 존재하고 이를 기반으로 상속하여 바이트 단위로 처리하도록 하는 ByteArrayInputStreamByteArrayOutputStream이 있습니다.

 

 

바이트 스트림

데이터를 읽고 쓸 때 바이트 스트림을 사용합니다. 사용하는 방법은 아래와 같습니다.

import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream


val inScr = byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9)
var outScr : ByteArray? = null

var input = ByteArrayInputStream(inScr)
var output = ByteArrayOutputStream()


fun main() {
    var data = input.read()
    while (data != - 1) {
        output.write(data)
        data = input.read()
    }
    println(inScr.contentToString()) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
    outScr = output.toByteArray()
    println(outScr.contentToString()) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
}

입력할 바이트 배열 객체를 만들어 변수에 할당하고, 이것들을 스트림 객체로 변환해서 변수에 할당합니다.

바이트 스트림은 `read()` 메서드로 바이트 단위로 읽을 수 있고 `write()` 메서드로 쓸 수 있습니다.

 

위의 과정은 배열의 요소를 하나 씩 읽고 쓰는 과정을 보여줍니다. 


while문에 조건으로 data != -1이 있습니다. 이는 read() 메서드가 바이트 스트림을 모두 읽었다면 -1을 반환하기 때문에 이를 체크하기 위함입니다.

 

 

버퍼 스트림

일반적인 스트림 방식에서는 1Byte 스트림 하나를 입력하면 1Byte 스트림 하나를 출력합니다.

 

그렇기 때문에 다수의 데이터를 처리할 때에는 상당히 느리게 동작합니다. 이 때, 버퍼를 사용하면 문제를 해결할 수 있습니다.

 

BufferInputStream은 바이트 입력 스트림에 연결되어서 버퍼를 제공해줍니다. BufferReader, BufferWriter는 문자 입력 스트림에 연결되어서 버퍼를 제공하고 입출력을 처리할 수 있습니다. 이러한 버퍼 클래스들은 java.nio (NIO: New Input/Output) 패키지 안에 있습니다.

 

 

BufferReader 버퍼 객체를 만들 기 위해서는 InputStream을 인자로 전달해야 합니다. BufferWriter 버퍼 객체를 만들 기 위해서는 OutputStream을 인자로 전달해야 합니다.

var input = ByteArrayInputStream(inScr)
var bfReader = BufferedReader(InputStreamReader(input))
var output = ByteArrayOutputStream()
var bfWriter = BufferedWriter(OutputStreamWriter(output))

 

 

 

close


스트림은 데이터의 흐름으로 데이터를 나르는 통로같은 역할을 합니다. 이러한 통로를 사용했다면, 통로를 닫아줘야 합니다. 통로를 닫는 것을 close라 합니다.

 

개발을 하면서 close 처리를 해야 하는 것들이 있습니다.

  • InputStream, OutputStream
  • java.io.Reader를 상속받는 부분(InputStreamReader 등등)
  • java.net.Socket
  • 등등

 

위와 같은 class들은 이런 Java 1.7에 추가되어 있는 interface AutoCloseable을 상속 받고 있는데, java 1.5에 추가된 interface Closeable에서 AutoCloseable을 다시 상속받는 형태로 구성되어 있습니다.

 

아래와 같이 InputStreamAutoCloseable을 상속받고 있는 것을 확인할 수 있습니다.

public interface AutoCloseable {
    void close() throws Exception;
}

public interface Closeable extends AutoCloseable {
    public void close() throws IOException;
}

public abstract class InputStream implements Closeable {
	
    ...
    
    public static InputStream nullInputStream() {
        return new InputStream() {
            private volatile boolean closed;

            private void ensureOpen() throws IOException {
                if (closed) {
                    throw new IOException("Stream closed");
                }
            }

            ...

            @Override
            public void close() throws IOException {
                closed = true;
            }
        };
    }
}

 

 

close 예외 처리

아래와 같이 close처리를 하는 코틀린 함수가 있을 때 AutoCloseable Exception을 던질 수 있기 때문에 예외 처리를 해야 합니다.

@Test
fun test() = runBlocking {
    val socket = Socket("thdev.tech", 80)
    val inputStream = socket.getInputStream()
    val reader = InputStreamReader(inputStream)
    println(reader.readText())
    reader.close()
    inputStream.close()
    socket.close()
}

 

 

코틀린의 예외처리는 try/catch/finally를 사용해 아래와 같이 예외처리를 해야합니다.

@Test
fun test() = runBlocking {
    var socket: Socket? = null
    var inputStream: InputStream? = null
    var reader: InputStreamReader? = null
    try {
        socket = Socket("thdev.tech", 80)
        inputStream = socket.getInputStream()
        reader = InputStreamReader(inputStream)
        println(reader.readText())
    } catch (e: Exception) {
        // ...
    } finally {
        socket?.close()
        inputStream?.close()
        reader?.close()
    }
}

 

 

use 확장 함수

이러한 복잡한 코드를 `use()` 를 사용하면 close를 내부에서 처리해 간단하게 만들 수 있습니다.

 

내부 코드를 보면 제네릭으로 Cloeable 객체를 받고 이를 사용한 다음 자동으로 닫아주는 코드를 구현하고 있습니다.

@InlineOnly
public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    var exception: Throwable? = null
    try {
        return block(this)
    } catch (e: Throwable) {
        exception = e
        throw e
    } finally {
        when {
            apiVersionIsAtLeast(1, 1, 0) -> this.closeFinally(exception)
            this == null -> {}
            exception == null -> close()
            else ->
                try {
                    close()
                } catch (closeException: Throwable) {
                    // cause.addSuppressed(closeException) // ignored here
                }
        }
    }
}

 

다만 위에 코드에서도 여전히 exception을 던지고 있기 때문에 use 사용할 때에도 예외 처리를 하는 것이 좋습니다. use를 사용하면 위의 복잡한 코드를 아래와 같이 바꿀 수 있습니다.

@Test
fun test() = runBlocking {
    try {
        Socket("thdev.tech", 80).use {
            it.getInputStream().use {
                    println(it.readLines())
                }
            }
        }
    } catch (e: Exception) {
         ...
    }
}

 

 

 

 

파일 처리


파일 읽기

파일을 읽을 때에는 바이트 단위로 읽는 것이 기본입니다. 그럼 바로 위에서 배운 바이트 스트림과 버퍼 스트림 방식으로 파일을 읽을 수 있습니다.

 

파일을 입력 바이트 스트림으로 만드는 방법은 아래와 같습니다.

val fin = FileInputStream("/data.txt")

 

아래와 같이 `inputStream()` 확장함수를 이용하면 동일한 결과를 얻을 수 있습니다.

@kotlin.internal.InlineOnly
public inline fun File.inputStream(): FileInputStream {
    return FileInputStream(this)
}

val fin = File("/data.txt").inputStream()

 

위에서 BufferedReader를 만들기 위해서 했던 방식도 `bufferReader()` 확장함수로 동일하게 수행 가능합니다.

@kotlin.internal.InlineOnly
public inline fun InputStream.bufferedReader(
	charset: Charset = Charsets.UTF_8
): BufferedReader = reader(charset).buffered()

val bfReader = FileInputStream("/data.txt").bufferedReader()

 

 

useLines

`useLines()` 확장 함수를 이용하면 use를 사용함과 동시에 `readLine`을 함께 할 수 있습니다.

public inline fun <T> Reader.useLines(block: (Sequence<String>) -> T): T =
    buffered().use { block(it.lineSequence()) }

`useLines()` 확장 함수는  BufferedReader에 있는 전체 파일 데이터를 Sequence<String>으로 반환합니다.

 

 

파일 쓰기

파일을 새로 만들거나 기존 파일을 새로 작성할 때 파일 쓰기를 사용합니다. 이를 위해서는 FileWriter 클래스를 사용합니다.

val fileName = "data.txt"
val fw = FileWriter(fileName)
val writer = BufferedWriter(fw)
writer.append("content")
writer.flush()
writer.close()

 FileWriter 에는 파일 이름을 지정합니다. 이를 BufferedWriter에 넘겨줘 파일 버퍼를 생성한 다음 버퍼에 씁니다.

 

 

기존 파일을 새로 작성하려면 File 클래스를 사용해 파일을 다시 가져와서 write하면 됩니다.

File(fileName).bufferedWriter()
    .use { out -> out.write("new content") }

 

 

 

 

프로세스(Process)와 스레드(Thread)


프로세스는 실행되고 있는 프로그램을 말합니다. 스레드는 어떠한 프로그램, 프로세스 내에서 실행되는 흐름의 단위를 말합니다.

 

JVM은 하나의 프로세스의 여러개의 스레드를 가질 수 있는 `멀티 스레드` 방식을 사용합니다.

 

 

프로세스는 자신만의 고유한 공간을 할당받아서 사용합니다. 스레드는 이 공간에서 스택 영역만 따로 할당받아서 처리합니다.

 

 

스레드

스레드는 프로세스의 자원을 사용할 때, 자신만 사용할 수 있도록 합니다. 그렇기에 다른 스레드들은 해당 공유 자원에 접근할 수 없게 블로킹(Blocking)됩니다.

 

스레드는 보통 메인 스레드와 백그라운드 스레드로 나눠집니다.

 

1. 메인 스레드

액티비티를 포함해 모든 컴포넌트가 실행되는 오직 한 개만 존재하는 스레드입니다. 

  • 프로그램에 실행될 때 자동으로 생성되는 기본 스레드입니다.
  • 화면의 UI를 그리는 처리를 담당합니다.
  • 메인 애플리케이션 로직 실행 등과 같은 중요한 작업을 담당합니다.
  • 메인 스레드는 플로킹되면 안됩니다.
  • 메인 스레드는 non-daemon 스레드로 

 

2. 백그라운드 스레드

메인 스레드 외에 생성되는 스레드로, 보조 작업을 수행하는 데 사용됩니다.

  • 백그라운드 스레드는 메인 스레드와 병렬로 실행되어 여러 작업을 동시에 처리할 수 있습다.
  • 파일 다운로드, 데이터베이스 작업, 네트워크 요청 등은 주로 백그라운드 스레드에서 처리됩니다.
  • 백그라운드 스레드는 애플리케이션의 반응성을 유지하면서 오래 걸리는 작업을 분리하여 실행함으로 사용자 경험을 향상시킵니다.

 

우리가 생성하는 스레드는 기본적으로 모두 백그라운드 스레드입니다.

 

 

스레드 만들기

스레드를 만들기 위해서는 Thread 클래스를 상속하거나 Runnable 인터페이스와 Thread 클래스를 사용해야 합니다.

 

Thread 클래스는 Runnable 인터페이스를 구현하고 있습니다.

public
class Thread implements Runnable {
    ...
}

 

 

Runnable 인터페이스는 해당 클래스가 스레드에 의해 실행되도록 의도된 경우 구현해야 하는 인터페이스입니다.

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

Runnable 인터페이스를 구현하는 객체를 사용하여 스레드를 생성할 때, 스레드를 시작하면 해당 객체의 `run()` 메서드가 별도로 실행되는 스레드에서 호출됩니다.

 

 

스레드 객체 생성

아래와 같이 스레드 객체를 생성하고 메서드를 사용할 수 있습니다.

fun main() {
    val thread = Thread()
    println(thread.isAlive) // false
    thread.start()
    println(thread.isAlive) // false
    println(thread.isDaemon) // false
}

 

 

스레드 클래스 상속

클래스에서 클래스를 상속받아서 별도의 스레드를 생성할 수도 있습니다. `start()` 메서드를 실행하면 `run()` 메서드가 자동으로 실행됩니다.  `run()` 메서드에는 스레드가 실행될 코드를 작성합니다.

fun main() {
    val tr = Thread.currentThread()
    println(tr) // Thread[main,5,main]
    val myThread = MyThread()
    myThread.start()
    myThread.join()
}

class MyThread: Thread() {
    override fun run() {
        val tr = Thread.currentThread()
        println(tr) // Thread[Thread-0,5,main]
    }
}

`join()` 메서드를 사용하면 해당 스레드가 종료될 때까지 기다립니다.
`sleep()` 메서드를 사용하면 스레드를 잠시 중단하고 다른 스레드를 처리한 후 다시 자신의 스레드를 작동할 수 있게 만듭니다.

 

 

코틀린 스레드

코틀린에서는 이러한 스레드를 쉽게 생성할 수 있는 함수를 제공합니다. 내부적으로 object를 사용해 스레드를 생성하는 것을 확인할 수 있습니다.

public fun thread(
    start: Boolean = true,
    isDaemon: Boolean = false,
    contextClassLoader: ClassLoader? = null,
    name: String? = null,
    priority: Int = -1,
    block: () -> Unit
): Thread {
    val thread = object : Thread() {
        public override fun run() {
            block()
        }
    }
    if (isDaemon)
        thread.isDaemon = true
    if (priority > 0)
        thread.priority = priority
    if (name != null)
        thread.name = name
    if (contextClassLoader != null)
        thread.contextClassLoader = contextClassLoader
    if (start)
        thread.start()
    return thread
}

 

다음은 코틀린 스레드에서 사용되는 매개변수들입니다.

  • start: 스레드를 바로 시작할지 (Default: true)
  • isDaemon: 스레드를 데몬모드로 시작할지 (Default: true), 데몬 모드는 JVM의 종료를 방해하지 않고 메인 스레드가 종료될 때 자동으로 함께 종료된다.
  • contextClassLoader: 스레드 코드가 클래스와 자원을 적재할 때 상용할 클래스 로더 (Default:null)
  • name: 커스텀 스레드 이름 (Default: null)
  • priority: Thread.MIN_PRIORITY(=1) 부터 Thread.MAX_PRIORITY(=10) 사이의 갑승로 정해지는 우선순위 (Default:1)
  • block: () -> Unit 타입의 함숫값으로 새 스레드가 생성되면 실행할 코드

 

실제로 사용한 예는 아래와 같습니다.

import kotlin.concurrent.thread

fun main() {
    println("스레드 시작하기...")

    thread(name = "준형", isDaemon = true) {
        for (i in 1..5) {
            println("${Thread.currentThread().name}: $i")
            Thread.sleep(150)
        }
    }

    Thread.sleep(500)
    println("스레드 종료하기...")
}

스레드 시작하기...
준형: 1
준형: 2
준형: 3
준형: 4
스레드 종료하기...

 

 

 

 

Thread Pool


스레드를 생성하는 법을 알아보았습니다. 하지만 이렇게 스레드를 늘리면 하드웨어의 무리가 갑니다. 그렇기 때문에 Thread Pool이라는 개념이 생겨났습니다.

 

Thread Pool은 작업 처리에 사용되는 스레드를 제한된 개수만큼 정해 놓고 작업 큐에 들어오는 작업들을 하나씩 스레드가 맡아 처리하는 것을 말합니다. 코틀린에서는 Thread Pool을 사용하기 위해 Executor Service 인터페이스를 활용합니다. Executor Service는 스레드 풀을 관리하고 작업을 스케줄링하기 위한 메서드를 제공합니다.

 

Executor의 `newFixedThreadPool`을 사용하면 사용자가 원하는 개수의 스레드를 만듭니다.

val executorService = Executors.newFixedThreadPool(4)

 

사용할 수 있는 Thread Pool의 종류는 다음과 같습니다.

  1. FixedThreadPool: 고정된 크기의 스레드 풀을 생성합니다. 지정된 수의 스레드로 작업을 처리합니다. 추가적인 작업은 작업 큐에서 대기하게 됩니다.
  2. CachedThreadPool: 필요에 따라 스레드를 동적으로 생성 및 제거하는 스레드 풀입니다. 작업 요청이 증가하면 새로운 스레드를 생성하고, 스레드가 유휴 상태로 장시간 대기하면 스레드를 제거합니다. 크기를 동적으로 조정하여 작업 부하에 최적화됩니다.
  3. SingleThreadExecutor: 하나의 스레드만을 사용하는 스레드 풀입니다. 작업 큐에 있는 작업을 순차적으로 처리합니다. 주로 순차적인 작업이 필요한 경우에 사용됩니다.
  4. ScheduledThreadPool: 일정한 시간 간격으로 작업을 실행하는 스레드 풀입니다. 예약된 작업을 처리할 수 있으며, 일정한 주기로 작업을 반복 실행할 수도 있습니다.

 

아래는 스레드 풀을 사용하여 실행한 예시입니다.

import java.util.concurrent.Executors

fun main() {

    val executorService = Executors.newFixedThreadPool(2)

    for (i in 1..5) {
        executorService.submit {
            println("Task $i is being processed by ${Thread.currentThread().name}")
            Thread.sleep(1000)
            println("Task $i is completed by ${Thread.currentThread().name}")
        }
    }

    executorService.shutdown()
}

 

위의 코드를 출력하면 아래와 같은 출력이 나옵니다. 잘 보게 되면 스레드 풀의 크기가 2이기에 2개의 스레드가 작업이 시작되고 2개의 스레드의 작업이 종료되어야 다음에 2개의 스레드의 작업이 시작되는 것을 확인할 수 있습니다.

Task 2 is being processed by pool-1-thread-2
Task 1 is being processed by pool-1-thread-1
Task 1 is completed by pool-1-thread-1
Task 2 is completed by pool-1-thread-2
Task 3 is being processed by pool-1-thread-2
Task 4 is being processed by pool-1-thread-1
Task 4 is completed by pool-1-thread-1
Task 3 is completed by pool-1-thread-2
Task 5 is being processed by pool-1-thread-1
Task 5 is completed by pool-1-thread-1

 

 

 

Reference

 

Kotlin extensions use를 알아보고, 사용법을 알아보자. |

I’m an Android Developer.

thdev.tech

 

profile

Developing Myself Everyday

@배준형

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!