// 에드센스

다시 초심으로 돌아가 솔리디티부터 해볼려고한다.

걷지도 못하는데 어떻게 뛰겠는가!

그래서 크립토좀비를 한 챕터마다 정리하면서 진행해보려고했다만

첫 챕터는 그냥 완전 기본 문법(변수형 선언, 형변환, 구조체)이런것들이라 딱히 적을게 없긴하다.

 

 

 


 

 

 

그래도! 몇 가지 기존의 프로그래밍 언어와 조금 다른 부분을 정리해보자면

 

1. view

function sayHello() public view returns (string) {}

이런식으로 view라는 키워드를 쓴다.

얘는 솔리디티에서 상태를 변화시키지 않는다는 의미다.

즉, 어떤 값을 쓰거나 변경하지 않는다는 의미. only view만 하는 함수라는 의미.

 

-> storage를 읽을 수 있지만 변경은 안됨

 

 

2. pure

function _multiply(uint a, uint b) private pure returns (uint) {
  return a * b;
}

pure는 함수가 앱에서 어떤 데이터도 접근하지 않는것을 의미한다.

 

-> storage를 읽을수도 없고 변경도 안된다

 

 

3. 이벤트

// 이벤트를 선언한다
event IntegersAdded(uint x, uint y, uint result);

function add(uint _x, uint _y) public {
  uint result = _x + _y;
  // 이벤트를 실행하여 앱에게 add 함수가 실행되었음을 알린다:
  IntegersAdded(_x, _y, result);
  return result;
}
YourContract.IntegersAdded(function(error, result) {
  // 결과와 관련된 행동을 취한다
})

이벤트는 나중에 더 깊게 다뤄볼 것이고 일단 챕터1에서는 간단히 보고 넘어간다.

컨트랙트는 특정 이벤트가 일어나는지 "귀를 기울이고" 그 이벤트가 발생하면 행동을 취한다.

 

사용자단의 자바스크립트 코드가 IntegerAdded 이벤트를 듣고있다가 컨트랙트에서 실행되면 행동을 취한다.

 

 

 

 

 

 

https://cryptozombies.io/ko/course

 

#1 Solidity Tutorial & Ethereum Blockchain Programming Course | CryptoZombies

CryptoZombies is The Most Popular, Interactive Solidity Tutorial That Will Help You Learn Blockchain Programming on Ethereum by Building Your Own Fun Game with Zombies — Master Blockchain Development with Web3, Infura, Metamask & Ethereum Smart Contracts

cryptozombies.io

 

무브랭이 뭐고 솔리디티보다 뭐가 나은걸까?

 

 

 

무브언어는 리브라 스마트 컨트랙 프로그래밍을 위해 개발된 언어다

 

리브라는 페이스북이 발행한 결제용 암호화폐 -> 현재는 명칭이 바뀌어 디엠(Diem)이다

 

페이스북은 왜 결제용 암호화폐를 만든걸까? -> 국가가 발행하는 화폐를 대체하고 은행 계좌가 없는 수십억명에게 지불 네트워크를 제공하기 위해ㅇㅇ -> 지갑 이름은 "캘리브라"

 

리브라는 주요국의 법정통화와 채권을 담보로하는 안정된 스테이블코인이라는 강점을 내세웠었다.

 

하지만 각국 규제 당국들은 리브라를 전통 통화에 대한 도전이라고 여기어 강한 규제를 맥여버렸고 리브라는 디엠이 되었다

 

즉, 무브언어는 디엠코인 스마트 컨트랙 프로그래밍을 위해 개발된 언어이다

 

 


 

 

솔리디티랑 뭐가 달라?

 

솔리디티도 많이 안해보았기에.. 체감되는 부분은 없고 글로만 이해해보자면

 

  • 블록체인에서 사용되는 기존 스크립트 언어들의 한계 -> 코인을 매개하는 과정상 단순한 정수형을 사용한다는 점. 코인, 토큰을 규정하는 자료타입이 없다. 근데 무브는 있다(고한다 아직안써봄ㅎㅎ)
  • 무브는 지금까지 발생했던 스마트 컨트랙 관련 사고들을 참고하여 설계되었고 개발자의 의도를 쉽고 정확하게 표현할 수 있기에 예기치 못한 버그를 줄일 수 있다(고 한다)
  • 디지털 실물 자산에 성격을 부여하여 디지털 자산의 주인을 한명으로 규정하고 한번밖에 사용될 수 없도록하며 동일한 자산의 생성을 제한하는 기능도 있다(고한다)
  • 선언된 데이터는 해당 리소스를 선언한 모듈에서만 변경, 삭제할 수 있다(고한다)
  • 무브언어에서 리소스 객체는 소모될 뿐 복사는 되지 않는다(고한다)

 

eth면 eth지 weth는 모지?

 

랩드 토큰(Wrapped Token)이란 한 메인넷 기반 토큰을 다른 메인넷 위에서 사용할 수 있도록 래핑한 것을 의미

 

예를들어 비트코인은 이더리움 네트워크에서 사용할 수 없다.

