Kotlin :: Scope 함수, 가독성 있게 쓰는 법

숑숑·2024년 3월 3일
2

kotlin

목록 보기
1/1

Kotlin의 스코프 함수(scope function)란:
특정 객체 컨텍스트 내에서 코드 블록을 실행합니다.
코드의 명확성, 가독성 개선에 도움을 줍니다.

스코프 함수는 코드 가독성과, 라인 수 단축에 큰 도움을 줍니다.
코드 블록 범위를 지정해줘서, 메소드 분리 없이도 변수의 종료시점을 정할 수 있다는게 큰 장점입니다.

그러나 이렇게 좋은만큼, 어뷰징하기도 쉬운 기능입니다.

아래는 정확히 같은 기능을 수행하는 코드입니다.
어떤 코드가 더 읽기 쉬운가요?

대부분은 왼쪽 형태가 더 익숙하실거라 생각합니다.

차이를 더 잘 보여드리기 위해 제가 과장해서 코드를 꼬아두긴 했지만, 이렇게 안 쓰느니만 못한 상황이 있습니다.

즉 적재적소에 써야하지만, 문제는 어떤 함수를 써야하는지 헷갈리는 경우가 많습니다.
이 함수 써도 그만, 저 함수 써도 그만인 상황이 많아서인데요,
Kotlin에서도 공식 가이드라인을 주지는 않고, 기능적 차이에 대해서만 안내하고 있습니다.
팀 컨벤션이 있다면 그에 따르면 되지만, 없는 경우 어떻게 정해야 하는지 저 또한 곤란했습니다.

그래서 저는 나름의 기준을 세워서 개인 프로젝트에 활용해보고 있습니다.
스코프 함수에 대해 간단히 정리하고, 제가 어떤 컨벤션으로 사용하고 있는지 적어보려 합니다.


Scope 함수 정리

기본적인 스코프 함수 종류입니다.

함수참조반환값확장함수 여부
letitLambda resultO
runthisLambda resultO
run-Lambda resultX (별도 객체 컨텍스트 없음)
withthisLambda resultX (객체 컨텍스트를 매개변수로 받음)
applythisContext objectO
alsoitContext objectO

thisit의 차이점?

  • this
    • 람다 함수의 Receiver 객체를 참조 ( T.() -> R )
    • 프로퍼티 접근 시 this 키워드 생략 가능
    • Receiver 객체 내부에서 코드를 실행하는 것처럼 작성 가능
    • 코드 블록 내에서 외부 변수 혼용 시, 키워드 생략으로 인해 무엇이 멤버고 외부 변수인지 혼란스러울 수 있음
  • it
    • 람다 함수의 Argument로 들어온 객체를 참조 ( (T) -> R )
    • this와 달리, 외부 변수와 명확하게 구분됨

Convention

💖 표식을 붙여둔 문항을 가장 중요하게 생각합니다.

대원칙

  • 중첩 스코프 함수는 사용하지 않습니다. (코틀린에서도 공식적으로 권장하지 않습니다.)
  • 아래 중 어떤 use case에도 해당하지 경우, 아예 스코프 함수를 사용하지 않습니다.

1. let

💖 1-1. null 체크하기

사실 상 let의 주 용도라고 볼 수 있는 케이스입니다.
if문 없이 간결하게 null 체크가 가능합니다.

before

if (nums == null) {
  return null
} else {
  return nums.length == 0
}

after

nums?.let {
  nums.length == 0
}
  • let을 사용해 null 체크하는 경우, IDE에서 non-null 타입으로 인식합니다. (IntelliJ-based IDE 기준)
  • 반면 before 버전처럼 작성하는 경우 nullable로 인식합니다.

1-2. 변수 체이닝 단축하기

접근해야 하는 프로퍼티의 depth가 너무 깊은 경우, 별도 변수 선언 없이 let으로 단축할 수 있습니다.

before

val students = school.classroom.students

students.get(0).friend.age += 1
students.length == 1
students.get(0).age += 1

after

school.classroom.students.let {
	it.get(0).friend.age += 1
   	it.length == 1
    it.get(0).age += 1
}

1-3. 임시적으로 필요한 변수 생성

메소드 분리 없이도, 임시적으로 필요한 변수의 생명주기를 정할 수 있습니다.

before

val tmp = something()
...
// 같은 범위 안에 있는 변수에서 계속 tmp에 접근 가능

after

something().let { tmp ->
	...
}

// tmp 접근 불가능

2. run

2-1. 로직을 그룹핑할 때

로직을 그룹핑할 때 일반적으로 메소드를 분리하는데요,
run을 아래처럼 활용하면 별도로 메소드를 분리하지 않아도 됩니다.
(재활용 할 가능성이 없는 로직인 경우에만 권장합니다.)

before

fun test() {
	testPositiveCase()
    testNegativeCase()
}

private fun testPosiiveCase() {
	...
}

private fun testNegativeCase() {
	...
}

after

fun test() {
	// test positive case
    run {
    	
    }
    
    // test negative case
    run {
    }
}

3. with

3-1. 객체의 멤버에만 접근할 때

this로 참조하기에, 멤버 접근 시 키워드를 생략할 수 있어 코드가 간결해보입니다.
오로지 멤버 프로퍼티에만 접근할 경우에 추천합니다.

before

val dto = Dto(member1 = foo, member2 = bar)
assertEqual(dto.member1, true)
assertEqual(dto.member2, true)

after

val dto = Dto(member1 = foo, member2 = bar)
with (dto) {
  assertEqual(member1, true)
  assertEqual(member2, true)
  ...
}

4. apply

💖 4-1. 빌더 패턴으로 객체 초기화

따로 생성자를 만들 필요 없이 객체를 생성할 수 있다는게 큰 장점입니다.

before

class Instance(
	val x: Int,
    val y: Int,
    val w: Int,
    val h: Int
)

val instance1 = Instance(
  x = 10
  y = 20
  w = 5
  h = 15
)

after

class Instance()

val instance1 = Instance().apply {
  x = 10
  y = 20
  w = 5
  h = 15
}

4-2. 멤버 프로퍼티 값 수정하기

전 기본적으로는 setter를 이용하지 않고,
객체 내부 메소드를 선언해서 값을 수정하도록 작성하고 있습니다.

before

class Student(
  var age: Int
)

...

val student = Student(age = 1)
student.age += 1

after

class Student(
  private var age: Int
) {

  fun increaseAge() = apply {
	  age += 1
  }
}

5. also

5-1. 후작업 실행 (로깅 등)

let과 비슷하게 후작업에 사용되지만 반환형이 필요없을 때 사용합니다.

before

fun createEntity() {
	val entity = Entity(...)
    logger.debug('$entity created')
}

after

fun createEntity() = Entity(...)
  .also { logger.debug('$it created') }

Reference

profile
툴 만들기 좋아하는 삽질 전문(...) 주니어 백엔드 개발자입니다.

0개의 댓글