Published on

Chapter 18. 함수형 관점으로 생각하기

Chapter 18. 함수형 관점으로 생각하기

18.1 시스템 구현과 유지보수

프로그램을 쉽게 유지보수할 수 있으려면 시스템의 구조를 이해하기 쉽게 클래스 계층으로 반영한다면 좋을 것입니다.

시스템의 각 부분의 상호 의존성을 가리키는 결합성과 시스템의 다양한 부분이 서로 어떤 관계를 갖는지 가리키는 응집성으로 프로그램 구조를 평가할 수 있습니다.

그러나 실질적으로 유지보수 중 코드 크래시 디버깅 문제를 가장 많이 겪게 됩니다.

코드 크래시는 예상치 못한 변숫값 때문에 발생할 수 있습니다.

함수형 프로그래밍이 제공하는 부작용 없음(no side effect)불변성(immutability) 개념이 이 문제를 해결하는데 많은 도움을 줍니다.

실제로 경험해 본 적없는 대규모 소프트웨어 시스템 업그레이드 관리를 요청 받았을 때, synchronized라는 키워드가 발견된다면 제안을 거절하길 바랍니다.

동시성 버그를 고치는 일은 정말 어렵기 때문입니다.

18.1.1 공유된 가변 데이터

변수가 예상하지 못한 값을 갖는 이유는 여러 메소드에서 공유된 가변 데이터 구조를 읽고 갱신하기 때문입니다.

여러 곳에서 가변 데이터를 읽고 갱신한다면 어디에서 갱신이 일어났는지 추적하기가 어렵습니다.

다른 객체의 상태를 바꾸지 않으며 return문을 통해서만 자신의 결과를 반환하는 메소드를 순수 메소드 또는 부작용 없는 메소드라고 합니다.

여기서 부작용이란 함수 내에 포함되지 못한 기능을 의미합니다.

  • 자료구조를 고치거나 필드에 값을 할당
  • 예외 발생
  • 파일에 쓰기 등의 I/O 동작 수행 등

불변 객체를 이용해서 부작용을 없앨 수 있습니다.

불변 객체는 복사하지 않고 공유할 수 있으며, 상태를 바꿀 수 없기 때문에 스레드 안정성을 제공합니다.

18.1.2 선언형 프로그래밍

  • 명령형 프로그래밍: 어떻게에 집중하는 프로그래밍 형식
public class Chap18 {

	@Test
	void test() {
		List<Integer> numbers = List.of(1, 2, 3, 4, 5);
		int max = Integer.MIN_VALUE;
		for (Integer number : numbers) {
			if (max < number) max = number;
		}
		System.out.println(max);
	}
}
  • 선언형 프로그래밍: 무엇을에 집중하는 프로그래밍 형식
public class Chap18 {

	@Test
	void test() {
		int max = IntStream.of(1, 2, 3, 4, 5)
				.max()
				.orElse(0);
		System.out.println(max);
	}
}

18.1.3 왜 함수형 프로그래밍인가?

함수형 프로그래밍선언형 프로그래밍을 따르는 대표적인 방식이며 부작용이 없는 계산을 지향합니다.

선언형 프로그래밍과 부작용을 멀리 한다는 두 가지 개념은 좀 더 쉽게 시스템을 구현 하고 유지보수하는 데 도움을 줍니다.

18.2 함수형 프로그래밍이란 무엇인가?

함수형 프로그래밍은 당연하게도 함수를 이용하는 프로그래밍 방식입니다.

여기서 함수는 수학적인 함수(0개 이상의 인수를 가지며, 한 개 이상의 결과를 반환하지만 부작용이 없어야 함)와 동일합니다.

  • 순수 함수형 프로그래밍: 함수, if-then-else 등의수학적 표현만 사용
  • 함수형 프로그래밍: 시스템의 다른 부분에 영향을 미치지 않는다면 내부적으로는 함수형이 아닌 기능도 사용

18.2.1 함수형 자바

실질적으로 자바로는 완벽한 순수 함수형 프로그래밍을 구현하기 어렵습니다.

I/O 모델 자체에는 부작용 메소드가 포함됩니다. 하지만 시스템의 컴포넌트가 순수한 함수형인 것처럼 동작하도록 코르르 구현할 수는 있습니다.

함수나 메소드는 지역 변수만을 변경해야 함수형이라 할 수 있습니다. 그리고 함수나 메소드에서 참조하는 객체가 있다면 해당 객체는 불변 객체여야 하며, 모든 필드는 상수(final) 여야합니다.

또한, 함수나 메소드가 어떤 예외도 일으키지 않아야 합니다. 예외가 발생하면 return으로 결과를 반환할 수 없기 때문입니다.

