> THE-ARSENAL
> _

코드를 읽는 건지, 수학 공식을 해독하는 건지 알 수 없던 구버전 문법

코드에는 미학이 존재한다. 잘 짜인 코드는 마치 잘 재단된 수트처럼 군더더기가 없고, 흐름이 유려하며, 읽는 이로 하여금 그 의도를 명확하게 파악하게 한다. 하지만 불과 몇 년 전까지만 해도, 스마트 컨트랙트 개발의 최전선인 솔리디티(Solidity) 생태계는 이러한 미학과는 거리가 멀었다. 그것은 차라리 누더기에 가까웠다. 우리는 덧셈을 덧셈이라 부르지 못하고, 뺄셈을 뺄셈이라 적지 못하는 기이한 시대를 살았다.

상상해 보라. 당신이 간단한 금융 로직을 작성하고 있다. "내 잔고에 100원을 더한다"는 아주 기초적인 연산이다. 일반적인 프로그래밍 언어, 아니 초등학생의 수학 노트에서도 이것은 balance + 100으로 표현된다. 직관적이고 명쾌하다. 하지만 솔리디티 0.8.0 버전 이전의 세계, 특히 디파이(DeFi) 붐이 일던 그 시절의 코드는 흡사 암호문 같았다. balance.add(100).

이것 하나라면 참을 만하다. 하지만 금융 로직은 복잡하다. "A값에 B를 더하고, 그 결과에 C를 곱한 뒤 D로 나눈다"는 로직을 작성한다고 치자. 우리가 기대하는 우아한 문장은 (A + B) * C / D이다. 그러나 당시 우리가 마주해야 했던 현실은 A.add(B).mul(C).div(D)라는, 객체 지향의 탈을 쓴 기괴한 메서드 체이닝의 향연이었다. 괄호가 중첩되고, 온점이 난무하며, 연산자 우선순위는 시각적으로 뭉개진다. 개발자는 로직의 흐름을 읽는 것이 아니라, 괄호의 짝을 맞추고 함수 이름을 해독하는 데 뇌 용량을 낭비해야 했다.

이것은 단순한 불편함의 문제가 아니었다. 이것은 가독성에 대한 폭력이나 다름없었다. 코드가 복잡해질수록 버그가 숨을 공간은 넓어진다. 덧셈 기호 +가 주는 그 직관적인 명료함을 포기하고, 모든 산술 연산을 함수 호출로 대체해야 했던 그 시절, 우리는 코드를 '읽는' 것이 아니라 '해독'하고 있었다. 수많은 괄호 속에 파묻혀 정작 중요한 비즈니스 로직, 즉 돈이 어디서 어디로 흐르는지는 희미해졌다.

이러한 문법적 퇴행은 역설적이게도 '안전'이라는 명분 아래 자행되었다. 블록체인이라는, 한 번 기록되면 되돌릴 수 없는 불변의 장부 위에서 숫자가 넘쳐흐르는 '오버플로우(Overflow)' 사고는 곧장 자산 증발로 이어지기 때문이다. 우리는 그 공포에 질려 아름다움을 포기하고, 덕지덕지 안전장치를 붙인 투박한 갑옷을 입기로 했던 것이다. 그 갑옷의 이름이 바로 전설적인, 그리고 이제는 역사 속으로 사라져 가는 SafeMath 라이브러리다.

SafeMath 라이브러리 뜯어보기, 사실은 단순한 require 문이었다

그 시절, 모든 솔리디티 파일의 최상단에는 마치 부적처럼 붙어 있던 문구가 있었다. using SafeMath for uint256;. 이 한 줄이 없으면 불안해서 배포조차 할 수 없었던 시절이다. 초보 개발자들에게 SafeMath는 마치 마법의 방패처럼 여겨졌다. 이 라이브러리만 쓰면 해커가 내 돈을 훔쳐 가는 것을 막아주고, 수학적 오류로부터 나를 구원해 줄 것이라는 막연한 믿음이 있었다.

