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!");
}
}
주요 구성 요소
- RouterFunction: 특정 경로와 HTTP 메서드에 대해 어떤 핸들러가 처리할지를 정의한다.
- 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);
반응형