오늘 열심히 새로운 프로젝트를 위한 분석설계 작업을 하던 중 옆 동료들과 잠시 머리도 식힐겸 이야기를 하다가 연동 시스템에서 HTTP 요청 시 Spring Boot 기반의 Multi Instance 서버들이 해당 요청을 Broadcast 받은 것 처럼 각 Instance 의 State 들을 업데이트 해야 하는 요건이 생겨 논의하고 있다는걸 알게 되었다.

머리 식히러 왔어서 자리를 피하고 싶었으나.. 최소한의 Effort 로 구현하고 싶고, Message Broker 나 Redis 같은 IMDB, Zookeper 등을 사용하지 않았으면 좋겠다 하고, NoSQL DB 로 MongoDB 를 사용하고 있으나 DB Polling 으로 인해 부하를 주고싶지 않다고 하고..

잠시 생각하던 중 MongoDB 에서 Collection 의 변경이 일어나면 이 이벤트를 Listen 할 수 있는 기능이 생각나서 해당 기능을 사용해서 구현해보는게 어떤가 이야기 했다.

말이 나온김에 ChatGPT 를 통해 Sample Code 를 만들어 보라 시켜봤다.

GPT write a pub sub example springboot java code using mongodb


ChatGPT 놈의 조언

Here is an example of a basic implementation of the publish-subscribe pattern in a Spring Boot application using MongoDB as the message broker:

@Service
public class PublisherService {
    private final MongoTemplate mongoTemplate;

    @Autowired
    public PublisherService(MongoTemplate mongoTemplate) {
        this.mongoTemplate = mongoTemplate;
    }

    public void publishMessage(String message) {
        mongoTemplate.save(new Message(message));
    }
}

@Document
class Message {
    private String message;

    public Message(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

@Service
public class SubscriberService {
    private final MongoTemplate mongoTemplate;

    @Autowired
    public SubscriberService(MongoTemplate mongoTemplate) {
        this.mongoTemplate = mongoTemplate;
    }

    @PostConstruct
    public void subscribe() {
        mongoTemplate.getDb().getCollection("message").watch().forEach((Consumer<ChangeStreamDocument<Document>>) changeStreamDocument -> {
            Message message = mongoTemplate.getConverter().read(Message.class, changeStreamDocument.getFullDocument());
            System.out.println("Received message: " + message.getMessage());
        });
    }
}

이녀석 초반에는 무늬만 pub sub 같은 코드를 내놓더니만 몇번 심도있는(?) 대화를 해주고 난 이후에는 Collection 의 watch() 로 ChangeStreamDocument 를 사용해 Event 를 Listen 하는 제대로 되어보이는 코드를 내어준다. 한번 테스트 해보자.

 

테스트 코드

package mongo.account;

import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Getter
@Setter
@Document(collection = "account")
public class Account {
    @Id
    private String id;
    private String userName;
    private String email;
}
@Repository
public interface AccountRepository extends MongoRepository<Account, String> {
    Account findAccountByUserName(String userName);
    List<Account> findAccountsByUserName(String userName);

    @Query(value = "distinct('userName')", fields = "userName")
    List<String> findDistinct();
}
@Service
@RequiredArgsConstructor
@Slf4j
public class AccountSubscriber {
    private final MongoTemplate mongoTemplate;

    @PostConstruct
    public void subscribe() {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                log.info("Registering Subscriber");
                mongoTemplate.getDb().getCollection("account").watch()
                        .forEach((Consumer<ChangeStreamDocument<Document>>) changeStreamDocument -> {
                            Account account = mongoTemplate.getConverter().read(Account.class, changeStreamDocument.getFullDocument());
                            log.info("[{}] userName : {}", changeStreamDocument.getOperationType(), account!=null?account.getUserName():null);
                        });
                log.info("Subscriber Registered");
            }
        });
        t.start();
    }
}
@Bean
    @Order(1)
    public ApplicationRunner applicationRunner() {
        return args -> {
            Account account = new Account();
            account.setUserName("joon");
            account.setEmail("abcdef@naver.com");

            accountRepository.insert(account);
            log.info("Done");

            List<Account> result = accountRepository.findAccountsByUserName("joon");
            log.info("result 1 : {}", result.size());


            // .... 생략 ...

            accountRepository.deleteAll();

        };
    }

 

