스마트 컨트랙트 소개

간단한 스마트 컨트랙트

변수의 값을 설정하고 다른 컨트랙트에 액세스 할 수 있도록 노출시키는 기본 예제부터 시작해봅시다. 나중에 더 자세히 살펴볼 것이기 때문에 지금 모든 걸 이해하지 않아도 괜찮습니다.

Storage

pragma solidity >=0.4.0 <0.6.0;

contract SimpleStorage {
    uint storedData;

    function set(uint x) public {
        storedData = x;
    }

    function get() public view returns (uint) {
        return storedData;
    }
}

첫 줄은 코드가 Solidity 0.4.0 버전을 기반으로 작성되었다는 것을 뜻하며, 이후 버전(0.6.0 버전 직전까지)에서도 정상 동작할 수 있게 합니다. 이 줄을 통해 컨트랙트가 다르게 동작 할 수 있는 새로운(깨지기 쉬운) 컴파일러 버전에서 컴파일 할 수 없도록 보장합니다. pragma 라는 키워드는 컴파일러가 소스 코드를 어떻게 처리해야하는지를 알려줍니다. (참고. pragma once).

Solidity의 관점에서 컨트랙트란 무수한 코드들(함수)과 데이터(상태)가 Ethereum 블록체인의 특정 주소에 존재하는 것입니다. 다음 줄의 uint storedData;uint (256 비트의 부호없는 양의 정수) 타입의 storedData 로 불리는 변수를 선언한 것입니다. 이것은 데이터베이스에서 함수를 호출함으로써 값을 조회하거나 변경할 수 있는 하나의 영역으로 생각할 수 있습니다. Ethereum에서, 변수들은 컨트랙트에 포함되어 있으며 setget 함수로 변수의 값을 변경하거나 조회할 수 있습니다.

상태 변수에 접근할 때 다른 프로그래밍 언어에서 일반적으로 사용되는 this. 키워드를 사용하지 않습니다.

이 컨트랙트는 누구나 접근 가능한 숫자를 저장하는 단순한 일 외에는 아직 할 수 있는게 많지 않습니다. 물론 누구나 set 을 호출하여 다른 값으로 덮어쓰는 것이 가능합니다. 하지만 이전 숫자는 블록체인 히스토리 안에 여전히 저장됩니다. 이후에, 숫자를 바꿀 수 있는 접근 제한을 어떻게 둘 수 있는지를 알아볼 것입니다.

주석

모든 지시자(컨트랙트, 함수, 변수 이름들)는 ASCII 문자열로 제한됩니다. UTF-8로 인코딩된 데이터도 string 변수로 저장할 수 있습니다.

경고

문자열처럼 보이는(혹은 동일한) 유니코드 텍스트 사용은 다른 코드 지점을 가지고 다른 바이트 배열로 인코딩될 수 있다는 점에 주의하세요.

Subcurrency 예제

다음으로는 간단한 가상화폐를 만들어보겠습니다. 코인 발행은 컨트랙트를 만든 사람만이 할 수 있습니다. 코인을 전송할 땐 아이디와 비밀번호 등이 필요하지 않습니다. 오직 필요한 것은 Ethereum 키 쌍 뿐입니다.

pragma solidity ^0.5.0;

contract Coin {
    // The keyword "public" makes those variables
    // easily readable from outside.
    address public minter;
    mapping (address => uint) public balances;

    // Events allow light clients to react to
    // changes efficiently.
    event Sent(address from, address to, uint amount);

    // This is the constructor whose code is
    // run only when the contract is created.
    constructor() public {
        minter = msg.sender;
    }

    function mint(address receiver, uint amount) public {
        require(msg.sender == minter);
        require(amount < 1e60);
        balances[receiver] += amount;
    }

    function send(address receiver, uint amount) public {
        require(amount <= balances[msg.sender], "Insufficient balance.");
        balances[msg.sender] -= amount;
        balances[receiver] += amount;
        emit Sent(msg.sender, receiver, amount);
    }
}

이번 컨트랙트는 좀 다릅니다. 하나씩 차근히 살펴보죠.