하지만 비트코인을 래핑하여 WBTC로 만든다면 이더리움 메인넷 위에서 비트코인을 사용할 수 있다.

 

 

랩드 토큰은 중간관리자가 필요하다

 

  • 사용자는 중개인에게 1BTC를 전송한다(담보로 맡긴다)
  • 중개인은 1BTC 만큼의 가치를 가지는 1WBTC를 이더리움 네트워크상에서 생성하여 사용자에게 반환한다
  • 사용자가 중개인에게 보냈던(담보로 맡긴) 1BTC는 일종의 디지털 금고인 래퍼(Wrapper)에 보관
  • 사용자가 1WBTC를 가지고 볼 일을 마치고 다시 중개인에게 1WBTC를 보내면
  • 중개인은 1WBTC를 소각(burn)시키고 1BTC를 돌려준다

 

 


래핑의 장점

  • 트랜잭션 속도와 가스비 절약
    • 비트코인은 느리고 가스비 높지만 WBTC로 래핑하여 이더리움 메인넷에서 사용한다면 이더리움 메인넷의 트잭속도와 가스비를 적용받는다
  • 유동성 증대
    • 유동적으로 메인넷을 오가며 사용가능

 


래핑의 단점

  • 관리자에 대한 신뢰
    • 래핑과정이 완전히 탈중앙화가 이루어지지 않았기에 관리자(중개인)을 거쳐야한다는 리스크(결국 래퍼는 중앙화된 디비)
    • 담보를 보관하는 디지털자산 금고가 해킹당할수도있다
  • 가스비
    • 래핑 후에는 가스비가 적을 수 있지만 래핑과정에서 가스비가 소모됨

 

 

 

출처:

'어질어질 코인판' 카테고리의 다른 글

[Coin] 앱토스?  (0) 2023.02.01

앱토스란?

블록체인이 진정으로 대중화되기 위해서 L1의 보안과 확장성이 개선되어야한다는 문제의식에서 출발한 레이어1 블록체인 프로젝트다

 

가장 안전하고

확장성이 뛰어난 블록체인을 구축하는 것을 목표로 삼고있다

초당 1000건 이상의 빠른 트랜잭션 수를 자랑한다

 

앱토스는 페이스북(메타)의 블록체인 사업인 Diem의 핵심 인력들에 의해 시작되었다

 

앱토스 블록체인의 코드는 러스트기반의 언어인 무브로 작성되었다. 이더리움보다 빠르고 저렴한 레이어1 프로토콜을 지향한다

 

앱토스의 특징

1. 무브언어

토큰 정보와 스마트 계약을 기록할 때 데이터 복사 및 분실이 발생하기 어려운 방식으로 설정돼있다. -> 따라서 이중 지불, 소유권의 중복등 부정이 일어나기 힘들다는 이점이 있다. -> 솔리디티와 비교해도 안전성이 우수하다고 할 수 있다.

 

2. Diem BFT라는 컨센서스 알고리즘 사용

Diem BFT는 HotStuff라는 비잔틴 장애에 내성을 가진 프로토콜을 가지고있다.

 

앱토스의 생태계

 

 

 


 

왜 앱토스?

1. 페이스북(메타)에서 추진하던 블록체인 기반 결제용 자산 Diem을 기반 기술로 하고있다

2.  Diem이 페이스북 유저 30억명을 온보딩 할 수 있는 블록체인이 목표였던 만큼 큰 기대

 

 


블록체인 레이어란?

 

Layer0

데이터 전송과 채굴(표준 프로토콜, 생태계 환경)

블록체인이 서로 상호작용할 수 있도록 네트워크를 형성

 

Layer1

블록체인 플랫폼

비트코인과 이더리움 등

여기서 합의(PoW, PoS등)와 같은 작업과 블록 시간 및 분쟁 해결같은 과정이 발생

 

Layer2

속도와 확장(분산 아키텍처)

확장성과 초당 트랜잭션을 높이기 위해 사용되는 기술

폴리곤, 이더리움 라이트닝등

레이어1에서 처리할 양을 레이어2에서 일부 부담하여 레이어1의 처리부담을 줄여준다

 

Layer3

애플리케이션

소비자로서 우리가 상호작용하는 UI를 가지는 비즈니스 영역

디센트럴랜드, 유니스왑, 크립토키티 등

 

 


비잔틴 장애 허용(BFT)이란?

시스템 내부에 장애가 있더라도 문제가 되는 부분의 1/3을 넘지 않는다면 정상 작동하도록 허용하는 합의 구조

 

비잔틴 장군 문제

어떻게 하면 멀리 떨어져 있는 독립된 사람들이 특정 시점에 특정 행동을 하기전에 서로 의견을 일치할 수 있느냐 라는 문제

 

비잔틴 장애 허용 -> 장부를 유지하는 참여자 중 2/3 이상의 장부가 동일하다면 1/3의 장부가 일치하지 않더라도 2/3의 장부를 공식적으로 인정하는 개념

 

 

 