실행결과

 

오늘도 chatGPT 가 나름 참고할만한 코드를 잘 만들어주었다. 

물론 그대로 갖다 붙여넣는다고 원하는 동작을 하지는 못한다 (뭐 이러면 개발자 죽어야지..). watch() 에서 blocking 되기땜에 thread 로 빼주는 코드를 추가해주는 센스까지는 아직 chatGPT 가 못내주기는 하다마는 그래도 이게 어디냐..

구글링 해서 찾을수도 있지만 chatGPT 를 썼을 때에는 좀더 "덜 읽어도" 된다는게 좋은것 같다

반응형
블로그 이미지

Good Joon

IT Professionalist Since 1999

,
요즘 ChatGPT 와 시간내서 노는게 참 재미있다. 이제야 SF 영화에서 보던 AI 라고 할 수 있을만한게 나온것 같다는 생각이 든다.

 작년부터 회사에서 AICC 솔루션 얘기가 나오고 있었다. SWA 시절 부터 Aka*** 회사에서의 Solutions Architect 시절 등 나야 솔루션을 만들거나 거대한 솔루션을 각종 업무 도메인과 각색의 아키텍쳐를 갖는 시스템들과 연계하던 경험이 있으나 현재 소속해있는 우리 팀은 사실 솔루션에 대한 경험이 있는 사람이 적고 그나마 이번 프로젝트에 involve 된 사람은 전무한 상황이다.

 나도 서비스 개발을 지속하다 보니 예전 솔루션 개발 방법론까지 Tailoring 해가며 개발해왔던 경험도 기억이 가물한 상황에서 술한잔 먹고 chatGPT 와 대화를 해봤다.

 우리회사에 AI 기술은 있으나, 이걸 AICC 솔루션으로 만들기 위해 우리에게 필요한게 뭐가 있을까?  기특한 GPT 가 답을 해준다.


If you and your team have experience with AI technologies but have not built an AI-powered contact center solution before, there are a few things you may need to consider:

  1. Domain expertise: Having a deep understanding of the domain of contact center operations will be important to ensure that your solution addresses the specific needs of your customers and meets the requirements of your business.
  2. Business understanding: A good understanding of the business side of contact center operations will be important to ensure that your solution is cost-effective, scalable, and able to integrate with existing systems and processes.
  3. Project management skills: Building an AI-powered contact center solution will likely be a complex and time-consuming project, so it will be important to have project management skills and experience to ensure that the project stays on track and meets its deadlines.
  4. Data expertise: Having experience working with large datasets and data science tools will be important for collecting, cleaning, and preparing data for training your models.
  5. Software development skills: Experience in software development will be important for designing and implementing the system, including the integration with existing systems, and creating an API to access the AI models.
  6. Support and maintenance: Building a robust and maintainable system is important, and that means having the capacity to support and maintain the system after it's deployed, this includes monitoring and fine-tuning the system to improve its performance and adapt it to new requirements.
  7. Legal and ethical considerations: As mentioned before, AI-based contact center solutions may raise legal and ethical concerns, such as data privacy and bias. It's important to consider these issues and take appropriate measures to mitigate them.
  8. User experience design: having a good understanding of user experience design will be important to make sure that the system is easy to use and understand, this will help to improve customer satisfaction.

Building an AI-powered contact center solution is not an easy task and will require a combination of skills and expertise from different areas. Depending on the specific needs of your solution, you may need to bring in additional experts or resources to help with the development.


이쁜녀석.. 이렇게 체계적으로 말을 잘 만들어서 대답을 해주니 얼마나 좋은가? 내가 CEO 이고 의사결정자라면 이녀석 참 쓸데가 많을것 같다.

1. 도메인 전문지식

