Notice
Recent Posts
Recent Comments
Link
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

josolha

람다, 스트림API 본문

JAVA

람다, 스트림API

josolha 2023. 12. 27. 21:52
서론
List<String> imageUrls = board.getImages().stream()
            .map(Image::getImageUrl)
            .collect(Collectors.toList());

해당 코드는 진행 중인 프로젝트에서 서비스 내 코드이다

 

프로젝트를 진행하면서 종종 이렇게 stream을 활용해 쉽게 값을 리스트로 가져온다.

 

하지만 어떻게,  왜 가능하게 하는지 좀 더 구체적으로 이해하기 위해 정리한다.

 

알아야할 순서 이러하다.

  1. 익명객체
  2. 함수형 인터페이스 & 람다
  3. 스트림API

1.익명객체

 

익명 객체(Anonymous Object)는 이름이 없는 객체로, 주로 일회성으로 사용되는
객체에 대해 간편하게 정의하고 생성할 때 사용된다.

익명 객체는 인터페이스 또는 추상 클래스의 구현 또는 서브클래스를 정의하고 동시에 인스턴스한다.

 

예를들어

뺄샘을 하는 계산기를 만들어보자.

public interface Operate {
    int operate(int a, int b);
}

1.Operate 메소드를 가진 인터페이스 객체.

 

public class Minus implements Operate{
    @Override
    public int operate(int a, int b) {
        return a - b;
    }
}

2.Operate 인터페이스를 상속받아 구현.

 

public class Calculator {
    private int a;
    private int b;

    public Calculator(int a, int b) {
        this.a = a;
        this.b = b;
    }
    public int result(Operate op){
        return op.operate(this.a, this.b);
    }
}

3.계산기 기능을 하는 클래스를 만든다(생성자를 통해 값을 받고 결과호출해주는 메소드를 가진다). 

 

그럼 우리는 계산기의 뺄샘을 사용하기 위해서는 일반적으로 코드가 아래처럼 메인에 사용하게 된다.

public class Main {
    public static void main(String[] args) {
    	//1.계산기 클래스 생성 
        Calculator calculator = new Calculator(20,10);
        
        //2.뺄샘 클래스 생성
        Operate operate_minus = new Minus();
        
        //3.결과 값 담기
        int result = calculator.result(operate_minus);
    }
}

 

 

하지만 이 코드에서 Operate operate_minus = new Minus(); 가 좀 이상하다.

 

만약 뺄샘 뿐만 아니라, 덧셈, 곱하기 등 기능을 사용한다면 별도의 클래스(Minus, Add,mutiple 등)를 만들어야 하고

또한 operate_minus 객체는 단순하게 인터페이스의 operate 메소드를 구현하는 데 사용된다.

이는 각 연산에 대한 별도의 구현 파일을 필요로 하며, 프로젝트의 복잡성을 증가시킬 수 있고 단일 사용 목적에 비해 과도할 수 있다.

 

Operate operate_minus = new Minus();와 같은 방식의 한계를 극복하기 위해 익명 객체가 등장한다.

public class Main {
    public static void main(String[] args) {
    	//1.계산기 클래스 생성 
        Calculator calculator = new Calculator(20,10);
        
        //2.객체 생성과 동시에 뺄셈 기능 구현
        int result = calculator.result(new Operate() {
            @Override
            public int operate(int a, int b) {
                return a - b;
            }
        });
        
    }
}

위에 코드는 익명 객체를 사용한 코드이다.

 

Operate 인터페이스를 익명 객체로 구현을 하면서 new Operate() {...} 구문으로 Operate의 메소드인 operate를 직접 구현한다.

 

Minus와 같은 별도의 클래스를 만들 필요가 없어지고 Operate operate_minus = new Minus(); 와 같은 코드는 더 이상 필요하지 않게 된다. 또한 덧셈이나 곱셈도 비슷한 방식으로 익명 객체를 사용하여 구현할 수 있다.

 

이 방식은 각 연산에 대해 별도의 클래스를 만들 필요 없이, 필요한 연산을 그 자리에서 구현할 수 있게 해준다.

따라서 익명 객체를 사용하면, 간단한 인터페이스 구현을 위해 별도의 클래스를 만드는 번거로움과 코드 중복을 피할 수 있다.

 

하지만 이 방식에는 몇 가지 한계가 있다. 연산에 대해 상대적으로 많은 코드를 필요로 한다는 것이다.
이는 특히 여러 번 반복되는 경우, 코드의 가독성을 저하시키고 유지보수를 더 복잡하게 만들 수 있다.

 

그래서 람다 표현식을 통해 간결하게 만들수있다.


2.함수형 인터페이스 & 람다

 

람다란

