Kotlin

[Kotlin] 문자열 & 정규식

sh1mj1 2024. 1. 4. 14:04

Kotlin in Action 을 공부하고 Effective kotlin 의 내용을 조금 참조하여 정리한 글입니다.

 

이미지 출처     https://commons.wikimedia.org/wiki/File:Kotlin_Icon.svg

 

 

코틀린 문자열은 자바 문자열과 같다.

모든 코틀린 문자열을 자바 메서드에 넘겨도, 자바 문자열을 코틀린 메서드에 넘겨도 괜찮다.

특별한 변환도 필요없고 별도의 `Wrapper`(래퍼)도 필요 없다.

코틀린의 다양한 확장 함수 덕분에 표준 자바 문자열을 더 쉽게 다룰 수 있다. 

문자열 나누기

코틀린에서의 문자열을 구분 문자열에 따라 나누는 작업을 알아보자

 

먼저 자바의 `split` 메서드를 간단히 사용해보자.

public void splitString() {
    String str = "12.345-6.A";
    String[] strings = str.split(".");

    for (String string : strings) {
        System.out.println(string); // 아무것도 출력되지 않음
    }
}

코드의 의도는 문자열을 점(`.`) 으로 분리하는 것이다.

그런데 의도대로 동작하지 않는다.

실제로는 아무것도 출력되지 않는다. `strings` 는 빈 문자열이 된다.

 

`split` 의 구분 문자열은 실제로는 정규식(regular expression) 이기 때문이다.

즉, 마침표(`.`) 는 모든 문자를 나타내는 정규식으로 해석된다.

원래의 의도대로 라면 `.` 이 아닌 `\\.` 으로 수정해야 한다.

 

코틀린에서는 자바의 `split` 대신 여러 다른 조합의 파라미터를 받는 `split` 확장 함수를 제공하여 혼동을 야기하는 메서드를 감춰준다.

`split` 함수에 전달하는 값의 타입에 따라 정규식이나 일반 텍스트 중 어느 것으로 문자열을 분리하는지 쉽게 알 수 있다.

인텔리제이의 자동완성 기능을 보면 `Regex` 와 `Char`, `String` 등으로 패러미터를 받을 수 있는 것을 확인할 수 있다.

따라서 코틀린에서는 `split` 함수에 전달하는 값의 타입에 따라 정규식이나 일반 텍스트 중 어느 것으로 문자열을 분리하는지 쉽게 알 수 있다.

 

아래 코드는 마침표(`.`)나 대시(`-`) 로 문자열을 분리하는 예이다. 

fun splitString(string: String) = println(string.split("[.\\-]".toRegex()))

@Test
fun splitStringTest() = splitString("12.345-6.A")
결과
[12.345, 6.A]

당연히 코틀린 정규식 문법은 자바와 같다.

정규식을 처리하는 API 는 조금 더 코틀린답게 변경되었다.

예를 들어 `toRegex()` 라는  확장 함수를 통해 정규식을 명시적으로 변환한다.

 

이러한 간단한 경우에는 꼭 정규식을 쓸 필요가 없다.

정규식은 꽤나 무거운 객체이다. 정규식이 아닌 여러 구분 문자열을 사용하여 문자열을 분리해보자.

fun splitString2(string: String) = println(string.split(".", "-"))

@Test
fun splitString2Test() = splitString2("12.345-6.A")
결과
[12.345, 6.A]

`split` 확장 함수를 오버로딩한 버전 중에서는 구분 문자열을 하나 이상 인자로 받는 함수가 있다.

혹은 `string.split('.', '-')` 처럼 `String` 대신 `Char` 을 인자로 넘겨도 마찬가지 결과를 볼 수 있다.

이렇게 여러 문자를 받을 수 있는 코틀린 확장 함수는 자바에 있는 단 하나의 문자만 받을 수 있는 메서드를 대신한다.

정규식 & 3중 따옴표로 묶은 문자열

이번에는 다른 예를 들어보자.

어떤 파일의 전체 경로명을 디렉토리, 파일 이름, 확장자로 구분해 볼 것이다.

코틀린 표준 라이브러리에는 어떤 문자열에서 구분 문자열이 맨 나중(OR 처음)에 나타난 곳 뒤(OR 앞)의 부분 문자열을 리턴하는 `String `확장 함수가 있다.

  1. `String` 확장 함수를 사용하여 구현하기

fun parsePath1(path: String) {
    val directory = path.substringBeforeLast("/")
    val fullName = path.substringAfterLast("/")
    val fileName = fullName.substringBeforeLast(".")
    val extension = fullName.substringAfterLast(".")
    println("Dir: $directory, name: $fileName, ext: $extension")
}

코틀린에서는 정규식을 사용하지 않고도 문자열을 쉽게 파싱할 수 있다.

정규식은 강력하지만, 나중에 알아보기  힘든 경우가 있으며, 무거운 객체이다.

정규식이 필요할 때는 코틀린 라이브러리를 사용하면 더 편하다.

결과는 위와 아래 방법 모두 같다.

Dir: /Users/sh1mj1/kotlin-book, name: chapter, ext: adoc

  2. 정규식 사용하기

fun parsePath2(path: String) {
    val regex = """(.+)/(.+)\.(.+)""".toRegex()
    val matchResult = regex.matchEntire(path)
    if (matchResult != null) {
        val (directory, fileName, extension) = matchResult.destructured
        println("Dir: $directory, name: $fileName, ext: $extension")
    }
}