address public minter; 로 누구나 접근 가능한 address 타입의 변수를 선언했습니다. address 타입은 160 비트의 값으로 그 어떤 산술 연산을 허용하지 않습니다. 이 타입은 컨트랙트 주소나 외부 사용자들의 키 쌍을 저장하는 데 적합합니다. public 키워드는 변수의 현재 값을 컨트랙트 바깥에서 접근할 수 있도록 하는 함수를 자동으로 만들어줍니다.

이 키워드 없이는 다른 컨트랙트가 이 변수에 접근할 방법이 없습니다. 키워드 사용 결과로 컴파일러가 자동으로 만든 함수 코드는 대강 다음과 같습니다 (당분간은 externalview 키워드는 무시합니다.):

function minter() returns (address) { return minter; }

물론, 위 함수를 정확하게 입력해도 이름이 같아서 제대로 동작하지는 않을 것입니다. 그러나 컴파일러가 이런 식으로 동작한다는 것을 알아두세요.

다음 줄의 mapping (address => uint) public balances; 또한 public 상태의 변수를 선언하지만 조금 더 복잡한 데이터 타입입니다. 이 타입은 주소와 양의 정수를 연결(매핑) 짓습니다.

매핑은 가상으로 초기화되는 해시테이블 로 볼 수 있습니다. 그래서 모든 가능한 키값은 처음부터 존재하며, 이 키 값들은 바이트 표현이 모두 0인 값에 매핑됩니다. 그렇다고 모든 키와 값들을 쉽게 가져올 수 있다고 생각해서는 안 되며, 내가 추가한 게 무엇인지 알고(리스트를 유지하거나 더 나은 데이터 타입을 사용하면 더 좋습니다) 전체를 가져오지 않는 상황에서 사용해야 합니다. public 키워드를 통해 만들어진 getter function 은 조금더 복잡합니다. 대략 이런 형태인데요:

function balances(address _account) external view returns (uint) {
    return balances[_account];
}

보시는 것처럼, 특정 계좌의 잔액이 어떤지 알아내는 데 이 함수을 사용할 수 있습니다.

다음 줄의 event Sent(address from, address to, uint amount); 는 소위 "이벤트" 로 불리며 send 함수 마지막 줄에서 발생됩니다. 유저 인터페이스(서버 애플리케이션 포함) 는 블록체인 상에서 발생한 이벤트들을 큰 비용을 들이지 않고 받아볼 수 있습니다. 이벤트가 발생되었을 때 이를 받는 곳에서는 from, to, amount 의 인자를 함께 받으며, 이는 트랜잭션을 파악하는데 도움을 줍니다. 이벤트를 받아보기 위해, 다음의 JavaScript 코드(Coin 이 web3.js나 비슷한 모듈을 통해 만들어진 콘트랙트 객체라고 가정합니다) 를 사용합니다:

Coin.Sent().watch({}, "", function(error, result) {
    if (!error) {
        console.log("Coin transfer: " + result.args.amount +
            " coins were sent from " + result.args.from +
            " to " + result.args.to + ".");
        console.log("Balances now:\n" +
            "Sender: " + Coin.balances.call(result.args.from) +
            "Receiver: " + Coin.balances.call(result.args.to));
    }
})

유저 인터페이스 상에서 자동으로 만들어진 함수 balances 가 어떻게 불리고 있는지 함께 알아두세요.

생성자는 컨트랙트 생성 시 실행되는 특별한 함수이고, 이후에는 사용되지 않습니다. 이것은 컨트랙트를 만든 사람의 주소를 영구적으로 저장합니다: msg (txblock 포함)는 유용한 전역 변수로 블록체인에 접근할 수 있는 다양한 속성들을 담고 있습니다. msg.sender 는 외부에서 지금 함수를 호출한 주소를 나타냅니다.

마지막으로, 사용자나 컨트랙트가 호출할 수 있는 함수들은 mintsend 입니다. 만약 mint 를 호출한 사용자가 컨트랙트를 만든 사람이 아니면 아무일도 일어나지 않습니다. 이는 인수가 false로 평가될 경우 모든 변경 사항이 원래대로 되돌아가도록 하는 특수 함수 require 에 의해 보장됩니다. require 를 두 번째로 호출하면 코인이 너무 많아지게 되고, 이는 차후에 오버플로우 에러의 원인이 될 수 있습니다.

반대로 send 는 어디든 코인을 보낼 사람이면 (이미 이 코인을 가진) 누구나 호출 가능합니다. 전송하려고 하는 코인의 양이 충분하지 않을 경우, require 호출은 실패하게 되며, 적절한 에러메세지를 사용자에게 제공합니다.

