본문 바로가기

코틀린

자바로 배우는 코틀린 개념 - 일급 객체(First Class Object)와 고차함수

코틀린 카테고리에 웬 자바에서 일급 객체(First Class Object)란 일까 했지만

코틀린을 잘 이해하기 위해서 기존에 알고 있던 자바의 개념으로 정리해 본다. 

 

글을 쓰게 된 계기는 코틀린에서 아래 개념 때문이다. 

  코틀린에서는 함수가 1급객체의 성질을 가지는 것이 가능하다.

1급 객체란 무엇일까

나는 1급 객체가 뭔지 개념이 아예 없었고, 

1급 객체를 이해하기 위해서 익명함수의 이해부터 필요하다 

이제 하나하나 차근차근 써보겟다.

 

1급 객체를 달성하기 위한 조건 3가지이다 

  1. 변수나 데이터에 할당할 수 있어야 합니다.
  2. 객체의 인자로 넘길 수 있어야 합니다.
  3. 객체의 리턴값으로 리턴할 수 있어야 합니다.

자바에서는 1급 객체를 만족시키기 위해서 익명함수와 람다로 구현을 했다. 

 

익명함수란

먼저 익명 함수라는 것은 말 그대로 이름이 없는 함수를 말한다.

익명 함수를 사용하는 이유

  • 프로그램 안에서 일시적으로 한 번만 사용되고 버려져도 되는 객체일 때 사용

 

많이 나오는 익명함수의 예시중 객체 비교를 위한 Comparable을 보자

보통 Comparable을 구현을 하면 아래와 같을 것이다. 

Comparable cp = new Comparable() {
            @Override
            public int compareTo(@NotNull Object o) {
                return 0;
            }
        };

우리는 Comparable의 사용법을 알고 싶은 게 아니기 때문에

Comparable은 어떻게 만들어져 있는지 볼 것이다.

Comparable의 구현을 살펴보면 아래처럼 정의되어있다.

public interface Comparable<T> {
	public int compareTo(T o);
}

 

해당 인터페이스를 보면

  1. 인터페이스로 선언되어 있고
  2. 인터페이스의 메소드가 하나만 있다.

당연해 보이지만 이 특성으로 인해서, 개발자는 익명함수 라는것 을 사용할 수 있다.

 

특히 익명함수는 위에 있는 특징으로 여러 가지 할 수 있는 능력을 가진다.

위 특징을 꼭 알아두자. 

 

보통 인터페이스는 클래스의 구현할 때 미리 정의돼있는 정의서처럼 쓰인다.

Comparable은 이해하기 살짝 어려우니깐 이해하기 쉬운 개념인 Potion으로 바꿔보자.


익명함수 예제 

게임에서 포션을 먹으면 HP가 채워진다.

이 인터페이스는 현재 체력수치 health를 받아서 int를 리턴해주는 인터페이스이다.

interface Potion {
    int eat(Integer health);
}

이 포션이라는 인터페이스를 받아서 빨간 표현을 구현해 본다 하자 

이걸 구현한다고 하면 

class RedPotion을 만들고 Potion 인터페이스를 implement 한다. 

RedPotion을 클래스로 선언한 것을 기억하자.

class RedPotion implements Potion{
    @Override
    public int eat(Integer health) {
        return health + 50;
    }
}

 

아마 코드에서 사용한다고 하면 아래와 사용될 것이다.

        RedPotion redPotion = new RedPotion();
        int hp = redPotion.eat(10);

근데 포션인터페이스가 아래와 같은 조건을 만족한다고 생각해 보자

나중에 다시 부를 일이 없고 프로그램 안에서 일시적으로 한 번만 사용되고 버려져도 되는 객체

 

이러면 익명함수를 쓸 수 있는 조건에 만족한다.

 

인터페이스 Potion은

  1. 인터페이스로 선언되어 있고
  2. 인터페이스의 메소드가 하나만 있다.

아까 파란색으로 강조한 익명함수를 쓸 수 있는 조건을 만족한다. 