람다 표현식(Lambda Expression)은 Java 8에서 도입된 기능으로,
간결한 방식으로 익명 함수(anonymous functions)를 정의하는 데 사용된다.
람다 표현식은 함수형 인터페이스의 구현을 간단하고 명확하게 제공하며,
이를 통해 코드의 간결성과 가독성을 크게 향상시킬 수 있다.

 

람다 표현식은 함수형 인터페이스의 구현을 간다하게 한다는데..

함수형 인터페이스가 뭐지..?

 

함수형 인터페이스란

함수형 인터페이는 추상메서드가 1개만 정의된 인터페이스를 통칭하여 일컫는다.
인터페이스 형태의 목적은 자바 람다 표현식을 이용해 함수형 프로그래밍을 구현하기 위해서이다.

 

위에서 봤던 Operate 인터페이스 코드를 다시 보자.

//@FunctionalInterface -> 두 개 이상의 추상 메소드가 선언되지 않도록 컴파일러가 checking 해주는 기능을 사용 할 수 있다.
public interface Operate {
    int operate(int a, int b);
}

 

그렇다 이 Operate 인터페이스는 추상메서드가 1개만 정의된 함수형 인터페이스이다.

(@FunctionalInterface 애너테이션은 선택적이다. 인터페이스가 단 하나의 추상 메소드를 가지고 있으면, 이 애너테이션 없이도 함수형

인터페이스로 사용가능하다.)

 

따라서 Operate는 함수형 인터페이스라고 할 수 있고,  람다식을 사용할 수 있다.

 

기본적으로 람다식은 이렇게 표현한다.

(매개변수 목록) -> { 표현식 혹은 문장들; }

매개변수 목록은 함수에 전달할 매개변수 목록이고. 매개변수의 타입을 명시할 수도 있고 생략할 수도 있다.

화살표(->)는 매개변수와 람다 본문을 구분한다.
본문은 람다가 수행할 코드이다. 본문이 하나의 표현식만 포함한다면 중괄호 {}와 return 키워드를 생략할 수 있다.

 

그럼 적용해보자

public class Main {
    public static void main(String[] args) {
    	//1.계산기 클래스 생성 
        Calculator calculator = new Calculator(20,10);
        
        //2.람다 사용
        int result = calculator.result((a,b) ->{
            return a - b;
        });
        
    }
}

(a,b) ->{ return a - b}); 이렇게 람다식을 사용 함으로써 더 간단한 코드가 되었다

 

초기 단계에서 Operate와 같은 사용자 정의 인터페이스를 구현하기 위해 익명 객체를 사용했다.

이 방식은 별도의 구현 클래스를 작성하지 않고도 인터페이스의 메서드를 오버라이드할 수 있게 해주었다.

하지만 접근 방식은 필요한 기능을 '그 자리에서' 구현할 수 있게 해주지만, 문법적으로 다소 장황했다.


여기서 한 걸음 더 나아가, 자바 8의 람다 표현식 도입으로 코드를 더욱 간결하게 만들 수 있었다.

람다 표현식은 기본적으로 익명 객체의 '간결한 버전'으로 볼 수 있다.


람다는 함수형 인터페이스의 단일 추상 메소드를 구현하는 간단한 방법을 제공한다.

즉, 람다 표현식을 사용하면 위의 익명 객체 구현을 위에와 같이 한 줄로 줄일 수 있다.

 

어떻게 가능한걸까?

 

이렇게 (a, b) -> { return a - b; } Calculator 클래스의 result 메소드에 전달될 수 있는 이유는,
Calculator 객체의 result 메소드가 Operate 함수형 인터페이스 타입의 매개변수를 받기 때문이다.


따라서 자바 컴파일러는 람다 표현식((a, b) -> { return a - b; })가 Operate 인터페이스의 operate 메소드 기능과 일치하는지 확인한다.

(Operate 인터페이스는 2개의 int를 받아서 int를 return 하는 기능을 가진다)

 

이후 일치한다면 이를 Operate 타입의 객체로 취급한다.
이렇게 함으로써, 람다 표현식은 Operate 인터페이스의 구현체로서 Calculator의 result 메소드에 전달된다.

 

또한 람다 본문이 단일 표현식으로 구성되어 있고, 해당 표현식이 바로 반환 값인 경우 중괄호({})와 return 키워드를 생략할 수 있다.

public class Main {
    public static void main(String[] args) {
    	//1.계산기 클래스 생성 
        Calculator calculator = new Calculator(20,10);
        
        //2.람다 사용
        int result = calculator.result((a, b) -> a - b);
    }
}

따라서 이렇게 더 간략화가 될수 있다.

 

