Header Ads Widget

Responsive Advertisement

[Java] 5장. 자바 함수형 프로그래밍

5장. 자바 함수형 프로그래밍

📚 5장. 자바 함수형 프로그래밍 (Functional Programming)

5.1 함수형 프로그래밍이란?

함수형 프로그래밍(Functional Programming)은 순수 함수와 불변성을 중시하는 선언형 프로그래밍 스타일입니다. 자바 8부터는 람다식Stream API, 함수형 인터페이스 등을 통해 함수형 프로그래밍 스타일을 지원합니다.

🧠 핵심 개념 요약

개념설명
명령형 프로그래밍어떻게 할 것인가에 집중 (반복문, 변수 조작)
선언형 프로그래밍무엇을 할 것인가에 집중 (ex. SQL, Stream API)
순수 함수입력이 같으면 항상 같은 출력을 반환, 부작용 없음
불변성데이터 상태를 변경하지 않음
일급 함수함수를 값처럼 취급하여 전달하거나 반환 가능


차이 설명:
명령형 프로그래밍은 "어떻게 처리할지 절차 중심으로 설명"합니다. 즉, 작업을 수행하기 위해 반복문과 조건문 등을 사용하여 모든 처리 흐름을 직접 작성합니다.

반면 선언형 프로그래밍은 "무엇을 할 것인지 선언"하고, 내부 구현 세부 사항은 시스템이나 언어의 라이브러리가 처리합니다. 개발자는 결과를 설명할 뿐, 처리 방법은 직접 지정하지 않아도 됩니다.

예를 들어, SQL은 "SELECT * FROM ..." 형태로 원하는 결과만 선언하고, 실제 데이터 검색 알고리즘은 데이터베이스 시스템이 처리합니다. 마찬가지로 자바의 Stream API무엇을 하고 싶은가에 집중하게 해줍니다.

// 명령형 스타일
List<String> result = new ArrayList<>();
for (String s : list) {
    if (s.startsWith("A")) {
        result.add(s.toLowerCase());
    }
}

코드 분석:

  • new ArrayList<>(): 결과를 담을 리스트 생성
  • for (String s : list): 리스트의 모든 요소 반복
  • if (s.startsWith("A")): 'A'로 시작하는 항목만 필터링
  • s.toLowerCase(): 소문자로 변환
  • result.add(): 결과 리스트에 추가
// 선언형 스타일 (함수형)
List<String> result = list.stream()
    .filter(s -> s.startsWith("A"))
    .map(String::toLowerCase)
    .collect(Collectors.toList());

코드 분석:

  • filter(): 'A'로 시작하는 항목만 남김
  • map(String::toLowerCase): 각 요소에 대해 String 클래스의 toLowerCase() 메서드를 적용해 소문자로 변환
  • collect(): 리스트로 수집

5.2 함수형 인터페이스란?

함수형 인터페이스(Functional Interface)추상 메서드가 하나만 존재하는 인터페이스를 말합니다. 자바 8부터 도입되었으며, 람다식과 함께 자주 사용됩니다.

자바 표준 라이브러리에는 이미 다양한 함수형 인터페이스가 제공되며, 대표적으로 아래 4가지를 자주 사용합니다.

✅ 주요 함수형 인터페이스

이름타입설명예시
Function<T, R>입력 → 출력T 타입을 받아 R 타입으로 반환str -> str.length()
Consumer<T>입력 → 소비T 타입을 받아 처리하지만 반환은 없음System.out::println
Supplier<T>출력만 있음T 타입 값을 반환 (입력 없음)() -> "Hello"
Predicate<T>입력 → booleanT 타입을 받아 true/false 반환n -> n > 0

🧪 Function 예제

Function<String, Integer> getLength = str -> str.length();
int len = getLength.apply("Stream");
System.out.println(len);

코드 분석:

  • Function<String, Integer>: 입력은 String, 반환은 Integer
  • str -> str.length(): 람다식으로 문자열 길이 반환
  • apply("Stream"): Function을 실행하여 6 반환

🧪 Consumer 예제