주석

코인을 전송하려고 이 컨트랙트를 사용해도 블록체인 탐색기로 본 해당 주소에는 변화가 없을 것 입니다. 코인을 보낸 것과 잔액이 변경된 사실은 이 코인 컨트랙트 내의 데이터 저장소에만 저장되어 있기 때문입니다. 이벤트를 사용하면 새 코인의 트랜잭션과 잔액을 추적하는 "블록체인 탐색기"를 만드는것이 상대적으로 쉽습니다. 하지만, 여러분은 주인의 주소가 아닌 코인 컨트랙트의 주소를 조사해야 합니다.

블록체인 개론

블록체인의 개념은 개발자들에게는 그리 어려운 건 아닙니다. 그 이유는 대부분의 복잡한 것들(mining, hashing, elliptic-curve cryptography, peer-to-peer networks, etc.) 은 단지 일련의 플랫폼에 대한 약속들로 정해져 있기 때문입니다. 이러한 개념들을 받아들일 때 여러분은 그 기반이 되는 기술에 대해 걱정할 필요는 없습니다. 아마존의 AWS가 내부적으로 어떻게 동작하는지를 알고 쓰는 건 아닌 것처럼 말입니다.

트랜잭션

블록체인은 전세계적으로 공유되어 트랜잭션이 일어나는 데이터베이스입니다. 이것은 네트워크에 참여하면 누구나 데이터베이스를 살펴볼 수 있다는 것을 뜻합니다. 만약 여러분이 데이터베이스의 어떤 것을 변경하려고 한다면, 소위 트랜잭션을 만들어야 하며 이는 다른 모두가 동의해야만 합니다. 트랜잭션이라는 단어는 당신이 만드려는 어떤 변화(동시에 두 값을 바꾸려 할 때)가 모두 안 되었거나, 모두 되었다는 것을 뜻합니다. 그리고 여러분의 트랜잭션이 데이터베이스에 적용되는 동안 어떤 트랜잭션도 그 값을 바꿀 수 없습니다.

예를 들어, 모든 계좌의 전자 화폐 잔액을 나타내는 도표를 상상해봅시다. 한 계좌에서 다른 계좌로 이체하는 작업이 필요할 때, 데이터베이스의 트랜잭션은 한 계좌에서 돈이 빠져나갔으면 다른 계좌에 그 금액만큼 추가가 되있어야 한다는 걸 보장해야 합니다. 어떤 이유로 금액 추가가 되지 않으면 돈도 빠져나가지 않아야겠죠.

그리고 트랜잭션은 항상 만든 사용자에 의해 암호화됩니다. 그래서 데이터베이스를 직접 수정하려는 것을 차단할 수 있습니다. 전자화폐의 경우 이 간단한 검사가 계좌의 키를 소유한 사용자만이 이체할 권한을 가지는 것을 보장합니다.

블록

비트코인이 극복해야할 가장 큰 장애물은 "이중 지불 공격" 입니다. 계정을 초기화할 2개의 트랜잭션이 함께 일어난다면 어떻게 될까요? 하나의 트랜잭션만이 유효할 것이고, 둘 중 처음으로 수용되는 쪽일 것입니다. 문제는 "첫 번째"가 Peer-to-Peer 네트워크에서 객관적인 용어가 아니라는 점입니다.

그에 대한 추상적인 답은 여러분이 딱히 신경 쓸 필요는 없다는 것입니다. 전반적으로 수용되는 트랜잭션들의 순서는 여러분이 설정한대로 선택되고, 이는 충돌을 해결해 줄 것입니다. 트랜잭션들은 "블록" 이라 불리는 곳에 합쳐집니다. 그리고 네트워크에 참여한 모든 노드들에 전파됩니다. 만약 두 개의 트랜잭션이 충돌한다면, 두 번째가 되는 트랜잭션은 거절될 것이며 블록의 일부가 되지 않습니다.

이러한 블록들은 시간에 따라 선형의 순서를 가진 형태를 띄며 "블록체인"의 어원이 되었습니다. 블록들은 일정한 간격에 의해 체인으로 연결됩니다. Ethereum은 약 17초마다 만들어지고요.