https://kr.beincrypto.com/learn-kr/%EC%95%B1%ED%86%A0%EC%8A%A4-%EC%95%94%ED%98%B8%ED%99%94%ED%8F%90apt%EC%99%80-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EC%A7%91%EC%A4%91%ED%83%90%EA%B5%AC/

 

앱토스 암호화폐(APT)와 네트워크 집중탐구 

앱토스는 무브 프로그래밍 언어를 사용하여 구축된 레이어1 블록체인이다. 또는 지금은 포기한 메타(Meta)의 디엠 블록체인 프로젝트의 개념적인 후계자라고 부를 수도 있다. 

kr.beincrypto.com

 

'어질어질 코인판' 카테고리의 다른 글

[Coin] 래핑된(Wrapped) 토큰?  (1) 2023.02.02


1. 느슨한 결합을 위한 노력

 

예시 정책.

1. 회원 도메인과 가족계정 도메인이 있다.

2. 회원의 인증정보가 초기화되면 가족계정에서 탈퇴된다.

 

 

 

1단계. 모놀리틱한 구조(그저 동기적인 코드로 구현)

회원의 인증이 초기화되면 -> 가족계정 서비스에서 탈퇴된다.

 

행위의 주체는 회원이다.

즉, 회원 본인인증 해제 로직과 가족계정 탈퇴 로직이 강한 결합을 이루고 있다.

 

 

 

 

 

2단계. 시스템의 물리적인 분리

유튜브 영상을 캡쳐해 온 것이라 발표자님 얼굴이 나온다ㅠ 출처는 밑에 정리해 두었다.

물리적으로는 분리되었지만 코드레벨의 호출이 HTTP 요청에 의한 호출로 변경되었을 뿐 여전히 강한 결합이다.

 

 

 

 

 

3. 비동기 HTTP 요청

비동기로 구현된 HTTP 요청은 쓰레드레벨의 의존을 제거할 뿐 여전히 강한 결합이다.

 

 

 

 

 

4. 메시지 큐를 사용한 요청

 

 

회원 인증 초기화시 가족계정 탈퇴를 요구하는 메시지를 발행함으로 물리적인 결합은 제거되었지만 논리적으로는 강하게 결합된 상태다.

회원인증을 담당하는 도메인에서 가족계정을 담당하는 도메인에서 해야할 작업을 명시하고 있으니까 말이다.

 

주의해야할 것.

요청을 위해 발행한 메시지가 대상 도메인에게 바라는 행위를 담은 메시지라면 그저 비동기 구조일 뿐,

우리가 다루고자 하는 이벤트가 아니다.

 

.

.

.

.

.

.

.

 

5. 드디어 느슨한 결합

메시지큐에 똑같이 메시지를 발행하였지만 4단계와 다른점이 있다면

회원 도메인에서 발행한 메시지는 가족계정의 탈퇴를 지시하는 메시지가 아니라 그저 "회원인증 초기화"를 알리는 메시지라는 점이다.

 

 

 

출처:

https://youtu.be/b65zIH7sDug

캡쳐본에서는 얼굴 가렸는데 썸네일에 대문짝만하게 나오네ㅋㅋ

 

 

 

 

 


2. 이벤트란?

여기서 말하는 이벤트는 다음과 같다.

밥을 먹고 설거지를 하는 서비스라면

밥을 먹고나서 "설거지를 하라"는 이벤트를 발행하는 것이 아닌

"밥을 먹었다"라는 이벤트를 발행하는 것이다.

 

 

다른 예시로,

 

 

경찰관이 자동차에게 "직진하라"는 이벤트를 발행한다면,

 

 

 

 

 

이렇게 차들이 많아지면 이벤트를 발행하는 경찰관에게는 부하가 걸리게 된다. 차 하나하나마다 발행을 해줘야하기 때문

 

 

 

 

하지만, 신호등은 우리에게 이벤트를 발행하지 않는다. 정확히 말하면 경찰관처럼 우리에게 직진하라고 지시하지 않는다.

 

그저 지금이 빨간불인지 파란불인지만 알려줄 뿐 자동차가 신호등을 관찰하다가 판단한다.

 

 

 

 

따라서 신호등은 자신의 상태만 메시지로 발행하고 후속 행동은 이를 관찰하는 자동차들이 판단하여 처리한다.

한마디로 줄여서

 

"주목할 만한 상태를 이벤트로 발행한다"

 

고 보면 될 것같다.

 

 

 

AWS에서 발생하는 거의 모든 동작은 이벤트를 발생시킨다. 다음은 EC2의 생명주기 변경 이벤트이다.

 

 

 

출처:

https://youtu.be/4aoKv545ViA

 

 

 

 

 


3. 이벤트 드리븐 아키텍쳐의 이모저모

 

이벤트 드리븐 아키텍쳐의 제약사항

  • 이벤트는 발생한 사건에 대해 약속된 형식 사용
  • 불변 : 과거의 메시지를 변경할 수 없음
  • 발생한 사건에 대한 결과 상태 전달
  • 생성자는 이벤트를 누가 처리했는지 이벤트 처리 상태에 관여하지 않음
  • 소비자는 이벤트를 누가 생성했는지, 생성자에 관심 갖지 않음

 

 

