Kotlin

[Kotlin] 로컬 함수 & 확장

sh1mj1 2024. 1. 4. 14:49

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

 

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

 

좋은 코드의 특징 중 하나는 중복이 없는 것이다.

이에 관해서 DRY(Don't Repeat Yourself) 원칙이라는 이름도 있다.

만약 어떤 함수 안에 같은 구조의 코드가 반복된다면 어떻게 이 반복을 줄일 수 있을까?

메서드를 더 작은 단위의 메서드로 나눌 수 있을 것이다.

물론 이것도 좋은 방법이다. 코틀린에서는 다른 방법도 존재한다.

로컬(local) 함수

코틀린에서 로컬 함수함수에서 추출한 함수를 원 함수 내부에 중첩시키는 기능이다.

이렇게 하면 문법적인 부가 비용 없이도 깔끔하게 코드를 만들 수 있다.

 

아래 `User`를 DB 에 저장하는 함수 예제가 있다.

class User(val id: Int, val name: String, val address: String)

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")")
    }

    // user 를 DB 에 저장하는 부분

}

이 `saveUser` 메서드에서는 코드 중복이 그리 많지 않다.

하지만 훨씬 더 중복이 많은 메서드라고 상상하고 보자.

그리고 코틀린이 제공하는 `require` 같은 메서드를 사용할 수도 있지만 이를 사용할 수 없는 환경이라고 생각하자.

 

이런 경우 검증 코드를 로컬 함수로 분리하면 중복을 없애면서 코드 구조를 깔끔히 유지할 수 있다. 

fun saveUser1(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")")

    // user 를 DB 에 저장하는 부분
    
}

이렇게 수정하면 검증 로직 중복이 사라져 훨씬 나아보인다.

로컬 함수의 개선 ver 2

하지만 위 코드로는 `User` 객체를 로컬 함수에게 하나하나 전달해야 한다.

사실 그럴 필요가 없다.

로컬 함수는 자신이 속한 바깥 함수의 모든 파라미터와 변수를 사용할 수 있다.

그렇다면 아래처럼 개선할 수 있다.

fun saveUser2(user: User) {
    fun validate(value: String, fieldName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException("Can't save user ${user.id}: empty $fieldName")
        }
    }
    validate(user.name, "Name")
    validate(user.address, "Address")

    // user 를 DB 에 저장하는 부분

}

로컬 함수의 개선 ver 3

이 예제를 더 개선하고 싶다면 검증 로직을 `User` 클래스를 확장한 함수로 만들 수도 있다.

fun saveUser3(user: User) {
    user.validateBeforeSave()

    // user 를 DB 에 저장하는 부분
}

fun User.validateBeforeSave() {
    fun validate(value: String, fieldName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException("Can't save user $id: empty $fieldName") // $id 는 ${this.id}
        }
    }
    validate(name, "Name")
    validate(address, "Address")
}

`User` 는 라이브러리에 있는 클래스가 아니라 우리 코드 기반에 있는 클래스이지만,

이 경우 검증 로직은 `User` 를 사용하는 다른 곳에서는 쓰이지 않는 기능이다.

 

즉, `user` 를 사용하는 다른 곳에서는 쓰이지 않는 기능이기 때문에, Effective Koltin 의 Item 43 에서 말하는 'API 의 필수적이지 않은 부분을 확장 함수로 추출하자' 에 맞게  `User` 의 멤버로 포함시키지 않았다.

 

이렇게 `User` 를 간결하게 유지하면 생각해야 할 내용이 줄어들어서 더 쉽게 코드를 파악할 수 있다.

한 객체만을 다루면서 객체의 private 데이터를 다루지 않는 함수는 위처럼 확장 함수로 만들어서 수신 객체를 지정(`객체.멤버`)하지 않고도 public 멤버 프로퍼티나 메서드에 접근할 수 있다. 

(기존에 `${user.id}` 로 쓰던 것을 `id` 로만 사용)  

 

마지막 코드에서 확장 함수를 로컬 함수로 만들 수도 있지만,

중첩된 함수의 깊이가 너무 깊어지면 가독성이 어려워지므로 보통 한 단계만 함수를 중첩시키는 것이 권고된다.

로컬 함수 사용의 장점 & 주의점 정리

  1. 캡슐화 및 컨텍스트 접근: 로컬 함수는 자신이 정의된 상위 함수의 변수와 파라미터에 직접 접근할 수 있다.
    • 로컬 함수는 상위 함수의 컨텍스트를 활용하여 더 간결하고 직관적인 코드를 작성할 수 있다.
    • 상위 함수의 로직과 밀접한 관련이 있는 작업을 수행할 때 유용하다.
  2. 네임스페이스 오염 방지: 일반 함수로 정의하면 전역 네임스페이스나 클래스의 네임스페이스를 오염시킬 수 있다.
    • 특히, 한 함수만을 위해 사용되는 유틸리티 함수일 경우, 그 함수가 공용 네임스페이스에 존재하면 다른 부분의 코드와 불필요한 연결을 만들거나 혼란을 야기할 수 있다.
    • 반면, 로컬 함수는 오직 그 함수가 정의된 범위 내에서만 존재하므로, 네임스페이스를 깔끔하게 유지할 수 있다.
  3. 재사용성과 가독성 향상: 로컬 함수를 사용하면 코드의 특정 블록을 함수로 추출함으로써 재사용성을 높일 수 있다.
    • 각 블록의 목적이 명확해지므로 전체적인 코드의 가독성도 향상된다.
    • 함수의 이름을 통해 해당 블록의 역할을 설명함으로써, 코드를 읽고 이해하는 것이 더 쉬워진다.
  4. 오버엔지니어링 방지: 프로젝트 내에서 한 번만 사용되는 특정 로직을 위해 별도의 일반 함수를 만드는 것은 오버엔지니어링일 수 있다.
    • 로컬 함수를 사용하면, 그 로직을 필요한 곳 가까이에 위치시켜 관리하기 쉽고, 코드베이스를 더 간결하게 유지할 수 있다.
  5. 유지보수 용이: 로컬 함수는 그 사용 범위가 명확하기 때문에, 코드 변경이나 리팩토링 시 실수를 줄일 수 있다.
    • 일반 함수로 분리할 경우, 다른 곳에서 의도치 않게 사용될 수 있으나, 로컬 함수는 그러한 우려가 없다.