관리 메뉴

생각해보기

모던 자바 인 액션 -2- 본문

자바

모던 자바 인 액션 -2-

정한_s 2021. 12. 7. 16:30

소비자의 요구사항은 항상 바뀌고 변화하는 사용자 요구 사항에 효과적으로 대응해야 한다. 효과적으로 대응하기 위해서는 새로 추가하는 기능은 쉽게 구현할 수 있어야 하며 유지보수가 쉬워야 한다. 

 

동작 파라미터화를 이용하면 자주 바뀌는 요구사항에 효과적으로 대응할 수 있다. 

동작 파라미터화란 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록을 의미한다. 이 코드 블록은 나중에 프로그램에서 호출한다. 즉, 코드 블록의 실행이 나중으로 미루어 진다.

 

변화하는 요구 사항에 맞추어 코드가 변경되는 과정을 통해 자바 8의 장점을 보여준다.

 

초기 : 녹색 사과를 필터링 하자

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

녹색 사과만 필터링 하는 코드이다. 색은 녹색으로 정했지만 요구 사항이 바뀌어 다른 색으로 필터링하는 조건이 생겼다.

 

변경 1 : 녹색 사과 뿐만이 아니라 원하는 색으로 필터링 하자

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

녹색 뿐만 아니라 색으로 필터링 할 수 있게 되었다. 이번에는 색이 아니라 무게에 따라 분류하고 싶어졌다

 

변경 2 :  원하는 무게에 따라 필터링 하자

public static List<Apple> filterApplesByColors(List<Apple> inventory, int weight){
        List<Apple> result = new ArrayList<>();
        for(Apple apple : inventory){
            if(apple.weight() > weight)
                result.add(apple);
        }
        return result;
    }

원하는 무게 또는 색으로 선택해서 필터링 하고 싶어졌다.

 

변경 3 : 원하는 색 또는 무게로 필터링

public static List<Apple> filterApplesByColors(List<Apple> inventory, Color color, int weight, boolean flag){
        List<Apple> result = new ArrayList<>();
        for(Apple apple : inventory){
            if(flag &&apple.equals(color) || 
            !flag && apple.getWeight()> weight)
                result.add(apple);
        }
        return result;
    }

이렇게 요구사항이 계속해서 바뀌었을 때 기존에 있는 코드를 변경해야 하는 것은 좋지 않은 코드이다. 우리는 요구사항에 유연하게 대응해야 한다. 동적 파라미터화를 이용해서 유연성을 얻어 보자

 

동작 파라미터화

// 선택 조건을 결정 하는 인터페이스
interface ApplePredicate{
	boolean test(Apple apple)
}

// 특정 무게에 조건으로 필터링 클래스
 class AppleHeavyWeightPredicate implements ApplePredicate{
 
    @Override
    public boolean test(Apple apple) {
    	return apple.getWeight()> 150;
    }
}
// 특정 색깔을 조건으로 필터링 클래스
class AppleGreenColorPredicate implements ApplePredicate{

	@Override
	public boolean test(Apple apple) {
		return "green".equals(apple.getColor());
	}
}