하지만 그 '마법'의 뚜껑을 열어보면, 그 안에는 허무할 정도로 단순한 기계적 장치만이 들어 있었다. SafeMath의 .add(b) 함수 내부를 들여다본 적이 있는가? 그곳엔 고도의 수학적 알고리즘이나 블록체인의 심오한 원리가 숨어 있지 않았다. 그저 아주 투박하고 원초적인 조건문 하나가 덩그러니 놓여 있을 뿐이었다.

function add(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 c = a + b;
    require(c >= a, "SafeMath: addition overflow");
    return c;
}

이것이 전부다. 두 수를 더한 결과(c)가 원래의 수(a)보다 작아졌는지 확인하는 것. 컴퓨터 구조론 기초 시간에 배우는 정수 오버플로우의 원리, 즉 '양수끼리 더했는데 갑자기 작아지거나 음수가 되면 오버플로우'라는 그 단순한 논리를 코드로 옮겨 놓은 것에 불과했다. 뺄셈은 더 단순하다. 뺄려는 수가 원래 수보다 큰지 확인하는 require(b <= a) 한 줄이 핵심이었다.

우리는 이 단순한 require 문, 즉 "이 조건이 안 맞으면 에러를 내고 멈춰라"라는 명령어를 매번 치기 귀찮아서, 그리고 실수로 빼먹을까 봐 두려워서 거대한 라이브러리에 의존했던 것이다. 물론 소프트웨어 공학에서 코드 재사용과 모듈화는 권장되는 덕목이다. 하지만 언어의 가장 기초적인 구성 요소인 '산술 연산'조차 외부 라이브러리에 의존해야 한다는 것은, 그 언어가 아직 미성숙하다는 방증이기도 했다.

SafeMath는 분명 훌륭한 역할을 수행했다. 수많은 오버플로우 공격을 막아냈고, 개발자들에게 심리적 안정감을 주었다. 하지만 그 대가는 혹독했다. 코드는 비대해졌고, 가스비(Gas Fee)는 증가했다. 함수 호출 스택이 깊어질수록 이더리움 가상 머신(EVM)은 더 많은 연산 비용을 청구했다. 단순한 덧셈 한 번을 위해 라이브러리를 로드하고, 함수를 호출하고, 조건문을 검사하고, 값을 반환하는 일련의 과정은 비효율의 극치였다.

우리는 안전을 샀지만, 그 대가로 직관성과 효율성을 지불했다. 개발자들은 a + b라고 쓰고 싶은 본능을 억누르며 a.add(b)를 타이핑했다. 그것은 마치 말을 할 때마다 문법 교정기를 거쳐야 하는 것과 같은 답답함이었다. 언어 자체가 안전하지 않아서, 사용자가 스스로 안전장치를 덕지덕지 붙여야만 했던 그 시절은 솔리디티 개발자들에게 있어 일종의 '야만의 시대'였다.

왜 비탈릭과 이더리움 재단은 처음부터 오버플로우 체크를 넣지 않았을까?

여기서 의문이 든다. 이더리움을 창시한 비탈릭 부테린과 똑똑한 재단 개발자들은 왜 처음부터 오버플로우 체크 기능을 언어 차원에서 제공하지 않았을까? 파이썬(Python) 같은 현대적인 언어들은 숫자가 커지면 알아서 메모리를 더 할당하거나, 오버플로우가 발생하면 예외를 던진다. 자바(Java)조차도 오버플로우를 허용하긴 하지만, 적어도 개발자들이 그 위험성을 인지하고 제어할 수 있는 수단들이 있다. 왜 솔리디티는 그토록 오랫동안 '침묵하는 오버플로우'를 방치했을까?

그 답은 이더리움의 태생적 환경, 바로 '가스비'와 'EVM의 설계 철학'에서 찾을 수 있다. 이더리움 초창기, 가장 중요한 지상 과제는 '효율성'이었다. 블록체인 네트워크 위의 연산 자원은 매우 비싸고 한정적이다. 모든 노드가 똑같은 연산을 중복 수행해야 하는 구조 탓에, 불필요한 CPU 사이클 하나하나가 곧 비용이었다.

