Search

우리 모두 느슨하게 결합해볼까요~?

Created
2024/03/04 05:00
Tags
Backend
Message Queue
Event
Date
2024/03/04
설명
메세지 큐와 이벤트 소싱을 사용해서 어플리케이션 간의 결합도를 낮추어 보자.
카테고리
컴퓨터 과학 ⚙️
제가 회사에 입사해서 처음 본 소스코드의 점진적인 리팩토링을 과정과 시스템을 더 효율적으로 리팩토링한 과정을 블로그에 정리했습니다.

기존 NFT 발행 관련 소스 코드

// NFT 등록 @PostMapping("/register") public JSONObject nftRegister(NFTVO vo) { JSONObject result = new JSONObject(); try { if (commonUtil.nullReplace(vo.getContractName()).equals("")) { result.put("result", commonUtil.result("error", APIException.empty_param.getCode(), "contractName을 넣어주세요")); } else if (commonUtil.nullReplace(vo.getOfficalAddress()).equals("")) { result.put("result", commonUtil.result("error", APIException.empty_param.getCode(), "officalAddress를 넣어주세요")); } else if (commonUtil.nullReplace(vo.getWalletName()).equals("")) { result.put("result", commonUtil.result("error", APIException.empty_param.getCode(), "walletName을 넣어주세요")); } else if (commonUtil.nullReplace(vo.getWalletPw()).equals("")) { result.put("result", commonUtil.result("error", APIException.empty_param.getCode(), "walletPw를 넣어주세요")); } else if (commonUtil.nullReplace(vo.getURL()).equals("")) { result.put("result", commonUtil.result("error", APIException.empty_param.getCode(), "URL을 넣어주세요")); } else if (commonUtil.nullReplace(vo.getDate()).equals("")) { result.put("result", commonUtil.result("error", APIException.empty_param.getCode(), "date를 넣어주세요")); } else if (commonUtil.nullReplace(vo.getPrice()).equals("")) { result.put("result", commonUtil.result("error", APIException.empty_param.getCode(), "price를 넣어주세요")); } else if (commonUtil.nullReplace(vo.getDescription()).equals("")) { result.put("result", commonUtil.result("error", APIException.empty_param.getCode(), "description을 넣어주세요")); } else if (commonUtil.nullReplace(vo.getMode()).equals("")) { result.put("result", commonUtil.result("error", APIException.empty_param.getCode(), "mode를 넣어주세요")); } else if (commonUtil.nullReplace(vo.getCompany_type()).equals("")) { result.put("result", commonUtil.result("error", APIException.empty_param.getCode(), "company_type을 넣어주세요")); } else if (commonUtil.nullReplace(vo.getTransfer_yn()).equals("")) { result.put("result", commonUtil.result("error", APIException.empty_param.getCode(), "transfer_yn을 넣어주세요")); } else if (commonUtil.nullReplace(vo.getType()).equals("")) { result.put("result", commonUtil.result("error", APIException.empty_param.getCode(), "type을 넣어주세요")); } else { log.info("------------------------------------------[nft][register][Start]------------------------------------------"); // note : 22.06.13 description JSON형식으로 들어왔을경우 String 형식으로 재변환 // note : 22.06.15 blockchain에 JSON형식 저장불가. 간단한 설명만 적고 각사 DB에서 데이터 읽어오는걸로 result = nftService.registerNFTService(vo); vo.setWalletPw(""); log.info("[nft][register][REQ_VO] : " + vo); log.info("[nft][register][result] : " + result); log.info("------------------------------------------[nft][register][End]------------------------------------------"); } } catch (Exception e) { e.printStackTrace(); result.put("result", commonUtil.result("error", APIException.internal_server_error.getCode(), e.getMessage())); } return result; }
Java
복사
가독성이 좋지 않으나 데이터가 빈 값인지 여부를 검사하는 것 같습니다.
result = nftService.registerNFTService(vo);
Java
복사
아마도 이 코드가 NFT를 생성하는 메인 로직으로 보입니다. NftService 인터페이스의 구현체를 찾아가봅시다.
@Override public JSONObject registerNFTService(NFTVO nftvo) throws Exception { JSONObject result = new JSONObject(); // company_type 체크 JSONObject companyInfo = chainDao.SELECT_NFT_COMPANY(nftvo.getCompany_type()); // me nft 여부 체크 String me_nft_yn = nftvo.getMe_nft_yn(); // 노드 정보 추출 NodeVO nodevo = new NodeVO(); nodevo.setNode_name(NFT_NODE_NAME); nodevo.setNode_mode(nftvo.getMode()); nodevo.setBlind_yn("N"); List<NodeVO> nodeVOList = chainDao.LIST_CHAIN_NODE(nodevo); nodevo = nodeVOList.get(0); if (companyInfo == null || companyInfo.isEmpty()) { result.put("result", commonUtil.result("error", DBException.company_type.getCode(), "company_type error")); return result; } if (!nftDao.wallet_open(nftvo, nodevo)) { result.put("result", commonUtil.result("error", RPCException.wallet_open.getCode(), "wallet_open error")); return result; } if (!nftDao.wallet_unlock(nftvo, nodevo)) { result.put("result", commonUtil.result("error", RPCException.wallet_unlock.getCode(), "wallet_unlock error")); return result; } // note : 등록 전 잔고체크 JSONObject walletParam = new JSONObject(); walletParam.put("wl_symbol", "TTCOIN"); walletParam.put("wl_account_name", ACCOUNT_NAME + nftvo.getWalletName()); String walletAddr = chainDao.SELECT_WALLET_ADDR(walletParam); ArrayList<LinkedHashMap> beforeBalanceList = nftDao.blockchain_list_address_balances(walletAddr, nodevo); JSONObject beforeBalance = commonUtil.getRpcBalance(beforeBalanceList); // beforeBalance: <TTCOIN, Balance> if (me_nft_yn.equals("Y")) { // note : me nft의 경우 description에 암호화된 정보넣어준다. MeNFTVO meNFTVO = nftvo.getMe_nft_info(); String param = "name=" + meNFTVO.getName() + "||" + "statusMsg=" + meNFTVO.getStatusMsg() + "||" + "sex=" + meNFTVO.getSex() + "||" + "birthYear=" + meNFTVO.getBirthYear() + "||" + "adultYn=" + meNFTVO.getAdultYn() + "||" + "postCode=" + meNFTVO.getPostCode() + "||" + "address=" + meNFTVO.getAddress() + "||" + "phoneNum=" + meNFTVO.getPhoneNum() + "||" + "graduate=" + meNFTVO.getGraduate() + "||" + "job=" + meNFTVO.getJob() + "||" + "marryYn=" + meNFTVO.getMarryYn() + "||" + "property=" + meNFTVO.getProperty() + "||" + "yearlyIncome=" + meNFTVO.getYearlyIncome() + "||" + "ownRealEstate=" + meNFTVO.getOwnRealEstate() + "||" + "ownCar=" + meNFTVO.getOwnCar(); // 발행 wallet name을 MD5 암호화해서 해당키값으로 암호화 String encKey = commonUtil.MD5_Encode(nftvo.getWalletName()); String encodeData = AES256.AES_Encode(param, encKey); nftvo.setDescription(encodeData); } String gluaFilePath = commonUtil.makeGluaFile(nftvo); nftvo.setGluaFilePath(gluaFilePath); String compileFilepath = nftDao.compileContract(nftvo, nodevo); if (compileFilepath.equals("")) { result.put("result", commonUtil.result("error", RPCException.compileContract.getCode(), "contract file compile error")); return result; } nftvo.setGluaCompileFilePath(compileFilepath); String contractId = nftDao.registerContract(nftvo, nodevo); if (contractId.equals("insufficient_funds")) { result.put("result", commonUtil.result("error", RPCException.insufficient_funds.getCode(), "contract 등록에 필요한 지갑잔고가 부족합니다.")); return result; } if (contractId.equals("")) { result.put("result", commonUtil.result("error", RPCException.register_contract.getCode(), "register_contract error")); return result; } nftvo.setContractId(contractId); // call 이후 블록생성 대기. LinkedHashMap getContractInfo = new LinkedHashMap(); int checkCnt = 0; while (checkCnt < CHAIN_COUNT_LIMIT) { Thread.sleep(THREAD_TIME); checkCnt++; getContractInfo = nftDao.get_contract_info(nftvo, nodevo); if ((Boolean) getContractInfo.get("block_check")) { break; } } if (!(Boolean) getContractInfo.get("block_check")) { result.put("result", commonUtil.result("error", RPCException.get_contract_info.getCode(), "get_contract_info error")); } else { // note : 등록 후 잔고체크 ArrayList<LinkedHashMap> afterBalanceList = nftDao.blockchain_list_address_balances(walletAddr, nodevo); JSONObject afterBalance = commonUtil.getRpcBalance(afterBalanceList); BigDecimal beforeTTC = new BigDecimal((String) beforeBalance.get("TTCOIN")); BigDecimal afterTTC = new BigDecimal((String) afterBalance.get("TTCOIN")); BigDecimal fee = beforeTTC.subtract(afterTTC); // fix_me : 22.10.25 노드 동기화 이슈로 무한루프 발생해서 수수료 고정값 처리중 - 김진수 // while (fee.compareTo(EMPTY_FEE) == 0){ int feeCnt = 0; while (feeCnt < 5) { afterBalanceList = nftDao.blockchain_list_address_balances(walletAddr, nodevo); afterBalance = commonUtil.getRpcBalance(afterBalanceList); afterTTC = new BigDecimal((String) afterBalance.get("TTCOIN")); fee = beforeTTC.subtract(afterTTC); feeCnt++; } if (fee.compareTo(EMPTY_FEE) == 0) { fee = REGISTER_FEE; } fee = fee.divide(DIVIDE_VALUE, 5, RoundingMode.HALF_EVEN); nftvo.setFee(fee); // NFT register 결과 DB 저장 chainDao.INSERT_NFT_PRODUCT_INFO(nftvo); JSONObject data = new JSONObject(); data.put("contractId", contractId); data.put("fee", fee); result.put("result", commonUtil.result("success", 200, "성공")); result.put("data", data); } return result; }
Java
복사
코드의 가독성이 좋지 않고 블록체인에 대한 이해가 어려웠지만 간략히 설명하면 다음과 같습니다.
1.
NFT 발행을 위한 노드를 찾습니다.
2.
사용자의 지갑 잔고를 확인합니다.
3.
.glua 파일을 노드 내에 생성하고 컴파일합니다.
4.
NFT 생성 요청에 대한 블록이 블록체인에 만들어질 때까지 대기합니다.
5.
발행이 완료되면 컨트랙트 ID를 확인하고 저장한 후 클라이언트에 반환합니다.

처음 소스코드를 보고 아쉬웠던 점

1.
소스 코드의 가독성이 비교적 좋지 않았습니다.
2.
각 객체의 역할 분리가 되어있지 않았습니다.
3.
책임 분리도 명확하지 않습니다.
4.
Method Extract 기능을 사용해서 조금이라도 분리했으면 하는 아쉬움이 있었습니다

리팩토링 시작

우선 저는 리팩토링 과정을 크게 두 가지로 나누어 보았습니다.
1.
소스코드 리팩토링
2.
프로세스 리팩토링

소스코드 리팩토링

1.
입력 값 검증에 대한 조건 문을 제거
2.
도메인 필드 값이 변경될 수 있는 곳을 제한
소스코드 설계는 눈에 보이는 코드 스멜과 가독성이 낮은 코드를 점진적으로 리팩토링 하는 것입니다.
예를 들면 조건 문으로 검증하던 데이터를 Spring-Validation을 사용해서 줄이는 것이 그 예시입니다.
또한 setter 메서드를 사용해서 변경되던 도메인 데이터를 도메인 내부에서 메서드 단위로 처리하도록 변경했습니다.
기존 소스코드 예시
public void registerNft(){ // blahblah productDataVO.setFee(fee); productDataVO.setPrice(price); productDataVO.setBlah(blah); }
Java
복사
변경 후 예시
public void registerNft(){ // blahblah productDataVO.processProductDetail(someData); } public class ProductDataVO { private BigDecimal fee; private BigDecimal price; private Blah blah; public void processProductDetail(final SomeData data){ this.fee = calculateFee(data.fee); // blahblah } }
Java
복사
이렇게 소스코드가 변경되면 ProductDataVo 라는 클래스의 필드가 변경되는 곳이 명확해집니다.
필드 값의 변경이 명확해지니 그 만큼 가독성과 책임분리가 확실히 좋아진 모습입니다.

프로세스 리팩토링

사실 로직을 보고 가장 먼저 든 생각은 비교적 시간이 오래 걸리는 NFT 발행 작업은 메세지 큐에 넣고 그 메세지 큐를 구독하는 어떤 프로그램이 Queue에 쌓인 NFT 발행 요청을 하나씩 꺼내어 NFT를 발행해주는 방법으로 바꾸고 싶었습니다.
//TODO