ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 개발자를 위한 수학 마스터 3회차: 함수와 알고리즘 - 수학 함수와 프로그래밍 함수
    [프로그램]/Maths 2025. 9. 11. 14:00
    728x90

    "매일 작성하는 함수, 수학적 원리를 알면 코드가 달라집니다!"

    "드모르간 법칙 적용했더니 코드리뷰에서 '이해하기 쉽다'는 피드백을 받았어요!"
    "복잡한 if문이 한눈에 들어오니까 버그도 줄어든 것 같아요."

    오늘은 개발자가 가장 자주 사용하는 함수(Function)에 대해 알아보겠습니다.

    “함수는 그냥 코드 묶어놓은 거 아닌가?”라고 생각하시는 분들을 위해 준비했어요.

    수학적 함수 개념을 이해하면 더 안전하고 예측 가능한 코드를 작성할 수 있습니다.

    함수, 정말 제대로 알고 쓰고 계신가요?

    실제 코드리뷰에서 발견한 문제

    // 😵 이 함수는 언제 어떤 결과를 낼지 예측하기 어려워요
    let processCount = 0;
    let lastProcessedUser = null;
    
    function processUser(user) {
      processCount++; // 전역 변수 변경
      lastProcessedUser = user; // 전역 변수 변경
    
      if (Math.random() > 0.1) { // 랜덤한 결과
        console.log(`Processing ${user.name}...`); // 부수 효과
    
        // 때때로 외부 API 호출 (브라우저 환경 예시)
        if (user.needsValidation) {
          fetch('/api/validate', { method: 'POST', body: JSON.stringify(user) });
        }
    
        return user.name.toUpperCase();
      }
    
      return user.name; // 때로는 원본, 때로는 대문자
    }
    
    // 같은 입력인데 다른 결과가...? 🤯
    console.log(processUser({ name: "김철수", needsValidation: true }));
    console.log(processUser({ name: "김철수", needsValidation: true }));
    • 같은 입력에 대해 다른 결과가 나올 수 있음
    • 전역 변수를 변경하여 예측 불가능한 부수 효과 발생
    • 외부 시스템에 의존하여 테스트하기 어려움
    • 함수의 책임이 너무 많아서 재사용하기 어려움

    이 문제들을 수학이 어떻게 해결해줄까요?

    수학에서 함수는 매우 엄격한 규칙을 가지고 있습니다. 이 규칙들을 프로그래밍에 적용하면 위 문제들이 자연스럽게 해결돼요!

    수학 함수의 핵심 개념

    1) 함수의 정의: f(x) = y

    • 정의역(Domain): 입력 가능한 값들의 집합
    • 공역(Codomain): 이론적으로 출력될 수 있는 값들의 집합
    • 치역(Range/Image): 실제 출력되는 값들의 집합
    • 함수 관계: 정의역의 각 원소가 치역의 정확히 하나의 원소와 대응

    ※ 본 글에서는 이해를 돕기 위해 “치역 = 실제 결과값의 범위”로 설명합니다.

    function square(x) {
      // 정의역: 모든 숫자
      // 치역: 0 이상의 숫자
      // 관계: 하나의 입력 → 하나의 출력 (항상 동일)
      return x * x;
    }
    
    console.log(square(3)); // 항상 9

    2) 순수함수 (Pure Function)

    // ✅ 순수함수의 특징들
    function addTax(price, taxRate) {
      // 1. 동일한 입력 → 동일한 출력 (결정적)
      // 2. 외부 상태 변경 없음 (부수 효과 없음)
      // 3. 외부 상태에 의존하지 않음
      return price * (1 + taxRate);
    }
    
    console.log(addTax(1000, 0.1)); // 1100
    // ❌ 순수함수가 아닌 예시
    let globalDiscount = 0.1;
    function addTaxWithDiscount(price, taxRate) {
      globalDiscount += 0.01; // 외부 상태 변경 - 부수 효과!
      return price * (1 + taxRate - globalDiscount); // 외부 상태 의존!
    }

    함수의 성질별 분류와 프로그래밍 응용

    1) 단사함수 (Injective)

    function createUserId(email) {
      let hash = 0;
      for (let i = 0; i < email.length; i++) {
        hash = ((hash << 5) - hash + email.charCodeAt(i)) & 0xffffffff;
      }
      return Math.abs(hash).toString(36);
    }

    2) 전사함수 (Surjective)

    function scoreToGrade(score) {
      if (score >= 90) return 'A';
      if (score >= 80) return 'B';
      if (score >= 70) return 'C';
      if (score >= 60) return 'D';
      return 'F';
    }

    3) 전단사함수 (Bijective)

    function simpleEncrypt(text, shift = 3) {
      return text.split('').map(char => {
        if (char.match(/[a-z]/)) {
          return String.fromCharCode((char.charCodeAt(0) - 97 + shift) % 26 + 97);
        }
        return char;
      }).join('');
    }
    
    function simpleDecrypt(encryptedText, shift = 3) {
      return encryptedText.split('').map(char => {
        if (char.match(/[a-z]/)) {
          return String.fromCharCode((char.charCodeAt(0) - 97 - shift + 26) % 26 + 97);
        }
        return char;
      }).join('');
    }
    
    const original = "hello world";
    const encrypted = simpleEncrypt(original);
    const decrypted = simpleDecrypt(encrypted);
    
    console.log(original);  // "hello world"
    console.log(encrypted); // "khoor zruog"
    console.log(decrypted); // "hello world" - 완벽하게 복원!

    함수 합성 (Function Composition)

    const trim = str => str.trim();
    const toLowerCase = str => str.toLowerCase();
    const removeSpaces = str => str.replace(/\s+/g, '');
    const addPrefix = prefix => str => prefix + str;
    
    // 합성을 위한 헬퍼
    const compose = (...fns) => (v) => fns.reduceRight((acc, fn) => fn(acc), v);
    
    const processUsername = compose(
      addPrefix('user_'),
      removeSpaces,
      toLowerCase,
      trim
    );
    
    console.log(processUsername("  John Doe Smith  ")); // "user_johndoesmith"

    Map-Reduce 패턴 (실무 활용)

    const calculateSubtotal = items => 
      items.reduce((sum, item) => sum + (item.price * item.qty), 0);
    
    const calculateTotal = (subtotal, taxRate) => subtotal * (1 + taxRate);
    
    const processOrder = order => {
      const subtotal = calculateSubtotal(order.items); // ✅ 중복 계산 제거
      return {
        id: order.id,
        subtotal,
        tax: order.tax,
        total: calculateTotal(subtotal, order.tax)
      };
    };

    함수의 부수효과와 불변성

    부수효과가 많은 코드의 문제점

    아래 예시는 브라우저 환경 기준입니다. Node.js 환경에서는 localStorage가 없으므로 대체 스토리지를 사용해야 합니다.

    // ❌ 예측하기 어려운 코드 (부수효과 많음)
    let globalConfig = { theme: 'light', language: 'ko' };
    let userSessions = [];
    
    function loginUser(username, password) {
      // 1. 전역 상태 변경
      globalConfig.lastLoginTime = new Date();
    
      // 2. 배열 직접 수정
      userSessions.push({ username, loginTime: new Date() });
    
      // 3. 외부 의존성
      console.log(`${username} 로그인 성공`);
      localStorage.setItem('currentUser', username);
    
      // 4. 네트워크 호출
      fetch('/api/analytics', { 
        method: 'POST',
        body: JSON.stringify({ event: 'login', user: username })
      });
    
      return { success: true, user: username };
    }

    함수형 접근으로 개선하기

    // ✅ 순수함수로 나누고, 부수효과를 가장자리로 분리
    const updateConfig = (config, updates) => ({ ...config, ...updates });
    const addUserSession = (sessions, newSession) => [...sessions, newSession];
    
    const createLoginSession = (username) => ({
      username,
      loginTime: new Date(),
      sessionId: Math.random().toString(36).slice(2, 11)
    });
    
    // (부수효과) 실제 입출력은 별도 함수로
    const logLoginAttempt = (username) => console.log(`${username} 로그인 시도`);
    const saveToStorage = (key, value) => localStorage.setItem(key, JSON.stringify(value));
    const sendAnalytics = (event, data) => fetch('/api/analytics', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ event, ...data })
    });
    
    // (핵심 비즈니스 로직) 순수함수
    function processLogin(currentConfig, currentSessions, username, password) {
      const isValidUser = Boolean(username && password);
      if (!isValidUser) {
        return {
          success: false,
          error: '잘못된 사용자 정보',
          newConfig: currentConfig,
          newSessions: currentSessions
        };
      }
    
      const newConfig = updateConfig(currentConfig, { 
        lastLoginTime: new Date(),
        lastUser: username 
      });
    
      const newSession = createLoginSession(username);
      const newSessions = addUserSession(currentSessions, newSession);
    
      return { success: true, user: username, session: newSession, newConfig, newSessions };
    }
    
    function executeLoginSideEffects(username, session) {
      logLoginAttempt(username);
      saveToStorage('currentUser', { username, sessionId: session.sessionId });
      sendAnalytics('login', { user: username, timestamp: session.loginTime });
    }

    연습 문제: 직접 도전해보세요!

    🎯 초급

    힌트: 상태는 외부에서 관리하고, 로깅은 별도 함수로 분리
    let counter = 0;
    function incrementAndLog() {
      counter++;
      console.log(`현재 카운터: ${counter}`);
      return counter;
    }

    🎯 중급

    힌트: map 으로 불변 업데이트, Date 생성은 호출 시점 한 번만
    const users = [
      { id: 1, name: '김철수', email: 'kim@test.com', active: true },
      { id: 2, name: '이영희', email: 'lee@test.com', active: false },
      { id: 3, name: '박민수', email: 'park@test.com', active: true }
    ];
    
    function updateUserStatus(userId, newStatus) {
      for (let i = 0; i < users.length; i++) {
        if (users[i].id === userId) {
          users[i].active = newStatus;
          users[i].lastModified = new Date();
          break;
        }
      }
      return users;
    }

    🎯 고급

    힌트: trim, split, toLowerCase, 함수 합성을 단계별로 적용
    const rawData = [
      "  JOHN DOE, 25, DEVELOPER, john@company.com  ",
      "jane smith, 30, designer, JANE@DESIGN.CO",
      "  Bob Johnson, 35, manager, bob.johnson@corp.org  "
    ];
    
    // 목표:
    // [
    //   { name: "John Doe", age: 25, role: "developer", email: "john@company.com", domain: "company.com" },
    //   { name: "Jane Smith", age: 30, role: "designer", email: "jane@design.co", domain: "design.co" },
    //   { name: "Bob Johnson", age: 35, role: "manager", email: "bob.johnson@corp.org", domain: "corp.org" }
    // ]

    실무 적용 가이드라인

    • 단일 책임 원칙 — 하나의 함수는 하나의 일만
    • 순수함수 우선 — 같은 입력 → 같은 출력, 부수효과 최소화
    • 작고 명확한 함수 — 10~15줄, 매개변수 ≤ 3
    • 불변성 유지 — 기존 데이터 변경 금지, 새 객체/배열 반환

    실습 프로젝트: 해시 함수 구현

    직접 실행해보고 싶다면, 아래 데모 페이지에서 해시 함수의 결정성과 단방향성을 확인할 수 있습니다 👇

    🔗 Hash Function Demo 실행하기

    마무리 및 다음 예고

    핵심 포인트 정리

    • 순수함수: 같은 입력 → 같은 출력
    • 함수의 성질: 단사, 전사, 전단사
    • 함수 합성: 작은 함수들을 조합
    • 불변성: 기존 데이터를 변경하지 않음

    🗓️ 다음 회차 예고 — “이산수학 기초 - 개발자가 알아야 할 핵심 개념”

    • 순열과 조합: 패스워드 강도 계산
    • 점화식과 동적 프로그래밍: 피보나치, 최적화 문제
    • 수학적 귀납법: 재귀 함수의 정확성 증명
    • 비둘기집 원리: 해시 충돌과 메모리

    *일부 코드(예: fetch, localStorage)는 브라우저 환경 기준입니다. Node.js에서는 대체 수단을 사용하세요.

    728x90

    댓글

Designed by Tistory.