솔루션이 고객의 특정 요구 사항을 해결하고 비즈니스 요구 사항을 충족하는지 확인하려면 컨택 센터 운영 도메인을 깊이 이해하는 것이 중요합니다.

2. 비즈니스 이해

솔루션이 비용 효율적이고 확장 가능하며 기존 시스템 및 프로세스와 통합될 수 있도록 하려면 컨택 센터 운영의 비즈니스 측면을 잘 이해하는 것이 중요합니다.

3. 프로젝트 관리 스킬

AI 기반 컨택 센터 솔루션을 구축하는 것은 복잡하고 시간이 많이 소요되는 프로젝트일 가능성이 높으므로 프로젝트가 순조롭게 진행되고 기한을 맞추도록 프로젝트 관리 기술과 경험을 갖추는 것이 중요합니다.

4. 데이터 전문성

대규모 데이터 세트 및 데이터 과학 도구로 작업한 경험이 있으면 모델 교육을 위해 데이터를 수집, 정리 및 준비하는 데 중요합니다.

5. Software 개발 스킬

소프트웨어 개발 경험은 기존 시스템과의 통합을 포함하여 시스템을 설계 및 구현하고 AI 모델에 액세스하기 위한 API를 만드는 데 중요합니다.

6. 지원과 유지보수

강력하고 유지 관리 가능한 시스템을 구축하는 것이 중요합니다. 즉, 배포 후 시스템을 지원하고 유지 관리할 수 있는 능력을 갖추는 것입니다. 여기에는 성능을 개선하고 새로운 요구 사항에 맞게 조정하기 위한 시스템 모니터링 및 미세 조정이 포함됩니다.

7. 법과 윤리 고려

앞서 언급한 바와 같이 AI 기반 컨택 센터 솔루션은 데이터 프라이버시 및 편견과 같은 법적 및 윤리적 문제를 제기할 수 있습니다. 이러한 문제를 고려하고 이를 완화하기 위한 적절한 조치를 취하는 것이 중요합니다.

8. UX 디자인

사용자 경험 디자인을 잘 이해하는 것은 시스템이 사용하기 쉽고 이해하기 쉬운지 확인하는 데 중요하며 이는 고객 만족도를 향상시키는 데 도움이 됩니다.

AI 기반 컨택 센터 솔루션을 구축하는 것은 쉬운 일이 아니며 다양한 분야의 기술과 전문 지식의 조합이 필요합니다. 솔루션의 특정 요구 사항에 따라 개발을 돕기 위해 추가 전문가 또는 리소스를 가져와야 할 수 있습니다.


딱 보면 일을 시작하기 전에 어떤 측면의 중요성을 상기하고 있어야 하는지 잘 짚어주었다. 물론 일반론적 이야기이긴 하겠지만 개발하기 전 설계나 솔루션 개발 및 이행 단계에서 고려해야 할 요소들을 정확히 짚어주지 않았나 싶다.

앞으로 OpenAI 의 GPT 가 상당히 기대된다!

반응형
블로그 이미지

Good Joon

IT Professionalist Since 1999

,
Edit

Multisig Wallet #2

220.Blockchain 이더리움 Ethereum 티스토리 multisigwallet ethereum

볼만한 Multisig Wallet 코드는 Consensys Gnosis 가 있다.

Consensys 의 Multisig Wallet 은 아직도 상당히 많은 양의 ETH 를 Holding 하며 잘 쓰이고 있고, 핵심인 Contract Code 만 올라가 있는 상태이다. Contract Code 는 업데이트 되지는 않고 있다.

Gnosis 의 코드같은 경우, Angular.js 기반의 Front-End 까지 Push 되어 있으며, Contract 또한 잘 구조화 하였고 Truffle 로 Test 와 Migration 이 가능하도록 Truffle Project 로 만들어 놓았다. 아직도 간간히 dApp 부분은 업데이트 되고 있다.

