728x90
반응형
SMALL

참고자료

 

김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍 강의 | 김영한 - 인프런

김영한 | , [사진]국내 개발 분야 누적 수강생 1위,제대로 만든 김영한의 실전 자바[사진][임베딩 영상]단순히 자바 문법을 안다? 이걸로는 안됩니다!전 우아한형제들 기술이사, 누적 수강생 40만

www.inflearn.com

 

자바 8 이후로 람다가 도입됐고, 람다가 필요한 이유에 대해서 알아보자. 아래 코드를 보자.

package lambda.start;

public class Ex0Main {
    public static void helloJava() {
        System.out.println("프로그램 시작");
        System.out.println("Hello Java");
        System.out.println("프로그램 종료");
    }

    public static void helloSpring() {
        System.out.println("프로그램 시작");
        System.out.println("Hello Spring");
        System.out.println("프로그램 종료");
    }

    public static void main(String[] args) {
        helloJava();
        helloSpring();
    }
}
프로그램 시작
Hello Java
프로그램 종료
프로그램 시작
Hello Spring
프로그램 종료
  • 코드의 중복이 보일 것이다. 코드를 리팩토링해서 코드의 중복을 제거해보자.

리팩토링 후

package lambda.start;

public class Ex0RefMain {
    public static void hello() {
        System.out.println("프로그램 시작"); // 변하지 않는 부분

        System.out.println("Hello Java");
        System.out.println("Hello Spring");

        System.out.println("프로그램 종료"); // 변하지 않는 부분
    }

    public static void hello(String str) {
        System.out.println("프로그램 시작");
        System.out.println(str);
        System.out.println("프로그램 종료");
    }

    public static void main(String[] args) {
        hello("hello Java");
        hello("hello Spring");
    }
}
  • 기존 코드에서 변하는 부분과 변하지 않는 부분을 분리해야 한다.
  • 여기서 핵심은 변하는 부분과 변하지 않는 부분을 분리하는 것이다. 변하는 부분은 그대로 유지하고 변하지 않는 부분을 어떻게 해결할 것인가에 집중하면 된다.

단순한 문제였지만, 프로그래밍에서 중복을 제거하고 좋은 코드를 유지하는 핵심은 변하는 부분과 변하지 않는 부분을 분리하는 것이다. 여기서는 변하지 않는 "프로그램 시작", "프로그램 종료"를 출력하는 부분은 그대로 유지하고, 상황에 따라 변화가 필요한 문자열은 외부에서 전달 받아서 처리했다.

 

이렇게 변하는 부분과 변하지 않는 부분을 분리하고, 변하는 부분을 외부에서 전달 받으면, 메서드의 재사용성을 높일 수 있다. 리팩토링 전과 후를 비교해보자. hello(String str) 메서드의 재사용성은 매우 높아졌다. 여기서 핵심은 변하는 부분을 메서드 내부에서 가지고 있는 것이 아니라, 외부에서 전달 받는다는 점이다.

 

값 매개변수화(Value Parameterization)

여기서 변하는 부분은 "Hello Java", "Hello Spring"같은 문자값(Value)이다. 

System.out.println("Hello Java");
System.out.println("Hello Spring");

 

이번 예제에서는 String str 매개변수(파라미터)를 사용해서 문자값을 매개변수로 만들었다.

public static void hello(String str) {
    System.out.println("프로그램 시작");
    System.out.println(str); // str: 변하는 부분
    System.out.println("프로그램 종료");
}
  • 문자값(Value), 숫자값(Value)처럼 구체적인 값을 메서드(함수)안에 두는 것이 아니라, 매개변수(파라미터)를 통해 외부에서 전달 받도록 해서, 메서드의 동작을 달리하고, 재사용성을 높이는 방법을 값 매개변수화(Value Parameterization)라 한다.

 

이번에는 비슷한 다른 문제를 한번 풀어보자.

public class Ex1Main {
    public static void helloDice() {
        long startNs = System.nanoTime();
        //코드 조각 시작
        int randomValue = new Random().nextInt(6) + 1;
        System.out.println("주사위 = " + randomValue);
        //코드 조각 종료
        long endNs = System.nanoTime();
        System.out.println("실행 시간: " + (endNs - startNs) + "ns");
    }