이벤트 드리븐 아키텍쳐 설계시 고려사항

Loosely coupling
  • 한 모듈이 따른 모듈에 너무 종속적이면 안된다.
  • 고려사항이라고는 하지만 비동기적인 프로그램을 하기 위해선 사실상 필수 사항

 

Removing dependencies 
  • 의존성을 제거
  • 의존성을 제거 해서 Loosely한 coupling 상태로 만들어야 함

 

Nonblocking transaction
  • 트랜잭션 단계에서 서로 영향을 주지 않도록 해야함.
  • 회원가입을 할때, 핸드폰 인증을 하고 이메일 인증을 순차적으로 진행을 하는데 이메일 인증에서 오류가 났다고 해서 핸드폰 인증까지 같이 취소가 되면 안됨

 

Asynchronized callback
  • 비동기 콜백은 필수는 아님
  • 그러나 사용의 편리성으로 가장 많이 사용

 

Fallback /Retry
  • 실패 했을 경우 어떻게 하고 어디까지 돌아가서 재시도 할것인가를 구분 지을 필요 있음
  • 딱히 정해진 방식이 없어서 설계자의 능력이 중요

 

Event logging
  • 비동기는 로그가 중구난방으로 찍히기 때문에, 이벤트 로그가 잘 찍히게 설계해야함.

 

 

 

 

이벤트 드리븐 아키텍쳐의 장단점

특징과 장점
  • Sevice간 호출이 많은 Microservice Architecture에 적합.
  • Infra Structure의 유연한 사용
  • Polyglot 구성에 용이 -> 모듈별로 서로 다른 언어와 DB 사용 가능
  • 가용성, 응답성 ↑ -> 효율성, 성능 ↑ -> 요청이 많아질 수록 성능 향상 기대
  • Fault Isolation : 에러가 생겨도 각각의 모듈에 독립적으로 생기기 때문에 다른 모듈은 정상 가동이 가능

 

 

단점
  • 설계 복잡도, 운영 복잡도 증가
  • Message Broker 의존성 증가 ->Fault Isolation의 장점이 있어도 모든 Message가 모이는 Message Broker 자체가 고장나면 시스템이 멈춰버림
  • 코드 가독성 하락 -> 눈에 익은 코드가 아니므로 코드 읽기가 어려움
  • 디버깅 난이도 상승 -> 스텝투스텝으로 동기적으로 따라갈 수가 없고, 중구난방으로 로그가 찍히기 때문에 어디서 고장이 났는지 파악하기 힘듬
  • log를 통한 system flow 가독성 하락

 

 

출처: https://be-developer.tistory.com/37

 

[빗썸테크아카데미] 첫번째 - OT, 동기/비동기와 Event-driven Architecture

[빗썸테크아카데미] 첫번째 - OT, 동기/비동기와 Event-driven Architecture https://be-developer.tistory.com/34 [빗썸 테크 아카데미] BE 심화 과정 최종 합격 후기 >< (+기술과제) [빗썸 테크 아카데미] BE 심화 과정

be-developer.tistory.com

 


1. 객체 정렬?

class Person {
    String name;
    int weight;
    int height;

    public Person(String name, int height, int weight) {
        this.name = name;
        this.weight = weight;
        this.height = height;
    }

    public void print() {
        System.out.println(name + " " + height + " " + weight);
    }

    public int getWeight() {
        return weight;
    }

    public int getHeight() {
        return height;
    }
}

여기 Person 클래스가 있다.

 

 

List<Person> list = new ArrayList<>();

Person 클래스로 이루어진 어레이리스트도 있다.

 

이 리스트를 정렬하고하자한다.

name을 기준으로 정렬을 해야할까 weight를 기준으로 정렬을 해야할까?

우리는 이 문제를 Comparable이나 Comparator를 통해서 해결할 수 있다.

 

 

 

 


1. Comparable

정렬하고자 하는 객체의 클래스에 Comparable 인터페이스를 구현하는 방법이다.

물론 그 클래스에 Comparable 인터페이스를 구현할 수 있다면 말이다.

// 정렬 대상 클레스에 인터페이스를 구현할 수 있다면 Comparable 사용 가능
class Person implements Comparable<Person> {
    String name;
    int weight;
    int height;

    public Person(String name, int height, int weight) {
        this.name = name;
        this.weight = weight;
        this.height = height;
    }

    public void print() {
        System.out.println(name + " " + height + " " + weight);
    }

    public int getWeight() {
        return weight;
    }

    public int getHeight() {
        return height;
    }

    @Override
    public int compareTo(Person p) {
        // 오름차순: this 객체 - 인자로 넘어온 객체
        // 내림차순: 인자로 넘어온 객체 - this 객체
        return this.height - p.height; // 오름차순 정렬
    }
}
Collections.sort(list);

Comparable 인터페이스의 compareTo 메소드를 구현하면 된다.

위 코드는 height를 기준으로 오름차순 정렬의 예시다.

 

 

 