이번 글에서는 Consensys 의 MultisigWallet 코드를 봐보도록 하겠다. Gnosis 의 것이 잘 되어있기는 하지만, dApp 의 UI 쪽 코드가 대부분이며 핵심을 보기에는 오히려 그 외 적인 코드들이 많다.

Test 환경

이제는 dApp 개발하시는 분들이면 다들 쓰고 있을 TruffleGanache-CLI 를 사용하도록 한다.
Truffle 의 develop 커맨드를 사용해도 되겠지만 옵션 지정에 한계가 있는 관계로 보통은 Ganache-CLI 를 사용한다.

IDE 는 IntelliJ 에 Solidity 플러그인Solhint 를 사용하고 있지만, Solhint 의 Lint 기능이 Remix-IDE 보다 떨어지고 특히 내가 사용중인 IntelliJ 버전의 solhint 플러그인은 .solhint.json 이 제대로 안먹는 관계로 편집은 Remix 로 한다. 필요한 경우 Terminal 에서 solhint 를 직접 쳐서 Linting 을 해주는 상황이다.

Test 는 UI 가 필요한 상황이 아니라면, 대부분 Truffle Test 를 사용하도록 한다. Mocha + Chai 를 사용하여 테스트 코드를 만드는게 좀더 체계적이고 많은 테스트를 자동화 해줄 수 있기 때문에 사용한다.

요즘은 개발환경 구성과 관련한 글들은 여기저기서 많이 찾을 수 있으니 설치나 구성 방법은 생략 한다.

주요 Contract 코드

Consensys 의 MultisigWallet 은 딱 하나의 Contract 인 MultisigWalletWithDailyLimit.sol 이 들어있다. 이 Contract 를 이해 하면 시간내어 Gnosis 의 MultisigWallet 같은 녀석도 만들어갈 수 있다

Contract 는 간단히 2개로 구성된다. modifier 몇개와 이름만 봐도 어떤기능을 할지 짐작이 가는 녀석들이 있다. 중요 modifier 는 onlyWallet, confirmed, validRequirement 정도가 볼만 하며, Function 으로는 addOwner, submitTransaction, confirmTransaction, executeTransaction 정도라고 할 수 있겠다

아래 Contract 는 solc 0.4.10 에서 Compile 된다. pragma definition 만 바꾸면 0.4.13 까지는 큰 오류 없이 compile 된다. constant, throw, constructor 등등.. 그간의 변화가 반영되지 않은 코드이지만 원리 이해에는 별 무리가 없어서 그냥 사용한다

solc 는 지난달 공식 Release 된 0.5.0 에 와서 많은 변화들이 생겼다. 나중에 직접 코딩하는 경우에는 0.5 버전대를 기준으로 올리도록 하겠다. (하아.. 수없이 바뀌어가는 Solidity 도 이제 힘들다. Vyper 로 넘어가야 하나..ㅋ)

주요 Modifier

onlyWallet

modifier onlyWallet() {
if (msg.sender != address(this))
throw;
_;
}

위 Modifier 는 msg.sender 가 현재 Contract 가 아니면 throw 로 튕기라는 내용이다. 이게 왜 필요할까 싶을 수 있다.

onlyWallet modifier 를 사용하는 Function 은 addOwner, removeOwner, replaceOwner, changeRequirement,changeDailiyLimit 이다. 이 Function 들은 Wallet 고유의 기능(설정)을 변경하는 기능을 수행한다는 것이다. 즉 Wallet 이 다른 Address 로 Transaction 을 Submit 하지 않으며 Wallet 내부에서 모든 기능이 끝난다.

이러한 함수는 EOA 에서 Wallet Contract 로 Message Transaction 을 Submit 할 때, Data 파트에 to 에 해당하는 parameter(여기서는 submitTransaction 의 destination) 를 본 MultiSigWallet 의 Contract 로 하고, 함수의 Signature 를 위 5개 중의 하나의 함수로 하며 함수의 파라미터에 맞는 값을 넣어 submitTransaction 혹은 executeTransaction 으로 Submit 해주어야 한다.
(아래 나올 submitTransaction, executeTransaction 을 보면 이해가 갈 것이다)