EVM은 256비트(32바이트) 머신이다. 256비트 정수는 상상할 수 없을 정도로 큰 수다. 살짝 과장하면 우주의 원자 개수 만큼이나 많은 수를 표현할 수 있다. 설계자들은 생각했을 것이다. "이렇게 큰 범위의 숫자를 다루는데, 일반적인 거래에서 오버플로우가 일어날 확률이 얼마나 되겠어? 굳이 모든 덧셈 연산마다 값비싼 검사 코드를 강제로 넣어서 가스비를 낭비할 필요가 있을까?"

그들은 개발자의 '자율성'과 '성능'을 선택했다. 오버플로우 검사가 필요한 곳에만 개발자가 알아서 검사 코드를 넣으라는 것이었다. 그것이 가스비를 아끼는 길이라고 믿었다. 실제로 언어 차원에서 강제적인 오버플로우 검사를 수행하려면, 모든 산술 연산코드(Opcode) 앞뒤로 분기(Branch) 명령어가 추가되어야 한다. 이는 스마트 컨트랙트의 크기를 키우고 실행 비용을 높인다. 초기 이더리움 네트워크의 척박한 환경에서 이는 사치스러운 기능으로 보였을지 모른다.

하지만 그들은 인간의 탐욕과 실수를 과소평가했다. 256비트 정수는 크지만, 해커들은 그 틈을 파고들었다. 2018년, '뷰티체인(BeautyChain)' 사건이 터졌다. 해커는 batchTransfer라는 함수에서 오버플로우 취약점을 이용해 조 단위의 토큰을 허공에서 생성해냈다. 아주 단순한 덧셈 오버플로우 하나가 수천억 원의 가치를 휴지 조각으로 만들 수 있음을 전 세계가 목격했다.

이 사건들은 패러다임을 바꾸어 놓았다. "약간의 가스비를 아끼는 것"보다 "보안 사고 한 번을 막는 것"의 가치가 비교할 수 없을 만큼 크다는 것을 깨달은 것이다. 개발자에게 안전을 맡기는 '자율'은 실패했다. 사람은 누구나 실수를 한다. 특히 돈이 걸린 문제에서 실수는 용납되지 않는다. 결국 이더리움 커뮤니티는 성능을 조금 희생하더라도, '안전한 기본값(Safe Default)'을 제공하는 것이 옳다는 결론에 도달하게 된다. 그것이 바로 긴 논의 끝에 0.8.0 버전의 업데이트가 결정된 배경이다. 효율성이라는 신화가 무너지고, 안전성이라는 새로운 신이 등극한 순간이었다.

버전 0.8.0의 혁명, 드디어 + 기호가 우리가 아는 덧셈으로 돌아오다

2020년 말, 솔리디티 0.8.0 버전이 발표되었을 때, 개발자 커뮤니티는 환호했다. 릴리즈 노트의 수많은 항목 중에서도 가장 빛나는 한 줄은 바로 "산술 연산 시 오버플로우/언더플로우 자동 체크(Checked Arithmetic by Default)"였다. 이것은 단순한 기능 추가가 아니었다. 이것은 개발자 경험(DX)의 위대한 승리이자, 코드의 가독성을 되찾아준 문법적 광복이었다.

이제 우리는 더 이상 a.add(b)를 쓰지 않아도 된다. 당당하게 a + b를 입력하면 된다. 컴파일러는 이 코드를 기계어로 번역할 때, 자동으로 SafeMath가 하던 일(검증 로직)을 삽입해 준다. 결과값이 범위를 벗어나면 트랜잭션은 자동으로 실패(Revert)한다. 별도의 라이브러리를 임포트(import)할 필요도, using SafeMath for ... 구문을 선언할 필요도 없어졌다.

