Contents
see List개요
Java 8 이후 람다, Stream, Optional 등의 도입으로 Java에서도 함수형 프로그래밍(FP) 패턴을 효과적으로 활용할 수 있게 되었습니다. 그러나 Java는 순수 함수형 언어가 아니기에, 객체지향과 함수형의 장점을 적절히 결합하는 것이 중요합니다. 이 글에서는 실무에서 바로 적용할 수 있는 Java FP 패턴을 소개합니다.
핵심 개념
Java에서 활용할 수 있는 함수형 프로그래밍의 핵심 요소들입니다.
- 불변성(Immutability): Record와 불변 컬렉션으로 상태 변경을 최소화합니다.
- 순수 함수(Pure Function): 동일 입력에 항상 동일 출력, 부수 효과 없는 함수를 지향합니다.
- 고차 함수(Higher-Order Function): 함수를 매개변수로 받거나 반환하는 함수입니다.
- 합성(Composition): 작은 함수를 조합하여 복잡한 로직을 구성합니다.
- 모나드 패턴: Optional, CompletableFuture 등으로 부수 효과를 캡슐화합니다.
실전 예제
함수 합성과 파이프라인 패턴입니다.
// 1. Function 합성으로 데이터 변환 파이프라인 구성
Function<String, String> trim = String::trim;
Function<String, String> toLowerCase = String::toLowerCase;
Function<String, String> removeSpecialChars =
s -> s.replaceAll("[^a-z0-9가-힣\s]", "");
// andThen으로 파이프라인 합성
Function<String, String> sanitize = trim
.andThen(toLowerCase)
.andThen(removeSpecialChars);
String result = sanitize.apply(" Hello, World! 안녕하세요 ");
// "hello world 안녕하세요"
// 2. 유효성 검사 합성 패턴
@FunctionalInterface
interface Validator<T> {
ValidationResult validate(T target);
default Validator<T> and(Validator<T> other) {
return target -> {
ValidationResult result = this.validate(target);
return result.isValid() ? other.validate(target) : result;
};
}
}
Validator<String> notEmpty = s ->
s != null && !s.isBlank()
? ValidationResult.valid()
: ValidationResult.invalid("빈 값입니다");
Validator<String> maxLength = s ->
s.length() <= 100
? ValidationResult.valid()
: ValidationResult.invalid("100자를 초과했습니다");
Validator<String> noHtml = s ->
!s.contains("<")
? ValidationResult.valid()
: ValidationResult.invalid("HTML 태그를 포함할 수 없습니다");
Validator<String> titleValidator = notEmpty.and(maxLength).and(noHtml);
ValidationResult result = titleValidator.validate(userInput);
모나드 패턴과 Railway-Oriented Programming입니다.
// 3. Either 모나드 구현 - 에러 처리의 함수형 접근
sealed interface Either<L, R> {
record Left<L, R>(L value) implements Either<L, R> {}
record Right<L, R>(R value) implements Either<L, R> {}
static <L, R> Either<L, R> right(R value) { return new Right<>(value); }
static <L, R> Either<L, R> left(L value) { return new Left<>(value); }
default <U> Either<L, U> map(Function<R, U> fn) {
return switch (this) {
case Right<L, R> r -> Either.right(fn.apply(r.value()));
case Left<L, R> l -> Either.left(l.value());
};
}
default <U> Either<L, U> flatMap(Function<R, Either<L, U>> fn) {
return switch (this) {
case Right<L, R> r -> fn.apply(r.value());
case Left<L, R> l -> Either.left(l.value());
};
}
}
// Railway-Oriented Programming
public Either<String, Order> processOrder(OrderRequest request) {
return validateRequest(request) // Either<String, OrderRequest>
.flatMap(this::checkInventory) // Either<String, OrderRequest>
.flatMap(this::calculatePrice) // Either<String, PricedOrder>
.flatMap(this::applyDiscount) // Either<String, PricedOrder>
.map(this::createOrder); // Either<String, Order>
}
// 4. Optional 체이닝으로 null 안전한 파이프라인
public String getUserCity(long userId) {
return findUser(userId)
.flatMap(User::getAddress)
.map(Address::getCity)
.map(String::toUpperCase)
.orElse("알 수 없음");
}
활용 팁
- 모든 코드를 함수형으로 작성할 필요는 없습니다. 데이터 변환, 유효성 검사, 에러 처리 등 특정 영역에서 선택적으로 적용하세요.
- Function 합성은 전략 패턴(Strategy Pattern)을 대체할 수 있으며, 더 유연하고 테스트하기 쉽습니다.
- Optional을 필드 타입이나 메서드 매개변수로 사용하는 것은 안티패턴입니다. 반환 타입으로만 사용하세요.
- Either 패턴은 checked exception의 대안으로, 에러를 값으로 다루어 합성 가능한 에러 처리를 구현합니다.
- Java의 함수형 인터페이스(
Function,Predicate,Consumer,Supplier)를 숙지하고 적극 활용하세요.
마무리
Java에서의 함수형 프로그래밍은 객체지향과 대립하는 것이 아니라, 보완하는 도구입니다. Record와 Sealed Class의 도입으로 대수적 데이터 타입이 가능해지고, 패턴 매칭으로 타입 안전한 분기가 가능해지면서 Java의 함수형 표현력은 크게 향상되었습니다. 핵심은 적재적소에 활용하는 것이며, 팀의 이해도와 코드베이스의 일관성을 고려하여 점진적으로 도입하는 것을 권장합니다.