이때, 위 5개 함수는 MultiSigWallet 이 .call() 로 자신이 Sender 가 되어 호출하게 된다. 그래서 이런 류의 호출만을 Accept 하기 위해 onlyWallet() modifier 를 만들어서 쓴다.

사실 이번 Blog 를 쓰는 주된 이유가 .call() / .delegatecall() / staticcall() 과 관련된 설명을 이후에 하기 위함이니 이어질 설명과 테스트를 통해 잘 이해 해두도록 하자

추가로, throw 는 deprecate 된지 오래이며, revert() 와 동일하게 취급된다. 즉, gas 는 throw 이전 까지 실행된 만큼만 소모된다

confirmed

mapping (uint => mapping (address => bool)) public confirmations;
modifier confirmed(uint transactionId, address owner) {
if (!confirmations[transactionId][owner])
throw;
_;
}

confirmed modifier 는 함수 중 revokeConfirmation() 에서만 사용된다. confirmations mapping 을 보면, uint => address => bool 로 되어있는것을 볼 수 있으며, transactionId => owner => true/false 형태로, submit 된 transactionId 에 revokeConfirmation() 을 실행한 ownerconfirm을 했는지 가 기록되어있는지를 확인해주고, 아니라면 throw 로 튕겨내는 기능을 한다.

validRequirement

uint public required;
modifier validRequirement(uint ownerCount, uint _required) {
if ( ownerCount > MAX_OWNER_COUNT
|| _required > ownerCount
|| _required == 0
|| ownerCount == 0)
throw;
_;
}

required 는 MultiSigWallet 의 constructor 에서 지정하도록 되어있으며, 전체 owner 의 수보다는 작아야 하며 owner 가 0 보다 커야 valid 하다는 조건을 가지도록 modifier 로 filtering 한다.

required 는 changeRequirement() 를 통해 변경될 수 있으며, changeRequirement() 함수는 onlyWallet modifier 에 의해 실행될 수 있다. 그 얘기는, changeRequirement() 를 실행시키기 위한 Transaction 을 Wallet 으로 submit 해야 하며, 이에 대해 이전에 설정된 required 만큼의 owner 가 confirm 을 해야 이 또한 실행될 수 있음을 의미한다

주요 Functions

addOwner

function addOwner(address owner)
public
onlyWallet
ownerDoesNotExist(owner)
notNull(owner)
validRequirement(owners.length + 1, required)
{
isOwner[owner] = true;
owners.push(owner);
OwnerAddition(owner);
}

confirm 할 자격이 있는 owner 를 추가하는 함수이다. 이 함수 또한 onlyWallet modifier 가 있어, 이 함수를 실행하기 위한 Transaction 이 Submit 되어 있어야 하고, Wallet 이 내부적으로 이 함수를 .call() 로 호출 하여야 한다. 결국, required 만큼의 confirm 이 있어야 owner 가 추가될 수 있다.

submitTransaction

function submitTransaction(address destination, uint value, bytes data)
public
returns (uint transactionId)
{
transactionId = addTransaction(destination, value, data);
confirmTransaction(transactionId);
}

실행시키고자 하는 Transaction 을 submit 하기 위해 호출하는 함수이다. Transaction 의 to 주소를 destination 으로, 이체 되어야 하는 ETH 의 양을 value 에 넣어준다. 그리고 to 주소가 Contract 라면 호출할 Contract 의 Call Code Data 를 data parameter 에 넣어준다. (이부분은 테스트 코드에서 설명 예정이다)

addTransaction() 은 internal 함수로, transactions mapping (transactionId => Transaction)에 Transaction 구조체의 Instance 를 만들어, 단순한 transaction count 를 ID 로 하는 transactionId 를 key 로 하여 저장 해준다.

마지막으로, confirmTransaction() 함수를 통해 submitTransaction 을 호출한 owner 의 confirm 을 자동 실행하게 한다.

confirmTransaction

