Skip to content

아이템 42. 익명 클래스보다는 람다를 사용하라 #107

@JoisFe

Description

@JoisFe

Discussed in https://github.com/orgs/Study-2-Effective-Java/discussions/106

Originally posted by JoisFe February 6, 2023

아이템 42. 익명 클래스보다는 람다를 사용하라

자바의 함수 타입 표현의 문제점?

  • 자바 관련 스터디에서 자바스크립트에 대해 좀 알아보려 한다 (해당 주제의 이해를 위해)

자바스크립트의 일급 함수?

일급 시민 (first class citizen)

  • 변수에 담을 수 있다.
  • 인자로 전달할 수 있다.
  • 반환값으로 전달할 수 있다.

일급 객체

: 객체를 일급 시민으로 취급한다는 것

일급 함수 (first class function)

: 함수를 일급 시민으로 취급한다는 것

자바스크립트의 함수를 인수로 전달하는 예시 (일급 함수)

var numbers = [3, 1, 5, 7];

numbers.sort(function(firstNum, secondNum) {
  return firstNum - secondNum;
});

console.log(numbers);
image
  • 결과를 보면 정렬이 잘 된 것을 알 수 있음
  • 정렬을 하는 함수를 인수로 전달하는 것을 확인할 수 있음 -> 일급 함수

자바에서의 일급 함수?

  • 자바에서는 일급 함수는 존재하지 않음 즉 함수는 일급 취급을 받지 못함
  • 파라미터로 넘기거나 변수에 담을 수도 없고 반환 타입으로도 사용할 수 없다는 것

자바에서 함수를 일급 함수처럼 사용하기 위한 방법

  • 자바는 함수를 인수로 전달할 수는 없지만 객체는 전달할 수 있다.
  • 즉 자바의 객체는 일급 객체의 특징을 가지는데 객체를 이용하여 위의 문제점을 해결한다.

1. 메서드를 하나 가진 객체를 이용한다.

  • 예시를 들어보면 멜론을 타입에 따라 필터, 무게에 따라 필터하는 기능을 구현하려 한다
image
public interface MelonPredicate {
    boolean test(Melon melon);
}
public class HugeMelonPredicate implements MelonPredicate {

    @Override
    public boolean test(Melon melon) {
        return melon.getWeight() > 5000;
    }
}
public class Melon {

    private final String type;
    private final int weight;
    private final String origin;

    public Melon(String type, int weight, String origin) {
        this.type = type;
        this.weight = weight;
        this.origin = origin;
    }

    public String getType() {
        return type;
    }

    public int getWeight() {
        return weight;
    }

    public String getOrigin() {
        return origin;
    }

    @Override
    public String toString() {
        return "java.Melon{" +
            "type='" + type + '\'' +
            ", weight=" + weight +
            ", origin='" + origin + '\'' +
            '}';
    }
}
public class TastelessMelonPredicate implements MelonPredicate {

    @Override
    public boolean test(Melon melon) {
        return "맛없는 멜론".equalsIgnoreCase(melon.getType());
    }
}```

``` java
public class Filters {

    public static List<Melon> filterMelons(
        List<Melon> melons, MelonPredicate predicate) {

        List<Melon> result = new ArrayList<>();
        for (Melon melon : melons) {
            if (melon != null && predicate.test(melon)) {
                result.add(melon);
            }
        }
        return result;
    }
}
public class Main {

    public static void main(String[] args) {
        List<Melon> melons = List.of(new Melon("맛좋은 멜론", 5001, "한국"), new Melon("맛없는 멜론", 50, "미국"));

        List<Melon> tastelessMelons = Filters.filterMelons(melons, new TastelessMelonPredicate());

        List<Melon> hugeMelons = Filters.filterMelons(melons, new HugeMelonPredicate());

        System.out.println("맛없는 Melon들은 : ");
        for (Melon melon : tastelessMelons) {
            System.out.println(melon.toString());
        }

        System.out.println();

        System.out.println("huge Melon들은 : ");
        for (Melon melon : hugeMelons) {
            System.out.println(melon.toString());
        }
    }
}
image
  • 위 코드는 Filters 클래스의 filterMelons 메서드가 조건에 따라 필터링을 해주는 기능을 한다
  • 필터링을 해주는 조건 메서드를 flterMelons 메서드 인수로 넘기고 싶지만 자바는 불가능 함 (일급 함수가 아니야...)
  • 따라서 필터링을 해주는 조건 메서드를 가진 객체를 넘기고자 한다
  • 따라서 MelonPredicate 라는 인터페이스를 만들고 test라는 추상 메서드를 둔다
  • 해당 필터링 조건을 구현할 구현체 클래스를 만든다 (HugeMelonPredicate, TastelessMelonPredicate)
  • 해당 구현체 객체를 인수로 넘긴다면 코드가 깔끔해진다!!

