์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿš€ Go์–ธ์–ด๋กœ ๋ธ”๋ก์ฒด์ธ DApp ๋ฐฑ์—”๋“œ ๋งŒ๋“ค๊ธฐ

๐Ÿš€ Go์–ธ์–ด๋กœ ๋ธ”๋ก์ฒด์ธ DApp ๋ฐฑ์—”๋“œ ๋งŒ๋“ค๊ธฐ

์ด๋”๋ฆฌ์›€ ํด๋ผ์ด์–ธํŠธ์™€ ํ•จ๊ป˜ํ•˜๋Š” ํƒˆ์ค‘์•™ํ™” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ฐœ๋ฐœ ์™„์ „์ •๋ณต ๐Ÿ’Ž

๐ŸŽฏ ๋ธ”๋ก์ฒด์ธ ๋ฐฑ์—”๋“œ, ์™œ Go์–ธ์–ด์ผ๊นŒ?

์•ˆ๋…•! ์˜ค๋Š˜์€ ์ •๋ง ํ•ซํ•œ ์ฃผ์ œ๋ฅผ ๊ฐ€์ง€๊ณ  ์™”์–ด. ๋ฐ”๋กœ Go์–ธ์–ด๋กœ ๋ธ”๋ก์ฒด์ธ DApp ๋ฐฑ์—”๋“œ๋ฅผ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์ด์•ผ. ์š”์ฆ˜ Web3, ๋ธ”๋ก์ฒด์ธ, NFT ์ด๋Ÿฐ ๋‹จ์–ด๋“ค ์ •๋ง ๋งŽ์ด ๋“ค๋ฆฌ์ง€? ๊ทผ๋ฐ ๋ง‰์ƒ ๊ฐœ๋ฐœํ•˜๋ ค๊ณ  ํ•˜๋ฉด ์–ด๋””์„œ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์•ผ ํ• ์ง€ ๋ง‰๋ง‰ํ•˜๋”๋ผ๊ณ . ๐Ÿ˜…

ํŠนํžˆ ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์ž๋ผ๋ฉด "ํ”„๋ก ํŠธ์—”๋“œ๋Š” ๊ทธ๋ ‡๋‹ค ์น˜๊ณ , ๋ฐฑ์—”๋“œ๋Š” ์–ด๋–ป๊ฒŒ ๊ตฌ์„ฑํ•˜์ง€?"๋ผ๋Š” ๊ณ ๋ฏผ์ด ํด ๊ฑฐ์•ผ. ์ „ํ†ต์ ์ธ ์ค‘์•™ํ™” ์„œ๋ฒ„์™€๋Š” ์™„์ „ํžˆ ๋‹ค๋ฅธ ๊ตฌ์กฐ๋‹ˆ๊นŒ ๋ง์ด์•ผ.

๊ทธ๋ž˜์„œ ์˜ค๋Š˜์€ Go์–ธ์–ด๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ด๋”๋ฆฌ์›€ ๋ธ”๋ก์ฒด์ธ๊ณผ ํ†ต์‹ ํ•˜๊ณ , ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ์™€ ์ƒํ˜ธ์ž‘์šฉํ•˜๋ฉฐ, ์‹ค์ œ DApp(ํƒˆ์ค‘์•™ํ™” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜)์˜ ๋ฐฑ์—”๋“œ๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ์ „ ๊ณผ์ •์„ ์นœ๊ตฌ์ฒ˜๋Ÿผ ํŽธํ•˜๊ฒŒ ์„ค๋ช…ํ•ด์ค„๊ฒŒ! ๐ŸŽ‰
Go ETH DApp Smart Contract Blockchain Backend API

๐Ÿค” DApp ๋ฐฑ์—”๋“œ๊ฐ€ ๋ญ๊ธธ๋ž˜?

