[TIL] Kotlin map { } 은 왜 괄호가 없을까

곽태민·2026년 10월 6일

TIL

목록 보기
81/81

Kotlin map { } 은 왜 괄호가 없을까?

1. 문제 상황

페이징 처리 관련 응답 구조를 만드는 과정에서 아래와 같은 코드를 마주쳤다.

PageResponse.from(result) { PendingLibrarianResponse.from(it) }

인자는 하나인 것 같은데 뒤에 중괄호가 붙어 있고, 안에서 it이라는 변수를 사용하는데 선언한 적이 없다. 무엇보다 content만 변환했는데 page, size 같은 나머지 필드들은 어떻게 채워지는지 헷갈렸다.

그래서 이와 관련된 내용을 적어보려고 한다.

2. 함수도 타입이다.

출발점은 이거다. Kotlin에서 함수는 값으로 넘길 수 있고, 그래서 타입이 있다.

(UserResult) -> PendingLibrarianResponse

"UserResult를 받아PendingLibrarianResponse를 반환하는 함수"라는 타입이다. String이나 Int처럼 하나의 타입으로 취급한다.

Typescript에도 비슷한 게 있다.

(user: UserResult) => PendingLibrarianResponse

그래서 파라미터로 선언할 수 있다.

fun <E : Any, T> from(page: Page<E>, mapper: (E) -> T): PageResponse<T>

mapper는 값이 아니라 방법을 받는 파라미터다.

3. 왜 방법을 받아야 하나?

PageResponse는 모든 페이징 응답에 공통으로 쓰려고 만든 래퍼다.
공용이기 때문에 UserResult가 뭔지, PendingLibrarianResponse가 뭔지 몰라야 한다. 알게 되면 common에 있는 클래스가 특정 도메인에 묶이기 때문이다.

알게 된다면 common에 존재하는 클래스가 특정 도메인에 묶이게된다. 하지만 변환은 필요하다. 그래서 "어떻게 바꾸는지" 호출하는 쪽에서 받는다.

companion object {
	fun <E : Any, T> from(page: Page<E>, mapper: (E) -> T): PageResponse<T> =
    PageResponse(
    	content = page.content.map(mapper),   // mapper는 여기서만 쓰인다
        page = page.number,
        size = page.size,
        totalElements = page.totalElements,
        totalPages = page.totalPages,
    )
}

여기서 처음 의문이 풀리게된다. mapper는 content에만 쓰인다. 나머지 네 필드는 page 객체가 이미 가지고 있는 값을 그대로 꺼내서 사용하는 것이고, 변환이 필요없다.

호출부에서 page = ... 을 사용하지 않는 이유는 from이 대신해주고 있기 때문이다. 그게 이 함수를 만든 이유다.

4. 중괄호는 두 번째 인자

이제 문법 문제다. 원래 모습은 이렇다.

PageResponse.from(result, { PendingLibrarianResponse.from(it) })

인자가 두 개고, 두 번째가 람다다. Kotlin은 마지막 파라미터가 함수 타입이면 괄호 밖으로 뺄 수 있다.

PageResponse.from(result) { PendingLibrarianResponse.from(it) }

두 줄은 완전히 같은 코드다. 괄호 안에 인자 하나만 남아서 함수 호출처럼 보이지 않을 뿐이다.

5. map { }도 같은 문법이었다.

여기가 아!하게 되는 지점일 거다. 매일 사용하던 이 코드도 사실 같은 코드다.

list.map({ it.name }) // 원래
list.map { it.name } // 관례적으로 생략

map도 함수를 인자로 받는 함수였다. 괄호가 없어서 특별한 문법처럼 보였을 뿐이다.
filter, forEach, apply, let 전부 마찬가지다.

6. it은 어디서 왔나?

람다 파라미터가 하나일 경우 이름은 생략하고it으로 사용할 수 있다. Kotlin이 기본으로 주는 이름이다.

{ result -> PendingLibrarianResponse.from(result) }   // 이름 지정
{ PendingLibrarianResponse.from(it) }                  // 여기

파라미터가 둘 이상이면 it을 사용할 수 없고 반드시 이름을 지정해줘야 한다.

{ a, b -> a + b }

7. 타입은 추론된다

it의 타입을 적지 않아도 IDE가 자동완성 해주는 이유는

fun <E : Any, T> from(page: Page<E>, mapper: (E) -> T)

PageResponse.from(result) { PendingLibrarianResponse.from(it) }
//                  ↑                      ↑
//          Page<UserResult>     UserResult → PendingLibrarianResponse

page가 Page<UserResult>이므로 E = UserResult로 확정되고, it이 UserResult로 확정된다. 람다가 PendingLibrarianResponse를 반환하므로 T도 추론된다.

8. 메서드 참조로 더 줄이기

람다가 함수 호출만 하고 끝나면 ::로 줄일 수 있다.

PageResponse.from(result, PendingLibrarianResponse::from)

::from은 "그 함수 자체"를 가리킨다. 괄호 안에 다 들어가서 중괄호가 사라진다.

9. 정리

TypeScriptKotlin
함수 타입(e: E) => T(E) -> T
람다(e) => expr{ e -> expr }
단일 파라미터이름 필수it으로 생략 가능
마지막 함수 인자괄호 안괄호 밖으로 가능
메서드 참조없음 ((e) => f(e))::f

결국 특별한 문법이 아니라 함수를 값으로 넘기는 것이었고, Kotlin이 괄호를 생략할 수 있어서 블록처럼 보였을 뿐이다.

profile
Node.js 백엔드 개발자입니다!

0개의 댓글