함수형 프로그래밍

のの·2020년 12월 26일
  • 애플리케이션의 요구 조건 변경에 대응하는 절차를 살펴보면서
    왜 함수형 프로그래밍이 필요한가?를 이해하자

  • 인터페이스로 명세서와 실체 구현체를 분리하고 유연한 구조를 확보한 후, 람다 표현식을 이용해서 코드의 중복을 제거하는 방법에 대해서 알아본다.

  • 함수를 구현한 코드를 전달하는 메서드 참조 기능에 대해서 알아본다.

  • 조건이 추가되거나 변경될 때마다 메서드가 계속 추가되는 구조
  • 추가된 메서드의 내용이 매우 유사하다. 여행 상품의 List 객체를 for 반복문으로 돌면서 기준이 되는 데이터를 필터링한다는 점이 동일하다.
  • 만일 국가와 도시를 AND 조건으로 조회하려면 새로운 메서드를 추가해야한다.
  • 결국 비즈니스의 요건 변경에 따라 클래스의 API가 너무 자주 바뀌게 되어 이 클래스를 사용하는 다른 클래스에 큰 영향을 준다.

인터페이스로 대응

  • 원하는 조건을 더 정확하게 검색할 수 있는 옵션이 필요
  • 검색 메서드를 인터페이스로 노출
    • 상품을 관리하는 클래스와 상품을 조회하는 로직을 분리
      다양하게 요청되는 조회 조건 및 처리 메서드는 인터페이스로 분리해서
      외부에서 정의하도록 하고, 여행사 소프트웨어는 상품을 관리하고 요청에 대응
  • 자바 8에서는 인터페이스에 하나의 메서드만 정의한 것을 함수형 인터페이스라 부름


public interface TravelInfoFilter {
    public boolean isMacthed(TravelInfo travelInfo);
}
public List<TravelInfo> searchTravelInfo(TravelInfoFilter searchCondition){

	List<TravelInfo> returnValue = new ArrayList<>();

        for(TravelInfo travelInfo : travelInfoList){
        	// 인터페이스의 isMatched 메서드를 호출.
             	// 실제 구현에 대해서는 캡슐화되어 있음
            if(searchCondition.isMacthed(travelInfo)){
                returnValue.add(travelInfo);
            }
        }
        return returnValue;
    }

searchTravelInfo 메서드의 파라미터로 TravelInfoFilter를 전달
TravelInfoFilter 인터페이스의 isMatched 메서드를 호출하여 주어진
TravelInfo 데이터가 조회 조건에 맞는지 확인하고 맞으면 리턴 객체에 포함시키고 틀리면 계속 진행된다.

isMatched에 내부적으로 어떤 조건을 구현해 놓았는지 알지 못하지만 그 결괏값에 따라 true/false 값을 확인할 수 있으므로 외부에서 들어오는 다양한 조건에 대해 처리가 가능하도록 메서드가 개선되었다.

public static void main(String[] args) {

Travel travel = new Travel();

List<TravelInfo> searchList =
	travel.searchTravelInfo(new TravelInfoFilter() {
                    @Override
                    public boolean isMacthed(TravelInfo travelInfo) {
                        if (travelInfo.getCountry().equals("vietnam")) {
                            return true;
                        } else {
                            return false;
                        }
                    }
                });
  • 메서드 추가방법, 파라미터 추가방법, 인터페이스 분리 방법 중에 가장 유연하게 변화의 요구에 대응할 수 있는 방식은 인터페이스로 조회 조건을 분리하는 방식이다.
  • 익명 클래스를 이용하여 메서드를 구현하는 방식이기 때문에 코드의 중복이 매우 심하다. (ex 국가 정보로 조회, 도시 정보로 조회 따로 따로 구현해야됨)
  • 익명 클래스 방식은 필요로 하는 일부 코드를 위해 반복적인 패턴을 기계적으로 작업해야한다.
  • 익명 클래스도 하나의 클래스이기 때문에 실제로 컴파일하면 클래스 파일이 별도로 생성된다. 익명 클래스가 반복적으로 생성된다면 추후에 배포하거나 업데이트할 때도 불편함이 계속해서 따라다닌다.

람다 표현식으로 코드 함축

List<TravelInfo> searchList =
                travel.searchTravelInfo(travelInfo -> {
                    if (travelInfo.getCountry().equals("vietnam")) {
                        return true;
                    } else {
                        return false;
                    }
                });
List<TravelInfo> searchListByCountry =
                travel.searchTravelInfo(TravelInfo travelInfo) -> travelInfo.getCountry().equals("vietnam"));
