이전 포스팅에서는 좀비의 이름을 받아서 랜덤으로 좀비를 생성하고 데이터베이스에 추가했다.
이번에는 좀비가 다른 생명체를 "먹어서" 생성되도록 해보겠다.
새로운 좀비는 좀비의 DNA와 먹이의 DNA에 의해 생성되도록 하겠다.
매핑은 구조체처럼 구조화 된 데이터를 저장하는 또 다른 방법이다
매핑은 키와 값으로 저장되어있어, 데이터를 저장하고 검색할 때 사용한다.
주소는 계정이다. 즉 이더리움 블록체인의 통화인 이더의 잔액을 뜻한다.
이 주소는 계정을 가지는 고유한 식별자이기 때문에 "주소는 특정 유저(혹은 스마트 컨트렉트)가 소유" 한다.
즉, 고유 ID 라고 생각하면 된다.
좀비의 소유권을 저장하기위해 두가지 매핑을 했다.
하나는 좀비 소유자의 주소를 추적하기 위한 것이고 하나는 소유한 좀비의 숫자를 추적한다.
// 키
mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;
솔리디티는 모든 함수가 사용할 수 있는 특정한 전역변수가 있다.
msg.sender는 현재 함수를 누가 호출했는지 알 수 있다.
참고: 솔리디티에서 함수 실행은 항상 외부 호출자가 시작한다. 컨트랙트는 누군가가 컨트랙트의 함수를 호출할 때까지 블록체인 상에서 아무 것도 안 하고 있을 것이다.
그러니 항상 msg.sender가 있어야 한다.
mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;
function _createZombie(string _name, uint _dna) private {
uint id = zombies.push(Zombie(_name, _dna)) - 1;
zombieToOwner[id] = msg.sender;
ownerZombieCount[msg.sender]++;
NewZombie(id, _name, _dna);
}
위 코드에서 새로운 좀비가 생성되면 새로운 좀비의 id를 zombieToOwner
매핑에 msg.sender
가 저장되고 ownerZombieCount
가 하나 증가하게 수정했다.
좀비는 유저 한명당 한번만 생성 할 수 있도록 수정해보겠다.
즉, createRandomZombie
함수는 한 번씩만 호출이 되게 수정해야한다.
이때 require
를 활용하면 특정 조건이 참이 아닐 때 함수가 에러 메시지를 발생하고 실행을 멈추게 된다.
function createRandomZombie(string _name) public {
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
_createZombie
에서 ownerZombieCount[msg.sender]
를 증가시키기 때문에 값이 0 이상이면 이미 좀비 좀비를 생성한 계정이므로 실행을 중지한다.
ZombieFactory
를 상속하는 ZombieFeeding
컨트렉트를 생성했다.
"cat is animal" 처럼 상속은 is
를 사용해 작성한다.
contract ZombieFeeding is ZombieFactory {}
솔리디티는 변수를 저장할 수 있는 공간으로 Storage와 Memory가 있다.
Storage
- 블록체인 상에 영구적으로 저장되는 변수를 의미한다. (=하드디스크)
- 상태 변수에 해당
Memory
- 임시적으로 저장되는 변수로 컨트렉트 함수에 대한 외부 호출들이 일어나면 지워진다. (=RAM)
- 지역 변수 (함수 내부에 선언된 변수)
이 키워드는 구조체와 배열을 처리할 때 사용된다.
다음 예제를 보면 쓰임새를 할 수있다.
contract SandwichFactory { struct Sandwich { string name; string status; } Sandwich[] sandwiches; function eatSandwich(uint _index) public { // [1] : Sandwich mySandwich = sandwiches[_index]; Sandwich storage mySandwich = sandwiches[_index]; // [2] : 이 경우, `mySandwich`는 저장된 `sandwiches[_index]`를 가리키는 포인터이다. // 그리고 mySandwich.status = "Eaten!"; // ...이 코드는 블록체인 상에서 `sandwiches[_index]`을 영구적으로 변경한다. // 단순히 복사를 하고자 한다면 `memory`를 이용하면 된다: Sandwich memory anotherSandwich = sandwiches[_index + 1]; // ...이 경우, `anotherSandwich`는 단순히 메모리에 데이터를 복사하는 것이 된다. // 그리고 anotherSandwich.status = "Eaten!"; // ...이 코드는 임시 변수인 `anotherSandwich`를 변경하는 것으로 // `sandwiches[_index + 1]`에는 아무런 영향을 끼치지 않는다. 그러나 다음과 같이 코드를 작성할 수 있다: sandwiches[_index + 1] = anotherSandwich; // ...이는 임시 변경한 내용을 블록체인 저장소에 저장하고자 하는 경우이다. } }
[1]번 주석을 보자.
해당 주석과 같이 작성해도 문제가 없을 것처럼 보이지만 솔리디티는 여기서
storage
나 memory
를 명시적으로 선언해야 한다는 경고 메시지를 발생한다.
그러므로 storage
키워드를 활용하여 [2]와 같이 선언해야 한다.
storage
를 사용하면 블록체인 데이터베이스 상에 영구적으로 업데이트된다.
memory
는 storage
와 반대이다.
단순히 값을 복사해서 사용하는 경우 데이터베이스까지 변경되서는 안된다.
그러므로 anotherSandwich
는 sandwiches
에 아무 영향을 끼치지 않는다.
다음 코드는 새로 생성되는 좀비dna를 구해서 좀비를 생성하는 코드다.
contract ZombieFeeding is ZombieFactory {
function feedAndMultiply(uint _zombieId, uint _targetDna) public {
require(msg.sender == zombieToOwner[_zombieId]);
Zombie storage myZombie = zombies[_zombieId];
_targetDna = _targetDna % dnaModulus;
uint newDna = (myZombie.dna + _targetDna) / 2;
_createZombie("NoName", newDna);
}
}
ZombieFeeding
는 ZombieFactory
를 상속하므로 ZombieFactory
의 상태 변수 및 구조체, 일부 함수들을 사용할 수 있다.
좀비의 먹이는 좀비의 주인만 줄 수 있기 때문에 require
를 사용했다.
메세지를 전달하는 사람은 좀비의 주인이어야 한다.
먹이를 먹는 좀비의 dna를 얻아야 하므로 좀비 아이디에 맞는 좀비 구조체 정보를 얻어온다.
dna는 16자리이어야하므로 dnaModulus와 % 연산을 해 새로운 값을 얻는다.
이들의 평균값이 새로운 좀비의 새 dna가 된다.
이 dna를 가지고 좀비를 만들기 위해 _createZombie
함수를 호출한다.
_createZombie
함수를 호출할 때, 컴파일러가 에러메세지를 출력하게 될 것이다.
이유는 _createZombie
가 ZombieFactory
컨트랙트 내의 private
함수이기 때문이다.
Internal
함수가 정의된 컨트랙트를 상속하는 컨트랙트에서도 접근이 가능하다는External
함수가 컨트랙트 바깥에서만 호출될 수 있고 컨트랙트 내의 다른 함수에 의해 호출될 수 없다.
즉 _createZombie
함수는 internal
을 사용해야한다.
function _createZombie(string _name, uint _dna) private {
...
}
위와 같이 되어있던 함수를 다음과 같이 변경한다.
function _createZombie(string _name, uint _dna) internal {
...
}