๋จผ์ € ๊ฐœ๋…๋ถ€ํ„ฐ ์ •๋ฆฌํ•˜๊ณ  ๊ฐ€์ž! ์ „ํ†ต์ ์ธ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ํ”„๋ก ํŠธ์—”๋“œ-๋ฐฑ์—”๋“œ-๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ตฌ์กฐ์ž–์•„? ๊ทผ๋ฐ DApp์€ ์ข€ ๋‹ฌ๋ผ.

๐Ÿ“Œ ์ „ํ†ต์ ์ธ ์•ฑ vs DApp

์ „ํ†ต์ ์ธ ์•ฑ: ํด๋ผ์ด์–ธํŠธ โ†’ ์ค‘์•™ ์„œ๋ฒ„ โ†’ ์ค‘์•™ DB
DApp: ํด๋ผ์ด์–ธํŠธ โ†’ ๋ธ”๋ก์ฒด์ธ ๋„คํŠธ์›Œํฌ โ†’ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ

๊ทธ๋Ÿผ ๋ฐฑ์—”๋“œ๋Š” ํ•„์š” ์—†๋Š” ๊ฑฐ์•ผ? ์•„๋‹ˆ์•ผ! ์˜คํžˆ๋ ค ๋” ์ค‘์š”ํ•ด! ๐ŸŽฏ
DApp ๋ฐฑ์—”๋“œ์˜ ์—ญํ• ์€ ์ด๋ž˜:

1๏ธโƒฃ ๋ธ”๋ก์ฒด์ธ ๋ฐ์ดํ„ฐ ์ธ๋ฑ์‹ฑ
๋ธ”๋ก์ฒด์ธ์—์„œ ์ง์ ‘ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ๋Š” ๊ฑด ๋А๋ฆฌ๊ณ  ๋น„ํšจ์œจ์ ์ด์•ผ. ๋ฐฑ์—”๋“œ์—์„œ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฏธ๋ฆฌ ๊ฐ€์ ธ์™€์„œ ์บ์‹ฑํ•˜๊ณ  ์ธ๋ฑ์‹ฑํ•ด๋‘๋ฉด ํ›จ์”ฌ ๋น ๋ฅด์ง€!

2๏ธโƒฃ ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์ฒ˜๋ฆฌ
๋ชจ๋“  ๊ฑธ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ์— ๋„ฃ์œผ๋ฉด ๊ฐ€์Šค๋น„๊ฐ€ ์—„์ฒญ๋‚˜๊ฒŒ ๋‚˜์™€. ๋ธ”๋ก์ฒด์ธ์— ๊ผญ ํ•„์š”ํ•œ ๊ฒƒ๋งŒ ์˜ฌ๋ฆฌ๊ณ , ๋‚˜๋จธ์ง€๋Š” ๋ฐฑ์—”๋“œ์—์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒŒ ํ˜„๋ช…ํ•ด.

3๏ธโƒฃ ์™ธ๋ถ€ API ํ†ตํ•ฉ
๊ฒฐ์ œ ์‹œ์Šคํ…œ, ์ด๋ฉ”์ผ ์•Œ๋ฆผ, ํŒŒ์ผ ์ €์žฅ์†Œ(IPFS) ๋“ฑ ์™ธ๋ถ€ ์„œ๋น„์Šค์™€์˜ ํ†ตํ•ฉ์€ ๋ฐฑ์—”๋“œ๊ฐ€ ๋‹ด๋‹นํ•ด์•ผ ํ•ด.

4๏ธโƒฃ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฐœ์„ 
ํŠธ๋žœ์žญ์…˜ ์ƒํƒœ ์ถ”์ , ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ, ๋ฐ์ดํ„ฐ ๋ถ„์„ ๋“ฑ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๋†’์ด๋Š” ๊ธฐ๋Šฅ๋“ค์€ ๋ฐฑ์—”๋“œ์—์„œ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒŒ ์ข‹์•„.

๊ทธ๋Ÿผ ์™œ Go์–ธ์–ด๋ฅผ ์„ ํƒํ•ด์•ผ ํ• ๊นŒ? ๐Ÿคทโ€โ™‚๏ธ

