ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [모던 자바 인 액션] 동작 파라미터화와 람다 표현식
    자바 2024. 10. 20. 22:51

    동작 파라미터화

    동작 파라미터화동작(행동) 자체를 메서드의 인수로 전달하는 방식으로, 메서드의 동작을 유연하게 정의할 수 있게 해주는 프로그래밍 기법이다. 쉽게 말하면 메서드의 인수에 다른 메서드를 집어 넣는 것이다. 지금부터 예제를 통해서 동작 파라미터화를 사용하여 필요성을 느끼고 코드를 점점 개선하여 정리할 것이다.

     

    1. 녹색 사과 필터링

    public static List<Apple> filterGreenAppes(List<Apple> inventory) {
    	List<Apple> result = new ArrayList<>();
        for (Apple apple: inventory) {
        	if (GREEN.equals(apple.getColor()) {
            	result.add(apple);
            }
        }
        return result;
     }

     

    만약 Apple 객체에서 색깔이 초록색인 사과만 찾고 싶다고 가정하자. 그러면 위의 코드와 같이 작성할 것이다.  만약 녹색 사과 필터링이 아니라 빨간 사과 필터링을 하고 싶다면 현재 코드에서는 저 코드를 복사 붙여 넣기 한 후 GREEN을 RED로 바꿔야할 것이다. 그러면 엄청난 코드 중복이 발생한다...ㅜ 그렇다면 어떻게 해결할 수 있을까? 간단한 방법으로는 내가 원하는 컬러를 인자로 받으면 된다!

    2. 색 파라미터화

    public static List<Apple> filterAppleByColor(List<Apple> inventory, Color color) {
    	List<Apple> result = new ArrayList<>();
        for (Apple apple: inventory) {
        	if (apple.getColor().equals(color)) {
            	result.add(apple);
            }
        }
        return result;
     }

     

    이렇게 컬러를 인자로 받고 유연하게 다른 색깔에 대해서도 필터가 가능하도록 수정하였다. 만약에 색깔이 아니라 사과의 크기로 필터를 하려면 어떻게 해야할까? 바로 키에 관련해서 필터하는 다른 메서드를 위의 형태처럼 작성할 것이다. 즉, 위의 코드에서 if 문을 사과의 크기를 기준으로 result에 추가할지 결정하면 된다. 좋은 코드라고 생각할 수 있지만 이렇게 필터 요구 사항이 계속 생긴다면 어떡할까? 결국 우린 if 문안에 조건만 바꾸면 되는데 계속 중복적인 코드를 복사 붙여 넣기하여 각 요구 사항마다 맡는 필터처리 메서드를 만들어야한다. 이를 편하게 만들어주는 것이 동작 파라미터화이다. 즉, 요구 사항 자체를 메서드의 인자로 받는 것이다!

    3. 추상적 조건으로 필터링

    참 또는 거짓을 반환하는 함수를 프레디케이트라고 한다. 프레디케이트를 가지는 인터페이스를 구성하고 이를 요구 사항에 맡게 구현하여 동작 파라마티터화 하도록 변경하면 다음과 같다.

    public interface ApplePredicate {
    	boolean test (Apple apple);
    }
    
    public class AppleHeacyWeightPredicate implements Appleredicate { 
    	public boolean test(Apple apple) {
        	return apple.getWeight() > 150;
        }
    }
    
    public class AppleGreenColorPredicate implements Appleredicate { 
    	public boolean test(Apple apple) {
        	return GREEN.equals(apple.getColor());
        }
    }
    
    public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p) {
    	List<Apple> result = new ArrayList<>();
        for(Apple apple: inventory) { 
        	if(p.test(apple)) { 
            	result.add(apple);
            }
        }
        return result;
    }

     

    이렇게 구성하면 이전 코드에 비해 유연한 코드를 생성할 수 있고, 가독성 또한 좋아진다. 만약 새로운 요구 사항이 생겨 다른 조건으로 필터링을 해야한다면 ApplePredicate를 구현하여 요구 사항을 만족시키면 된다. 그런데 이렇게 발전시켜도 아쉬운 점이 있다. 그것은 바로 귀찮음.. ㅋㅋㅋㅋㅋ 요구사항이 생길 때 마다 새로운 ApplePredicate를 상속받은 클래스를 만들어야한다. 만약 필터를 딱 한번 사용하는데 이렇게 클래스를 만든다면 비효율적이다. 그래서 이를 방지하기 위해 익명 클래스를 사용한다. 익명 클래는 자바의 지역 클래스와 비슷한 개념이다. 익명 클래스를 사용하면 클래스 선언과 인스턴스화를 동시에 할 수 있다. 즉, 즉석에서 필요한 구현을 만들어서 사용할 수 있게 된다.

     

    4. 익명 클래스 사용

    List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
    	public boolean test(Apple apple) {
        	return RED.equals(apple.getColor());
        }
    }

     

    이렇게 ApplePredicate를 구현한 클래스를 만들지 않고 일회용으로 인자로 전달할 때 잠시 임의의 익명 클래스를 넣어 필터링할 수 있다. 하지만 이 방법 또한 ApplePredicate를 구현하는 코드가 길다. 코드의 장황함은 나쁜 특성이 있다. 이런 코드는 유지보수하는데 시간이 오래 걸리고 읽고 이해하는데 힘들다. 그래서 이를 더 간단히 람다 표현식을 사용하면 해결할 수 있다.

     

    5. 람다 표현식 사용

    List<Apple> result = filterApples(inventory, (Apple apple) -> GREEN.equals(apple.getColor()));

     

    이렇게 하면 정말 코드 길이가 짧아지고 한눈에 알아보기 쉽다. 그런데 여기에 사용된 람다 표현식은 무엇일까? 람다 표현식에 대해 자세히 알아보자!

     

    람다

    람다란?

    람다 표현식은 매서드로 전달할 수 있는 익명 함수를 단순화 한 것이다. 람다의 특징은 다음과 같다.

    • 익명
      • 보통의 메서드와 달리 이름이 없기에 익명이라고 표현한다.
    • 함수
      • 람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수라고 부른다.
    • 전달
      • 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다.
    • 간결성
      • 익명 클래스처럼 많은 코드를 구현할 필요가 없다.

    람다 표현식의 구성 요소를 살펴보자!

     

    다음과 같이 크게 두 가지로 나뉘는데 여기서 중요한 것은 람다 바디이다. 람다 바디에는 유효한 식이 따로 존재한다. 예시로 살펴보자! 간단하므로 주석으로 설명을 적어놓았다.

     

    /**
    String 형식의 파라미터 하나를 가지며 int를 반환한다. 람다 표현식에는 return이 함축되어 있으므로
    return 문을 명시적으로 사용하지 않아도 된다.
    **/
    (String s) -> s.length()
    
    (Apple a) -> a.getWeight() > 150; // boolean 리턴
    
    /**
    리턴은 void형이다.
    람다 표현식은 여러 행의 문장을 포함할 수 있다.
    */
    (int x,int y) -> {
    	System.out.println("Result: ");
        System.out.println(x+y);
    }
    
    ()->42 // 파라미터가 없으며 int 42를 반환한다.
    
    (Integer i) -> {return "Aplian"+i;} // return은 흐름 제어문이기에 만약 사용한다면 {} 블록 스타일을 사용해야한다.

     

    람다를 어디에 사용할 수 있을까?

    그렇다면 우리는 이 람다를 어디에 사용할 수 있을까? 아무데나 막 가져다 붙일 순 없을 것이다 ㅋㅋㅋㅋ 당연히 규칙이 존재한다! 바로 함수형 인터페이스라는 문맥에서 람다 표현식을 사용할 수 있다. 함수형 인터페이스오직 하나의 추상 메서드만 가지는 인터페이스이다. 이전에 만든 ApplePredicate도 함수형 인터페이스이다. test라는 추상 메서드 하나만 존재하기 때문이다. 그렇기에 람다 표현식을 사용할 수 있었다. 대표적인 함수형 인터페이스에는 뭐가 있을까? 대표적인 3개만 살펴보자!

    1. Predicate

    java.util.function.Predicate<T> 인터페이스는 test라는 추상 메서드를 정의하며 test는 제니릭 형식 T의 객체를 인수로 받아 불리언을 반환한다.

    // Predicate 추상 메서드 test
    public interface Predicate<T> { 
    	boolean test(T t);
    }
    
    // 사용 예시 -> boolean 반환
    Predicate<String> notEmptyStringPredicate = (String s) -> !s.isEmpty();

     

    2. Consumer

    java.util.function.Consumer<T> 인터페이스는 제네릭 형식 T 객체를 받아서 void를 반환하는 accept 이라는 추상 메서드를 정의한다.

    public interface Consumer<T> {
    	void accept(T t);
    }
    
    public <T> void forEach(List<T> list, Consumer<T> c) {
    	for(T t:list) {
        	c.accept(t);
         }
     }
     
    // 메서드 사용
    forEach(
    	Arrays.asList(1,2,3,4,5);
        (Integer i) -> System.out.println(i)
    );

     

    3. Function

    java.util.function.Function<T,R> 인터페이스는 제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를 반환하는 추상 메서드 apply를 정의한다.

    public interface Function<T,R> {
     	R apply(T t);
    }
    
    public <T,R> List<R> map(List<T> list, Function<T,R> f) {
    	List<R> result = new ArrayList<>();
        for(T t: list) {
        	result.add(f.apply(t));
        }
        return result;
    }
    
    // 메서드 사용
    List<Integer> l = map(
    	Arrays.asList("lambdas", "in", "action"),
        (String s) -> s.length() // Function apple 메서드를 구현하는 람다
    );

     

    람다 주의점!

    지역 변수의 제약

    지금까지 람다 예시를 살펴보면 모두 파라미터로 넘겨진 변수만을 사용하였다. 하지만 람다 표현식에는 익명 함수가 하는 것 처럼 자유 변수를 사용할 수 있다. 자유 변수는 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수를 의미한다. 정확히 말로 이해가 안된다면 다음 예시를 살펴보자!

    int number = 1337;
    Runnable r = () -> System.out.println(number);

     

    이렇게 파라미터로 받은 변수를 사용하는 것이 아니라 외부에서 정의된 변수를 사용할 수 있다. 하지만 여기엔 주의점이 있다. 람다는 인스변수 변수와 정적 변수를 자유롭게 캡쳐할 수 있다. 하지만 그러러면 지역 변수는 명시적으로 final로 선언이 되어 있거나 실질적으로 final로 선언되 변수와 똑같이 변경되지 않고 사용되어야한다. 자바에서 람다에 사용되는 지역 변수는 복사본을 사용한다. 이 람다는 쓰레드를 활용할 때 사용할 수 있는데 만약 이 값이 나중에 변경이 된다면 동기화 문제가 발생하게 된다. 그렇기에 복사본의 값이 바뀌지 않도록 지역 변수에는 한 번만 값을 할당해야 한다는 제약이 생긴다.

     

    // 컴파일 오류
    int number = 1337;
    Runnable r = () -> System.out.println(number);
    number = 123;

     

    '자바' 카테고리의 다른 글

    [모던 자바 인 액션] 스트림  (0) 2024.10.21
    [모던 자바 인 액션] 자바 8 이후의 변화  (0) 2024.10.15
Designed by Tistory.