Spring + Java

[Spring] 함수형 프로그래밍이란?

Stark97 2023. 8. 9. 16:15
반응형
 
 
 

함수형 프로그래밍에 대해 알아보자.

 

1. 함수형 프로그래밍이란?

함수형 프로그래밍은 수학의 함수 개념을 프로그래밍에 적용한 패러다임이다. 이 패러다임에서는 순수 함수, 불변성, 고차 함수 등을 중시한다. 간단히 말해, 상태와 데이터를 변경하지 않고 함수를 조합하여 로직을 구성하는 방식이다.

 

  • 순수 함수: 동일한 입력에 대해 항상 동일한 출력을 반환하며, 외부 상태에 의존하거나 변경하지 않는다.
  • 불변성: 데이터는 변경되지 않고, 새로운 데이터가 생성된다.
  • 고차 함수: 함수를 인자로 받거나 함수를 반환하는 함수다.

 

순수 함수 (Pure Function)

  • 순수 함수는 동일한 입력에 대해 항상 동일한 출력을 반환하며, 외부 상태에 의존하거나 변경하지 않는다. 이는 함수의 예측 가능성을 높이고, 테스트를 용이하게 만든다.
// 순수 함수 예제: 두 수의 합을 반환
public int add(int a, int b) {
    return a + b;
}

// 사용 예
int result1 = add(2, 3); // 결과: 5
int result2 = add(2, 3); // 결과: 5
  • 입력에만 의존: 함수 외부의 상태나 변수를 참조하지 않는다.
  • 부작용 없음: 함수 실행이 외부 상태를 변경하지 않는다.

불변성 (Immutability)

  • 불변성은 데이터가 한 번 생성되면 변경되지 않는 특성을 의미한다. 데이터를 변경해야 할 경우, 기존 데이터를 수정하는 대신 새로운 데이터를 생성한다.
public final class User {

    private final String name;
    private final int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 게터만 제공, 세터 없음
    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    // 새로운 User 객체 생성 메서드
    public User withAge(int newAge) {
        return new User(this.name, newAge);
    }
    
}

// 사용 예시
User user1 = new User("Alice", 30);
User user2 = user1.withAge(31);

System.out.println(user1.getAge()); // 출력: 30
System.out.println(user2.getAge()); // 출력: 31
  • 데이터 변경 불가: 객체의 상태를 변경할 수 없다.
  • 새로운 인스턴스 생성: 변경이 필요할 경우 새로운 객체를 생성한다.

고차 함수 (Higher-Order Function)

  • 고차 함수는 함수를 인자로 받거나 함수를 반환하는 함수를 말한다. 이를 통해 함수의 재사용성과 모듈성을 높일 수 있다.
import java.util.function.Function;

// 함수를 인자로 받는 고차 함수 예제
public void applyFunction(int value, Function<Integer, Integer> func) {
    int result = func.apply(value);
    System.out.println("Result: " + result);
}

// 함수를 반환하는 고차 함수 예제
public Function<Integer, Integer> multiplyBy(int factor) {
    return x -> x * factor;
}

// 사용 예
public static void main(String[] args) {
    applyFunction(5, x -> x * 2); // 출력: Result: 10

    Function<Integer, Integer> multiplyBy3 = multiplyBy(3);
    System.out.println(multiplyBy3.apply(5)); // 출력: 15
}
  • 함수 인자로 사용: 함수를 다른 함수의 인자로 전달할 수 있다.
  • 함수 반환: 함수가 다른 함수를 반환할 수 있다.
  • 코드 재사용성: 다양한 함수 조합을 통해 유연한 로직 구성이 가능하다.

 

2. Java에서의 함수형 프로그래밍 기초

Java 8부터 함수형 프로그래밍을 지원하는 다양한 기능이 도입되었다.

 

람다 표현식

  • 람다 표현식은 익명 함수를 간결하게 작성할 수 있게 해 준다. 이를 통해 코드의 가독성과 간결성이 크게 향상된다.
// 기존 방식: 익명 클래스
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Collections.sort(names, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.compareTo(b);
    }
});

// 람다 표현식 사용
Collections.sort(names, (a, b) -> a.compareTo(b));

스트림(Stream) API

  • 스트림 API는 컬렉션을 선언적으로 처리할 수 있는 기능을 제공한다. 데이터를 필터링, 매핑, 정렬, 집계 등의 작업을 간편하게 수행할 수 있다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

// 짝수만 필터링하고, 각 값을 제곱하여 합계를 계산
int sum = numbers.stream()
                 .filter(n -> n % 2 == 0)
                 .map(n -> n * n)
                 .reduce(0, Integer::sum);

System.out.println("합계: " + sum); // 출력: 합계: 20

Optional 클래스

  • Optional 클래스는 null 값을 안전하게 처리할 수 있게 도와준다. 이를 통해 NullPointerException을 예방하고, 더 명확한 의도를 표현할 수 있다.
public Optional<String> getUserEmail(String userId) {
    User user = userRepository.findById(userId);
    return Optional.ofNullable(user).map(User::getEmail);
}

// 사용 예
getUserEmail("user123")
    .ifPresent(email -> System.out.println("이메일: " + email));

CompletableFuture

  • CompletableFuture는 비동기 프로그래밍을 지원하는 클래스다. 비동기 작업을 체이닝 하고, 결합하는 등의 작업을 함수형 방식으로 수행할 수 있다.
