CodeGym /행동 /JAVA 25 SELF /함수를 매개변수로 전달하기: 예제

함수를 매개변수로 전달하기: 예제

JAVA 25 SELF
레벨 49 , 레슨 2
사용 가능

1. 데이터 대신 동작 전달하기

왜 함수를 매개변수로 전달해야 할까요?

상상해 봅시다. 목록을 정렬하는 메서드가 있습니다. 그런데 정확히 어떻게 정렬해야 하는지 메서드는 어떻게 알까요? 사전순? 길이 기준? 생년월일 기준? 경우마다 별도의 메서드를 만들 수도 있지만, 곧 지옥 같은 복붙이 됩니다.

대신 Java에서는 동작(즉, 함수나 람다)을 전달해 정렬, 필터링, 변환 등을 어떻게 할지 결정할 수 있습니다. 이렇게 하면 코드는 더 유연하고 재사용 가능해집니다.

예시: Comparator로 정렬하기

List<String> names = List.of("안나", "보리스", "비카");

// 동작 전달: 요소를 어떻게 비교할지(문자열 길이 기준)
names.stream()
     .sorted((a, b) -> a.length() - b.length())
     .forEach(System.out::println);

예시: Predicate로 필터링하기

List<String> names = List.of("안나", "보리스", "비카");

// 동작 전달: 어떤 요소를 남길지(이름 길이가 4보다 큼)
names.stream()
     .filter(name -> name.length() > 4)
     .forEach(System.out::println);

두 경우 모두 메서드에 로직을 “하드코딩”하는 대신, 각 요소에 적용할 동작의 조각을 건네는 것입니다.

장점: 중복은 줄이고, 유연성은 높이고

유사한 코드에 로직만 다른 수십 개의 메서드 대신, ‘무엇을 할지’를 함수로 받는 범용 메서드 하나만 작성하면 됩니다. 시간도 절약되고, 오류도 줄어들며, 테스트도 쉬워집니다.

2. 함수 전달 문법

람다식을 인자로 전달

가장 흔한 방법은 메서드를 호출하는 자리에서 바로 화살표 ->를 사용해 람다를 작성하는 것입니다:

list.forEach(item -> System.out.println(item));

혹은 더 간단하게, 이미 알맞은 메서드가 있다면 메서드 참조(::)를 사용할 수 있습니다:

list.forEach(System.out::println);

메서드 참조 (method reference)

이미 알맞은 메서드가 있다면 참조 ::를 전달할 수 있습니다:

// 일반 메서드
public static boolean isLongName(String name) {
    return name.length() > 4;
}

// 메서드 참조 전달
names.stream()
     .filter(MyClass::isLongName)
     .forEach(System.out::println);

이는 메서드 시그니처가 함수형 인터페이스에서 기대하는 형태(Predicate<T>, Function<T, R>, Comparator<T> 등)와 일치할 때 동작합니다.

3. 표준 라이브러리에서의 예시

Collections.sort와 Comparator

List<String> names = new ArrayList<>(List.of("안나", "보리스", "비카"));

// 이름 길이로 정렬
names.sort((a, b) -> a.length() - b.length());
System.out.println(names); // [비카, 안나, 보리스]

Stream.filter와 Predicate

List<String> names = List.of("안나", "보리스", "비카");

// '비'로 시작하는 이름만 남기기
names.stream()
     .filter(name -> name.startsWith("비"))
     .forEach(System.out::println); // 비카

Stream.map과 Function

List<String> names = List.of("안나", "보리스", "비카");

// 이름을 대문자로 변환
names.stream()
     .map(String::toUpperCase)
     .forEach(System.out::println);

Optional.ifPresent와 Consumer

Optional<String> opt = Optional.of("안녕!");

// 값이 있으면 출력하기
opt.ifPresent(s -> System.out.println("문자열: " + s));

4. 실습: 함수 매개변수를 받는 메서드 직접 작성하기

이제 배운 것을 실전에 적용해 봅시다! 함수형 매개변수를 받아 내부에서 사용하는 몇 가지 메서드를 작성해 보겠습니다.

