먼저 함수형 프로그래밍에 대해서 이야기를 하기 전에 프로그래밍 패러다임에 대해서 이야기를 해보자.
패러다임
먼저 패러다임의 정의는 다음과 같다.
어떤 한 시대 사람들의 견해나 사고를 근본적으로 규정하고 있는 테두리로서의 인식의 체계, 또는 사물에 대한 이론적인 틀이나 체계를 의미하는 개념
프로그래밍 패러다임
그럼 프로그래밍 패러다임(Programming Paradigm)은 무엇일까?
오브젝트의 저자이신 조영호님은 프로그래밍 패러다임을 다음과 같이 정의한다.
특정 시대에 수용된 프로그래밍 방법과 문제 해결 방법, 프로그래밍 스타일
우리가 어떤 프로그래밍 패러다임을 사용하느냐에 따라 우리가 해결할 문제를 바라보는 방식과 프로그램을 작성하는 방식이 달라진다. 즉, 새로운 프로그래밍 패러다임이 등장하면 새로운 방식으로 생각하는 법을 배우게 되고, 이를 바탕으로 코드를 작성하게 된다는 것이다.
최근에는 프로그래밍 패러다임은 크게 다음과 같이 구분을 한다고 한다.
명령형 프로그래밍
무엇(What)을 할 것인지를 나타내기보다 어떻게(How) 할 것인지를 설명하는 방식이다. 종류로는 크게 2가지가 존재한다.
1. 절차지향 프로그래밍 : 수행되어야 할 순차적인 처리 과정을 포함하는 방식
2. 객체지향 프로그래밍 : 객체들의 집합으로 프로그램의 상호작용을 표현
선언형 프로그래밍
어떻게 할것인지(How)를 나타내기보다 무엇(What)을 할 것인지를 설명하는 방식
1. 함수형 프로그래밍 : 순수 함수를 조합하고 소프트웨어를 만드는 방식이다.
이제 이어서 함수형 프로그래밍(Functional Programming)이 무엇인지 자세히 들여다보자.
함수형 프로그래밍
패러다임의 정의처럼, 어떤 견해나 사고 이런 규정들은 무엇인가 불편하기에 깨지고 발견되기 마련이다. 함수형 프로그래밍은 어떤 이유로 갑자기 등장하게 된 것일까?
함수형 프로그래밍의 등장
기존에 명령형 프로그래밍을 기반으로 개발하던 개발자들은 소프트웨어의 크기가 커짐에 따라, 복잡하게 엉켜있는 스파게티 코드를 유지보수 하기 힘들다는 것을 깨닫게 되었다. 그리고 이를 해결하기 위해 어떠한 프로그래밍 방식에 대해서 고민을 하게 되었고, 단순히 함수를 이용해 모든 것을 순수 함수로 만들어 나누어 문제를 해결하는 기법을 생각해내게 되었다. 따라서, 함수형 프로그래밍을 사용하게 되면 작은 문제를 해결하기 위한 함수를 작성해서 가독성과 유지보수를 용이하게 해준다고 한다.
코딩계의 밥 아저씨인 로버트 마틴(Robert C.Martin)은 함수형 프로그래밍을 대입문이 없는 프로그래밍이라고 정의한다.
그동안 명령형 프로그래밍을 사용하던 사람들은 대입문이 없는 프로그래밍이라는 얘기 자체가 이해가 되질 않을 것이다. 거의 대부분의 일을 수행하기 위해서는 대입문이 필수인데, 도대체 무슨 얘기일까? 다음 두 가지 예시를 한 번 살펴보자.
// 1. 명령형 프로그래밍
for(int i = 1; i < 10; i++) {
System.out.println(i);
}
// 2. 함수형 프로그래밍
process(10, print(num));
명령형 프로그래밍을 사용하던 사람이라면 아마 다음과 같이 해석할 지도 모르겠다. 아~ print(num) 이 뭔가 인자를 반환하나보다~ 하지만 놀랍게도 전혀 아니다. print(num)은 단순히 명령형 프로그래밍의 예시처럼 출력을 해주기 위한 메서드이다. 즉, 반환값이 없는 실행되어야 할 함수 자체를 인자로 넘긴 것이다. 이러한 방식이 바로 함수형 프로그래밍이다.
위에서 얘기했듯이 함수형 프로그래밍은 무엇을(What) 에 초점을 맞춘다고 하였다. 이런 관점에서 위의 함수형 프로그래밍으로 작성된 수도코드를 해석해보면 두 번째 print(num) 함수는 첫 번째 인자 만큼 iteration을 돌면서 출력하라 를 의미를 가진 함수이다. 그리고 이를 가능하게 하는 것이 함수형 프로그래밍의 기본 원리인 1급 시민(First-Class Citizen) 또는 1급 객체(First-Class Object)로 관리하는 특징 때문이다.
그럼 이 1급 시민 혹은 1급 객체가 무엇일까? 이에 대해서 한 번 살펴보자.
일급 객체란 ?
여기서 일급 이란 뭔가 혜택을 받는다는 것을 의미하는 것이 아니라, 사용할 때 다른 요소들과 아무런 차별이 없다. 라는 것을 의미한다.
그리고 일급 객체는 다음과 같은 3가지 조건을 충족한 객체를 일컫는다.
- 모든 일급 객체는 변수나 데이터에 담을 수 있어야 한다.
- 모든 일급 객체는 함수의 파라미터로 전달 할 수 있어야 한다.
- 모든 일급 객체는 함수의 리턴값으로 사용할 수 있어야 한다.
먼저 결론부터 듣고 자세히 알아보자. 자바의 메서드가 함수형 프로그래밍이 될 수 없는 이유는 위와 같은 조건을 잘 생각해보자.
1. 메서드를 변수에 담을 수 있는가? 그렇지 않다. 여기서 메서드를 담는 다는 의미는 반환 값이 없는 단순한 함수를 담을 수 있냐는 이야기이다.
2. 메서드 자체를 파라미터로 전달 할 수 있는가? 이것도 반환 값이 있다면 가능 할 수도 있겠지만 반환 값이 없는 단순환 함수는 전달 할 수 없다.
3. 함수의 리턴값으로 함수를 사용할 수 있는가? 이것 또한 반환타입이 있다면 가능하겠지만 일반적으로 불가능하다.
하지만 우리는 일급 객체로 된 코드를 자바스크립트에서 손 쉽게 찾아 볼 수 있다.
const hello = function() {
console.log("Hello World");
}
위와 같은 코드가 바로 일급 객체로서 함수가 지정된 코드를 의미하는 것이다. 이제 분명한 차이가 눈에 보일 것이다. 만약 자바 코드로 작성한다면 해당 코드를 다음과 같이 작성해야 할 것이다.
public class Main {
public static void hello(){
System.out.println("Hello World");
}
public static void main(String[] args) {
Object a = hello();
}
}
예상하겠지만 해당 코드는 컴파일 에러를 발생시킨다.
함수형 프로그래밍의 특징
함수형 프로그래밍을 한 줄로 요약하면 다음과 같이 말 할 수 있다고 한다.
부수 효과가 없는 순수 함수를 1급 객체로 간주하여 파라미터나 반환값으로 사용할 수 있으며, 참조 투명성을 지킬 수 있다.
1급 객체는 우리가 위에서 한 번 자세히 알아보았다. 그렇다면 도대체 부수효과와 순수함수 그리고 참조 투명성은 무엇을 의미하는 것일까?
부수 효과(Side Effect)
부수 효과는 사전적 정의로는 다음과 같이 얘기한다. 부과적으로 설명된 동작을 더 강조하기 위해 생기는 부작용. 하지만 프로그래밍 관점에서는 다음과 같은 변화 또는 변화가 발생하는 작업들을 얘기한다고 한다.
- 변수의 상태가 변경되는 경우, 이는 객체 내부의 상태에도 동일하게 적용된다.
- 예외나 오류가 발생하여 실행이 중단되는 경우
- 콘솔 또는 파일 I/O 가 발생하는 경우
그럼 이어서 순수함수에 대해서 알아보자.
순수 함수(Pure Function)
바로 위에서 얘기한 부수 효과들을 제거한 함수들을 순수 함수라고 부른다. 우리가 이어서 잠시 예제로 살펴볼 함수형 프로그래밍에서 사용하는 함수들이 바로 이러한 순수 함수들이다.
- Memory or I/O 관점에서 Side Effect가 없는 함수
- 함수의 실행이 외부에 영향을 끼치지 않는 함수
그리고 이러한 순수 함수를 사용했을 때에는 다음과 같은 효과를 얻게 된다.
- 함수 자체가 독립적이며 Side-Effect가 없기 때문에 Thread의 안정성을 보장받을 수 있다. Thread-Safe 하다.
- Thread-safe 하기 때문에 병렬 처리를 동기화 없이 진행할 수 있다.
즉, 함수형 프로그래밍을 사용하는 경우에는 함수 자체가 독립적으로 돌아가기 때문에 멀티 쓰레드 상황에서도 사용이 가능하다는 것을 볼 수 있다. 이어서 마지막으로 참조 투명성에 대해서 알아보자.
참조 투명성(Referential Transparency)
참조 투명성의 정의는 다음과 같다.
- 동일한 인자에 대해 항상 동일한 결과를 반환해야 한다. (멱등성)
- 참주 투명성을 통해 기존의 값은 변경되지 않고 유지된다. (불변성)
사실 이러한 참조 투명성은 부수효과가 존재하지 않으면 자연스럽게 참조 투명성을 지킬 수 있게 된다. 따라서, 명령형 프로그래밍인지 함수형 프로그래밍인지 구분하기 위한 가장 큰 특징은 부수효과가 존재하는지 존재하지 않는지에 따라 결정된다. 여기서 함수형 프로그래밍이 생기게게 된 개발의 핵심 동기를 이해할 수 있다.
부작용을 제거하여 프로그램의 동작을 이해하고 예측을 용이하게 하는 것
자바에서는 위와 같은 부수효과가 빈번하게 발생하여 함수형 프로그래밍을 사용할 수가 없었다. 하지만 JDK 1.8 이후로 자바에는 람다와 Stream이 도입이 되면서 함수형 프로그래밍을 사용 할 수 있게 되었다. 간단한 예시를 하나 살펴보자.
자바 함수형 프로그래밍 예시
@Test
@DisplayName("중간연산_Distinct")
void 중간연산_Distinct() {
list = Arrays.asList("Java", "Scala", "Groovy", "Python", "Go", "Swift", "Java");
long count = list.stream()
.distinct()
.count();
assertThat(list.size() - 1).isEqualTo(count);
}
리스트에서 중복된 값을 제외한 개수를 반환하는 코드이다. 스트림을 사용하지 않았다면 아마 다음과 같이 구현이 가능 할 것이다.
@Test
@DisplayName("중간연산_Distinct")
void 중간연산_Distinct() {
list = Arrays.asList("Java", "Scala", "Groovy", "Python", "Go", "Swift", "Java");
int count = 0;
for(int i = 0; i < list.size(); i++) {
boolean isFlag = true;
for(int j = i + 1; j < list.size(); j++) {
if(list.get(i).equals(list.get(j))) {
isFlag = false;
break;
}
}
if(isFlag)
count++;
}
assertThat(list.size() - 1).isEqualTo(count);
}
위와 아래를 비교해봐도 가독성 차이가 엄청나다는 것을 알 수 있다. 그럼 무조건 스트림을 쓰는 것이 좋은가?라고 하면 꼭 그렇지는 않다. 스트림은 생각보다 연산 속도가 일반적인 연산에 비해서 느리다. 따라서 성능이 중요한 코딩 테스트에서는 사용하지 않는 것이 좋지만, 협업에서는 가독성을 위해 되도록 사용하는 것이 좋다.
참고 자료
https://mangkyu.tistory.com/111
https://ko.wikipedia.org/wiki/%ED%8C%A8%EB%9F%AC%EB%8B%A4%EC%9E%84
조영호. (2019). 오브젝트 (코드로 이해하는 객체지향 설계). 위키북스.
https://inpa.tistory.com/entry/CS-%F0%9F%91%A8%E2%80%8D%F0%9F%92%BB-%EC%9D%BC%EA%B8%89-%EA%B0%9D%EC%B2%B4first-class-object
'Language > Java' 카테고리의 다른 글
Logger에서 hibernate SQL 로그 출력이 안될 경우 (0) | 2023.05.18 |
---|---|
가비지 콜렉션(Garbage Collection) (0) | 2023.04.22 |
JVM (0) | 2023.04.21 |
enum의 Enum 상수 객체의 변수 사용과 생성 (0) | 2023.04.12 |
7-7 내부 클래스 (재업로드), 익명 클래스 (0) | 2023.03.29 |