이 코드에서는 3중 따옴표 문자열을 사용하여 정규식을 썼다. 

3중 따옴표 문자열에서는 `\` 를 포함한 어떤 문자열도 escape 할 필요가 없다.

일반 문자열에서 마침표 기호를 escape 하려면 `\\.` 라고 써야 하지만, 3중 따옴표 문자열에서는 `\.` 라고 쓰면 된다.

 

이 예제에서 쓴 정규식은 슬래시와 마침표를 기준으로 `path` 를 세 그룹으로 분리한다.

첫 그룹인 `(.+)` 는 마지막 슬래시 이전 부분 문자열이다.

두 번째 그룹은 마지막 마침표 이전이면서 마지막 슬래시 이후인 부분 문자열이다.

세 번째 그룹은 나머지 모든 문자가 들어간다.

정규식 엔진은 각 패턴을 가능한 가장 긴 부분 문자열과 매치하려고 한다.
그래서 `"path/to/dir/filename.ext"` 에서는 (.+)/ 와 일치하는 패턴을 찾으면 `"path"` 가 아닌 `"path/to/dir"` 라는 부분 문자열이 된다. 
이와 관련해 정규식 문서에서 greedy 나 lazy 를 찾아보면 좋다.

 

그렇게 정규식으로 `MathResult` 타입의 `mathResult` 를 생성한다.

이것이 성공하면 그룹별로 분해한 매치 결과를 의미하는 `destructured` 프로퍼티를 각 변수에 대입한다. 

구조 분해 선언을 사용한 것이다.

이에 대해 자세한 내용은 나중에 다시 다루겠다.

public interface MatchResult {
    public open val destructured: kotlin.text.MatchResult.Destructured /* compiled code */
    // ...
}​

`MathResult` 인터페이스와 그 프로퍼티 `destructured`

여러 줄 3중 따옴표 문자열

3중 따옴표 문자열은 줄 바꿈을 나타내는 아무 문자열이나 그대로 들어간다.

즉, 줄바꿈이 있는 텍스트를 쉽게 만들 수 있다.

여러 줄 문자열에는 들여쓰기나 줄 바꿈을 포함한 모든 문자가 들어간다. 

fun multilineString() {
    val kotlinLogo = """|  //
                       .| //
                       .|/ \"""
    println(kotlinLogo.trimMargin("."))
}
결과

multiline String 을 코드에서 더 보기 좋게 표현하고 싶다면 들여쓰기를 하되 들여쓰기의 끝부분을 특별한 문자열로 표시하고 `trimMargin` 을 사용해서 그 문자열과 그 직전의 공백을 제거한다.

위 예제 코드에서는 `.` 를 구분 문자열로 사용했다.

참고로 `trimMargin` 의 구분문자열 기본값은 `|` 이다.

 

multiline String 에는 줄 바꿈이 들어가지만 줄 바꿈을 `\n` 특수문자를 사용할 수는 없다.

반면, `\` 를 문자열에 넣고 싶으면 따로 escape 할 필요도 없다.

3중 따옴표 문자열 안에 String Template(문자열 템플릿)을 사용할 수도 있다.

만약 `$` 라는 문자를 문자열 안에 넣어야 한다면 escape 할 수 없기 때문에 `${'$'}` 처럼 문자열 템플릿 안에 '$' 문자를 넣어야 한다.

3중 따옴표 사이에 HTML 도 넣을 수 있다. 

코틀린 코딩 컨벤션에서의 3중 따옴표 문자열

코틀린 코딩 컨벤션 문서에서 String convention 에 대한 내용이 있다.

  • 문자열 연결보다 String Template 을 권고한다.
  • 일반 문자열 리터럴에 escape 시퀀스를 삽입하는 것보다 multiline String 을 권고한다.
  • multline String 에서 들여쓰기를 유지하려면 결과 문자열에 내부 들여쓰기가 필요하지 않은 경우에는 TrimIndent를 사용하고,
    내부 들여쓰기가 필요한 경우에는 TrimMargin을 사용하라.
println("""
    Not
    trimmed
    text
    """
       )

println("""
    Trimmed
    text
    """.trimIndent()
       )

println()

val a = """Trimmed to margin text:
          |if(a > 1) {
          |    return a
          |}""".trimMargin()

println(a)

// print
/*

    Not
    trimmed
    text
    
Trimmed
text

Trimmed to margin text:
if(a > 1) {
    return a
}
*/

자바의 multiline String 사용법 차이

Java 15 이전에는 여러 줄 문자열을 만드는 방법 중 예로 `String` 클래스의 `join()` 함수를 사용할 수 있다.

String lineSeparator = System.getProperty("line.separator");
String result = String.join(lineSeparator,
       "Kotlin",
       "Java");
System.out.println(result);

Java 15에서는 텍스트 블록이 생겼다.

주의할 점은 multiline String 을 출력하고 삼중 따옴표가 다음 줄에 있으면 추가 빈 줄까지 출력된다.

String result = """
    Kotlin
       Java
    """;
System.out.println(result);

 

 

 

 

참조

https://kotlinlang.org/docs/coding-conventions.html#strings

https://kotlinlang.org/docs/java-to-kotlin-idioms-strings.html#use-multiline-strings