그런데 만약 Operate 같은 사용자 정의 함수형 인터페이스를 계속 만들어야 한다면 어떨까?
프로젝트가 커질수록 비슷한 기능을 하는 인터페이스들이 점점 늘어 날 것이다.
이는 코드의 중복성을 높이고, 유지보수를 어렵게 만든다.
또한, 팀 내에서 다양한 방식의 인터페이스가 생겨나면 프로젝트의 일관성도 떨어질 수 있다.
그래서 이런 경우 자바에서 이미 제공하는 표준 함수형 인터페이스가 있다. 

 

자바의 주요 내장 함수형 인터페이스들

더보기

Runnable  : 매개변수 없이 반환값도 없는 run 메소드를 가진다

Supplier<T> : 매개변수 없이 T 타입의 값을 반환하는 get 메소드를 가진다.

Consumer<T> : T 타입의 매개변수를 받고 반환값이 없는 accept 메소드를 가진다.

Function<T, R>  : T 타입의 매개변수를 받고 R 타입의 값을 반환하는 apply 메소드를 가진다.

Predicate<T> :T 타입의 매개변수를 받고 boolean 값을 반환하는 test 메소드를 가진다.

BiConsumer<T, U> : 두 매개변수를 받고 반환값이 없는 accept 메소드를 가진다.

BiFunction<T, U, R> : 두 개의 매개변수를 받고 하나의 결과를 반환하는 apply 메소드를 가진다.

UnaryOperator<T> :Function<T, T>의 특별한 형태로, 하나의 매개변수를 받아 같은 타입의 결과를 반환한다.

BiPredicate<T, U> :두 매개변수를 받고 boolean 값을 반환하는 test 메소드를 가진다.

ToIntFunction<T>, ToLongFunction<T>, ToDoubleFunction<T> : 객체를 받아 기본 타입(int, long, double)의 결과를 반환하는 applyAsInt, applyAsLong, applyAsDouble 메소드를 가진다.

BiOperator<T>: BiFunction<T, T, T>의 특별한 형태로, 두 매개변수를 받아 같은 타입의 결과를 반환합니다.

등등

좀더 자세한 설명과 사용법은 따로 찾아보길 바란다.

 

그렇다면 내장 함수형 인터페이스를 사용해서 위에 코드를 변경하면 어떻게 될까??

public class Calculator {
    private int a;
    private int b;

    public Calculator(int a, int b) {
        this.a = a;
        this.b = b;
    }
    public int result(BinaryOperator<Integer> operator) {
        return operator.apply(a, b);
    }
}

내장 함수형 인터페이스 BinaryOperator<Integer> 를 사용해보자

 

 

public class Main {
    public static void main(String[] args) {
    	//1.계산기 클래스 생성 
        Calculator calculator = new Calculator(20,10);
        
        //2.람다 사용
        int result = calculator.result((a, b) -> a - b);
    }
}

이 코드는 그대로 사용하면 된다.

 

결국 Calculator 클래스는 이제 BinaryOperator<Integer>를 사용하여 두 정수의 연산을 처리하며,
기존의
Operate 인터페이스를 대체하게 된다.
메인 메소드에서의 사용법은 동일하게 유지되면서도, 코드는 더욱 효율적이고 간결해졌다.

따라서 사용자 정의 Operate 인터페이스가 더 이상 필요하지 않게 된다.


결론

 

처음에는 Operate와 같은 사용자 정의 함수형 인터페이스와 각각의 연산을 위한 별도의 구현 클래스 (Minus, Add, Multiply 등)가 필요했다. 이 방식은 각 연산에 대한 명시적인 클래스 구현을 필요로 하며, 코드의 길이와 복잡성을 증가시켰다.

 

변화의 첫 단계는 익명 객체의 도입이었다.
익명 객체를 사용하면서 Operate 인터페이스의 구현체를 별도의 클래스로 작성하는 대신,
필요한 곳에 직접 인터페이스를 구현하게 되었다.
이는 중간 단계의 개선이었으나, 여전히 람다 표현식에 비해 코드는 다소 장황했다.

 

다음 단계의 진화는 람다 표현식의 도입이었다.
람다를 사용하면서 코드는 더욱 간결해지고 읽기 쉬워졌다.
람다 표현식은 함수형 인터페이스의 추상 메소드 구현을 한 줄로 줄여주어 코드의 가독성을 크게 향상시켰다.

 

마지막으로, 자바의 내장 함수형 인터페이스 BinaryOperator<Integer>를 도입함으로써,
우리는 더 이상 사용자 정의 인터페이스인
Operate를 필요로 하지 않게 되었다.
이러한 변화는 코드를 표준화하고, 재사용성을 높이며, 유지보수를 용이하게 했다.


 

3.스프림API

 

진행중

'JAVA' 카테고리의 다른 글

예외처리  (0) 2024.04.27
StringBuilder , System.out.println  (1) 2023.12.06
빌드도구  (0) 2023.07.28
Servlet, JSP, Tomcat, Apache - 웹 개발 기초 이해하기  (0) 2023.07.28