๐Ÿ’ช Go์–ธ์–ด์˜ ๊ฐ•๋ ฅํ•œ ์žฅ์ ๋“ค

Go์–ธ์–ด๋Š” ๋ธ”๋ก์ฒด์ธ ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์— ์ •๋ง ์ตœ์ ํ™”๋œ ์–ธ์–ด์•ผ. ์ด์œ ๋ฅผ ํ•˜๋‚˜์”ฉ ์‚ดํŽด๋ณด์ž!

โšก ์„ฑ๋Šฅ๊ณผ ๋™์‹œ์„ฑ

Go๋Š” ์ปดํŒŒ์ผ ์–ธ์–ด๋ผ์„œ ์‹คํ–‰ ์†๋„๊ฐ€ ๋น ๋ฅด๊ณ , ๊ณ ๋ฃจํ‹ด(Goroutine)์ด๋ผ๋Š” ๊ฒฝ๋Ÿ‰ ์Šค๋ ˆ๋“œ ๋•๋ถ„์— ๋™์‹œ์— ์ˆ˜์ฒœ ๊ฐœ์˜ ๋ธ”๋ก์ฒด์ธ ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด. ์ด๋”๋ฆฌ์›€ ๋„คํŠธ์›Œํฌ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๋ฉด์„œ ์—ฌ๋Ÿฌ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ์™€ ๋™์‹œ์— ํ†ต์‹ ํ•ด์•ผ ํ•˜๋Š” DApp ๋ฐฑ์—”๋“œ์—๋Š” ๋”ฑ์ด์ง€! ๐Ÿš€
1. ์ด๋”๋ฆฌ์›€ ๊ณต์‹ ํด๋ผ์ด์–ธํŠธ Geth
์ด๋”๋ฆฌ์›€์˜ ๊ฐ€์žฅ ์ธ๊ธฐ ์žˆ๋Š” ํด๋ผ์ด์–ธํŠธ์ธ Geth๊ฐ€ Go๋กœ ์ž‘์„ฑ๋˜์–ด ์žˆ์–ด. ๊ทธ๋ž˜์„œ Go ์ƒํƒœ๊ณ„์—๋Š” ๋ธ”๋ก์ฒด์ธ ๊ด€๋ จ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์ •๋ง ํ’๋ถ€ํ•ด. ํŠนํžˆ go-ethereum ํŒจํ‚ค์ง€๋Š” ์ด๋”๋ฆฌ์›€๊ณผ ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š” ๋ชจ๋“  ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜์ง€.

2. ๊ฐ„๊ฒฐํ•˜๊ณ  ์ฝ๊ธฐ ์‰ฌ์šด ์ฝ”๋“œ
Go์˜ ๋ฌธ๋ฒ•์€ ์ •๋ง ์‹ฌํ”Œํ•ด. ๋ณต์žกํ•œ ๋ธ”๋ก์ฒด์ธ ๋กœ์ง์„ ๊ตฌํ˜„ํ•  ๋•Œ ์ฝ”๋“œ๊ฐ€ ์ง€์ €๋ถ„ํ•ด์ง€์ง€ ์•Š๊ณ  ๊น”๋”ํ•˜๊ฒŒ ์œ ์ง€๋ผ. ํŒ€ ํ”„๋กœ์ ํŠธ์—์„œ ํ˜‘์—…ํ•  ๋•Œ๋„ ์ฝ”๋“œ ๋ฆฌ๋ทฐ๊ฐ€ ์ˆ˜์›”ํ•˜์ง€.

3. ๊ฐ•๋ ฅํ•œ ํ‘œ์ค€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
HTTP ์„œ๋ฒ„, JSON ์ฒ˜๋ฆฌ, ์•”ํ˜ธํ™” ๋“ฑ ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์— ํ•„์š”ํ•œ ๊ฑฐ์˜ ๋ชจ๋“  ๊ธฐ๋Šฅ์ด ํ‘œ์ค€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ํฌํ•จ๋˜์–ด ์žˆ์–ด. ์™ธ๋ถ€ ์˜์กด์„ฑ์„ ์ตœ์†Œํ™”ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒŒ ํฐ ์žฅ์ ์ด์•ผ.

