백엔드 서버와 블록체인 서버 간의 데이터 정합성을 유지하는 방법

Taewoo·2023년 4월 26일
0

NFT 거래 서비스

목록 보기
2/3
post-thumbnail

회사에서 키우는 강아지랑 놀던 중 앱 개발팀으로 부터 NFT를 전송하면서 복사 버그를 발견했다고 Slack DM을 받았다. 😭

문제와 원인 파악

확인해보니 NFT를 전송하던 도중 오류가 발생했고 데이터베이스에서는 @Transactional 어노테이션이 있어서 롤되었는데 블록체인 서버로 전송된 NFT는 롤백되지 않았던 것이다.

/**
*	fromWallet: 보내는 지갑
*   toWallet:   받는 지갑
* 	contractId: NFT
*/
@Transactional
public void transferProduct(
	@CurrentWallet Wallet fromWallet,
    String fromWalletPassword,
    Wallet toWallet,
    String contractId
){
	// 1 블록체인에 NFT 전송을 요청 (바로 처리되지 않음) 
    // Mainnet 클래스는 블록체인에 쉽게 요청할 수 있도록 만든 모듈이다.
    Mainnet
    	.transfer()
        .withFrom(fromWallet)
        .withTo(toWallet)
        .withContractId(contractId)
        .execute();
    
   // 2 데이터베이스 캐싱 관련 로직
    product.updateWallet(toWallet);
   // ... 
    
   // 3 비즈니스 로직 
   // ... 
   
}

위의 코드는 실제 로직을 간단하게 줄여서 작성했다.

1번 로직에서 토큰 전송을 요청하는데 블록과 트랜잭션을 쌓는데 시간이 걸리기 때문에 성공 실패 여부를 바로 알 수 없다. 2번 로직이 데이터베이스에 반영되면 받은 사람의 지갑에 받은 NFT가 보이게되고 보내는 사람의 지갑에서 NFT가 사라지게 된다.

버그의 원인은 3번에서 어떤 오류가 발생해서 롤백이 발생했다면 데이터베이스에 관한 로직은 트랜잭션에 의해 롤백되지만 서버간 RPC 요청으로 전송한 NFT는 롤백되지 않는다. 그래서 다른 NFT 로직들은 try~catch를 사용해 예외처리를 해줘야되지만 전송하는 부분에서 잊어버린 것이었다.

용어 정리

이름설명
Contract IDNFT를 생성하면 부여되는 고유의 ID
Wallet유저의 지갑 정보들이 들어간 도메인
@CurrentWallet요청한 사용자의 지갑을 AOP를 활용해서 매개변수에 자동으로 들어가도록 만들었다.

해결 방법

/**
*	fromWallet: 보내는 지갑
*   toWallet:   받는 지갑
* 	contractId: NFT
*/
@Transactional
public void transferProduct(
	@CurrentWallet Wallet fromWallet,
    String fromWalletPassword,
    Wallet toWallet,
    String contractId
){

    Mainnet
    	.transfer()
        .withFrom(fromWallet)
        .withTo(toWallet)
        .withContractId(contractId)
        .execute();

       // 2 데이터베이스 캐싱 관련 로직
        product.updateWallet(toWallet);
       // ... 
	try{
       // 3 비즈니스 로직 
       // ... w
    }catch (xxException e){
    	// 블록체인 롤백 로직
        throw e;
    }

https://rollbar.com/blog/how-to-handle-checked-unchecked-exceptions-in-java/

이렇게 만들면 비즈니스 로직 중 예외가 발생해도 트랜잭션 롤백과 함께 블록체인 롤백이 실행되지만 일일이 예외 로직을 실행하도록 코딩하면 트랜잭션이 롤백될 때마다 캐시서버, 블록체인 같은 @Transactional 롤백 범위에 해당되지 않는 외부 기능이 있을 경우에 try~catch문을 하나하나 찾아서 롤백 시켜줘야한다.

나의 경우 처럼 롤백 로직을 중간에 빠뜨릴 수도 있고 프로젝트가 커질 수록 점점 유지보수가 힘들어진다.

더 나은 방법? 🔥

트랜잭션 롤백에 대해 이것 저것 공부하던 중 TransactionSynchronization을 알게 되었다.

트랜잭션 동기화는 트랜잭션이 커밋될 때 또는 롤백될 때 수행할 작업을 등록할 수 있고 트랜잭션 완료 후 실행되므로 데이터베이스 트랜잭션의 안정성과 일관성을 보장할 수 있다.

@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MainnetRollback extends TransactionSynchronizationAdapter {

	private toWallet toWallet = null;
	private fromWallet fromWallet = null;
	
    // 생성자
    
	@Override
	public void afterCompletion(int status) {
		if (status == STATUS_ROLLED_BACK) {
			// 블록체인 롤백 로직 ...
            Mainnet()
            	.transfer()
                ... 
            // 가스비 환불 로직 ...
		}
	}
}

afterCompletion()은 트랜잭션이 종료되었을 떄 실행하는 메서드다.

status는 커밋과 롤백을 나타내는데 롤백의 경우 1이다.

/**
*	fromWallet: 보내는 지갑
*   toWallet:   받는 지갑
* 	contractId: NFT
*/
@Transactional
public void transferProduct(
	@CurrentWallet Wallet fromWallet,
    String fromWalletPassword,
    Wallet toWallet,
    String contractId
){
	// 트랜잭션 동기화 매니저를 사용해 위 클래스를 등록해주자. 
	TransactionSynchronizationManager.
			registerSynchronization(new MainnetRollback(toWallet, fromWallet));

	// 1 블록체인에 NFT 전송을 요청 (바로 처리되지 않음) 
    Mainnet
    	.transfer()
        .withFrom(fromWallet)
        .withTo(toWallet)
        .withContractId(contractId)
        .execute();
    
   // 2 데이터베이스 캐싱 관련 로직
    product.updateWallet(toWallet);
   // ... 
    
   // 3 비즈니스 로직 
   // ... 
   
}

이렇게 리팩토링된 코드는 3번 로직에서 오류가 생겼을 경우 트랜잭션 롤백이 일어나고 해당 트랜잭션이 종료되면 오버라이드한 afterCompletion() 메서드가 실행되면서 롤백을 커스터마이즈 할 수 있다.

0개의 댓글