만약 1순위로 height를 기준으로 오름차순 정렬하고

height가 같은 객체가 있다면

2순위로 weight를 기준으로 내림차순 정렬하고 싶다면?

 

class Person implements Comparable<Person> {
    String name;
    int weight;
    int height;

    public Person(String name, int height, int weight) {
        this.name = name;
        this.weight = weight;
        this.height = height;
    }

    public void print() {
        System.out.println(name + " " + height + " " + weight);
    }

    public int getWeight() {
        return weight;
    }

    public int getHeight() {
        return height;
    }

    @Override
    public int compareTo(Person p) {
        // this 객체 > 인자로 넘어온 객체 => return 1이라는것은
        // this 객체 - 인자로 넘어온 객체 > 0 => 오름차순
        if (this.height > p.height) return 1;
        else if (this.height == p.height) { // height가 같다면
            // this 객체 < 인자로 넘어온 객체 => return 1이라면
            // this 객체 - 인자로 넘어온 객체 < 0 => 내림차순
            if (this.weight < p.weight) return 1; // weight를 내림차순으로
        }
        return -1;
    }
}

이렇게 하면 된다.

 

 

 

 


2. Comparator

정렬하고자 하는 객체의 클래스에 Comparable 인터페이스를 구현할 수 없을때 사용하는 방법이다.

혹은 Comprable 인터페이스를 통해 이미 정렬기준이 정해져있지만 다른 기준으로 정렬하고 싶을때 사용하는 방법이다.

 

// 정렬 대상 클레스에 인터페이스를 구현할 수 없다면
// 혹은 Comparable을 통해 이미 정해져있는 정렬 기준과 다르게 정렬하고 싶다면
class Person {
    String name;
    int weight;
    int height;

    public Person(String name, int height, int weight) {
        this.name = name;
        this.weight = weight;
        this.height = height;
    }

    public void print() {
        System.out.println(name + " " + height + " " + weight);
    }

    public int getWeight() {
        return weight;
    }

    public int getHeight() {
        return height;
    }
}

Person 클래스는 동일하다.

 

 

 

우리는 Comparator라는 일종의 정렬기준을 정의한 객체를 만들어서 Collections.sort()의 인자로 넣어주어야 한다.

 

    public static <T> void sort(List<T> list, Comparator<? super T> c) {
        list.sort(c);
    }

 

Collections.sort 함수를 까보면 첫번째로 정렬 할 리스트와 두번째로 정렬 기준인 Comparator를 받는 것을 볼 수 있다.

 

 

 

Collections.sort(list, new Comparator<Person>() {
    @Override
    public int compare(Person o1, Person o2) {
    	// 오름차순
        return o1.height - o2.height;
    }
});

 

Comparator객체를 한번쓰고 버릴것 이니 익명클래스로 넣어주었다.

저 익명클래스의 객체는 Comparator 인터페이스를 구현받고있기에 compare 함수를 오버라이드하고있다.

 

 

 

정렬 기준이 2개 이상일 때도 Comparable과 동일하게 해주면 된다.

Collections.sort(list, new Comparator<Person>() {
    @Override
    public int compare(Person o1, Person o2) {
    	// height 기준 오름차순
        if (o1.height > o2.height) return 1;
        else if (o1.height == o2.height) {
        	// height가 같다면 weight 기준으로 내림차순
            if (o1.weight < o2.weight) return 1;
        }
        return -1;
    }
});

 

 

 

Comparator를 람다함수로 조금더 간단하게 표현할 수 있다.

Collections.sort(list, (a, b) -> a.getHeight() - b.getHeight());

 

 

 

stream을 쓰면 더더더 간단하게 표현할 수 있다.

list.sort(Comparator.comparing(Person::getHeight));

 

 

 

stream을 사용했을때 2개 이상의 기준을 적용하고 싶다면

list.sort(Comparator.comparing(Person::getHeight).thenComparing(Person::getWeight));

 

이렇게 thenComparing을 통해 이어주면 된다.

하나의 조건은 역순으로 하고싶다면 .reversed()를 붙혀주면 되는데 주의 할 것이

 

 

 

list.sort(Comparator.comparing(Person::getHeight).thenComparing(Person::getWeight).reversed());

 

이렇게 뒤에 reversed()를 바로 붙이면 height에 대한 정렬도 reversed()에 영향을 받아서 두가지 기준 다 내림차순 정렬이 돼 버린다.

 

 

 

두번째 기준만 내림차순으로 하고 싶다면

Comparator<Person> reverseCompare = Comparator.comparing(Person::getWeight).reversed();
list.sort(Comparator.comparing(Person::getHeight).thenComparing(reverseCompare));

 

이렇게 reverse용 Comparator를 따로 만들어서 넣어주어야 한다.

 

 

 

 

참고:

 

'Java' 카테고리의 다른 글

[Java] 해시/해시테이블이란?  (0) 2021.07.12
[Java] 제네릭(Generic)  (0) 2021.07.11
[Java] Garbage Collection  (4) 2021.07.01
[Java] Thread/MultiThread 4 - 동시성 문제  (0) 2021.06.29
[Java] Static 키워드  (0) 2021.06.28