4. ํฌ๋กœ์Šค ํ”Œ๋žซํผ ์ง€์›
ํ•œ ๋ฒˆ ์ž‘์„ฑํ•œ ์ฝ”๋“œ๋ฅผ Linux, Windows, macOS ์–ด๋””์„œ๋“  ์ปดํŒŒ์ผํ•ด์„œ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์–ด. ๋ฐฐํฌ๊ฐ€ ์ •๋ง ๊ฐ„ํŽธํ•˜์ง€!

์‹ค์ œ๋กœ ๋งŽ์€ ๋ธ”๋ก์ฒด์ธ ํ”„๋กœ์ ํŠธ๋“ค์ด Go๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์–ด. Ethereum, Hyperledger Fabric, Cosmos SDK ๋“ฑ์ด ๋Œ€ํ‘œ์ ์ด์•ผ. ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ๋ธ”๋ก์ฒด์ธ ๊ด€๋ จ ๊ฐœ๋ฐœ ์žฌ๋Šฅ์„ ๊ฑฐ๋ž˜ํ•  ๋•Œ Go ๊ฐœ๋ฐœ์ž ์ˆ˜์š”๊ฐ€ ์ •๋ง ๋†’๋”๋ผ๊ณ ! ๐Ÿ“ˆ

๐Ÿ› ๏ธ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์„ธํŒ…ํ•˜๊ธฐ

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ๊ฐœ๋ฐœ์„ ์‹œ์ž‘ํ•ด๋ณด์ž! ๋จผ์ € ํ•„์š”ํ•œ ๋„๊ตฌ๋“ค์„ ์„ค์น˜ํ•ด์•ผ ํ•ด.

1๋‹จ๊ณ„: Go ์„ค์น˜
Go ๊ณต์‹ ์›น์‚ฌ์ดํŠธ(golang.org)์—์„œ ์ตœ์‹  ๋ฒ„์ „์„ ๋‹ค์šด๋กœ๋“œํ•ด. 2024๋…„ ๊ธฐ์ค€์œผ๋กœ Go 1.21 ์ด์ƒ์„ ๊ถŒ์žฅํ•ด.

# ์„ค์น˜ ํ™•์ธ
go version

# ์ถœ๋ ฅ ์˜ˆ์‹œ: go version go1.21.0 linux/amd64

2๋‹จ๊ณ„: go-ethereum ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜
์ด๋”๋ฆฌ์›€๊ณผ ์ƒํ˜ธ์ž‘์šฉํ•˜๊ธฐ ์œ„ํ•œ ํ•ต์‹ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์•ผ. ์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ์ •๋ง ๊ฐ•๋ ฅํ•ด์„œ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ๋ฐฐํฌ, ํŠธ๋žœ์žญ์…˜ ์ „์†ก, ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋‹ ๋“ฑ ๊ฑฐ์˜ ๋ชจ๋“  ๊ฑธ ํ•  ์ˆ˜ ์žˆ์–ด.

# ํ”„๋กœ์ ํŠธ ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ
mkdir dapp-backend
cd dapp-backend

# Go ๋ชจ๋“ˆ ์ดˆ๊ธฐํ™”
go mod init github.com/yourusername/dapp-backend

# go-ethereum ์„ค์น˜
go get github.com/ethereum/go-ethereum

3๋‹จ๊ณ„: ๊ฐœ๋ฐœ ๋„๊ตฌ ์ค€๋น„
ํ•„์ˆ˜ ๋„๊ตฌ๋“ค:

