본문 바로가기
IT/Java

[Java] Java 함수형 프로그래밍

by kyu-nahc 2024. 8. 27.
반응형

Java 함수형 프로그래밍

자바는 Java 8 버전부터 함수형 프로그래밍을 지원하기 위해 

람다(lambda)와 스트림(stream)이 도입되었다.

람다와 스트림을 사용하면 함수형 프로그래밍 스타일로 자바 코드를 작성할 수 있다.

 

물론 람다와 스트림을 사용하여 작성한 코드를

일반 스타일의 자바 코드로 바꾸어 작성하는 것이 불가능하지는 않다. 

달리 말하면 람다와 스트림 없이도 자바 코드를 작성하는 데 어려움이 없다는 뜻이다.

그런데도 람다와 스트림을 사용하는 이유는

작성하는 코드의 양이 줄어들고 읽기 쉬운 코드를 만들 수 있기 때문이다.

아래 예시가 명령형 프로그래밍과 함수형 프로그래밍의 예시이다.

public class DeploytestApplication {

	public static void main(String[] args) {

		printAllNumbersInListStructure(List.of(12,24,31,5,8,9));

	}
    
	private static void print(int number){
		System.out.println(number);
	}
    
	private static void printAllNumbersInListStructure(List<Integer> numbers){

            // 명령형 for문 접근법
            for(int index=0;index<numbers.size();index++){
                System.out.println(numbers.get(index));
            }

            // 함수형 프로그래밍을 이용
            // 메소드 참조 사용
            numbers.forEach(DeploytestApplication::print);
            
            // 더욱 단순한 함수형 프로그래밍
            numbers.forEach(System.out::println);
		
	}
}

 

명령형 for문 접근법에서는  i와 numbers의 사이즈와 비교하면서

i 값을 계속해서 증가시키면서 숫자값을 가져오고 표준 출력을 한다.

이처럼 명령형 반복문 안에는 가변적인 값들과 처리에 대한 코드가 섞여 있다.

 

하지만 forEach 문은 위의 코드에서 보시다시피 간단한 구조로 되어있다.

메서드 참조를 이용할 수 있는데, 여기서 메서드 참조란

해당 메서드를 실제 호출하는 것이 아닌, 호출할 메서드를 알려주는 것이다.

{ Class명 } :: { 메서드명 }를 통해 메서드 참조를 이용하여,

각 요소에 적용할 메서드를 지정해 줄 수 있다.

함수형 프로그래밍에서는 for, while문과 같은 반복문을 사용하지 않는다.

함수형에서는 반복문 대신 map, filter 같은 함수를 매개변수로 받는 메서드를 이용한다.

아래는 filter를 사용한 자바코드 예제이다.

 

Stream

public class DeploytestApplication {

	public static void main(String[] args) {
    
		printAllNumbersInListStructure(List.of(12,24,31,5,8,9));
        
	}
    
	private static boolean isEven(int number){
		return number % 2 == 0;
	}


	private static void printAllNumbersInListStructure(List<Integer> numbers){

        numbers.stream()
                .filter(DeploytestApplication::isEven)
                .forEach(System.out::println);
	}
}

 

filter는 stream의 요소 중 통과할 요소와 제거할 요소를 걸러내는 작업을 해주는 함수형 명령어이다.

먼저 stream() 메서드를 통해 Integer 스트림을 준비하고 각 숫자에 대해 filter 체크를 진행한다.

filter()에서 true를 반환한 요소에 대해서만 forEach() 문을 실행한다.

그러나 이 경우 isEven이라는 별도의 메서드 생성이 필요하였다.

즉 세부 사항을 추가해서 메서드를 정의하고 호출이 필요하였다.

메서드 참조를 이용하기 위해서는 메서드를 정의해야 하는 번거로움이 생긴다.

이를 개선하는 것이 람다식이다. 람다식으로 개선한 자바 코드는 다음과 같다.

 

 

람다식

private static void printAllNumbersInListStructure(List<Integer> numbers){

    numbers.stream()
            .filter(number -> number % 2 == 0)
            .forEach(System.out::println);
}

 

람다식을 사용한다면 세부 사항을 추가하는 메서드 정의 및 호출이 필요 없어진다.

람다식메서드를 간단하게 표현하는 방법이다.

따라서 위의 isEven()이라는 메서드를 사용하지 않고 

동일한 로직을 더 간단한 구문으로 사용할 수 있다.

 

정리하자면 리스트 객체를 스트림으로 변환스트림의 각 요소에 대해 실행할 작업을 지정한다.

여기서 스트림은 요소의 순서이다.