interface Potion {
    int eat(Integer health);
}

 

그러면 아까 예시와 다르게 아래와 같이 쓸 수 있다.

기존방법 : class RedPotion을 만들고 Potion 인터페이스를 implement 한다. 

수정된 방법 : Potion 인터페이스에서 익명함수로 구현한다. 

        Potion redPotion = new Potion() {
            @Override
            public int eat(Integer health) {
                return health+50;
            }
        };

클래스를 Implement 해서 구현하는 게 아니라, 익명함수로 구현한 것이다. 

 

원래 인터페이스는 new Potion()으로 인스턴스화를 시킬 수 없는데

조건 ‘인터페이스의 메소드가 하나만 있다.’를 만족시키면

익명함수로 구현할 수 있는 조건이 된다.

 

익명함수를 람다로 바꾸는 과정

하지만 eat이라는 익명함수를 구현하는데 불필요한 부분이 너무 많다.

예를 들어 ‘인터페이스의 메소드가 하나만 있다.’에서

하나만 있으니 @Override는 빼도 될 거 같은 느낌이다. 

반복되는 부분을 그냥 내버려두는 것은 프로그래머에게 죄악이다.

 

하나씩 차근차근 생략해 보자.

일단 생략을 하려면 어떤 경우라도 반복적이고 똑같은 부분이 있어야 한다.

어떤 특이사항으로 달라진다면 생략해서는 안된다.

 

1. new Potion() 은 익명함수를 선언하기 위해서 필요한 부분이다.

→ 이 부분은 어떤 인터페이스에서 익명함수를 선언해도 반복되기 때문에 생략 가능하다.

        Potion redPotion = new Potion() {  //1
            @Override
            public int eat(Integer health) {
                return health+50;
            }
        };

2. Potion은 하나의 구현체(eat)만 있다는 것은 알고 있다.

그럼 인터페이스에 의미 정의된 int eat(Integer health)에서 반복되는 부분인

@Override public int eat(Integer 은 생략 가능하다.

        Potion redPotion = new Potion() {    //2
            @Override   		     //2
            public int eat(Integer health) { //2
                return health+50;
            }
        };

3. 생략해야 될 부분을 지우고 남아 있는 부분에서 람다라고 표현하기 위해 ‘→’를 추가(법칙)한다.

        Potion redPotion = (health) -> {  //3
                return health+50;
            }
        };

4. return과 {}가 생략이 가능하다.

        Potion redPotion = (health) -> health+50;

위 결과물이 생략할 수 있는 건 다 하고 축약시킨 '람다' 표현식이다. 

'익명함수로 표현되는 표현식'과 '람다로 표현' 되는 형식만 다르고 동작은 같다.

아래 익명함수 표현식과 람다표현식을 다시 비교해 보자. 

        //익명함수
        Potion redPotion = new Potion() {
            @Override
            public int eat(Integer health) {
                return health+50;
            }
        };

	//람다
        Potion redPotion = (health) -> health+50;

 

만약 여기서 redPotion 이 아니라 whitePotion을 람다로 구현한다고 해보자

        Potion whitePotion = (health) -> health+100;

 

익명함수로 이것저것 써서 쓸 필요 없이,

람다로 간단하게 구현될 수 있을 것이다.

1급 객체를 이야기하다가 갑자기 익명함수부터 람다까지 나왔을까???

-> 자바는 람다와 익명함수로 일급 객체를 구현했다.

아래에서 일급객체의 특징을 알아보고 이 특징을 람다가 

어떻게 구현했는지 알아보자.

 

일급 객체의 특징

'일급'이란 뜻은 우선시 제공되는 게 아니라 

다른 것들도 원래 다 일급이다.

원래 일급이 아니었기 때문에 사용할 때 다른 요소들과 아무런 차별이 없다는 것을 뜻한다.

1. 모든 일급 객체는 변수나 데이터에 담을 수 있어야 한다.