โ€ข Ganache: ๋กœ์ปฌ ์ด๋”๋ฆฌ์›€ ํ…Œ์ŠคํŠธ ๋„คํŠธ์›Œํฌ (๊ฐœ๋ฐœ์šฉ)
โ€ข MetaMask: ์ด๋”๋ฆฌ์›€ ์ง€๊ฐ‘ (ํ…Œ์ŠคํŠธ์šฉ)
โ€ข Remix IDE: ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์ž‘์„ฑ ๋ฐ ๋ฐฐํฌ
โ€ข Infura ๋˜๋Š” Alchemy: ์ด๋”๋ฆฌ์›€ ๋…ธ๋“œ ์ ‘๊ทผ ์„œ๋น„์Šค
โ€ข Solidity ์ปดํŒŒ์ผ๋Ÿฌ: ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์ปดํŒŒ์ผ

๐Ÿ’ก Pro Tip

์ฒ˜์Œ์—๋Š” 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์— ์˜ฌ๋ ธ์„ ๋•Œ ๋ณด์•ˆ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธธ ์ˆ˜ ์žˆ์–ด!

ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ API ํ‚ค ๊ด€๋ฆฌํ•˜๊ธฐ
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...

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ํ›จ์”ฌ ์•ˆ์ „ํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๊ธฐ๋„ ์‰ฌ์›Œ์ ธ! ๐Ÿ”
Go Backend ethclient.Dial Context Transaction Ethereum Smart Contract Blockchain Events RPC Connection API Web3 Infura / Alchemy Node Provider

๐Ÿ“ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ์™€ ์ƒํ˜ธ์ž‘์šฉํ•˜๊ธฐ

์ด์ œ ์ง„์งœ ํ•ต์‹ฌ์œผ๋กœ ๋“ค์–ด๊ฐ€๋ณด์ž! ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ์™€ ํ†ต์‹ ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด์•ผ. ์ด๊ฒŒ DApp ๋ฐฑ์—”๋“œ์˜ ๊ฝƒ์ด๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ์ง€. ๐ŸŒธ

์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์ค€๋น„
๋จผ์ € ๊ฐ„๋‹จํ•œ ERC-20 ํ† ํฐ ์ปจํŠธ๋ž™ํŠธ๋ฅผ ์˜ˆ์‹œ๋กœ ์‚ฌ์šฉํ• ๊ฒŒ. Solidity๋กœ ์ž‘์„ฑ๋œ ์ปจํŠธ๋ž™ํŠธ์•ผ:

// 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;
    }
}

Go ๋ฐ”์ธ๋”ฉ ์ƒ์„ฑํ•˜๊ธฐ
์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ๋ฅผ Go์—์„œ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ABI(Application Binary Interface)๋ฅผ Go ์ฝ”๋“œ๋กœ ๋ณ€ํ™˜ํ•ด์•ผ ํ•ด. ์ด๊ฑธ "๋ฐ”์ธ๋”ฉ"์ด๋ผ๊ณ  ๋ถˆ๋Ÿฌ. abigen ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์–ด!

# 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์—์„œ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋Š” ๋ฉ”์„œ๋“œ๋“ค์ด ๋“ค์–ด์žˆ์–ด!

์ปจํŠธ๋ž™ํŠธ ๋ฐฐํฌํ•˜๊ธฐ
์ด์ œ 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๏ธโƒฃ ๋ฐฐํฌ๋œ ์ปจํŠธ๋ž™ํŠธ ์ฃผ์†Œ ๋ฐ›๊ธฐ

๐Ÿ”„ ์ปจํŠธ๋ž™ํŠธ ํ•จ์ˆ˜ ํ˜ธ์ถœํ•˜๊ธฐ

์ปจํŠธ๋ž™ํŠธ๊ฐ€ ๋ฐฐํฌ๋˜์—ˆ์œผ๋‹ˆ ์ด์ œ ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•ด๋ณด์ž! ํฌ๊ฒŒ ๋‘ ๊ฐ€์ง€ ํƒ€์ž…์ด ์žˆ์–ด:

