코틀린에서 빠르게 입출력하기

순순·2025년 1월 5일

자바나 코틀린에서 제공해주는 입출력 함수는 아주 다양하다. 그런데 알고리즘을 풀이할 때 어떤 조합을 사용해야 입출력이 가장 빠를까? 그리고 왜 다른 것들에 비해 빠를까?

이전 포스팅 에서 split 과 stringTokenizer 에 대해서만 알아봐서 아쉬웠는데, 그 연장선으로 조금 더 자세하게 정리해보고 싶었다.

이번 글에서는 어떤 입출력 함수들이 있는지 알아보고, 위에서 말한 궁금증을 해결하기 위해 시도해본 것들을 정리해보려고 한다.
참고로 컴파일 에러는 import java.io.StreamTokenizer 등 import 를 까먹고 안해줘서 발생했으니 나처럼 까먹지 말자...


가장 많이 쓰이는 함수들


기본 입출력

readln(), readLine(), print(), println()

빠른 입력

System.'in'.bufferedReader()
StringBuilder()

빠른 출력

System.out.bufferedWriter()
StringBuilder()

입력된 문자열 쪼개기

입력값을 받고 공백 등으로 구분할 경우
split() , StringTokenizer()


입출력 함수 알아보기


(1) readln, readLine

readln 과 readLine 은 같은 기능을 수행한다. (단, readln 은 null 이면 예외를 발생시킨다)

    // readln
    fun ex1() {
        val name1 = readln().toInt()
        val age1 = readln().toInt()
    
        println("name1: $name1, age1: $age1")
    }
    
    // readLine
    fun ex2() {
        val br = BufferedReader(InputStreamReader(System.`in`))
        val name2 = br.readLine().toInt()
        val age2 = br.readLine().toInt()
    
        println("name2: $name2, age2: $age2")
    }

(2) bufferedReader

Scanner 는 사용자가 값을 입력할 때마다 전달하지만, BufferReader 는 입력을 마친 뒤 한번에 전달하기 때문에 더 빠르다. 참고로 br.read() 는 아스키코드 반환하는거라 readLine() 을 써줘야 한다.

또한 마지막에 flush 또는 close 꼭 해줘서 버퍼를 비워야한다. (단, 반복문 안에 선언하면 성능 저하되니 유의)

    fun main() {
    	// bufferedReader 로 입력받고 bufferedWriter 로 출력
      val br = System.`in`.bufferedReader()
      val bw = System.out.bufferedWriter()
    
    	val userInput = br.readLine()
    	// 출력
    	bw.write(userInput)
    
    	// 또는 bw.close()
    	bw.flush()
    }	

(3) stringBuilder

생성된 후 내용이 변경 될 수 있는 문자열을 처리할 때 빠르다. 왜냐하면 String 은 불변객체다. 따라서 문자열을 수정해야 할 때, 기존 참조하고 있던 곳에 추가하는 것이 아니라 새로 변경할 값으로 String 객체를 생성하고 참조를 바꾼다. 그러나 StringBuilder 는 가변객체다. 기존에 참조하고 있는 값을 바꾸기 때문에 String 보다 빠르게 처리할 수 있는 것이다.

val sb = StringBuilder("Hello")
println(sb) // Hello

stringBuilder 메서드 (입출력)

  • append() : 문자열의 끝에 주어진 값을 추가한다.
  • insert() : 지정된 인덱스 위치에 문자열 또는 다른 타입의 데이터 삽입
  • delete(start, end) : 시작 인덱스~끝 인덱스 전까지 문자들을 삭제
  • deleteCharAt() : 특정 인덱스의 문자 하나 삭제
  • reverse() : 문자열의 순서를 반전
  • replace() : 지정된 범위의 문자열을 새로운 문자열로 대체
  • toString() : StringBuilder 객체를 일반 문자열로 변환
  • clear() : StringBuilder 객체의 모든 내용을 지우고, 빈 상태로 초기화
  • 코드 예시
    sb.append("World")
    println(sb) // "Hello World
    
    sb.insert(6, "Kotlin")
    println(sb) // Hello Kotlin World
    
    sb.delete(6, 12)
    println(sb) // Hello World
    
    sb.reverse()
    println(sb) // dlroW olleH
    
    sb.replace(0, 5, "Hi")
    println(sb)
    
    val resultString = sb.toString()
    
    sb.clear()
    println(clear)
    

문자열 분리 알아보기


(bufferedReader 또는 StringBuilder 등으로) 문자열 입력을 받았을 때, 공백 혹은 컴마 같은 특정 문자를 기준으로 문자열을 나누고 싶을 때 사용한다.

split, chunked는 배열로 돌아오기 때문에 StringToKenizer 보다 살짝 느리다.

(1) split

기본 split

    // 일반
    fun main() {
    	val text = "Hello World"
    	val split = text.split(" ")
    	println(split) // [Hello, World]
    }