예제 1: 리스트 요소 처리 메서드 (forEach 나만의 버전)

사용자(User) 목록이 있다고 가정해 봅시다. 각 사용자에 대해 어떤 동작을 수행하고 싶습니다 — 예를 들어 이름을 출력하거나, e-mail을 보내거나, 보너스를 적립하는 등입니다. 동작을 코드에 “하드코딩”하는 대신, 매개변수로 전달해 봅시다 — Consumer<User>!

import java.util.List;
import java.util.function.Consumer;

class User {
    String name;
    int age;
    User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

public class UserProcessor {
    // Consumer<User>를 인수로 받는 메서드
    public static void processUsers(List<User> users, Consumer<User> action) {
        for (User user : users) {
            action.accept(user);
        }
    }

    public static void main(String[] args) {
        List<User> users = List.of(
            new User("안나", 25),
            new User("보리스", 30),
            new User("비카", 22)
        );

        // 모든 사용자의 이름 출력
        processUsers(users, user -> System.out.println("이름: " + user.name));

        // 보너스 지급(예제에서는 출력만)
        processUsers(users, user -> System.out.println(user.name + " 보너스를 받았습니다!"));
    }
}

예제 2: 요소를 거르는 메서드 (Predicate)

조건에 맞는 사용자만 반환하는 메서드를 작성해 봅시다(예: 성인만):

import java.util.List;
import java.util.ArrayList;
import java.util.function.Predicate;

public class UserFilter {
    public static List<User> filterUsers(List<User> users, Predicate<User> condition) {
        List<User> result = new ArrayList<>();
        for (User user : users) {
            if (condition.test(user)) {
                result.add(user);
            }
        }
        return result;
    }

    public static void main(String[] args) {
        List<User> users = List.of(
            new User("안나", 25),
            new User("보리스", 17),
            new User("비카", 22)
        );

        // 성인만 필터링
        List<User> adults = filterUsers(users, user -> user.age >= 18);
        adults.forEach(user -> System.out.println(user.name)); // 안나, 비카
    }
}

예제 3: 변환 메서드 (Function)

이제 사용자 목록에서 이름 목록(또는 다른 임의의 변환)을 만드는 메서드입니다:

import java.util.List;
import java.util.ArrayList;
import java.util.function.Function;

public class UserMapper {
    public static <R> List<R> mapUsers(List<User> users, Function<User, R> mapper) {
        List<R> result = new ArrayList<>();
        for (User user : users) {
            result.add(mapper.apply(user));
        }
        return result;
    }

    public static void main(String[] args) {
        List<User> users = List.of(
            new User("안나", 25),
            new User("보리스", 17),
            new User("비카", 22)
        );

        // 이름 목록 가져오기
        List<String> names = mapUsers(users, user -> user.name);
        System.out.println(names); // [안나, 보리스, 비카]

        // 나이 목록 가져오기
        List<Integer> ages = mapUsers(users, user -> user.age);
        System.out.println(ages); // [25, 17, 22]
    }
}

예제 4: 생성 메서드 (Supplier)

때로는 ‘요청 시’ 값을 받아야 합니다 — 예를 들어 난수를 생성하거나, 객체를 만들거나, 현재 시간을 얻는 경우입니다. 이를 위해 인터페이스 Supplier<T>가 적합합니다.

import java.util.function.Supplier;

public class ValueGenerator {
    public static int getValue(Supplier<Integer> supplier) {
        return supplier.get();
    }

    public static void main(String[] args) {
        // 난수 얻기
        int random = getValue(() -> (int)(Math.random() * 100));
        System.out.println("난수: " + random);

        // 현재 시간을 밀리초로 얻기
        long time = getValue(System::currentTimeMillis);
        System.out.println("시간: " + time);
    }
}

5. 하나의 애플리케이션: 예제 묶어 보기

간단한 “사용자 목록” 애플리케이션을 만든다고 가정해 봅시다. 우리는 이미 다음을 할 수 있습니다:

