FAQ

Week 6: 스마트 계약 심화 - 자주 묻는 질문

대전대학교 대학원 | 블록체인 기술 특론 | 연삼흠 교수 | WIA-FIN-007

데이터 구조 (mapping, struct)

Q1. mapping에서 키가 존재하는지 어떻게 확인하나요?

mapping은 모든 키에 대해 기본값을 반환하므로, 존재 여부를 직접 확인할 수 없습니다. 해결 방법:

  • struct에 bool exists 필드를 추가하여 플래그로 관리
  • 별도의 mapping(key => bool)을 유지
  • 값이 기본값(0, false, "")인지 확인 (단, 실제로 기본값을 저장한 경우와 구분 불가)

Q2. mapping을 순회(iterate)할 수 없는 이유는?

mapping은 해시 기반 자료구조로, 어떤 키가 설정되었는지 추적하지 않습니다. 모든 가능한 키에 대해 기본값이 존재하는 것으로 간주합니다. 순회가 필요하면 별도의 배열에 키를 저장해야 합니다:

address[] public keys;

OpenZeppelin의 EnumerableMap 라이브러리를 사용하면 순회 가능한 mapping을 구현할 수 있습니다.

Q3. struct를 mapping의 값으로 사용할 때 주의점은?

struct를 mapping에 저장하면 storage에 저장됩니다. 주의점:

  • struct 내에 mapping이나 동적 배열을 포함할 수 있지만, 초기화 방식이 다름
  • struct를 통째로 반환하려면 함수에서 각 필드를 개별 반환하거나, ABI encoder v2 사용
  • 중첩 mapping(mapping(a => mapping(b => c)))은 가능하지만 가스비 주의

Q4. 배열과 mapping 중 어떤 것을 사용해야 하나요?

용도에 따라 다릅니다:

  • mapping: 키로 빠르게 값을 조회할 때 (O(1)), 순회 불필요 시
  • 배열: 순서가 중요하거나 전체 순회가 필요할 때
  • 둘 다: 조회 + 순회 모두 필요할 때 (mapping + 배열 조합)

대부분의 DApp에서는 mapping + 배열 조합이 가장 실용적입니다.

보안과 패턴

Q5. modifier에서 _; 의 위치가 중요한가요?

매우 중요합니다. _;는 원래 함수의 본문이 삽입되는 위치입니다.

  • _; 앞의 코드: 함수 실행 전에 실행 (전처리)
  • _; 뒤의 코드: 함수 실행 후에 실행 (후처리)

예를 들어, 재진입 공격(Reentrancy) 방지를 위한 mutex modifier에서는 _;의 위치가 보안에 직접적인 영향을 줍니다.

Q6. 재진입 공격(Reentrancy Attack)이란?

외부 계약을 호출할 때, 호출된 계약이 원래 계약의 함수를 다시 호출하여 상태가 업데이트되기 전에 자금을 반복 인출하는 공격입니다. 2016년 The DAO 해킹(약 6천만 달러 피해)의 원인이었습니다.

방지법:

  • Checks-Effects-Interactions 패턴: 검증 -> 상태 변경 -> 외부 호출 순서
  • OpenZeppelin의 ReentrancyGuard 사용
  • transfer()/send() 대신 call() 사용 시 주의

Q7. OpenZeppelin이란 무엇인가요?

OpenZeppelin은 검증된 스마트 계약 라이브러리를 제공하는 오픈소스 프로젝트입니다. ERC-20, ERC-721, AccessControl, ReentrancyGuard 등 표준 구현을 제공합니다. 실무에서는 직접 구현보다 OpenZeppelin 라이브러리를 사용하는 것이 안전합니다. 수많은 감사(audit)를 거쳤기 때문입니다.

Q8. enum은 언제 사용하나요?

계약의 상태 머신을 구현할 때 사용합니다. 투표 계약의 Created/Voting/Ended처럼, 계약이 특정 생애주기를 가질 때 유용합니다. enum은 내부적으로 uint8로 저장되며, 유효하지 않은 값은 자동으로 거부됩니다. WIA-FIN-007에서는 금융 계약의 상태 관리에 enum 사용을 권장합니다.