("채굴" 이라 불리는) "순서 선택 메커니즘"의 일환으로 블록들의 순서가 바뀌는 경우도 있는데, 이는 블록의 끝 부분에서만 일어납니다. 이런 현상은 특정 블록 위에 더 많은 블록이 생길수록 되돌릴 가능성도 점점 낮아집니다. 따라서 여러분의 트랜잭션이 블록체인에서 바뀌거나 제거되는 경우도 있지만, 시간이 지날수록 그럴 가능성은 낮아집니다.

주석

트랜잭션은 다음 블록이나 향후 특정 블록을 포함하지 않을 수도 있습니다. 어떤 트랜잭션 블록이 포함될지 결정하는 것은 트랜잭션의 제출자가 아니라 채굴자에게 달려있기 때문입니다.

향후 컨트랙트 호출을 예약하길 원한다면, 알람시계 나 이와 비슷한 오라클 서비스를 사용할 수 있습니다.

Ethereum 가상 머신

소개

Ethereum 가상머신, EVM은 Ethereum의 스마트 컨트랙트를 위한 런타임 환경입니다. 이것은 완전히 독립되어 있기 때문에 EVM 에서 실행되는 코드는 네트워크나 파일 시스템, 기타 프로세스들에 접근할 수 없습니다. 심지어 스마트 컨트랙트는 다른 스마트 컨트랙트에 접근이 제한적으로 불가능합니다.

계정

Ethereum 내에는 같은 공간을 공유하는 2가지의 계정 종류가 있습니다: 외부 계정 은 사람이 가지고 있는 공개키, 비밀키 쌍으로 동작되며, 컨트랙트 계정 은 계정과 함께 저장된 코드에 의해 동작됩니다.

외부 계정의 주소는 공개키에 의해 정해지는 반면 컨트랙트의 주소는 생성되는 시점에 정해집니다. (생성한 사용자의 주소와 주소로부터 보내진 트랜잭션의 수, "논스"에 기반합니다.)

계정이 코드를 저장하든 아니든 상관없이 두 종류는 모두 EVM 내에서는 동일하게 다뤄집니다.

모든 계정은 256비트의 문자열들이 서로 키-값으로 영구히 매핑된 스토리지 를 가지고 있습니다. 그리고 모든 계정은 트랜잭션으로 바뀔 수 있는 Ether(정확히는 "Wei", 1 ether10**18 wei) 잔액을 가지고 있습니다.

트랜잭션

트랜잭션은 한 계정에서 다른 계정(같을수도 있고, 비어있을 수도 있습니다. 아래 참조)으로 보내지는 일종의 메시지입니다. 그리고 바이너리 데이터("페이로드"라고 불림)와 Ether 양을 포함할 수 있습니다.

대상 계정이 코드를 포함하고 있으면 코드는 실행되고 페이로드는 입력 데이터로 제공됩니다.

대상 계정이 설정되지 않은 경우(트랜잭션에 받는 사람이 없거나 받는 사람이 null 로 설정된 경우) 일 땐, 트랜잭션은 새로운 컨트랙트 를 생성하며 앞서 말씀드렸던 것처럼 사용자와 "논스"로 불리는 트랜잭션의 수에 의해 주소가 결정됩니다. 각 컨트랙트 생성 트랜잭션 페이로드는 EVM 바이트코드로 실행되기 위해 사용됩니다. 이 실행 데이터는 컨트랙트의 코드로 영구히 저장됩니다. 즉, 컨트랙트를 만들기 위해 실제 코드를 보내는 대신, 실행될 때의 코드를 리턴하는 코드를 보내야 한다는 것을 뜻합니다.

주석

컨트랙트가 생성되는 동안, 컨트랙트의 코드는 비어있습니다. 이 때문에, 생성자가 실행을 끝낼 때까지 컨트랙트를 다시 호출해서는 안됩니다.

가스

트랜잭션 발생 시, 일정량의 가스 가 동시에 사용되며 이는 트랜잭션 실행에 필요한 작업의 양을 제한하는 목적을 가지고 있습니다. 그리고 특별한 규칙에 의해 작업 중 가스는 조금씩 고갈되게 됩니다.

가스 가격 은 트랜잭션을 만든 사용자가 정하고 최대 가스 가격 * 가스 을 지불합니다. 실행이 끝난 이후에도 가스가 남았다면 이는 같은 방식으로 사용자에게 다시 환불됩니다.

