함수형 인터페이스와 람다식, 스트림

우리가 이전에는 선언형 프로그래밍과 명령형 프로그래밍에 대해서 알아보았는데

각각 해당 프로그래밍 기법에는 뭐가있고, 장단점, 특징 같은것을 대충 훑어보았다

이번에는 선언형 프로그래밍에서 자주쓰이는 람다식과 스트림 그리고 함수형인터페이스에 대해서 알아볼것인데 먼저 장단점을 서술하고 해당 특징들에 대해서 이해하면서 배워보도록 하자

 

해당 게시물에서는 람다식이나, 함수형 인터페이스에 대해서 깊게 다루지는않고 어떤의미인지에 대해서만 알아보도록 할것이다

 

 

 

 

 

 

람다식(Lambda Expression), 스트림(Stream) 과 함수형 프로그래밍


먼저 결론적으로 말해서 람다식이 등장하게 된 이유는 불필요한 코드를 줄이면서, 가독성을 높이기위해서 나왔다고 생각하면 될것이다

람다식으로 표현하게되면 메소드의 이름이 따로 없기때문에 익명함수 라고 생각하면되고, 보통 스트림과 람다는 묶여서 사용하는 경우가 흔하다

 

우선 장점에 대해서 알아보자

 

코드를 간결하게 만들수있다.

 

List<Integer> list = new ArrayList<>();

        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);

        List<Integer> numberList = new ArrayList<>();

        for (int item : list ) {
            if(item % 2 == 0) {
                numberList.add(item);
            }
        }

        for (int Numberitem : numberList) {
            System.out.println("Numberitem = " + Numberitem);
        }

 

해당 코드는 list에 1~5까지의 숫자를 넣고 짝수번째인 숫자만 골라서 numberList에 담는 과정이다

이것을 람다와 스트림을 이용해서 간단하게 바꿔보도록 하자

참고로 스트림이나 람다에 관한 자세한 문법보다는 ' 왜 ' 쓰는지에 초점을 맞추고있기때문에 이해가되지않아도 ' 아 이렇게 되는구나 ' 라고 생각하면 될것이다

 

 

아래는 람다와 스트림을 이용해서 해당 문법을 간결하게 바꾼코드이다

 

        List<Integer> list = new ArrayList<>();

        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);

        List<Integer> numberListlist = list.stream().filter( item -> item % 2 == 0 )
                .peek(System.out::println)
                .collect(Collectors.toList());

 

보면 위에 코드에 비해서 훨씬간결하고 깔끔해진것을 확인할수있다

또한 filter, peek, collect 라는 메서드 이름을 통해서 어떤작업을 거치는지에 대해서 명시되어있기때문에 가독성이 훨씬 좋아지게된다

 

해당 스트림에서는 짝수번호값들을 필터처리하고 연산된 값을 중간처리로 peek을 통해 출력 그리고 collect를 통해 리스트에 담는작업을 하나의 함수로 끝나게된다

 

루프하고.. 루프하고.. 같은 귀찮은 작업들이 사라지게 된다는것이다

또한 이렇게 하나의 함수안에서 모든작업을 거치게때문에 병렬프로그래밍에 용이하게되는데 병렬프로그래밍에 경우 하나의 스레드를 할당받고 작업할때 작업하는 내용이 길어지면 정말 보기 불편하다

 

하지만 코드가 하나의 함수로 바뀌어 깔끔하게 볼수있다는것은 병렬프로그래밍에 있어서 간편함을 주게된다

또한 스트림은 불변(Final)한 데이터들을 파이프라인의 형태처럼 여러 처리과정을 거치는것이다

하나의 필터작업이 끝나면 불변데이터들이 나오며 이를통해 병렬프로그래밍에 즉 multi thread 환경에서 유리하다고 생각한다

 

정리해보자면 람다식의 장점은 다음과같다

 

- 상대적으로 코드를 깔끔하게 만들수있다.

- 개발자가 어떠한 과정을 거치는지 명확하게 한눈에 볼수있어서 가독성이 좋아지게된다

- 함수를 만드는 과정이 필요없어지며, 생산성이 높아진다

- 병렬프로그래밍에 있어서 도움을 줄수있다

 

하지만 람다식이라고 해서 장점만 있는것은 아닌데 사실 람다식 만에 문제가아니라 익명함수의 단점을 어느정도 그대로 가져오게된다

 

1. 람다식으로 만든 함수는 그자리에서 바로 쓰이고, 다른곳에서는 재사용 할수없다

이는 곧 다른문제를 발생하게 되는데 람다를 너무 자주사용하게되면 중복코드가 많이 생성될수있다는 것이다 즉 오히려 코드가 지저분 해질수있다

 

재사용되는 함수는 공통로직을 따로 관리할수 있지만 람다는 그것이 불가능하다

 

2. 디버깅이 어렵다

물론 할순있지만 아무래도 하나의 함수안에 여러 처리과정을 거치기떄문에 아무래도 상대적으로 디버깅에 어려움을 ㅕㄱ겪을수도 있다

 

 

다시 람다식의 단점에 대해서 정리해보자면

