[모던 자바] 함수형 프로그래밍의 패턴 매칭(pattern matching)
패턴 매칭
일반적으로 함수형 프로그래밍을 구분하는 중요한 특징으로 구조적인 패턴 매칭(pattern matching)을 들 수 있다.
예를 들어 자동차 연료를 주입하는 로직이 있다고 가정했을때 가솔린, 디젤, 전기 차량에 따라 다른 연료를 주입해야 한다. 이럴때 자바에서는 if-then-else나 switch문을 사용해서 구현하곤 한다.
public class Car {
}
public class GasolineCar extends Car {
}
public class DieselCar extends Car {
}
public class ElectricCar extends Car {
}
// 연료 주입
public Car fuel(Car car) {
if (car instanceof GasolineCar) {
System.out.println("휘발유 주유");
} else if (car instanceof DieselCar) {
System.out.println("경유 주유");
} else {
System.out.println("전기 충전");
}
return car;
}
자료형이 복잡해지면서 이러한 작업을 처리하는데 필요한 코드의 양도 증가했다.
패턴 매칭을 사용하면 이러한 불필요한 잡동사니 코드를 줄일 수 있다.
방문자 디자인 패턴
자바에서는 방문자 디자인 패턴(visitor design pattern)으로 자료형을 언랩할 수 있다.
특히 특정 데이터 형식을 방문하는 알고리즘을 캡슐화하는 클래스를 따로 만들 수 있다.
방문자 클래스는 지정된 데이터 형식의 인스턴스를 인수로 받는다. 그리고 인스턴스의 모든 멤버에 접근한다.
public class GasolineCar extends Car {
public Car accept(FuelVisitor v) {
return v.visit(this);
}
}
public class DieselCar extends Car {
public Car accept(FuelVisitor v) {
return v.visit(this);
}
}
public class ElectricCar extends Car {
public Car accept(FuelVisitor v) {
return v.visit(this);
}
}
public class FuelVisitor {
public Car visit(GasolineCar car) {
System.out.println("휘발유 주유");
return car;
}
public Car visit(DieselCar car) {
System.out.println("경유 주유");
return car;
}
public Car visit(ElectricCar car) {
System.out.println("전기 충전");
return car;
}
}
스칼라의 패턴 매칭 흉내 내기
패턴 매칭이라는 좀 더 단순한 해결 방법도 있다.
하지만 자바는 패턴 매칭을 지원하지 않고 스칼라 프로그래밍 언어에서 지원한다.
이러한 패턴매칭을 자바로 흉내낼 수 있는데 자바8의 람다를 이용할 수 있다. 람다는 단일 수준의 패턴 매칭만 지원된다.
먼저 규칙을 정해보면, 람다를 이용하여 if-then-else가 없어야 한다. 삼항 연산자로 if-then-else를 대신할 수 있다.
물론 if-then-else를 사용하는 것이 코드의 명확성을 더 높힐 수 있지만, if-then-else나 switch가 패턴 매칭에는 도움이 되질 않으며 람다를 이용하면 단일 수준의 패턴 매칭을 간단하게 표현할 수 있으므로 여러 개의 if-then-else 구분이 연결되는 상황을 깔끔하게 정리할 수 있다.
public <T> T patternMatchFuel(Car car,
Function<GasolineCar, T> gasolineCarFuel,
Function<DieselCar, T> dieselCarFuel,
Function<ElectricCar, T> electricCarFuel,
Supplier<T> defaultFuel) {
return
(car instanceof GasolineCar) ?
gasolineCarFuel.apply((GasolineCar) car) :
(car instanceof DieselCar) ?
dieselCarFuel.apply((DieselCar) car) :
(car instanceof ElectricCar) ?
electricCarFuel.apply((ElectricCar) car) :
defaultFuel.get();
}
가솔린, 디젤, 전기차 별로 실행할 로직을 Function 함수형 인터페이스를 통해 구현한다.
만약 인수가 더 많다면 BiFunction, TriFunction 함수형 인터페이스를 사용해서 구현할 수 있다.
그럼 이제 위 코드를 실행해보자.
Car electricCar = new ElectricCar();
patternMatchFuel(
electricCar,
(car) -> { System.out.println("휘발유 주유"); return car; },
(car) -> { System.out.println("경유 주유"); return car; },
(car) -> { System.out.println("전기 충전"); return car; },
() -> { System.out.println("알수없는 유형의 자동차입니다."); return null; }
);
// 출력 결과 : 전기 충전
위 예제 코드를 메서드로 감싸 안쪽에서 처리해 더욱 단순화할 수도 있다.
public Car simplyPatternMatchFuel(Car targetCar) {
Function<Car, Car> gasolineCarFuel = (car) -> { System.out.println("휘발유 주유"); return car;};
Function<Car, Car> dieselCarFuel = (car) -> { System.out.println("경유 주유"); return car;};
Function<Car, Car> electricCarFuel = (car) -> { System.out.println("전기 충전"); return car;};
Supplier<Car> defaultFuel = () -> { System.out.println("알수없는 유형의 자동차입니다."); return null; };
return patternMatchFuel(targetCar, gasolineCarFuel, dieselCarFuel, electricCarFuel, defaultFuel);
}
Car electricCar = new ElectricCar();
simplyPatternMatchFuel(electricCar);