위 방식의 문제점

  • 필터링 조건마다 구헌체 클래스를 만들어야 함
  • 클래스가 너무 많아짐!! --> 코드와 시간이 많이 듬

해결책

  • 자바에서 제공하는 추상 클래스를 이용

2. 이전에 자바에서 함수 타입을 표현할 때 추상 메서드를 하나만 담은 인터페이스(드물게는 추상 클래스)를 사용했음

  • 이런 인터페이스의 인스턴스를 함수 객체 (function object) 라고 하여 특정 함수나 동작을 나타내는 데 사용했음
  • 1997년 JDK 1.1이 등장하면서 함수 객체를 만드는 주요 수단은 익명 클래스가 되었음
 Collections.sort(words, new Comparator<String>() {
            public int compare(String s1, String s2) {
                return Integer.compare(s1.length(), s2.length());
            }
        }); 
  • 위 코드는 문자열을 길이순으로 정렬하기위한 비교 함수로 익명 클래스를 사용한 예이다

이전 멜론 예시를 추상클래스로 리팩토링 해보자

image
  • 먼저 MelonPredicate의 구현체 클래스를 모두 삭제한다
public class Main {

    public static void main(String[] args) {
        List<Melon> melons = List.of(new Melon("맛좋은 멜론", 5001, "한국"), new Melon("맛없는 멜론", 50, "미국"));

        List<Melon> tastelessMelons = Filters.filterMelons(melons, new MelonPredicate() {
            @Override
            public boolean test(Melon melon) {
                return "맛없는 멜론".equalsIgnoreCase(melon.getType());
            }
        });

        List<Melon> hugeMelons = Filters.filterMelons(melons, new MelonPredicate() {
            @Override
            public boolean test(Melon melon) {
                return melon.getWeight() > 5000;
            }
        });

        System.out.println("맛없는 Melon들은 : ");
        for (Melon melon : tastelessMelons) {
            System.out.println(melon.toString());
        }

        System.out.println();

        System.out.println("huge Melon들은 : ");
        for (Melon melon : hugeMelons) {
            System.out.println(melon.toString());
        }
    }
}
image
  • 각각의 필터 조건에 따라 익명클래스를 활용하여 MelonPredicate를 구현한다
  • 같은 결과를 확인할 수 있다.
  • 여러 클래스를 작성하지 않아도 됨!!

익명 클래스의 문제점

  • 여전히 대량의 코드를 작성해야 함..
  • 익명 클래스는 상당히 복잡, 초보자에게는 불완전하고 이상해보임
  • 위와 같은 문제점으로 인해 자바가 함수형 프로그래밍에 적합하지 않게됨

해결책

  • 람다를 사용

3. 익명 클래스보다는 람다를 사용

  • 먼저 위의 코드들을 보면 익명 클래스로 변환을 하면서도 MelonPredicate 인터페이스와 같이 추상 메서드 하나짜리 인터페이스는 유지됨
  • 이러한 추상 메서드 하나를 가진 인터페이스는 특별한 의미를 인정 받아 특별한 대우를 받음

함수형 인터페이스 (Functional Interface)

  • 추상 메서드 하나를 가진 인터페이스
  • 이 인터페이스들의 인스턴스를 람다식 (lambda expression) 혹은 람다라고 부르는 것을 사용해 만들 수 있게 됨
  • 람다는 함수나 익명 클래스와 개념은 비슷하지만 코드가 훨씬 간결해짐
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
  • 위 코드는 이전에 익명 클래스를 이용하여 문자열 길이로 정렬한 예시를 람다를 이용한 것이다.
  • 여기서 람다, 매개변수(s1, s2), 반환값의 타입은 각각 (Comparator), String, int 지만 코드에 언급이 없음
  • 컴파일러가 문맥을 살펴서 타입을 추론하기 때문에 가능한 일
  • 상황에 따라 컴파일러가 타입을 결정하지 못할 수도 있으므로 그러한 경우 직접 명시해줘야 함
  • 컴파일러의 타입 추론 규칙은 따로 공부하길 바란다!!!

타입을 명시해야 코드가 더 명확한 경우 혹은 직접 명시해야하는 경우를 제외하고는 람다의 모든 매개변수 타입은 생략하자

  • 그래야만 코드를 간단하게 하는 목적을 더욱 달성할 수 있기 때문!!

익명 클래스를 활용하여 리팩토링한 Melon 필터 코드를 람다를 이용하여 리팩토링 해보자

public class Main {