- 익명함수 이기떄문에 재사용이 불가능하다

- 상대적으로 디버깅하는데 어려움을 겪을수도있다

- 너무 람다를 많이사용하게되면 중복코드로 인해 오히려 코드가 지저분해질수있다

 

 

그러면 람다식은 어떨때 사용하는것이좋을까?

우선 일회용, 즉 재사용에 가능성이 적을때 사용하면된다 코드를 만들다보면 예시에서 보다싶이 저런 상황이 은근히 자주일어난다 쓸모없는 함수를 만들어야할때 그럴때 람다와 스트림을 조합해서 사용하면 좋은 효과를 볼수있게된다

 

 

 

 

 

 

람다와 스트림 그리고 함수형 인터페이스


람다와 스트림은 자주 묶여서 사용된다 그리고 이와 파생되어 JAVA에서는 함수형 인터페이스를 제공한다

그냥 인터페이스와는 다르게 추상메서드를 ' 하나 ' 만 가지고있는 인터페이스이며, default 같은 메서드는 여러개 가지고있을수있다

 

람다와 스트림을 함수형 인터페이스와 엮는 이유는 아마 이것들을 조합하여 사용했을때 좋은 시너지 효과가 발생하기떄문인거같다

스트림 '안' 에서 람다식을 이용해 데이터를 가공하는것이 스트림을 좀더 빛내주는거처럼 말이다

 

지금부터 함수형 인터페이스를 사용하고, 사용하지 않고의 차이를 중점으로 하여 보도록하자

두개의 파라미터 값을 받아 덧셈을 수행하는 코드를 먼저 작성해보도록 하자

 

 

public interface sum {

    int sum(int x, int y);

}

 

sum 이라는 인터페이스에는 sum 이라는 추상메서드가 존재한다

이제 이 sum을 구현하여 해당 메서드의 구현체를 만들어보도록 한다

 

 

public class sumService implements sum {

    @Override
    int sum(int x, int y) {
        return x+y;
    }

}

 

sum의 구현체인 sumService는 x+y를 반환하는 메서드를 가지고있다

이제 이를 main 메서드안에서 실행해보도록 하자

 

 

public class main  {

    public static void main(String[] args) {
        sum sumService = new sumService();
        
        int result = sumService.sum(1,2);
        System.out.println("result = " + result);
    }

}

 

 

우리가 주목해야할것은 다음에 사용자에게 새로운 요구사항이 들어옴에 따라 두개의 값을 빼는 작업도 생길수있다

그렇게되면 다시 인터페이스에서 추상메서드를 정의하고 해당 추상메서드를 선언하며, 다시 메인메서드에서 출력하는 3개의 작업과정을 또다시 거치게된다

 

한두개면 괜찮지만 만약 여러개의 요구사항이 계속해서 늘어나게 된다면 어떤일이 벌어지게될까?

추상메서드도 계속해서 늘어날테고 계속해서 같은 과정을 거치게된다

 

사실 서비스를 진행하다보면 직접 구현하는 로직에서 재사용하는 함수들의 비중은 그렇게 많이 크지않다

더군나나 한번사용하고 버려질꺼같은 함수를 게속해서 일일이 만들어주는것도 귀찮고 유지보수에 안좋을수있다

 

이것을 해결해주기 위해 함수형 인터페이스와 람다를 이용하여 바로 main안에서 3가지의 일련과정들을 스킵할수있다

 

 

@FunctionalInterface
public interface sum {

    int operations(int x, int y);

}

 

우선함수형 인터페이스를 만들어주도록 한다 해당 추상메서드안에서는 사칙연산 즉 덧셈, 뺄셈, 나눗셈등 여러 작업을 수행할수있다

 

우리가 보통 매개변수에서는 변수만 줄수있었지만 함수형 인터페이스는 함수 그 자체를 보내고 실행할수있다

 

 

public static void main(String args[]) {
	sum sumService = ((x, y) -> x+y);

    System.out.println(sumService.operations(1,2));
}

 

해당 람다를 통해서 직접 함수를 보낼수있다 즉 x , y의 인자를 받아 해당 값을 덧셈하는 함수자체를 보내 정의한다음

해당메서드를 불러와서 바로 실행할수 있는것이다

 

만약 다른 요구사항이 와도 (해당 요구사항에 함수가 많이 안쓰일경우) 해당 함수형 인터페이스를 통해서 함수를 정의하고 바로 사용하는것이 가능하다 뺄셈이라는 요구사항이 오게되도 바로 쉽게 대처할수있다는 것이다

 

불필요한 뺄셈메서드, 나눗셈 메서드등을 인터페이스에 늘리지 않아도 되는것이다

 

함수형 인터페이스의 장단점은 사실 위에 람다와 스트림에서 봤다싶이 코드가 간결해지지만, 재사용이 불가능 하다는 특징을 가지고있다

 

하지만 일회용성 메서드들을 그때마다 선언해줄필요없어 활용만 잘한다면 아주 편리해진다는 강점이있다

 

위에서 나열한 람다, 스트림, 함수형 인터페이스를 적재적소 잘 사용한다면 훨씬 좋은 코드를 생산해낼수있을것이다