transfer / send / call 비교
컨트랙트에서 다른 주소로 Ether 를 보내는 방법은 세 가지가 있습니다. transfer, send, call
입니다. 셋 다 결국 "돈을 보낸다"는 같은 일을 하지만, 실패했을 때의 동작과 전달되는 가스의
양이 서로 다릅니다. 이 차이가 곧 컨트랙트의 안전성과 호환성을 좌우하므로 정확히 구분해야 합니다.
transfer, send, call 의 차이
| 방법 | 실패 시 동작 | 가스 한도 | 권장 여부 |
|---|---|---|---|
transfer | 자동 revert | 2300 gas 고정 | 제한적 |
send | false 반환 | 2300 gas 고정 | 비권장 |
call | false 반환 | 가스 전달 조절 가능 | 권장 |
transfer: 송금이 실패하면 자동으로 전체 트랜잭션을 되돌립니다(revert). 별도 검사 없이도 안전해 보이지만, 단 2300 gas 만 전달합니다.send: 실패해도 revert 하지 않고false를 반환만 합니다. 따라서 반환값을 직접 확인하지 않으면 송금이 실패했는데도 코드가 그냥 진행되는 위험한 버그가 생깁니다.call: 가스를 자유롭게 전달할 수 있고 가장 유연합니다. 실패 시false를 반환하므로 반드시 결과를 확인해야 하며, 오늘날 표준으로 권장되는 방식입니다.
아래 예제를 Run 하면 transfer·send 줄에서 'transfer' is deprecated and scheduled for removal
경고가 나옵니다. 오류가 아니라 경고이지만, 컴파일러가 직접 이 둘을 쓰지 말라고 알려 주는
것입니다. 셋의 차이를 이해하기 위해 예제는 그대로 두었고, 실제 코드에서는 call 만 쓰면 됩니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Sender {
// 1) transfer: 실패하면 자동으로 revert
function sendByTransfer(address payable to) public payable {
to.transfer(msg.value);
}
// 2) send: 실패하면 false 반환 -> 직접 확인 필요
function sendBySend(address payable to) public payable {
bool ok = to.send(msg.value);
require(ok, "send failed"); // send 실패
}
// 3) call: 가장 유연하며 현재 권장되는 방식
function sendByCall(address payable to) public payable {
(bool ok, ) = to.call{value: msg.value}("");
require(ok, "call failed"); // call 실패
}
}왜 call 이 권장되는가
transfer 와 send 가 전달하는 2300 gas 는 단순한 로그 한 줄 정도만 처리할 수 있는 아주
적은 양입니다. 받는 쪽이 컨트랙트인데 그 receive/fallback 안에 조금이라도 복잡한 로직이 있으면
가스가 부족해 송금이 실패합니다. 과거에는 이 고정 가스가 재진입 공격을 막는 안전장치로 여겨졌지만,
이더리움 가스 비용이 바뀌면서 오히려 정상적인 컨트랙트로의 송금까지 깨뜨리는 문제가 되었습니다.
그래서 가스를 충분히 전달할 수 있는 call 이 현재의 표준이 되었습니다.
call 은 강력한 만큼 재진입(reentrancy) 공격에 주의해야 합니다. 송금하기 전에 먼저 상태
변수를 갱신하는 Checks-Effects-Interactions 패턴(검사 -> 상태 변경 -> 외부 호출 순서)을
지키면 재진입 공격을 효과적으로 막을 수 있습니다.