-
순수 함수란?Programming🧑💻/Functional Programming 2025. 2. 10. 23:22
수학에서 f(x) = x + 1 같이 함수는 x에 특정 값을 넣으면 항상 동일한 결과가 나옵니다.
프로그래밍에서 순수 함수도 이와 같은 원칙을 따릅니다.
- 항상 단일 값을 반환합니다 (동일한 입력에 대해 동이한 출력 반환)
- 반환 값은 오직 입력 매개변수에만 기반하여 계산됩니다
- 기존 값을 변경하거나 부작용을 일으키지 않습니다
위의 원칙들을 따르면서 나타나는 다음 특성들 때문에 이를 '순수'하다고 합니다
- 입력과 출력의 관계가 순수하게 유지됩니다 (같은 입력 = 같은 출력)
- 외부 세계와의 상호작용 없이 순수하게 계산만 수행합니다
- 부작용 없이 순수하게 자신의 역할만 수행합니다
순수함수가 왜 필요한데?
전통적인 클래스 기반 접근 방식의 문제점
장바구니에 책이 포함되어 있을 때 할인을 해주는 비즈니스 로직을 구현한다고 해봅시다.
기존의 클래스 기반의 장바구니 구현은 다음과 같을 수 있습니다.
- ShoppingCart라는 장바구니 클래스를 만들었습니다.
- 그 안에 포함된 물건 목록과 책 포함 여부를 상태값으로 관리합니다.
- 이 값들의 접근과 변경을 통해 로직을 구현합니다.
class ShoppingCart { private var items: [String] = [] private var bookAdded = false func addItem(_ item: String) { items.append(item) if item == "Book" { bookAdded = true } } func getDiscountPercentage() -> Int { return bookAdded ? 5 : 0 } func getItems() -> [String] { return items } }
이 코드는 언뜻 보기에는 문제가 없어 보이지만, 실제 운영 환경에서는 여러 가지 문제를 일으킬 수 있습니다.
1. 상태 관리의 취약성
- 현재 addItem 함수에서 bookAdded 상태값을 관리하고 있습니다.
- 하지만 getItems 를 통해 ShoppingCart의 상태값에 직접 접근하여 변경이 가능합니다.
- 따라서 직접 리스트에 접근하여 삭제하는 경우 bookAdded 상태 값이 정상적으로 변경되지 않을 수 있습니다.
let cart = ShoppingCart() let items = cart.getItems() // items 리스트를 받아옴 items.remove("Book") // 직접 리스트 수정 print(cart.getDiscountPercentage()) // 여전히 5% 할인을 반환
2. 동시성 문제
- 여러 쓰레드에서 하나의 장바구니 객체에 동시에 접근하여 상태값 변경을 시도하면 동시성 이슈가 발생합니다.
// 여러 스레드에서 동시에 실행될 경우 Thread 1: cart.addItem("Book") Thread 2: cart.getItems().removeAll() Thread 3: cart.getDiscountPercentage() // 결과를 예측할 수 없음
위와 같은 문제들이 발생하는 근본적인 이유는 바로 '상태'를 가지고 있기 때문입니다.
상태를 가진다는 것은 다음을 의미합니다.
- 데이터가 시간에 따라 변경될 수 있음
- 여러 곳에서 같은 데이터를 수정 할수 있음
- 데이터의 일관성을 보장하기 어려움
위와 같은 문제들을 해결하기 위해 순수 함수적 접근이 필요졌습니다.
순수 함수의 이용
이제 같은 기능을 순수 함수로 구현했을 때 어떠한 장점이 있는지 살펴봅시다.
struct ShoppingCart { static func getDiscountPercentage(items: [String]) -> Int { return items.contains("Book") ? 5 : 0 } }
1. 예측 가능성
- 순수 함수는 입력이 같으면 항상 같은 결과를 반환합니다.
- 내부 상태가 없기 때문에, 프로그램의 다른 부분에서 무슨 일이 발생하던 결과를 정확히 예측할 수 있습니다.
let items = ["Book", "Apple"] // 항상 5를 반환합니다 let discount = ShoppingCart.getDiscountPercentage(items: items)
2. 멀티쓰레드 환경에서의 안전성
- 순수 함수는 공유 상태가 없기 때문에 여러 쓰레드에서 동시에 실행해도 안전합니다.
// 각 스레드가 자신만의 데이터 복사본을 사용 DispatchQueue.concurrentPerform(iterations: 100) { index in var threadItems = ["Book", "Apple"] let discount = ShoppingCart.getDiscountPercentage(items: threadItems) // 각 스레드의 결과는 항상 예측 가능 }
3. 테스트 용이성
- 순수함수는 테스트하기가 용이합니다.
- 복잡한 설정이나 목(mock) 객체가 필요 없으며, 단순히 입력과 출력만 확인하면 됩니다.
func testDiscounts() { assert(ShoppingCart.getDiscountPercentage(items: []) == 0) assert(ShoppingCart.getDiscountPercentage(items: ["Book"]) == 5) }
4. 조합 가능성
- 순수 함수는 자신이 필요한 모든 데이터를 매개변수로 받고, 결과를 반환값으로만 전달하기 때문에 명확한 연결지점을 제공합니다.
- 순수 함수는 외부 상태를 변경하지 않기 때문에 여러 함수를 조합할 때 예기치 않은 상호작용을 걱정하지 않아도 됩니다.
- 따라서 여러 순수함수들을 조합하여 데이터 처리 파이프라인을 만들 수 있습니다.
func calculateFinalPrice(items: [String], basePrice: Double) -> Double { let discount = Double(ShoppingCart.getDiscountPercentage(items: items)) return basePrice * (100.0 - discount) / 100.0 }
순수 함수의 한계와 현실적 접근
모든 코드를 순수 함수로 작성하는 것은 현실적으로 어렵고, 때로는 비효율적일 수 있습니다.
다음과 같은 상황에서는 비순수 함수를 사용하는 것이 더 적절할 수 있습니다.
1. 로깅(Logging)의 경우
- 매번 실행할 때마다 로그 파일이라는 '외부 세계'(상태)가 변경됩니다
- 같은 입력을 넣어도 시간에 따라 다른 결과가 발생합니다(로그 파일이 계속 커짐)
- 하지만 이런 기록은 필수적입니다(나중에 문제가 생겼을 때 확인이 필요)
func logUserAction(_ action: UserAction) { // 로깅은 본질적으로 부작용을 포함합니다 Logger.log("User performed action: \(action)") }
2. 하드웨어 상호작용
- 카메라는 물리적 장치를 제어하기 때문에 해당 장치 상태에 따라 결과가 달라질 수 있습니다.
func capturePhoto() async throws -> Image { // 카메라로 사진 촬영 return try await Camera.capturePhoto() }
위와 같이 태생적으로 '비순수'한 작업들은 다음 방법을 이용해 다룰 수 있습니다.
class PhotoService { // 비순수 작업을 최대한 분리하여 관리 private let camera: Camera private let logger: Logger // 순수한 부분: 이미지 처리 로직 func processImage(_ image: Image) -> ProcessedImage { // 순수 함수로 이미지 처리 return image.resize().enhance().compress() } // 비순수한 부분: 실제 카메라 작동 func takePhoto() async throws -> ProcessedImage { // 1. 비순수 작업: 카메라로 사진 촬영 let rawImage = try await camera.capturePhoto() // 2. 순수 작업: 이미지 처리 let processed = processImage(rawImage) // 3. 비순수 작업: 로깅 logger.log("Photo taken and processed") return processed } }
위와 같은 방식을 통해
- 순수한 부분과 비순수한 부분을 명확히 구분할 수 있습니다
- 테스트하기 쉬운 부분(순수 함수)과 어려운 부분(비순수 함수)을 분리할 수 있습니다
- 비순수한 작업들을 한 곳에서 관리할 수 있습니다
순수 함수는 코드의 예측 가능성, 테스트 용이성, 재사용성을 높이는 강력한 도구입니다.
하지만 이는 모든 코드를 순수 함수로 작성해야 한다는 의미는 아닙니다.
대신, 비즈니스 로직의 핵심 부분은 가능한 한 순수 함수로 작성하고,
필요한 부작용은 명확히 분리된 계층에서 처리하는 것이 좋은 접근 방법입니다.
'Programming🧑💻 > Functional Programming' 카테고리의 다른 글
함수야 거짓말 하지마라 (0) 2025.02.09