솔리디티에서 구조체에 데이터를 담을 그릇을 만들 때 mapping
과 array
를 사용한다.
결론부터 말하면 트랜젝션 비용의 절감과 빠른 처리를 위해 가능한 mapping을 사용해야 한다.
///@notice When Contract be ended, ReadOnly
bool isContractRun;
///@notice non kicked user size
uint distributableDeositUserSize;
///@notice Response Msg for ABI server
string response_success_msg = "200_OK";
string response_post_success_msg = "201_OK";
string response_fail_msg = "400_FAIL";
///@notice Suite deposit object for control
SuiteDeposit suiteDeposit;
///@notice Suite company wallet address identifier
address _owner;
constructor() {
_owner = msg.sender;
isContractRun = true;
}
/**
* @notice Set status of group
* @enum:pending -> recruit study group member status & not all payed group deposit
* @enum:start -> study group start after checking all guests pay the deposit
* @enum:end -> study group end when presetted time peroid over, in this status
* study group guest and host can pay back their deposit
*/
enum GroupStatus {
pending,
start,
end
}
/**
* @notice The Suite Finance Info
* @key:deposit_balance -> unit (원)
* @key:suiteFinance -> finance flow log when profit occured
*/
struct SuiteDeposit {
uint deposit_balance;
SuiteFinance[] suiteFinance;
}
struct SuiteFinance {
string group_id;
string payer_id;
string payed_reason;
uint payed_amount;
uint timestamp;
}
/**
* @notice Study group members' deposit payment struct
* @key:deposit_amount -> unit (원)
* @key:payment_timestamp -> unit(unix time)
*/
struct Deposit {
string deposit_payer_id;
uint deposit_amount;
uint payment_timestamp;
bool kicked_flag;
}
/**
* @notice Study group contract details
* @key:group_capacity -> unit (명)
* @key:group_deposit_per_person -> unit (원)
* @key:group_period -> unit (unix time)
* @key:recruitment_period -> uint (unix time)
* @key:minimum_attendance -> uint
* @key: minimum_mission_completion -> uint
* @enum:GroupStatus
*/
struct GroupContract {
string leader_id;
string group_id;
uint group_capacity;
uint group_deposit_per_person;
uint group_deadline;
uint recruitment_period;
uint minimum_attendance;
uint minimum_mission_completion;
GroupStatus groupStatus;
}
///@notice Study Group Contract object for control
GroupContract groupContract;
/**
* @notice Study groups Deposits Finance Info are handled by studyGroupDeposits Array
*/
Deposit[] studyGroupDeposits;
/**
* @notice After stop the Study group, Final Study groups Deposits Balance and This Deposit[] only wrote one time.
*/
Deposit[] finalStudyGroupDeposits;
//@notice 트랜젝션 요청자는 반드시 suite 관계자여야만 합니다.
address _owner;
string private SUITE = "SUITE";
uint private CONTRACT_ID;
//@notice 스위트룸 별 계약서 구조체
struct GroupContract {
string leader_id;
string group_title;
uint group_capacity;
uint group_deposit_per_person;
uint group_period;
uint recruitment_period;
uint minimum_attendance;
uint minimum_mission_completion;
bool isRunning;
mapping(string => ParticipantDeposit) participantDeposits; //@key user-service의 member id
FinalGroupDeposit[] finalGroupDeposits;
}
//@notice 스위트룸 별 계약서 반환 구조체
struct GroupContractDto {
string leader_id;
string group_title;
uint group_capacity;
uint group_deposit_per_person;
uint group_period;
uint recruitment_period;
uint minimum_attendance;
uint minimum_mission_completion;
bool isRunning;
}
//@notice 스위트룸 참가자 별 보증금 구조체
struct ParticipantDeposit {
uint deposit_amount;
uint payment_timestamp;
bool kicked_flag;
}
//@notice 최종 정산 보증금 장부
struct FinalGroupDeposit {
string id;
uint deposit_amount;
uint payment_timestamp;
bool kicked_flag;
}
//@key suite-service pk id 와 suite-service-title 의 합 해시값
mapping(string => GroupContract) public groupContracts;
프로젝트를 진행하면서 많은 내용들이 변경됐다.
하나의 트랜젝션이 하나의 API와 대응되던 이전 버전에서는 배치 작업의 처음부터 끝까지 웹서버의 API와 일대일 대응돼 한 사이클이 돌기 위해 600초가 넘는 시간이 걸렸다.
하지만 일련의 공통된 도메인의 작업들을 하나의 트랜젝션으로 묶어버리고 대시보드용 API를 제외하고 2개의 API만 남게 됐다.
서비스 아키텍처가 MSA를 따르고 있기에 각기 다른 서비스마다 트랜젝션이 발생할 때에 보상 트랜젝션을 고려해주어야 했다. 하지만 API의 개수대로 카프카 토픽을 사용해 트렌젝션과 리커버를 진행하다보니 서버리소스가 너무 많이 낭비되는 상황이 왔다.
사실 위 카프카 토픽 관계 명세서도 일련의 비동기 트렌젝션 작업들을 동적 패턴으로 묶어 서비스 서버 API측에서는 한 번의 메시지 프로듀싱으로 작업을 끝내게 해논 구조였다.
하지만 블록체인 특성상 롤백이 불가능한 네트워크이고 신뢰성이 굉장히 중요한 서비스파트였기 때문에 블록체인에 트렌젝션을 날릴 ABI 서버가 많은 과정의 보상 과정을 거쳐야 했다. ( SAGA 패턴의 Choreography )
그러다보니 너무나 많은 리소스가 낭비되고 스마트 컨트랙트 또한 현재 기획 단계에서의 도메인과는 동떨어진 구조를 가지고 있었기에 리팩토링이 필요했다.
결론적으로 사용자 그룹 단위별로 컨트랙트를 배포해주던 이전 방향에서는 mapping을 전혀 사용하지 않았다.
왜냐면 값들이 모두 하나의 컨트랙트에 독립적으로 분리돼있기 때문이였다.
하지만 이는 무차별한 Array의 사용과 단일 책임 원칙에 목 메어 아주 작은 단위로의 트렌젝션의 분할이란 두 개의 문제점이 합쳐져서 매우 많은 비용의 가스비가 사용됐다.
왜인지는 아직도 모르겠으나 컨트랙트를 올리는 것도 실패했는데 0.7MATIC
의 폴리곤 토큰이 사용됐다...
지금 와서 생각을 해보니 배포 과정에 web3.js
를 사용해서 저수준 메소드를 사용해 트렌젝션 하나하나가 최적화가 되지 않았던 것이다. 이를 해결하기 위해 배포를 할 때에 Hardhat
을 사용했다.
컨트렉트를 온체인 시키는데 있어 0.003794995024MATIC
만 소비돼었다.
여러 개의 배치 작업으로 나뉘어져있었던 테스크들을 하나로 묶어서 트랜젝션을 처리함으로써 컨트랙트 내부적으로도 revert
가 발생했을 때 자동 롤백이 되고, 보상 트렌젝션도 쉽게 할 수 있어서 정말 많이 아키텍처 복잡도가 개선됐다고 생각한다.
블록체인 API 서버는 다른 서비스와 다르게 설계 없이 무작정 들어갔었다. 일단 만들고보자라는 생각이였는데 생각보다 많은 리팩토링 비용이 들었고 서비스간 연간관계없이 무작정 코드를 구현하는 것은 매우 위험한 일임을 몸소 깨달았다.