에외가 발생한다면 Optional를 사용하여 요청이 성공적으로 처리되었는지 확인하여 해결할 수 있습니다.
또는, 다른 컴포넌트에 영향을 미치지 않도록 지역적으로만 예외를 사용하는 방법을 고려할 수도 있습니다.

함수형 에서는 비항수형 동작을 감출 수 있는 상황에서만 부작용을 포함하는 라이브러리를 사용해야 합니다.

즉, 먼저 자료구조를 복사한다든가 발생할 수 있는 예제를 적절하게 내부적으로 처리하여 자료구조의 변경을 호출자가 알 수 없도록 감춰야합니다.

18.2.2 참조 투명성

참조 투명성: 같은 인수로 함수를 호출했을 때 항상 가튼 결과를 반환

부작용을 감춰야 한다라는 제약은 참조 투명성 개념으로 귀결됩니다.

List를 반환하는 메소드를 두 번 호출하는 경우 List의 요소는 같으나 각각 독립적인 객체일 수 있습니다.

  • 가변 객체일 경우: 참조적으로 투명하지 않음
  • 불변 객체일 경우: 순수 값으로만 사용되면 두 객체는 같은 객체라고 볼 수 있으므로 참조적으로 투명한 것으로 간주

18.2.3 객체지향 프로그래밍과 함수형 프로그래밍

Java 8은 함수형 프로그래밍을 익스트림 객체지향 프로그래밍의 일종으로 간주합니다.

  • 익스트림 객체지향 방식: 모든 것을 객체로 간주하고 프로그램이 객체의 필드를 갱신하고, 메소드를 호출하고, 관련 객체를 갱신하는 방식으로 동작
  • 함수형 프로그래밍: 참조 투명성을 중시하는, 즉 변화는 허용하지 않는 방식

18.3 재귀와 반복

for, while같은 반복문을 사용해도 지역변수만 갱신한다면 안정성이 보장되니 괜찮습니다.

하지만, 외부 객체를 변경하는 경우라면 문제가 생길 수 있습니다.

재귀를 사용하여 이런 부분을 해결할 수 있습니다.

  • 일반 반복
public class Chap18 {

	@Test
	void test() {
		factorialIterative(10);
	}

	int factorialIterative(int n) {
		int r = 1;
		for (int i = 1; i <= n; i++) {
			r *= i;
		}
		return r;
	}
}
  • 재귀
public class Chap18 {

	@Test
	void test() {
		factorialIterative(10);
	}

	int factorialIterative(int n) {
		return n == 1 ? 1 : n * factorialIterative(n - 1);
	}
}

무조건 반복보다 재귀가 좋다고 주장하는 함수형 프로그래밍 광신도의 주장은 주의해야 합니다.

재귀 코드는 호출 할 때마다 호출 스택에 각 호출시 생성되는 정보를 저장할 새로운 스택 프레임을 만들어야 하기 때문에 반복 코드보다 비용이 더 듭니다.

  • 꼬리 재귀
public class Chap18 {

	@Test
	void test() {
		factorialIterative(10);
	}

	long factorialIterative(long n) {
		return factorialHelper(1, n);
	}

	private long factorialHelper(long acc, long n) {
		return n == 1 ? acc : factorialHelper(acc * n, n - 1);
	}
}

재귀 호출이 가장 마지막에 이루어지므로 꼬리 재귀입니다.

  • 재귀: 중간 결과를 각각의 스택 프레임으로 저장
  • 꼬리 재귀: 컴파일러가 하나의 스택 프레임을 재활용할 가능성이 생김

하지만 자바는 이와 같은 최적화를 제공하지 않습니다.(???)

최신 JVM 언어(스칼라, 그루비)는 이와 같은 재귀를 반복으로 변환하는 최적화를 제공합니다.

그렇기 때문에 최적화 여지를 남겨둘 수 있는 꼬리 재귀를 적용하는 것이 좋습니다.

18.4 마치며

  • 공유된 가변 자료구조를 줄이는 것은 장기적으로 프로그램을 유지보수 하고 디버깅 하는데 도움 됨
  • 함수형 프로그래밍은 부작용이 없는 메소드와 선언형 프로그래밍 방식을 지향
  • 함수형 메소드는 입력 인수와 출력 결과만을 갖는다.
  • 같은 인수값으로 함수를 호출했을 때 항상 같은 값을 반환하면 참조 투명성을 갖는 함수다.
  • while, for같은 반복문은 재귀로 대체할 수 있다.
  • 자바에서는 고전 방식의 재귀보다는 꼬리 재귀사용해야 추가적인 컴파일러 최적화를 기대할 수 있음.