> THE-ARSENAL
> _

디지털 자산의 세계에서 우리가 매일같이 수행하는 가장 기본적인 행위는 '전송'이다. 지갑을 열고, 상대방의 주소를 입력하고, 수량을 적어 버튼을 누른다. 이 모든 과정의 배후에서, transfer라는 이름의 함수가 묵묵히 호출된다. 이더리움과 같은 스마트 컨트랙트 플랫폼 위에서 이 transfer 함수는 약속된 표준, 즉 ERC-20이라는 규약의 일부다. 그것은 토큰이 토큰으로서 기능하기 위한 최소한의 인터페이스이며, 모든 지갑과 거래소가 우리를 대신해 상호작용하는 공용 출입문이다.

 

이 함수는 겉보기엔 지극히 단순하다. transfer(address to, uint amount). 누구에게, 얼마를 보낼 것인가. 명확하다. 하지만 이 명확한 표면 아래에는, 잘 설계된 소프트웨어 엔지니어링의 정수가 숨어있다. 우리가 호출하는 그 transfer 함수는, 사실 '진짜' 전송 로직이 아닐 가능성이 높다. 그것은 단지 우아하게 디자인된 '포장지'에 불과하다.

 

 

예쁜 포장지와 안전 스티커

우리가 만나는 public 함수, 즉 외부 세계에 공개되어 누구나 호출할 수 있는 함수는 사용자와 직접 대면하는 '쇼윈도'다. transfer 함수는 이 쇼윈도의 가장 눈에 띄는 자리에 위치한 상품이다. 표준화된 규격 덕분에 누구나 이 상품을 어떻게 사용해야 하는지 즉시 알 수 있다.

 

이 '포장지'의 첫 번째 임무는 선물을 전달하는 것이 아니라, 선물을 전달해도 되는지 확인하는 것이다. 이것이 바로 require 구문, 즉 '안전 스티커'의 역할이다. 코드를 열어보면, 이 public 함수들의 상단에는 언제나 촘촘한 검증 로직이 자리 잡고 있다.

 

require(to != address(0), "ERC20: transfer to the zero address")

require(balanceOf[msg.sender] >= amount, "ERC20: transfer amount exceeds balance")

 

이 검증들은 지극히 합리적이다. 주소가 없는 곳(0번 주소)으로 토큰을 보내는 실수를 막는다. 이는 사실상 토큰을 소각하는 행위와 같지만, 사용자의 의도된 실수가 아닐 가능성이 높기에 시스템이 한 번 더 확인해주는 것이다. 그리고 당연하게도, 보내는 사람이 가진 잔고보다 더 많은 금액을 보낼 수 없도록 막는다.

 

이 '안전 스티커' 단계는 중요하지만, 이것은 본질적인 '전송' 행위와는 다르다. 이것은 '검증'의 영역이다. 보안 업계에서 가장 기본이 되는 '체크-이펙트-인터랙션(Checks-Effects-Interactions)' 패턴의 첫 번째 단계인 '체크(Checks)'에 해당한다. 시스템의 상태를 변경하기 전에, 모든 조건이 유효한지 확인하는 것이다. 이 포장지는 화려하진 않지만, 내용물을 안전하게 보호하는 가장 중요한 1차 방어선이다.

 

 

포장지 속의 진짜 선물, _transfer

이 모든 검증, 즉 '안전 스티커'가 무사히 통과되고 나면, transfer 함수는 드디어 자신의 본래 임무를 수행한다. 그런데 그 임무라는 것이 놀랍도록 간단하다. transfer 함수는 자신이 직접 잔고를 계산하고 장부를 수정하지 않는다. 대신, 내부의 다른 함수를 호출하며 모든 일을 넘겨버린다.

 

_transfer(msg.sender, to, amount);

바로 이것이 포장지 속에 숨겨진 '진짜 선물'이자, 이 글의 핵심인 _transfer 함수다. 왜 이런 번거로운 분리를 하는 것일까? 왜 transfer 함수가 직접 장부를 수정하지 않고, 굳이 _가 붙은 내부 함수를 한 번 더 호출하는 것일까?

 

가장 중요한 차이는 _transfer가 public 함수가 아니라는 점이다. 이것은 internal 혹은 private으로 선언된 보조 함수(Helper Function)다. public 함수가 누구나 드나들 수 있는 백화점 1층 정문이라면, internal 함수는 '직원 전용' 배지를 찍어야만 들어갈 수 있는 백오피스(Back Office)다.

 

transfer 함수는 msg.sender라는 마법 같은 전역 변수를 통해 '누가 이 함수를 호출했는지' 즉시 알 수 있다. 하지만 _transfer는 internal 함수이기에 msg.sender에 직접 접근할 수 없다. 따라서 transfer 함수는 '누가(msg.sender)', '누구에게(to)', '얼마나(amount)'라는 세 가지 정보를 모두 모아, 이 핵심 로직을 처리하는 전문가(_transfer)에게 안전하게 전달한다. transfer는 검증 담당자, _transfer는 실행 담당자인 셈이다.

 

 

두 번 일하지 않는 법: 로직의 재사용

이러한 분리의 진정한 아름다움은 '재사용성'에서 드러난다. ERC-20 표준에는 transfer 말고도 토큰을 전송하는 또 다른 함수가 존재한다. 바로 transferFrom(address from, address to, uint amount)이다.

 

