ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Function vs Procedure
    Knowledge Base/Applied 2026. 3. 5. 02:57

    Date: 2026-03-05
    Author: Claude Code Opus 4.6, mangowhoiscloud
    Scope: SQL / Script (JS, Python) / Low-level (C, C++, Go) + 이론적 기반
    Loop:
    1. 4-agent 병렬 리서치 (SQL, Script, Low-level, CS Theory)
    2. 초안 합성
    3.3-reviewer 패널 검수 (CS Theory Prof, Senior DBA, Systems Expert) × 3 iterations(70.3 → 88.67 → 97.0 PASS, threshold 96)
    4. Technical Writer 검수 × 1 iteration


    본 문서는 프로그래밍 언어의 핵심 개념인 Function(함수)과 Procedure(프로시저)를 이론적 기반부터 SQL, 스크립트 언어, 저수준 언어까지 종합적으로 서술합니다. 각 섹션은 독립적으로 참조할 수 있도록 구성되어 있습니다.

    목차

    1. 이론적 기반 (Theoretical Foundations)
    2. SQL: Function vs Stored Procedure
    3. Script: JavaScript/TypeScript & Python
    4. Low-level: C / C++ / Go
    5. 종합 비교 매트릭스
    6. 참고 문헌

    1. 이론적 기반 (Theoretical Foundations)

    1.1 수학적 기원: Lambda Calculus vs Turing Machine

    모델 창시자 핵심 개념 프로그래밍 대응
    Lambda Calculus (1936) Alonzo Church 함수 = 입력→출력 매핑 Function (값 반환, 순수 변환)
    Turing Machine (1936) Alan Turing 계산 = 상태 전이 시퀀스 Procedure (상태 변경, 부수효과 중심)
    • Lambda Calculus: λx.e — 추상화(abstraction)와 적용(application)만으로 모든 계산을 표현합니다. 순수 lambda calculus에서는 부수효과가 존재하지 않으나, ML계열 언어의 기반인 불순(impure) lambda calculi는 참조와 상태를 포함합니다.
    • Turing Machine: 유한 상태 + 무한 테이프 위에서 read/write/move 명령을 반복합니다. 상태 변이가 계산의 본질입니다.
    • Church-Turing Thesis: "효과적으로 계산 가능한(effectively computable) 모든 함수는 Turing Machine으로 계산할 수 있다"는 철학적 명제(thesis)이며, 증명된 정리(theorem)가 아닙니다. Lambda calculus와 Turing Machine의 계산 능력 동등성은 별도의 수학적 증명으로 확립된 결과이며, 이 동등성이 두 모델 간 차이는 표현 방식에 있음을 보여줍니다.

    1.2 참조 투명성 (Referential Transparency)

    정의: 표현식을 그 결과값으로 대체해도 프로그램의 동작이 변하지 않는 성질입니다.

    f(x) = y 일 때, 프로그램 내 모든 f(x)를 y로 치환 가능 → 참조 투명
    • 순수 함수 (Pure Function): 참조 투명성을 만족합니다. 동일 입력 → 동일 출력이며, 부수효과가 없습니다.
    • 프로시저 (Procedure): 참조 투명성을 위반합니다. 외부 상태를 변경하거나 I/O를 수행합니다.

    실용적 의미:

    • 순수 함수는 메모이제이션, 병렬화, 공통 부분식 제거(CSE)가 안전합니다
    • 프로시저는 호출 순서와 횟수가 결과에 영향을 미칩니다

    1.3 Command-Query Separation (CQS)

    Bertrand Meyer (Eiffel 언어, 1988):

    "Every method should either be a command that performs an action, or a query that returns data, but not both."

    "모든 메서드는 동작을 수행하는 커맨드 또는 데이터를 반환하는 쿼리 중 하나여야 하며, 둘 다여서는 안 됩니다."

    분류 반환값 부수효과 호출 안전성
    Query (함수) 있음 없음 몇 번이든 안전하게 호출 가능
    Command (프로시저) 없음 (void/None) 있음 호출 횟수/순서가 중요

     

    CQS → CQRS 확장: Greg Young (2010)이 메서드 레벨 CQS를 시스템 아키텍처로 확장했습니다.

    읽기/쓰기 모델을 물리적으로 분리하고, Event Sourcing과 결합합니다.

    1.4 타입 이론의 관점

    타입 값의 수 의미 사용 언어
    Unit () 1개 "정상 종료, 반환값 무의미" → 프로시저 Haskell (), Rust (), Kotlin Unit
    Void (C계열) Unit처럼 작동 "반환값 없음"을 표현 * C/C++ void, Java void
    Void (TS) 특수 "반환값을 사용하지 말라"는 의미. 실제로는 undefined 반환 가능 TypeScript void
    Never/Bottom 0개 "절대 반환하지 않음" (무한루프, 예외, 프로세스 종료) TS never, Rust !, Python Never, Haskell Void

    * C의 void는 "반환값 없음"을 표현하는 점에서 Unit처럼 작동하나, 엄밀한 Unit 타입(값이 1개 존재)과 달리 void 변수를 선언할 수 없는 불완전 타입(incomplete type)입니다.

    주의: 타입 이론의 Void (값이 0개인 공타입, bottom에 대응)와 C의 void는 이름이 같지만 다른 개념입니다. Haskell의 Void는 타입 이론적 Void(공타입, 값 0개)이고, C의 void는 Unit처럼 작동하는 불완전 타입입니다. C에서 void x;는 불가능하므로 진정한 Unit 타입(정확히 1개의 값을 가지는 타입)과도 다릅니다.

    전체성(Totality) vs 부분성(Partiality): Never/ 타입은 부분 함수(partial function)와 관련됩니다. 전체 함수는 모든 입력에 대해 반드시 종료하고 값을 반환합니다. 부분 함수는 일부 입력에서 발산(무한루프)하거나 예외로 종료할 수 있습니다. f :: A → Never는 어떤 입력에서도 정상 반환하지 않는 함수입니다 — panic, process.exit, 무한 이벤트 루프 등이 이에 해당합니다.

    1.5 Monad: 부수효과의 타입 수준 격리

    Haskell의 IO Monad는 부수효과를 타입 시스템으로 격리합니다:

    -- 순수 함수: 부수효과 없음
    add :: Int -> Int -> Int
    add a b = a + b
    
    -- IO 프로시저: 부수효과가 타입에 명시
    greet :: String -> IO ()
    greet name = putStrLn ("Hello, " ++ name)
    • IO () = "부수효과가 있고 의미 있는 반환값이 없는 액션"으로, 타입 수준의 프로시저입니다
    • IO 값을 구성하는 것은 순수하지만, 런타임이 실행할 때만 효과가 발생합니다. 이 구분이 참조 투명성을 유지하는 핵심입니다
    • Monad의 핵심: return :: a -> m a, (>>=) :: m a -> (a -> m b) -> m b (현대 Haskell에서 returnApplicativepure와 동일합니다 — Functor → Applicative → Monad 계층 구조)
    • Monad Laws (세 법칙이 충족되어야 올바른 추상화입니다):
      • Left identity: return a >>= f ≡ f a
      • Right identity: m >>= return ≡ m
      • Associativity: (m >>= f) >>= g ≡ m >>= (λx → f x >>= g)
    • State Monad는 상태 변이를 순수하게 모델링합니다 — 프로시저적 상태 변경을 함수적으로 표현하는 예시입니다

    1.6 범주론(Category Theory) 관점

    범주론에서 함수사상(morphism)입니다 — 두 대상(타입) 사이의 화살표입니다:

    • 대상(Object) = 타입 (Int, String, ...)
    • 사상(Morphism) = 함수 (f :: A → B)
    • 합성: f :: A → B, g :: B → Cg ∘ f :: A → C

    프로시저의 범주론적 위치: Unit 타입 ()으로의 사상 f :: A → ()입니다. 반환값이 하나뿐이므로 정보량이 0이며, 모든 의미는 부수효과에 있습니다.

    Functor: 범주 간 구조 보존 매핑입니다. 프로그래밍에서는 타입→타입, 함수→함수의 매핑입니다:

    class Functor f where
      fmap :: (a -> b) -> f a -> f b  -- 함수를 컨텍스트 안으로 리프트

    "A monad in X is just a monoid in the category of endofunctors of X"
    — Mac Lane, Categories for the Working Mathematician (1971, p.138)의 의역

     

    Monad의 return(pure)이 항등원, >>=(bind)가 결합적 이항 연산입니다. 이 구조가 IO, State, Maybe 등 모든 모나드에 공통입니다. 이론적 기반을 바탕으로, 이하 섹션에서는 각 언어 계열별 구체적 구현을 살펴봅니다.


    2. SQL: Function vs Stored Procedure

    2.1 핵심 차이 요약

    특성 Function Stored Procedure
    반환 RETURNS 필수 (scalar/table) RETURNS 없음. 단, SP는 정수 상태코드 반환 가능 (SQL Server RETURN <int>), OUT/INOUT 파라미터, 다중 결과셋(MySQL), REFCURSOR(PostgreSQL) 지원
    쿼리 내 사용 SELECT, WHERE, FROM 가능 불가 (CALL/EXEC만)
    트랜잭션 제어 COMMIT/ROLLBACK 불가 가능
    DML 제한적 (DB별 상이) 자유
    에러 처리 제한적 완전한 TRY/CATCH
    결정론 DETERMINISTIC/IMMUTABLE 선언 해당 없음

    2.2 트랜잭션 제어 — 가장 날카로운 차이

    -- PostgreSQL 11+: 프로시저에서만 COMMIT 가능
    CREATE PROCEDURE batch_insert()
    LANGUAGE plpgsql AS $$
    BEGIN
        FOR i IN 1..1000 LOOP
            INSERT INTO log_table(val) VALUES (i);
            IF i % 100 = 0 THEN
                COMMIT;  -- 100건마다 체크포인트
            END IF;
        END LOOP;
    END;
    $$;
    
    -- 함수에서 COMMIT 시도 → ERROR
    CREATE FUNCTION bad_func() RETURNS void AS $$
    BEGIN
        COMMIT;  -- ERROR: invalid transaction termination
    END;
    $$ LANGUAGE plpgsql;

    주의: 트랜잭션 제어 프로시저는 클라이언트가 시작한 트랜잭션 블록(BEGIN ... CALL proc ... COMMIT) 안에서 호출할 수 없습니다. 클라이언트 트랜잭션 내에서 CALL하면 에러가 발생합니다 — 프로시저 자체가 트랜잭션 경계를 관리해야 합니다. 이것이 가장 흔한 PostgreSQL 11+ 운영 실수 중 하나입니다.

    2.3 쿼리 컨텍스트 사용

    -- 함수: SELECT 내에서 사용 가능 (inline 평가)
    SELECT name, get_salary(id) AS salary FROM employees;
    SELECT * FROM orders WHERE calculate_discount(total) > 100;
    
    -- 프로시저: 독립 호출만 가능
    CALL update_employee_salary(1, 75000);
    EXEC dbo.TransferFunds @From=1, @To=2, @Amount=100;

    2.4 DML 제한: DB별 비교

    DB Function 내 INSERT/UPDATE/DELETE
    PostgreSQL 허용되지만 위험합니다: 병렬 쿼리 강제 직렬화, read-your-own-writes 이상 동작, SECURITY DEFINER와 결합 시 보안 위험
    MySQL 조건부 허용됩니다: log_bin_trust_function_creators=1 설정 필요, 또는 함수에 DETERMINISTIC / NO SQL / READS SQL DATA 선언 필수. MODIFIES SQL DATA만으로는 불충분합니다
    SQL Server 기본 테이블 DML 금지입니다 (로컬 테이블 변수만 가능)

    2.5 결정론 선언

    DB 키워드 효과
    PostgreSQL IMMUTABLE / STABLE / VOLATILE IMMUTABLE은 인덱스 표현식에 사용 가능, 계획 시점 평가
    MySQL DETERMINISTIC / NOT DETERMINISTIC 바이너리 로깅 시 필수 선언
    SQL Server 추론 기반 + SCHEMABINDING 인덱스 뷰/계산 열에 사용 가능

    2.6 성능: 실행 계획 최적화

    SQL Server의 3가지 프로파일:

    1. Inline TVF (ITVF): 쿼리 계획에 인라인되어 뷰처럼 최적화됩니다. 최고 성능입니다.
    2. Scalar UDF Inlining (2019+): 적격 스칼라 함수를 관계형 하위쿼리로 변환합니다. 20배+ 개선됩니다. 전제조건: EXECUTE AS CALLER(기본값), TOP/DISTINCT 미사용, CTE 제한, 단일 RETURN 문(Cumulative Update 5 이상, CU5+).
    3. Multi-Statement TVF (MSTVF): 블랙박스 취급됩니다. pre-2017에서 카디널리티를 1행으로 고정 추정하여 조인 성능이 치명적이었습니다. SQL Server 2017의 Interleaved Execution으로 개선되었고, 2019에서 추가 향상되었습니다. 레거시에서는 최악 성능입니다.
    -- 권장: Inline TVF (옵티마이저가 내부를 볼 수 있음)
    CREATE FUNCTION dbo.GetOrders(@CustID INT)
    RETURNS TABLE AS RETURN (
        SELECT * FROM Orders WHERE CustomerID = @CustID
    );
    
    -- 비권장: MSTVF (pre-2017: 옵티마이저가 1행으로 추정, 2017+: Interleaved Execution으로 개선)
    CREATE FUNCTION dbo.GetOrdersMulti(@CustID INT)
    RETURNS @t TABLE (OrderID INT, Total DECIMAL)
    AS BEGIN
        INSERT @t SELECT OrderID, Total FROM Orders WHERE CustomerID = @CustID;
        RETURN;
    END;

    2.7 PostgreSQL: SQL-language 함수 인라인과 병렬 안전성

    PostgreSQL에서 단순한 LANGUAGE sql 함수는 쿼리 계획에 인라인되어 옵티마이저가 내부를 볼 수 있습니다 (SQL Server ITVF와 유사합니다). 반면 LANGUAGE plpgsql 함수는 블랙박스 취급됩니다. 성능 차이가 크므로 단순 쿼리 래퍼는 LANGUAGE sql 사용을 권장합니다.

    인라인 제한 조건: STRICT 옵션이 있거나, 집합 반환(SETOF)의 특정 패턴, 또는 복잡한 구조를 포함하면 인라인이 적용되지 않을 수 있습니다. 단일 SELECT 문으로 구성된 간단한 함수가 인라인 대상입니다.

    PARALLEL 안전성: PostgreSQL 함수는 PARALLEL SAFE/RESTRICTED/UNSAFE 속성을 가집니다. 기본값이 UNSAFE이므로, 함수를 호출하는 쿼리는 병렬 실행이 자동으로 비활성화됩니다. 순수 읽기 전용 함수는 명시적으로 PARALLEL SAFE를 선언해야 병렬 쿼리 성능을 활용할 수 있습니다. 이것이 프로덕션에서 가장 흔한 PostgreSQL 함수 성능 함정 중 하나입니다.

    -- 인라인 가능: LANGUAGE sql (옵티마이저가 내부를 봄)
    CREATE FUNCTION get_active(cust_id INT) RETURNS SETOF orders
    LANGUAGE sql AS $$
        SELECT * FROM orders WHERE customer_id = cust_id AND status = 'active';
    $$;
    
    -- 인라인 불가: LANGUAGE plpgsql (블랙박스)
    CREATE FUNCTION get_active_pl(cust_id INT) RETURNS SETOF orders
    LANGUAGE plpgsql AS $$
    BEGIN
        RETURN QUERY SELECT * FROM orders WHERE customer_id = cust_id AND status = 'active';
    END;
    $$;

    2.8 Window Functions — 함수의 특수 카테고리

    Window Function (OVER())은 함수/프로시저 구분과 직교하는 별도 카테고리입니다:

    • 집계 함수처럼 여러 행을 참조하지만, 스칼라 함수처럼 입력 행당 하나의 값을 반환합니다
    • WHERE 절에서 사용할 수 없습니다 (스칼라 함수와의 핵심 차이)
    • SELECTORDER BY에서만 사용 가능합니다
    -- Window Function: 각 행에 순위를 매기지만 행 수를 줄이지 않음
    SELECT name, score,
           RANK() OVER (ORDER BY score DESC) AS ranking,
           AVG(score) OVER (PARTITION BY genre) AS genre_avg
    FROM games;

    2.9 보안 컨텍스트

    DB 함수/프로시저 공통 주의사항
    PostgreSQL SECURITY DEFINER / INVOKER DEFINER 사용 시 SET search_path 필수 (권한 상승 방지)
    MySQL SQL SECURITY DEFINER / INVOKER DEFINER 사용자 권한으로 실행
    SQL Server EXECUTE AS CALLER/OWNER/SELF/'user' 2019+ UDF Inlining은 EXECUTE AS CALLER 필수

    3. Script: JavaScript/TypeScript & Python

    3.1 설계 철학: "Procedure" 키워드가 없는 이유

    JS와 Python 모두 함수와 프로시저를 하나의 키워드로 통합했습니다:

    • JavaScript: function / =>
    • Python: def

    프로시저는 "반환값이 없는 함수"의 특수 케이스로 취급됩니다.

    반환하지 않으면 암묵적으로 undefined(JS) / None(Python)을 반환합니다.

    3.2 반환 시맨틱스

    // JS: 명시적 반환
    function add(a, b) { return a + b; }
    
    // JS: 암묵적 undefined 반환 (프로시저 역할)
    function logValue(x) { console.log(x); }
    console.log(logValue(42)); // 42 출력 후 undefined
    # Python: 명시적 반환
    def add(a: int, b: int) -> int:
        return a + b
    
    # Python: 암묵적 None 반환 (프로시저 역할)
    def log_value(x: object) -> None:
        print(x)

    3.3 순수 함수 vs 부수효과 함수

    # 순수 함수: 동일 입력 → 동일 출력, 외부 상태 불변
    def normalize(values: list[float]) -> list[float]:
        total = sum(values)
        return [v / total for v in values]
    
    # 부수효과 함수 (프로시저): 외부 상태 변경
    selected: list[str] = []
    def add_game(game: str) -> None:
        selected.append(game)  # 외부 리스트 변이
    // 순수: 새 배열 반환, 원본 불변
    const normalize = (values: number[]): number[] => {
      const total = values.reduce((s, v) => s + v, 0);
      return values.map(v => v / total);
    };
    
    // 프로시저: 외부 상태 변이
    const selected: string[] = [];
    function addGame(game: string): void {
      selected.push(game);
    }

    3.4 Lambda / Arrow 함수

    특성 JavaScript Arrow => Python lambda
    문법 (x) => x * 2 lambda x: x * 2
    본문 식 본문(암묵적 반환) + 블록 본문 단일 식만 (항상 반환)
    this 바인딩 렉시컬 (상위 스코프) 해당 없음
    문(statement) 포함 블록 본문에서 가능 불가

    3.5 Generator: yield vs return

      return yield
    실행 함수 종료, 로컬 상태 폐기 함수 일시 중단, 로컬 상태 보존
    반환 단일 값 여러 값 (lazy iterator)
    재진입 불가 next() 호출로 재진입
    def fibonacci() -> Generator[int, None, None]:
        a, b = 0, 1
        while True:
            yield a       # 여기서 중단, 다음 next()에서 재개
            a, b = b, a + b

    3.6 Async 함수

      JavaScript Python
    반환 타입 항상 Promise<T> Coroutine (await 필요)
    실행 시점 즉시 시작 (첫 await까지) 지연 실행 (await/create_task 전까지 미실행)
    프로시저 형태 async function f(): Promise<void> async def f() -> None:

    3.7 타입 어노테이션으로 CQS 표현

    class GameCatalog {
      // COMMAND (프로시저): void 반환, 상태 변경
      addGame(game: Game): void { this.games.push(game); }
    
      // QUERY (함수): 데이터 반환, 상태 불변
      getGame(id: string): Game | undefined {
        return this.games.find(g => g.id === id);
      }
    }
    class GameCatalog:
        # COMMAND: -> None, 상태 변경
        def add_game(self, game: dict) -> None:
            self._games.append(game)
    
        # QUERY: 데이터 반환, 상태 불변
        def get_game(self, game_id: str) -> dict | None:
            return next((g for g in self._games if g["id"] == game_id), None)

    3.8 Method vs Function

      Python JavaScript/TypeScript
    Instance Method self 자동 바인딩 (descriptor protocol) this 바인딩이 호출 방식에 의존
    Static Method @staticmethod (바인딩 없음) static 키워드
    Class Method @classmethod (cls 바인딩) 해당 없음 (static으로 대체)
    this/self 이슈 안전 (바인딩 고정) 콜백 전달 시 this 유실 위험
    // JS/TS this 유실 문제
    const scorer = new GameScorer(0.8);
    setTimeout(scorer.computeScore, 100, "RPG");  // this가 undefined!
    
    // 해결: arrow method 또는 .bind()
    setTimeout(scorer.computeScore.bind(scorer), 100, "RPG");

    4. Low-level: C / C++ / Go

    4.1 역사적 진화

    Wheeler Jump (1951)
        ↓ 최초의 체계적 서브루틴(subroutine, 이하 프로시저와 동의어) 메커니즘 (Wheeler, Wilkes, Gill)
    FORTRAN I/II (1957-58)
        ↓ FUNCTION vs SUBROUTINE 구분 도입 (I에서 FUNCTION, II에서 SUBROUTINE 추가)
    ALGOL 60 (1960)
        ↓ 블록 구조, 렉시컬 스코프, 재귀 프로시저
    Pascal (1970)
        ↓ function vs procedure 구문적 강제
    C (1972)
        ↓ void로 통합 — "모든 것은 함수"
    C++ (1979→1983→1985)  ["C with Classes" → 명명 → 첫 릴리스]
        ↓ 오버로딩, 템플릿, constexpr, 람다, [[nodiscard]]
    Go (2009)
        ↓ 다중 반환값, defer/panic/recover, 인터페이스

    4.2 C: void에 의한 통합

    // 프로시저: void 반환
    void reset_counter(int *counter) {
        *counter = 0;
    }
    
    // 함수: 값 반환
    int add(int a, int b) {
        return a + b;
    }
    • 초기 C/B에서는 모든 함수가 암묵적으로 int를 반환했습니다. "프로시저"는 반환값을 무시하는 함수였습니다.
    • void 타입 도입으로 의도를 명시적으로 표현할 수 있게 되었습니다.

    4.3 C++ 확장

    constexpr / consteval (컴파일 타임 함수):

    // C++14+: constexpr에서 루프와 지역 변수 허용 (C++11에서는 단일 return 식만 가능)
    constexpr int factorial(int n) {
        int result = 1;
        while (n > 1) result *= n--;
        return result;
    }
    constexpr int val = factorial(10);  // 컴파일 타임에 계산
    static_assert(val == 3628800);
    
    // C++20 consteval: 반드시 컴파일 타임에만 실행 (런타임 호출 불가)
    consteval int must_be_compile_time(int n) { return n * 2; }
    // int x = must_be_compile_time(runtime_var);  // ERROR: not a constant expression
    • constexpr: 컴파일 타임 가능 (인자가 상수이면)이며, 런타임도 가능합니다
    • consteval: 컴파일 타임 전용입니다 — 런타임 호출 시 컴파일 에러가 발생합니다. 함수/프로시저 구분의 시간 축(compile-time vs runtime)을 강제합니다

    const 멤버 함수 (순수성 타입 수준 강제):

    class Matrix {
        double data[4][4];
    public:
        double get(int r, int c) const { return data[r][c]; }  // Query
        void set(int r, int c, double v) { data[r][c] = v; }   // Command
    };
    
    const Matrix m = Matrix::identity();
    m.get(0, 0);      // OK: const 함수
    // m.set(0, 0, 1);  // COMPILE ERROR: const 객체에서 비-const 호출 불가

    [[nodiscard]] (C++17) — 함수/프로시저 구분의 컴파일러 강제:

    [[nodiscard]] int computeScore(int a, int b) { return a + b; }
    
    computeScore(3, 4);  // WARNING: ignoring return value of function declared with 'nodiscard'
    int s = computeScore(3, 4);  // OK
    
    // C++20: 이유 메시지 추가 가능
    [[nodiscard("Score must be used for decision")]]
    int evaluateGame(const Game& g);
    • [[nodiscard]]가 있는 함수의 반환값을 무시하면 컴파일 경고가 발생합니다 → "이것은 함수이지 프로시저가 아니다"를 강제합니다
    • 반환값이 중요한 함수 (에러 코드, 계산 결과)에 적극 사용을 권장합니다

    noexcept — 프로시저의 예외 안전성 보장:

    void cleanup(Resource& r) noexcept {  // 절대 예외를 던지지 않음을 보장
        r.release();
    }
    • noexcept void 함수는 "부수효과만 수행하며 예외 없이 완료"를 타입 수준에서 보장하는 순수 프로시저입니다
    • 소멸자, move 연산, cleanup 루틴에 필수입니다 — noexcept가 없으면 std::vector 등이 move 대신 copy를 사용합니다

    GCC 순수 함수 어트리뷰트:

    __attribute__((pure))   // 파라미터 + 전역 읽기 전용 메모리에만 의존
    int string_length(const char *s);
    
    __attribute__((const))  // 파라미터 값에만 의존. 전역 메모리 접근 불가.
                            // 포인터 인자를 역참조하여 메모리를 읽는 것도 불가 (pure와의 핵심 차이)
    double square(double x);
    • const 함수: 컴파일러가 동일 인자 호출을 제거합니다 (CSE)
    • pure vs const: pure는 포인터 역참조로 메모리 읽기를 허용하지만, const는 불허합니다
    • 잘못된 선언 시 조용한 정확성 버그가 발생합니다 — 컴파일러가 호출을 최적화로 제거합니다

    4.4 Go: 다중 반환값과 에러 처리

    // Go의 핵심 혁신: (value, error) 다중 반환
    func divide(a, b float64) (float64, error) {
        if b == 0 {
            return 0, fmt.Errorf("division by zero")
        }
        return a / b, nil
    }
    
    result, err := divide(10.0, 3.0)
    if err != nil {
        log.Fatal(err)
    }

    에러 처리 패러다임 비교:

    언어 방식 가시성
    C errno + 반환 코드 숨겨짐 (암묵적)
    C++ / Python 비검사 예외 (unchecked exception) 숨겨짐 (암묵적 전파)
    Java 검사 예외 (checked exception) 부분 명시적 (throws 선언 필수이나 실행 흐름은 숨겨짐)
    Go (value, error) 다중 반환 명시적 (호출 지점에서 확인)

    Named Return + defer:

    func safeDivide(a, b int) (result int, err error) {
        defer func() {
            if r := recover(); r != nil {
                err = fmt.Errorf("recovered: %v", r)  // named return 수정
            }
        }()
        result = a / b  // b == 0이면 panic
        return
    }

    4.5 ABI 관점: 바이너리 레벨 차이는 없습니다

    void 함수 (프로시저)          int 함수
    ┌─────────────────┐        ┌─────────────────┐
    │ push rbp        │        │ push rbp        │
    │ mov rbp, rsp    │        │ mov rbp, rsp    │
    │ ...             │        │ ...             │
    │ pop rbp         │        │ mov eax, result │
    │ ret             │        │ pop rbp         │
    └─────────────────┘        │ ret             │
                               └─────────────────┘
    • 유일한 차이: 호출자가 RAX 레지스터를 읽는지 여부입니다
    • call 명령어, 스택 프레임 구조, ret 명령어는 동일합니다
    • 큰 반환값의 경우 호출자가 공간을 할당한 후 숨겨진 첫 번째 파라미터로 포인터를 전달합니다

    4.6 Calling Convention

    규약 스택 정리 인자 순서 사용처
    cdecl 호출자 오른쪽→왼쪽 C 기본값
    stdcall 피호출자 오른쪽→왼쪽 Win32 API
    System V AMD64 호출자 RDI, RSI, RDX, RCX, R8, R9 Linux/macOS 64bit
    Microsoft x64 호출자 RCX, RDX, R8, R9 Windows 64bit

    x86-64에서는 cdecl/stdcall/fastcall 모두 하나의 통합 규약으로 수렴합니다.

    4.7 함수 포인터와 콜백

    // C: 원시 함수 포인터
    typedef int (*BinaryOp)(int, int);
    BinaryOp ops[] = { add, subtract, multiply };
    int result = ops[0](3, 4);
    // C++: std::function (타입 소거. Small Buffer Optimization으로 작은 callable은 힙 할당 회피,
    //       대부분 구현체에서 16-32바이트 이하 callable은 인라인 저장)
    std::function<int(int, int)> f = [](int a, int b) { return a + b; };
    
    // C++: 템플릿 + auto (제로 오버헤드, 인라인)
    template<typename F>
    auto apply(F&& fn, int a, int b) { return fn(a, b); }
    // Go: 함수 타입은 일급 값
    type BinaryOp func(int, int) int
    var f BinaryOp = func(a, b int) int { return a + b }
    
    // 클로저
    func adder(base int) func(int) int {
        return func(x int) int { return base + x }
    }

    4.8 RVO/NRVO (C++ Return Value Optimization)

    // RVO (Mandatory in C++17): prvalue 반환 시 복사 생략이 표준에 의해 보장
    Expensive make() {
        return Expensive(1000);  // 호출자 슬롯에 직접 생성 — 복사/이동 생성자 불필요
    }
    
    // NRVO (Not mandatory): 이름 있는 지역 변수 반환. 표준은 보장하지 않지만
    // GCC/Clang/MSVC 모두 적용. C++11 move semantics가 NRVO 실패 시 fallback 제공.
    Expensive make_named() {
        Expensive result(1000);
        result.data.push_back(42);
        return result;  // NRVO 적용 시 복사 생략, 실패 시 move
    }
    
    // NRVO 차단 사례: 여러 지역 변수 중 조건 반환
    Expensive make_blocked(bool flag) {
        Expensive a(100), b(200);
        return flag ? a : b;  // 컴파일러가 어느 것을 caller 슬롯에 생성할지 결정 불가 → NRVO 불가
    }

    RVO vs NRVO 구분이 중요한 이유: C++17은 prvalue(임시 객체) 반환에 대해서만 복사 생략을 보장합니다. Named 지역 변수 반환(NRVO)은 최적화이지 보장이 아닙니다. 실패 시 C++11 move semantics가 비용을 줄여줍니다.

    메커니즘: 호출자가 반환 객체 공간을 미리 할당하고, 숨겨진 포인터로 전달하며, 피호출자가 해당 위치에 직접 생성합니다.

    저수준 언어의 최적화 특성까지 살펴보았으므로, 이제 세 언어 계열을 종합 비교합니다.


    5. 종합 비교 매트릭스

    5.1 언어별 Function/Procedure 구분

    언어 구분 방식 Function Procedure
    FORTRAN 키워드 분리 FUNCTION SUBROUTINE
    Pascal 키워드 분리 function f(): T procedure p()
    SQL 키워드 분리 CREATE FUNCTION CREATE PROCEDURE
    C/C++ 반환 타입 int f() void f()
    Go 반환 유무 func f() int func f()
    Python 반환 유무 def f() -> int def f() -> None
    JS/TS 반환 유무 function f(): number function f(): void
    Rust 반환 타입 fn f() -> i32 fn f() (→ ())
    Haskell 타입 시스템 f :: a -> b f :: a -> IO ()

    5.2 세 관점 핵심 차이

    관점 Function 핵심 Procedure 핵심 실무 지침
    SQL 쿼리 내 인라인 가능, 결정론 선언 트랜잭션 제어, DML 자유 읽기 전용 로직 → Function, 쓰기/트랜잭션 → Procedure
    Script 타입 어노테이션 (-> T), 순수성 권장 -> None/: void, 부수효과 CQS 원칙으로 Query/Command 분리
    Low-level 반환 레지스터(RAX) 사용, const 강제 void, 포인터로 상태 변경 ABI 레벨 차이 없음, 의도 표현이 핵심

    5.3 설계 원칙 체크리스트

    원칙 이론적 근거 실무 규칙
    순수 함수 우선 Lambda Calculus, 참조 투명성 테스트, 병렬화, 메모이제이션이 안전합니다. 부수효과는 시스템 경계로 밀어내십시오
    프로시저는 의도 표현 Turing 모델, FORTRAN SUBROUTINE void/None 반환은 "부수효과만 수행"을 의미합니다. 명령형 동사로 명명하십시오
    CQS 분리 Meyer's Eiffel getter에서 상태를 변경하지 마십시오. Query와 Command 메서드를 분리하십시오
    타입으로 의도 인코딩 Unit/Void/Never 타입 이론 () → () = 프로시저, A → B = 함수, A → Never = 발산입니다
    에러는 명시적으로 Go의 (value, error) 패턴 함수의 에러 경로를 타입에 반영하십시오. 조용한 실패를 방지하십시오

    5.4 결론

    Function과 Procedure의 구분은 궁극적으로 계산 의도의 표현에 관한 것입니다. Function은 값을 계산하여 반환하고, Procedure는 상태를 변경합니다. 이 구분은 1936년 Church의 lambda calculus와 Turing의 상태 기계에서 시작되어, FORTRAN의 키워드 분리, C의 void 통합, Haskell의 타입 시스템 격리까지 다양한 형태로 진화해 왔습니다.

    언어마다 이 구분을 인코딩하는 방식이 다르지만 — SQL은 구문적 키워드 분리, 스크립트 언어는 반환 타입 어노테이션, 저수준 언어는 ABI 규약 — 핵심 원칙은 동일합니다: 반환값이 있는 연산은 부수효과를 피하고, 부수효과가 있는 연산은 그 의도를 명시적으로 선언하십시오.


    6. 참고 문헌

    이론

    • Church, A. (1936). "An Unsolvable Problem of Elementary Number Theory"
    • Turing, A. (1936). "On Computable Numbers, with an Application to the Entscheidungsproblem"
    • Meyer, B. (1988). Object-Oriented Software Construction
    • Wadler, P. (1992). "Monads for Functional Programming"
    • Mac Lane, S. (1971). Categories for the Working Mathematician
    • Scott, D. and Strachey, C. (1971). "Toward a Mathematical Semantics for Computer Languages"

    SQL

    Script

    Low-level

    댓글

ABOUT ME

🎓 부산대학교 정보컴퓨터공학과 학사: 2017.03 - 2023.08
☁️ Rakuten Symphony Jr. Cloud Engineer, Full-time: 2024.12.09 - 2025.08.31
🏆 2025 AI 새싹톤 우수상 수상: 2025.10.30 - 2025.12.02
🌏 이코에코(Eco²) BE/AI(Harness)/Infra/FE 24-node E2E 고도화 및 운영, 2600만원 소모: 2025.12 - 2026.02
🪂 넥슨 AI 엔지니어(2-3년, 과제합 -> 면접 탈락), 무신사 AI-Native(전환형 인턴, 진행 X) 채용 프로세스: 2026.01.31 - 2026.03.05
🪂 GEODE/REODE 개발, Agentic Loop-based 자율 수행 하네스 + 도메인 특화 DAG(Plug-In), AI R&D Freelance @Pinxlab : 2026.03 - 2026.05

Designed by Mango