PHpullh
언어 비교/Function

LANGUAGE COMPARISON

Function를 언어별로 비교하기

같은 개념을 실제 예제로 나란히 확인하고, 각 언어의 개별 설명으로 이어갈 수 있습니다.

함수 선언에 들어 있는 네 가지 결정

함수는 어느 언어에나 있지만, 선언 한 줄이 담고 있는 정보량은 크게 다릅니다. 아래 열 개 언어를 비교할 때 문법 대신 다음 네 가지를 축으로 보시면 차이가 정리됩니다. 인자를 값으로 넘기는가 참조로 넘기는가, 이름이 같은 함수를 여러 개 둘 수 있는가, 반환값이 하나인가 여럿인가, 그리고 함수 자체를 값처럼 다룰 수 있는가입니다.

반환값 개수부터가 갈립니다. Go는 여러 값을 그대로 반환하며 마지막 자리에 오류를 두는 관례가 언어 문화 전체를 떠받칩니다. Python은 튜플을 돌려주고 호출부에서 풀어 씁니다. Java, C#, C++, Kotlin, JavaScript는 반환값이 하나뿐이라 여러 값을 내보내려면 객체나 배열로 묶어야 하고, 그래서 Java의 record나 Kotlin의 data class 같은 가벼운 묶음 타입이 필요해집니다.

오버로딩도 문화 차이를 만듭니다. Java, C#, C++, Kotlin은 같은 이름에 다른 시그니처를 여러 개 둘 수 있어 API가 이름 하나로 여러 형태를 흡수합니다. Python, JavaScript, Go에는 오버로딩이 없어서 기본 인자, 가변 인자, 또는 아예 다른 이름으로 문제를 풉니다. TypeScript는 타입 수준의 오버로드 선언을 허용하지만 구현은 하나이므로, 런타임에서 분기하는 코드는 직접 써야 합니다.

Kotlin

함수 선언 & 기본값 & Named Argument

기본값 파라미터로 오버로딩을 줄이고, Named Argument로 호출 가독성을 높입니다.

// 기본 함수
fun add(a: Int, b: Int): Int {
    return a + b
}

// 단일 표현식 함수 (타입 추론)
fun square(n: Int) = n * n

// 기본값 파라미터
fun greet(
    name: String,
    prefix: String = "안녕하세요",
    suffix: String = "님!"
) = println("$prefix, $name$suffix")

fun main() {
    greet("Kotlin")                    // 안녕하세요, Kotlin님!
    greet("Java", "Hello")             // Hello, Java님!

    // Named argument — 순서 무관, 명확한 호출
    greet(
        name = "Go",
        suffix = "!",
        prefix = "Hi"
    )

    // vararg
    fun sum(vararg nums: Int) = nums.sum()
    println(sum(1, 2, 3, 4, 5))        // 15

    // spread operator (배열 → vararg)
    val arr = intArrayOf(1, 2, 3)
    println(sum(*arr))
}

Python

함수 정의 & *args & **kwargs

가변 인자와 키워드 인자로 유연한 함수를 설계합니다.

# 기본 함수
def add(a: int, b: int) -> int:
    return a + b

# 기본값 파라미터
def greet(name: str, prefix: str = "안녕하세요") -> str:
    return f"{prefix}, {name}!"

# *args — 가변 위치 인자 (튜플로 받음)
def sum_all(*nums: int) -> int:
    return sum(nums)

print(sum_all(1, 2, 3, 4, 5))  # 15

# **kwargs — 가변 키워드 인자 (딕셔너리로 받음)
def create_user(name: str, **attrs) -> dict:
    return {"name": name, **attrs}

user = create_user("Alice", age=30, role="admin")
# {'name': 'Alice', 'age': 30, 'role': 'admin'}

# / 와 * 파라미터 구분자 (Python 3.8+)
def strict(pos_only: int, /, normal: str, *, kw_only: bool):
    pass

# pos_only는 위치 전용, kw_only는 키워드 전용
strict(1, "hi", kw_only=True)

