Programming/Java

자바 기초부터 모던 자바까지 - 함수형 프로그래밍 편

KOOCCI 2022. 7. 3. 15:55

목표 : 자바에서의 함수형 프로그래밍에 대해 알아본다.


왜 함수형 프로그래밍이 도입되기 시작했을까?

객체 지향 프로그래밍의 정수였던 java에서 왜 새로운 패러다임을 도전하게 된 것일까?

결국에는 애플리케이션의 요구 조건 변경에 대응하는 절차였다.

먼저 그 흐름을 따라가 보도록 하자

 

여행 정보를 조회하는 클래스가 있다.

국가명을 통해 조회하며, 그 결과를 리스트로 제공한다.

SearchTravel 이라는 이 클래스에는 searchTravelInfo(String country) 라는 메소드로 이를 제공하고 있었다.

 

새로운 요구사항이 등장했다.

국가명도 제공해야하지만, 도시명으로도 제공해야했다.

그에 따라 2가지 변경 방안이 등장하게 된다.

 

  1. 도시 정보를 파라미터로 받아서 처리할 수 있는 메서드를 추가한다.
    1. searchTravelInfo를, searchTravelInfoByCountry로 변경한다.
    2. searchTravelInfoByCity를 추가한다.
  2. 기존 메서드에 파라미터를 추가한다.
    1. searchTravelInfo(String country)를 searchTravelInfo(String searchType, String searchValue) 로 변경해, 로직으로 구분한다.
    2. 함수 내부에서 searchType에 따라, 국가명 혹은 도시명을 검색하도록 한다.

문제점은 동일하게 등장한다.

새로운 요구사항이 등장할 때 마다, 메소드를 늘리거나, 논리적 구분 소스를 추가해야 한다.

그러나 AND 요건 OR 요건 등에 따라 결국 또다른 문제가 등장하게 된다.

 

인터페이스로 대응

AND 조건 등이 등장함에 따라 결국에는 이리저리 유지보수하면서 개발할 수는 있다.

그러나 유연한 대처를 위해 전체 소스를 변경하기로 하였다.

 

검색 메서드를 인터페이스로 노출하고, 실제 실행 결과는 별도로 분리한다.

여행 상품을 관리하는 클래스와 상품을 조회하는 로직을 분리하겠다는 것이다.

즉, 너무나 다양하게 요청되는 조회 조건 및 처리 메서드는 인터페이스로 분리해 외부에서 정의하고, 여행사 소프트웨어는 상품을 관리하고 요청에 대응하는 것이다.

 

public interface TravelInfoFilter {
    public boolean isMatched(TravelInfoVO travelInfo);
}

딱 한개의 메소드만 가진 인터페이스를 구현하였다.

아래에 다시한번 설명하겠지만, 자바8부터 한개의 메소드를 정의한 것을 함수형 인터페이스라고 한다.

그럼 searchTravelInfo 메소드를 변경해보도록 하자.

public class SearchingTravel {
    private List<TravelInfoVO> travelInfoList = new ArrayList<>();
    public List<TravelInfoVO> searchTravelInfo(TravelInfoFilter searchCondition) {
        List<TravelInfoVO> returnValue = new ArrayList<>();
        
        for(TravelInfoVO travelInfo : travelInfoList) {
            if(searchCondition.isMatched(travelInfo)) {
                returnValue.add(travelInfo);
            }
        }
        return returnValue;
    }
}

매개변수로 인터페이스를 받아서, 인터페이스의 메소드를 통해 isMatched를 비교하는 로직으로 변경하였다.

즉, 해당 인터페이스로 구현된 클래스는 외부에게 맞기는 것으로 변경한 것이다.

 

public class Main {
    public static void main(String[] args) {
        SearchingTravel st = new SearchingTravel();
        
        List<TravelInfoVO> searchTravel = st.searchTravelInfo(new TravelInfoFilter() {
            @Override
            public boolean isMatched(TravelInfoVO travelInfo) {
                if(travelInfo.getCountry().equals("vietnam")) {
                    return true;
                }
                return false;
            }
        })
    }
}

해당 클래스를 호출하는 로직을 구현하였다. (익명 클래스를 활용하였다)

이제 람다 표현식으로 조금씩 바꿔보도록 하자.

 

람다 표현식으로 코드 함축

익명 클래스를 활용하면 코드의 중복이 매우 심해질 것이다.