만약 가스가 모두 사용되었다면(음수가 되었다면), 가스 부족 예외 오류가 발생하며 현재 단계에서 발생하는 모든 변화를 되돌립니다.

스토리지, 메모리와 스택

Ethereum 가상 머신은 데이터 스토리지, 메모리, 스택이라 불리는 3가지 영역이 있습니다. 이는 다음 문단에서 설명합니다.

각 계정에는 스토리지 라 불리는 데이터 영역이 있습니다. 해당 영역은 함수호출과 트랜잭션 사이에서 영구적으로 존재합니다.

스토리지는 256비트 문자가 키-값 형태로 연결된 저장소입니다. 컨트랙트 내의 스토리지를 탐색하는 건 불가능하며 읽고 수정하는데 비용이 많이 듭니다. 컨트랙트가 소유하지 않은 스토리지는 읽거나 쓸 수 없습니다.

두번째 영역은 메모리 이며 각 메시지 콜에 대해 새로 초기화된 인스턴스를 가지고 있습니다. 메모리는 선형이며 바이트 레벨로 다뤄집니다. 쓰기가 8 비트나 256 비트가 될 수 있는 반면 읽기는 256 비트로 한정됩니다. 이전에 변경되지 않은 메모리 워드 영역(즉, 워드 내 오프셋) 에 액세스할 때(읽기, 쓰기 모두) 메모리는 256비트 워드 영역으로 확장됩니다. 확장되는 시점에 가스 비용이 지불되어야 합니다. 메모리는 커질수록 비용도 커집니다. (2차식으로 증가합니다)

EVM은 레지스터 머신이 아니라 스택 머신입니다. 모든 연산은 스택 이라 불리는 영역에서 처리됩니다. 최대 1024개의 요소를 가질 수 있고 256비트의 단어들을 포함합니다. 스택은 상단 꼭대기에서 접근이 일어납니다:

스택 최상단의 16개 요소들 중 하나를 최상단에 복사하거나 최상단의 요소를 밑의 16개 요소 중 하나와 교체하는 것이 가능합니다. 연산들은 스택의 최상단 2개(어떤 연산이냐에 따라 하나일수도, 더 많을수도) 를 가져오며 그 결과를 스택에 푸시합니다. 물론 더 깊은 스택의 액세스를 위해 스택 요소들을 스토리지나 메모리로 옮기는 것도 가능합니다. 하지만 스택의 상단 요소를 제거하지 않으면 그 밑에 존재하는 요소를 임의로 접근하는 건 불가능합니다.

명령어 집합

EVM의 명령어들은 최소로 구성되며 합의 문제를 야기할 수 있는 잘못된 구현을 방지합니다. 모든 명령어는 기본 데이터 타입, 256비트 단어나 메모리 조각(혹은 다른 바이트 배열)을 기반으로 동작합니다. 일반적인 산술, 비트, 논리, 비교 연산이 있습니다. 조건과 조건 없는 점프도 가능합니다. 그리고 컨트랙트는 현재 블록의 수나 타임스탬프 관련 속성에도 접근할 수 있습니다.

전체 목록은 인라인 어셈블리 문서 리스트를 참조하시기 바랍니다. list of opcodes

메시지 콜

메시지 콜을 사용하면 컨트랙트는 다른 컨트랙트를 호출하거나 컨트랙트가 아닌 계정으로 Ether를 송금할 수 있습니다. 메시지 콜은 송신자, 수신자, 데이터 페이로드, Ether, 가스와 리턴 값 등을 가지고 있어 트랜잭션과 유사합니다. 실제로 모든 트랜잭션은 상위 메시지 콜로 구성되며 추가 메시지 콜도 만들 수 있습니다.

컨트랙트는 내부 메시지 호출과 함께 보내고 남길 가스량을 정할 수 있습니다. 만약 내부 호출 중 가스 부족 오류(아니면 다른 오류) 가 발생하면 스택에 에러 값이 추가되며 알리게 됩니다. 이 경우 호출을 위해 사용된 가스만 소모됩니다. Solidity에서 호출하는 계약은 이런 상황에서 기본적으로 수동 예외를 발생시키므로 호출 스택의 우선순위를 올립니다.