Consumer<String> printUpper = s -> System.out.println(s.toUpperCase());
printUpper.accept("java");

코드 분석:

  • Consumer<String>: 입력은 String, 반환 없음
  • s -> System.out.println(...): 입력값을 대문자로 출력
  • accept("java"): 실행 결과 JAVA 출력

🧪 Supplier 예제

Supplier<Double> random = () -> Math.random();
System.out.println(random.get());

코드 분석:

  • Supplier<Double>: 입력 없이 Double 값 생성
  • () -> Math.random(): Math 클래스에서 난수 생성
  • get(): 호출 시 새로운 난수 반환

🧪 Predicate 예제

Predicate<Integer> isEven = n -> n % 2 == 0;
System.out.println(isEven.test(10));

코드 분석:

  • Predicate<Integer>: 입력은 Integer, 결과는 boolean
  • n -> n % 2 == 0: 짝수인지 검사
  • test(10): true 반환

이러한 함수형 인터페이스들은 람다와 함께 사용될 때 코드의 간결성, 유연성, 재사용성을 크게 향상시킵니다.

5.3 람다와 함수형 인터페이스 실습

람다는 익명 함수를 표현하는 방식으로, 함수형 인터페이스와 함께 사용됩니다. 아래는 각 인터페이스와 람다식이 실제 코드에 어떻게 적용되는지를 보여주는 예시입니다.

🎯 실습 예제: Function + 람다

Function<Integer, String> gradeMapper = score -> {
    if (score >= 90) return "A";
    else if (score >= 80) return "B";
    else if (score >= 70) return "C";
    else return "F";
};
System.out.println(gradeMapper.apply(85));

코드 분석:

  • Function<Integer, String>: 정수를 받아 등급 문자열 반환
  • score -> { ... }: 조건에 따라 등급 매핑
  • apply(85): "B" 반환

🎯 실습 예제: Predicate + 람다

Predicate<String> isEmail = s -> s.contains("@");
System.out.println(isEmail.test("user@example.com"));

코드 분석:

  • Predicate<String>: 문자열 입력 받아 boolean 결과 반환
  • s.contains("@"): 이메일 형식인지 단순 확인

🎯 실습 예제: Consumer + 람다

Consumer<List<String>> printer = list -> list.forEach(System.out::println);
printer.accept(Arrays.asList("apple", "banana"));

코드 분석:

  • Consumer<List<String>>: 리스트를 받아 출력 작업 수행
  • list.forEach(...): 리스트의 모든 요소 출력

🎯 실습 예제: Supplier + 람다

Supplier<LocalDate> today = () -> LocalDate.now();
System.out.println(today.get());

코드 분석:

  • Supplier<LocalDate>: 현재 날짜 반환
  • () -> LocalDate.now(): 입력 없이 실행 시점 날짜 제공

5.4 메서드 참조 vs 익명 클래스

람다식과 함께 자주 사용되는 문법이 메서드 참조(Method Reference)입니다. 이는 메서드를 직접 호출하는 것이 아닌, 이름으로 간단히 표현하는 방식입니다. 또한 람다가 도입되기 전 자바에서는 익명 클래스(Anonymous Class)를 사용해 함수형 스타일을 흉내냈습니다.

🔁 예제: 메서드 참조 vs 람다 vs 익명 클래스

// 메서드 참조
Consumer<String> printer1 = System.out::println;
printer1.accept("Hello Method Reference");

// 람다식
Consumer<String> printer2 = s -> System.out.println(s);
printer2.accept("Hello Lambda");

// 익명 클래스
Consumer<String> printer3 = new Consumer<String>() {
    @Override
    public void accept(String s) {
        System.out.println(s);
    }
};
printer3.accept("Hello Anonymous Class");

코드 분석:

  • System.out::println: 메서드 참조 방식, 가장 간결함
  • s -> System.out.println(s): 람다식, 명시적으로 표현
  • new Consumer<>() { ... }: 익명 클래스, 문법이 복잡하고 장황함

📌 정리