1. ์ฝ๊ธฐ ์ „์šฉ ํ˜ธ์ถœ (Call)
๋ธ”๋ก์ฒด์ธ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๋Š” ํ•จ์ˆ˜๋“ค์ด์•ผ. ๊ฐ€์Šค๋น„๊ฐ€ ๋“ค์ง€ ์•Š์•„์„œ ๋ถ€๋‹ด ์—†์ด ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์ง€!

// ๊ธฐ์กด ์ปจํŠธ๋ž™ํŠธ์— ์—ฐ๊ฒฐ
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())

2. ์“ฐ๊ธฐ ํ˜ธ์ถœ (Transaction)
๋ธ”๋ก์ฒด์ธ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ํ•จ์ˆ˜๋“ค์ด์•ผ. ๊ฐ€์Šค๋น„๊ฐ€ ๋“ค๊ณ , ํŠธ๋žœ์žญ์…˜์ด ๋ธ”๋ก์— ํฌํ•จ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ ค์•ผ ํ•ด.

// ํŠธ๋žœ์žญ์…˜ ์˜ต์…˜ ์„ค์ •
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())
    }
}

์‹ค์‹œ๊ฐ„ ์ด๋ฒคํŠธ ๊ตฌ๋…ํ•˜๊ธฐ
์ด์ œ ์ง„์งœ ๋ฉ‹์ง„ ๋ถ€๋ถ„! ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ด๋ฒคํŠธ๋ฅผ ๊ฐ์ง€ํ•˜๋Š” ๊ฑฐ์•ผ. WebSocket ์—ฐ๊ฒฐ์„ ์‚ฌ์šฉํ•ด์„œ ์ƒˆ๋กœ์šด ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์ฆ‰์‹œ ์•Œ๋ฆผ์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด! ๐Ÿ””

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 ํ”Œ๋žซํผ: ํˆฌํ‘œ, ์ œ์•ˆ ์ƒ์„ฑ ๋“ฑ ๊ฑฐ๋ฒ„๋„Œ์Šค ํ™œ๋™
Smart Contract Event Transfer Approval Mint Burn Go Backend Listen Process Store Notify Update WebSocket Real-time Event Stream Subscribe Filter Logs

๐Ÿ—๏ธ 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

ํ•ต์‹ฌ ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„
1๏ธโƒฃ ๋ธ”๋ก์ฒด์ธ ํด๋ผ์ด์–ธํŠธ ๋งค๋‹ˆ์ €

์—ฌ๋Ÿฌ ๋„คํŠธ์›Œํฌ๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ  ์—ฐ๊ฒฐ ํ’€๋ง์„ ์ œ๊ณตํ•˜๋Š” ํด๋ผ์ด์–ธํŠธ ๋งค๋‹ˆ์ €์•ผ:

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
            }
        }
    }
}

๐Ÿ’ก ํ”„๋กœ๋•์…˜ ํŒ

์‹ค์ œ ์„œ๋น„์Šค์—์„œ๋Š” ํŠธ๋žœ์žญ์…˜ ์žฌ์‹œ๋„ ๋กœ์ง, ๊ฐ€์Šค ๊ฐ€๊ฒฉ ์ตœ์ ํ™”, ๋…ผ์Šค ๊ด€๋ฆฌ ๋“ฑ์„ ๋” ์ •๊ตํ•˜๊ฒŒ ๊ตฌํ˜„ํ•ด์•ผ ํ•ด. ํŠนํžˆ ๋†’์€ ํŠธ๋ž˜ํ”ฝ ํ™˜๊ฒฝ์—์„œ๋Š” ๋…ผ์Šค ์ถฉ๋Œ์„ ๋ฐฉ์ง€ํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•˜์ง€!

๐Ÿ—„๏ธ ๋ฐ์ดํ„ฐ ์ธ๋ฑ์‹ฑ๊ณผ ์บ์‹ฑ