    public static void helloSum() {
        long startNs = System.nanoTime();
        //코드 조각 시작
        for (int i = 1; i <= 3; i++) {
            System.out.println("i = " + i);
        }
        //코드 조각 종료
        long endNs = System.nanoTime();
        System.out.println("실행 시간: " + (endNs - startNs) + "ns");
    }

    public static void main(String[] args) {
        helloDice();
        helloSum();
    }
}
주사위 = 2
실행 시간: 2882959ns
i = 1
i = 2
i = 3
실행 시간: 191083ns

 

이 코드를 이전에 리팩토링 한 예와 같이 하나의 메서드에서 실행할 수 있도록 리팩토링 해보자. 참고로 이전 문제는 변하는 문자열 값을 매개변수화 해서 외부에서 전달하면 되었다. 이번에는 문자열 같은 단순한 값이 아니라 "코드 조각"을 전달해야 한다.

 

리팩토링 후

@FunctionalInterface
public interface Procedure {
    void run();
}
// 정적 중첩 클래스 사용
public class Ex1RefMainV1 {
    
    public static void hello(Procedure procedure) {
        long startNs = System.nanoTime();
        //코드 조각 시작
        procedure.run();
        //코드 조각 종료
        long endNs = System.nanoTime();
        System.out.println("실행 시간: " + (endNs - startNs) + "ns");
    }

    static class Dice implements Procedure {
        @Override
        public void run() {
            int randomValue = new Random().nextInt(6) + 1;
            System.out.println("주사위 = " + randomValue);
        }
    }

    static class Sum implements Procedure {
        @Override
        public void run() {
            for (int i = 1; i <= 3; i++) {
                System.out.println("i = " + i);
            }
        }
    }

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

여기서는 단순히 데이터를 전달하는 수준을 넘어서, 코드 조각을 전달해야 한다. 

 

어떻게 외부에서 코드 조각을 전달할 수 있을까?

코드 조각은 보통 메서드에 정의한다. 따라서 코드 조각을 전달하기 위해서는 메서드가 필요하다. 그런데 지금까지 학습한 내용으로는 메서드만 전달할 수 있는 방법이 없다. 대신에 인스턴스를 전달하고, 인스턴스에 있는 메서드를 호출하면 된다. 이 문제를 해결하기 위해 인터페이스를 정의하고 구현 클래스를 만들었다. 

 

물론 정적 중첩 클래스가 아니라 외부에 따로 클래스를 만들어도 상관없다. 리팩토링한 hello() 메서드에는 Procedure 매개변수(파라미터)를 통해 인스턴스를 전달할 수 있다. 이 인스턴스의 run() 메서드를 실행하면 필요한 코드 조각을 실행할 수 있다. 이때, 다형성을 활용해서 외부에서 전달되는 인스턴스에 따라 각각 다른 코드 조각이 실행된다. 

 

실행 결과

주사위 = 4
실행 시간: 3570583ns
i = 1
i = 2
i = 3
실행 시간: 172542ns

 

동작 매개변수화 (Behavior Parameterization)

값 매개변수화는 문자값, 숫자값처럼 구체적인 값을 메서드(함수)안에 두는 것이 아니라, 매개변수(파라미터)를 통해 외부에서 전달 받도록 해서, 메서드의 동작을 달리하고, 재사용성을 높이는 방법이었다. 

 

동작 매개변수화는, 코드 조각(코드의 동작 방법, 로직, Behavior)을 메서드(함수)안에 두는 것이 아니라, 매개변수(파라미터)를 통해서 외부에서 전달 받도록 해서, 메서드의 동작을 달리하고, 재사용성을 높이는 방법이다. 동작 매개변수화, 동작 파라미터화, 행동 매개변수화, 행위 파라미터화 등 다양하게 불린다.

 

자바에서 동작 매개변수화를 하려면 클래스를 정의하고 해당 클래스를 인스턴스로 만들어서 전달해야 한다. 자바8에서 등장한 람다를 사용하면 코드 조각을 매우 편리하게 전달할 수 있다!

 

 

익명 클래스 사용

람다를 사용하기 전에 기존 자바로 할 수 있는 다양한 방법들을 먼저 알아보자.

package lambda.start;

import lambda.Procedure;

import java.util.Random;

// 익명 클래스 사용
public class Ex1RefMainV2 {
    public static void hello(Procedure procedure) {
        long startNs = System.nanoTime();
        //코드 조각 시작
        procedure.run();
        //코드 조각 종료
        long endNs = System.nanoTime();
        System.out.println("실행 시간: " + (endNs - startNs) + "ns");
    }