CompletableFuture.supplyAsync(() -> {
    // 비동기 작업
    return "Hello";
})
.thenApply(greeting -> greeting + ", World!")
.thenAccept(System.out::println); // 출력: Hello, World!

 

3. Spring에서의 함수형 프로그래밍 활용

Spring 프레임워크는 함수형 프로그래밍을 효과적으로 활용할 수 있는 다양한 기능을 제공한다.

 

함수형 웹 프로그래밍

  • Spring 5부터는 함수형 웹 프로그래밍을 지원하여, 전통적인 애노테이션 기반의 방식 대신 라우터와 핸들러를 사용하여 웹 애플리케이션을 구성할 수 있다.

  • 전통적인 스프링 웹 애플리케이션은 @Controller나 @RestController와 같은 애노테이션을 사용하여 요청을 처리한다. 반면, 함수형 웹 프로그래밍은 라우터(Router)와 핸들러(Handler)를 사용하여 요청과 응답을 처리한다. 이는 보다 선언적이고 모듈화 된 방식으로 웹 애플리케이션을 구성할 수 있게 해 준다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;

import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

@Configuration
public class RouterConfig {

    @Bean
    public RouterFunction<ServerResponse> route() {
        return route(GET("/hello"), this::helloHandler);
    }

    public Mono<ServerResponse> helloHandler(org.springframework.web.reactive.function.server.ServerRequest request) {
        return ServerResponse.ok().bodyValue("Hello, World!");
    }
    
}

주요 구성 요소

  1. RouterFunction: 특정 경로와 HTTP 메서드에 대해 어떤 핸들러가 처리할지를 정의한다.
  2. HandlerFunction: 실제로 요청을 처리하고 응답을 생성하는 함수다.

함수형 빈 등록

  • Spring Boot에서는 함수형 스타일로 빈을 등록할 수 있다. 이를 통해 더 선언적이고 간결한 구성이 가능하다.
@Configuration
public class AppConfig {

    @Bean
    public UserService userService() {
        return new UserServiceImpl();
    }

    @Bean
    public UserRepository userRepository() {
        return new UserRepositoryImpl();
    }
    
}
  • Kotlin을 사용하면 더욱 간결하게 작성할 수 있다.
@Bean
fun userService() = UserServiceImpl()

@Bean
fun userRepository() = UserRepositoryImpl()

리액티브 프로그래밍 with WebFlux

  • Spring WebFlux는 리액티브 프로그래밍을 지원하여, 논블로킹 방식으로 고성능 애플리케이션을 구축할 수 있다. Reactor 라이브러리를 기반으로 하며, 함수형 스타일로 데이터를 처리한다.
@RestController
public class ReactiveController {

    @GetMapping("/flux")
    public Flux<String> getFlux() {
        return Flux.just("Spring", "Java", "Reactor")
                   .delayElements(Duration.ofSeconds(1));
    }

    @GetMapping("/mono")
    public Mono<String> getMono() {
        return Mono.just("Hello, WebFlux!");
    }
    
}

함수형 웹 프로그래밍 vs. 애노테이션 기반 프로그래밍

항목 애노테이션 기반 함수형 웹 프로그래밍
구성 방식 클래스와 메서드에 애노테이션을 사용하여 매핑 라우터와 핸들러 함수를 사용하여 매핑
유연성 상대적으로 덜 유연할 수 있음 더 선언적이고 유연하게 구성 가능
코드 구조 컨트롤러 클래스를 중심으로 구성됨 라우터와 핸들러 함수로 명확히 분리
테스트 용이성 애노테이션과 스프링 컨텍스트에 의존적임 함수 단위로 테스트 가능

 

4. 함수형 프로그래밍의 장점

코드 간결성

  • 함수형 프로그래밍은 람다 표현식과 스트림 API 등을 통해 코드를 간결하게 작성할 수 있게 해 준다. 이는 가독성을 높이고, 개발 생산성을 향상시킨다.
// 전통적인 for 루프
List<String> filtered = new ArrayList<>();
for (String name : names) {
    if (name.startsWith("A")) {
        filtered.add(name);
    }
}

// 스트림 API 사용
List<String> filtered = names.stream()
                             .filter(name -> name.startsWith("A"))
                             .collect(Collectors.toList());

병렬 처리의 용이성

  • 함수형 프로그래밍은 불변성과 순수 함수의 특성 덕분에 병렬 처리가 용이하다. 스트림 API에서는 parallelStream()을 사용하여 간단히 병렬 처리를 적용할 수 있다.
int sum = numbers.parallelStream()
                 .map(n -> n * 2)
                 .reduce(0, Integer::sum);

유지보수성 향상

  • 작은 단위의 함수로 로직을 분리함으로써 코드의 모듈화가 촉진된다. 이는 테스트 용이성을 높이고, 코드의 재사용성을 향상시킨다.
// 순수 함수 예제
public int add(int a, int b) {
    return a + b;
}

// 고차 함수 예제
public void processNumbers(List<Integer> numbers, Function<Integer, Integer> processor) {
    numbers.stream()
           .map(processor)
           .forEach(System.out::println);
}

// 사용 예
processNumbers(numbers, this::addOne);

 

반응형