List<TravelInfo> searchListByCity =
                travel.searchTravelInfo(TravelInfo travelInfo) -> travelInfo.getCity().equals("hanoi"));

List<TravelInfo> searchListByCountry =
	travel.searchTravelInfo(travelInfo -> trvelInfo.getCountry().equals("vietnam"));

for(TravelInfo travelInfo: searchListByCountry){
	System.out.println(travelInfo);
}

List<TravelInfo> searchListByCity =
                travel.searchTravelInfo(travelInfo -> travelInfo.getCity().equals("hanoi"));

for(TravelInfo travelInfo: searchListByCity){
	System.out.println(travelInfo);
}

메서드 참조

자바 7까지 변수 혹은 메서드의 파라미터로 전달할 수 있는 것은 객체나 기본 데이터 뿐이었다. 자바에서만 사용할 수 있는 데이터 종류만 변수로 참조하거나 인수로 전달할 수 있고 그 외의 것은 참조할 수 없었다.
하지만 자바 8에서는 추가적으로 메서드 자체를 참조할 수 있게 되었으며 이를 특별히 메서드 참조(method reference)라 한다. 자바 8에서 추가된 메서드 참조를 이용하면 앞에서 실행한 람다 표현식을 훨씬 깔끔하게 활용할 수 있다.
람다 표현식을 사용하면 익명 클래스의 소스 코드 중복성은 해결할 수 있지만, 소스 코드의 재사용성이라는 측면에서는 활용도가 떨어진다. 이 경우 람다 표현식을 하나의 함수로 선언하고 이 함수를 다른 곳에서 활용하면 재사용성을 높일 수 있다.

메서드 참조 유형

  • 정적메서드 참조
  • 한정적(bound) 인스턴스 메서드 참조 : 수신 객체를 특정
  • 비한정적(unbound) 인스턴스 메서드 참조 : 수신 객체를 특정하지 않음
  • 클래스 생성자
  • 배열 생성자

정적 (둘은 같은 역할을 한다.)

Integer::parseInt; // 메서드 참조
str -> Integer.parseIng(str); // 람다

한정적 (인스턴스)

Instant.now()::isAfter;
Instant then = Instant.now();
t -> then.isAfter(t);

비한정적 (인스턴스)

String::toLowerCase;
str -> str.toLowerCase();

클래스 생성자

TreeMap<K,V>::new;
() -> new TreeMap<K,V>();

배열 생성자

int[]::new;
len -> new int[len];

어떤 상황에서는, 람다의 매개변수의 이름 자체가 프로그래머에게 좋은 가이드가 되기도 한다. 이런 람다는 길이는 더 길지만 메서드 참조보다 읽기 쉽고 유지보수도 쉬운코드가 될 수 있다.
따라서, 메서드 참조 쪽이 짧고 명확하다면 메서드 참조를 쓰고, 그렇지 않을 때만 람다를 사용해야 한다.


public static boolean isTailand(TravelInfo travelInfo){
        if(travelInfo.getCountry().equals("tailand")){
            return true;
        }
        else{
            return false;
        }
    }
    
List<TravelInfo> searchInfoTailand = searchInfoTailand.searchTravelInfo(Travel::isTailand);

for(TravelInfo travelInfo : searchInfoTailand){
	System.out.println(travelInfo);
}

실제 구현해야할 부분만을 코드로 작성하고 이를 람다 표현식으로 선언할 지 아니면 메서드를 추가한 후 메서드 참조를 이용해서 정의한 기능을 전달할지 판단하면 된다.
함수의 메서드명은 동일할 필요 없이 개발자 필요에 따라 명명 규칙에 맞춰 선언하면 되지만 입력되는 인수와 리턴 타입은 인터페이스의 public 메서드와 동일해야 한다.(리턴타입: boolean, 인수 : TravelInfo travelInfo)


  • 특정 조건의 기능을 추가/변경하기 위해 메서드를 추가하거나 인수를 추가하는 방법 외에도 인터페이스를 이용해서 구현을 분리
  • 인터페이스로 분리하면 익명 클래스 사용 빈도가 높아지게 되고 결과적으로 중복 코드와 컴파일된 클래스가 늘어나는 단점이 있다.
  • 익명 클래스의 단점을 해결하기 위해 람다 표현식을 사용한다.
  • 람다 표현식을 재활용하고 단점을 보완하기 위해 메서드 참조 기능을 사용한다.
  • 오직 하나의 public 메서드만 정의해 놓은 인터페이스를 특별히 함수형 인터페이스라고 한다.
profile
wannabe developer

0개의 댓글