    public static void main(String[] args) {
        Procedure dice = new Procedure() {
            @Override
            public void run() {
                int randomValue = new Random().nextInt(6) + 1;
                System.out.println("주사위 = " + randomValue);
            }
        };
        Procedure sum = new Procedure() {
            @Override
            public void run() {
                for (int i = 1; i <= 3; i++) {
                    System.out.println("i = " + i);
                }
            }
        };
        hello(dice);
        hello(sum);
    }
}
  • 익명 클래스를 사용해서, 번거롭게 클래스를 계속 만들지 않는 것이다.
  • 여기서 더 나아가서, 참조값을 변수에 담지 않고 매개변수에 바로 전달도 가능하다.
package lambda.start;

import lambda.Procedure;

import java.util.Random;

// 익명 클래스 사용, 변수 제거, 익명 클래스의 참조값을 매개변수(파라미터)에 직접 전달
public class Ex1RefMainV3 {
    public static void hello(Procedure procedure) {
        long startNs = System.nanoTime();
        //코드 조각 시작
        procedure.run();
        //코드 조각 종료
        long endNs = System.nanoTime();
        System.out.println("실행 시간: " + (endNs - startNs) + "ns");
    }

    public static void main(String[] args) {
        hello(new Procedure() {
            @Override
            public void run() {
                int randomValue = new Random().nextInt(6) + 1;
                System.out.println("주사위 = " + randomValue);
            }
        });
        hello(new Procedure() {
            @Override
            public void run() {
                for (int i = 1; i <= 3; i++) {
                    System.out.println("i = " + i);
                }
            }
        });
    }
}

 

 

람다(Lambda)

자바에서 메서드의 매개변수에 인수로 전달할 수 있는 것은 크게 2가지이다. 

  • int, double과 같은 기본형 타입
  • Procedure, Member와 같은 참조형 타입(인스턴스)

결국 메서드에 인수로 전달할 수 있는 것은 간단한 값이나, 인스턴스의 참조이다. 지금처럼 코드 조각을 전달하기 위해 클래스를 정의하고 메서드를 만들고 또 인스턴스까지 생성하는 복잡한 과정을 거쳐야 할까? 생각해보면 클래스나 인스턴스와 관계 없이 다음과 같이 직접 코드 블럭을 전달할 수 있다면 더 간단하지 않을까?

public static void main(String[] args) {
        hello({ // 랜덤 값을 출력하는 코드 블럭
            int randomValue = new Random().nextInt(6) + 1;
            System.out.println("주사위 = " + randomValue);
        });
        
        hello({ // 1 ~ 3 출력하는 코드 블럭
            for (int i = 1; i <= 3; i++) {
                System.out.println("i = " + i);
            }
        });
    }

 

자바 8에 들어서면서 큰 변화가 있었는데, 바로 람다라는 것을 통해 코드 블럭을 인수로 전달할 수 있게 되었다. 다음 코드로 확인해보자.

 

리팩토링 - 람다

package lambda.start;

import lambda.Procedure;

public class Ex1Main {

    public static void process(Procedure procedure) {
        long startMs = System.nanoTime();

        procedure.run();

        long endMs = System.nanoTime();
        System.out.println("실행 시간 = " + (endMs - startMs) + " ms");
    }