가스 최적화

Q9. constant와 immutable의 차이는?

둘 다 변경 불가능한 변수를 선언하지만 차이가 있습니다:

  • constant: 컴파일 시점에 값이 결정. uint256 constant MAX = 100;
  • immutable: 생성자에서 값이 결정. address immutable owner;

둘 다 storage 슬롯을 사용하지 않으므로 가스비가 크게 절감됩니다. constant는 바이트코드에 직접 포함되고, immutable은 배포 시 바이트코드에 기록됩니다.

Q10. 변수 패킹(Variable Packing)이란?

EVM의 storage 슬롯은 32바이트(256비트)입니다. 작은 타입의 변수를 연속으로 선언하면 하나의 슬롯에 함께 저장됩니다:

  • uint128 a; uint128 b; -> 1슬롯 (32바이트)
  • uint256 a; uint128 b; -> 2슬롯

struct 내에서도 작은 타입을 인접하게 배치하면 가스를 절약할 수 있습니다. 선언 순서가 중요합니다.

Q11. for 루프를 사용하면 안 되나요?

사용할 수 있지만 주의가 필요합니다. 반복 횟수가 많으면 블록 가스 한도를 초과하여 트랜잭션이 영원히 실행 불가능해질 수 있습니다. 이를 "DoS(Denial of Service) 취약점"이라고 합니다.

권장 사항:

  • 반복 횟수에 상한선을 설정
  • 가능하면 mapping으로 O(1) 접근
  • 큰 배열 순회는 오프체인(프론트엔드)에서 처리

Q12. 커스텀 에러(Custom Error)가 왜 가스를 절약하나요?

Solidity 0.8.4부터 도입된 커스텀 에러는 문자열 에러 메시지보다 적은 바이트코드를 생성합니다:

error Unauthorized(); (4바이트) vs require(false, "Only admin can call") (문자열 전체 저장)

특히 에러 메시지가 긴 경우 가스 절감 효과가 큽니다. 대규모 계약에서는 모든 require를 커스텀 에러로 교체하는 것이 좋습니다.

이벤트와 DApp

Q13. 이벤트 데이터를 계약 내에서 읽을 수 있나요?

아닙니다. 이벤트는 트랜잭션 로그(receipt)에만 저장되며, 계약 코드 내에서는 접근할 수 없습니다. 오직 외부 애플리케이션(Web3.js, ethers.js, The Graph 등)에서만 읽을 수 있습니다. 계약 내에서 읽어야 하는 데이터는 반드시 상태 변수(storage)에 저장해야 합니다.

Q14. indexed 키워드는 최대 몇 개 사용할 수 있나요?

하나의 이벤트에서 indexed 매개변수는 최대 3개입니다. indexed 매개변수는 토픽(topic)으로 저장되어 효율적인 필터링이 가능합니다. indexed가 아닌 매개변수는 데이터 부분에 저장되며, 필터링은 불가능하지만 더 많은 데이터를 저장할 수 있습니다.

Q15. 프론트엔드에서 스마트 계약과 통신하려면?

주로 두 가지 라이브러리를 사용합니다:

  • ethers.js: 경량, 현대적 API, 현재 가장 인기
  • Web3.js: 오래된 표준, 여전히 널리 사용

필요한 것은 (1) 계약의 ABI (2) 배포된 계약 주소 (3) Provider(MetaMask 등)입니다. ABI는 계약의 인터페이스를 정의하는 JSON으로, Remix에서 컴파일 후 복사할 수 있습니다.

Q16. 투표 DApp에서 비밀 투표는 어떻게 구현하나요?

블록체인은 투명하므로 기본적으로 비밀 투표가 어렵습니다. 구현 방법:

  • Commit-Reveal 패턴: 투표를 해시로 제출(commit)한 후, 투표 마감 후 원본을 공개(reveal)
  • 영지식 증명(ZKP): 투표 내용을 공개하지 않고 유효성만 검증
  • 동형 암호화: 암호화된 상태로 집계

Commit-Reveal이 가장 실용적이며, 심화 과제로 구현해볼 수 있습니다.