Programming🧑‍💻/Functional Programming

함수야 거짓말 하지마라

생각 깎는 아이 2025. 2. 9. 23:07

 

함수 시그니쳐란? 

함수 시그니쳐는 함수의 정체성을 나타내는 ID 카드와 같습니다. 

함수의 이름, 매개변수 타입, 반환 타입을 포함하며,

이를 통해 함수가 무엇을 받아서 무엇을 돌려주는지를 명확하게 알 수 있어야합니다. 

func functionName(parameterName: ParameterType) -> ReturnType {
    // 함수 구현
}

예를 들어, 사용자의 나이를 받아서 성인 여부를 반환하는 함수의 시그니처는 이렇게 됩니다

func isAdult(age: Int) -> Bool {
    return age >= 18
}

위와 같이 함수의 시그니쳐들을 통해 해당 함수가 어떤 "역할을 하는지?" ,"어떤 값들을 필요로 하는지?", "어떤 결과값이 나올 수 있는지?"등의 함수의 동작을 예측할 수 있어야합니다. 

거짓말쟁이 함수들 

작성한 코드속에는 거짓말을 하고 있는 함수들이 숨어있을 수 있습니다. 

함수 시그니처가 약속한 것과 다른 행동을 하여 우리를 혼란스럽게 만듭니다. 

 

1. 잘못된 이름 사용 

  • 함수명에서 양수값만 반환될 수 있음을 명시해두고 실제 호출시 음수값이 반환될 수 있습니다. 
func getPositiveNumber(num: Int) -> Int {
    return num - 1  // 음수가 반환될 수 있음!
}

 

2. 부작용 숨기기 

 

  • 현재 함수 구현부에서는 값을 더할 뿐만아니라  UserDefaults를 이용해 값을 저장하고 있습니다. 
  • 함수 이름에서는 단순히 "계산"만 할 것이라고 암시하고 있습니다
  • 함수의 반환 타입은 단순히 Int로, 저장 작업이 일어난다는 것을 전혀 알려주지 않습니다
  • 이 저장 작업은 호출하는 쪽에서 예상하지 못한 상태 변경을 일으킵니다

 

func calculateSum(numbers: [Int]) -> Int {
    UserDefaults.standard.set(numbers, forKey: "lastCalculation")  // 숨겨진 부작용!
    return numbers.reduce(0, +)
}

 

3. 실패 상황의 모호한 처리 

 

  • 데이터베이스에서 사용자를 찾지 못했을 때 빈 문자열("")을 반환합니다
  • 이것은 "사용자를 찾지 못함"이라는 실패 상황을 "이름이 빈 문자열인 사용자"와 구분할 수 없게 만듭니다
  • 함수의 반환 타입이 String이라는 것은 "항상 유효한 문자열을 반환하겠다"는 약속입니다
  • 하지만 실제로는 실패할 수 있는 작업인데, 이 가능성이 시그니처에 반영되어 있지 않습니다

 

func fetchUserName(id: Int) -> String {
    guard let name = database.getName(id) else {
        return ""  // 빈 문자열 반환
    }
    return name
}

 

 

함수부터 제대로 

1. 명확한 이름 사용

// 좋은 예
func findUserIfExists(id: Int) -> User? {
    // Optional 반환으로 실패 가능성을 명시
    return database.findUser(id)
}

 

2. 부작용 명시하기 

func saveAndCalculateSum(numbers: [Int]) -> (sum: Int, savedSuccessfully: Bool) {
    let saved = UserDefaults.standard.set(numbers, forKey: "lastCalculation")
    let sum = numbers.reduce(0, +)
    return (sum, saved)
}

 

3. 실패 가능성 명확히 하기 

// 좋은 예
enum ValidationError: Error {
    case invalidAge
    case invalidName
}

func validateUser(age: Int, name: String) throws -> User {
    guard age >= 0 else {
        throw ValidationError.invalidAge
    }
    guard !name.isEmpty else {
        throw ValidationError.invalidName
    }
    return User(age: age, name: name)
}

 

개발자로서 성장하다 보면, 우리는 늘 새로운 것들을 배우고 적용하려 합니다. 최신 아키텍처 패턴, 멋진 디자인 패턴, 효율적인 상태 관리 도구..., 

 

하지만 잠깐, 그 화려한 기술들을 도입하기 전에 우리가 놓치고 있는 것은 없을까요?

 

함수는 우리 코드의 가장 기본적인 구성 요소입니다. 마치 집을 짓는 블록과 같죠.

하나의 함수가 제대로 자신의 역할을 하지 못한다면, 그 위에 아무리 멋진 아키텍처를 쌓아올려도 소용이 없습니다.

// MVVM 아키텍처를 사용하지만...
class UserViewModel {
    func getUserData(id: Int) -> UserData {
        // 실패할 수 있는데 처리하지 않음
        // 부작용이 숨어있음
        // 이름과 다른 동작을 함
        // 책임이 너무 많음
        ...
    }
}

이런 기초적인 문제들이 있는 코드는, 아무리 좋은 아키텍처 패턴으로 감싸도 결국 문제를 일으킬 것입니다.

 

새로운 기술을 배우고 적용하는 것은 분명 중요합니다. 하지만 그전에 우리는 이런 질문들을 해볼 필요가 있습니다

 

"내가 작성한 이 함수는..."

  • 정직한가요? (이름이 약속한 일만 하나요?)
  • 믿을 수 있나요? (예외 상황을 제대로 처리하나요?)
  • 이해하기 쉬운가요? (다른 개발자가 봐도 의도를 파악할 수 있나요?)
  • 하나의 일만 하나요? (너무 많은 책임을 지고 있지는 않나요?)