220121 - 함수형 프로그래밍에 관해

Suntory·2022년 1월 21일
0

TIL

목록 보기
15/57

클로저

클로저(Closure)란?

외부 함수에 접근할 수 있는 내부 함수 혹은 이러한 원리를 일컫는 용어이다. 스코프에 따라서 내부 함수의 범위에서는 외부 함수 범위에 있는 변수에 접근이 가능하지만 그 반대는 실현이 불가능하다는 개념이다. 쉽게 말해 다음과 같은 코드가 가능하다.

사용 예제

코드 출처

public class Store {
	private String storeNo = "9000";
    
    public void lambdaClosure() {
    	Function<String, Integer> lambdaFunction = i -> {
        	System.out.println(this.storeNo);
            	return null;
		};
    }
}

Store 클래스의 메서드인 lambdaClosure는 내부에 lambdaFunction을 하나 가지고 있는데, 이 경우 외부 메서드가 lambdaClosure이고 내부 메서드가 lambdaFunction이다. 이 람다함수 내부에서 this.storeNo라는 인스턴스 필드를 참조하고 있다. 이 경우 자신의 범위 밖에 있는 변수를 사용하고 있기 때문에 클로저라고 한다.

순수 함수

순수 함수란?

일반적인 프로그래밍의 함수는 수학적 함수와 다른 성격을 띤다. 수학의 함수는 input을 넣으면 계산 값이 나오는 형태라면, 프로그래밍의 함수는 그 값이 계산되는 일련의 과정을 담고 있다. 그래서 프로그래밍 함수를 프로시저 또는 메서드라는 이름으로 부른다. 메서드가 담고 있는 계산 절차 도중에 외부의 값을 참조하는 경우가 많다. 이 경우 외부의 값에 따라서 같은 입력값이 들어오더라도 다른 결과를 출력할 수 있다.

이에 따라, 멀티 쓰레드 환경 등 자원에 동시에 접근이 가능한 환경에서는 같은 입력값에 대해서도 어떤 기댓값을 가질지 판단하기 어렵다. 이는 테스트와 디버깅을 어렵게 만드는 요소이다. 그렇기 때문에 함수형 프로그래밍에서는 순수함수라는 개념을 추구한다.

정말 수학에서 사용하는 함수처럼 같은 인자가 들어오면 외부 환경에 관계없이 항상 일정한 값을 출력하는 함수이다. 이렇게 작성한 함수는 부작용이 없다. 함수 외부의 환경을 참조하거나 의존성이 없기 때문이다. 그렇기 때문에 테스트도 용이하고 깔끔한 코드를 만들 수 있다.

사용 예제

// not pure
class example {
	
    public String name = "seyeong"; // 이 필드의 값에 따라 아래 함수의 출력 결과가 바뀐다. 
    
    public String introduce(int age) {
    	return String.format("안녕하세요. 저는 %d살인 %s입니다.", age, name);
    }
    
// pure
    public String introduce(String name, int age) {
        BiFunction<Integer, String, String> introduce =
                (a, n) -> String.format("안녕하세요. 저는 %d살인 %s입니다.", a, n);
        return introduce.apply(age, name);
    }
}        

고차 함수

고차 함수란?

함수를 인수로 받거나 함수를 반환하는 함수를 고차 함수라고 이른다. 중고등학교 때 배웠던 합성함수처럼 함수끼리는 함수를 인자로 받거나 함수를 반환시킬 수 있다.

사용 예제

자바의 stream이나 List 등에서 제공하는 forEach, map, reduce, filter 등이 있다. 실제로 활용하면 데이터 처리 과정을 나타낸 함수를 간결하게 표현할 수 있다.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
	// for문을 활용한 출력
        for (int i = 0; i < numbers.size(); i++) {
        	System.out.println(numbers.get(i));
        }
        
        // forEach 함수를 이용함
     	numbers.forEach(System.out::println);

forEach 등의 고차함수를 이용하면 내부 반복을 이용하여 for문을 숨기고 작업을 추상화할 수 있다. 그렇다면 forEach의 내부 동작은 어떻게 이루어질까?

    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }

java List의 forEach 함수 코드이다. 함수형 인터페이스인 Consumer (인자값이 하나이고, 리턴값은 없는 함수를 표현한다) 를 전달받는다.
(System.out::println)은 뜯어보면 value -> System.out.println(value)와 같다.
즉, 이 람다식이 인자로 전달받는 action에 해당한다.
그 다음 forEach는 단순히 List 객체 내의 원소를 꺼내서 action을 적용시킨다.
이처럼 forEach는 함수를 인자로 받아 데이터의 각 요소에 대해 그 함수를 적용시켜주는 고차 함수이다.

프로그래밍 패러다임이란?

개발자가 코드를 짤 때 가지는 관점이라고 생각한다.
어떤 기능을 구현하기 위해 코드를 짜더라도 다음과 같은 관점을 가질 수 있다.

  • 난 실제로 기능이 구현되는 과정의 흐름대로 코드를 작성할거야 -> 절차지향 프로그래밍
  • 그 기능이 구현되는 데 필요한 데이터들의 동작과 속성은 무엇이 있을까? 이것을 한데 묶어 하나의 객체로 관리하고 그것들을 통해 메시지를 주고받으며 기능을 구현할거야 -> 객체지향 프로그래밍
  • 모든 기능을 독립적으로 잘게 쪼개서 단위 블록으로 만든 다음 필요한 기능에 따라 조합해서 만들거야 -> 함수형 프로그래밍

이러한 패러다임의 필요성은 코드를 짜는 원칙을 제시한다는 것에서 기인한다고 생각한다. 각자의 코딩 스타일이 있지만 여러 개발자가 모여서 개발하는 환경에서는 통일된 원칙이 필요하다.

그리고 그 원칙은 우리가 맞닥뜨리는 문제를 효율적으로 해결할 수 있는 원칙이면 좋을 것이다. 객체지향이나 함수형과 같은 현재 널리 사용되는 패러다임들은 개발을 하면서 마주치는 여러 문제를 해결하기 위해 등장한 방법론들이다.

자주 변경되는 요구사항을 효율적으로 반영하기 위해서, 반복적으로 사용되는 코드를 줄이고 재사용하기 위해서, 코드의 가독성을 높이기 위해 등등 다양한 목적을 가지고 발전해온 형태이기 때문에 패러다임을 지킨다면 코드의 통일성을 높이고 효율적인 코드를 작성할 수 있을 것이라고 생각한다.

객체지향과 함수형

객체지향

  • 객체의 속성과 기능을 한데 모아서 캡슐화한다.
  • 연관있는 객체끼리 상속을 통해 불필요한 코드의 반복은 줄이고 다형성을 확보하여 변화에 유연하게 대응이 가능하다.
  • 객체의 상태를 메서드들이 공유할 수 있다.

함수형 프로그래밍

  • 기능을 순수함수 단위로 쪼개서 관리한다.
  • 간결한 함수간의 연결로 복잡한 기능을 만들고, 내부 로직은 작은 함수들로 추상화한다.
  • 함수 내부의 인자로 받은 값은 불변성을 가지며, 이 인자를 저장하거나 하지 않고, 함수 외부를 참조하지 않는다.

공통점

  • 두 패러다임 모두 중복되는 코드를 줄이고 효율적으로 재사용할 수 있는 단위가 존재한다.
profile
천천히, 하지만 꾸준히 그리고 열심히

0개의 댓글