이 함수는 내가 아닌, '다른 사람(from)'의 토큰을 '그 사람의 허락(allowance)을 받아' 제3자(to)에게 전송하는 기능이다. 스마트 컨트랙트가 사용자를 대신해 토큰을 지불하는 디파이(DeFi)의 핵심 기능이다.

 

transferFrom 함수 역시 자신만의 '안전 스티커'를 가지고 있다.

require(allowance[from][msg.sender] >= amount, "ERC20: transfer amount exceeds allowance")

 

이 함수는 '잔고'가 아니라 '허용량'을 검사한다. 이 검증을 통과한 뒤, transferFrom은 무엇을 해야 할까? from 주소의 잔고를 줄이고 to 주소의 잔고를 늘려야 한다. 이 로직, 어디서 많이 보지 않았는가? 그렇다. _transfer가 하는 일과 정확히 동일하다.

 

여기서 만약 _transfer라는 분리된 엔진이 없다면, transferFrom 함수 내부에 잔고를 수정하는 로직을 transfer에서 복사-붙여넣기 해야 할 것이다. 동일한 코드가 두 개의 다른 함수에 중복으로 존재하게 된다. 이는 유지보수의 재앙이다. 만약 잔고를 수정하는 로직에 미세한 버그가 발견된다면, 개발자는 두 곳을 모두 찾아 수정해야 한다. 하나라도 놓친다면, 시스템은 즉시 위험에 처한다.

 

하지만 _transfer라는 핵심 엔진이 분리되어 있다면, 이야기는 간단해진다. transferFrom 역시 자신의 검증이 끝난 후, _transfer(from, to, amount)를 호출하면 그만이다.

 

transfer -> _transfer(msg.sender, ...)

transferFrom -> _transfer(from, ...)

두 개의 다른 공용 출입문(transfer, transferFrom)이 각기 다른 검증(잔고 검사, 허용량 검사)을 거친 뒤, 결국 하나의 잘 만들어진 핵심 엔진(_transfer)을 공유하여 사용하는 것이다. 이것이 '두 번 일하지 않는 법(Don't Repeat Yourself, DRY)' 원칙이며, _ 함수를 사용하는 가장 강력한 이유다.

 

 

_의 약속: 내부 전용 엔진 룸

솔리디티에서 _ (언더스코어)로 시작하는 함수는 강력한 '관례(Convention)'다. 이것은 "이 함수는 내부용(Internal/Private)이니, 외부 세계는 이 존재를 알 필요도 없고, 알아서도 안 된다"는 개발자들 사이의 약속이다. 파이썬과 같은 다른 언어에서 가져온 이 관례는, 코드의 가독성과 보안성을 극적으로 높여준다.

 

_transfer와 같은 internal 함수들은 계약의 외부 인터페이스(ABI)에 포함되지 않는다. 즉, 사용자가 지갑을 통해 _transfer를 직접 호출할 방법은 원천적으로 존재하지 않는다.

 

이것은 핵심 로직을 보호하는 행위다. _transfer는 transfer와 transferFrom이라는 '신뢰할 수 있는' 내부 동료들로부터만 호출된다고 가정한다. 이 '신뢰'가 있기에, _transfer는 이미 겉에서 1차 검증이 끝났다고 보고, 오직 '잔고 수정'이라는 자신의 본질적인 임무에만 집중할 수 있다.

 

_ 함수들은 계약의 '엔진 룸'이다. _transfer, _mint(토큰 생성), _burn(토큰 소각) 같은 함수들은 시스템의 심장부이며, 강력한 만큼 위험하다. 이 엔진들이 외부의 오염된 입력값에 의해 직접 호출된다면 시스템 전체가 폭주할 수 있다.

 

따라서 public 함수들은 이 엔진 룸과 외부 세계를 연결하는 '안전한 제어판' 역할을 한다. 사용자는 제어판의 버튼(transfer)만 누를 수 있다. 버튼이 눌리면, 제어판은 모든 안전 수칙을 검사한 뒤, 깨끗하고 검증된 신호를 엔진 룸(_transfer)으로 보낸다.

 

결론적으로, transfer가 _transfer를 호출하는 것은 코드를 복잡하게 만드는 것이 아니라, 오히려 코드를 '단순하게' 만드는 고도로 세련된 설계다. 그것은 '검증'과 '실행'이라는 책임을 명확히 분리하고, '재사용성'을 극대화하며, '보안성'을 높이는 '우아한 포장술'인 것이다. 우리가 무심코 누르는 '전송' 버튼 뒤에는, 이처럼 보이지 않는 곳에서 묵묵히 돌아가는 수많은 _ 엔진들이 있다.

 

 

3줄 요약

[1] transfer는 사용자가 호출하는 '공개용(public)' 함수로, require를 통한 '검증'이 주 임무인 포장지다.

[2] _transfer는 internal 함수로, 실제 잔고를 변경하는 '핵심 로직'을 담은 엔진 룸이다.

[3] 이렇게 분리하는 이유는 transferFrom 같은 다른 함수도 _transfer 엔진을 '재사용'할 수 있게 하여, 코드의 중복을 막고 보안과 유지보수성을 높이기 위함이다.