혹시 제가 사용한 방법보다 더 간단하고 좋은 방법이 있다면 훈수 부탁드립니다.


1. 마주한 문제

클레이튼 기반의 dapp을 개발하며 블록체인의 트랜잭션과 서비스의 데이터베이스간의 싱크를 보장해야하는 상황을 마주했다.

 

 

성공한 트랜잭션이면 관련 정보를 데이터베이스에 반영해주고,

실패한 트랜잭션이면 관련 정보를 데이터베이스에 반영해주지 않는, 그런 요구사항이었다.

 

 

클레이튼 블록체인에서 자체적으로 실패한 트랜잭션에 대한 롤백은 시켜주기에 클레이튼의 성공여부를 먼저 확인하고 데이터베이스 반영 여부를 정해야했다. [클레이튼 트랜잭션] -> [데이터베이스 트랜잭션]의 플로우였기에 이 사이에 트랜잭션의 성공여부만 확인하고 데이터베이스의 트랜잭션을 발생시킬지 말지 결정하면 됐다. 

 

[클레이튼 트랜잭션] -> [트랜잭션이 성공했는지 확인] -> [데이터베이스 트랜잭션]

 

하지만 블록체인은 트랜잭션이 확정되기까지 걸리는 시간인 Finality라는 개념이 있기에 트랜잭션의 성공 여부를 조회하려면 이  Finality 시간 이후에 해야했다. 클레이튼의 경우 이 시간이 평균 1초라고 알려져있다. 평균이기에 실제 상황에서 약간의 오차는 있을 것 이다. 따라서 Finality만큼의 딜레이를 하고 트랜잭션 조회 -> 데이터베이스 트랜잭션 실행의 절차를 밟아야 했다. 

 

 

즉, 필요한 조건은

 

1. 트랜잭션 조회에 delay를 줘야한다. -> Finality 때문에

2. 적어도 2회 이상 retry를 하며 조회해야한다. -> Finality 시간이 일정하지 않기 때문에

3. 최대한 빠르게 트랜잭션의 성공 여부를 판단해야한다. -> 원활한 서비스 제공을 위해

 

 

 

 


2. 고려했던 선택지

세가지를 고려했었다. 참고로 NodeJS로 개발했다.

 

1. 스케줄러를 통해 짧은 간격으로 확정되지 않은 트랜잭션들의 성공 여부를 조회하기

  1. 트랜잭션을 발생시킬 때 transaction_history 테이블에 트랜잭션의 발생 여부를 기록해두고 is_confirmed는 false로 둔다.
  2. NodeJS의 스케줄러를 통해서 is_confirmed가 false인 기록들을 짧은 간격으로 주기적으로 조회하며 트랜잭션의 성공 여부를 확인한다.(트랜잭션 성공여부는 caverExtKAS 라이브러리의 getTransferHistoryByTxHash 함수를 사용했다)

 

문제점.

트랜잭션이 발생하지 않을때도 몇 초마다 항상 실행되기에 리소스 낭비라고 생각됐다.

 

 

 

2. 클레이튼 이벤트 리스너를 통해 트랜잭션이 수행될 때 발생되는 이벤트를 추적하여 판별하기

  1. 트랜잭션을 발생시킬 때 transaction_history 테이블에 트랜잭션의 발생 여부를 기록해두고 is_confirmed는 false로 둔다.
  2. 트랜잭션이 성공하면 열어둔 소켓을 통해 클레이튼 이벤트를 Listen할 수 있는데 트랜잭션이 성공하면 이벤트가 수신되므로(아마..?) 수신했다는 것은 해당 트랜잭션이 성공했다는 의미.

 

문제점.

가끔 성공한 트랜잭션의 이벤트가 리슨되지 않을때가 있었다. (왜일까..)

 

 

 

3. RabbitMQ를 통해서 딜레이와 재시도 기능을 구현하여 조회하기 (이 방법을 사용했다)

  1. 트랜잭션을 발생시킬 때 transaction_history 테이블에 트랜잭션의 발생 여부를 기록해두고 is_confirmed는 false로 둔다. 또 동시에 RabbitMQ로 발생한 트랜잭션의 정보를 publish한다.
  2. 컨슈머에서 이를 구독하고 이 시점에 트랜잭션 성공 여부를 조회한다.
  3. 트랜잭션이 성공이면 데이터베이스에도 관련 정보를 반영 후 ack으로 마무리하고, 실패면 retry_count를 1추가해서 DLX로 보낸다. 이곳에서 5000ms동안 대기하고 다시 작업큐로 메시지를 보내서 반복한다.(넉넉히 5초로 딜레이를 주었다)
  4. 3번에서 반복 횟수동안 모두 트랜잭션이 확정이 안됐거나 실패 판정이 나면 데이터베이스에 관련 정보를 반영하지 않고 ack을 반환한다.

 

이 방법을 선택한 이유

  • RabbitMQ를 통해서 사용자가 몰릴때 비동기적으로 안정적인 로직 수행이 가능하기에
  • RabbitMQ의 Durability를 통해 중요한 정보를 잃어버리지 않을 수 있기에
  • RabbitMQ의 DLX 기능을 통해 딜레이와 재시도를 구현할 수 있기에

 

 

 

 


3. RabbitMQ 구성

RabbitMQ는 위와같이 구성해보았다.

 

[1], [2]

클레이튼 트랜잭션을 발생시킴과 동시에 트랜잭션의 정보와 retry_count를 담은 메시지를 publish한다.

 

[3], [4]

지정한 익스체인지에서 지정한 큐로 메시지를 전달한다.

 

[5]

트랜잭션의 성공 여부를 확인하는 컨슈머에서 트랜잭션의 성공 여부를 확인한다.

성공이라면 서비스 로직을 실행하고 ack을 반환한다.

 

[6]

트랜잭션이 아직 확정되지 않았거나 실패했다면 WAITING 익스체인지로 같은 메시지를 그대로 전달한다.

이때마다 retry_count에 1 씩 더해서 전달한다.

 

[7]

전달된 메시지는 TX.Q.WAIT_CONFIRM_CHECK 큐로 보내지는데 이 큐는 DLX와 TTL 설정이 돼있다.

 

[8]

지정한 TTL 시간이 지나면 메시지가 x-dead-letter-exchange로 지정된 익스체인지로 x-dead-letter-routing-key 바인딩 키를 사용해서 전달된다.

즉, 이 큐에서 5000ms 동안 대기하고 TX.E.DIRECT 익스체인지로 가게된다.

이때 바인딩키는 TX를 가지고간다.

TX는 TX.E.DIRECT와 TX.Q.CONFIRM_CHECK의 바인딩키다.

따라서 TX.E.DIRECT 익스체인지에 도착한 메시지는 TX.Q.CONFIRM_CHECK 큐로 다시 보내진다.

 

 

 

[4], [5], [6], [7], [8]의 과정을 최대 3번까지 반복하고도 트랜잭션이 실패라고 판별되면 최종적으로 실패처리(데이터베이스에 반영하지 않기)를 한 후 ack를 반환한다.

 

 

 

DLX의 개념과 동작방식은 아래 아저씨가 쉽게 설명해준다!

https://www.youtube.com/watch?v=ovE8NKAwqTI&ab_channel=MikeM%C3%B8llerNielsen 

 

 

 

 


4. 세부 구성

메시지는 

[1] -> [2]에서 5000ms 대기 -> [3] -> [4]의 순서로 움직인다.

 

 

 

 

 

후기.

뭔가 긴 고민끝에 찾은 방법이지만 결국은 기본적인 RabbitMQ의 활용법이었다!

RabbitMQ가 참 유용한 도구임을 새삼스래 느꼈던 경험이었다.

 

 

참고:

더보기

오늘의 강의는 뭘까~용


1. 스마트 컨트랙 코드는 실행할때 돈이 든다. 

왜?

나의 트랜잭션을 블록에 넣어줄 마이너는 땅파서 컴퓨터를 돌려주는게 아니기때문.

 

얼마나 들까?

밑에서 대~강 구해봤다.

 

왜 알아야하나?

알아야 나중에 코딩할때 더 값싼 방법으로 코딩할 수 있으니까

 

가스비는 일반적으로 gwei로 표시되며, 이는 ETH의 10억분의 1(0.000000001 ETH)이다

 

이더리움의 수수료

수수료 = gas(gasLimit) * gasPrice

 

예시)

수수료 = gas(21000) * gasPrice(1gwei)

수수료 = 21000000000000gwei

이때, 1ETH = 10 ** 18wei이니까

 

수수료 = 0.00021ETH 가 된다.

 

gas와 gasPrice는 다음과 같은 차이점이 있다.

  • gas(gasLimit) : 본 송금 '작업'에서 소비되는 가스량
    (estimated 한 수치라서 변경가능하다. 하지만 넘 작게하면 거부됨)
  • gasPrice : 내가 가스당 지불할 가격
    (경매처럼 내가 금액을 제안하는 것임)

 

MAX TOTAL : gas * gasPrice 로 형성된 최종지불금액

gasPrice를 높게 잡을수록 이더리움 블록체인에서 마이너들이 내 트랜잭션을 빠르게 채굴해준다.

 

 

 

 

확인해보자

이더스캔에서 아무 트랜잭션이나 한번 봐보자

 

Transaction Fee(0.000739157072325) = gas(21000) * gasPrice(0.000000035197955825)

임을 확인할 수 있다.

 

Gas Limit & Usage by Txn은 뭐냐?

Gas Limit(앞에서 말한 gas와 동일)은 내가 이 값 이상으로는 가스비를 내지 않을 것이다라는 최대한도

Usage는 실제로 사용된 가스비를 의미

이 예시의 경우는 Limit의 100%를 사용했다.

 

넉넉하게 Gas Limit을 주면 알아서 최적의 가스비로 지불이 된다.