# Unpacking
nums = [1, 2, 3]
print(sum_all(*nums))              # 15
cfg = {"age": 25, "role": "user"}
print(create_user("Bob", **cfg))

Go

함수 — 다중 반환값 & Named Return

Go 함수의 핵심 특징: 여러 값을 반환하고, 에러를 마지막 반환값으로 처리합니다.

package main

import (
	"errors"
	"fmt"
	"strconv"
)

// 다중 반환값 — Go의 관용구
func divide(a, b float64) (float64, error) {
	if b == 0 {
		return 0, errors.New("0으로 나눌 수 없습니다")
	}
	return a / b, nil
}

// Named return values
func minMax(nums []int) (min, max int) {
	min, max = nums[0], nums[0]
	for _, n := range nums[1:] {
		if n < min { min = n }
		if n > max { max = n }
	}
	return // naked return
}

// 가변 인자
func sum(nums ...int) int {
	total := 0
	for _, n := range nums { total += n }
	return total
}

// 함수를 값으로 (일급 함수)
func apply(fn func(int) int, vals []int) []int {
	result := make([]int, len(vals))
	for i, v := range vals {
		result[i] = fn(v)
	}
	return result
}

func main() {
	// 에러 처리 패턴
	result, err := divide(10, 3)
	if err != nil {
		fmt.Println("에러:", err)
	} else {
		fmt.Printf("%.4f
", result)
	}

	_, err = divide(5, 0)
	if err != nil { fmt.Println(err) }

	// Named return
	lo, hi := minMax([]int{3, 1, 4, 1, 5, 9, 2, 6})
	fmt.Println("min:", lo, "max:", hi)

	// 가변 인자
	fmt.Println(sum(1, 2, 3, 4, 5))
	nums := []int{1, 2, 3}
	fmt.Println(sum(nums...)) // 슬라이스 전개

	// 고차 함수
	doubled := apply(func(n int) int { return n * 2 }, nums)
	fmt.Println(doubled)

	_ = strconv.Itoa(0)
}

Java

메서드 참조 & 커스텀 함수형 인터페이스

:: 연산자로 메서드를 람다처럼 사용하고 커스텀 함수형 인터페이스를 정의합니다.

import java.util.*;
import java.util.function.*;

public class MethodRefs {

    // @FunctionalInterface — 단 하나의 추상 메서드
    @FunctionalInterface
    interface Transformer<T> {
        T transform(T input);
        default Transformer<T> andThen(Transformer<T> next) {
            return input -> next.transform(this.transform(input));
        }
    }

    @FunctionalInterface
    interface ThrowingSupplier<T> {
        T get() throws Exception;

        static <T> Supplier<T> wrap(ThrowingSupplier<T> s) {
            return () -> {
                try { return s.get(); }
                catch (Exception e) { throw new RuntimeException(e); }
            };
        }
    }

    static int doubleIt(int n) { return n * 2; }

    public static void main(String[] args) {
        var nums = List.of(1, 2, 3, 4, 5);

        // 정적 메서드 참조
        Function<Integer, Integer> dbl = MethodRefs::doubleIt;

        // 인스턴스 메서드 참조 (특정 객체)
        String prefix = "Hello";
        Predicate<String> startsWith = prefix::startsWith;

        // 인스턴스 메서드 참조 (임의 객체)
        Function<String, String> toUpper = String::toUpperCase;
        Function<String, Integer> length = String::length;

        // 생성자 참조
        Function<String, StringBuilder> sbFactory = StringBuilder::new;
        BiFunction<Integer, Integer, int[]> arrFactory = int[]::new;

        // 커스텀 함수형 인터페이스 사용
        Transformer<String> trim  = String::trim;
        Transformer<String> upper = String::toUpperCase;
        Transformer<String> pipeline = trim.andThen(upper);
        System.out.println(pipeline.transform("  hello  ")); // HELLO

        // ThrowingSupplier — checked exception 래핑
        Supplier<Properties> props = ThrowingSupplier.wrap(() -> {
            var p = new Properties();
            p.setProperty("key", "value");
            return p;
        });
        System.out.println(props.get().getProperty("key"));

        // 실용 예시
        nums.stream()
            .map(MethodRefs::doubleIt)
            .map(String::valueOf)
            .forEach(System.out::println);
    }
}

JavaScript

기본 함수 선언

add는 매개변수 두 개와 반환값에 모두 number를 붙인 가장 기본적인 함수 선언입니다. 인자 타입을 적어 두면 호출부에서 문자열을 넘겼을 때 실행 전에 잡힙니다. 반환 타입은 추론으로도 채워지지만, 명시해 두면 구현을 잘못 고쳤을 때 오류가 호출부가 아닌 함수 안에서 발생합니다.

// 기본 함수 선언
function add(a, b) {
  return a + b;
}
console.log(add(1, 2));

TypeScript

기본 함수 선언

add는 매개변수 두 개와 반환값에 모두 number를 붙인 가장 기본적인 함수 선언입니다. 인자 타입을 적어 두면 호출부에서 문자열을 넘겼을 때 실행 전에 잡힙니다. 반환 타입은 추론으로도 채워지지만, 명시해 두면 구현을 잘못 고쳤을 때 오류가 호출부가 아닌 함수 안에서 발생합니다.

// 기본 함수 선언
function add(a: number, b: number): number {
  return a + b;
}
console.log(add(1, 2));

C#

기본 함수 선언

add는 매개변수 두 개와 반환값에 모두 number를 붙인 가장 기본적인 함수 선언입니다. 인자 타입을 적어 두면 호출부에서 문자열을 넘겼을 때 실행 전에 잡힙니다. 반환 타입은 추론으로도 채워지지만, 명시해 두면 구현을 잘못 고쳤을 때 오류가 호출부가 아닌 함수 안에서 발생합니다.

// 기본 함수 선언
using System;

int Add(int a, int b) => a + b;
Console.WriteLine(Add(1, 2));

C++

기본 함수 선언

add는 매개변수 두 개와 반환값에 모두 number를 붙인 가장 기본적인 함수 선언입니다. 인자 타입을 적어 두면 호출부에서 문자열을 넘겼을 때 실행 전에 잡힙니다. 반환 타입은 추론으로도 채워지지만, 명시해 두면 구현을 잘못 고쳤을 때 오류가 호출부가 아닌 함수 안에서 발생합니다.

// 기본 함수 선언
#include <iostream>

int add(int a, int b) { return a + b; }
int main() {
    std::cout << add(1, 2) << "\n";
}

Rust

기본 함수 선언

add는 매개변수 두 개와 반환값에 모두 number를 붙인 가장 기본적인 함수 선언입니다. 인자 타입을 적어 두면 호출부에서 문자열을 넘겼을 때 실행 전에 잡힙니다. 반환 타입은 추론으로도 채워지지만, 명시해 두면 구현을 잘못 고쳤을 때 오류가 호출부가 아닌 함수 안에서 발생합니다.

// 기본 함수 선언
fn add(a: i32, b: i32) -> i32 { a + b }
fn main() {
    println!("{}", add(1, 2));
}

인자를 넘길 때 실제로 복사되는 것

"값 전달이냐 참조 전달이냐"는 질문은 대부분의 현대 언어에서 잘못 세워진 질문입니다. Python, Java, JavaScript, Go는 모두 값을 복사해서 넘기되, 그 값이 참조인 경우가 있는 방식입니다. 리스트를 넘긴 함수가 원소를 바꾸면 호출자에게 보이지만, 인자 이름에 새 리스트를 대입하면 호출자에게는 아무 일도 일어나지 않습니다. 이 두 문장이 동시에 참인 이유를 이해하면 관련된 혼란이 대부분 정리됩니다.

여기서 벗어나는 언어가 C++과 PHP입니다. C++은 값 복사, 참조(&), const 참조, 포인터를 문법으로 구분하며 큰 객체를 값으로 받으면 실제로 복사 비용이 발생합니다. PHP는 배열을 값 의미론으로 넘기므로 함수 안의 수정이 밖에 보이지 않고, 보이게 하려면 참조를 명시해야 합니다. Python 습관으로 PHP 함수를 쓰면 여기서 결과가 어긋납니다.

호출부를 읽기 좋게 만드는 장치

인자가 네다섯 개를 넘어가면 호출부는 의미를 잃습니다. createUser("kim", true, false, 3)에서 두 번째 불리언이 무엇인지 아무도 모릅니다. Python, Kotlin, C#, PHP는 이름 있는 인자를 지원해 호출부에서 의미를 되살릴 수 있습니다. Java와 Go에는 이 기능이 없어 옵션 구조체나 빌더 패턴으로 대신하는데, 문법이 없어서 생긴 우회가 오히려 표준 관용구가 된 경우입니다. 기능이 적은 언어일수록 패턴 이름이 많아지는 전형적인 예입니다.

  • Python의 가변 기본 인자 — 기본값은 함수 정의 시점에 한 번만 만들어집니다. 빈 리스트를 기본값으로 두면 호출할 때마다 같은 리스트가 재사용되어 값이 누적됩니다. None을 기본값으로 두고 본문에서 만드는 것이 정석입니다.
  • Go의 named return과 defer — 이름 있는 반환값은 defer 안에서 수정할 수 있습니다. 오류를 감싸는 데 유용하지만, 어디서 값이 바뀌었는지 추적하기 어려워지므로 짧은 함수에서만 쓰는 편이 낫습니다.
  • JavaScript의 this — 일반 함수는 호출 방식에 따라 this가 바뀌고 화살표 함수는 정의된 위치의 것을 그대로 씁니다. 메서드를 콜백으로 떼어 넘길 때 깨지는 원인이 대부분 이것입니다.
  • C++의 반환 타입 — 지역 객체를 참조로 반환하면 이미 파괴된 메모리를 가리킵니다. 컴파일은 통과하고 실행 중에 예측 불가능하게 실패합니다.

함수를 값으로 다루는 부분은 Lambda 비교에서, 실패를 어떻게 반환하는지는 Error 비교에서 이어집니다. 언어별 상세 문서는 Go 함수, Kotlin 함수, Python 함수에 있습니다.

함수의 크기와 경계

문법 비교와 별개로, 함수를 어디서 끊을지는 언어와 무관한 판단입니다. 실무에서 쓸 만한 기준 하나는 "이 함수가 하는 일을 접속사 없이 한 문장으로 설명할 수 있는가"입니다. "검증하고 저장하고 알림을 보낸다"처럼 설명에 접속사가 들어가면 이미 세 가지 일을 하고 있고, 테스트를 쓸 때 세 가지를 한꺼번에 준비해야 해서 어려워집니다.

또 하나는 인자와 반환값에 부작용을 숨기지 않는 것입니다. 값을 계산해 돌려주는 함수와 상태를 바꾸는 함수를 섞으면 호출 순서에 의미가 생겨 리팩터링이 위험해집니다. Go의 다중 반환과 Rust의 결과 타입이 실패까지 반환값으로 드러내는 것도 같은 방향의 설계입니다. 호출부만 읽어도 무슨 일이 일어나는지 짐작할 수 있게 만드는 것이 목표입니다.

마지막으로 문서화 습관도 언어별로 관례가 다릅니다. Go는 함수 바로 위 주석이 이름으로 시작하는 형식을 표준 도구가 그대로 문서로 뽑아 주고, Java는 Javadoc, Python은 독스트링, Kotlin은 KDoc이 같은 자리를 채웁니다. 공통점은 무엇을 하는지가 아니라 어떤 조건에서 실패하는지와 인자에 어떤 제약이 있는지를 적을 때 값이 크다는 것입니다. 시그니처만 읽어도 알 수 있는 내용을 다시 적은 주석은 금방 코드와 어긋납니다.