쉽게 다가가는 최신 프로그래밍: 코틀린에서는 정규 표현을 전혀 다루지 못해 여기서 자세히 설명하겠습니다.
정규 표현(regular expression)은 문자열 패턴을 정의합니다. 정규 표현은 메타 기호(meta symbol)를 사용하기 때문에, 메타 기호의 의미를 잘 알고 있어야 합니다. DOS 창이나 Unix/Linux 쉘에서 명령어 "dir *.exe" 를 사용해 본 적 없나요? *(star)가 바로 메타 기호입니다. "dir *.exe" 는 해당 폴더에서 확장자가 exe인 모든 파일을 보여달라는 명령이죠.
정규 표현은 컴퓨터 전공자에게는 기본입니다. 프로그래밍 언어의 문법(Context Free Grammar)을 이해하려면 정규 표현을 알아야 하기 때문입니다. 정규 표현은 유한 오토마타(finite automata)로 변환할 수 있습니다. 유한 오토마타는 문자열이 정규 표현에서 정의한 패턴인지 아닌지를 판별하는 장치(=프로그램)입니다. 한글 워드 프로세서를 만들려면 한글 오토마타가 필요합니다. 한글 오토마타는 초성+중성+종성으로 구성된 한글 음절(syllable)을 찾아줍니다. 한글 오토마타는 역으로 정규 표현으로 바꿀 수 있습니다.
대부분 프로그래밍 언어는 정규 표현을 지원하는 라이브러리를 제공합니다. 프로그래밍 언어마다 메타 기호의 종류나 의미가 조금씩 다르지만, 특정 언어에서 제공하는 정규 표현만 잘 익히면 다른 언어의 정규 표현을 사용하는 데 크게 문제는 없습니다. 여기서 특정 언어는 당연히 코틀린입니다.
■ 기본적인 정규 표현 사용법: 코딩 스타일 익히기
정규 표현을 사용하려면 2개의 변수, 즉 정규 표현을 정의하는 변수 pattern과 이 정규 표현을 적용할 문자열 text가 필요합니다. 생성자 Regex()를 사용해 패턴 객체를 만들 수 있지만, toRegex()를 사용해 문자열을 정규 표현으로 변환할 수 있습니다. "Kotlin".toRegex()에서 "Kotlin"은 상수 문자열입니다. 대문자 K로 시작하는 "Kotlin"을 찾으라는 거죠. 상수 패턴은 1:1 매칭이 발생합니다.
객체 pattern의 메소드 containsMatchIn()를 호출해 문자열 text에 우리가 찾는 패턴("Kotlin")과 일치하는 문자열이 있는지 확인합니다. containsMatchIn()은 Boolean 값을 반환합니다.
fun main() {
// val pattern: Regex = Regex("Kotlin") // 생성자 Regex()를 사용해 패턴 객체를 생성
val pattern: Regex = "Kotlin".toRegex() // 문자열을 정규 표현으로 변환
val text = "Hello, Kotlin!"
if (pattern.containsMatchIn(text)) {
println("Match found!")
} else {
println("No match found!")
}
// Match found -- 문자열 "Hello, Kotlin"에 "Kotlin"이 있기 때문
}
toRegex() 대신 생성자 Regex()를 사용하는 것이 필요할 때가 있습니다. Regex()의 2번째 파라미터를 사용해 옵션을 설정할 수 있기 때문입니다. 아래 예에서 옵션 IGONORE_CASE는 대소문자를 구분하지 않는다는 설정입니다. 또 1:1 매칭되는 상수 문자열 대신 여러 상수 문자열 중 어느 하나와 매칭되는 조건을 설정할 수 있습니다. '|'는 메타 기호가 아니라 논리 OR 연산자입니다. "Kotlin|Hello"는 2개 중 어느 하나와 일치하는 문자열을 찾는 패턴입니다. 이 패턴은 2:1 매칭이 발생합니다.
val pattern: Regex = Regex("Kotlin|Hello", RegexOption.IGNORE_CASE)
val text = "HELLO, kotlin!"
사실 위 예제는 정규 표현을 사용하지 않고 String 객체의 contains() 메소드를 사용하는 게 간단합니다. 이 예제는 정규 표현의 코딩 스타일을 이해하는 것이 목적이었습니다. 이제부터 정규 표현이 제공하는 메타 기호를 사용한 예를 자세히 살펴보겠습니다.
fun main() {
if ("Hello, Kotlin!".contains("Kotlin")) {
println("Match found!")
}
}
■ 영문자로 이루어진 문자열 찾기
메타 기호를 사용해 영어 소문자 또는 대문자로 이루어진 문자열을 정의합니다. [a-z]는 영어 소문자 26개 중 하나를 의미합니다. [a-zA-Z]는 소문자 또는 대문자 중 하나를 가리킵니다. '['와 ']'가 범위를 나타내는 메타 기호입니다. [a-zA-Z] 대신 [A-Za-z]로 순서를 바꿔도 괜찮습니다. 주의할 점은 [a-Z]은 사용할 수 없습니다. '+'는 덧셈이 아니라 해당 패턴이 1번 이상 발생함을 의미하는 메타 기호입니다. [a-zA-Z]+는 길이가 1이상인 문자열입니다.
문제는 containsMatchIn()이 찾은 문자열이 무엇일까 입니다. containsMatchIn()은 text에서 패턴과 일치하는 문자열을 하나라도 찾으면 true를 반환하기 때문입니다. 문자열 text에서 우리가 정의한 패턴과 일치하는 문자열은 "Hello"와 "Kotlin" 2개이기 때문입니다.
fun main() {
val pattern = "[a-zA-Z]+".toRegex() // 길이가 1이상인 (소문자 또는 대문자로 이루어진)문자열
val text = "Hello, Kotlin!"
println(pattern.containsMatchIn(text))
// true. Hello와 Kotlin 중 어떤 문자열을 찾았을까요?
}
어떤 문자열을 찾았는지 확인하기 위해 코드를 수정합니다. 이를 위해 패턴 객체의 메소드 중 MatchResult 타입을 반환하는 메소드 matchAt()를 호출합니다. matchAt(text, 0)에서 인덱스 0은 패턴과 일치하는 문자열의 시작 위치입니다. matchAt(text, 0)은 Hello를 찾지만, matchAt(text, 7)은 Kotlin을 찾습니다. matchAt()의 반환 결과를 객체 matchResult에 저장합니다. matchResult의 타입은 널을 포함하는 MatchResult? 입니다. if 문을 사용해 matchResult가 널인지 여부를 조사해야 합니다. 객체 matchResult의 속성 range는 찾은 문자열의 범위(0..4)이며, matchResult의 속성 value는 우리가 지정한 패턴을 갖는 문자열(Hello)입니다.
fun main() {
val pattern = "[a-zA-Z]+".toRegex()
val text = "Hello, Kotlin!"
val matchResult: MatchResult? = pattern.matchAt(text, 0) // Hello를 찾음
// val matchResult: MatchResult? = pattern.matchAt(text, 7) // Kotlin을 찾음
if (matchResult != null) {
println("${matchResult.range}, ${matchResult.value}")
}
// 0..4, Hello
}
인덱스까지 지정하니 뭔가 귀찮은 작업을 한 느낌이죠! 이번에는 matchAt() 대신 findAll()을 호출합니다. findAll()은 이름 그대로 패턴과 일치하는 모든 문자열을 찾습니다. 다만, findAll()에서 2번째 파라미터 startIndex의 기본 값은 0이며, startIndex 값을 생략하면 문자열의 처음부터 찾습니다. findAll(text, 7)은 Kotlin만 찾습니다.
fun main() {
val pattern = "[a-zA-Z]+".toRegex()
val text = "Hello, Kotlin!"
val matchResult = pattern.findAll(text) // startIndex = 0. 생략 가능.
// val matchResult = pattern.findAll(text, 7) // startIndex=7, Kotlin만 찾음
for (m in matchResult) {
println("${m.range}, ${m.value}")
}
// 0..4, Hello
// 7..12, Kotlin
}
■ Wrap-up Quiz(마무리 퀴즈)
아래 실행 결과는 무엇일까요? 정규 표현에서 메타 기호 '^'을 맨 앞에 추가했습니다.
fun main() {
val pattern = "^[a-zA-Z]+".toRegex()
val text = "Hello, Kotlin!"
val matchResult = pattern.findAll(text)
for (m in matchResult) {
println("${m.range}, ${m.value}")
}
}
아래 실행 결과는 무엇일까요? 정규 표현에서 메타 기호 '$'를 맨 끝에 추가했으며, 문자열 text에 '!'(느낌표)가 없습니다.
fun main() {
val pattern = "[a-zA-Z]+$".toRegex()
val text = "Hello, Kotlin"
val matchResult = pattern.findAll(text)
for (m in matchResult) {
println("${m.range}, ${m.value}")
}
}
'코틀린' 카테고리의 다른 글
코틀린: 정규 표현(3) (0) | 2025.01.31 |
---|---|
코틀린: 정규 표현(2) (0) | 2025.01.31 |
쉬어가는 글: 람다 식은 어떻게 유래되었나요? (0) | 2025.01.27 |
코틀린: DSL(2/2) - CSV 빌더를 만들어 봅시다. (0) | 2025.01.17 |
코틀린: DSL(1/2) - CSV 빌더를 만들어 봅시다. (0) | 2025.01.17 |