각 요소에 대해 메서드를 호출해야 하는데 이때 메서드 참조를 사용해 이를 정의한다.

여기서 각 요소에 실행할 작업을 정의하는 또 다른 접근법이 람다식이다.

람다식을 이용한다면 메서드 정의 및 호출 필요 없이 메서드 로직을 간단하게 표현할 수 있다.

이번에는 filter()가 아닌 map()을 이용한 자바 코드 예제이다.

map매핑을 사용하여 요소 각각을 매핑하여 요소의 값을 변형시킬 수 있다.

아래 예제는 정수 요소의 값을 각각 매핑하여 제곱수로 변형시키는 예제이다.

public class DeploytestApplication {

	public static void main(String[] args) {

		printAllNumbersInListStructure(List.of(12,24,31,5,8,9));

	}
    
	private static boolean isEven(int number){
		return number % 2 == 0;
	}

	private static void printAllNumbersInListStructure(List<Integer> numbers){

		numbers.stream()
				.filter(number -> number % 2 == 0)
				.map(number -> number * number)
				.forEach(System.out::println);
	}
}

 

 

Predicate

Predicate interfaceT에 대한 조건에 대해서 true / false를 반환하는 Functional Interface이다.
Predicate<T>로 사용되고 여기서 T는 파라미터이자 조건이다.
T가 true 또는 false를 return 하도록 작성하면 된다.

Predicate의 대표적인 사용 예제가 바로 Stream의 filter에서 사용한다.
Predicate 객체를 filter 매개변수로 넣어서 조건에 부합하는 요소만을 걸러내 준다.

private static void printAllNumbersInListStructure(List<Integer> numbers){

	// predicate 객체에는 조건을 명시
    Predicate<? super Integer> predicate = number -> number % 2 == 0;

    numbers.stream()
            .filter(predicate)
            .map(number -> number * number)
            .forEach(System.out::println);
}

 

 

Optional

개발을 할 때 가장 많이 발생하는 예외 중 하나가 바로 NullPointerException(NPE)이다.

NPE를 피하려면 null 여부를 검사해야 하는데,

null 검사를 해야 하는 변수가 많은 경우 코드가 복잡해지고 번거롭다.

그래서 null 대신 초기값을 사용하길 권장하기도 한다.

 

하지만 Java에서 Null 사용의 문제를 해결하는 방법이 Optional이다.

자바 예제코드를 보면서 설명하면 다음과 같다.

private static void printAllNumbersInListStructure(List<Integer> numbers){

    Predicate<? super Integer> predicate = number -> number % 2 == 0;

    Optional<Integer> numberList = numbers.stream()
                                    .filter(predicate)
                                    .findFirst();
                                    
    System.out.println(numberList.isEmpty());
    System.out.println(numberList.isPresent());
    System.out.println(numberList.get())
}

 

위의 코드에서 Optional을 사용하는 이유는 짝수가 존재하지 않을 수도 있기 때문이다.

만약 짝수가 존재하지 않아 numberList 값이 Null이 된다면,

System.out.println()에서 값이 없기 때문에 예외가 발생한다.

OptionalNull이 아닌 값을 포함할 수도, 포함하지 않을 수도 있는 컨테이너 객체이다.

값이 있으면 특정 값이 반환되고, 값이 없으면 객체는 비어 있는 것으로 간주된다.

 

get()은 만약 Optional이 값을 포함하지 않을 경우 Optional.empty가 반환된다.

따라서 isEmpty() 혹은 isPresent()로 값 존재 여부를 미리 확인해야 한다.

Optional 객체를 생성하는 코드는 다음과 같다.

private static void optionalTestMethod(){

    // 값이 있는 Optional 객체 생성
    Optional<String> number = Optional.of("Number");

    // 값이 없는 빈 객체 생성
    Optional<String> empty = Optional.empty();
}

 

Stream API의 findFirst()나 findAny()로 값을 찾는 경우에,

Null을 반환하는 것보다 값의 유무를 나타내는 객체를 반환하는 것이 합리적일 것이다.

이에 대한 고민으로 나온 것이 Optional 객체이다. 따라서 Stream API를 이용하여

값의 유무가 확실하지 않을 경우에는 컨테이너 객체인 Optional을 사용하는 것이 좋다.

 

 

참고자료

https://wikidocs.net/157858

 

반응형

'IT > Java' 카테고리의 다른 글

[Java] Java Builder Pattern  (2) 2024.09.12
[Java] Java Factory Pattern  (1) 2024.09.11
[Java] Java Decorator Pattern  (0) 2024.09.09
[Java] Java Observer Pattern  (7) 2024.09.08
[Java] Java Strategy Pattern  (0) 2024.08.29

loading