String 을 다른 형태로 바꿔줘야 할 때 map 을 사용한다.

    // 형변환이 필요할 때
    // readln() + split()
    fun main(){
        val (x,y) = readln().split(" ").map { it.toInt() }
        println("$x $y")
    }
    // 형변환이 필요할 때
    // bufferedReader() + split()
    fun main() {
        val br = System.`in`.bufferedReader()
        val bw = System.`out`.bufferedWriter()
    
        val (a, b) = br.readLine().split(" ").map { it.toInt() }
    
        bw.write("${a + b}")
        br.close()
        bw.close()
    }

bufferedReader 혹은 bufferedWriter 사용 시, close 로 메모리 관리하는 게 번거롭다면 아래처럼 use 를 써도 된다. use 블록을 벗어나면 스트림이 자동으로 닫히므로 close()를 명시적으로 호출할 필요가 없다.


    fun main() {
        System.`in`.bufferedReader().use { br ->
            System.out.bufferedWriter().use { bw ->
                val (a, b) = br.readLine().split(" ").map { it.toInt() }
                bw.write("a + b = ${a + b}")
            }
        }
    }

(2) chunked

    fun main() {
    	val text = "HelloWorld"
    	val chunked = text.chunked(2)
    	println(chunked) // [He, ll, ow, or, ld]
    }

(3) StringTokenizer

지정한 구분자로 문자열을 쪼개주는 클래스. 문자열을 토큰화 한다는 것은 하나의 문자열을 여러 개의 토큰으로 분리한다는 의미이다.

    fun main() {
       val str = "a b c d"
       val st = StringTokenizer(str) // 문자열을 쪼갬, 미지정시 공백 기준
    
    }

(4) StreamTokenizer

  • 스트림 형식으로 토큰화 한다.
  • 공식문서 참고
  • 토큰 생성 방법 .nextToken()
  • 아래 메소드로 토큰을 불러올 수 있다.
    • double 일 때 .nval
    • String 일 때 sval
    • int 일 때 ttype
fun main() {
    val tokenizer = StreamTokenizer(InputStreamReader(System.`in`))

    // 첫 번째 숫자 읽기
    tokenizer.nextToken()
    val num1 = tokenizer.nval.toInt()

    // 두 번째 숫자 읽기
    tokenizer.nextToken()
    val num2 = tokenizer.nval.toInt()

    // 두 숫자 더하기
    println(num1 + num2)
}

그래서 어떤 것이 가장 빠른가


BufferedReader 와 StringTokenizer 를 함께 썼을 때 가장 빨랐다. 아래 소요시간별 코드 참고.

의외의 성능차이

System.in.bufferedReader()
vs BufferedReader(InputStreamReader(System.in))

같은 BufferedReader 도 선언 방식에 따라 소모 시간이 다르다는 점이 흥미로웠다. 후자가 전자보다 빨랐다.

이유를 찾아보니 전자는 Kotlin에서 제공하는 확장 함수였기 때문인데, IDE 에서 System.in.bufferedReader() 코드를 타고 들어가면 아래와 같은 내용을 확인할 수 있다.

결국 System.in.bufferedReader()는 자바의 BufferedReader(InputStreamReader(System.in)) 를 좀 더 편리하게 사용할 수 있도록 한겹 감싸고 있던 코드였던 것이다.

또, 각각 다른 변수에 담아서 print로 출력하는 것과 변수에 담지 않고 바로 print 하는 것 사이에도 의외로 시간 차이가 났다. 사실 내 기준으로 5번 코드가 익숙하고 깔끔해서 좋은데 다른 코드가 더 빠른 것은 조금 아쉬운 결과였다.


(1) 80ms

fun main() = with(StreamTokenizer(System.`in`.bufferedReader())){
    fun nextInt() : Int { nextToken(); return nval.toInt() }

    val a = nextInt(); val b = nextInt()
    print(a+b)
    
 	// print(nextInt()+nextInt()) 도 동일하게 80ms
}

(2) 80ms

fun main() {
    val br = System.`in`.bufferedReader()
    val st = StringTokenizer(br.readLine())
    print(Integer.parseInt(st.nextToken()) + Integer.parseInt(st.nextToken()))
}

(3) 72ms

fun main() {
    val br = BufferedReader(InputStreamReader(System.`in`))
    val st = StringTokenizer(br.readLine())
    print(Integer.parseInt(st.nextToken()) + Integer.parseInt(st.nextToken()))
}

(4) 88ms


fun main() {
    val br = BufferedReader(InputStreamReader(System.`in`))
    val st = StringTokenizer(br.readLine())
    val a = Integer.parseInt(st.nextToken())
    val b = Integer.parseInt(st.nextToken())
    print(a+b)
}

(5) 112ms

fun main() {
   val br = System.`in`.bufferedReader()
   val bw = System.out.bufferedWriter()

    val (a, b) = br.readLine().split(" ").map { it.toInt() }
    bw.write("${a+b}")

    br.close()
    bw.close()
}

참고한 블로그


https://velog.io/@stdiodh/Kotlin-Fast-IO-빠른-입출력

https://velog.io/@alswp006/StringTokenizer와-StreamTokenizerfeat.StringTokenizer뜯어보기

profile
플러터와 안드로이드를 공부합니다

0개의 댓글