728x90
함수형 프로그래밍
- 수학 함수를 사용하고 부수 효과(side effect)를 피하는 것이 특징인 프로그래밍 패러다임
- 부수 효과 : 함수에서 결과값을 주는 것 외에 하는 행동
- 순수 함수 : 부수 효과 없이 결괏값이 인자에만 의존하는 함수
- 부수 효과 없이 순수 함수만 사용하는 프로그래밍 스타일
실용적 측면에서의 함수형 프로그래밍
- 부수 효과는 필요하다.
- 상태에 따라 결과가 달라질 필요가 있다.
- 함수형 프로그래밍은 부수 효과를 잘 다룰 수 있다.
- 함수형 프로그래밍은 순수하지 않은 함수도 사용하지만 순수하지 않은 함수를 다룰 수 있는 기술도 많이 있다.
- 함수형 프로그래밍은 실용적이다.
- 함수형 프로그래밍은 객체 지향 프로그래밍이나 절차적 프로그래밍을 다루지 않고 모든 프로그래밍에서 사용가능하다.
함수형 프로그래밍에서 순수하지 않은 함수를 순수 함수로 분리하는 과정
함수형 프로그래밍을 실용적인 관점에서 접근한다면, 모든 함수를 순수 함수로 만들 수는 없다.
대신 비순수 함수를 최대한 작게 만들고 순수한 부분을 분리한다.
액션, 계산, 데이터
액션 (부수 효과)
- 실행 시점이나 횟수에 의존한다.
- 예시
- 반죽 펴기
- 피자 배달
- 도구
- 시간이 지남에 따라 안전하게 상태를 바꿀 수 있는 방법
- 순서를 보장하는 방법
- 액션이 정확히 한번만 실행되게 보장하는 방법
계산 (순수 함수)
- 입력값으로 출력값을 만드는 것
- 같은 입력값을 가지고 계산하면 항상 같은 결괏값이 나온다.
- 멱등성을 가진다.
- 같은 입력값을 가지고 계산하면 항상 같은 결괏값이 나온다.
- 예시
- 쇼핑 목록 결정
- 도구
- 정확성을 위한 정적 분석
- 소프트웨어에서 쓸 수 있는 수학적 지식
- 테스트 전략
데이터
- 이벤트에 대해 기록한 사실
- 예시
- 고객 주문
- 영수증
- 도구
- 효율적으로 접근하기 위해 데이터를 구성하는 방법
- 데이터를 보관하기 위한 기술
- 데이터를 이용해 중요한 것을 발견하는 원칙
데이터, 계산, 액션을 구분하는 이유
요즘 유행하는 분산 시스템에 잘 어울린다.
처리해야하는 메세지가 순서가 섞이거나 중복될 때, 실행 시점이나 횟수에 의존하는 코드를 없애면, 코드를 더 쉽게 이해할수 있고 버그를 막을 수 있다.
데이터와 계산은 실행 시점이나 횟수에 의존하지 않는다.
고려할 점
- 액션 안에는 계산과 데이터, 또 다른 액션이 숨어 있을지도 모른다.
- 계산은 더 작은 계산과 데이터로 나누고 연결할수 있다.
- 데이터는 데이터만 조합할 수 있다.
- 계산은 때로 우리 머릿속에서 일어난다.
액션 vs 계산
액션보다 계산이 좋다
-
- 테스트하기 쉽다.
- 계산은 언제 어디서나 원하는 만큼 테스트할 수 있다.
- 계산은 조합해서 더 큰 계산을 만들 수 있다.
- 신경쓰지 않아도 된다.
- 동시에 실행되는 것
- 과거에 실행되었던 것이나 미래에 실행할 것
- 실행 횟수
- 테스트하기 쉽다.
설계와도 맞닿아 있는 것이 아닌가?
액션 특징
- 액션은 코드 전체로 퍼진다.
- 액션을 부르는 함수가 있다면 그 함수도 액션이 된다.
- 어떤 함수 안에 액션이 하나만 있어도 그 함수 전체가 액션이 된다.
- 액션은 다양한 형태로 나타난다.
- 함수 호출, 메서드 호출, 생성자, 표현 식, 상태
최대한 작은 액션을 만들어라
- 계산에서 액션을 가능한 작게 만든다
- 액션에서 결정이나 계획과 관련된 부분은 계산으로 빼낸다.
- 액션은 외부 세계와 상호작용하는 것을 제한할 수 있다.
- 내부에 계산과 데이터만 있고, 가장 바깥쪽에 액션이 있는 구조가 이상적이다.
- 액션은 외부 세계에 영향을 주거나 받을 수 있다.
액션에서 계산 빼내기
- 함수에 암묵적 입력과 암묵적 출력이 있으면 액션이 된다.
계산을 만드려면 암묵적 입력이나 출력이 없어야한다.
- 명시적/암묵적
- 명시적 입력 : 인자
- 암묵적 입력 : 인자 외 다른 입력 -> 부작용을 일으킴
- 명시적 출력 : 리턴값
- 암묵적 출력 : 리턴값 외 다른 출력 -> 부작용을 일으킴
함수형 원칙을 적용하면 액션은 줄어들고 계산은 늘어난다.
방법
-
- 계산 코드를 찾아 빼낸다 (함수 추출 - 리팩토링)
- 새 함수에 암묵적 입력과 출력을 찾는다
- 암묵적 입력은 인자로, 암묵적 출력은 리턴값으로 바꾼다.
Copy-on-write
- 복사본 만들기
- 복사본 변경하기
- 복사본 리턴하기
- 사용 이유
- 데이터가 바뀌지 않도록 하기 위해 copy-on-write를 적용한다.
- 쓰기 동작을
copy-on-write
로 변경하여 불변성을 유지할 수 있다. (복사본만 수정하여 반환하기 때문) - 여러 스레드가 동시에 읽을 수 있다 (동시성 처리의 안정성)
통제할 수 있는 데이터를 바꿀 때 카피-온-라이트를 쓴다.
불변 데이터 구조는 충분히 빠르다
바뀔때마다 복사를 하면 너무 비효율적이라고 생각할 수 있다.
하지만 불변 데이터 구조에는 아래와 같은 장점이 있다.
- 언제든 최적화할 수 있다.
- 불변 데이터 구조를 사용하고 속도가 느린 부분이 있다면 그때 최적화한다.
- 가비지 콜렉터는 매우 빠르다.
- 충분히 최적화되어있다.
- 생각보다 많이 복사하지 않는다.
- 얕은 복사라면 같은 메모리를 가리키는 참조에 대한 복사본을 만든다.
- 함수형 프로그래밍 언어에는 빠른 구현체가 있다.
액션 vs 계산
- 변경 가능한 데이터를 읽는 것은 액션이다.
- 조회 시점에 따라 데이터가 달라지기 때문이다.
- 쓰기는 데이터를 변경 가능한 구조(액션이 가능한 구조)로 만든다.
- 데이터에 쓰기가 없다면 데이터는 변경 불가능한 데이터이다.
- 불변 데이터 구조를 읽는 것은 계산이다.
- 쓰기를 읽기로 바꾸면 코드에 계산이 많아진다.
방어적 복사
- 레거시 코드나 신뢰할 수 없는 코드로부터 내 코드를 보호하기 위해 방어적 복사를 만든다.
- 얕은 복사와 깊은 복사를 공유한다.
- 규칙
- 데이터가 안전한 코드에서 나갈 때 복사하기
- 깊은 복사(모든 계층에 있는 중첩된 데이터 구조를 복사)
- 안전한 코드로 데이터가 들어올 때 복사하기
- 데이터가 안전한 코드에서 나갈 때 복사하기
신뢰할 수 없는 코드와 데이터를 주고받아야할 때 방어적 복사를 쓴다.
- 깊은 복사는 비싸므로 카피-온-라이트를 사용할 수 없는 곳에 사용한다.
계층형 설계 방법
패턴 1. 직접 구현
- 직접 구현된 함수를 읽을 때 함수 시그니처가 나타내고 있는 문제를 함수 본문에서 적절한 구체화 수준에서 해결해야한다.
- 만약 너무 구체적이라면 코드에서 나는 냄새(code-smell)이다.
패턴 2. 추상화 벽
- 호출 그래프의 어떤 계층은 중요한 세부 구현을 감추고 인터페이스를 제공한다.
- 인터페이스를 사용하여 코드를 만들면 고수준의 추상화 단계만 생각하면 되기 때문에 두뇌 용량의 한계를 극복할 수 있다.
패턴 3. 작은 인터페이스
- 시스템이 커질수록 비즈니스 개념을 나타내는 중요한 인터페이스는 작고 강력한 동작으로 구성하는 것이 좋다.
- 직간접적으로 최소한의 인터페이스를 유지하면서 정의해야한다.
너무 인터페이스가 커지면 구현이 변경되어야할 때 고쳐야 할 것이 많다.
패턴 4. 편리한 계층
- 개발자의 요구를 만족시키면서 비즈니스 문제를 잘 풀 수 있어야한다.
- 소프트웨어를 더 빠르고 고품질로 제공하는데 도움이 되는 계층에 시간을 투자해야한다.
- 그냥 좋아서 계층을 추가하면 안된다.
호출 그래프
- 그래프의 가장 위에 있는 코드가 고치기 가장 쉽다.
- 호출하는 횟수가 적기 때문이다.
- 자주 바뀌는 부분을 여기에 두기!
- 가장 아래에 있는 코드는 고치기 힘들다.
- 코드 위에 너무 많은 코드(외부로부터 너무 많은 호출)를 만들었기 때문이다.
- 제대로 만들었다면 가장 아래에 있는 코드보다 가장 위에 있는 코드가 더 자주 바뀐다.
- 하위 계층 코드를 테스트할수록 얻은 것이 더 오래간다.
// 최상위 계층 : 자주 변경된다. 비즈니스 로직
public OrderSummary processOrder(Order order) {
return order.getItems()
.stream()
.filter(this::isInStock)
.map(this::applyDiscount)
.collect(toOrderSummary());
}
// 중간 계층 : 구체적인 변환 로직
private Item applyDiscount(Item item) {
return calculateDiscount(item)
.map(discount -> applyDiscountToItem(item, discount))
.orElse(item);
}
// 최하위 계층 : 거의 변경 X. 핵심 유틸리티 함수
private double calculateBaseDiscount(double price) {
return price * 0.1;
}
호출 그래프가 우리에게 알려주는 것
- 유지보수성
- 위로 연결된 것이 적은 함수가 바꾸기 쉽다.
자주 바뀌는 코드는 가능한 위쪽에 있어야 한다.
- 테스트 가능성
- 위쪽으로 많이 연결된 함수를 테스트하는 것이 더 가치 있다.
- 전체 코드에 더 많은 영향을 주기 때문이다.
- 위쪽으로 많이 연결된 함수를 테스트하는 것이 더 가치 있다.
아래쪽에 있는 함수를 테스트하는 것이 위쪽에 있는 함수를 테스트하는 것보다 가치있다.
- 재사용성
- 아래쪽에 함수가 적을 수록 더 재사용하기 좋다.
- 호출하는 함수가 많아질수록 재사용성이 적다.
- 아래쪽에 함수가 적을 수록 더 재사용하기 좋다.
낮은 수준의 단계로 함수를 빼내면 재사용성이 더 높아진다.
Reference
https://product.kyobobook.co.kr/detail/S000001952246
쏙쏙 들어오는 함수형 코딩 | 에릭 노먼드 - 교보문고
쏙쏙 들어오는 함수형 코딩 | 함수형 개발자는 어떻게 생각하고 코드를 작성할까함수형 프로그래밍은 절차적 프로그래밍, 객체 지향 프로그래밍과는 다른 새로운 방식의 프로그래밍이다. 따라
product.kyobobook.co.kr
728x90
'설계' 카테고리의 다른 글
함수형 프로그래밍과 객체 지향 프로그래밍의 교집합 (1) | 2025.02.22 |
---|---|
인터페이스, 추상 클래스, 합성 사용 시점 (6) | 2024.12.02 |
[오브젝트] 상속과 다형성 (2) | 2024.12.01 |
[오브젝트] 객체 지향 설계 (0) | 2024.12.01 |
[설계] Okky ERD 분석하기 (0) | 2024.04.26 |