Ver3.0 ๐ Go์ธ์ด๋ก ๋ธ๋ก์ฒด์ธ DApp ๋ฐฑ์๋ ๋ง๋ค๊ธฐ

๐ Go์ธ์ด๋ก ๋ธ๋ก์ฒด์ธ DApp ๋ฐฑ์๋ ๋ง๋ค๊ธฐ
์ด๋๋ฆฌ์ ํด๋ผ์ด์ธํธ์ ํจ๊ปํ๋ ํ์ค์ํ ์ ํ๋ฆฌ์ผ์ด์ ๊ฐ๋ฐ ์์ ์ ๋ณต ๐
๐ฏ ๋ธ๋ก์ฒด์ธ ๋ฐฑ์๋, ์ Go์ธ์ด์ผ๊น?
์๋ ! ์ค๋์ ์ ๋ง ํซํ ์ฃผ์ ๋ฅผ ๊ฐ์ง๊ณ ์์ด. ๋ฐ๋ก Go์ธ์ด๋ก ๋ธ๋ก์ฒด์ธ DApp ๋ฐฑ์๋๋ฅผ ๋ง๋๋ ๋ฐฉ๋ฒ์ด์ผ. ์์ฆ Web3, ๋ธ๋ก์ฒด์ธ, NFT ์ด๋ฐ ๋จ์ด๋ค ์ ๋ง ๋ง์ด ๋ค๋ฆฌ์ง? ๊ทผ๋ฐ ๋ง์ ๊ฐ๋ฐํ๋ ค๊ณ ํ๋ฉด ์ด๋์๋ถํฐ ์์ํด์ผ ํ ์ง ๋ง๋งํ๋๋ผ๊ณ . ๐ํนํ ๋ฐฑ์๋ ๊ฐ๋ฐ์๋ผ๋ฉด "ํ๋ก ํธ์๋๋ ๊ทธ๋ ๋ค ์น๊ณ , ๋ฐฑ์๋๋ ์ด๋ป๊ฒ ๊ตฌ์ฑํ์ง?"๋ผ๋ ๊ณ ๋ฏผ์ด ํด ๊ฑฐ์ผ. ์ ํต์ ์ธ ์ค์ํ ์๋ฒ์๋ ์์ ํ ๋ค๋ฅธ ๊ตฌ์กฐ๋๊น ๋ง์ด์ผ.
๊ทธ๋์ ์ค๋์ Go์ธ์ด๋ฅผ ์ฌ์ฉํด์ ์ด๋๋ฆฌ์ ๋ธ๋ก์ฒด์ธ๊ณผ ํต์ ํ๊ณ , ์ค๋งํธ ์ปจํธ๋ํธ์ ์ํธ์์ฉํ๋ฉฐ, ์ค์ DApp(ํ์ค์ํ ์ ํ๋ฆฌ์ผ์ด์ )์ ๋ฐฑ์๋๋ฅผ ๊ตฌ์ถํ๋ ์ ๊ณผ์ ์ ์น๊ตฌ์ฒ๋ผ ํธํ๊ฒ ์ค๋ช ํด์ค๊ฒ! ๐
๐ค DApp ๋ฐฑ์๋๊ฐ ๋ญ๊ธธ๋?
๋จผ์ ๊ฐ๋ ๋ถํฐ ์ ๋ฆฌํ๊ณ ๊ฐ์! ์ ํต์ ์ธ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ํ๋ก ํธ์๋-๋ฐฑ์๋-๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ตฌ์กฐ์์? ๊ทผ๋ฐ DApp์ ์ข ๋ฌ๋ผ.์ ํต์ ์ธ ์ฑ: ํด๋ผ์ด์ธํธ โ ์ค์ ์๋ฒ โ ์ค์ DB
DApp: ํด๋ผ์ด์ธํธ โ ๋ธ๋ก์ฒด์ธ ๋คํธ์ํฌ โ ์ค๋งํธ ์ปจํธ๋ํธ
๊ทธ๋ผ ๋ฐฑ์๋๋ ํ์ ์๋ ๊ฑฐ์ผ? ์๋์ผ! ์คํ๋ ค ๋ ์ค์ํด! ๐ฏ
1๏ธโฃ ๋ธ๋ก์ฒด์ธ ๋ฐ์ดํฐ ์ธ๋ฑ์ฑ
๋ธ๋ก์ฒด์ธ์์ ์ง์ ๋ฐ์ดํฐ๋ฅผ ์ฝ๋ ๊ฑด ๋๋ฆฌ๊ณ ๋นํจ์จ์ ์ด์ผ. ๋ฐฑ์๋์์ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ๋ฏธ๋ฆฌ ๊ฐ์ ธ์์ ์บ์ฑํ๊ณ ์ธ๋ฑ์ฑํด๋๋ฉด ํจ์ฌ ๋น ๋ฅด์ง!
2๏ธโฃ ๋ณต์กํ ๋น์ฆ๋์ค ๋ก์ง ์ฒ๋ฆฌ
๋ชจ๋ ๊ฑธ ์ค๋งํธ ์ปจํธ๋ํธ์ ๋ฃ์ผ๋ฉด ๊ฐ์ค๋น๊ฐ ์์ฒญ๋๊ฒ ๋์. ๋ธ๋ก์ฒด์ธ์ ๊ผญ ํ์ํ ๊ฒ๋ง ์ฌ๋ฆฌ๊ณ , ๋๋จธ์ง๋ ๋ฐฑ์๋์์ ์ฒ๋ฆฌํ๋ ๊ฒ ํ๋ช ํด.
3๏ธโฃ ์ธ๋ถ API ํตํฉ
๊ฒฐ์ ์์คํ , ์ด๋ฉ์ผ ์๋ฆผ, ํ์ผ ์ ์ฅ์(IPFS) ๋ฑ ์ธ๋ถ ์๋น์ค์์ ํตํฉ์ ๋ฐฑ์๋๊ฐ ๋ด๋นํด์ผ ํด.
4๏ธโฃ ์ฌ์ฉ์ ๊ฒฝํ ๊ฐ์
ํธ๋์ญ์ ์ํ ์ถ์ , ์ค์๊ฐ ์๋ฆผ, ๋ฐ์ดํฐ ๋ถ์ ๋ฑ ์ฌ์ฉ์ ๊ฒฝํ์ ๋์ด๋ ๊ธฐ๋ฅ๋ค์ ๋ฐฑ์๋์์ ๊ตฌํํ๋ ๊ฒ ์ข์.
๊ทธ๋ผ ์ Go์ธ์ด๋ฅผ ์ ํํด์ผ ํ ๊น? ๐คทโโ๏ธ
๐ช Go์ธ์ด์ ๊ฐ๋ ฅํ ์ฅ์ ๋ค
Go์ธ์ด๋ ๋ธ๋ก์ฒด์ธ ๋ฐฑ์๋ ๊ฐ๋ฐ์ ์ ๋ง ์ต์ ํ๋ ์ธ์ด์ผ. ์ด์ ๋ฅผ ํ๋์ฉ ์ดํด๋ณด์!Go๋ ์ปดํ์ผ ์ธ์ด๋ผ์ ์คํ ์๋๊ฐ ๋น ๋ฅด๊ณ , ๊ณ ๋ฃจํด(Goroutine)์ด๋ผ๋ ๊ฒฝ๋ ์ค๋ ๋ ๋๋ถ์ ๋์์ ์์ฒ ๊ฐ์ ๋ธ๋ก์ฒด์ธ ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ ์ ์์ด. ์ด๋๋ฆฌ์ ๋คํธ์ํฌ๋ฅผ ์ค์๊ฐ์ผ๋ก ๋ชจ๋ํฐ๋งํ๋ฉด์ ์ฌ๋ฌ ์ค๋งํธ ์ปจํธ๋ํธ์ ๋์์ ํต์ ํด์ผ ํ๋ DApp ๋ฐฑ์๋์๋ ๋ฑ์ด์ง! ๐
์ด๋๋ฆฌ์์ ๊ฐ์ฅ ์ธ๊ธฐ ์๋ ํด๋ผ์ด์ธํธ์ธ Geth๊ฐ Go๋ก ์์ฑ๋์ด ์์ด. ๊ทธ๋์ Go ์ํ๊ณ์๋ ๋ธ๋ก์ฒด์ธ ๊ด๋ จ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์ ๋ง ํ๋ถํด. ํนํ go-ethereum ํจํค์ง๋ ์ด๋๋ฆฌ์๊ณผ ์ํธ์์ฉํ๋ ๋ชจ๋ ๊ธฐ๋ฅ์ ์ ๊ณตํ์ง.
2. ๊ฐ๊ฒฐํ๊ณ ์ฝ๊ธฐ ์ฌ์ด ์ฝ๋
Go์ ๋ฌธ๋ฒ์ ์ ๋ง ์ฌํํด. ๋ณต์กํ ๋ธ๋ก์ฒด์ธ ๋ก์ง์ ๊ตฌํํ ๋ ์ฝ๋๊ฐ ์ง์ ๋ถํด์ง์ง ์๊ณ ๊น๋ํ๊ฒ ์ ์ง๋ผ. ํ ํ๋ก์ ํธ์์ ํ์ ํ ๋๋ ์ฝ๋ ๋ฆฌ๋ทฐ๊ฐ ์์ํ์ง.
3. ๊ฐ๋ ฅํ ํ์ค ๋ผ์ด๋ธ๋ฌ๋ฆฌ
HTTP ์๋ฒ, JSON ์ฒ๋ฆฌ, ์ํธํ ๋ฑ ๋ฐฑ์๋ ๊ฐ๋ฐ์ ํ์ํ ๊ฑฐ์ ๋ชจ๋ ๊ธฐ๋ฅ์ด ํ์ค ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ํฌํจ๋์ด ์์ด. ์ธ๋ถ ์์กด์ฑ์ ์ต์ํํ ์ ์๋ค๋ ๊ฒ ํฐ ์ฅ์ ์ด์ผ.
4. ํฌ๋ก์ค ํ๋ซํผ ์ง์
ํ ๋ฒ ์์ฑํ ์ฝ๋๋ฅผ Linux, Windows, macOS ์ด๋์๋ ์ปดํ์ผํด์ ์คํํ ์ ์์ด. ๋ฐฐํฌ๊ฐ ์ ๋ง ๊ฐํธํ์ง!
์ค์ ๋ก ๋ง์ ๋ธ๋ก์ฒด์ธ ํ๋ก์ ํธ๋ค์ด Go๋ฅผ ์ฌ์ฉํ๊ณ ์์ด. Ethereum, Hyperledger Fabric, Cosmos SDK ๋ฑ์ด ๋ํ์ ์ด์ผ. ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์๋ ๋ธ๋ก์ฒด์ธ ๊ด๋ จ ๊ฐ๋ฐ ์ฌ๋ฅ์ ๊ฑฐ๋ํ ๋ Go ๊ฐ๋ฐ์ ์์๊ฐ ์ ๋ง ๋๋๋ผ๊ณ ! ๐
๐ ๏ธ ๊ฐ๋ฐ ํ๊ฒฝ ์ธํ ํ๊ธฐ
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ๊ฐ๋ฐ์ ์์ํด๋ณด์! ๋จผ์ ํ์ํ ๋๊ตฌ๋ค์ ์ค์นํด์ผ ํด.# ์ค์น ํ์ธ
go version
# ์ถ๋ ฅ ์์: go version go1.21.0 linux/amd64
# ํ๋ก์ ํธ ๋๋ ํ ๋ฆฌ ์์ฑ
mkdir dapp-backend
cd dapp-backend
# Go ๋ชจ๋ ์ด๊ธฐํ
go mod init github.com/yourusername/dapp-backend
# go-ethereum ์ค์น
go get github.com/ethereum/go-ethereum
โข Ganache: ๋ก์ปฌ ์ด๋๋ฆฌ์ ํ ์คํธ ๋คํธ์ํฌ (๊ฐ๋ฐ์ฉ)
โข MetaMask: ์ด๋๋ฆฌ์ ์ง๊ฐ (ํ ์คํธ์ฉ)
โข Remix IDE: ์ค๋งํธ ์ปจํธ๋ํธ ์์ฑ ๋ฐ ๋ฐฐํฌ
โข Infura ๋๋ Alchemy: ์ด๋๋ฆฌ์ ๋ ธ๋ ์ ๊ทผ ์๋น์ค
โข Solidity ์ปดํ์ผ๋ฌ: ์ค๋งํธ ์ปจํธ๋ํธ ์ปดํ์ผ
์ฒ์์๋ Ganache๋ก ๋ก์ปฌ ํ๊ฒฝ์์ ๊ฐ๋ฐํ๊ณ , ๋์ค์ Goerli๋ Sepolia ๊ฐ์ ํ ์คํธ๋ท์ผ๋ก ์ฎ๊ธฐ๋ ๊ฒ ์ข์. ์ค์ ๋ฉ์ธ๋ท์ ๊ฐ์ค๋น๊ฐ ๋ค์ด๊ฐ๋๊น ์ถฉ๋ถํ ํ ์คํธํ ํ์ ๋ฐฐํฌํด์ผ ํด!
๐ ์ด๋๋ฆฌ์ ํด๋ผ์ด์ธํธ ์ฐ๊ฒฐํ๊ธฐ
์ด์ Go ์ฝ๋๋ก ์ด๋๋ฆฌ์ ๋คํธ์ํฌ์ ์ฐ๊ฒฐํด๋ณด์! ์ด๊ฒ ๋ชจ๋ ๊ฒ์ ์์์ด์ผ.package main
import (
"context"
"fmt"
"log"
"math/big"
"github.com/ethereum/go-ethereum/ethclient"
)
func main() {
// Infura ๋๋ ๋ก์ปฌ ๋
ธ๋์ ์ฐ๊ฒฐ
client, err := ethclient.Dial("https://mainnet.infura.io/v3/YOUR_PROJECT_ID")
if err != nil {
log.Fatal(err)
}
fmt.Println("์ด๋๋ฆฌ์ ๋คํธ์ํฌ์ ์ฐ๊ฒฐ๋์์ต๋๋ค!")
// ์ต์ ๋ธ๋ก ๋ฒํธ ๊ฐ์ ธ์ค๊ธฐ
header, err := client.HeaderByNumber(context.Background(), nil)
if err != nil {
log.Fatal(err)
}
fmt.Println("์ต์ ๋ธ๋ก:", header.Number.String())
// ํน์ ์ฃผ์์ ์์ก ์กฐํ
address := "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"
balance, err := client.BalanceAt(context.Background(),
common.HexToAddress(address), nil)
if err != nil {
log.Fatal(err)
}
// Wei๋ฅผ Ether๋ก ๋ณํ
fbalance := new(big.Float)
fbalance.SetString(balance.String())
ethValue := new(big.Float).Quo(fbalance, big.NewFloat(1e18))
fmt.Printf("์์ก: %f ETH\n", ethValue)
}
์ด ์ฝ๋๋ฅผ ์คํํ๋ฉด ์ค์๊ฐ์ผ๋ก ์ด๋๋ฆฌ์ ๋ฉ์ธ๋ท์ ์ต์ ๋ธ๋ก ์ ๋ณด์ ํน์ ์ฃผ์์ ์์ก์ ํ์ธํ ์ ์์ด! ๐
Infura๋ Alchemy ๊ฐ์ ์๋น์ค๋ฅผ ์ฌ์ฉํ ๋๋ ๋ฐ๋์ API ํค๋ฅผ ํ๊ฒฝ๋ณ์๋ก ๊ด๋ฆฌํด์ผ ํด. ์ฝ๋์ ์ง์ ํ๋์ฝ๋ฉํ๋ฉด GitHub์ ์ฌ๋ ธ์ ๋ ๋ณด์ ๋ฌธ์ ๊ฐ ์๊ธธ ์ ์์ด!
package main
import (
"os"
"github.com/joho/godotenv"
)
func init() {
// .env ํ์ผ ๋ก๋
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
}
func main() {
// ํ๊ฒฝ๋ณ์์์ API ํค ๊ฐ์ ธ์ค๊ธฐ
infuraURL := os.Getenv("INFURA_URL")
client, err := ethclient.Dial(infuraURL)
// ... ๋๋จธ์ง ์ฝ๋
}
.env ํ์ผ:INFURA_URL=https://mainnet.infura.io/v3/your_project_id_here
PRIVATE_KEY=your_private_key_here
CONTRACT_ADDRESS=0x...
์ด๋ ๊ฒ ํ๋ฉด ํจ์ฌ ์์ ํ๊ณ ๊ด๋ฆฌํ๊ธฐ๋ ์ฌ์์ ธ! ๐
๐ ์ค๋งํธ ์ปจํธ๋ํธ์ ์ํธ์์ฉํ๊ธฐ
์ด์ ์ง์ง ํต์ฌ์ผ๋ก ๋ค์ด๊ฐ๋ณด์! ์ค๋งํธ ์ปจํธ๋ํธ์ ํต์ ํ๋ ๋ฐฉ๋ฒ์ด์ผ. ์ด๊ฒ DApp ๋ฐฑ์๋์ ๊ฝ์ด๋ผ๊ณ ํ ์ ์์ง. ๐ธ// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleToken {
string public name = "MyToken";
string public symbol = "MTK";
uint8 public decimals = 18;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
event Transfer(address indexed from, address indexed to, uint256 value);
constructor(uint256 _initialSupply) {
totalSupply = _initialSupply * 10 ** uint256(decimals);
balanceOf[msg.sender] = totalSupply;
}
function transfer(address _to, uint256 _value) public returns (bool success) {
require(balanceOf[msg.sender] >= _value, "Insufficient balance");
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
emit Transfer(msg.sender, _to, _value);
return true;
}
}
# abigen ์ค์น
go install github.com/ethereum/go-ethereum/cmd/abigen@latest
# ์ปจํธ๋ํธ ์ปดํ์ผ (solc ํ์)
solc --abi --bin SimpleToken.sol -o build/
# Go ๋ฐ์ธ๋ฉ ์์ฑ
abigen --abi=build/SimpleToken.abi \
--bin=build/SimpleToken.bin \
--pkg=token \
--out=token/SimpleToken.go
์ด๋ ๊ฒ ํ๋ฉด token/SimpleToken.go ํ์ผ์ด ์์ฑ๋ผ. ์ด ํ์ผ์๋ ์ปจํธ๋ํธ์ ๋ชจ๋ ํจ์๋ฅผ Go์์ ํธ์ถํ ์ ์๋ ๋ฉ์๋๋ค์ด ๋ค์ด์์ด!
package main
import (
"context"
"crypto/ecdsa"
"fmt"
"log"
"math/big"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
token "github.com/yourusername/dapp-backend/token"
)
func main() {
// ํด๋ผ์ด์ธํธ ์ฐ๊ฒฐ
client, err := ethclient.Dial("http://localhost:8545") // Ganache
if err != nil {
log.Fatal(err)
}
// ๊ฐ์ธํค ๋ก๋
privateKey, err := crypto.HexToECDSA("your_private_key_here")
if err != nil {
log.Fatal(err)
}
publicKey := privateKey.Public()
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
log.Fatal("error casting public key to ECDSA")
}
fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
// Nonce ๊ฐ์ ธ์ค๊ธฐ
nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
if err != nil {
log.Fatal(err)
}
// ๊ฐ์ค ๊ฐ๊ฒฉ ์ค์
gasPrice, err := client.SuggestGasPrice(context.Background())
if err != nil {
log.Fatal(err)
}
// ํธ๋์ญ์
์ต์
์ค์
auth, err := bind.NewKeyedTransactorWithChainID(privateKey, big.NewInt(1337))
if err != nil {
log.Fatal(err)
}
auth.Nonce = big.NewInt(int64(nonce))
auth.Value = big.NewInt(0)
auth.GasLimit = uint64(3000000)
auth.GasPrice = gasPrice
// ์ปจํธ๋ํธ ๋ฐฐํฌ
initialSupply := big.NewInt(1000000)
address, tx, instance, err := token.DeploySimpleToken(auth, client, initialSupply)
if err != nil {
log.Fatal(err)
}
fmt.Printf("์ปจํธ๋ํธ ๋ฐฐํฌ๋จ! ์ฃผ์: %s\n", address.Hex())
fmt.Printf("ํธ๋์ญ์
ํด์: %s\n", tx.Hash().Hex())
// ์ปจํธ๋ํธ ์ธ์คํด์ค ์ฌ์ฉ
name, err := instance.Name(nil)
if err != nil {
log.Fatal(err)
}
fmt.Printf("ํ ํฐ ์ด๋ฆ: %s\n", name)
}
์! ์ฝ๋๊ฐ ์ข ๊ธธ์ง? ํ์ง๋ง ํ๋ํ๋ ๋ฏ์ด๋ณด๋ฉด ๋ณ๋ก ์ด๋ ต์ง ์์. ํต์ฌ์ ์ด๊ฑฐ์ผ:
1๏ธโฃ ์ด๋๋ฆฌ์ ํด๋ผ์ด์ธํธ์ ์ฐ๊ฒฐ
2๏ธโฃ ๊ฐ์ธํค๋ก ํธ๋์ญ์ ์๋ช ์ค๋น
3๏ธโฃ Nonce์ ๊ฐ์ค ๊ฐ๊ฒฉ ์ค์
4๏ธโฃ ์ปจํธ๋ํธ ๋ฐฐํฌ ํจ์ ํธ์ถ
5๏ธโฃ ๋ฐฐํฌ๋ ์ปจํธ๋ํธ ์ฃผ์ ๋ฐ๊ธฐ
๐ ์ปจํธ๋ํธ ํจ์ ํธ์ถํ๊ธฐ
์ปจํธ๋ํธ๊ฐ ๋ฐฐํฌ๋์์ผ๋ ์ด์ ํจ์๋ฅผ ํธ์ถํด๋ณด์! ํฌ๊ฒ ๋ ๊ฐ์ง ํ์ ์ด ์์ด:// ๊ธฐ์กด ์ปจํธ๋ํธ์ ์ฐ๊ฒฐ
contractAddress := common.HexToAddress("0x...")
instance, err := token.NewSimpleToken(contractAddress, client)
if err != nil {
log.Fatal(err)
}
// ํ ํฐ ์ด๋ฆ ์กฐํ
name, err := instance.Name(&bind.CallOpts{})
if err != nil {
log.Fatal(err)
}
fmt.Printf("ํ ํฐ ์ด๋ฆ: %s\n", name)
// ํน์ ์ฃผ์์ ์์ก ์กฐํ
userAddress := common.HexToAddress("0x...")
balance, err := instance.BalanceOf(&bind.CallOpts{}, userAddress)
if err != nil {
log.Fatal(err)
}
fmt.Printf("์์ก: %s\n", balance.String())
// ์ด ๊ณต๊ธ๋ ์กฐํ
totalSupply, err := instance.TotalSupply(&bind.CallOpts{})
if err != nil {
log.Fatal(err)
}
fmt.Printf("์ด ๊ณต๊ธ๋: %s\n", totalSupply.String())
// ํธ๋์ญ์
์ต์
์ค์
auth, err := bind.NewKeyedTransactorWithChainID(privateKey, chainID)
if err != nil {
log.Fatal(err)
}
// Nonce ์
๋ฐ์ดํธ
nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
if err != nil {
log.Fatal(err)
}
auth.Nonce = big.NewInt(int64(nonce))
// ๊ฐ์ค ์ค์
auth.GasLimit = uint64(300000)
gasPrice, err := client.SuggestGasPrice(context.Background())
if err != nil {
log.Fatal(err)
}
auth.GasPrice = gasPrice
// ํ ํฐ ์ ์ก
toAddress := common.HexToAddress("0x...")
amount := big.NewInt(1000000000000000000) // 1 ํ ํฐ (18 decimals)
tx, err := instance.Transfer(auth, toAddress, amount)
if err != nil {
log.Fatal(err)
}
fmt.Printf("ํธ๋์ญ์
์ ์ก๋จ: %s\n", tx.Hash().Hex())
// ํธ๋์ญ์
์ด ๋ง์ด๋๋ ๋๊น์ง ๋๊ธฐ
receipt, err := bind.WaitMined(context.Background(), client, tx)
if err != nil {
log.Fatal(err)
}
if receipt.Status == 1 {
fmt.Println("ํธ๋์ญ์
์ฑ๊ณต!")
} else {
fmt.Println("ํธ๋์ญ์
์คํจ!")
}
๊ฐ์ค๋น๋ฅผ ์ ์ฝํ๋ ค๋ฉด:
โข ๋ถํ์ํ ์ํ ๋ณ๊ฒฝ ์ต์ํ
โข ๋ฐฐ์น ์ฒ๋ฆฌ๋ก ์ฌ๋ฌ ์์ ์ ํ ๋ฒ์
โข ๊ฐ์ค ๊ฐ๊ฒฉ์ด ๋ฎ์ ์๊ฐ๋ ํ์ฉ
โข Layer 2 ์๋ฃจ์ ๊ณ ๋ ค (Polygon, Arbitrum ๋ฑ)
๐ก ์ด๋ฒคํธ ๋ฆฌ์ค๋ํ๊ธฐ
์ค๋งํธ ์ปจํธ๋ํธ์์ ๋ฐ์ํ๋ ์ด๋ฒคํธ๋ฅผ ์ค์๊ฐ์ผ๋ก ๊ฐ์งํ๋ ๊ฑด DApp ๋ฐฑ์๋์ ํต์ฌ ๊ธฐ๋ฅ์ด์ผ! ์๋ฅผ ๋ค์ด ํ ํฐ ์ ์ก์ด ์ผ์ด๋ ๋๋ง๋ค ์๋ฆผ์ ๋ณด๋ด๊ฑฐ๋, ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ ๋ฐ์ดํธํ ์ ์์ง.package main
import (
"context"
"fmt"
"log"
"math/big"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
token "github.com/yourusername/dapp-backend/token"
)
func main() {
client, err := ethclient.Dial("http://localhost:8545")
if err != nil {
log.Fatal(err)
}
contractAddress := common.HexToAddress("0x...")
// ์ด๋ฒคํธ ํํฐ ์ฟผ๋ฆฌ ์์ฑ
query := ethereum.FilterQuery{
FromBlock: big.NewInt(0),
ToBlock: big.NewInt(1000),
Addresses: []common.Address{contractAddress},
}
// ๋ก๊ทธ ์กฐํ
logs, err := client.FilterLogs(context.Background(), query)
if err != nil {
log.Fatal(err)
}
// ์ปจํธ๋ํธ ABI ํ์ฑ
contractAbi, err := abi.JSON(strings.NewReader(token.SimpleTokenABI))
if err != nil {
log.Fatal(err)
}
// ๊ฐ ๋ก๊ทธ ์ฒ๋ฆฌ
for _, vLog := range logs {
// Transfer ์ด๋ฒคํธ์ธ์ง ํ์ธ
event := struct {
From common.Address
To common.Address
Value *big.Int
}{}
err := contractAbi.UnpackIntoInterface(&event, "Transfer", vLog.Data)
if err != nil {
log.Fatal(err)
}
// Indexed ํ๋ผ๋ฏธํฐ ์ฒ๋ฆฌ
event.From = common.HexToAddress(vLog.Topics[1].Hex())
event.To = common.HexToAddress(vLog.Topics[2].Hex())
fmt.Printf("Transfer: %s -> %s, Amount: %s\n",
event.From.Hex(), event.To.Hex(), event.Value.String())
}
}
package main
import (
"context"
"fmt"
"log"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
)
func main() {
// WebSocket์ผ๋ก ์ฐ๊ฒฐ (์ค์!)
client, err := ethclient.Dial("ws://localhost:8546")
if err != nil {
log.Fatal(err)
}
contractAddress := common.HexToAddress("0x...")
// ๊ตฌ๋
์ฟผ๋ฆฌ ์์ฑ
query := ethereum.FilterQuery{
Addresses: []common.Address{contractAddress},
}
// ๋ก๊ทธ ์ฑ๋ ์์ฑ
logs := make(chan types.Log)
// ๊ตฌ๋
์์
sub, err := client.SubscribeFilterLogs(context.Background(), query, logs)
if err != nil {
log.Fatal(err)
}
fmt.Println("์ด๋ฒคํธ ๋ฆฌ์ค๋ ์์...")
// ๋ฌดํ ๋ฃจํ๋ก ์ด๋ฒคํธ ๋๊ธฐ
for {
select {
case err := <-sub.Err():
log.Fatal(err)
case vLog := <-logs:
fmt.Printf("์ ์ด๋ฒคํธ ๊ฐ์ง! ๋ธ๋ก: %d, ํธ๋์ญ์
: %s\n",
vLog.BlockNumber, vLog.TxHash.Hex())
// ์ฌ๊ธฐ์ ์ด๋ฒคํธ ์ฒ๋ฆฌ ๋ก์ง ๊ตฌํ
// - ๋ฐ์ดํฐ๋ฒ ์ด์ค ์
๋ฐ์ดํธ
// - ์ฌ์ฉ์์๊ฒ ์๋ฆผ ์ ์ก
// - ํต๊ณ ๋ฐ์ดํฐ ๊ฐฑ์ ๋ฑ
}
}
}
์ค์๊ฐ ์ด๋ฒคํธ ๋ฆฌ์ค๋์ ์ด๋ฐ ๊ณณ์ ํ์ฉ๋ผ:
โข NFT ๋ง์ผํ๋ ์ด์ค: ์๋ก์ด NFT ๋ฏผํ ์ด๋ ๊ฑฐ๋ ๊ฐ์ง
โข DeFi ๋์๋ณด๋: ์ค์, ์์น, ์ธ์ถ ์ด๋ฒคํธ ์ถ์
โข ๊ฒ์ DApp: ์์ดํ ํ๋, ๋ ๋ฒจ์ ๋ฑ ๊ฒ์ ์ด๋ฒคํธ
โข DAO ํ๋ซํผ: ํฌํ, ์ ์ ์์ฑ ๋ฑ ๊ฑฐ๋ฒ๋์ค ํ๋
๐๏ธ DApp ๋ฐฑ์๋ ์ํคํ ์ฒ ์ค๊ณ
์ด์ ์ค์ ํ๋ก๋์ ๋ ๋ฒจ์ DApp ๋ฐฑ์๋ ๊ตฌ์กฐ๋ฅผ ์ค๊ณํด๋ณด์! ๋จ์ํ ๋ธ๋ก์ฒด์ธ๊ณผ ํต์ ํ๋ ๊ฒ์ ๋์ด์, ํ์ฅ ๊ฐ๋ฅํ๊ณ ์ ์ง๋ณด์ํ๊ธฐ ์ฌ์ด ์์คํ ์ ๋ง๋๋ ๊ฒ ๋ชฉํ์ผ. ๐ฏdapp-backend/
โโโ cmd/
โ โโโ server/
โ โโโ main.go # ์ ํ๋ฆฌ์ผ์ด์
์ง์
์
โโโ internal/
โ โโโ api/ # HTTP API ํธ๋ค๋ฌ
โ โ โโโ handlers/
โ โ โโโ middleware/
โ โ โโโ routes.go
โ โโโ blockchain/ # ๋ธ๋ก์ฒด์ธ ์ํธ์์ฉ
โ โ โโโ client.go
โ โ โโโ contract.go
โ โ โโโ listener.go
โ โโโ service/ # ๋น์ฆ๋์ค ๋ก์ง
โ โ โโโ token_service.go
โ โ โโโ user_service.go
โ โ โโโ transaction_service.go
โ โโโ repository/ # ๋ฐ์ดํฐ ์ ๊ทผ
โ โ โโโ postgres/
โ โ โโโ redis/
โ โโโ models/ # ๋ฐ์ดํฐ ๋ชจ๋ธ
โ โโโ user.go
โ โโโ transaction.go
โโโ pkg/ # ๊ณต์ฉ ํจํค์ง
โ โโโ config/
โ โโโ logger/
โ โโโ utils/
โโโ contracts/ # ์ค๋งํธ ์ปจํธ๋ํธ
โ โโโ SimpleToken.sol
โ โโโ bindings/
โโโ docker-compose.yml
์ฌ๋ฌ ๋คํธ์ํฌ๋ฅผ ๊ด๋ฆฌํ๊ณ ์ฐ๊ฒฐ ํ๋ง์ ์ ๊ณตํ๋ ํด๋ผ์ด์ธํธ ๋งค๋์ ์ผ:
package blockchain
import (
"context"
"fmt"
"sync"
"github.com/ethereum/go-ethereum/ethclient"
)
type ClientManager struct {
clients map[string]*ethclient.Client
mu sync.RWMutex
}
func NewClientManager() *ClientManager {
return &ClientManager{
clients: make(map[string]*ethclient.Client),
}
}
func (cm *ClientManager) AddClient(name, url string) error {
cm.mu.Lock()
defer cm.mu.Unlock()
client, err := ethclient.Dial(url)
if err != nil {
return fmt.Errorf("failed to connect to %s: %w", name, err)
}
// ์ฐ๊ฒฐ ํ
์คํธ
_, err = client.ChainID(context.Background())
if err != nil {
return fmt.Errorf("failed to get chain ID: %w", err)
}
cm.clients[name] = client
return nil
}
func (cm *ClientManager) GetClient(name string) (*ethclient.Client, error) {
cm.mu.RLock()
defer cm.mu.RUnlock()
client, exists := cm.clients[name]
if !exists {
return nil, fmt.Errorf("client %s not found", name)
}
return client, nil
}
func (cm *ClientManager) Close() {
cm.mu.Lock()
defer cm.mu.Unlock()
for _, client := range cm.clients {
client.Close()
}
}
2๏ธโฃ ํธ๋์ญ์ ์๋น์ค
ํธ๋์ญ์ ์์ฑ, ์ ์ก, ์ถ์ ์ ๋ด๋นํ๋ ์๋น์ค์ผ:
package service
import (
"context"
"crypto/ecdsa"
"fmt"
"math/big"
"time"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
)
type TransactionService struct {
client *ethclient.Client
privateKey *ecdsa.PrivateKey
chainID *big.Int
}
func NewTransactionService(client *ethclient.Client, privateKeyHex string) (*TransactionService, error) {
privateKey, err := crypto.HexToECDSA(privateKeyHex)
if err != nil {
return nil, err
}
chainID, err := client.ChainID(context.Background())
if err != nil {
return nil, err
}
return &TransactionService{
client: client,
privateKey: privateKey,
chainID: chainID,
}, nil
}
func (ts *TransactionService) SendTransaction(
ctx context.Context,
to common.Address,
value *big.Int,
data []byte,
) (*types.Transaction, error) {
fromAddress := crypto.PubkeyToAddress(ts.privateKey.Public().(*ecdsa.PublicKey))
nonce, err := ts.client.PendingNonceAt(ctx, fromAddress)
if err != nil {
return nil, fmt.Errorf("failed to get nonce: %w", err)
}
gasPrice, err := ts.client.SuggestGasPrice(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get gas price: %w", err)
}
// ๊ฐ์ค ๋ฆฌ๋ฏธํธ ์ถ์
gasLimit, err := ts.client.EstimateGas(ctx, ethereum.CallMsg{
From: fromAddress,
To: &to,
Value: value,
Data: data,
})
if err != nil {
gasLimit = 300000 // ๊ธฐ๋ณธ๊ฐ
}
tx := types.NewTransaction(nonce, to, value, gasLimit, gasPrice, data)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(ts.chainID), ts.privateKey)
if err != nil {
return nil, fmt.Errorf("failed to sign transaction: %w", err)
}
err = ts.client.SendTransaction(ctx, signedTx)
if err != nil {
return nil, fmt.Errorf("failed to send transaction: %w", err)
}
return signedTx, nil
}
func (ts *TransactionService) WaitForReceipt(
ctx context.Context,
txHash common.Hash,
timeout time.Duration,
) (*types.Receipt, error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return nil, fmt.Errorf("timeout waiting for receipt")
case <-ticker.C:
receipt, err := ts.client.TransactionReceipt(ctx, txHash)
if err == nil {
return receipt, nil
}
}
}
}
์ค์ ์๋น์ค์์๋ ํธ๋์ญ์ ์ฌ์๋ ๋ก์ง, ๊ฐ์ค ๊ฐ๊ฒฉ ์ต์ ํ, ๋ ผ์ค ๊ด๋ฆฌ ๋ฑ์ ๋ ์ ๊ตํ๊ฒ ๊ตฌํํด์ผ ํด. ํนํ ๋์ ํธ๋ํฝ ํ๊ฒฝ์์๋ ๋ ผ์ค ์ถฉ๋์ ๋ฐฉ์งํ๋ ๊ฒ ์ค์ํ์ง!
๐๏ธ ๋ฐ์ดํฐ ์ธ๋ฑ์ฑ๊ณผ ์บ์ฑ
๋ธ๋ก์ฒด์ธ์์ ์ง์ ๋ฐ์ดํฐ๋ฅผ ์ฝ๋ ๊ฑด ๋๋ฆฌ๊ณ ๋นํจ์จ์ ์ด์ผ. ๊ทธ๋์ ์ค์ํ ๋ฐ์ดํฐ๋ฅผ ๋ก์ปฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ธ๋ฑ์ฑํ๊ณ ์บ์ฑํ๋ ๊ฒ ํ์์ผ! ๐package repository
import (
"context"
"database/sql"
"time"
_ "github.com/lib/pq"
)
type Transaction struct {
ID int64
TxHash string
From string
To string
Value string
BlockNumber int64
Timestamp time.Time
Status int
}
type TransactionRepository struct {
db *sql.DB
}
func NewTransactionRepository(db *sql.DB) *TransactionRepository {
return &TransactionRepository{db: db}
}
func (r *TransactionRepository) Create(ctx context.Context, tx *Transaction) error {
query := `
INSERT INTO transactions (tx_hash, from_address, to_address, value, block_number, timestamp, status)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id
`
return r.db.QueryRowContext(
ctx, query,
tx.TxHash, tx.From, tx.To, tx.Value, tx.BlockNumber, tx.Timestamp, tx.Status,
).Scan(&tx.ID)
}
func (r *TransactionRepository) GetByAddress(ctx context.Context, address string, limit int) ([]Transaction, error) {
query := `
SELECT id, tx_hash, from_address, to_address, value, block_number, timestamp, status
FROM transactions
WHERE from_address = $1 OR to_address = $1
ORDER BY block_number DESC
LIMIT $2
`
rows, err := r.db.QueryContext(ctx, query, address, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var transactions []Transaction
for rows.Next() {
var tx Transaction
err := rows.Scan(
&tx.ID, &tx.TxHash, &tx.From, &tx.To,
&tx.Value, &tx.BlockNumber, &tx.Timestamp, &tx.Status,
)
if err != nil {
return nil, err
}
transactions = append(transactions, tx)
}
return transactions, nil
}
package repository
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/go-redis/redis/v8"
)
type CacheRepository struct {
client *redis.Client
}
func NewCacheRepository(client *redis.Client) *CacheRepository {
return &CacheRepository{client: client}
}
func (r *CacheRepository) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
data, err := json.Marshal(value)
if err != nil {
return err
}
return r.client.Set(ctx, key, data, expiration).Err()
}
func (r *CacheRepository) Get(ctx context.Context, key string, dest interface{}) error {
data, err := r.client.Get(ctx, key).Bytes()
if err != nil {
return err
}
return json.Unmarshal(data, dest)
}
func (r *CacheRepository) GetBalance(ctx context.Context, address string) (string, error) {
key := fmt.Sprintf("balance:%s", address)
return r.client.Get(ctx, key).Result()
}
func (r *CacheRepository) SetBalance(ctx context.Context, address, balance string) error {
key := fmt.Sprintf("balance:%s", address)
return r.client.Set(ctx, key, balance, 5*time.Minute).Err()
}
package blockchain
import (
"context"
"log"
"math/big"
"time"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
)
type EventIndexer struct {
client *ethclient.Client
repository *TransactionRepository
cache *CacheRepository
}
func NewEventIndexer(
client *ethclient.Client,
repo *TransactionRepository,
cache *CacheRepository,
) *EventIndexer {
return &EventIndexer{
client: client,
repository: repo,
cache: cache,
}
}
func (ei *EventIndexer) Start(ctx context.Context, contractAddress common.Address) error {
query := ethereum.FilterQuery{
Addresses: []common.Address{contractAddress},
}
logs := make(chan types.Log)
sub, err := ei.client.SubscribeFilterLogs(ctx, query, logs)
if err != nil {
return err
}
log.Println("์ด๋ฒคํธ ์ธ๋ฑ์ ์์...")
for {
select {
case err := <-sub.Err():
log.Printf("๊ตฌ๋
์๋ฌ: %v", err)
return err
case vLog := <-logs:
if err := ei.processLog(ctx, vLog); err != nil {
log.Printf("๋ก๊ทธ ์ฒ๋ฆฌ ์๋ฌ: %v", err)
}
case <-ctx.Done():
log.Println("์ธ๋ฑ์ ์ข
๋ฃ...")
return ctx.Err()
}
}
}
func (ei *EventIndexer) processLog(ctx context.Context, vLog types.Log) error {
// ํธ๋์ญ์
์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
tx, _, err := ei.client.TransactionByHash(ctx, vLog.TxHash)
if err != nil {
return err
}
// ๋ธ๋ก ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
block, err := ei.client.BlockByNumber(ctx, big.NewInt(int64(vLog.BlockNumber)))
if err != nil {
return err
}
// ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ
transaction := &Transaction{
TxHash: vLog.TxHash.Hex(),
From: tx.From().Hex(),
To: tx.To().Hex(),
Value: tx.Value().String(),
BlockNumber: int64(vLog.BlockNumber),
Timestamp: time.Unix(int64(block.Time()), 0),
Status: 1,
}
if err := ei.repository.Create(ctx, transaction); err != nil {
return err
}
// ์บ์ ๋ฌดํจํ
ei.cache.Delete(ctx, fmt.Sprintf("balance:%s", transaction.From))
ei.cache.Delete(ctx, fmt.Sprintf("balance:%s", transaction.To))
log.Printf("ํธ๋์ญ์
์ธ๋ฑ์ฑ ์๋ฃ: %s", transaction.TxHash)
return nil
}
๐ RESTful API ๊ตฌํ
์ด์ ํ๋ก ํธ์๋๊ฐ ์ฌ์ฉํ ์ ์๋ REST API๋ฅผ ๋ง๋ค์ด๋ณด์! Gin ํ๋ ์์ํฌ๋ฅผ ์ฌ์ฉํ ๊ฑฐ์ผ. ๊ฐ๋ณ๊ณ ๋น ๋ฅด๊ฑฐ๋ ! ๐package handlers
import (
"net/http"
"github.com/ethereum/go-ethereum/common"
"github.com/gin-gonic/gin"
)
type TokenHandler struct {
tokenService *TokenService
}
func NewTokenHandler(tokenService *TokenService) *TokenHandler {
return &TokenHandler{tokenService: tokenService}
}
// GET /api/tokens/:address/balance
func (h *TokenHandler) GetBalance(c *gin.Context) {
address := c.Param("address")
if !common.IsHexAddress(address) {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid address format",
})
return
}
balance, err := h.tokenService.GetBalance(c.Request.Context(), address)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"address": address,
"balance": balance,
})
}
// POST /api/tokens/transfer
func (h *TokenHandler) Transfer(c *gin.Context) {
var req struct {
From string `json:"from" binding:"required"`
To string `json:"to" binding:"required"`
Amount string `json:"amount" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
txHash, err := h.tokenService.Transfer(
c.Request.Context(),
req.From,
req.To,
req.Amount,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"tx_hash": txHash,
"status": "pending",
})
}
// GET /api/transactions/:address
func (h *TokenHandler) GetTransactions(c *gin.Context) {
address := c.Param("address")
transactions, err := h.tokenService.GetTransactionHistory(
c.Request.Context(),
address,
50, // limit
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"address": address,
"transactions": transactions,
})
}
package api
import (
"github.com/gin-gonic/gin"
"github.com/yourusername/dapp-backend/internal/api/handlers"
"github.com/yourusername/dapp-backend/internal/api/middleware"
)
func SetupRouter(
tokenHandler *handlers.TokenHandler,
userHandler *handlers.UserHandler,
) *gin.Engine {
router := gin.Default()
// ๋ฏธ๋ค์จ์ด ์ค์
router.Use(middleware.CORS())
router.Use(middleware.RateLimiter())
router.Use(middleware.Logger())
// Health check
router.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
// API v1 ๊ทธ๋ฃน
v1 := router.Group("/api/v1")
{
// ํ ํฐ ๊ด๋ จ ์๋ํฌ์ธํธ
tokens := v1.Group("/tokens")
{
tokens.GET("/:address/balance", tokenHandler.GetBalance)
tokens.POST("/transfer", tokenHandler.Transfer)
tokens.GET("/transactions/:address", tokenHandler.GetTransactions)
}
// ์ฌ์ฉ์ ๊ด๋ จ ์๋ํฌ์ธํธ
users := v1.Group("/users")
{
users.POST("/register", userHandler.Register)
users.GET("/:address", userHandler.GetProfile)
users.PUT("/:address", userHandler.UpdateProfile)
}
}
return router
}
package middleware
import (
"time"
"github.com/gin-gonic/gin"
"golang.org/x/time/rate"
)
// CORS ๋ฏธ๋ค์จ์ด
func CORS() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
// Rate Limiter ๋ฏธ๋ค์จ์ด
func RateLimiter() gin.HandlerFunc {
limiter := rate.NewLimiter(rate.Every(time.Second), 10) // ์ด๋น 10๊ฐ ์์ฒญ
return func(c *gin.Context) {
if !limiter.Allow() {
c.JSON(429, gin.H{
"error": "Too many requests",
})
c.Abort()
return
}
c.Next()
}
}
// ๋ก๊ฑฐ ๋ฏธ๋ค์จ์ด
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
c.Next()
latency := time.Since(start)
statusCode := c.Writer.Status()
log.Printf("[%s] %s - %d (%v)",
c.Request.Method,
path,
statusCode,
latency,
)
}
}
๐ ๋ณด์ ๊ณ ๋ ค์ฌํญ
DApp ๋ฐฑ์๋๋ฅผ ๊ฐ๋ฐํ ๋ ๋ณด์์ ์ ๋ง์ ๋ง ์ค์ํด! ํ ๋ฒ ์ค์ํ๋ฉด ํฐ ๊ธ์ ์ ์์ค๋ก ์ด์ด์ง ์ ์๊ฑฐ๋ . ๐ฐ1. ๊ฐ์ธํค๋ฅผ ์ฝ๋์ ํ๋์ฝ๋ฉ
2. ๊ฐ์ธํค๋ฅผ Git์ ์ปค๋ฐ
3. ๋ก๊ทธ์ ๋ฏผ๊ฐํ ์ ๋ณด ์ถ๋ ฅ
4. ๊ฒ์ฆ๋์ง ์์ ์ฌ์ฉ์ ์ ๋ ฅ ์ฌ์ฉ
5. ๊ฐ์ค ๋ฆฌ๋ฏธํธ๋ฅผ ๋ฌดํ๋๋ก ์ค์
package config
import (
"os"
"github.com/joho/godotenv"
)
type Config struct {
PrivateKey string
InfuraURL string
ContractAddress string
DatabaseURL string
RedisURL string
}
func Load() (*Config, error) {
if err := godotenv.Load(); err != nil {
return nil, err
}
return &Config{
PrivateKey: os.Getenv("PRIVATE_KEY"),
InfuraURL: os.Getenv("INFURA_URL"),
ContractAddress: os.Getenv("CONTRACT_ADDRESS"),
DatabaseURL: os.Getenv("DATABASE_URL"),
RedisURL: os.Getenv("REDIS_URL"),
}, nil
}
2. AWS Secrets Manager ๋๋ HashiCorp Vault ์ฌ์ฉ
ํ๋ก๋์ ํ๊ฒฝ์์๋ ์ ๋ฌธ์ ์ธ ๋น๋ฐ ๊ด๋ฆฌ ์๋น์ค๋ฅผ ์ฌ์ฉํ๋ ๊ฒ ์ข์:
package secrets
import (
"context"
"encoding/json"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
)
type SecretsManager struct {
client *secretsmanager.Client
}
func NewSecretsManager(cfg aws.Config) *SecretsManager {
return &SecretsManager{
client: secretsmanager.NewFromConfig(cfg),
}
}
func (sm *SecretsManager) GetSecret(ctx context.Context, secretName string) (map[string]string, error) {
result, err := sm.client.GetSecretValue(ctx, &secretsmanager.GetSecretValueInput{
SecretId: aws.String(secretName),
})
if err != nil {
return nil, err
}
var secrets map[string]string
if err := json.Unmarshal([]byte(*result.SecretString), &secrets); err != nil {
return nil, err
}
return secrets, nil
}
package validator
import (
"errors"
"math/big"
"regexp"
"github.com/ethereum/go-ethereum/common"
)
var (
ErrInvalidAddress = errors.New("invalid ethereum address")
ErrInvalidAmount = errors.New("invalid amount")
)
func ValidateAddress(address string) error {
if !common.IsHexAddress(address) {
return ErrInvalidAddress
}
return nil
}
func ValidateAmount(amount string) (*big.Int, error) {
value, ok := new(big.Int).SetString(amount, 10)
if !ok {
return nil, ErrInvalidAmount
}
if value.Sign() <= 0 {
return nil, errors.New("amount must be positive")
}
// ์ต๋๊ฐ ์ฒดํฌ (์: 1์ต ํ ํฐ)
maxValue := new(big.Int).Mul(big.NewInt(100000000), big.NewInt(1e18))
if value.Cmp(maxValue) > 0 {
return nil, errors.New("amount too large")
}
return value, nil
}
func ValidateTxHash(txHash string) error {
matched, err := regexp.MatchString("^0x[0-9a-fA-F]{64}$", txHash)
if err != nil || !matched {
return errors.New("invalid transaction hash")
}
return nil
}
package blockchain
import (
"context"
"sync"
"github.com/ethereum/go-ethereum/common"
)
type NonceManager struct {
nonces map[common.Address]uint64
mu sync.Mutex
}
func NewNonceManager() *NonceManager {
return &NonceManager{
nonces: make(map[common.Address]uint64),
}
}
func (nm *NonceManager) GetNonce(ctx context.Context, client *ethclient.Client, address common.Address) (uint64, error) {
nm.mu.Lock()
defer nm.mu.Unlock()
// ์บ์๋ ๋
ผ์ค ํ์ธ
if nonce, exists := nm.nonces[address]; exists {
nm.nonces[address]++
return nonce, nil
}
// ๋ธ๋ก์ฒด์ธ์์ ๋
ผ์ค ๊ฐ์ ธ์ค๊ธฐ
nonce, err := client.PendingNonceAt(ctx, address)
if err != nil {
return 0, err
}
nm.nonces[address] = nonce + 1
return nonce, nil
}
func (nm *NonceManager) Reset(address common.Address) {
nm.mu.Lock()
defer nm.mu.Unlock()
delete(nm.nonces, address)
}
๐ ๋ชจ๋ํฐ๋ง๊ณผ ๋ก๊น
ํ๋ก๋์ ํ๊ฒฝ์์๋ ์์คํ ์ ์ค์๊ฐ์ผ๋ก ๋ชจ๋ํฐ๋งํ๊ณ ๋ฌธ์ ๋ฅผ ๋น ๋ฅด๊ฒ ํ์ ํ ์ ์์ด์ผ ํด! ๐package logger
import (
"os"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var Log *zap.Logger
func Init() error {
config := zap.NewProductionConfig()
config.EncoderConfig.TimeKey = "timestamp"
config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
var err error
Log, err = config.Build()
if err != nil {
return err
}
return nil
}
func Info(msg string, fields ...zap.Field) {
Log.Info(msg, fields...)
}
func Error(msg string, fields ...zap.Field) {
Log.Error(msg, fields...)
}
func Debug(msg string, fields ...zap.Field) {
Log.Debug(msg, fields...)
}
// ์ฌ์ฉ ์์
func ExampleUsage() {
logger.Info("Transaction sent",
zap.String("tx_hash", "0x..."),
zap.String("from", "0x..."),
zap.String("to", "0x..."),
zap.String("value", "1000000000000000000"),
)
}
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
TransactionCounter = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "dapp_transactions_total",
Help: "Total number of transactions",
},
[]string{"status"},
)
TransactionDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "dapp_transaction_duration_seconds",
Help: "Transaction processing duration",
Buckets: prometheus.DefBuckets,
},
[]string{"method"},
)
BlockHeight = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "dapp_block_height",
Help: "Current block height",
},
)
GasPrice = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "dapp_gas_price_gwei",
Help: "Current gas price in Gwei",
},
)
)
// ์ฌ์ฉ ์์
func RecordTransaction(status string) {
TransactionCounter.WithLabelValues(status).Inc()
}
func RecordDuration(method string, duration float64) {
TransactionDuration.WithLabelValues(method).Observe(duration)
}
package handlers
import (
"context"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
type HealthHandler struct {
client *ethclient.Client
db *sql.DB
redis *redis.Client
}
func (h *HealthHandler) Check(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)
defer cancel()
health := gin.H{
"status": "ok",
"checks": gin.H{},
}
// ๋ธ๋ก์ฒด์ธ ์ฐ๊ฒฐ ์ฒดํฌ
if _, err := h.client.ChainID(ctx); err != nil {
health["checks"].(gin.H)["blockchain"] = "unhealthy"
health["status"] = "degraded"
} else {
health["checks"].(gin.H)["blockchain"] = "healthy"
}
// ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฒดํฌ
if err := h.db.PingContext(ctx); err != nil {
health["checks"].(gin.H)["database"] = "unhealthy"
health["status"] = "degraded"
} else {
health["checks"].(gin.H)["database"] = "healthy"
}
// Redis ์ฒดํฌ
if err := h.redis.Ping(ctx).Err(); err != nil {
health["checks"].(gin.H)["cache"] = "unhealthy"
health["status"] = "degraded"
} else {
health["checks"].(gin.H)["cache"] = "healthy"
}
statusCode := http.StatusOK
if health["status"] == "degraded" {
statusCode = http.StatusServiceUnavailable
}
c.JSON(statusCode, health)
}
๐ ๋ฐฐํฌ์ ์ด์
๊ฐ๋ฐ์ด ๋๋ฌ์ผ๋ฉด ์ด์ ๋ฐฐํฌํ ์ฐจ๋ก์ผ! Docker์ Kubernetes๋ฅผ ์ฌ์ฉํด์ ํ๋ก๋์ ํ๊ฒฝ์ ๋ฐฐํฌํด๋ณด์. ๐# ๋น๋ ์คํ
์ด์ง
FROM golang:1.21-alpine AS builder
WORKDIR /app
# ์์กด์ฑ ๋ณต์ฌ ๋ฐ ๋ค์ด๋ก๋
COPY go.mod go.sum ./
RUN go mod download
# ์์ค ์ฝ๋ ๋ณต์ฌ
COPY . .
# ๋น๋
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main ./cmd/server
# ์คํ ์คํ
์ด์ง
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
# ๋น๋๋ ๋ฐ์ด๋๋ฆฌ ๋ณต์ฌ
COPY --from=builder /app/main .
COPY --from=builder /app/.env .
EXPOSE 8080
CMD ["./main"]
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- DATABASE_URL=postgres://user:password@postgres:5432/dapp
- REDIS_URL=redis://redis:6379
depends_on:
- postgres
- redis
networks:
- dapp-network
postgres:
image: postgres:15-alpine
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
- POSTGRES_DB=dapp
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- dapp-network
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
networks:
- dapp-network
prometheus:
image: prom/prometheus:latest
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
ports:
- "9090:9090"
networks:
- dapp-network
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana-data:/var/lib/grafana
networks:
- dapp-network
volumes:
postgres-data:
redis-data:
prometheus-data:
grafana-data:
networks:
dapp-network:
driver: bridge
apiVersion: apps/v1
kind: Deployment
metadata:
name: dapp-backend
labels:
app: dapp-backend
spec:
replicas: 3
selector:
matchLabels:
app: dapp-backend
template:
metadata:
labels:
app: dapp-backend
spec:
containers:
- name: dapp-backend
image: yourusername/dapp-backend:latest
ports:
- containerPort: 8080
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: dapp-secrets
key: database-url
- name: PRIVATE_KEY
valueFrom:
secretKeyRef:
name: dapp-secrets
key: private-key
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: dapp-backend-service
spec:
selector:
app: dapp-backend
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: LoadBalancer
- โ ํ๊ฒฝ๋ณ์ ์ค์ ํ์ธ
- โ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ง์ด๊ทธ๋ ์ด์ ์คํ
- โ SSL/TLS ์ธ์ฆ์ ์ค์
- โ ๋ก๊น ๋ฐ ๋ชจ๋ํฐ๋ง ์ค์
- โ ๋ฐฑ์ ์ ๋ต ์๋ฆฝ
- โ ๋กค๋ฐฑ ๊ณํ ์ค๋น
- โ ๋ถํ ํ ์คํธ ์ํ
๐จ ์ค์ ํ๋ก์ ํธ: NFT ๋ง์ผํ๋ ์ด์ค ๋ฐฑ์๋
์ด๋ก ๋ง ๋ฐฐ์ฐ๋ฉด ์ฌ๋ฏธ์์์? ์ค์ ๋ก NFT ๋ง์ผํ๋ ์ด์ค ๋ฐฑ์๋๋ฅผ ๋ง๋ค์ด๋ณด์! ์ด๊ฑด ์ ๋ง ์ธ๊ธฐ ์๋ DApp ์ ํ์ด์ผ. ๐ผ๏ธ// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract NFTMarketplace is ERC721URIStorage, Ownable {
uint256 private _tokenIds;
uint256 private _itemsSold;
uint256 listingPrice = 0.025 ether;
struct MarketItem {
uint256 tokenId;
address payable seller;
address payable owner;
uint256 price;
bool sold;
}
mapping(uint256 => MarketItem) private idToMarketItem;
event MarketItemCreated(
uint256 indexed tokenId,
address seller,
address owner,
uint256 price,
bool sold
);
constructor() ERC721("NFT Marketplace", "NFTM") {}
function createToken(string memory tokenURI, uint256 price) public payable returns (uint256) {
_tokenIds++;
uint256 newTokenId = _tokenIds;
_mint(msg.sender, newTokenId);
_setTokenURI(newTokenId, tokenURI);
createMarketItem(newTokenId, price);
return newTokenId;
}
function createMarketItem(uint256 tokenId, uint256 price) private {
require(price > 0, "Price must be at least 1 wei");
require(msg.value == listingPrice, "Must pay listing price");
idToMarketItem[tokenId] = MarketItem(
tokenId,
payable(msg.sender),
payable(address(this)),
price,
false
);
_transfer(msg.sender, address(this), tokenId);
emit MarketItemCreated(
tokenId,
msg.sender,
address(this),
price,
false
);
}
function createMarketSale(uint256 tokenId) public payable {
uint256 price = idToMarketItem[tokenId].price;
address seller = idToMarketItem[tokenId].seller;
require(msg.value == price, "Please submit the asking price");
idToMarketItem[tokenId].owner = payable(msg.sender);
idToMarketItem[tokenId].sold = true;
idToMarketItem[tokenId].seller = payable(address(0));
_itemsSold++;
_transfer(address(this), msg.sender, tokenId);
payable(owner()).transfer(listingPrice);
payable(seller).transfer(msg.value);
}
}
package service
import (
"context"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
)
type NFTService struct {
contract *nft.NFTMarketplace
client *ethclient.Client
}
func NewNFTService(contractAddress common.Address, client *ethclient.Client) (*NFTService, error) {
contract, err := nft.NewNFTMarketplace(contractAddress, client)
if err != nil {
return nil, err
}
return &NFTService{
contract: contract,
client: client,
}, nil
}
// NFT ๋ฏผํ
func (s *NFTService) MintNFT(
ctx context.Context,
auth *bind.TransactOpts,
tokenURI string,
price *big.Int,
) (uint64, error) {
tx, err := s.contract.CreateToken(auth, tokenURI, price)
if err != nil {
return 0, fmt.Errorf("failed to mint NFT: %w", err)
}
receipt, err := bind.WaitMined(ctx, s.client, tx)
if err != nil {
return 0, fmt.Errorf("failed to wait for transaction: %w", err)
}
// ์ด๋ฒคํธ์์ ํ ํฐ ID ์ถ์ถ
for _, log := range receipt.Logs {
event, err := s.contract.ParseMarketItemCreated(*log)
if err == nil {
return event.TokenId.Uint64(), nil
}
}
return 0, fmt.Errorf("token ID not found in events")
}
// NFT ๊ตฌ๋งค
func (s *NFTService) BuyNFT(
ctx context.Context,
auth *bind.TransactOpts,
tokenId *big.Int,
) error {
// ๊ฐ๊ฒฉ ์กฐํ
item, err := s.contract.IdToMarketItem(nil, tokenId)
if err != nil {
return fmt.Errorf("failed to get market item: %w", err)
}
// ๊ฐ๊ฒฉ ์ค์
auth.Value = item.Price
tx, err := s.contract.CreateMarketSale(auth, tokenId)
if err != nil {
return fmt.Errorf("failed to buy NFT: %w", err)
}
_, err = bind.WaitMined(ctx, s.client, tx)
if err != nil {
return fmt.Errorf("failed to wait for transaction: %w", err)
}
return nil
}
// ๋ง์ผ ์์ดํ
์กฐํ
func (s *NFTService) GetMarketItems(ctx context.Context) ([]MarketItem, error) {
// ์ด๋ฒคํธ ํํฐ๋ง์ผ๋ก ๋ชจ๋ ๋ง์ผ ์์ดํ
์กฐํ
// ์ค์ ๋ก๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์กฐํํ๋ ๊ฒ ๋ ํจ์จ์
// ... ๊ตฌํ
return items, nil
}
package ipfs
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
)
type IPFSClient struct {
apiURL string
apiKey string
}
type NFTMetadata struct {
Name string `json:"name"`
Description string `json:"description"`
Image string `json:"image"`
Attributes []NFTAttribute `json:"attributes"`
}
type NFTAttribute struct {
TraitType string `json:"trait_type"`
Value string `json:"value"`
}
func NewIPFSClient(apiURL, apiKey string) *IPFSClient {
return &IPFSClient{
apiURL: apiURL,
apiKey: apiKey,
}
}
// ํ์ผ ์
๋ก๋
func (c *IPFSClient) UploadFile(file multipart.File, filename string) (string, error) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("file", filename)
if err != nil {
return "", err
}
_, err = io.Copy(part, file)
if err != nil {
return "", err
}
writer.Close()
req, err := http.NewRequest("POST", c.apiURL+"/api/v0/add", body)
if err != nil {
return "", err
}
req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Set("Authorization", "Bearer "+c.apiKey)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
var result struct {
Hash string `json:"Hash"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", err
}
return fmt.Sprintf("ipfs://%s", result.Hash), nil
}
// ๋ฉํ๋ฐ์ดํฐ ์
๋ก๋
func (c *IPFSClient) UploadMetadata(metadata *NFTMetadata) (string, error) {
data, err := json.Marshal(metadata)
if err != nil {
return "", err
}
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("file", "metadata.json")
if err != nil {
return "", err
}
part.Write(data)
writer.Close()
req, err := http.NewRequest("POST", c.apiURL+"/api/v0/add", body)
if err != nil {
return "", err
}
req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Set("Authorization", "Bearer "+c.apiKey)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
var result struct {
Hash string `json:"Hash"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", err
}
return fmt.Sprintf("ipfs://%s", result.Hash), nil
}
โก ์ฑ๋ฅ ์ต์ ํ ํ
DApp ๋ฐฑ์๋์ ์ฑ๋ฅ์ ์ต๋ํ ๋์ด์ฌ๋ฆฌ๋ ๋ฐฉ๋ฒ๋ค์ ์๋ ค์ค๊ฒ! ๐package blockchain
import (
"context"
"sync"
"github.com/ethereum/go-ethereum/common"
)
type BatchBalanceChecker struct {
client *ethclient.Client
}
func (b *BatchBalanceChecker) GetBalances(
ctx context.Context,
addresses []common.Address,
) (map[common.Address]*big.Int, error) {
results := make(map[common.Address]*big.Int)
var mu sync.Mutex
var wg sync.WaitGroup
// ๋์์ 10๊ฐ์ฉ ์ฒ๋ฆฌ
semaphore := make(chan struct{}, 10)
for _, addr := range addresses {
wg.Add(1)
go func(address common.Address) {
defer wg.Done()
semaphore <- struct{}{}
defer func() { <-semaphore }()
balance, err := b.client.BalanceAt(ctx, address, nil)
if err != nil {
return
}
mu.Lock()
results[address] = balance
mu.Unlock()
}(addr)
}
wg.Wait()
return results, nil
}
package blockchain
import (
"sync"
"github.com/ethereum/go-ethereum/ethclient"
)
type ClientPool struct {
clients []*ethclient.Client
current int
mu sync.Mutex
}
func NewClientPool(urls []string) (*ClientPool, error) {
pool := &ClientPool{
clients: make([]*ethclient.Client, len(urls)),
}
for i, url := range urls {
client, err := ethclient.Dial(url)
if err != nil {
return nil, err
}
pool.clients[i] = client
}
return pool, nil
}
func (p *ClientPool) GetClient() *ethclient.Client {
p.mu.Lock()
defer p.mu.Unlock()
client := p.clients[p.current]
p.current = (p.current + 1) % len(p.clients)
return client
}
package cache
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/go-redis/redis/v8"
)
type SmartCache struct {
redis *redis.Client
}
func NewSmartCache(redis *redis.Client) *SmartCache {
return &SmartCache{redis: redis}
}
// ๊ณ์ธต์ ์บ์ฑ
func (c *SmartCache) GetOrSet(
ctx context.Context,
key string,
ttl time.Duration,
fetchFunc func() (interface{}, error),
) (interface{}, error) {
// 1๋จ๊ณ: ์บ์์์ ์กฐํ
cached, err := c.redis.Get(ctx, key).Bytes()
if err == nil {
var result interface{}
if err := json.Unmarshal(cached, &result); err == nil {
return result, nil
}
}
// 2๋จ๊ณ: ์๋ณธ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
data, err := fetchFunc()
if err != nil {
return nil, err
}
// 3๋จ๊ณ: ์บ์์ ์ ์ฅ
jsonData, err := json.Marshal(data)
if err == nil {
c.redis.Set(ctx, key, jsonData, ttl)
}
return data, nil
}
// ๋ธ๋ก ๋ฒํธ ๊ธฐ๋ฐ ์บ์ฑ (๋ถ๋ณ ๋ฐ์ดํฐ)
func (c *SmartCache) CacheByBlock(
ctx context.Context,
blockNumber uint64,
key string,
data interface{},
) error {
cacheKey := fmt.Sprintf("block:%d:%s", blockNumber, key)
jsonData, err := json.Marshal(data)
if err != nil {
return err
}
// ํ์ ๋ ๋ธ๋ก์ ์๊ตฌ ์บ์ฑ
return c.redis.Set(ctx, cacheKey, jsonData, 0).Err()
}
๐ง ๋ฌธ์ ํด๊ฒฐ ๊ฐ์ด๋
๊ฐ๋ฐํ๋ค ๋ณด๋ฉด ์ฌ๋ฌ ๋ฌธ์ ์ ๋ถ๋ชํ๊ฒ ๋ผ. ์์ฃผ ๋ฐ์ํ๋ ๋ฌธ์ ๋ค๊ณผ ํด๊ฒฐ ๋ฐฉ๋ฒ์ ์ ๋ฆฌํด๋ดค์ด! ๐ ๏ธ์์ธ: ๊ณ์ ์ ๊ฐ์ค๋น๋ฅผ ์ง๋ถํ ETH๊ฐ ๋ถ์กฑํด.
ํด๊ฒฐ:
โข ํ ์คํธ๋ท faucet์์ ETH ๋ฐ๊ธฐ
โข ๊ฐ์ค ๊ฐ๊ฒฉ ๋ฎ์ถ๊ธฐ (ํ ์คํธ ํ๊ฒฝ์์๋ง)
โข ๊ฐ์ค ๋ฆฌ๋ฏธํธ ์กฐ์ ํ๊ธฐ
์์ธ: ์ด๋ฏธ ์ฌ์ฉ๋ nonce๋ฅผ ์ฌ์ฌ์ฉํ๋ ค๊ณ ํจ.
ํด๊ฒฐ:
โข NonceManager ์ฌ์ฉํด์ nonce ๊ด๋ฆฌ
โข PendingNonceAt์ผ๋ก ์ต์ nonce ๊ฐ์ ธ์ค๊ธฐ
โข ํธ๋์ญ์ ์คํจ ์ nonce ๋ฆฌ์
์์ธ: ์ค๋งํธ ์ปจํธ๋ํธ ์คํ ์ค ์๋ฌ ๋ฐ์.
ํด๊ฒฐ:
โข Remix์์ ์ปจํธ๋ํธ ํ ์คํธ
โข require ์กฐ๊ฑด ํ์ธ
โข ์ด๋ฒคํธ ๋ก๊ทธ ๋ถ์
โข Tenderly ๊ฐ์ ๋๋ฒ๊น ๋๊ตฌ ์ฌ์ฉ
์์ธ: ์ด๋๋ฆฌ์ ๋ ธ๋์ ์ฐ๊ฒฐํ ์ ์์.
ํด๊ฒฐ:
โข ๋ ธ๋ URL ํ์ธ (http vs ws)
โข Infura/Alchemy API ํค ํ์ธ
โข ๋ฐฉํ๋ฒฝ ์ค์ ํ์ธ
โข ๋คํธ์ํฌ ์ฐ๊ฒฐ ์ํ ํ์ธ
๐ ์ถ๊ฐ ํ์ต ๋ฆฌ์์ค
๋ ๊น์ด ๊ณต๋ถํ๊ณ ์ถ๋ค๋ฉด ์ด๋ฐ ์๋ฃ๋ค์ ์ถ์ฒํด! ๐๊ณต์ ๋ฌธ์:
โข Go-Ethereum (Geth) ๋ฌธ์
โข Solidity ๊ณต์ ๋ฌธ์
โข OpenZeppelin ์ปจํธ๋ํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
โข Ethereum.org ๊ฐ๋ฐ์ ๊ฐ์ด๋
์จ๋ผ์ธ ๊ฐ์:
โข Udemy - Ethereum and Solidity: The Complete Developer's Guide
โข Coursera - Blockchain Specialization
โข YouTube - Dapp University, Eat The Blocks
์ปค๋ฎค๋ํฐ:
โข Ethereum Stack Exchange
โข Reddit r/ethdev
โข Discord - Ethereum, OpenZeppelin
โข GitHub - awesome-ethereum
๋๊ตฌ:
โข Hardhat - ์ค๋งํธ ์ปจํธ๋ํธ ๊ฐ๋ฐ ํ๊ฒฝ
โข Truffle Suite - ๊ฐ๋ฐ ํ๋ ์์ํฌ
โข Tenderly - ๋๋ฒ๊น ๋ฐ ๋ชจ๋ํฐ๋ง
โข Etherscan - ๋ธ๋ก์ฒด์ธ ํ์๊ธฐ
์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์๋ ๋ธ๋ก์ฒด์ธ ๊ฐ๋ฐ ๊ด๋ จ ๋ฉํ ๋ง์ด๋ ๊ฐ์๋ฅผ ์ฐพ์๋ณผ ์ ์์ด. ์ค๋ฌด ๊ฒฝํ์ด ์๋ ๊ฐ๋ฐ์์๊ฒ ์ง์ ๋ฐฐ์ฐ๋ ๊ฒ๋ ์ ๋ง ์ข์ ๋ฐฉ๋ฒ์ด์ผ! ๐ก
๐ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์! ์ ๋ง ๊ธด ์ฌ์ ์ด์์ง? ๐ Go์ธ์ด๋ก ๋ธ๋ก์ฒด์ธ DApp ๋ฐฑ์๋๋ฅผ ๋ง๋๋ ์ ๊ณผ์ ์ ํจ๊ป ์ดํด๋ดค์ด.1๏ธโฃ ๊ธฐ์ด ๋ค์ง๊ธฐ
โข Go์ธ์ด์ ๊ฐ๋ ฅํ ๋์์ฑ๊ณผ ์ฑ๋ฅ
โข go-ethereum ๋ผ์ด๋ธ๋ฌ๋ฆฌ ํ์ฉ
โข ์ด๋๋ฆฌ์ ํด๋ผ์ด์ธํธ ์ฐ๊ฒฐ
2๏ธโฃ ์ค๋งํธ ์ปจํธ๋ํธ ํต์
โข ABI ๋ฐ์ธ๋ฉ ์์ฑ
โข ์ฝ๊ธฐ/์ฐ๊ธฐ ํจ์ ํธ์ถ
โข ์ด๋ฒคํธ ๋ฆฌ์ค๋
3๏ธโฃ ๋ฐฑ์๋ ์ํคํ ์ฒ
โข ๋ ์ด์ด๋ ๊ตฌ์กฐ ์ค๊ณ
โข ๋ฐ์ดํฐ ์ธ๋ฑ์ฑ๊ณผ ์บ์ฑ
โข RESTful API ๊ตฌํ
4๏ธโฃ ๋ณด์๊ณผ ์ต์ ํ
โข ๊ฐ์ธํค ์์ ๊ด๋ฆฌ
โข ์ ๋ ฅ ๊ฒ์ฆ
โข ์ฑ๋ฅ ์ต์ ํ
5๏ธโฃ ๋ฐฐํฌ์ ์ด์
โข Docker ์ปจํ ์ด๋ํ
โข Kubernetes ์ค์ผ์คํธ๋ ์ด์
โข ๋ชจ๋ํฐ๋ง๊ณผ ๋ก๊น
๋ธ๋ก์ฒด์ธ ๊ฐ๋ฐ์ ๊ณ์ ์งํํ๊ณ ์์ด. ์๋ก์ด ๊ธฐ์ ๊ณผ ํจํด์ด ๊ณ์ ๋์ค๊ณ ์์ง. ํ์ง๋ง ์ด ๊ธ์์ ๋ฐฐ์ด ๊ธฐ์ด๋ฅผ ํํํ ๋ค์ก๋ค๋ฉด, ์ด๋ค ๋ณํ๊ฐ ์๋ ๋น ๋ฅด๊ฒ ์ ์ํ ์ ์์ ๊ฑฐ์ผ! ๐ช
1. ๊ฐ๋จํ ํ ํฐ DApp ๋ง๋ค์ด๋ณด๊ธฐ
2. NFT ๋ง์ผํ๋ ์ด์ค ๊ตฌํํ๊ธฐ
3. DeFi ํ๋กํ ์ฝ ์ฐ๋ํ๊ธฐ
4. Layer 2 ์๋ฃจ์ ํ๊ตฌํ๊ธฐ
5. ์คํ์์ค ํ๋ก์ ํธ์ ๊ธฐ์ฌํ๊ธฐ
๊ธฐ์ตํด! ์ค์ ๋ก ์ฝ๋๋ฅผ ์์ฑํ๊ณ ๋ฐฐํฌํด๋ณด๋ ๊ฒ ๊ฐ์ฅ ์ค์ํด. ์ด๋ก ๋ง ๊ณต๋ถํ์ง ๋ง๊ณ ์ง์ ๋ง๋ค์ด๋ณด๋ฉด์ ๋ฐฐ์๋๊ฐ์! ๐
ํน์ ๋งํ๋ ๋ถ๋ถ์ด ์๊ฑฐ๋ ๋ ๊ถ๊ธํ ๊ฒ ์๋ค๋ฉด, ์ปค๋ฎค๋ํฐ์ ์ง๋ฌธํ๊ฑฐ๋ ์ฌ๋ฅ๋ท์์ ์ ๋ฌธ๊ฐ์ ๋์์ ๋ฐ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ. ํจ๊ป ์ฑ์ฅํ๋ ๊ฐ๋ฐ์๊ฐ ๋์! ๐ฑ
ํ์ด์ ๋น์ด! ๋ฉ์ง DApp์ ๋ง๋ค์ด๋ณด๊ธธ ๋ฐ๋! ๐โจ
๐ Happy Coding! ๐
Go + Blockchain = Awesome DApps
๊ด๋ จ ํค์๋
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.

๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.