    public static void main(String[] args) {
        List<Melon> melons = List.of(new Melon("맛좋은 멜론", 5001, "한국"), new Melon("맛없는 멜론", 50, "미국"));

        List<Melon> tastelessMelons = Filters.filterMelons(melons, melon -> "맛없는 멜론".equalsIgnoreCase(melon.getType()));

        List<Melon> hugeMelons = Filters.filterMelons(melons, melon -> melon.getWeight() > 5000);

        System.out.println("맛없는 Melon들은 : ");
        for (Melon melon : tastelessMelons) {
            System.out.println(melon.toString());
        }

        System.out.println();

        System.out.println("huge Melon들은 : ");
        for (Melon melon : hugeMelons) {
            System.out.println(melon.toString());
        }
    }
}
image
  • 결과는 동일하면서 훨씬 코드가 간단해 졌음을 확인할 수 있다.
image - intellJ IDE 에서도 익명 클래스를 람다로 변환하기를 추천하고 있다.

더 나아가서

  • 람다를 언어 차원에서 지원하면서 기존에는 적합하지 않았던 곳에서도 함수 객체를 실용적으로 사용할 수 있게됨
public enum Operation {

    PLUS("+") { @Override public double apply(double x, double y) { return x + y; } },
    MINUS("-") { @Override public double apply(double x, double y) { return x - y; } },
    TIMES("*") { @Override public double apply(double x, double y) { return x * y; } },
    DIVIDE("/") { @Override public double apply(double x, double y) { return x / y; } };

    private final String symbol;

    Operation(String symbol) {
        this.symbol = symbol;
    }

    @Override
    public String toString() {
        return symbol;
    }

    public abstract double apply(double x, double y);

}
public enum Operation {

    PLUS("+", (x, y) -> x + y), 
    MINUS("-", (x, y) -> x - y),
    TIMES("*", (x, y) -> x * y),
    DIVIDE("/", (x, y) -> x / y);

    private final String symbol;
    private final DoubleBinaryOperator op;

    Operation(String symbol, DoubleBinaryOperator op) {
        this.symbol = symbol;
        this.op = op;
    }

    @Override
    public String toString() {
        return symbol;
    }

    public double apply(double x, double y) {
        return op.applyAsDouble(x, y);
    }
}
  • 상수별 클래스 몸체를 구현하는 방식보다 열거 타입에 인스턴스 필드를 두는 편이 낫다고 하였음
  • 람다를 이용하면 후자의 방식 즉 열거타입의 인스턴스 필드를 이용하는 방식으로 상수별로 다르게 동작하는 코드를 쉽게 구현 가능
  • 단순히 각 열거 타입 상수의 동작을 람다로 구현해 생성자에 넘기고 생성자는 이 람다를 인스턴스 필드로 저장
  • 이후 apply 메서드에서 필드에 저장된 람다를 호출하기만 하면 됨
  • 결론적으로 훨씬 깔끔한 코드가 되었음

주의 사항

  • 람다 기반 Operation 코드를 보면 상수별 클래스 몸체는 더 이상 사용할 이유가 없다고 오해할 수 있음
  • 하지만 메서드나 클래스와 달리 람다는 이름이 없고 문서화를 하지 못함

따라서 코드 자체로 동작이 명확히 설명되지 않거나 코드 줄 수가 많아진다면 람다를 사용하지 말아야 함!!

  • 추상 클래스의 인스턴스를 만들 때 람다를 쓸수 없으니 익명클래스를 사용해야함
  • 람다는 자신을 참조할 수 없음
    • 람다에서의 this 키워드는 바깥 인스턴스를 가르킴
    • 익명 클래스에서는 this는 익명 클래스의 인스턴스를 가리킴
    • 따라서 함수 객체가 자신을 참조해야한다면 익명 클래스를 사용해야 함
  • 람다도 익명 클래스와 마찬가지로 직렬화 형태가 구현별로 (가령 가상머신별로) 다를 수 있음 따라서 람다를 직렬화 하는 일은 극히 삼가해야함

람다 사용의 기준

  • 람다는 한 줄일때 가장 좋고 길어야 세 줄안에 끝나는 것이 좋음
  • 그 이상은 가독성이 매우 떨어짐

람다 그 이후

image
  • intelliJ IDE를 보면 람다 코드를 메서드 참조로 수정하는 것을 추천한다.
  • 해당 주제는 아이템 43 람다보다는 메서드 참조를 사용하라 라는 주제를 참고하길 바람

정리

  • 자바 8로 오면서 작은 함수 객체를 구현하는 데 적합한 ㄹ마다가 도입
  • 익명 클래스는 (함수형 인터페이스가 아닌) 타입의 인스턴스를 만들 때만 사용하자
  • 람다는 작은 함수 객체를 아주 쉽게 표현할 수 있음 --> 함수형 프로그래밍의 지평을 열었음

Reference

코딩 개념 잡는 자바 코딩 문제집, 길벗, [앵겔 레너드]

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions