티스토리 뷰
본 게시글은 Nomad Coder의 강의 '타입스크립트로 블록체인 만들기'의 일부를 Golang을 사용하여 컨버팅 하는 과정을 기술하고 있습니다.
컨버팅(Converting)
같은 플랫폼 상에서 프로그램이 동일하게 돌아갈 수 있게 기존 언어 A에서 새로운 언어 B로 변경하는 과정
🐱👤깃허브 저장소
https://github.com/piatoss3612/simple-blockchain
🔧 초기 설정
1. Golang 버전 확인
$ go version
go version go1.20.4 linux/amd64
최신 버전은 1.21입니다.
2. 작업 디렉터리 생성
$ mkdir simple-blockchain
$ cd simple-blockchain
3. Go 모듈 초기화
$ go mod init simple-blockchain
go: creating new go.mod: module simple-blockchain
4. main.go 파일 생성
$ touch main.go
🧱 블록 구현하기
1. 기존 코드
import crypto from 'crypto';
interface BlockShape {
hash: string;
prevHash: string;
height: number;
data: string;
}
class Block implements BlockShape {
public hash: string;
constructor(
public prevHash: string,
public height: number,
public data: string
) {
this.hash = Block.calculateHash(prevHash, height, data);
}
static calculateHash(prevHash: string, height: number, data: string): string {
const toHash = `${prevHash}${height}${data}`;
const hash = crypto.createHash('sha256');
return hash.update(toHash).digest('hex');
}
}
2. 변경한 코드
package main
import (
"crypto/sha256"
"encoding/hex"
"fmt"
)
type Block struct {
hash string
prevHash string
data string
height int
}
func NewBlock(prevHash string, data string, height int) Block {
block := Block{
hash: calculateHash(prevHash, data, height),
prevHash: prevHash,
data: data,
height: height,
}
return block
}
func calculateHash(prevHash string, data string, height int) string {
hash := sha256.New()
_, _ = hash.Write([]byte(fmt.Sprintf("%s%d%s", prevHash, height, data)))
return hex.EncodeToString(hash.Sum(nil))
}
Golang은 강타입 언어(strongly typed language)로, 변수의 타입을 명시적으로 선언해 주어야 합니다. 그런 점에서 TypeScript와 비슷하지만, Golang은 클래스를 지원하지 않습니다. 대신 구조체(Struct)가 TypeScript의 클래스 역할을 합니다. 이 부분은 Golang이 객체 지향 언어인지 아닌지에 대한 설명이 필요한데, 해당사항은 별도의 게시물로 작성해 보겠습니다.
구조체를 사용해 정의한 블록과 블록의 생성자 함수입니다.
type Block struct {
hash string
prevHash string
data string
height int
}
func NewBlock(prevHash string, data string, height int) Block {
block := Block{
hash: calculateHash(prevHash, data, height),
prevHash: prevHash,
data: data,
height: height,
}
return block
}
블록의 해시를 생성하는 함수입니다. Golang도 마찬가지로 내장 함수를 사용하여 SHA256 해시를 생성할 수 있습니다.
func calculateHash(prevHash string, data string, height int) string {
hash := sha256.New()
_, _ = hash.Write([]byte(fmt.Sprintf("%s%d%s", prevHash, height, data)))
return hex.EncodeToString(hash.Sum(nil))
}
Write 함수는 쓰인 바이트의 길이와 오류를 반환합니다. 여기서는 해당 값들을 적절하게 처리해주고 있지 않기 때문에 만일을 대비해 언더스코어로 사용하지 않은 값임을 표시해 주는 것이 좋습니다.
_, _ = hash.Write([]byte(fmt.Sprintf("%s%d%s", prevHash, height, data)))
⛓ 블록체인 구현하기
1. 기존 코드
class Blockchain {
private blocks: Block[];
constructor() {
this.blocks = [];
}
private getPrevHash() {
if (this.blocks.length === 0) return "";
return this.blocks[this.blocks.length - 1].hash;
}
public addBlock(data: string) {
const block = new Block(this.getPrevHash(), this.blocks.length+1, data);
this.blocks.push(block);
}
public getBlocks() {
return this.blocks;
}
}
2. 변경한 코드
type Blockchain struct {
blocks []Block
}
func NewBlockchain() Blockchain {
return Blockchain{}
}
func (bc *Blockchain) AddBlock(data string) {
prevHash := bc.prevHash()
height := len(bc.blocks) + 1
block := NewBlock(prevHash, data, height)
bc.blocks = append(bc.blocks, block)
}
func (bc *Blockchain) prevHash() string {
if len(bc.blocks) == 0 {
return ""
}
return bc.blocks[len(bc.blocks)-1].hash
}
func (bc Blockchain) GetBlocks() []Block {
return bc.blocks
}
구조체를 사용해 정의한 블록체인과 블록체인의 생성자 함수입니다. 블록을 저장하기 위해 동적으로 크기를 조절할 수 있는 슬라이스를 사용했습니다. 슬라이스는 실제 데이터가 저장된 배열을 가리키는 포인터값을 가지고 있으며, 기본 길이는 0입니다.
type Blockchain struct {
blocks []Block
}
func NewBlockchain() Blockchain {
return Blockchain{
blocks: make([]Block, 0),
}
}
새로운 블록을 추가하는 메서드와 이전 블록의 해시를 가져오는 메서드입니다. 클래스에서는 private 접근자를 사용해 메서드에 대한 접근을 제한할 수 있지만 구조체에서는 메서드 이름이 대문자로 시작하면 public, 소문자로 시작하면 private 접근자가 적용됩니다. 그러나 클래스의 private 접근자는 클래스가 정의된 모듈 안에서도 유효하지만, 구조체는 외부에서 모듈을 불러와서 사용할 때만 적용됩니다.
func (bc *Blockchain) AddBlock(data string) {
prevHash := bc.prevHash()
height := len(bc.blocks) + 1
block := NewBlock(prevHash, data, height)
bc.blocks = append(bc.blocks, block)
}
func (bc *Blockchain) prevHash() string {
if len(bc.blocks) == 0 {
return ""
}
return bc.blocks[len(bc.blocks)-1].hash
}
블록체인의 모든 블록을 가져오는 메서드입니다.
func (bc Blockchain) GetBlocks() []Block {
return bc.blocks
}
🧨 실행하기
func main() {
blockchain := NewBlockchain()
blockchain.AddBlock("First block")
blockchain.AddBlock("Second block")
blockchain.AddBlock("Third block")
blocks := blockchain.GetBlocks()
for _, block := range blocks {
fmt.Printf("Prev. hash: %s\n", block.prevHash)
fmt.Printf("Data: %s\n", block.data)
fmt.Printf("Hash: %s\n", block.hash)
fmt.Println("==================================")
}
}
$ go run main.go
Prev. hash:
Data: First block
Hash: 7f647b7d55a6cf3598d1c4c386326f72bb9f28ee2e107168162195cd86e5ea23
==================================
Prev. hash: 7f647b7d55a6cf3598d1c4c386326f72bb9f28ee2e107168162195cd86e5ea23
Data: Second block
Hash: d552fe3755fc27d2680d65b516572ca7217bcdc5f7cee1aca115fab35035460e
==================================
Prev. hash: d552fe3755fc27d2680d65b516572ca7217bcdc5f7cee1aca115fab35035460e
Data: Third block
Hash: 3615128e0cffc4015b6d5113a1e6fd0533075d346ce12c116b8ff864cf09035e
==================================
🤔 오늘의 궁금증
1. 문제 상황
// getBlocks() is public, so we can modify private blocks array
blockchain.getBlocks().push(new Block("xxxxxxxx", 11111, "HACKED"));
TypeScript로 작성한 코드에서 private 인스턴스 변수로 선언된 blocks를 getBlocks 메서드를 사용해 불러오면 원래 배열 자체를 수정할 수 있는 문제가 발생합니다. 이는 배열이 참조값으로 반환되기 때문인데요.
public getBlocks() {
return [...this.blocks];
}
이런 식으로 새로운 배열을 생성하여 반환하면 문제를 해결할 수 있습니다. 그렇다면 Golang에서도 이와 비슷한 문제가 발생할까요?
2. 문제 적용
func main() {
blockchain := NewBlockchain()
blockchain.AddBlock("First block")
blockchain.AddBlock("Second block")
blockchain.AddBlock("Third block")
blocks := blockchain.GetBlocks() // Get blocks
blocks[0].data = "Genesis block" // Try to change data
blocks = blockchain.GetBlocks() // Get blocks again
for _, block := range blocks {
fmt.Printf("Prev. hash: %s\n", block.prevHash)
fmt.Printf("Data: %s\n", block.data)
fmt.Printf("Hash: %s\n", block.hash)
fmt.Println("==================================")
}
}
블록체인에 슬라이스로 저장된 블록들을 불러와 첫 번째 블록의 데이터를 변경해 보았습니다.
$ go run main.go
Prev. hash:
Data: Genesis block
Hash: 7f647b7d55a6cf3598d1c4c386326f72bb9f28ee2e107168162195cd86e5ea23
==================================
Prev. hash: 7f647b7d55a6cf3598d1c4c386326f72bb9f28ee2e107168162195cd86e5ea23
Data: Second block
Hash: d552fe3755fc27d2680d65b516572ca7217bcdc5f7cee1aca115fab35035460e
==================================
Prev. hash: d552fe3755fc27d2680d65b516572ca7217bcdc5f7cee1aca115fab35035460e
Data: Third block
Hash: 3615128e0cffc4015b6d5113a1e6fd0533075d346ce12c116b8ff864cf09035e
==================================
앗! 데이터가 변경되었네요! 왜 이런 일이 발생하는 걸까요?
3. 원인
앞서 '슬라이스는 실제 데이터가 저장된 배열을 가리키는 포인터값을 가지고 있다'라고 했습니다. 그리고 Golang의 함수나 메서드의 매개변수로 넘어가는 값이나 결과로 반환되는 값은 모두 복사된 값으로 전달됩니다. 즉, 그렇게 전달된 값을 변경한다고 해서 원본 값을 변경할 수 없다는 것이죠. 그러나 슬라이스가 가지고 있는 포인터가 복사되어 전달되는 경우는 조금 다릅니다. 포인터가 가리키는 실제 배열은 포인터를 통해서 접근할 수 있고 변경도 가능합니다.
다만 슬라이스가 가리키는 배열의 값에 접근할 수 있다는 것이지 원본 슬라이스의 원소를 삭제하거나 새로운 원소를 추가하는 것은 어렵습니다. 슬라이스에는 포인터값 외에도 슬라이스의 길이를 가리키는 len값과 용량을 가리키는 cap값이 포함되어 있습니다. 이 값들은 포인터값과 마찬가지로 복사되어 전달되므로 변경을 한다한들 원본 값은 영향을 받지 않습니다.
4. 해결 방법
1) 새로운 슬라이스를 생성해서 반환하기
func (bc Blockchain) GetBlocks() []Block {
newBlocks := make([]Block, len(bc.blocks))
copy(newBlocks, bc.blocks)
return newBlocks
}
새로운 슬라이스는 새로운 포인터값을 가지므로 기존 슬라이스에 접근할 수 없어집니다.
2) 패키지 분리
package block
import (
"crypto/sha256"
"encoding/hex"
)
type Block struct {
hash string
prevHash string
data string
height int
}
func NewBlock(prevHash string, data string, height int) Block {
block := Block{
hash: calculateHash(prevHash, data, height),
prevHash: prevHash,
data: data,
height: height,
}
return block
}
func (b Block) Hash() string {
return b.hash
}
func (b Block) PrevHash() string {
return b.prevHash
}
func (b Block) Data() string {
return b.data
}
func (b Block) Height() int {
return b.height
}
func calculateHash(prevHash string, data string, height int) string {
hash := sha256.New()
_, _ = hash.Write([]byte(fmt.Sprintf("%s%d%s", prevHash, height, data)))
return hex.EncodeToString(hash.Sum(nil))
}
block 패키지를 분리하게 되면 Block 구조체의 필드가 소문자로 시작하므로 public 접근이 불가능하여 블록 정보를 읽어 들이기 위해 별도의 getter 메서드를 정의해줘야 합니다.
📖 참고 자료
https://nomadcoders.co/typescript-for-beginners
https://stackoverflow.com/questions/39993688/are-slices-passed-by-value
'Go > 코딩 하기' 카테고리의 다른 글
[Go] gRPC 파헤치기 - gRPC란? (0) | 2023.10.11 |
---|---|
[Go] 문자열과 바이트 슬라이스를 상호 변환하는 여러가지 방법 (0) | 2023.10.01 |
[Go] 잠자는 이발사 문제 (0) | 2023.09.30 |
[Go] 식사하는 철학자들 문제 (0) | 2023.09.29 |
[Go] 깃허브 프로필 최신 블로그 글 목록 자동으로 업데이트하기 (0) | 2023.09.06 |