    public static void main(String[] args) {
        process(() -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("i = " + i);
            }
        });
    }
}
  • 람다를 사용한 코드를 보면 클래스나 인스턴스를 정의하지 않고, 매우 간편하게 코드 블럭을 직접 정의하고, 전달하는 것을 확인할 수 있다.
  • 이것이 람다를 사용하는 이유이다. 람다는 함수이다. 따라서 람다를 제대로 이해하기 위해서는 먼저 함수에 대해서 알아야 한다. 함수와 메서드의 차이를 먼저 간단히 알아보자.

 

함수 vs 메서드

함수(Function)와 메서드(Method)는 둘 다 어떤 작업(로직)을 수행하는 코드의 묶음이다. 하지만 일반적으로 객체지향 프로그래밍(OOP) 관점에서 다음과 같은 차이가 있다. 

 

객체(클래스)와의 관계

  • 함수(Function)
    • 독립적으로 존재하며, 클래스와 직접적인 연관이 없다.
    • 객체지향 언어가 아닌 C 등의 절차적 언어에서는 모든 로직이 함수 단위로 구성된다.
    • 객체지향 언어라 하더라도 예를 들어, Python이나 JavaScript처럼 클래스 밖에서도 정의할 수 있는 함수 개념을 지원하는 경우 이를 그냥 함수라고 부른다.
  • 메서드(Method)
    • 클래스(또는 객체)에 속해 있는 "함수"이다.
    • 객체의 상태(필드, 프로퍼티 등)에 직접 접근하거나, 객체가 제공해야 할 기능을 구현할 수 있다.
    • Java, C++, Python 등 대부분의 객체지향 언어에서 클래스 내부에 정의된 함수는 보통 "메서드"라 부른다. 

 

호출 방식과 스코프

  • 함수(Function)
    • 호출 시에 객체 인스턴스가 필요 없다.
    • 보통 이름(매개변수) 형태로 호출된다.
    • 지역 변수, 전역 변수 등과 함께 동작하며, 클래스나 객체 특유의 속성은 다루지 못한다.
  • 메서드(Method)
    • 보통 객체.메서드이름(매개변수) 형태로 호출된다.
    • 호출될 때, 해당 객체의 필드나 다른 메서드에 접근 가능하며 이를 이용해 로직을 수행한다.
    • 인스턴스 메서드, 클래스(정적) 메서드, 추상 메서드 등 다양한 형태가 있을 수 있다.

 

정리

  • 메서드는 기본적으로 클래스(객체) 내부의 함수를 가리키며, 객체의 상태와 밀접한 관련이 있다.
  • 함수는 클래스(객체)와 상관없이, 독립적으로 호출 가능한 로직의 단위이다.
  • 메서드는 객체지향에서 클래스 안에 정의하는 특별한 함수라고 생각하면 된다.

 

람다를 사용하는 이유

  • 변하는 부분과 변하지 않는 부분을 분리하는 것이 좋은 프로그래밍 코드를 작성하는 핵심
  • 변하는 부분이 '값'이라면 메서드로도 충분히 이 작업을 수행할 수 있지만,
  • 변하는 부분이 '코드 조각'이라면? 자바에서는 인스턴스를 넘기고 해당 인스턴스의 메서드를 호출해야 한다.
  • 지금까지는 이 '코드 조각'을 넘기기 위해 다형성을 이용하여 각각의 인스턴스가 서로 다른 동작을 하지만 부모를 상속받게 하여 변하는 부분을 받는 메서드에서 이 인스턴스를 받아 처리했다.
  • 그런데, 코드 조각을 넘길때마다 이 작업은 너무 귀찮고 번거롭고 가시성이 떨어진다.
  • 코드 조각 자체(함수)를 의미하는 람다를 자바8에서 도입하여 번거롭게 인스턴스를 만들거나 익명 클래스를 사용하지 않아도 된다.

 

728x90
반응형
LIST

'JAVA의 가장 기본이 되는 내용' 카테고리의 다른 글

takeWhile, dropWhile, flatMap - Stream API  (0) 2025.03.31
람다 vs 익명 클래스  (0) 2025.03.30
Lombok은 어떻게 동작하는걸까?  (0) 2024.12.01
[Java 8] CompletableFuture  (0) 2024.11.30
[Java 8] Optional  (0) 2024.11.27

+ Recent posts