동작 파라미터화
먼저 동작 파라미터화부터 알아보자. 말 그대로 어떤 동작을 파라미터로 전달한다는 뜻이다. 콜백 함수가 생각나기도 하고, 디자인 패턴의 템플릿 메소드 패턴, 전략 패턴, 데코레이터 패턴 등이 생각난다면 훌륭하다. 언제, 어떻게 실행될지 모르는 코드 조각을 다른 메소드에게 전달해주는 것이다.
예를 들면, 카페를 차린다고 해보자.
public class Menu {
String name;
int price;
MenuType type;
public Menu(String name, int price, MenuType type) {
this.name = name;
this.price = price;
this.type = type;
}
}
MenuType으로는 커피, 케이크 두 가지만 있다고 하자.
public enum MenuType {
COFFEE, CAKE
}
이제 구체적인 메뉴를 만든다.
Menu americano = new Menu("아메리카노", 3000, MenuType.COFFEE);
Menu latte = new Menu("라떼", 3500, MenuType.COFFEE);
Menu strawberry = new Menu("딸기", 6000, MenuType.CAKE);
Menu carrot = new Menu("당근", 6500, MenuType.CAKE);
List<Menu> menu = new ArrayList<>();
menu.add(americano);
menu.add(latte);
menu.add(strawberry);
menu.add(carrot);
어떤 손님은 커피를 마시고 싶어서 커피 메뉴만 골라 보고싶다.
List<Menu> coffee = new ArrayList<>();
for (Menu m : menu) {
if (m.type == MenuType.COFFEE) {
coffee.add(m);
}
}
또 다른 손님은 돈이 6천원밖에 없어서 6천원 이하의 메뉴를 보고싶다.
List<Menu> cheap = new ArrayList<>();
for (Menu m : menu) {
if (m.price <= 6000) {
cheap.add(m);
}
}
좀 이상한 손님은 이름이 두 글자인 메뉴만 보고싶을 수도 있다.
List<Menu> two = new ArrayList<>();
for (Menu m : menu) {
if (m.name.length() == 2) {
two.add(m);
}
}
반복되는 코드는 개발자를 불안하게 한다. List에서 List를 만드는 것이니 메소드로 추출하고 싶은데, if 조건을 어떻게 마음대로 바꿀 수 있을까?
이에 대한 해답이 바로 동작 파라미터화다.
전략 패턴
한 가지 방법은 전략 패턴을 사용하는 것이다. 먼저 인터페이스를 정의해보자.
public interface Condition {
boolean check(Menu menu);
}
커피를 찾는 전략은 이렇게 만들 수 있다.
public class CoffeeCondition implements Condition {
@Override
public boolean check(Menu menu) {
return menu.type == MenuType.COFFEE;
}
}
6000원 이하의 싼 메뉴는 이렇게 찾는다.
public class CheapCondition implements Condition {
@Override
public boolean check(Menu menu) {
return menu.price <= 6000;
}
}
두 글자인 메뉴는 이렇게 찾을 수 있다.
public class TwoCharacterCondition implements Condition {
@Override
public boolean check(Menu menu) {
return menu.name.length() == 2;
}
}
이렇게 만든 전략은 아래와 같이 적용할 수 있다.
public static List<Menu> filter(List<Menu> menu, Condition condition) {
List<Menu> result = new ArrayList<>();
for (Menu m : menu) {
if (condition.check(m)) {
result.add(m);
}
}
return result;
}
이렇게 특정 조건을 만족하는 메뉴를 필터링하는 기능을 하나의 메소드로 분리할 수 있었다. 이것은 모두 if 조건문을 메소드의 파라미터로 받은 덕분이다. 실제로 필터링을 하려면 아래와 같이 사용할 수 있다.
List<Menu> coffee = filter(menu, new CoffeeCondition());
for문을 제거하니 훨씬 깔끔해졌다. 근데 문제는 매번 이렇게 새로운 클래스를 만들어야 할까? 만약 수백개의 조건이 필요하다면 아마 필요한 조건으로 클래스를 만들었는지 찾기조차 힘들어서 '그냥 새로 만들어서 쓰자'가 되어버릴지도 모른다.
익명 클래스
익명 클래스는 소스코드 관리에 있어서 조금 더 나은 선택일 수 있다. 한 번 사용되고 버려지는 코드들을 클래스로 만들어서 관리해 줄 필요가 없기 때문이다. Condition 인터페이스만 있으면 충분하다.
List<Menu> coffee = filter(menu, new Condition() {
@Override
public boolean check(Menu menu) {
return menu.type == MenuType.COFFEE;
}
});
다만, 같은 코드를 여러 곳에서 재사용한다면 전략패턴으로 클래스를 만들어 놓고 사용하는 것이 나을 수 있다. 익명 클래스는 소스 코드에서만 이름이 없을 뿐이지, 실제로 컴파일되면 .class 바이트코드가 생성되고, 클래스 로더에 의해 메모리에 올라간다. 따라서 같은 동작을 하는 익명 클래스는 결국 불필요한 메모리 낭비이다.
람다 표현식
Java 8에서는 동작 파라미터화를 위한 방법으로 람다 표현식을 추가하였다.
List<Menu> coffee = filter(menu, m -> m.type == MenuType.COFFEE);
익명 클래스를 사용할 때 보다 코드가 훨씬 짧고 깔끔하다. 람다 표현식에 익숙하다면 별 감흥 없겠지만, 처음 보는 사람들은 신기할 것이다. 변수도 아닌것이, 메소드의 본문에 들어갈만한 코드가 메소드의 파라미터로 전달되고 있다.
그럼 이 람다 표현식의 정체가 무엇인지 보자. 우선, 위 filter 메소드에 한 줄을 추가해본다.
public static List<Menu> filter(List<Menu> menu, Condition condition) {
System.out.println(condition.getClass());
List<Menu> result = new ArrayList<>();
for (Menu m : menu) {
if (condition.check(m)) {
result.add(m);
}
}
return result;
}
람다 표현식 코드와 익명 클래스 코드를 돌려보면 결과는 아래와 같다.
class Test$$Lambda$14/0x0000000800c01a58
class Test$1
익명 클래스로 만든 condition은 Test$1이라는 클래스의 인스턴스이고, 람다 표현식으로 만든 condition은 Test$$Lambda$14/0x0000000800c01a58라는 이름의 클래스의 인스턴스인 것 같다. 이번에는 condition의 인터페이스를 보자.
public static List<Menu> filter(List<Menu> menu, Condition condition) {
Class<?>[] interfaces = condition.getClass().getInterfaces();
for (Class<?> i : interfaces) {
System.out.println(i);
}
List<Menu> result = new ArrayList<>();
for (Menu m : menu) {
if (condition.check(m)) {
result.add(m);
}
}
return result;
}
결과는 아래와 같다.
interface Condition
interface Condition
익명 클래스는 Condition 인터페이스를 구현해서 만들었으니 당연한 결과지만, 람다 표현식은 Condition 인터페이스를 구현한 적이 없다. 누군가 우리 대신 Condition 인터페이스를 구현해서 이상한 이름의 클래스를 만들고, 인스턴스를 만들어 파라미터로 넣어주었다는 뜻이다.
그럼 결국 익명 클래스랑 똑같잖아? 실제로 람다 표현식으로 할 수 있는 모든 것은 익명 클래스로도 가능하다. 하지만 람다 표현식을 사용할 때의 장단점이 분명히 있다.
람다 표현식과 익명 클래스의 차이점
첫 번째로, 람다 표현식을 사용하면 코드가 훨씬 간결해지고, 의미가 분명해진다. 우리의 예시만 보더라도 구현해야 할 인터페이스의 이름과 메소드 이름을 알 필요가 없다. 오로지 무엇을 할 것인가에 대한 로직만이 남았다. 이렇게 람다 표현식을 파라미터로 전달하면 람다 표현식의 파라미터 타입과 리턴 타입을 고려해 자동으로 적절한 메소드에 바인딩 해준다. 만약 적절한 메소드가 없다면 컴파일 오류를 발생시켜 빠르게 오류를 발견할 수 있다.
두 번째로, 둘은 사용 목적이 다르다. 익명 클래스는 딱 한번만 사용되는 인터페이스를 구현하기 위한 목적이고, 람다 표현식은 동작을 파라미터로 전달하기 위한 목적이다. 이것은 인터페이스의 메소드가 둘 이상일 때 분명해진다. 인터페이스가 정의하고 있는 메소드가 둘 이상이라면 람다 표현식을 사용할 수 없는 반면, 익명 클래스는 여전히 코드 내에서 구현하여 사용할 수 있다.
마지막으로, 람다 표현식을 사용할 때는 인터페이스를 따로 정의해 줄 필요가 없다. Java 8에서는 함수형 인터페이스 타입들을 기본적으로 지원해준다. 우리의 예시처럼 Menu를 파라미터로 받아 boolean을 반환하는 함수는 Predicate<Menu>로 대체할 수 있다. Predicate 외에도 Consumer, Supplier, Function, BiFunction 등 다양한 함수형 인터페이스가 지원된다. 따라서 람다 표현식 사용을 위해서 따로 인터페이스를 정의할 필요는 없다.
3줄 요약
- 어떤 동작을 파라미터로 전달하는 것을 동작 파라미터화라고 한다
- 동작 파라미터화를 활용하면 유연하게 기능을 확장할 수 있다
- 람다 표현식, 익명클래스 등으로 동작을 파라미터화 할 수 있다.
'Java > 모던 자바 인 액션' 카테고리의 다른 글
[Java] 모던 자바 인 액션(6) - 새로운 날짜, 시간 API (0) | 2023.03.03 |
---|---|
[Java] 모던 자바 인 액션(5) - Optional (0) | 2023.03.02 |
[Java] 모던 자바 인 액션(4) - 스트림 API (0) | 2023.02.28 |
[Java] 모던 자바 인 액션(3) - 함수형 인터페이스와 람다 표현식 (1) | 2023.02.26 |
[Java] 모던 자바 인 액션(1) - Java의 진화 (0) | 2023.02.25 |