// 적용 메서드
public static List<Apple> filterApplesByColors(List<Apple> inventory, ApplePredicate p){
        List<Apple> result = new ArrayList<>();
        for(Apple apple : inventory){
            if(p.test(apple){
            	result.add(p);
            }
        }
        return result;
    }

코드가 단순해지고 높은 응집도를 가질 수 있게 되었다. 하지만 새로운 동작을 추가하기 위해서는 여러 클래스를 구현하는 방법은 번거로워 진다.

 

동적 파라미터화 : 익명 클래스 사용

익명 클래스는 자바의 지역 클래스와 비슷한 개념으로, 말 그대로 이름이 없는 클래스 이다. 익명클래스를 이용하면 클래스 선언과 인스턴스화를 동시에 할 수 있다. 즉, 필요한 구현을 즉석에서 만들어 사용할 수 있다.

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

익명 클래스로 코드를 줄이긴 했지만, 아직 까지 반복되어 지저분한 코드가 남아 있다. 자바 8 람다 표현식을 사용해서 더 간단히 만들어보자

 

동적 파라미터화 : 람다 표현식 사용

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

동적 파라미터화 : 제네릭 사용

interface Predicate<T>{
	boolean test(T t);
}

static <T> List<T> filter(List<T> list, Predicate<T> p){
    List<T> result = new ArrayList<>();
    for(T e : list){
    	if(p.test(e))
    		result.add(e);
    }
    return result;
}

// 사용
List<Apple> redApples = filter(inventory, (Apple apple) -> "red".equals(apple.getColor());
List<Integer> evenNumbers = filter(numbers, (Integer i) -> i%2 == 0 );

 

람다에 대해서 알아보자

람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화 한 것

 

람다와 익명 클래스의 차이점

https://www.geeksforgeeks.org/difference-between-anonymous-inner-class-and-lambda-expression/

https://stackoverflow.com/questions/61921589/from-anonymous-class-to-lambda-expression

 

람다의 특징

  • 익명 : 보통의 메서드와 달리 이름이 없다. 구현해야 할 코드가 줄어든다
  • 함수 : 특정 클래스에 종속되지 않으므로 함수이다. 메서드처럼 파라미터 리스트, 바디, 반환 형식, 예외 리스트를 포함한다
  • 전달 : 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다
  • 간결성 : 익명 클래스 처럼 반복되는 코드를 구현할 필요가 없다

람다 표현식

람다 표현식은 파리미터, 화살표, 바디로 이루어진다

표현식 스타일 람다

  • (parameters) -> expression  (return이 함축되어 있으므로 return 문을 명시적으로 사용하지 않아도 된다)

블록 스타일 람다

  • (parameters) -> { statements; } ( void 리턴을 제외한 return 값이 있을 때 return을 명시적으로 사용해야 한다)

*람다는 함수형 인터페이스라는 문맥에서 사용할 수 있다.

 

함수형 인터페이스

함수형 인터페이스는 정확히 하나의 추상 메서드를 지정하는 인터페이스이다(Object 클래스의 메서드는 제외하고). 많은 디폴트 메서드가 있더라도 추상 메서드가 오직 하나면 함수형 인터페이스 이다.

/*
@FunctionalInterface 어노테이션이 붙어 있으면 
함수형인터페이스가 아닐 경우 컴파일 에러를 발생시킨다
*/


// 함수형 인터페이스
// @FunctionalInterface 컴파일 가능
public interface Adder{
	int add(int a, int b);
}

// 함수형 인터페이스 x, 부모로 부터 1개 상속을 받았으므로 총 2개의 추상 메서드가 있음
// @FunctionalInterface 컴파일 오류
public interface SmartAdder extends Adder {
	int add(double a, double b);
}

 

함수 디스크립터

함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식 시그니처를 가리킨다. 람다 표현식 시그니처를 서술하는 메서드를 함수 디스크립터라고 부른다.

 

* 시그니처 : 서드 시그니처는 메서드 명과 파라미터의 순서, 타입, 개수를 의미한다.

 

함수형 인터페이스

 

 

Predicate<T>

T -> boolean

test 라는 추상 메서드를 정의하며 test는 제너릭 형식 T 객체를 인수로 받아 boolean 반환

 

Consumer<T>

T -> void

accept라는 추상 메서드를 정의한다. T 형식의 객체를 인수로 받아서 어떤 동작을 수행하고 싶을 때 사용

 

Function<T, R>

T -> R

apply라는 추상 메서드를 정의한다. 제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를 반환한다. 

 

Supplier<T>

() -> T // * void -> T

get이라는 추상 메서드를 정의한다. 인자를 받지 않고  T 객체를 반환한다 

 

UnaryOperator<T>

T -> T

apply 라는 추상 메서드 정의한다. Function을 상속하였다. T 객체를 받아 T 객체를 반환한다

 

BinaryOperator<T>

(T , T) -> T

 apply 추상 메서드 정의한다. T의 인자 두개를 받고, 동일한 T 객체를 반환한다. BiFunction 상속하였다. 

 

BiPredicate<L, R>

(L , R) -> boolean

test 추상 메서드 정의한다. 2개의 인자를 받고 boolean을 리턴한다.

 

BiConsumer<T, U> 

(T, U) -> void

accept라는 추상 메서드를 정의한다. 2개의 인자를 받고 리턴 값은 없다

 

BiFunction<T, U, R>

(T, U) -> R

apply라는 추상 메서드 정의. 2개의 인자를 받고 R 객체를 반환한다.

 

사용 사례

  • 불리언 표현 - Predicate 종류
  • 객체 생성 - Supplier 종류
  • 객체에서 소비 - Consumer 종류
  • 객체에서 선택/추출 - Function 종류
  • 두 값 조합 - BinaryIOperation 종류
  • 두 객체 비교 - BiFunction, Comparator

 

 

* 추가로

자바 8에서 기본형을 입출력으로 사용하는 상황에 오토박싱 동작을 피할 수 있는 특별한 버전의 함수형 인터페이스도 제공한다. 일반적으로 특정 형식을 입력을 받는 함수형 인터페이스 앞에는 형식명이 붙는다. ex) DoublePredicate, IntConsumer 등

 

* 람다 표현식이 예외를 던질 수 있다면 추상 메서드도 같은 예외를 던질 수 있도록 thorows 선언해야 한다

* 특별한 void 호환 규칙 : 람다 표현식의 바디에 일반 표현식이 있으면 void를 반환하는 함수와 호환이 된다. 

/*
Consumer T -> void 이므로 void를 기대한다.
람다 표현식에서 list.add(s)는 return이 생략되었고 boolean을 return 한다.
하지만 특별한 void 호환 규칙에 의해서 유효한 코드가 된다
*/
Consumer<String> a = s-> list.add(s)

 

형식 추론

자바 컴파일러는 람다 표현식이 사용된 콘텍스트를 이용해서 람다 표현식과 관련된 함수형 인터페이스를 추론한다. 따라서 파라미터 형식을 생략할 수 있다.

// c1과 c2는 같은 코드이다
Comparator<Apple> c1 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
Comparator<Apple> c2 = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());

 

