생각해보기
모던 자바 인 액션 -16- 본문
시스템 구현과 유지보수
유지보수를 할 때 변수가 예상치 못한 값을 가지는 실수를 할 수 있다. 이는 유지 보수하는 시스템의 여러 메서드에서 공유된 가변 데이터 구조를 읽고 갱신하기 때문이다. 공유된 가변 데이터 구조는 프로그램 전체에서 데이터 갱신 사실을 추적하기 어려워 진다.
이와 반대로 자신을 포함하는 클래스의 상태 그리고 다른 객체의 상태를 바꾸지 않으면서 return 문을 통해서 자신의 결과를 반환하는 메서드를 순수메서드 또는 부작용 없는 메서드라고 부른다.
부작용은 함수내에 포함되지 못한 기능, 즉 변경이 가능한 상태를 가진 것을 부작용이라고 부른다
부작용의 예
- 자료구조를 고치거나 필드에 값을 할당
- 예외 발생
- 파일에 쓰기 등의 I/O 동작 수행
선언형 프로그래밍
명령헝 프로그래밍이란 문제 해결을 위해 How에 집중하는 방식이다. 조건문과 반복문(외부 순회) 등을 통해 작성하는 명령어가 컴퓨터의 저수준 언어와 유사하다.
반면 선언형 프로그래밍이란 What에 초점을 맞춘 방식으로서, 대표적으로 Stream API(내부 순회)를 통한 질의문 코드가 있다. 질의문 자체로 개발자가 원하는 것이 무엇이며 어떻게 해당 목표를 달성할지 등의 규칙을 명확하게 보여준다
함수형 프로그래밍은 선언형 프로그래밍을 따르는 방식으로 부작용 없는 계산을 지향한다. 이런 방식을 통해 자연스럽게 읽고 쓸 수 있는 코드를 구현할 수 있다.
함수형 프로그래밍이란
함수형 프로그래밍이란 함수란 수학적인 함수와 같다. 0개 이상의 인수를 가지며, 한개 이상의 결과를 반환하지만 부작용이 없어야 한다.
함수형 자바
Java에서 완벽한 순수 함수형 프로그래밍을 구현하기 어렵다. 따라서 순수 함수형이 아닌 함수형 프로그램을 구현한다. 실제로는 부작용이 있더라도 외부 호출자가 이를 보지 못하게 함으로써 함수형을 달성할 수 있다.
- 함수나 메서드는 지역 변수만을 변경해야 함수형이라고 할 수 있다
- 참조하는 객체가 있다면 그 객체는 불변 객체이어야 한다(필드 final)
- 함수형 매서드 내에서 생성한 객체의 필드는 갱신할 수 있다,
- 단, 새로 생성한 객체의 필드 갱신은 외부에 노출하지 않아야하며, 메서드 호출은 멱등해야한다.
- 함수형 이라면 함수나 메서드가 어떤 예외도 일으키지 않아야 한다.
- 예외가 발생하면 return으로 결과를 반환할 수 없으면 이는 수학적 함수 활용에 큰 제약을 가진다
- 예외를 사용하지 않고 값을 도출하기 위해 Optional을 사용할 수 있다
- 예외를 내부에서 처리해서 호출자가 모르게 감출 수 있다
- 비 함수형 동작을 감출 수 있는 상황에서만 부작용이 존재하는 라이브러리 함수를 사용해야 한다
- 배열 자체를 바꾸는 sort 함수 등은 배열을 복사후에 복사된 배열에서 내부적으로 사용해야 한다
참조 투명성
같은 인수로 함수를 호출 했을 때 항상 같은 결과를 반환한다면 참조적으로 투명한 함수라고 한다.(멱등)
- Random.nextInt 같은 함수는 항상 같은 값을 반환하지 않기 때문에 참조 투명성을 위해 한다
- 참조 투명성은 비싸거나 오래 시간이 걸리는 연산을 기억화 또는 캐싱을 통해 최적화 기능을 제공한다
- List 반환하는 함수를 두 번 호출했을 때 주소값이 다른 리스트가 반환 될 경우
- List가 불변 객체라면 참조 투명성을 지킨 것이다.
- List가 가변 객체라면 참조 투명성을 위배 한 것이다.
재귀와 반복
순수 함수형 프로그래밍은 반복문을 포함하지 않는다. 반복문을 잘못 사용하면 변화가 코드에 스며들 수 있기 때문이다.
따라서 재귀를 이용하여 코드를 구현 한다.
재귀 방식의 팩토리얼
static long factorialRecursive(long n){
return n==1 ? 1 : n*factorialRecursive(n-1);
}
일반적으로 반복 코드보다 재귀 코드가 비용 소모가 크다
factorialRecursive 함수를 호출할 때마다 호출 스택에 각 호출시 생성되는 정보를 저장할 새로운 스택 프레임이 생긴다. 새로운 스택 프레임에는 호출된 매서드의 매개변수, 결과값, 리턴됐을 때 돌아갈 위치 등이 저장된다.
재귀함수는 연속적으로 함수를 호출하기 때문에 스택에 메모리가 쌓이게 되고 잦은 점프의 반복으로 인해 성능이 저하된다.
꼬리 재귀 최적화
꼬리 재귀는 재귀 호출이 끝난 후 현재 함수에서 추가 연산을 요구하지 않도록 구현하는 재귀의 형태다. 이를 통해 기존의 재귀 방식을 최적화할 수 있다.
꼬리 재귀 방식의 팩토리얼
static long factorialTailRecursive(long n){
return factorialHelper(1,n);
}
static long factorialHelper(long acc, long n){
return n==1 : acc : factorialHelper(acc*n, n-1);
}
꼬리 재귀의 경우 중간 결과를 각각의 스택 프레임으로 저장해야 하는 일반 재귀와 달리 꼬리 재귀에서는 컴파일러가 하나의 스택 프레임을 재활용할 가능성이 생긴다.
재귀와 꼬리 재귀
- Java 컴파일러는 꼬리 재귀 최적화를 지원하지 않으나 Scala 등 모던 JVM 언어는 이를 지원한다.
- 반복 대신 스트림 혹은 꼬리 재귀를 사용함으로써 변화를 피하고 부작용을 없앨 수 있다.
함수형 프로그래밍 기법
일급 함수란 일반값처럼 취급할 수 있는 함수를 의미한다. 함수를 마치 일반값 처럼 사용해서 인수로 전달하거나, 결과로반환받거나, 자료 구조에 저장할 수 있다. Java에서 람다 표현식 혹은 메서드 참조 등을 통해 메서드를 함수 값으로 사용할 수 있다.
고차원 함수
다음중 하나 이상의 동작을 수행하는 함수가 고차원 함수라고 부른다
- 하나 이상의 함수를 인수로 받음
- 함수를 결과로 반환
따라서 Comparator.comparing()이나 Function.andThen() 등이 고차원 함수로 분류된다.
- 스트림 연산이나 고차원 함수에 전달하는 함수는 부작용이 없어야 한다.
- 부작용이 존재하는 함수는 부정확한 결과가 발생하거나 스레드 경쟁 상태로 인해 예상하지 못한 결과가 발생할 수 있다.
- 고차원 함수를 구현할 때 인수가 부작용을 포함할 가능성을 염두에 두어야 한다.
- 함수를 인수로 받아 사용하면 작업 수행 및 프로그램 상태 변화 등을 정확하게 예측하기 어렵다.
- 인수로 전달된 함수가 어떤 부작용을 포함하게 될지 문서화하는 것이 좋다
커링
커링(Currying)이란 x와 y라는 두 인수를 받는 함수 f를 한 개의 인수를 받는 g라는 함수로 대체하는 기법이다. g라는 함수 역시 하나의 인수를 받는 함수를 반환한다. 즉 f(x, y) = (g(x))(y)가 성립한다.
영속 자료구조
함수형 프로그래밍에서는 함수형 자료구조, 불변 자료구조를 보통 영속 자료구조라고 칭한다(DB에서 프로그램 종료후에도 남아있음을 뜻 하는 영속과 다른 의미). 함수형 메서드는 전역 자료구조나 인수로 전달된 자료구조를 업데이트할 수 없다. 자료구조를 변경한다면 같은 메서드를 두 번 호출했을 때 결과가 달라져 참조 투명성을 위배하게 된다.
함수형 프로그램에서는 자료구조를 변경하는 대신 새로운 자료구조를 만들어서 사용한다
스트림과 게으른 평가
게으른 평가란 특정한 작업이 일어나야 하는 상황에서만 스트림을 평가하는 것을 의미한다. 스트림에서는 종단 연산이 적용하는 시점에 실제 연산이 이루어진다. 스트림은 단 한번만 소비할 수 있다.
기타
- 패턴 매칭은 자료형을 언랩하는 함수형 기능이다. 자바의 switch문을 일반화 할 수 있다
- 참조 투명성을 유지하는 상황에서는 계산 결과를 캐시할 수 있다
- 콤비네이터는 둘 이상의 함수나 자료구조를 조합하는 함수형 개념이다
'자바' 카테고리의 다른 글
이펙티브 자바 -2- (0) | 2021.12.30 |
---|---|
이펙티브 자바 -1- (0) | 2021.12.29 |
모던 자바 인 액션 -15- (0) | 2021.12.27 |
모던 자바 인 액션 -14- (0) | 2021.12.27 |
모던 자바 인 액션 -13- (0) | 2021.12.21 |