[Kotlin IN ACTION] 03장 함수 정의와 호출2
3.5 문자열과 정규식 다루기
코틀린은 다양한 확장 함수를 제공함으로써 표준 자바 문자열을 더 즐겁게 다루게 해준다.
또한 혼동이 야기될 수 있는 일부 메소드에 대해 더 명확한 코틀린 확장 함수를 제공함으로써 프로그래머의 실수를 줄여준다.
3.5.1 문자열 나누기
코틀린은 다양한 확장 함수를 제공함으로써 표준 자바 문자열을 더 즐겁게 다루게 해준다.
또한 혼동이 야기될 수 있는 일부 메소드에 대해 더 명확한 코틀린 확장 함수를 제공함으로써 프로그래머의 실수를 줄여준다.
3.5.1 문자열 나누기
“12.345-6.A”.split(“.”)라는 호출의 결과가 [12, 345-6,A] 배열이라고 생각하는 실수를 저지르는 개발자가 많다.
하지만 자바의 split 메소드는 빈 배열을 반환한다. split의 구분 문자열은 실제로는 정규식이기 때문이다.
코틀린에서는 split 함수에 전달하는 값의 타입에 따라 정규식이나 일반 텍스트 중 어느 것으로 문자열을 분리하는지 쉽게 알 수 있다.
println("12.345-6.A".split("\\\\.|-".toRegex())) // 정규식을 명시적으로 만든다.
[12, 345, 6, A]
코틀린 정규식 문법은 자바와 똑같다.
이런 간단한 경우에는 꼭 정규식을 쓸 필요가 없다. split 확장 함수를 오버로딩한 버전 중에는 구분 문자열을 하나 이상 인자로 받는 함수가 있다.
println("12.345-6.A".split(".","-")) // 여러 구분 문자열을 지정한다.
[12, 345, 6, A]
3.5.2 정규식과 3중 따옴표로 묶은 문자열
다른 예로 두 가지 다른 구현을 만들어보자.
첫 번째 구현은 String을 확장한 함수를 사용하고 두 번째 구현은 정규식을 사용한다.
fun parsePath(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")
}
fun main(args: Array<String>) {
parsePath("/Users/yole/kotlin-book/chapter.adoc")
}
// result
// Dir: /Users/yole/kotlin-book, name: chapter, ext: adoc
path에서 처음부터 마지막 슬래시 직전까지의 부분 문자열은 파일이 들어있는 디렉터리경로다. path에서 마지막 마침표 다음부터 끝까지의 부분 문자열은 파일 확장자다.
코틀린에서는 정규식을 사용하지 않고도 문자열을 쉽게 파싱할 수 있다.
정규식은 강력하기는 하지만 나중에 알아보기 힘든 경우가 많다. 정규식이 필요할 때는 코틀린 라이브러리를 사용하면 더 편하다.
fun parsePath(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중 따옴표 문자열에서는 역슬래시()를 포함한 어떤 문자도이스케이르할 필요가 없다.
우선 정규식을 인자로 받은 path에 매치시킨다. 매치에 성공하면 그룹별로 분해한 매치 결과를 의미하는 destructured 프로퍼티를 각 변수에 대입한다.
이때 사용한 구조 분해 선언은 Pair로 두 변수를 초기화할 때 썼던 구문과 같다.
3.5.3 여러 줄 3중 따옴표 문자열
3중 따옴표 문자열을 문자열 이스케이프를 피하기 위해서만 사용하지는 않는다.
3중 따옴표 문자열에는 줄 바꿈을 표현하는 아무 문자열이나(이스케이프 없이) 그대로 들어간다.
val kotlinLogo = """| //
.| //
.|/ \"""
println(kotlinLogo.trimMargin("."))
| //
| //
|/ \
여러 줄 문자열을 코드에서 더 보기 좋게 표현하고 싶다면 들여쓰기를 하되 들여쓰기의 끝부분을 특별한 문자열로 표시하고, trimMargin을 사용해 그 문자열과 그 직전의 공백을 제거한다.
3.7 코드 다듬기: 로컬 함수와 확장
많은 개발자들이 좋은 코드의 중요한 특징 중 하나가 중복이 없는 것이라 믿는다.
그래서 그 원칙에는 반복하지 말라(DRY: Don’t Repeat Yourself)라는 이름도 붙어있다.
하지만 자바 코드를 작성할 때는 DRY 원칙을 피하기는 쉽지 않다. 많은 경우 메소드 추출 리팩토링을 적용해서 긴 메소드를 부분부분 나눠서 각 부분을 재활용할 수 있다.
하지만 그렇게 코드를 리팩토링하면 클래스 안에 작은 메소드가 많아지고 각 메소드 사이의 관계를 파악하기 힘들어서 코드를 이해하기 더 어려워질 수도 있다.
리팩토링을 진행해서 추출한 메소드를 별도의 내부 클래스안에 넣으면 코드를 깔끔하게 조직할 수는 있지만, 그에 따른 불필요한 준비 코드가 늘어난다.
코틀린에는 더 깔끔한 해법이 있다. 코틀린에서는 함수에서 추출한 함수를 원 함수 내부에 중첩시킬 수 있다. 그렇게 하면 문법적인 부가 비용을 들이지 않고도 깔끔하게 코드를 조직할 수 있다.
다음 리스트에는 사용자를 데이터베이스에 저장하는 함수가 있다.
이때 데이터베이스에 사용자 객체를 저장하기 전에 각 필드를 검증해야 한다.
fun saveUser(user: User) {
if (user.name.isEmpty()) {
throw IllegalArgumentException(
"Can't save user ${user.id}: empty Name")
}
if (user.address.isEmpty()) {
throw IllegalArgumentException(
"Can't save user ${user.id}: empty Address")
}
// Save user to the database
}
클래스가 사용자의 필드를 검증할 때 필요한 여러 경우를 하나씩 처리하는 메소드가 중복된것을 알 수 있다. 이를 개선해보도록 하자
fun saveUser(user: User) {
fun validate(user: User,
value: String,
fieldName: String) {
if (value.isEmpty()) {
throw IllegalArgumentException(
"Can't save user ${user.id}: empty $fieldName")
}
}
validate(user, user.name, "Name")
validate(user, user.address, "Address")
// Save user to the database
}
검증 로직 중복은 사라졌고, 필요하면 User의 다른 필드에 대한 검증도 쉽게 추가할 수 있다. 하지만 User 객체를 로컬 함수에게 하나하나 전달해야 한다는 점은 아쉽다.
다행이지만 사실은 전혀 그럴 필요가 없다. 로컬 함수는 자신이 속한 바깥 함수의 모든 파라미터와 변수를 사용할 수 있다. 이런 성질을 이용해 불필요한 User 파라미터를 없앨 수 있다.
fun saveUser(user: User) {
fun validate(value: String, fieldName: String) { // user 파라미터를 중복 사용하지 않는다.
if (value.isEmpty()) {
throw IllegalArgumentException(
"Can't save user ${user.id}: " + // 바깥 함수의 파라미터에 직접 접근할 수 있다.
"empty $fieldName")
}
}
validate(user.name, "Name")
validate(user.address, "Address")
// Save user to the database
}
이 예제를 더 개선하고 싶다면 검증 로직을 User 클래스를 확장한 함수로 만들 수도 있다.
fun User.validateBeforeSave() {
fun validate(value: String, fieldName: String) {
if (value.isEmpty()) {
throw IllegalArgumentException(
"Can't save user $id: empty $fieldName")
}
}
validate(name, "Name")
validate(address, "Address")
}
fun saveUser(user: User) {
user.validateBeforeSave()
// Save user to the database
}
3.7 요약
- 코틀린은 자체 컬렉션 클래스를 정의하지 않지만 자바 클래스를 확장해서 더 풍부한 API를 제공한다.
- 함수 파라미터의 디폴트 값을 정의하면 오버로딩한 함수를정의할 필요성이 줄어든다. 이름붙인 인자를 사용하면 함수의 인자가 많을 때 함수 호출의 가독성을 더 향상시킬 수 있다.
- 코틀린 파일에서 클래스 멤버가 아닌 최상위 함수와 프로퍼티를 직접 선언할 수 있다. 이를 활용하면 코드 구조를 더 유연하게 만들 수 있다.
- 확장 함수와 프로퍼티를 사용하면 외부 라이브러리에 정의된 클래스를 포함해 모든 클래스의 API를 그 클래스의 소스코드를 바꿀 필요 없이 확장할 수 있다. 확장 함수를 사용해도 실행 시점에 부가 비용이 들지 않는다.
- 중위 호출을 통해 인자가 하나 밖에 없는 메소드나 확장 함수를 더 깔끔한 구문으로 호출할 수 있다.
- 코틀린은 정규식과 일반 문자열을 처리할 때 유용한 다양한 문자열 처리 함수를 제공한다.
- 자바 문자열로 표현하려면 수많은 이스케이프가 필요한 문자열의 경우 3중 따옴표 문자열을 사용하면 더 깔끔하게 표현할 수 있다.
- 로컬 함수를 써서 코드를 더 깔끔하게 유지하면서 중복을 제거할 수 있다.
참조
드미트리 제메로프 · 스베트라나 이사코바, 『Kotlin IN ACTION』, 오현석 옮김, 에이콘(2017), P129-140.