인자를 넘길 때 실제로 복사되는 것
"값 전달이냐 참조 전달이냐"는 질문은 대부분의 현대 언어에서 잘못 세워진 질문입니다. 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이 같은 자리를 채웁니다. 공통점은 무엇을 하는지가 아니라 어떤 조건에서 실패하는지와 인자에 어떤 제약이 있는지를 적을 때 값이 크다는 것입니다. 시그니처만 읽어도 알 수 있는 내용을 다시 적은 주석은 금방 코드와 어긋납니다.