지역 변수 

 

람다 표현식에서는 외부에 정의된 변수를 활용할 수 있다. 이와 같은 동작을 람다 캡처링이라고 부른다. 하지만 자유 변수(파라미터 변수가 아닌 외부 변수)에 제약이 있다.

 

인스턴스 변수와 정적 변수는 자유롭게 캡처 가능하다

 

지역 변수는 명시적으로 final로 선언되어 있어야 하거나 final 선언 변수 처럼 사용되어야 한다.

즉, 람다 표현식은 한 번만 할당할 수 있는 지역 변수를 캡처할 수 있다.

 

*그 이유

더보기

인스턴스 변수는 힙에 저장되지만 지역 변수는 스택에 위치하기 때문이다.

람다가 스레드에서 실행된다면 변수를 할당한 스레드가 사라져서 변수 할당이 해제되었는데도 람다를 실행하는 스레드는 해당 변수에 접근하려 할 수 있기 때문이다. 따라서 람다에서 변수에 접근을 허용하는 것이 아닌 복사한 값을 제공한다. 이를 위해 복사본의 값이 바뀌지 말아햐 하므로 지역변수에 한 번만 값을 할당해야 하는 제약이 생긴다

 

메서드 참조

메서드 참조를 이용하면 기존의 메서드 정의를 재활용해서 람다처럼 전달할 수 있다. 메서드 참조는 특정 매서드만을 호출하는 람다의 축약형이라고 생각할 수 있다.

 

메서드 명 앞에 구분자(::)를 붙이는 방식으로 메서드 참조를 활용할 수 있다.

 

람다와 메서드 참조 단축 표현 예제

람다 메서드 참조 단축 표현
(Apple apple) -> apple.getWeight() Apple::getWeight
()-> Thread.currentThead().dumpStack() Thread.currentThead()::dumStack
(str, i) -> str.substring(i) String::substring
(String s) -> System.out.println(s) System.out::println
()-> this.isValidName() this::isValidName

 

메서드 참조를 만드는 방법

  1. 정적 메서드 참조 : ex) s -> Integer.parseInt(s)  Integer::parseInt
  2. 다양한 형식의 인스턴스 메서드 참조 : ex) s -> s.length()  String::length
  3. 기존 객체의 인스턴스 메서드 참조 : ex) 람다 외부 변수 apple에 대해 () -> apple.getWeight()  apple::getWeight

ClassName&amp;nbsp;

  • 생성자, 배열 생성자, super 호출 등에 사용할 수 있는 특별한 형식의 메서드 참조도 있다.
  • 컴파일러는 람다 표현식의 형식을 검사하던 방식과 비슷한 과정으로 메서드 참조가 주어진 함수형 인터페이스와 호환하는지 확인한다.
  • 즉, 메서드 참조는 컨텍스트의 형식과 일치해야 한다.

생성자 참조

ClassName::new처럼 클래스명과 new 키워드를 이용해서 생성자의 참조를 만들 수 있다.

 

* 인수가 세 개인 생성자의 생성자 참조를 사용하려고 할 때

// 직접 인수 세 개를 받는 함수형 인터페이스를 만들어 사용한다
public interface TriFunction<T, U, V, R>{
	R apply(T t, U u, V v);
}
 //사용 예시
 TriFunction<Integer, Integer, Integer, Color> colorFactory = Color::new

 

 

람다 표현식을 조합할 수 있는 유용한 메서드

 

Comparable<T> comparing 메소드, reversed 메소드 등

// 사용 예시 : 무게 정렬을 키로 사용한다
Comparator<Apple> c = Comparator.comparing(Apple::getWeight(= 정렬조건))

// 역정렬 예시 : 무게 정렬을 내림차순으로 정렬한다
inventory.sort(comparing(Apple::getWeight).reversed()) 

// Comperator 연결 : 무게를 내림차순 정렬, 무게 가 같으면 국가 별로 정렬
inventory.sort(comparing(Apple::getWeight)
			.reversed()
            .thenComparing(Apple::getCountry))

Predicate 조합

Perdicate 인터페이스는 복잡한 프레티케이트를 만들 수 있도록 negate, and, or 세가지 메서드를 제공한다. 

  • negate  : 특정 프레디케이트를 반전 시킬때
  • and : 람다를 and 조합 할 때
  • or : 람다를 or 조합 할 때 

Function 조합

Function 인터페이스는 Function 인스턴스를 반환하는 andThen, compose 디폴트 메서드를 제공한다

  • andThen : 주어진 함수를 먼저 적용한 결과를 다른 함수의 인수로 전달하는 함수
  • conpose : 인수로 주어진 함수를 먼저 실행 후 그 결과를 외부 함수의 인수로 전달하는 함수 

 

 

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

모던 자바 인 액션 -6-  (0) 2021.12.10
모던 자바 인 액션 -5-  (0) 2021.12.10
모던 자바 인 액션 -4-  (0) 2021.12.08
모던 자바 인 액션 -3-  (0) 2021.12.08
모던 자바 인 액션 -1-  (0) 2021.12.06
Comments