코드는 극적으로 간결해졌다. (A + B) * C / D라는 수식이 본연의 모습을 되찾았다. 개발자는 다시 수학적 사고의 흐름대로 코드를 작성할 수 있게 되었다. 괄호의 늪에서 빠져나와 로직 자체에 집중할 수 있게 된 것이다. 이는 코드 리뷰의 효율성도 비약적으로 높였다. 감사(Audit)를 진행할 때, 지루한 SafeMath 적용 여부를 체크하는 대신 비즈니스 로직의 허점을 찾는 데 더 많은 시간을 쏟을 수 있게 되었다.

더 놀라운 것은, 이 변화가 가져온 심리적 해방감이다. 이전에는 뺄셈을 할 때마다 "혹시 뺄려는 수가 더 크지 않을까?"라며 강박적으로 require 문을 확인해야 했다. 하지만 이제는 언어가 나를 지켜준다. 기본값이 '안전함'으로 설정되어 있다는 것은 개발자에게 엄청난 자신감을 부여한다. 실수를 해도 시스템이 막아줄 것이라는 믿음, 그것이 개발 속도를 높이고 더 창의적인 시도를 가능하게 한다.

물론, 가스비 최적화가 극한으로 필요한 경우에는 여전히 오버플로우 검사를 끄고 싶을 때가 있다. 솔리디티 팀은 이를 위해 unchecked { ... } 블록을 제공했다. 안전을 기본으로 하되, 전문가들이 필요할 때만 안전장치를 해제할 수 있도록 '선택적 위험'을 허용한 것이다. 이는 "모두가 위험하고, 안전하려면 노력해야 한다"는 구시대적 발상에서, "모두가 안전하고, 위험하려면 의도해야 한다"는 현대적 철학으로의 완벽한 전환이다.

0.8.0의 등장은 단순히 문법의 변화가 아니었다. 그것은 블록체인 개발 생태계가 성숙해졌음을 알리는 신호탄이었다. 더 이상 우리는 기본적인 산술 연산조차 두려워해야 하는 초기 개척자가 아니다. 우리는 이제 안전하게 포장된 도로 위를 달리는 드라이버가 되었다. + 기호의 부활은, 기술이 인간을 위해, 인간의 인지 능력에 맞춰 진화해 나가는 과정을 보여주는 가장 아름다운 사례다.

레거시 코드와의 전쟁, 구버전 코드를 포크할 때 반드시 수정해야 하는 이유

그러나 혁명에는 항상 잔재가 남는다. 블록체인, 특히 디파이(DeFi) 생태계는 '포크(Fork)'의 문화다. 유니스왑(Uniswap) 같이 검증된 명작 코드를 복사해서 새로운 프로젝트를 시작하는 것이 관례처럼 굳어져 있다. 문제는 이 명작들이 대부분 0.8.0 이전의 '야만의 시대'에 작성되었다는 점이다.

많은 신규 프로젝트들이 구버전 솔리디티(0.5.x, 0.6.x)로 작성된 코드를 그대로 가져와서, 컴파일러 버전만 최신(0.8.x)으로 바꾸는 실수를 범한다. 여기서 치명적인 비효율과 잠재적 위험이 발생한다.

첫째, 이중 안전장치의 낭비다. 0.8.x 버전에서 SafeMath를 사용하는 것은, 이미 방탄조끼를 입은 사람에게 또 하나의 방탄조끼를 입히는 것과 같다. 안전하긴 하겠지만, 무겁고 둔하다. 컴파일러가 자동으로 오버플로우 체크를 하는데, SafeMath 라이브러리가 또다시 체크를 수행한다. 이는 가스비의 이중 낭비다. 사용자는 불필요한 연산 비용을 지불해야 한다. 레거시 코드를 포크할 때는 반드시 SafeMath를 제거하고 네이티브 연산자(+, -)로 교체하는 리팩토링 작업을 거쳐야 한다. 이것은 선택이 아니라 의무다.

