람다가 필요한 이유

Jun·2025년 4월 7일

고급 자바

목록 보기
1/7
post-thumbnail

개요

저는 최근 자바를 활용한 프로젝트를 진행하면서, 반복적인 로직 처리나 컬렉션 데이터를 다루는 작업에서 점점 코드가 복잡해지는 걸 느꼈습니다.

더 간결하고 읽기 쉬운 방식으로 코드를 작성하고 싶다는 생각이 들었고, 자연스럽게 자바 8 이후에 도입된 람다, 스트림, 함수형 프로그래밍에 관심을 가지게 되었습니다.

그래서 저는 김영한님의 "실전 자바 - 고급 3편" 강의를 듣기로 했고, 강의를 들으며 배운 내용을 정리하고 제 것으로 만들기 위해 이렇게 글을 써보려고 합니다.

단순히 문법을 익히는 것을 넘어서, 왜 이런 문법이 필요하고, 기존 방식과 어떤 차이가 있는지, 실무에서는 어떻게 적용할 수 있는지에 대해서도 같이 고민해보려고 합니다.

값 매개변수화(Value Parameterization)

람다에 대해서 알아보기 전에 간단한 예제 하나를 살펴봅시다.

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
프로그램 종료

코드의 중복이 보여서 리팩토링해서 코드의 중복을 줄여봅시다.

public class Ex0RefMain {

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

    public static void main(String[] args) {
        hello("Java");
        hello("Spring");
    }
}

여기서 "Hello Java" , "Hello Spring" 와 같은 문자열은 상황에 따라서 변하기에 상황에 따라 변하는 문자열 데이터를 다음과 같이 매개변수(파라미터)를 통해 외부에서 전달 받아서 출력하도록 했다.

단순한 예제였지만 프로그래밍에서 중복을 제거하고, 좋은 코드를 유지하는 핵심은 변하는 부분과 변하지 않는 부분을 분리하는 것이다. 이렇게 이렇게 변하는 부분과 변하지 않는 부분을 분리하고, 변하는 부분을 외부에서 전달 받으면, 메서드(함수)의 재사용성을 높일 수 있다.

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

문자값(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

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

어떻게 코드 조각을 전달할 수 있을까?

코드 조각은 보통 메서드(함수)에 정의한다. 따라서 코드 조각을 전할하기 위해서는 메서드가 필요하다. 인스턴스를 전달하고, 인스턴스에 있는 메서드를 호출하면 됩니다.

리팩토링 후

public interface Procedure {
    void run();
}

public class Ex1RefMain {
    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 Dice();

        hello(dice);
        hello(sum);
    }
}
  • Dice , Sum 각각의 클래스는 Procedure 인터페이스를 구현하고 run() 메서드에 필요한 코드 조각을 구현했다.
  • 리팩토링한 hello() 메서드에는 Procedure procedure 매개변수(파라미터)를 통해 인스턴스를 전달할 수 있다. 이 인스턴스의 run() 메서드를 실행하면 필요한 코드 조각을 실행할 수 있다.
  • 이때 다형성을 활용해서 외부에서 전달되는 인스턴스에 따라 각각 다른 코드 조각이 실행된다.

동작 매개변수화(Behavior Parameterization)

값 매개변수화(Value Parameterization)

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

동작 매개변수화(Behavior Parameterization)

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

정리하면 다음과 같다.

  • 값 매개변수화: 값(숫자, 문자열 등)을 바꿔가며 메서드(함수)의 동작을 달리 함
  • 동작 매개변수화: 어떤 동작(로직)을 수행할지를 메서드(함수)에 전달(인스턴스 참조, 람다 등)

자바에서 동작 매개변수화를 하려면 클래스를 정의하고 해당 클래스를 인스턴스로 만들어서 전달해야 한다.
자바8에서 등장한 람다를 사용하면 코드 조각을 매우 편리하게 전달 할 수 있는데, 람다를 알아보기 전에 기존에 자바로 할 수 있는 다양한 방법들을 먼저 알아보자.