  • 조건에 따라 사용자를 필터링하기(예: 성인만);
  • 사용자를 다른 형태로 변환하기(이름, e-mail, 나이 등);
  • 각 사용자에 대해 동작 수행하기(출력, 보너스 지급 등);
  • 요청 시 값을 생성하기(예: 새 사용자 생성).

이제 이러한 메서드들을 조합해, 매번 새 요구사항마다 코드를 다시 쓰지 않고도 유연한 데이터 처리 시나리오를 구성할 수 있습니다.

import java.util.*;
import java.util.function.*;

public class UserApp {
    static class User {
        String name;
        int age;
        User(String name, int age) {
            this.name = name;
            this.age = age;
        }
        public String toString() {
            return name + " (" + age + ")";
        }
    }

    // 사용자 처리를 위한 범용 메서드
    static void processUsers(List<User> users, Consumer<User> action) {
        for (User user : users) action.accept(user);
    }

    // 범용 필터
    static List<User> filterUsers(List<User> users, Predicate<User> condition) {
        List<User> result = new ArrayList<>();
        for (User user : users) if (condition.test(user)) result.add(user);
        return result;
    }

    // 범용 변환기
    static <R> List<R> mapUsers(List<User> users, Function<User, R> mapper) {
        List<R> result = new ArrayList<>();
        for (User user : users) result.add(mapper.apply(user));
        return result;
    }

    public static void main(String[] args) {
        List<User> users = List.of(
            new User("안나", 25),
            new User("보리스", 17),
            new User("비카", 22)
        );

        // 1. 모든 사용자 출력
        processUsers(users, user -> System.out.println("사용자: " + user));

        // 2. 성인만 찾기
        List<User> adults = filterUsers(users, user -> user.age >= 18);
        System.out.println("성인: " + adults);

        // 3. 성인 이름만 얻기
        List<String> adultNames = mapUsers(adults, user -> user.name);
        System.out.println("성인 이름: " + adultNames);

        // 4. 성인에게 보너스 지급
        processUsers(adults, user -> System.out.println(user.name + " 보너스를 받았습니다!"));
    }
}

왜 “일반적인” 방식보다 좋을까요?

이 방법은 큰 유연성을 제공합니다. 같은 메서드라도 어떤 함수를 넘기느냐에 따라 전혀 다른 시나리오에 재사용할 수 있습니다. 코드가 “이렇게 필터링”, “저렇게 출력” 같은 끝없는 변형으로 불어나지 않고, 어디서든 쓸 수 있는 공용 도구를 갖게 됩니다. 덕분에 코드가 더 간결하고 이해하기 쉬우며 오류에도 강해집니다. 또한 이러한 스타일은 최신 Stream API와도 훌륭하게 맞물리므로, 스트림으로 넘어가더라도 새로 배울 것은 없습니다 — 접근 방식이 동일하기 때문입니다.

6. 함수를 매개변수로 전달할 때 흔한 실수

오류 №1: 시그니처가 기대하는 인터페이스와 일치하지 않음.
메서드가 Predicate<User>를 받는데, boolean이 아닌(예: String) 값을 반환하는 람다를 넘기면, 컴파일러가 곧바로 “아이아이아이!” 하며 빌드를 막습니다. 반환값과 매개변수가 기대와 일치하는지 확인하세요.

오류 №2: 람다가 변경될 수 있는 변수를 사용함.
Java에서 람다식은 외부 컨텍스트의 final 또는 effectively final 변수만 사용할 수 있습니다. 그런 변수를 람다 내부에서 변경하려 하면 컴파일 오류가 발생합니다.

오류 №3: 인터페이스 혼동.
가끔은 Consumer<T>를 넘겨야 하는데, 실수로 무언가를 반환하는 함수(예: Function<T, R>)를 작성할 때가 있습니다. 람다가 정확히 필요한 값을 반환하는지(아니면 아무것도 반환하지 않는지 — Consumer의 경우) 확인하세요.

오류 №4: 매개변수 자리에 지나치게 복잡한 람다 작성.
람다가 한두 줄을 넘어가면 별도의 변수나 메서드로 분리하는 편이 좋습니다. 그렇지 않으면 코드가 읽기 어려워지고, 결국 본인만(그마저도 항상은 아닐) 이해하게 됩니다.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION