Lamda Expression

박봉팔·2024년 7월 2일

람다 표현식 알아보기

우리는 개발을 하며 종종 람다식 혹은 람다 함수라는 말을 듣게 된다. 정확히는 람다 표현식 이라고 부르는데 람다 표현식이란 대체 뭘 말하는 걸까? 차근차근 알아보자.

익명함수

일반적으로 함수는 선언을 통해 정의하고, 해당 함수의 함수명을 통해 호출해 사용한다. 하지만 딱 한 번만 쓰는 함수를 매번 이런 식으로 정의해서 사용하게 된다면 어떻게 될까? 아마 코드에는 똑같은 이름의 함수가 수십 개가 생겨나게 될 것이다. 다행히 Kotlin을 포함한 대부분의 현대적인 고차언어 들에서는 함수를 정의하지 않고 사용하는 방법들이 존재한다. 익명 함수는 이처럼 함수를 정의 없이 사용하는 방법 중 하나다.

익명 함수는 말 그대로 이름이 없는 함수를 말한다. 일반적으로 계속해서 사용하는 함수의 경우 해당 함수의 함수명을 선언부에 입력해, 해당 함수의 함수명을 통해 호출해 사용이 가능하다. 하지만 익명 함수의 경우 함수명을 정의하지 않고 바로 실행하거나 변수, 혹은 다른 함수에 전달되는 매개변수로 사용된다.


fun sum(a: Int, b: Int): Int { return a + b }

기존의 함수가 위와 같이 이름과 매개변수, 반환값, 본문으로 구성되었다면, 익명 함수는 아래와 같이 함수명 없이 바로 선언된다.


fun(a: Int, b: Int): Int { return a + b } // 기본적인 익명 함수의 형태

// 익명 함수 뒤에 ()를 통해 매개변수를 전달하면 바로 실행된다.
println(fun(a: Int, b: Int): Int { return a + b }(3, 4)) // 출력 : 7

// 변수에 할당해 함수를 호출하듯 사용하는 것도 가능하다.
val sum = fun(a: Int, b: Int): Int { return a + b }

println(sum(1, 2)) // 출력: 3

// 변수에 즉시 실행한 결과를 할당하는 것도 가능하다.
val sumResult = fun(a: Int, b: Int): Int { return a + b }(10, 20)

println(sumResult) // 출력: 30

익명 함수는 기존의 함수가 *함수명(x, y)* 형식으로 호출해 사용하듯 익명 함수의 뒤에 *()*를 사용해 바로 사용이 가능하며, 변수에 할당해서 *변수명(x, y)*와 같은 방식으로도 사용할 수 있다.


함수값

이처럼 현대의 언어들은 익명 함수를 변수에 할당하거나, 다른 함수에 매개변수로 전달할 수 있도록 설계되어 있는데, 사실 익명 함수뿐만 아니라 모든 함수는 이렇게 다른 함수에 인자로 전달되는 게 가능하다. 이를 함수 값이라고 한다.
(변수의 경우에는 이름이 중복되니 익명 함수 할당 가능)

쉽게 말해 1, “Hello”, 3.4f와 같은 고정된 타입의 값뿐만 아니라 전달 받은 두 값을 하나로 더해서 합을 반환과 같이 특정 동작(함수)을 값처럼 취급하는게 가능하다는 말이다. 보통 람다 표현식이나 익명 함수에 대한 정보를 찾다 보면 일급 객체라는 말을 많이 보게 되는데 간단하게 일급 객체의 특성을 살펴보면 다음과 같다.

  • 변수에 할당 될 수 있다.
  • 함수의 매개변수로 전달이 가능하다.
  • 함수의 반환값으로도 사용할 수 있다.
  • 프로그램 실행중에 동적으로 생성이 가능해야한다.

Int, Char, Float, Double, String과 같은 기본적인 데이터 타입이 일급 객체인데, 많은 언어들이 기본 데이터 타입 말고도 함수를 일급 객체로 취급하기 때문에 함수를 값처럼 취급하는 게 가능해진다.


람다 표현식

그래서 결국 람다 표현식은 뭘 말하는 걸까?