앞서 말했듯, 호출된 컨트랙트는 깨끗이 비워진 메모리 인스턴스와 호출 데이터 라는 격리된 공간의 호출 페이로드 접근 권한을 가집니다. 실행이 완료되면 호출자에 의해 이미 할당된 메모리 영역 안에 저장될 데이터를 리턴받을 수 있습니다. 이런 호출은 모두 완전한 동기식입니다.

호출은 1024개의 깊이로 제한되며 이는 복잡한 연산일수록 재귀호출보다 반복문이 선호된다는 것을 뜻합니다. 게다가, 가스의 63/64 만이 메세지콜에서 포워딩 될 수 있으며, 이는 실질적으로 1000 이하의 깊이 제한 원인이 될 수 있습니다.

델리게이트 콜 / 콜코드와 라이브러리

메시지 콜은 다양한 변형이 있는데, 델리게이트 콜 의 경우는 대상 주소의 코드가 호출하는 컨트랙트의 컨텍스트 내에서 실행된다는 것과 msg.sendermsg.value 가 값이 바뀌지 않는다는 것 외에는 메시지 콜과 동일합니다.

이것은 컨트랙트가 실행 중 다양한 주소의 코드를 동적으로 불러온다는 것을 뜻합니다. 스토리지, 현재 주소와 잔액은 여전히 호출하는 컨트랙트를 참조하지만 코드는 호출된 주소에서 가져옵니다.

이것은 Solidity에서 복잡한 데이터 구조 구현이 가능한 컨트랙트의 스토리지에 적용 가능한 재사용 가능한 코드, "라이브러리"의 구현을 가능하도록 합니다.

로그

블록 레벨까지의 모든 절차를 매핑하며 특별히 인덱싱된 데이터 구조 데이터를 저장하는 것도 가능합니다. 이 기능은 로그 라 부르며 Solidity에서 이벤트 를 구현하기 위해 사용됩니다. 컨트랙트들은 로그 데이터를 만들고 접근할 수는 없지만 블록체인 바깥에서 효율적으로 접근 가능합니다.

일부 로그 데이터들은 bloom filters 안에 저장되기 때문에, 효율적이고 암호화되어 안전한 방법으로 데이터를 찾는게 가능합니다. 따라서 모든 블록체인(라이트 클라이언트라고 불리는)을 다운받지 않은 네트워크 피어들도 로그들을 여전히 찾을 수 있습니다.

생성

컨트랙트들은 특별한 연산 부호(단순히 트랜잭션으로 0 주소를 호출하지 않습니다)를 사용하여 다른 컨트랙트들을 생성할 수 있습니다. 이러한 생성 콜 과 일반 메시지 콜의 차이는 페이로드 데이터가 실행된다는 것과 결과가 코드로 저장된다는 점, 호출자와 생성자가 스택의 새 컨트랙트 주소를 받는다는 점 입니다.

비활성화와 자기 소멸

코드가 블록체인에서 코드가 지워지는 유일한 방법은 주소의 컨트랙트가 selfdestruct 연산을 사용했을 때입니다. 주소에 저장된 남은 Ether는 지정된 타겟으로 옮겨지고 스토리지와 코드는 해당 상태에서 지워집니다. 이론적으로 컨트랙트를 제거하는 것은 좋은 아이디어로 들릴지도 모르겠습니다만, 잠재적으로 위험한 행위입니다. 만일 누군가가 제거된 컨트랙트에 Ether를 전송하면, 해당 Ether는 영구적으로 손실되게 됩니다.

주석

컨트랙트 코드가 selfdestruct 를 포함하지 않더라도, delegatecall 이나 callcode 를 실행해 그 작업을 수행할 수 있습니다.

여러분의 컨트랙트를 비활성화하려면, 내부상태를 바꿈으로써 disable 해야 합니다. 이때, 내부 상태는 모든 함수를 되돌리는 원인이 됩니다. 이로인해 Ether가 즉시 반환되므로 컨트랙트를 사용할 수 없게 됩니다.

경고

"자기 소멸자"에 의해 컨트랙트가 제거되었더라도, 블록체인의 히스토리에 남아있게됩니다. 그리고, 대부분의 Ethereum 노드들이 이를 보유하게 될 것입니다. 그래서, "자기 소멸자"를 사용하는 것은 데이터를 하드디스크에서 삭제하는 것과는 다릅니다.