이번에는 익명 클래스를 사용해서 같은 기능을 구현해보자.

    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);
    }
    
    

실행 결과는 기존과 같습니다.

익명클래스 사용2 - 참조값 직접 전달

익명 클래스의 참조값을 지역 변수에 담아둘 필요 없이, 매개변수에 직접 전달해봅시다.

    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)

람다를 이용한 리팩토링

    public static void main(String[] args) {
        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);
            }
        });
    }
  • () -> {...} 부분이 람다를 사용한 코드이다.
  • 람다를 사용한 코드를 보면 클래스나 인스턴스를 정의하지 않고, 매우 간편하게 코드 블럭을 직접 정의하고, 전달하는 것을 확인할 수 있다.

함수 vs 메서드

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

객체(클래스)와의 관계

함수(Function)

  • 독립적으로 존재하며, 클래스(객체)와 직접적인 연관이 없다.
  • 객체지향 언어가 아닌 C 등의 절차적 언어에서는 모든 로직이 함수 단위로 구성된다.

메서드(Method)

  • 클래스(또는 객체)에 속해 있는 "함수"이다.
  • 객체의 상태(필드, 프로퍼티 등)에 직접 접근하거나, 객체가 제공해야 할 기능을 구현할 수 있다.
  • 객체지향 언어에서 클래스 내부에 정의된 함수는 보통 "메서드"라고 부른다.

호출 방식과 스코프

함수(Function)

  • 호출 시에 객체 인스턴스가 필요 없다.
  • 보통 이름(매개변수) 형태로 호출된다.
  • 지역 변수, 전역 변수 등과 함께 동작하며, 클래스나 객체 특유의 속성(인스턴스 변수 등)은 다루지 못한다.

메서드(Method)

  • 보통 객체(인스턴스).메서드이름(매개변수) 형태로 호출한다.
  • 호출될 때, 해당 객체의 필드(속성)나 다른 메서드에 접근 가능하며, 이를 이용해 로직을 수행한다.
  • 인스턴스 메서드, 클래스(정적) 메서드, 추상 메서드 등 다양한 형태가 있을 수 있다.

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

따라서 함수와 메서드는 수행하는 역할 자체는 같지만, 소속과 호출 방식에서 차이가 난다.

이제 본격적으로 람다에 대해서 알아보자.

매개변수가 없는 람다

    public static void main(String[] args) {
        Procedure procedure = () ->{
                System.out.println("hello! lambda");
            };

        procedure.run();
    }
  • 람다는 () -> {} 와 같이 표현한다. () 부분이 메서드의 매개변수라 생각하면 되고, {} 부분이 코드 조각이 들어가는 본문이다.
  • 람다를 사용할 때는 이름, 반환 타입은 생략하고, 매개변수와 본문만 간단하게 적으면 된다.
    (매개변수) -> { 본문 } , 여기서는 매개변수가 없으므로 () -> {본문}
  • 쉽게 이야기해서 익명 클래스를 만들기 위한 모든 부분을 생략하고, 꼭 필요한 매개변수와 본문만 작성하면 된다.
  • 익명 클래스를 사용하는 것 보다 람다를 사용할 때 코드가 훨씬 간결한 것을 확인할 수 있습니다.

매개변수가 있는 람다

    public static void main(String[] args) {
        MyFunction myFunction = (int a, int b) -> {
                return a + b;
            };

        int apply = myFunction.apply(1, 2);
        System.out.println("result = " + apply);
    }
  • 람다는 () -> {} 와 같이 표현한다.
  • 람다를 사용할 때는 이름, 반환 타입은 생략하고, 매개변수와 본문만 간단하게 적으면 된다.
  • 이번에는 매개변수가 있으므로 (int a, int b) -> {본문} 과 같이 작성하면 된다.

참고

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

profile
꾸준하게

0개의 댓글