Java 유지보수 코드 리팩토링 3 - 람다, 익명 클래스

이전 시간에 작성한 정적 중첩 클래스를 지역 클래스로 변경한다.

Process.class

public interface Process {
    void run();
}

Dice.class

import org.w3c.dom.ls.LSOutput;

import java.util.Random;

public class Dice {

    public static void hello(Process proc) {
        System.out.println("프로그램 시작");

        // 코드 시작
        proc.run();
        // 코드 종료

        System.out.println("프로그램 종료");
        System.out.println();
    }

    static class ThrowDice implements Process{
        public void run() {
            int random = new Random().nextInt(6) + 1;
            System.out.println("주사위 굴린 결과: " + random);
        }
    }

    static class Sum implements Process{
        public void run() {
            // 코드 시작
            for (int i = 0; i < 3; i++) {
                System.out.println("i = " + i);
            }
            // 코드 종료
        }
    }

    public static void main(String[] args) {
        ThrowDice dice = new ThrowDice();
        Sum sum = new Sum();
        hello(dice);
        hello(sum);
    }
}

프로그램 시작
주사위 굴린 결과: 3
프로그램 종료

프로그램 시작
i = 0
i = 1
i = 2
프로그램 종료


리팩토링1 - 지역 클래스로 변경하기

주어진 코드를 지역 클래스로 변경하였다.

Dice.class

import org.w3c.dom.ls.LSOutput;

import java.util.Random;

public class Dice {
    public static void hello(Process proc) {
        System.out.println("프로그램 시작");

        // 코드 시작
        proc.run();
        // 코드 종료

        System.out.println("프로그램 종료");
        System.out.println();
    }

    public static void main(String[] args) {
        class ThrowDice implements Process{
            public void run() {
                int random = new Random().nextInt(6) + 1;
                System.out.println("주사위 굴린 결과: " + random);
            }
        }

        class Sum implements Process{
            public void run() {
                // 코드 시작
                for (int i = 0; i < 3; i++) {
                    System.out.println("i = " + i);
                }
                // 코드 종료
            }
        }

        ThrowDice dice = new ThrowDice();
        Sum sum = new Sum();

        hello(dice);
        hello(sum);
    }
}

Dice 클래스에서 ThrowDice, Sum 클래스를 main 메소드로 지역 클래스로 만들면서 옮겼다.

  • 정적 중첩 클래스를 지역 클래스로 변경
  • 기존 코드를 main 메소드로 이동 (선택사항)
    • 전달하는 코드가 변경될 수 있으므로 호출부인 main 메소드로 옮겼음
    • Dice 클래스는 코드를 전달받아 출력하는 역할임

리팩토링2 - 익명 클래스로 변경하기

리팩토링한 지역클래스를 익명 클래스로 변경하도록 한다.

익명 클래스 선언과 생성하기

Dice.class

import org.w3c.dom.ls.LSOutput;

import java.util.Random;

public class Dice {
    public static void hello(Process proc) {
        System.out.println("프로그램 시작");

        // 코드 시작
        proc.run();
        // 코드 종료

        System.out.println("프로그램 종료");
        System.out.println();
    }

    public static void main(String[] args) {
        Process dice = new Process() {
            @Override
            public void run() {
                int random = new Random().nextInt(6) + 1;
                System.out.println("주사위 굴린 결과: " + random);
            }
        };

        Process sum = new Process() {
            @Override
            public void run() {
                // 코드 시작
                for (int i = 0; i < 3; i++) {
                    System.out.println("i = " + i);
                }
                // 코드 종료
            }
        };

        hello(dice);
        hello(sum);
    }
}
  • 지역 클래스의 선언 과 생성 별도로 진행 부분이 있었다.
  • 익명 클래스로 변경하면서 선언과 생성을 한번에 처리한다.

익명 클래스를 메소드 인자로 전달하기

Dice.class

import org.w3c.dom.ls.LSOutput;

import java.util.Random;

public class Dice {
    public static void hello(Process proc) {
        System.out.println("프로그램 시작");

        // 코드 시작
        proc.run();
        // 코드 종료

        System.out.println("프로그램 종료");
        System.out.println();
    }

    public static void main(String[] args) {

        hello(new Process() {
            @Override
            public void run() {
                int random = new Random().nextInt(6) + 1;
                System.out.println("주사위 굴린 결과: " + random);
            }
        });

        hello(new Process() {
            @Override
            public void run() {
                // 코드 시작
                for (int i = 0; i < 3; i++) {
                    System.out.println("i = " + i);
                }
                // 코드 종료
            }
        });
    }
}
  • hello 인자로 익명 클래스로 전달함

익명 클래스로 호출부에서 코드를 만들어 해당 함수로 전달해 실행하는 모습을 볼 수 있다. 이러한 특징은 다른 언어에서 콜백함수, 델리게이트, 익명함수 기능과 유사한 개념이 있다.

그리고 자바 개발자는 해당 익명 클래스의 편의성이 문제가 있다.

hello(new Process() {
    @Override
    public void run() {
        // 코드 시작
        for (int i = 0; i < 3; i++) {
            System.out.println("i = " + i);
        }
        // 코드 종료
    }
});

전달하고자 하는 코드에서 인스턴스를 생성하고 run() 메소드를 생성해 전달하는 모습을 볼 수 있다.

자바 개발자가 원하는건 인스턴스 생성과 run() 메소드 없이 실행한 것을 원한다.


람다(Lambda)

앞서 말한 코드에서 hello() 호출 인자로 "new Process() {...}" 생성 코드를 전달하는 모습을 볼 수 있는데

hello({
        for (int i = 0; i < 3; i++) {
            System.out.println("i = " + i);
        }
        // 코드 종료
});

생성 부분을 제거하고 위의 코드처럼 편의성을 높이는 방법을 찾게 되었다.

이 방법은 람다(Lambda) 함수 기능으로 자바8에서 지원하게 되었다.

람다 이전 코드 스타일

  • int, double 같은 기본형으로 인자 전달
  • Process, Member 참조형 타입 전달

메소드로 인수로 전달하는 것은 간단한 데이터나 인스턴스의 참조이다.

람다 필요성

public runDice() {
    int randomValue = new Random().nextInt(6) + 1;
    System.out.println("주사위 = " + randomValue);
}

public void runSum() {
    for (int i = 1; i <= 3; i++) {
        System.out.println("i = " + i);
    }
}
hello(runDice());
hello(runSum());

메소드(runDice, runSum)를 만들고 인스턴스를 생성하여 전달할 필요가 있는지 생각해 볼 수 있다.
위의 코드처럼 클래스나 인스턴스 관계 없이 메소드만 전달하면 더 간단할 수 있다.

자바8에서는 큰 변화로 이러한 인자로 메소드를 전달할 수 있게 되었다.
기능의 이름은 람다(Lambda)이다.

hello(() ->  {
    int randomValue = new Random().nextInt(6) + 1;
    System.out.println("주사위 = " + randomValue);
});

hello(() -> {
    for (int i = 1; i <= 3; i++) {
        System.out.println("i = " + i);
    }
});

익명 클래스 대신 람다만 사용해도 되지 않을까?

익명 클래스는 멤버변수 선언이 가능하여 사용할 수 있으므로 익명 클래스도 필요하다.