๋ธ”๋ก์ฒด์ธ์—์„œ ์ง์ ‘ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ๋Š” ๊ฑด ๋А๋ฆฌ๊ณ  ๋น„ํšจ์œจ์ ์ด์•ผ. ๊ทธ๋ž˜์„œ ์ค‘์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ์ปฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ธ๋ฑ์‹ฑํ•˜๊ณ  ์บ์‹ฑํ•˜๋Š” ๊ฒŒ ํ•„์ˆ˜์•ผ! ๐Ÿ“Š

PostgreSQL์„ ํ™œ์šฉํ•œ ๋ฐ์ดํ„ฐ ์ €์žฅ
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
}

Redis๋ฅผ ํ™œ์šฉํ•œ ์บ์‹ฑ
์ž์ฃผ ์กฐํšŒ๋˜๋Š” ๋ฐ์ดํ„ฐ๋Š” Redis์— ์บ์‹ฑํ•ด์„œ ์‘๋‹ต ์†๋„๋ฅผ ๋†’์ผ ์ˆ˜ ์žˆ์–ด:

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 ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•  ๊ฑฐ์•ผ. ๊ฐ€๋ณ๊ณ  ๋น ๋ฅด๊ฑฐ๋“ ! ๐Ÿš€

API ํ•ธ๋“ค๋Ÿฌ ๊ตฌํ˜„
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. ๊ฐ€์Šค ๋ฆฌ๋ฏธํŠธ๋ฅผ ๋ฌดํ•œ๋Œ€๋กœ ์„ค์ •

๊ฐœ์ธํ‚ค ์•ˆ์ „ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•˜๊ธฐ
1. ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์‚ฌ์šฉ

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
}

ํŠธ๋žœ์žญ์…˜ ์žฌ์ „์†ก ๊ณต๊ฒฉ ๋ฐฉ์ง€
๋…ผ์Šค(Nonce) ๊ด€๋ฆฌ๋ฅผ ์ œ๋Œ€๋กœ ํ•˜์ง€ ์•Š์œผ๋ฉด ๊ฐ™์€ ํŠธ๋žœ์žญ์…˜์ด ์—ฌ๋Ÿฌ ๋ฒˆ ์‹คํ–‰๋  ์ˆ˜ ์žˆ์–ด:

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"),
    )
}

Prometheus ๋ฉ”ํŠธ๋ฆญ
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)
}
DApp Backend Architecture API Layer REST / GraphQL Service Layer Business Logic Blockchain Smart Contracts Database PostgreSQL Cache Redis Queue RabbitMQ Monitor Logs

๐Ÿš€ ๋ฐฐํฌ์™€ ์šด์˜

๊ฐœ๋ฐœ์ด ๋๋‚ฌ์œผ๋ฉด ์ด์ œ ๋ฐฐํฌํ•  ์ฐจ๋ก€์•ผ! Docker์™€ Kubernetes๋ฅผ ์‚ฌ์šฉํ•ด์„œ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์— ๋ฐฐํฌํ•ด๋ณด์ž. ๐ŸŽ‰

Dockerfile ์ž‘์„ฑ
# ๋นŒ๋“œ ์Šคํ…Œ์ด์ง€
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"]

Docker Compose
๋กœ์ปฌ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์„ ์‰ฝ๊ฒŒ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์–ด:

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

Kubernetes ๋ฐฐํฌ
ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” Kubernetes๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํ™•์žฅ์„ฑ๊ณผ ์•ˆ์ •์„ฑ์„ ํ™•๋ณดํ•  ์ˆ˜ ์žˆ์–ด:

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 ์œ ํ˜•์ด์•ผ. ๐Ÿ–ผ๏ธ

NFT ์ปจํŠธ๋ž™ํŠธ (ERC-721)
// 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);
    }
}

Go ๋ฐฑ์—”๋“œ ๊ตฌํ˜„
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
}

