블록체인 기반 아티스트-클라이언트 계약 투명화 플랫폼
MOAS는 음악/예술 산업의 계약 불투명성 문제를 블록체인으로 해결하는 Web2.5 하이브리드 플랫폼입니다.
- 🔒 계약 위변조 방지: EIP-712 전자서명과 SHA-3 해싱으로 계약 무결성 보장
- 🏆 경력 증명 NFT: 계약 완료 시 ERC-1155 기반 포트폴리오 NFT 자동 발행
- 💰 투명한 정산: 스마트 컨트랙트 기반 자동 정산 및 수수료 처리
- 🤖 AI 매칭: LLM + Vector DB를 활용한 프로젝트-아티스트 매칭
"Web2의 편의성과 Web3의 신뢰성을 결합한 하이브리드 아키텍처 설계 및 구현"
- Architecture Design: RDB(MySQL)와 Blockchain(Ethereum) 간의 상태를 동기화하는 이벤트 기반 시스템 설계
- Core Business Logic:
- Electronic Signature: EIP-712 표준을 준수한 서명 검증 및 계약 위변조 방지 로직 구현
- Payment Integrity: PG사 결제 정보와 내부 주문 정보 간의 교차 검증 시스템 구축
- Transaction Safety: 분산 환경에서의 데이터 정합성 보장을 위한 재시도(Retry) 및 격리(Isolation) 전략 수립
┌─────────────────────────────────────────────────────────────┐
│ Frontend (React) │
│ Web3Auth (Google/Kakao/Apple) │
└───────────────────────┬─────────────────────────────────────┘
│ REST API + JWT
┌───────────────────────┴─────────────────────────────────────┐
│ Spring Boot Backend │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Contract │ │ Payment │ │ Blockchain │ │
│ │ Service │ │ Service │ │ Client │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ ┌──────▼──────────────────▼──────────────────▼──────┐ │
│ │ MySQL (Contract, Payment, Member) │ │
│ └───────────────────────────────────────────────────┘ │
└───────────────────────┬─────────────────────────────────────┘
│ Web3j + WebSocket
┌───────────────────────▼─────────────────────────────────────┐
│ Ethereum Sepolia Testnet (Blockchain) │
│ ┌────────────────────────────────────────────────────┐ │
│ │ MOASContract.sol (ERC-1155) │ │
│ │ - createContract() │ │
│ │ - updateContractStatus() │ │
│ │ - cancelContract() │ │
│ │ - EIP-712 Dual Signature Verification │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
[Web2 - MySQL] [Web3 - Blockchain]
│ │
│ 1. Contract Creation │
├────────────────────────────────────►│ 2. Mint NFT (txHash)
│ │
│ 3. OnchainRecord (PENDING) │
◄────────────────────────────────────┤ 4. Event Emitted
│ │
│ 5. OnchainRecord (SUCCEEDED) │
│ ContractNft Record Created │
└─────────────────────────────────────┘
- Language: Java 21 (Record, Pattern Matching, Switch Expression)
- Framework: Spring Boot 3.2.x
- Data Access: Spring Data JPA, QueryDSL, JPQL Projection
- Security: Spring Security, JWT (Access/Refresh Token Rotation), Web3Auth OIDC
- Validation: Hibernate Validator
- Smart Contract: Solidity 0.8.28, OpenZeppelin Contracts
- Blockchain Interaction: Web3j 4.x (HTTP + WebSocket)
- Standards: ERC-1155 (Non-Transferable NFT), EIP-712 (Typed Structured Data), ERC-2771 (Meta-Transactions)
- Network: Ethereum Sepolia Testnet
- Database: MySQL 8.0
- Cache/Session: Redis (Lettuce Client)
- Storage: AWS S3 (File Upload), Thumbnailator (Image Processing)
- Payment: Toss Payments API
- Vector DB: QDrant (Semantic Search)
- LLM: GMS GPT-4.1 API (SSAFY)
- Build: Gradle 8.x
- Testing: JUnit 5, Mockito, H2 (In-Memory DB)
- API Documentation: Swagger/OpenAPI 3.0
- Logging: SLF4j + Logback
- EIP-712 Dual Signature: 리더와 아티스트 양측의 전자서명으로 계약 체결
- SHA-3 해싱: 계약 내용을 해싱하여 위변조 방지
- Smart Contract 검증: 온체인에서 서명 검증 후 NFT 발행
// 서명 검증 예시 (SignatureVerifier.java)
public boolean verify(String signerAddress, TypedDataResponse typedData, String signature) {
StructuredDataEncoder encoder = new StructuredDataEncoder(typedDataJson);
byte[] messageHash = encoder.hashStructuredData();
BigInteger recoveredPublicKey = Sign.signedMessageHashToKey(messageHash, signatureData);
String recoveredAddress = "0x" + Keys.getAddress(recoveredPublicKey);
return signerAddress.equalsIgnoreCase(recoveredAddress);
}- 비동기 트랜잭션: Spring Event +
@Async로 블록체인 트랜잭션 전송 - 감사 로그 분리:
@Transactional(REQUIRES_NEW)로 트랜잭션 전파 제어 - 재시도 메커니즘:
@Retryable(5회, 2초 백오프)로 네트워크 장애 대응
// 블록체인 동기화 플로우 (ContractCommandListener.java)
@Async
@TransactionalEventListener
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handleContractPaidEvent(ContractPaidEvent event) {
OnchainRecord record = onchainRecordService.create(contractId, MINT);
String txHash = blockchainClient.createContract(contract);
record.updateTxHash(txHash);
}
// 이벤트 수신 및 재시도 (OnchainEventProcessService.java)
@Retryable(maxAttempts = 5, backoff = @Backoff(delay = 2000))
public void processContractCreatedEvent(ContractCreatedEvent event) {
onchainRecord.markSucceeded();
contractNftService.createNft(contractId, tokenId, txHash);
}- Toss Payments 연동: 결제 승인, 부분 취소, 구매 확정
- 금액 위변조 검증: 클라이언트 금액과 서버 금액 교차 검증
- 중복 승인 방지: 주문 상태 체크로 멱등성 보장
// 금액 검증 및 중복 방지 (PaymentService.java)
public PaymentApproveResultDto approvePayment(String paymentKey, String orderId, Long amount) {
Order order = orderRepository.findById(orderId).orElseThrow();
// 중복 승인 방지
if (order.getStatus() == OrderStatus.PAID) {
return PaymentApproveResultDto.from(order);
}
// 금액 위변조 검증
if (!order.getAmount().equals(amount)) {
throw PaymentException.amountMismatch();
}
// Toss Payments API 호출 및 처리
// ...
}- Vector Embedding: Ollama API로 프로젝트 설명 임베딩
- Semantic Search: QDrant에서 유사도 기반 프로젝트 추천
- LLM 추천: GMS GPT-4.1로 맞춤형 프로젝트 분석
- Java: 21 이상
- Gradle: 8.x
- MySQL: 8.0
- Redis: 6.x
- Node.js: 18.x (Solidity 컴파일용)
.env 파일을 생성하고 다음 변수를 설정하세요:
# Database
DB_URL=jdbc:mysql://localhost:3306/moas_db
DB_USERNAME=your_db_user
DB_PASSWORD=your_db_password
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
# JWT
JWT_SECRET=your-secret-key-min-256-bits
# AWS S3
AWS_S3_ACCESS_KEY=your_access_key
AWS_S3_SECRET_KEY=your_secret_key
AWS_S3_BUCKET_NAME=your_bucket_name
# Blockchain
BLOCKCHAIN_RPC_URL=https://sepolia.infura.io/v3/YOUR_PROJECT_ID
BLOCKCHAIN_WS_URL=wss://sepolia.infura.io/ws/v3/YOUR_PROJECT_ID
SERVER_WALLET_PRIVATE_KEY=0xYourPrivateKey
MOAS_CONTRACT_ADDRESS=0xYourDeployedContractAddress
BLOCKCHAIN_CHAIN_ID=11155111
# Web3Auth
WEB3AUTH_AUDIENCE=your_web3auth_client_id
# QDrant
QDRANT_HOST=localhost
QDRANT_PORT=6334
QDRANT_API_KEY=your_qdrant_api_key
# LLM (GMS)
GMS_KEY=your_gms_api_key
# Toss Payments
TOSS_PAYMENTS_SECRET_KEY=your_toss_secret_key
TOSS_PAYMENTS_API_URL=https://api.tosspayments.com/v1/payments
# Encryption
ACCOUNT_ENCRYPTION_KEY=your_32byte_base64_keygit clone https://github.com/yourusername/moas.git
cd moas/backend/moas# OpenZeppelin 의존성 설치
npm install
# Solidity → Java Wrapper 생성
./gradlew generateContractWrappers# 전체 빌드 (테스트 포함)
./gradlew build
# 빌드만 (테스트 제외)
./gradlew build -x test# 개발 환경
./gradlew bootRun --args='--spring.profiles.active=dev'
# 프로덕션 환경
java -jar build/libs/moas-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod# 전체 테스트
./gradlew test
# 특정 테스트 클래스
./gradlew test --tests "com.s401.moas.contract.service.ContractServiceTest"
# 커버리지 리포트
./gradlew jacocoTestReport문제: 네트워크 장애로 블록체인 트랜잭션 전송 실패 시 DB 상태 불일치
해결:
// OnchainRecord 테이블로 실패 추적
@Retryable(maxAttempts = 5)
public void retryFailedTransactions() {
List<OnchainRecord> failed = onchainRecordRepository.findByStatus(FAILED);
failed.forEach(record -> {
try {
blockchainClient.retryTransaction(record);
record.markSucceeded();
} catch (Exception e) {
log.error("재시도 실패: recordId={}", record.getId(), e);
}
});
}문제: Toss Payments에서 동일한 Webhook을 여러 번 전송하는 경우 중복 처리
해결:
// Order 상태로 멱등성 보장
if (order.getStatus() == OrderStatus.PAID) {
log.info("이미 처리된 주문: orderId={}", orderId);
return PaymentApproveResultDto.from(order);
}문제: 프론트엔드와 백엔드의 TypedData 구조 불일치로 서명 검증 실패
해결: TypedData 스펙을 REST API로 제공하여 프론트엔드와 동기화
@GetMapping("/contracts/{contractId}/typed-data")
public TypedDataResponse getTypedData(@PathVariable Long contractId) {
return contractService.generateTypedData(contractId);
}