너무 GasLimit을 낮게주면 트랜잭션이 실패되니 넉넉하게 주자

 

 

 

 


2. 우리가 작성한 스마트컨트랙이 사용할 Gas를 대~강 계산해보자

가스비가 많이드는 연산: 블록체인의 용량을 늘리는 연산(저장하는 연산)

 

 

참고로 한 블록에서 사용가능한 가스비가 정해져있기에(Block Gas Limit) 무제한으로 용량을 늘리는(정보를 저장하는) 연산을 할 수 없다.

 

이 블록안에 있는 모든 트랜잭션 들이 사용할 수 있는 gas limit은 30,029,295이다.

이 값을 초과할 정도의 연산은 블록에 들어갈 수 없다.

 

이더리움의 가스비는 다음과 같이 정해져있다

  • 32Bytes 저장에 20000gas가 든다
  • 32Bytes 기존 변수에 있는 값을 바꿀때 5000gas
  • 기존변수를 초기화해서 더 이상 쓰지 않을때 10000gas return

 

 

Lottery.sol의 가스비를 파헤쳐보자

우리가 만든 Bet함수를 실행할 때 발생하는 가스비를 계산해보자

truffle console을 통해서 Bet함수를 실행시켜보았고 90846gas가 발생했다

 

 

연속으로 한번 더 실행하면 75846gas가 발생한다.

왜 갑자기 적어졌지? 끝에서 설명하겠다.

 

 

 

 

일단 우리가 작성한 코드를 보자

 

  function bet(bytes challenges) public payable returns (bool result) {
    // 돈이 제대로 왔는지 확인
    // require는 if역할, msg.value는 컨트랙트가 받은금액, 문자열은 조건이 false때 출력할 문구
    require(msg.value == BET_AMOUNT, 'Not Enough ETH');

    // 큐에 베팅정보를 넣기
    require(pushBet(challenges), 'Fail to add a new Bew Info');

    // 이벤트 로그를 찍는다
    // (몇번째 배팅인지, 누가 배팅했는지, 얼마 배팅했는지, 어떤글자로 베팅했는지, 어떤 블록에 정답이 있는지)
    emit BET(_tail - 1, msg.sender, msg.value, challenges, block.number + BET_BLOCK_INTERVAL);
    return true;
  }

 

이러한 Bet함수에서 pushBet을 호출하는 부분이벤트를 emit하는 부분에서 가스비가 든다.

즉, 첫번째 발생한 90846가스로 계산을 시작해보면

90846 = 기본가스(21000) + pushBet(???가스) + emit(대략 5000gas)

 

 

자 그러면 pushBet함수를 보자

  function pushBet(bytes challenges) internal returns (bool) {
    BetInfo memory b; 
    b.bettor = msg.sender; // address형 변수 사용 (1)
    b.answerBlockNumber = block.number + BET_BLOCK_INTERVAL; // uint256형 변수 사용 (2)
    b.challenges = challenges; // bytes형 변수 사용 (3)

    _bets[_tail] = b; // 그닥 크지 않다 (4)
    _tail++; // 32Byte형 변수 사용(5)

    return true;
  }

(1)번은 address형 변수를 사용했으므로 20Byte를 사용한 것

(3)번은 Byte형 변수를 사용했으므로 1Byte를 사용한 것

이더리움 블록체인에서 사용하는 기본저장단위는 32Byte다

 

즉, 20Byte를 사용했든 1Byte를 사용했든 둘 다 32Byte를 사용한 것이므로 64Byte를 사용한 것이 맞으나

작은 단위의 바이트에 대해서는 쪼개서(?) 연산해준다고 한다.

따라서 (1)번과 (3)번 합쳐서 32Byte를 사용한 셈 치면 된다.

(1) + (3) = 20000gas (32Byte)

 

(2)번은 uint256형 변수를 사용했으므로 32Byte 사용한 것

(2) = 20000gas (32Byte)

(4) = 무시

(5) = 20000gas (32Byte)

 

 

즉, pushBet에서 발생한 gas는 (1) + (2) + (3) + (4) + (5) = 60000gas

 

 

90846 = 기본가스(21000) + pushBet(60000gas가스) + emit(대략 5000gas)

90846 = 86000gas + 기타 자잘한 연산들

 

이라고 할 수 있다.

 

그러면 왜 2번째 실행했을때는 75846gas만 발생했느냐?

 

32Bytes 기존 변수에 있는 값을 바꿀때 5000gas가 들기때문이다.

  function pushBet(bytes challenges) internal returns (bool) {
    BetInfo memory b; 
    b.bettor = msg.sender; // 함수 호출한 사람
    b.answerBlockNumber = block.number + BET_BLOCK_INTERVAL; 
    b.challenges = challenges; 

    _bets[_tail] = b;
    _tail++; // 초기 0에서 1되는 것은 20000gas지만 1에서 2되는 것은 수정이므로 5000gas만 든다

    return true;
  }

즉 첫번째 실행보다 15000gas가 덜 발생한다.

90846 - 15000 = 75846

대강 맞는것을 확인할 수 있다.

+ Recent posts