IPFS ํ†ตํ•ฉ
NFT ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์™€ ์ด๋ฏธ์ง€๋Š” IPFS์— ์ €์žฅํ•˜๋Š” ๊ฒŒ ์ผ๋ฐ˜์ ์ด์•ผ:

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 ๋ฐฑ์—”๋“œ์˜ ์„ฑ๋Šฅ์„ ์ตœ๋Œ€ํ•œ ๋Œ์–ด์˜ฌ๋ฆฌ๋Š” ๋ฐฉ๋ฒ•๋“ค์„ ์•Œ๋ ค์ค„๊ฒŒ! ๐Ÿš€

1. ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ
์—ฌ๋Ÿฌ ๊ฐœ์˜ ๋ธ”๋ก์ฒด์ธ ์กฐํšŒ๋ฅผ ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌํ•˜๋ฉด ํ›จ์”ฌ ๋น ๋ฅด์ง€:

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
}

2. ์—ฐ๊ฒฐ ํ’€๋ง
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
}

3. ์Šค๋งˆํŠธ ์บ์‹ฑ ์ „๋žต
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()
}

๐Ÿ”ง ๋ฌธ์ œ ํ•ด๊ฒฐ ๊ฐ€์ด๋“œ

๊ฐœ๋ฐœํ•˜๋‹ค ๋ณด๋ฉด ์—ฌ๋Ÿฌ ๋ฌธ์ œ์— ๋ถ€๋”ชํžˆ๊ฒŒ ๋ผ. ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๋“ค๊ณผ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ์ •๋ฆฌํ•ด๋ดค์–ด! ๐Ÿ› ๏ธ

โŒ ๋ฌธ์ œ 1: "insufficient funds for gas * price + value"

์›์ธ: ๊ณ„์ •์— ๊ฐ€์Šค๋น„๋ฅผ ์ง€๋ถˆํ•  ETH๊ฐ€ ๋ถ€์กฑํ•ด.

ํ•ด๊ฒฐ:
โ€ข ํ…Œ์ŠคํŠธ๋„ท faucet์—์„œ ETH ๋ฐ›๊ธฐ
โ€ข ๊ฐ€์Šค ๊ฐ€๊ฒฉ ๋‚ฎ์ถ”๊ธฐ (ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ๋งŒ)
โ€ข ๊ฐ€์Šค ๋ฆฌ๋ฏธํŠธ ์กฐ์ •ํ•˜๊ธฐ

โŒ ๋ฌธ์ œ 2: "nonce too low"

์›์ธ: ์ด๋ฏธ ์‚ฌ์šฉ๋œ nonce๋ฅผ ์žฌ์‚ฌ์šฉํ•˜๋ ค๊ณ  ํ•จ.

ํ•ด๊ฒฐ:
โ€ข NonceManager ์‚ฌ์šฉํ•ด์„œ nonce ๊ด€๋ฆฌ
โ€ข PendingNonceAt์œผ๋กœ ์ตœ์‹  nonce ๊ฐ€์ ธ์˜ค๊ธฐ
โ€ข ํŠธ๋žœ์žญ์…˜ ์‹คํŒจ ์‹œ nonce ๋ฆฌ์…‹

โŒ ๋ฌธ์ œ 3: "execution reverted"

์›์ธ: ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์‹คํ–‰ ์ค‘ ์—๋Ÿฌ ๋ฐœ์ƒ.

ํ•ด๊ฒฐ:
โ€ข Remix์—์„œ ์ปจํŠธ๋ž™ํŠธ ํ…Œ์ŠคํŠธ
โ€ข require ์กฐ๊ฑด ํ™•์ธ
โ€ข ์ด๋ฒคํŠธ ๋กœ๊ทธ ๋ถ„์„
โ€ข Tenderly ๊ฐ™์€ ๋””๋ฒ„๊น… ๋„๊ตฌ ์‚ฌ์šฉ

โŒ ๋ฌธ์ œ 4: "connection refused"

์›์ธ: ์ด๋”๋ฆฌ์›€ ๋…ธ๋“œ์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์Œ.

ํ•ด๊ฒฐ:
โ€ข ๋…ธ๋“œ 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