function confirmTransaction(uint transactionId)
public
ownerExists(msg.sender)
transactionExists(transactionId)
notConfirmed(transactionId, msg.sender)
{
confirmations[transactionId][msg.sender] = true;
Confirmation(msg.sender, transactionId);
executeTransaction(transactionId);
}

confirmTransaction 은 등록된 특정 transactionId 에 대해 owner 가 confirm 을 하기 위한 함수이다. 내용은 위에서 설명한 confirmations (transactionId => owner => true/false 관계를 갖는 mapping) 에 msg.sender (confirmTransaction 을 실행 한 sender) 가 owner 인 경우, 해당 owner 가 confirm 했다고 true 로 바꾸어 주고, Event 를 emit 하는것이다.

마지막으로, 다음에 설명할 executeTransaction() 을 통해, confirm 의 수가 required 보다 같거나 많으면 실제 Transaction 을 실행하게 한다.

executeTransaction

사실 이번 글을 쓰는 주된 목적중의 하나가 아래 코드, 그중 .call() 함수 이다.
call delegatecall staticcall 을 사용하면 Contract 가 ABI 를 모르는 다른 Contract 의 Method 를 호출하도록 하는 Internal Transaction 을 만들어 실행시킬 수 있다. 예전에는 callcode 도 있었으나 0.4.25 버전을 끝으로 사라졌고 staticcall 이 새로 생겨났다.

function executeTransaction(uint transactionId)
public
notExecuted(transactionId)
{
if (isConfirmed(transactionId)) {
Transaction tx = transactions[transactionId];
tx.executed = true;
if (tx.destination.call.value(tx.value)(tx.data))
Execution(transactionId);
else {
ExecutionFailure(transactionId);
tx.executed = false;
}
}
}

isConfirmed() 로 transactionId 에 해당하는 transaction 이 required 이상의 confirm 을 받은 경우, transactions 안에 저장해둔 Transaction 을 실행한다.

실제 transaction 을 실행하는 코드는 아래 부분이다

if (tx.destination.call.value(tx.value)(tx.data))

tx.destination 에는 submitTransaction() 을 호출할 때 넘겨준 destination 즉, transaction 의 to Address 가 들어있다. 이 to Address 에 .call() Low-Level 함수를 호출 해주면 argument 로 넘겨진 Code 를 Internal Transaction 으로 하여 대상 Smart Contract Account 의 Code 를 호출할 수 있게 된다.

주로 호출되는 함수들은 아래와 같은 Sequence 로 동작한다고 보면 된다.

  1. Sender 1 이 MultisigWallet Contract 의 submitTransacton() 을 호출하게 되는데, 이때 parameter 로 “0xCD01…” 주소의 destination 을 주고, 이체할 Ether (MultiSigWallet Contract 내에 자신이 Deposit 한 금액 중 일부)를 적어줄 수 있으며, 마지막으로, destination account (Smart Contract Account) 에서 실행되어야 하는 data 파트 (Function Hash + parameter 들) 을 넣어주는 것이다

  2. MultisigWallet 의 submitTransaction() 은 내부적으로 addTransaction() 을 통해 parameter 를 통해 받은 최종 Transaction 정보를 저장하고 있는다

  3. msg.sender 의 confirm 을 추가해주고

  4. Transation Receipt 의 Log 를 통해 Confirmation Event 를 Logging 해줄 것이다 (transactionId=1, 사실은 Submission Event 를 통해 Confirmation 전에 transactionId 가 Loggiing 된다)

  5. Sender 2 가 1번 Transaction 에 대해 Confirm 을 하고

  6. 이 또한 Confirmation Event 로 날아간다

  7. required 가 2 이라면, executeTransaction() 이 실행 된다

  8. 저장되어있던 Transaction Destination (“0xCD01…”) 으로 100 wei 만큼을 value 로 transfer 하면서 “0xD01A8D…” 의 data 부분을 “0xCD01…” Contract 에서 실행시키게 된다


다음편에는 Test Script 를 통해 실행 하고 마치는걸로~


반응형
블로그 이미지

Good Joon

IT Professionalist Since 1999

,