본문 바로가기

Dev Book/Kotlin IN ACTION

CH3(3.4~3.6). 함수 정의와 호출(2)

1. 컬렉션 처리

- 가변 인자 함수 정의하기: 코틀린의 가변 길이 인자는 파라미터 앞에 vararg 변경자를 붙이면 된다.

 

fun listOf<T> (vararg values:T): List<T> {...}

 

또한 코틀린에서는 자바처럼 배열을 그냥 넘기는 것이 아닌, 배열을 명시적으로 풀어서 배열의 각 원소가 인자로 전달되게 해야 한다. 이를 스프레드 연산자가 해주는데, 실제로는 전달하려는 배열 앞에 *를 붙이면 된다.

 

- 값의 쌍 다루기: 중위 호출과 구조 분해 선언

 

val map = mapOf(1 to "one", 2 to "two")

 

- 여기서 to 라는 단어는 코틀린 키워드가 아닌, 중위 호출을 이용해 to 라는 일반 메소드를 호출한 것이다. 

 

1.to("one") // to 메소드를 일반적인 방식으로 호출
1 to "one" // to 메소드를 중위 호출 방식으로 호출

 

(※ to 함수는 확장 함수로서 수신 객체가 제네릭 하다.)

인자가 하나뿐인 일반 메소드나 확장 함수에 중위 호출을 사용할 수 있다. 함수가 중위 호출을 사용할 수 있게 하고 싶으면 infix 변경자를 함수 선언 앞에 추가해야 한다.

 

- 구조 분해 선언: Pair 또는 다른 객체, 루프 등에 구조 분해 선언을 활용하면 두 변수를 다음과 같이 즉시 초기화 할 수 있다.

 

val (num, name) = 1 to "one"

  

2. 문자열과 정규식

- 문자열 나누기: 자바의 split 함수의 경우 구분 문자열이 실제로 정규식이기 때문에 마침표(.)를 사용해 문자열을 분리할 수 없는 문제가 생긴다. 마침표(.)는 모든 문자를 나타내는 정규식으로 해석되기 때문이다. 코틀린에서는 정규식을 파라미터로 받는 함수는 String이 아닌 Regex 타입의 값을 받는다. 따라서 코틀린에서는 split 함수에 전달하는 값의 타입에 따라 정규식이나 일반 텍스트 중 어느 것으로 문자열을 분리하는지 쉽게 알 수 있다. (toRegex 확장함수를 이용해 문자열을 정규식으로 변환하여 이를 명시적으로 만든다.)

 

다음과 같이 정규식을 쓰거나, 여러 구분 문자열을 지정하여 문자열을 나눌 수 있다.

 

>>println("12.345-6.A".split("\\.|-".toRegex()))
[12 ,345, 6, A]

>> println("12.345-6.A".split(".", "-"))
[12 ,345, 6, A]

 

- 3중 따옴표로 묶은 문자열

3중 따옴표 문자열을 사용해 정규식을 쓰면 역슬래시를 포함한 어떤 문자도 이스케이프 할 필요 없다. 

 

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중 따옴표 문자열

3중 따옴표 문자열에는 줄 바꿈을 표현하는 아무 문자열이나 그대로 들어간다. 이는 테스트의 예상 출력을 작성할 때 좋은 방법이 된다. 복잡하게 이스케이프를 쓰거나 외부 파일에서 텍스트를 불러올 필요가 없기 때문이다.

3중 따옴표 문자열 안에 문자열 템플릿도 사용할 수 있는데 이스케이프를 할 수 없기 때문에 어쩔 수 없이 문자열 템플릿 안에 '$' 문자를 넣어야 하는 문제가 있다.

3. 코드 다듬기: 로컬 함수와 확장

- 코틀린에서는 함수에서 추출한 함수를 원 함수 내부에 중첩시킬 수 있다. 흔히 발생하는 코드 중복을 로컬 함수를 통해 제거하는 방법을 알아보자.

 

fun saveUser(user:User) {
	if(user.name.isEmpty()){
    	throw IllegalArgumentException(
        	"Can't save user: Empty name")
   	 }
     
     if(user.address.isEmpty()){
     	\\ 위와 중복
     }
 }

 

다음과 같은 중복을 로컬 함수를 이용해 없앨 수 있다.

 

fun saveUser(user:User){
	fun validate(value:String, fieldName:String) {
    	if(value.isEmpty()){
        	throw IllegalArgumentExeption(
            	"Can't save user: Empty field"
             )
         }
     }
     
    validate(user.name, "Name")
    validate(user.address, "Address")
     
 }

 

이는 또 검증 로직을 User 클래스를 확장한 함수로 만드는 방식으로 리팩토링 가능하다. 하지만 이 경우, 검증 로직은 User를 사용하는 다른 곳에서 쓰이지 않는 기능이기 때문에 User에 포함시키지 않는게 좋을 수 있다. 확장 함수를 로컬 함수로 정의할 수도 있으나 중첩된 함수의 깊이기 깊어지면 코드의 가독성이 떨어지므로 일반적으로는 한 단계만 함수를 중첩시키는 것이 권장된다.