둘째, unchecked 블록의 오용이다. 구버전 코드를 그대로 옮겨오면서 가스비를 아끼겠답시고 무턱대고 unchecked 블록으로 감싸는 경우가 있다. "어차피 원본 코드가 잘 돌아갔으니까"라는 안일한 생각이다. 하지만 원본 코드는 SafeMath가 지켜주고 있었을지 모르지만, 당신이 수정한 로직은 무방비 상태일 수 있다. unchecked는 정말로 오버플로우가 발생하지 않는다는 수학적 증명이 끝난 곳에만 사용해야 하는, 일종의 '전문가용 모드'다. 이를 남용하는 것은 과거의 뷰티체인 사태를 다시 불러오는 초대장이 될 수 있다.

셋째, 가독성의 혼종이다. 어떤 파일은 a.add(b)를 쓰고, 어떤 파일은 a + b를 쓰는 프로젝트들이 있다. 이는 유지보수의 악몽이다. 팀원 간의 컨벤션이 무너지고, 코드를 읽는 리듬이 끊긴다. 레거시 코드를 다루는 태도는 그 팀의 기술적 역량을 보여주는 척도다. 단순히 "돌아가니까 놔둔다"는 태도는 금융 사고의 씨앗이 된다. 과거의 유산은 존중하되, 그것을 현대의 문법으로 재해석하고 번역하는 작업, 그것이 바로 지금의 블록체인 개발자들이 치러야 할 '레거시와의 전쟁'이다.

현대 Solidity 개발자가 누리는 '기본값(Default)의 안전함'에 대하여

우리는 지금 솔리디티 개발 역사상 가장 풍요롭고 안전한 시대를 살고 있다. a + b라고 적는 것만으로도 수많은 보안 위협으로부터 보호받는다. 이것은 단순한 기술적 진보를 넘어, 개발 문화의 성숙을 의미한다. '기본값(Default)'이 무엇이냐는 인간의 행동 양식을 결정한다.

과거의 기본값이 '위험'이었을 때, 개발자는 항상 긴장하고 방어적이어야 했다. 모든 코드가 의심의 대상이었다. 하지만 이제 기본값은 '안전'이다. 이 변화는 개발자가 더 높은 차원의 문제, 즉 비즈니스 로직의 혁신과 사용자 경험 개선에 집중할 수 있게 해 준다. 우리는 더 이상 덧셈이 터질까 봐 걱정하는 대신, 이 금융 상품이 사용자에게 어떤 가치를 줄 것인가를 고민할 수 있게 되었다.

'a.add(b)'에서 'a + b'로의 변화. 어찌 보면 사소해 보이는 이 문법적 변화 하나가 가져온 파급력은 실로 거대하다. 그것은 코드를 간결하게 만들었고, 가스비를 최적화했으며, 무엇보다 개발자에게 심리적 자유를 주었다. 이것은 개발자 경험(DX)의 위대한 승리다. 기술은 결국 인간을 향해야 하고, 인간이 쓰기 편한 도구가 살아남는다.

우아한 코드가 안전한 코드다. 복잡하고 난해한 구문 속에 숨어 있던 버그들은, 이제 간결하고 명징한 수식 앞에서 설 곳을 잃었다. 앞으로 등장할 수많은 블록체인 서비스들이 이 '기본값의 안전함' 위에서 더욱 과감하고 혁신적인 시도를 쌓아 올리기를 기대한다. 우리는 이제, 코드를 다시 '읽을' 수 있게 되었으니까.

3줄 요약

  1. 과거 솔리디티 개발은 오버플로우 방지를 위해 a.add(b) 같은 SafeMath 라이브러리를 강제로 써야 했고, 이는 가독성을 심각하게 해쳤다.
  2. 0.8.0 버전 업데이트로 언어 자체에 오버플로우 체크가 기본 탑재되면서, 개발자들은 다시 직관적인 a + b 문법을 안전하게 사용할 수 있게 되었다.
  3. 이는 단순한 문법 변화가 아니라 개발자의 인지 부하를 줄이고 비즈니스 로직에 집중하게 만든 '개발자 경험(DX)'의 혁명적인 승리다.