2. 모든 일급 객체는 함수의 파라미터로 전달할 수 있어야 한다.

3. 모든 일급 객체는 함수의 리턴값으로 사용할 수 있어야 한다.

 

1. 모든 일급 객체는 변수나 데이터에 담을 수 있어야 한다

일급 객체는 변수나 데이터에 담을 수 있어야 한다. 

아래 수식을 다시 보면 함수의 동작을 whitePotion에 할당했다.

위 코드는 'health + 100'이라는 동작을 정의해 놓고 whitePotion  할당했다. 

        Potion whitePotion = (health) -> health+100;
        whitePotion.eat(10);

이게 처음에 느낌이 잘 안 왔는데, 아래 코드와 위코드를 비교해 보면 확실히 느낌이 다르다.

아래코드를 보면 'health + 50'이라는 결과값을 Int로 리턴한다.

위는 람다의 동작방식을 저장. 

아래는 더한 값을 저장.

확실하게 다르다. 

    @Test
    void test() {
        int redPotion = eat(10);
    }
    public static int eat(Integer health) {
        return health + 50;
    }

 

 

람다로 한 방법은 일급 객체를 설명하는 것 중 하나인

‘모든 일급 객체는 변수나 데이터에 담을 수 있어야 한다.’의 조건을 충족한다

정리하자면 람다표현식은 변수에 함수를 할당한 기능이다.

이는 메소드에 결과를 할당한 것과 확실하게 다르다.

 

2. 함수의 파라미터로 전달할 수 있어야 한다.

기존에 했던 점을 확실하게 집고 가야

새로운 것에 개념이 왜 나왔는지 알기 때문에 다시 코드를 보자.

아래 개념은 ‘모든 일급 객체는 변수나 데이터에 담을 수 있어야 한다.’이다.

        Potion whitePotion = (health) -> health+100;

위에서 썼던 저 함수만 가져와서

‘함수의 파라미터로 전달할 수 있어야 한다.’를 구현하면 아래와 같다.

    public static Integer heal(Function<Integer, Integer> fun, Integer health) {
        return fun.apply(health);
    }

    int whitePotion = heal((health) -> health + 50, 30);

heal을 보면 파라미터로 함수(Function <Integer, Integer> fun)를 넘기고 있다.

Function에 대한 자세한 설명은 알아서 찾아보기로 하고

Integer health처럼 boxing 형을 넘기는 것이 아니라

@FunctionalInterface 어노테이션이 붙은 함수를 파라미터로 넘기고 있다.

 

3. 모든 함수의 리턴값으로 사용할 수 있어야 한다.

   Function<Integer, Integer> whitePotion = heal();
        
   public static Function<Integer, Integer> heal() {
        return x -> x + 50;
   }

heal() 함수의 리턴값으로 Function을 사용한 부분이다.

이건 쉬운 예제니 설명을 추가로 안 해도 될 거 같다. 

이런 일급 객체의 특성으로 고차 함수를 쓸 수 있게 된다.

여기서 이제 또 나오는 고차함수란 무엇일까..

 

고차함수

고차함수를 정리하자면 다음과 같다.

  1. 함수를 인수로 취하거나
  2. 결과로 반환하는 함수를 고차함수

위 정의로 다음과 같은 특징을 가지고 있다. 

  • 코드의 재사용성과 모듈성을 높여주는 강력한 도구
  • 조건이 성립하려면 기본적으로 함수가 First Class Citizen (일급객체)

 

결론

결국 기본적으로 함수가 일급객체여야 고차함수의 조건을 성립하고 

1. 일급 객체는 고차 함수를 구현하는 데 필수적인 요소이다. 

2. 고차 함수는 일급 객체를 활용하여 다양한 기능을 제공한다

3. 코틀린에서는 함수가 1급 객체의 성질을 가지는 것이 가능하다.

4. 1급객체의 성질을 가지면 코드의 재사용성, 유연성, 표현력을 높이는 데 중요한 역할을 한다.