위에서는 베트남 하나였지만, 한국, 중국, 일본 등 추가할 때마다 변화는 적은데 코드는 매우 많이 늘어난다.

따라서 람다 표현식으로 함축하면 다음과 같아진다.

List<TravelInfoVO> searchTravel = st.searchTravelInfo((TravelInfoVo travelInfo) -> travelInfo.getCountry().equals("vietnam"));

꽤 많은 것이 함축되었다. 

한가지 더 알아보도록 하자.

 

메서드 참조

자바 7까지 변수 혹은 메서드의 파라미터로 전달할 수 있는 것은 객체나 기본 데이터뿐이였다.

자바 8부터는 메서드 자체를 넘길 수 있게 되었고, 이를 메서드 참조(Method Reference)라고 한다.

 

익명 함수로 소스 코드 중복은 줄일 수 있을지 몰라도, 재사용성을 줄어들었는데, 이렇게 함수 자체를 넘길 수 있다면 재사용성 역시 개선될 수 있다.

public static boolean isVietnam(TravelInfoVO travelInfo) {
    if(travelInfo.getCountry().equals("vietnam")) {
        return true;
    }
    return false;
}

위 함수를 SearchTravel 클래스에 추가한다.

그리고 메인 함수는 이렇게 변경될 수 있다.

List<TravelInfoVO> searchTravel = st.searchTravelInfo(SearchingTravel::isVietnam);

파라미터조차 적지 않는 방식을 확인할 수 있다.

다음 포스팅 부터는 람다 표현식으로 변경하는 방법을 이제 알아볼 예정인데, 그 전에 한번 용어를 정리하고 가도록 하자.

용어 정리

함수형 프로그래밍

위에서 간단히 알아본 람다를 공부하려면 가장 먼저 함수형 프로그래밍을 알아야 한다.

또한, 람다라는 용어는 자바에서만 사용하는 것도 아니며 다른 언어(자바스크립트 등)에서 활용하는 것을 보았을 때, 사용법은 달라도 메소드 명까지 동일하게 활용되는 경우가 많다.

 

함수형 프로그래밍은 순수 함수(pure function)를 구현하고 호출함으로써 외부 자료에 부수적인 영향(Side Effect)를 주지 않도록 구현하는 방식이다.

 

순수 함수(pure function) 매개변수만을 사용하여 만드는 함수다.

즉, 함수 내부에서 함수 외부에 있는 변수를 사용하지 않아 함수가 수행되더라도 외부에는 영향을 주지 않는다.

 

이 말은 함수의 기능이 데이터에 독립적임을 보장하게 된다.

즉, 동일한 데이터에 대해 동일한 결과를 보장하고, 다양한 데이터에 대해 같은 기능을 수행한다는 것이다.

따라서, 병렬 처리에 안정성과 확장성을 제공하게 된다.

 

람다식

람다식은 자바에서 함수형 프로그램을 구현한 방식이다.

함수형 인터페이스를 선언하며 클래스를 호출하지 않고 메소드만 호출하여 기능을 수행한다.

몇 가지 특징들을 예시와 함께 알아보도록 하자.

  1. 매개 변수가 하나인 경우 생략이 가능하다.
  2. 중괄호 안의 구현부가 1문장이면 중괄호를 생략할 수 있다.
  3. 중괄호 안 구현부가 1문장이더라도, return 문이라면 생략할 수 없다.
  4. 만약 반환문 1개라면, return과 중괄호 모두 생략할 수 있다.
str -> {System.out.println(str);}; // 1번
str -> System.out.println(str); // 2번
str -> return str.length(); // 3번 에러
str -> str.length(); // 4번
(x,y) -> x+y; // 4번

함수형 인터페이스

람다식을 선언하기 위한 인터페이스다.

익명 함수와 매개 변수만으로 구현되므로, 단 하나의 메서드만을 선언해야 한다.

(두개 이상의 메서드가 생성되면, 어떤 메서드를 호출할지 애매해진다.)

@FunctionalInterface 어노테이션을 사용한다.

 

@FunctionalInterface
public interface MyNumber {
    int getMax(int num1, int num2);
}

public class Main {
    public static void main(String[] args) {
        MyNumber max = (x,y) -> x>=y? x:y;
        System.out.println(max.getMax(10,20));
    }
}

람다식을 구현하면 익명 내부 클래스가 만들어지고, 이를 통해 익명 객체가 생성된다.

앞선 포스팅에 익명 내부 클래스를 알아보았다.