람다 표현식은 앞서 알아본 익명 함수와 같이 정의와 함께 바로 함수를 실행시키는 함수 정의 방식 하나로, 익명 함수보다 훨씬 간단하게 사용할 수 있어 개발을 하며 수도 없이 사용하게 되는 함수 정의 방식이다. 람다 표현식은 간결함 때문에 보통 함수에 전달되는 매개변수나, 함수의 반환값으로 많이 사용된다.

익명 함수의 경우 함수 예약어(fun)매개변수를 명시하는 ()부분을 작성해야 했다면, 람다 함수는 코드 블록 안에 직접 매개변수를 명시하고 본문을 작성한다. 위에서 정의했던 익명 함수를 람다 표현식으로 작성하면 다음처럼 변형된다.

    // 기존의 익명 함수
    fun(a: Int, b: Int): Int { return a + b }
    
    //람다 표현식
    {a: Int, b: Int -> a + b } 

->기호로 매개변수와 함수 본문이 분리된다. 람다 표현식에서는 return문을 사용하지 않으며, 마지막 라인의 결과값이 반환값으로 반환된다.

또한 매개변수가 하나일 경우 매개변수를 명시하지 않고 it으로 사용하는 게 가능하다.


    { a: Int -> a * 2 }
    
    { it * 2 }

람다 표현식을 사용하면 함수에 함수값을 전달해야 할 경우 보다 간결하게 전달이 가능해진다.


    fun sumSum(numA: Int, NumB: Int, sum: (Int, Int) -> Int): Int {
    	val result = sum(numA, numB) + sum(numA, numB)
    	
    	return result
    }
    
    println(sumSum(2, 4, { numA, numB -> numA + numB })) // 결과 : 12
    // 람다표현식 안의 a, b는 매개변수의 타입이 (Int, Int) -> Int 이므로 자동으로 타입 추론

또한 Kotlin에서는 함수의 마지막 매개변수가 함수 타입이라면(함수값을 전달받아야 한다면) 람다 표현식의 블록{}괄호 () 밖으로 빼서 쓰는 게 가능한데, 이렇게 블록을 밖으로 빼고 보면 어딘가 익숙한 형태로 변하게 된다. 특히 매개변수가 1개뿐이라면 우리가 항상 사용하고 있는 함수와 비슷한 형태가 된다.

    sunSum(2, 4, { numA, numB -> numA + numB })
    sumSum(2, 4) { numA, numB -> numA + numB} // 두가지 모두 올바른 방법이다.
    
    list.forEach({ println(it) }) // 매개변수가 하나이기 때문에 생략 후 it으로 사용가능
    list.forEach{ println(it) } // ()안에 들어갈 값이 없어서 생략

우리가 자주 사용하는 forEach라든지 filter같은 함수들도 내부적으로 함수값을 전달받아 동작하기 때문에 람다 표현식을 사용하는 위와 같은 형태가 되는 것이다.
(우리는 알게 모르게 항상 람다 표현식을 사용하고 있었던 것이다! 람다? 이제 겁먹지 말라!)

메커니즘 이해

함수값익명 함수, 람다 표현식에 대해 이해하더라도 처음에는 함수값이란 게 어떤 식으로 동작되는지 와닿지 않아 헷갈리는 경우가 많다.

그래서 좀 더 함수값을 이해하기 편하도록 정리하며 마무리해 보자.

다음과 같은 코드에서 함수값을 전달받아 사용한다고 했을 때 흐름을 살펴보자.

    val twoFour = sumSum(2, 4) { a, b -> a + b }
    
    fun sumSum(numA: Int, NumB: Int, sum: (Int, Int) -> Int): Int {
    	val result = sum(numA, numB) + sum(numA, numB)
    	
    	return result
    }

*sumSum* 함수는 *Int* 타입의 값 두 개를 전달받아 *sum*으로 전달받은 람다를 실행한 후 반환된 값으로 계산한 결과를 *return* 한다. 복잡해 보이지만 그림으로 순서를 정리해 보면 좀 더 이해가 쉽다.

함수 내부의 값을 전달받은 함수값에 전달해 밖에서 함수값의 내용을 실행, 결과를 가지고 다시 함수 내부 돌아와 나머지 동작을 수행하고 결과를 반환하는 거라고 생각하면 된다.

profile
개발 첫걸음! 가보자구!

0개의 댓글