스타일장점단점
메서드 참조가장 간결하고 읽기 쉬움직관성이 떨어질 수 있음
람다식가독성 + 표현력 좋음중첩되면 가독성 떨어질 수 있음
익명 클래스클래스 구조를 명확히 표현 가능코드가 길고 복잡함

5.5 실무 활용 예제

실제 개발에서는 람다와 함수형 인터페이스를 함께 사용하여 코드의 복잡도를 줄이고 가독성을 높이는 다양한 패턴을 활용합니다. 아래는 리스트 필터링과 정렬, 데이터 수집을 함수형 스타일로 처리하는 실전 예제입니다.

🧩 예제: 조건 필터링 + 정렬 + 출력

List<String> names = Arrays.asList("alice", "bob", "alex", "brad", "anna");

names.stream()
     .filter(name -> name.startsWith("a"))
     .sorted()
     .map(String::toUpperCase)
     .forEach(System.out::println);

코드 분석:

  • filter(name -> name.startsWith("a")): 'a'로 시작하는 이름만 추출
  • sorted(): 알파벳 순 정렬
  • map(String::toUpperCase): 대문자로 변환
  • forEach(System.out::println): 최종 출력

🧩 예제: 객체 리스트에서 조건 필터 후 평균 구하기

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

List<User> users = Arrays.asList(
    new User("Tom", 28),
    new User("Jane", 35),
    new User("Sara", 24)
);

double avg = users.stream()
    .filter(u -> u.getAge() >= 30)
    .mapToInt(User::getAge)
    .average()
    .orElse(0);

System.out.println("평균 나이: " + avg);

코드 분석:

  • filter(): 나이가 30 이상인 사용자 필터링
  • mapToInt(User::getAge): 나이만 추출
  • average(): 평균값 계산
  • orElse(0): 값이 없을 경우 0 반환

🧩 예제: Map에서 값 조건 필터 + key 수집

Map<String, Integer> scores = Map.of(
    "Alice", 95,
    "Bob", 82,
    "Charlie", 67,
    "Diana", 88
);

List<String> passed = scores.entrySet().stream()
    .filter(entry -> entry.getValue() >= 80)
    .map(Map.Entry::getKey)
    .collect(Collectors.toList());

System.out.println(passed);

코드 분석:

  • entrySet().stream(): Map을 스트림으로 변환
  • filter(): 점수가 80 이상인 항목만 필터링
  • map(Map.Entry::getKey): key만 추출
  • collect(Collectors.toList()): 리스트로 수집

🧩 예제: List 그룹핑 후 갯수 세기

List<String> items = List.of("apple", "banana", "apple", "orange", "banana");

Map<String, Long> counted = items.stream()
    .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));

System.out.println(counted);

코드 분석:

  • groupingBy(Function.identity()): 값 자체를 기준으로 그룹화
  • Collectors.counting(): 각 항목의 개수 세기

🧩 예제: 문자열 리스트를 길이순으로 정렬

List<String> words = Arrays.asList("banana", "fig", "apple", "kiwi");

words.stream()
    .sorted(Comparator.comparingInt(String::length))
    .forEach(System.out::println);

코드 분석:

  • sorted(Comparator.comparingInt(...)): 길이를 기준으로 정렬
  • forEach(): 정렬된 단어 출력

🧩 예제: null 값 제거 후 정렬

List<String> cities = Arrays.asList("Seoul", null, "Busan", "Incheon", null);

cities.stream()
    .filter(Objects::nonNull)
    .sorted()
    .forEach(System.out::println);

코드 분석:

  • filter(Objects::nonNull): null 값 제거
  • sorted(): 알파벳 순 정렬

🧩 예제: 특정 접두사로 시작하는 문자열 개수 세기

List<String> items = Arrays.asList("car", "bus", "bike", "boat", "cab");

long count = items.stream()
    .filter(s -> s.startsWith("b"))
    .count();

System.out.println("'b'로 시작하는 단어 수: " + count);

코드 분석:

  • filter(...): 'b'로 시작하는 문자열만 통과
  • count(): 통과한 항목의 개수 반환





댓글 쓰기

0 댓글