// 에드센스

 

므ㅏ

강의출처:


1. 사용할 컨트랙에 대한 정보를 가져온다.

contract("Lottery", ([deployer, user1, user2]) => {
  ...
});

트러플에서는 mocha와 chai를 스마트컨트랙에서 사용할 수 있도록 조절해 놓았다고 한다.

따라서 테스트를 시작할 때 contract 키워드로 시작하면 된다.

 

첫번째 인자로는 컨트랙트 이름이 들어가고,

두번째 인자로는 콜백함수가 들어간다. 이 콜백함수의 파라미터로 ganache-cli에서 생성되는 10개의 account가 들어간다.

즉 최대 10개의 인자를 넣을 수 있다.

 

위 코드처럼 3개를 썼다는 것은 

deployer에 0xd53....

user1에 0xd07....

user2에 0x040....

이렇게 3개의 주소가 파라미터로 들어간 것이다.

 

 

 

 


2. 스마트 컨트랙 배포

테스트를 하려면 스마트컨트랙 코드가 블록에 들어가서 실행되어야 하므로 컨트랙을 배포해주자

contract("Lottery", ([deployer, user1, user2]) => {
  beforeEach(async () => {
    console.log("Before each");
    lottery = await Lottery.new(); //테스트에서 사용할 스마트 컨트랙 배포
  });
});

 

 

 

 


3. 테스트코드 작성

  let betAmount = 5 * 10 ** 15;
  let BET_BLOCK_INTERVAL = 3;
  
  describe("Bet", async () => {
    it("베팅큐에 값이 잘 들어갔는지 확인하기", async () => {
      // 베팅한다
      const receipt = await lottery.bet("0xab", {
        from: user1,
        value: betAmount, // 5 * 10 ** 15 -> 0.005ETH
      });

      // pot머니(상금)은 아직 결과가 들어나지 않았기에 0이어야 한다.
      let pot = await lottery.getPot();
      assert.equal(pot, 0);

      // 컨트랙트 주소로 0.005이더가 들어왔는지 확인한다
      let contractBalance = await web3.eth.getBalance(lottery.address); // web3가 자동으로 주입돼있다
      assert.equal(contractBalance, betAmount);

      // 베팅인포가 제대로 들어갔는지 확인한다
      let currentBlockNumber = await web3.eth.getBlockNumber();
      let bet = await lottery.getBetInfo(0); // 큐 제일 앞의 베팅정보를 가져온다

      // 큐에 넣은 베팅정보가 올바른지 확인
      assert.equal(
        bet.answerBlockNumber,
        currentBlockNumber + BET_BLOCK_INTERVAL
      );
      assert.equal(bet.bettor, user1);
      assert.equal(bet.challenges, "0xab");

      // 로그(BET이라는 이벤트)가 제대로 찍혔는지 확인한다
      await expectEvent.inLogs(receipt.logs, "BET");
    });

    it("0.005이더가 안들어왔을때는 실패해야한다", async () => {
      // 트랜잭션 실패
      await assertRevert(
        // 두번째 인자는 트랜잭션 오브젝트, transaction object란
        // (chainId, value, to, from, gas(limit), gasPrice)
        lottery.bet("0xab", { from: user1, value: 4000000000000000 }) // bet함수 실행 -> 0.004ETH가 들어갔기에 트잭이 실패나야한다
      );
    });
  });

 

 

event, emit을 테스트하기

const assert = require("chai").assert;

// 직접 발생시킨 event의 로그가 있는지 확인한다.
const inLogs = async (logs, eventName) => {
  const event = logs.find((e) => e.event === eventName);
  assert.exists(event);
};

module.exports = {
  inLogs,
};

[Lottery Dapp 개발하기] 2.4까지의 분량이다. 

밀린 블로그 글써내기중이라 그간 했던 분량을 한번에 쑤셔넣는중.

이 글을 읽는 여러분들에게 도움이 되면 좋겠지만 딱히 그럴려고 쓰는 글은 아니고 혼자서 끄적이는 글.

따라서 생략된 설명이나 이걸 왜 설명? 하는 부분이 있을것이다.

 

 

가보자~

강의 출처:


1. 코딩할 것들

  • 스마트 컨트랙
    • 베팅정보를 담는 구조체
    • 베팅정보를 저장할 큐 자료구조
    • 쌓인상금_가져오기()
    • 베팅큐에서_베팅정보_가져오기()
    • 베팅하기()
      • 베팅을_베팅큐로_밀어넣기()
      • 베팅을_베팅큐에서_빼기()
  • 테스트코드 (다음글에서)
    • beforeEach
    • 베팅큐에 베팅정보가 잘 들어간경우 테스트
    • 베팅큐에 베팅정보가 못들어간경우 테스트

 

 

 


2. Lottery.sol에 코딩한 것들

베팅 정보를 담는 구조체

  struct BetInfo {
    uint256 answerBlockNumber; // 맞출 블록의 넘버
    address payable bettor; // 돈을 건 사람 주소, 특정주소에 돈을 보내려면 payable을 써줘야함
    bytes challenges; // 문제. ex) 0xab
  }

 

 

 

BetInfo 구조체를 넣어줄 큐를 구현

  // 매핑으로 큐를 구현하기 위한 변수
  uint256 private _tail;
  uint256 private _head;

  // 키는 uint, 값은 BerInfo인 매핑
  mapping(uint256 => BetInfo) private _bets;

index - value의 쌍을 mapping을 통해 구현한다.

 

 

 

상금 액수 조회하기

  uint256 private _pot;
  
  // 스마트 컨트랙의 변수를 가져와서 쓰려면 view 키워드를 쓴다
  // 더 자세히는, Storage 자료를 읽어서 보여줄때 사용. 수정할때는 사용x
  // Storage 자료는 블록에 영구적으로 기록된 값(하드디스크)
  // 반대인 Memory 자료는 임시로 저장되는 값(RAM)
  function getPot() public view returns (uint256 value) {
    return _pot;
  }

 

 

 

베팅하기

  // 이벤트 객체
  event BET(uint256 index, address bettor, uint256 amount, bytes1 challenges, uint256 answerBlockNumber);

  function bet(bytes challenges) public payable returns (bool result) {
    // 베팅할 금액(0.005ETH)이 제대로 왔는지 확인
    // require는 if역할, msg.value는 컨트랙트가 받은금액, 'Not Enough ETH'은 조건이 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;
  }

event와 emit

트랜잭션이 완료되면 트랜잭션 영수증을 발행한다.

영수증에는 트랜잭션 실행 동안 발생했던 모든 행동로그가 기록되어 있다.

event는 이런 로그를 만들기 위한 객체

emit은 만들어진 event 객체를 체인에 올리기.

더보기

 

 

 

 

베팅정보들을 담은 큐에서 베팅정보 1개 꺼내오기

  function getBetInfo(uint256 index)
    public
    view
    returns (
      uint256 answerBlockNumber, // 반환값1
      address bettor, // 반환값2
      bytes challenges // 반환값3
    )
  {
    BetInfo memory b = _bets[index]; // memory형 변수는 함수가 끝나면 지워짐, storage형 변수는 블록에 영영 기록됨
    answerBlockNumber = b.answerBlockNumber; // 반환값1
    bettor = b.bettor; // 반환값2
    challenges = b.challenges; // 반환값3
  }

 

 

 

베팅큐에 베팅정보 push

  function pushBet(bytes challenges) internal returns (bool) {
    BetInfo memory b; // 베팅정보를 하나 생성하고 세팅한다
    b.bettor = msg.sender; // 함수 호출한 사람
    b.answerBlockNumber = block.number + BET_BLOCK_INTERVAL; // block.number는 현재 이 트랜잭션이 들어가게되는 블록넘버를 가져온다
    b.challenges = challenges; // 내가 베팅한 값

    _bets[_tail] = b; // 큐에 넣고
    _tail++; // 테일 포인터 조정

    return true;
  }

 

 

 

베팅큐에서 베팅정보 1개 pop

  function popBet(uint256 index) internal returns (bool) {
    // 스마트컨트랙에서 덧셈이든, 뺄셈이든 하면 가스를 소모한다.
    // delete를 하면 가스를 돌려받는다. 왜? 이더리움 블록체인에 저장하고 있는 데이터를 더 이상 저장하지 않겠다는 것이기에
    // 즉, 상태 데이터베이스에 있는 값을 그냥 가져오겠다는 것이기에
    // 그러니 필요하지 않은 값이 있다면 delete를 해주자
    delete _bets[index];
    return true;
  }

 

 

 

 


3. 전체 코드

pragma solidity >=0.4.21 <0.6.0;

contract Lottery {
  struct BetInfo {
    uint256 answerBlockNumber; // 맞출 블록의 넘버
    address payable bettor; // 돈을 건 사람 주소, 특정주소에 돈을 보내려면 payable을 써줘야함
    bytes challenges; // 문제. ex) 0xab
  }

  // 매핑으로 큐를 구현하기 위한 변수
  uint256 private _tail;
  uint256 private _head;

  // 키는 uint, 값은 BerInfo인 매핑
  mapping(uint256 => BetInfo) private _bets;

  address public owner;

  uint256 internal constant BLOCK_LIMIT = 256; // 블록해시를 확인할 수 있는 제한
  uint256 internal constant BET_BLOCK_INTERVAL = 3; // 2번 블록에서 베팅을 하면 5번 블록에서 결과가 나온다
  uint256 internal constant BET_AMOUNT = 5 * 10**15; // 0.005ETH

  uint256 private _pot;

  // 이벤트 로그들을 한번에 모을 수 있다, BET이라는 로그들을 찍어줘
  // (몇번째 배팅인지, 누가 배팅했는지, 얼마 배팅했는지, 어떤글자로 베팅했는지, 어떤 블록에 정답이 있는지)
  event BET(uint256 index, address bettor, uint256 amount, bytes1 challenges, uint256 answerBlockNumber);

  constructor() public {
    owner = msg.sender; // msg.sender는 전역변수
  }

  function getPot() public view returns (uint256 value) {
    // 스마트 컨트랙의 변수를 가져와서 쓰려면 view 키ㅕ드를 쓴다
    return _pot;
  }

  // Bet (베팅하기)
  /*
    @dev 베팅을 한다. 유저는 0.005ETH를 보내야하고 베팅용 1byte 글자를 보낸다, 큐에 저장된 베팅 정보는 이후 distribute 함수에서 해결한다
    @param challenges 유저가 베팅하는 글자
    @return 함수가 잘 수행되었는지 확인하는 bool 값
    */
  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;
  }

  // 베팅한 값을 큐에 저장함

  // Distribute (검증하기)
  // 베팅한 값을 토대로 결과값을 검증
  // 검증결과가 틀리면 팟머니에 돈을 넣고, 맞으면 돈을 유저에게 준다

  // 베팅정보들을 담고있는 큐에서 베팅정보 가져오기
  function getBetInfo(uint256 index)
    public
    view
    returns (
      uint256 answerBlockNumber,
      address bettor,
      bytes challenges
    )
  {
    BetInfo memory b = _bets[index]; // memory형 변수는 함수가 끝나면 지워짐, storage형 변수는 블록에 영영 기록됨
    answerBlockNumber = b.answerBlockNumber; // 반환값1
    bettor = b.bettor; // 반환값2
    challenges = b.challenges; // 반환값3
  }

  // 큐 push
  function pushBet(bytes challenges) internal returns (bool) {
    BetInfo memory b; // 베팅정보를 하나 생성하고 세팅한다
    b.bettor = msg.sender; // 함수 호출한 사람
    b.answerBlockNumber = block.number + BET_BLOCK_INTERVAL; // block.number는 현재 이 트랜잭션이 들어가게되는 블록넘버를 가져온다
    b.challenges = challenges; // 내가 베팅한 값

    _bets[_tail] = b; // 큐에 넣고
    _tail++; // 테일 포인터 조정

    return true;
  }

  // 큐 pop
  function popBet(uint256 index) internal returns (bool) {
    // delete를 하면 가스를 돌려받는다. 왜? 상태데이터베이스에 저장된 값을 그냥 뽑아오겠다는 것이기에
    // 그러니 필요하지 않은 값이 있다면 delete를 해주자
    delete _bets[index];
    return true;
  }
}

정말 오~랜만에 글을 쓴다.

써야지 써야지 하면서 너무 미뤘다.

 

 

 

더 이상 글을 안쓰면 블로그를 영영 잊게될까봐 다시 자리에 앉았다.

그래도 그간 쪼잡쪼잡 이거저거 맛보기는 하고있었는데 그 중 하나인 이더리움 dapp 개발 과정을 써볼까 한다.

유튜브 강좌를 보며 따라하는 것이고.. 그저 따라할 뿐...! 아직 어렵다

 

코딩하는 나의 모습

 

내가 보며 따라하는 강좌는 요거다.

https://youtu.be/Ud3_OrxNPDg

 

 


0. What we gonna do?

스마트컨트랙트를 기반으로하는 디앱을 만들어보는 프로젝트.

디앱이란?

대충 블록체인을 활용한 애플리케이션.

Decentralized App

 

 

 


1. Lottery Dapp preview

로또게임(?)을 만들건데 스마트컨트랙을 활용하여 만들 것이다.

 

이 프로젝트에서 우리는

  • 솔리디티를 사용한 스마트컨트랙 만들기
  • 배포스크립트 만들기
  • 테스트코드 만들기
  • 프론트에 연동하기

를 하게된다.

 

 

스마트컨트랙을 작성하여 블록체인에 올린다는 것은,

자바로 백엔드 코드를 작성하여 EC2에 올린다는 것과 같다.

 

블록체인도 결국 분산된 무수히 많은 컴퓨터들이니 블록이 곧 서버역할을 하는 셈이다. 스마트컨트랙은 코드역할이고.

따라서 기존 애플리케이션의 흔한 구조인

[프론트엔드]-[백엔드]가

[프론트엔드]-[블록체인]이 되는 것이다.

 

 

단, 경우에 따라서 디앱이라도 [프론트엔드]-[백엔드]-[블록체인]처럼 백엔드를 추가하는 구조도 많은데 아무래도 블록체인상에서 코드를 실행시키기 위해서는 가스비라는 비용이 발생하기에 이런 비용적인 측면을 개선하고자 백엔드단을 추가하기도 한다고 한다.

(블록체인을 구성하는 노드(컴퓨터)들을 제공하는 사람들은 땅파서 장사하는게 아니기에.. 이들에게 가스비라는 대가를 지불하고 우리의 코드를 실행시켜달라고 하는 것)

 

 

 

 

아무튼, 서론이 길었다.

 

우리가 만들 로터리 디앱은 어떤 프로젝트냐면,

https://youtu.be/div91ADl_7k

시연영상

 

 

블록해시 맞추기 게임이다.

 

내가 만든 트랜잭션이 들어가게 되는 블록보다 3개 뒤 블록의 블록해쉬를 때려맞추는 게임(재미는 없을듯)

 

 

예를들자면

  1. 내가 0xab라고 베팅을 한다.
  2. 이 베팅은 하나의 트랜잭션으로서 블록에 포함된다.
  3. 만약 이 트랜잭션이 3번 블록에 포함됐다고 하자.
  4. 3번 블록의 3개 뒷 블록인 6번 블록의 블록해쉬값이 0xab라면 내가 이긴 것이다.
  5. 즉 6번 블록이 생성완료되고 7번블록의 생성이 시작된 시점부터 게임결과를 알 수 있다.

 

pot머니라고 부르는 베팅금액을 가져가는 규칙은 이러하다.(전부 0.005ETH씩 베팅한다)

  1. 6번 블록이 완성되었을때 유저들이 베팅한 돈을 팟머니에 쌓는다.
  2. 맞춘사람이 여러명일 경우 첫번째로 맞춘 사람이 가져간다.
  3. 두글자중(ab중에서) 하나만 맞추었다면 돈을 돌려준다.
  4. 결과값을 검증할 수 없는 경우가 발생하는 경우 돈을 돌려준다.

 

 

동작으로만 보면 그닥 복잡하진 않아보인다.

이 프로젝트의 핵심은 스마트컨트랙을 작성하고 이를 배포하여 체인과 상호작용하는 것에 있다.

 

 

 

 


2. 개발 환경

  • NodeJS : 배포와 테스트 코드를 작성하기 위해
  • VSCode : ide 이거씀
  • Truffle : 솔리디티를 컴파일하고 스마트컨트랙을 테스트, 배포할 수 있는 블록체인 개발 프레임워크이다. Truffle 설치시 solc-js가 설치된다(JS기반의 솔리디티 컴파일러)
  • ganache-cli : 간이 블록체인. 근데 이제 내 PC 메모리로 실행되는. 블록체인을 쉽게 mock을 해놓은 툴(테스트 용도), 즉 테스트와 개발에 사용하는 이더리움 RPC 클라이언트.(테스트용 노드(서버)의 느낌으로 이해하면 되려나..?)

 

ganache-cli(편의상 가나슈라고 하겠다)를 실행하면 로컬에서 블록체인 환경이 실행되고 여기서 사용할 수 있는 계정 10개가 제공된다. 

 

이 간이 블록체인을 켜기 위해서는 콘솔창에서

ganache-cli -d -m tutorial

를 실행해준다

 

-d 옵션을 주면 생성되는 니모니깅 동일하고 주소와 키값이 동일하다

-m 옵션은 지정된 HD 지갑 니모닉 값으로 초기 주소를 생성하기 위해 사용한다.

우리는 니모닉으로 tutorial을 사용했다.

보통 니모닉은 12개 단어를 사용하지만 1개만 써도 된다(?)

 

즉 -d, -m 옵션을 사용하고 니모닉 단어(tutorial)를 사용하면 항상 같은 지갑주소가 생성된다.


1. 로컬에서 브랜치명 변경하기

 

git branch -m oldname newname

 

git branch -m [기존브랜치이름] [새로운브랜치이름]

간단하다.

 

 

 

 


2. 원격저장소의 브랜치명까지 변경하기

 

로컬저장소에서는 브랜치 이름이 변경되었다.

이어서 이 변경사항을 원격저장소에도 반영해보자.

 

현재 로컬저장소는 newname이지만 원격저장소는 oldname이다

 

 

 

 

아까 로컬에서의 git branch -m옵션으로 oldname브랜치의 이름을 newname으로 바꿨기에

이 사항을 원격저장소에 push한다.

 

git push origin :oldname

 

이전 이름인 oldname 앞에 콜론(:)을 붙혀서 oldname 브랜치의 삭제사항을 푸시하는 것이다.

그러면

요렇게 나오면서 

 

 

 

원격저장소에 있던 oldname 브랜치가 사라진다.

 

 

 

 

 

이제 이름을 바꾼 브랜치를 푸시하면 된다.

git push --set-upstream origin newname

그러면 이렇게 나오면서

 

 

 

원격저장소에도 수정된 이름의 브랜치가 잘 반영됨을 확인할 수 있다.

 

 

 

 

꿀팁 한 가지.

변경내용이 없어도 커밋을 하고싶다면

git commit --allow-empty -m "Empty Commit Message"

하면 된다 !

 

 

 

 

참고:

 

 

'Git' 카테고리의 다른 글

[Git] git stash  (0) 2021.11.24
[Git] Branch와 Merge  (0) 2021.08.14
[Git] BFG Repo-Cleaner를 사용한 민감한 히스토리 삭제  (0) 2021.08.14
[Git] git bash를 사용한 버전관리 시작하기  (0) 2021.08.13


1. 무슨일이야

Canary branch를 생성하며 기존의 [빈스톡+S3+RDS+Firebase] 세트의 인프라를 똑같이 하나 더 생성하려고했다.

순조롭게 하나씩 뚝딱뚝딱 만들어갔다..

아예 새로운 환경이기에 firebase나 kas api등 모든 키값들을 새로 넣어주어야했다.

따라서 NodeJS의 .env파일과 firebase 설정파일인 firebase.json을 불러오는 script까지 canary버전으로 새로 작성해주었다.

 

그런데 이게 무슨일인가

 

빈스톡에 배포를 하고나니 여전히 Dev DB, Dev Firebase, Dev KAS api를 가리키고있는게 아닌가?

데이터는 데브 디비로 저장되고 Firebase JWT는 프로젝트가 다르다며 풀리지 않았다.

왜! 분명 나는 잘 한거같은데!!!(물론 역시 내 실수였다)

 

빈스톡 로그파일을 까보면 데브환경으로 구성돼있음을 확인할 수 있었다.

 

 

 


2. 어디서 꼬였을까

의심1. 내가 S3에 env를 잘못올려놨겠지

  • 아니었다. 다시 다운받아서 열어보니까 잘 올라가 있더라.
  • Github Actions에서 env, firebase.json을 가져오는 스크립트에도 문제가 없었다.
  • 그 외에 모든 S3와 관련된 부분에서도 문제는 없었다.

깃헙액션에서 로그를 찍어가며 canary env파일임을 확인했다.

 

 

의심2. POSTMAN의 환경변수 문제인줄 알았다.

테스트를 포스트맨으로 하는데 엔드포인트를 데브 빈스톡으로 설정한줄 알았다.

  • 역시 아니었다. 잘 해놨다.

 

 

의심3. 빈스톡 deploy.zip이 생성될 때 뭔가 env가 누락이 되는가 싶었다.

  • 로컬에선 잘 포함되어 만들어지는데.. 이것도 아닌것 같았다.

 

 

 

 


3. 등잔밑이 어두웠다

깃헙액션 스크립트 곳곳에 로그를 찍어가며 어디서부터 잘못된 env로 바뀌는지 찾아갔고..

결국 Generate deployment packageDeploy to EB 사이 어딘가에서 문제가 생김을 알 수 있었다.

 

 

근데 조금 자세히 보면 

[deploy.zip 생성 -> 배포시작 -> 배포완료 -> 애플리케이션 구동] 의 흐름인건데

이 흐름에서 deploy.zip 생성에는 문제가 없었다면,

그 이후의 과정에는 내가 관여할 수 없는것 아닌가? 싶었다.

즉, 배포가 시작되고부터는 내 손을 떠난 상태라고 생각했다.

 

 

결국 선임님께 헬프를 쳤는데 30초만에 "혹시 이거 아닌가요?" 라고 답이 왔다.

 

정답은 빈스톡을 처음 설정할 때 멋모르고 만들어 놓은 .ebextension파일이었다.

 

 

 

 


4. ebextension이란?

한마디로

"빈스톡이 배포될때 실행되는 커맨드"

라고 할 수 있다.

 

프로젝트 최상위 경로에 .ebextension 폴더를 생성하고 폴더 안에 .config 파일을 생성하여 커맨드를 지정한다.

 

commands와 container_commands 이렇게 두가지 종류의 명령이 있다.

이 둘은 실행시점이 다르다.

 

  • commands는 웹서버가 설정되고 애플리케이션 버전이 추출되기 전에 실행되고,
  • container_commands는 웹서버가 설정되고 애플리케이션 버전 아카이브의 압축이 풀린 후에 실행된다.

 

위 이미지에는 없지만 나는 container_commands에 엉뚱한 env를 불러오는 S3 스크립트를 넣어놨었다.

그래서 깃헙액션에서 canary env를 불러와서 deploy.zip에 넣어줘도 빈스톡 배포 직후 엉뚱한 env로 덮어씌워졌던 것이다!!!!

바로 해당 커맨드를 빼주니 정상 동작하더라.

 

 

 

해결완료-

 

 

참고:


1. 왜 RabbitMQ를 사용하는가?

RabbitMQ는 *AMQP를 따르는 오픈소스 메세지 브로커이다.

RabbitMQ는 데이터를 잠시 보관하고 나중에 비동기적으로 처리하고 싶을 경우 사용하는 일종의 데이터 저장소이다.

 

*AMQP: Advanced Message Queing Protocol의 약자로, 흔히 알고 있는 MQ의 오픈소스에 기반한 표준 프로토콜

 

실생활에서 예를 들어보자

스타벅스에서 손님들이 줄을 서서 커피를 주문한다.

 

1. 메시지 큐X

첫번째 손님이 커피를 주문하면 첫번째 손님의 커피가 완성될때까지 두번째 손님은 계속 줄을 서서 대기해야한다.

 

2. 메시지 큐O

첫번째 손님은 커피를 주문하고 자리로 간다. 두번째, 세번째 손님들도 주문서만 던져놓고 자리로 간다. 바리스타는 쌓여가는 주문서들을 보며 순서대로 커피를 만든다. 커피가 만들어지면 손님들이 받아간다.

 

이때 손님(프로듀서)들이 바리스타(컨슈머)에게 던져놓는 주문서가 메시지가 되고 주문서가 쌓여가는 곳이 메시지 큐가 된다.

 

이런 구조는 커피를 비동기적으로 만들기에 효율적이며,

주문서는 바리스타에게 전달될때 까지 잠시 저장되기에 바리스타가 까먹거나 하는 주문 누락이 발생하지 않는다.

 

 

 

정리하자면

  • 메시지를 많은 사용자에게 전달해야할 때
  • 요청에 대한 처리시간이 길어 해당 요청을 다른 API에 위임하고 빠른 응답처리가 필요할 때
  • 애플리케이션 간 결합도를 낮춰야 할 때

RabbitMQ를 사용한다

 

 

 

 


2. RabbitMQ는 어떻게 이루어져있는가?

[프로듀서 → 브로커(익스체인지+큐) → 컨슈머]

의 구조로 메시지를 전달해주는 메시징 서버

RabbitMQ는 다음과 같이 구성된다.

  1. Producer: 메시지를 보내는 놈
  2. Exchange: 메시지를 알맞은 큐에 전달해주는 놈
  3. Queue: 메시지를 차곡차곡 쌓아두는 놈
  4. Consumer: 메시지를 받는 놈
 

 

위 그럼처럼 Producer는 Queue에 직접 메시지를 전달하는 것이 아니다.

[프로듀서 → 익스체인지 → 큐 → 컨슈머]의 절차를 밟는다.

Exchange에서 알맞은 Queue로 메시지를 분배한다.(Exchange들과 Queue들은 바인딩되어있다)

무슨 기준으로 분배하느냐?

Exchange Type에 따라 다르다.

 

Exchange Type 4가지

Direct

메시지에 포함된 Routing Key를 기반으로 특정 Queue에 메시지를 하나씩 전달한다.

 

 

Fanout

Routing Key에 상관 없이 연결돼있는 모든 Queue에 동일한 메시지를 전달한다.

라우팅키를 평가할 필요가 없기때문에 성능적인 이점이 있다.

 

Topic

라우팅키 전체가 일치하거나 일부 패턴과 일치하는 모든 Queue로 메시지가 전달된다.

Topic Exchange 에서 사용하는 binding key 는 점(.)으로 구분된 단어를 조합해서 정의한다.

* 와 #을 이용해 와일드 카드를 표현할 수 있으며, * 는 단어 하나 일치 # 는 0 또는 1개 이상의 단어 일치를 의미한다.

 

다음과 같이 binding key 를 정의한 경우에 메시지의 routing key 가 quick.orange.rabbit 또는 lazy.orange.elephant 이면, Q1, Q2 둘 다 전달된다. lazy.pink.rabbit 는 binding key 2개와 일치 하더라도 1번만 전달된다.

quick.brown.fox, quick.orange.male.rabbit 는 일치하는 binding key 가 없기 때문에 무시된다.

 

Header

메시지 속성 중 headers 테이블을 사용해 특정한 규칙의 라우팅을 처리한다.

  • x-match = any 일 경우 헤더 테이블 값 중 하나가 연결된 값 중 하나와 일치하면 메시지 전달
  • x-match = all 일 경우 모든 값이 일치해야 메시지를 전달한다.

 

 

추가적인 사용 패턴들은 다음 블로그에 잘 설명이 되어있더라.

https://hamait.tistory.com/402

 

RabbitMQ 사용패턴들

역주: 메세지큐에 대한 글을 적기 전에 왜 메세지큐냐? 를 먼저 생각해봐야한다. 메세지큐는 그냥 메세지를 전달해주는 서버인건데, 기술자체에 집중할 필요는 나중에 생각해보고 , 처음 생각해

hamait.tistory.com

 

 

 

 


3. Connection과 Channel

Connection

  • RabbitMQ에서 지원하는 모든 프로토콜은 TCP 기반이다.
  • 효율성을 위해 긴 연결을 가정한다. (프로토콜 작업당 새 연결이 열리지 않음.)
  • 하나의 클라이언트 연결은 단일 TCP 연결을 사용한다.
  • 클라이언트가 연결을 성공하려면, RabbitMQ 대상 노드는 특정 프로토콜에 대한 연결을 허용해야 한다.
  • 연결은 오래 지속되어야 하기 때문에 일반적으로 구독을 등록하고, 폴링 대신에 메시지를 전달하여 소비한다.
  • 연결이 더 이상 필요하지 않은 경우, 리소스 절약을 위해 연결을 닫아야 한다. 이를 수행하지 못하는 클라이언트는 리소스의 대상 노드를 고갈시킬 위험이 있다.
  • 운영 체제는 단일 프로세스가 동시에 열 수 있는 TCP 연결(소켓) 수에 대한 제한이 있다. QA 환경에서는 충분한 경우가 있지만, Production 환경에서는 더 높은 제한을 사용하도록 구성해야 할 수도 있다.

 

Channel

  • 단일 TCP 연결을 공유하는 논리적인 개념의 경량 연결로 다중화된다.
  • 클라이언트가 수행하는 모든 프로토콜 작업은 채널에서 발생한다.
  • 채널 안에 연결할 Queue를 선언할 수 있으며, 채널 하나당 하나의 Queue만 선언이 가능하다.
  • 채널은 Connection Context에만 존재하기 때문에 Connection이 닫히면, 연결된 모든 채널도 닫힌다.
  • 클라이언트에서 처리를 위해 멀티 프로세스/스레드를 사용한다면, 프로세스/스레드 별로 새 채널을 열고 공유하지 않는 것이 일반적이다.

 

아무튼.

Connection: Application과 RabbitMQ Broker사이의 물리적인 TCP 연결

Channel: connection내부에 정의된 가상의 연결. queue에서 데이터를 손볼 때 생기는 일종의 통로같은 개념

 

한개의 Connection에 여러개의 Channel,
한개의 Channel에 한개의 Queue

https://hyos-dev-log.tistory.com/8

 

RabbitMQ의 Connection과 Channel

사내에서 진행되는 서비스를 개편하게 되면서, RabbitMQ를 사용하게 되었습니다. RabbitMQ을 사용하고, 운영하면서 Connection과 Channel에 대한 개념이 잡히지 않아서 정리하게 되었습니다. Connection 일반

hyos-dev-log.tistory.com

 

 

 

 


4. 적용하기

NodeJS에 적용하기

1. 초기 설정

amqp.connect함수를 통해서 Rabbitmq에 연결한다.

export async function makeConnection() {
  const connection = await amqp.connect({
    protocol: config.rabbitmq.protocol,
    username: config.rabbitmq.username,
    password: config.rabbitmq.password,
    hostname: config.rabbitmq.hostname,
    port: config.rabbitmq.port,
    vhost: '/',
    heartbeat: 0,
  });
}

 

2.  NodeJS에서 메시지 Subscribe하기

const messageQueueConnectionString = {
  protocol: config.rabbitmq.protocol,
  username: config.rabbitmq.username,
  password: config.rabbitmq.password,
  hostname: config.rabbitmq.hostname,
  port: config.rabbitmq.port,
  vhost: '/',
  heartbeat: 0,
};


export async function listenForResults() {
  try {
    // connect to Rabbit MQ (1)
    const connection = await amqp.connect(messageQueueConnectionString);

    // create a channel and prefetch 1 message at a time (2)
    const myChannel1 = await connection.createChannel();
    await myChannel1.prefetch(1);

    const myChannel2 = await connection.createChannel();
    await myChannel2.prefetch(1);

    // start consuming messages (3)
    console.log('start consuming messages from web service');
    await myController.myConsume1({ connection, myChannel1 });
    await myController.myConsume2({ connection, myChannel2 });
  } catch (e) {
    Raven.captureException(e);
  }
}

(1) connection 획득

(2) connection으로부터 channel을 생성

(3) consume 실행

 

위 1, 2, 3번의 과정은 서버가 최초 실행될때 실행된다.

*Prefetch란, 큐의 메시지를 컨슈머의 메모리에 쌓아놓을 수 있는 최대 메시지의 양

 

https://minholee93.tistory.com/entry/RabbitMQ-Prefetch

 

[RabbitMQ] Prefetch

이번 글에서는 RabbitMQ의 Prefetch에 대해 알아보겠습니다. 1. Prefetch란? Queue의 메세지를 Consumer의 메모리에 쌓아놓을 수 있는 최대 메세지의 양 입니다. 예를 들어 Prefetch가 250일 경우, RabbitMQ는 250..

minholee93.tistory.com

 

 

export function myConsume1({ connection, myChannel1 }) {
  return new Promise((resolve, reject) => {
    myChannel1.consume('q1', async function (msg) { // queue 이름
      console.log('myConsume1');
      
      // (4)
      const msgBody = msg.content.toString();
      const data = JSON.parse(msgBody);

      // (5)
      // 비즈니스로직

      // acknowledge message as received
      await myChannel1.ack(msg);
    });

    // handle connection closed
    connection.on('close', err => {
      return reject(err);
    });

    // handle errors
    connection.on('error', err => {
      return reject(err);
    });
  });
}

 

(4) 메시지 파싱

(5) 원래 수행하고자 했던 로직 실행

ack를 전송하지 않고 소비자가 죽거나 (채널이 닫히거나 연결이 끊어 지거나 TCP 연결이 끊어지는 경우),

RabbitMQ는 메시지가 완전히 처리되지 않았 음을 인식하고 다시 대기한다.

즉 컨슈머(소비자)가 사망했을 경우를 대비

 

3.  NodeJS에서 메시지 Publish하기

export let channel1;

// (1)
export async function makeConnection() {
  const connection = await amqp.connect({
    protocol: config.rabbitmq.protocol,
    username: config.rabbitmq.username,
    password: config.rabbitmq.password,
    hostname: config.rabbitmq.hostname,
    port: config.rabbitmq.port,
    vhost: '/',
    heartbeat: 0,
  });
  
  // (2)
  channel1 = await connection.createConfirmChannel(); // createConfirmChannel는 ack과 nack을 확인한다
}

// (3)
export async function publishToChannel(channel, { exchangeName, routingKey, data }) {
  await channel.publish(exchangeName, routingKey, Buffer.from(JSON.stringify(data), 'utf-8'), { persistent: true });
}

(1) 서버 최초 실행시 makeConnection 메소드를 사용하여 연결한다

(2) channel당 하나의 queue만 생성이 가능하기에 필요한 큐만큼의 채널을 생성하고 전역변수로 export해준다

(3) channel의 publish메소드를 사용하는 함수를 만든다

(1), (2)의 과정은 서버가 최초 실행될때 실행된다.

 

RabbitMQ가 종료되거나 충돌하면 사용자가 알리지 않는 한 대기열과 메시지를 잃게된다. 메시지가 손실되지 않도록하려면 큐와 메시지에 durable과 persistent 설정을 주어야한다.

즉, ack과 달리 RabbitMQ가 사망할 경우를 대비

 

https://skarlsla.tistory.com/13

 

[RABBITMQ] persistent, durable, ack

1. 메시지 수신 자동 확인(ack , noAck) 작업을 수행하는 데 몇 초가 걸릴 수 있습니다. 소비자 중 한 명이 긴 작업을 시작하고 부분적으로 만 수행되어 사망하는 경우 어떻게되는지 궁금 할 수 있습

skarlsla.tistory.com

 

 

 

// (4)
const result = await rabbitmq.publishToChannel(rabbitmq.channel1, {
  exchangeName: 'ex1', // exchange name
  routingKey: 'p1', // bind pattern
  data: { uid: uid },
});

 

(4) 이후 메시지를 publish할 곳에서 export한 channel과 설정값을 publish함수의 인자로 넣어주며 사용한다.

 

 

 

Spring에서 사용하기

1. 초기설정 - application.yml

spring:
  rabbitmq:
    host: somethingsomethingaddress.mq.region.amazonaws.com
    port: 5671
    username: myname
    password: mypassword
    ssl:
      enabled: true
  listener:
    simple:
      acknowledge-mode: manual

 

2. Spring에서 메시지 Subscribe하기

@RabbitListener(queues = ["sample.queue"])
public fun receiveMessage (message : Message){
    System.out.println("수신받은 메시지는 $message");
}

 

 

조금 더 자세한 설정을 하려면 어노테이션에 옵션을 추가해주면 된다.

@RabbitListener(bindings = [QueueBinding(
    value = Queue(value = "q1"),
    exchange = Exchange(value = "ex1"),
)])
fun receiveMessage (message : Message){
    System.out.println("수신받은 메시지는 $message");
}

 

https://cheese10yun.github.io/spring-rabbitmq/

 

Rabbit MQ 기초 사용법 - Yun Blog | 기술 블로그

Rabbit MQ 기초 사용법 - Yun Blog | 기술 블로그

cheese10yun.github.io

https://syaku.tistory.com/?page=7 

 

개발자 샤쿠 @syaku

Full Stack Web Developer.

syaku.tistory.com

 

 

3. Spring에서 메시지 Publish하기

// (1)
val rabbitTemplate: RabbitTemplate            

// (2)
val dto = mqDto(uid, req.amount)
rabbitTemplate.convertAndSend("ex1", "p2", objectMapper.writeValueAsString(dto)); // (exchangeName, routingKey, value)

(1) 생성자 주입

(2) 보낼 메시지를 dto로 포장해서 objectMapper.writeValueAsString 로 직렬화해서 전송한다.

 

 

 

 


5. 그 외 읽어보면 좋을 RabbitMQ 관련 이모저모

1. RabbitMQ에서 최적을 성능을 뽑는 팁?

https://kamang-it.tistory.com/627

 

[AMQP][RabbitMQ]RabbitMQ그리고 Work Queue의 사용법 - (3)

참고 [AMQP][RabbitMQ]RabbitMQ를 사용하는 이유와 설치방법 - (1) [AMQP][RabbitMQ]RabbitMQ아주 기초적이게 사용하기 - Java(feat.Hello World!) - (2) https://www.rabbitmq.com/tutorials/tutorial-two-java...

kamang-it.tistory.com

 

 

2. Spring boot Exchange와 Queue 자동 생성 이해

스프링 부트가 구동될때 exchange와 queue 설정에 따라 자동으로 생성되지 않는 다.

자동 생성 시점은 메시지가 발행이 될때와 메시지를 구독하기 위해 RabbitMQ 서버에 연결될때 생성된다.

아래 설정은 자동 생성 여부를 설정하기 위한 옵션이고 기본 값은 true 이다.

spring.rabbitmq.dynamic=true

https://syaku.tistory.com/?page=7 

 

개발자 샤쿠 @syaku

Full Stack Web Developer.

syaku.tistory.com

 

 

3. kafka vs RabbitMQ

앞선 내용들을 통해 각각의 정의 및 프로세스에 대해 면밀히(?) 살펴봤다.
위 내용을 통해서도 kafkaRabbitMQ의 차이에 대해 어느정도 이해할 수 있겠지만 본 글의 주제가 주제인만큼 다시 한번 간단히 정리해보도록 하겠다.

  1. kafka는 pub/sub 방식 / RabbitMQ는 메시지 브로커 방식
    kafka의 pub/sub방식은 생산자 중심적인 설계로 구성. 생성자가 원하는 각 메시지를 게시할 수 있도록 하는 메시지 배포 패턴으로 진행
    RabbitMQ의메시지브로커방식은 브로커 중심적인 설계로 구성. 지정된 수신인에게 메시지를 확인, 라우팅, 저장 및 배달하는 역할을 수행하며 보장되는 메시지 전달에 초점
  2. 전달된 메시지에 대한 휘발성
    RabbitMQ는 queue에 저장되어 있던 메시지에 대해 Event Consumer가져가게 되면 queue에서 해당 메시지를 삭제한다.
    하지만, kafka는 생성자로부터 메시지가 들어오면 해당 메시지를 topic으로 분류하고 이를 event streamer에 저장한다. 그 후, 수신인이 특정 topic에 대한 메시지를 가져가더라도 event streamer는 해당 topic을 계속 유지하기 때문에 특정 상황이 발생하더라도 재생이 가능하다.
  3. 용도의 차이
    kafka는 클러스터를 통해 병렬처리가 주요 차별점인 만큼 방대한 양의 데이터를 처리할 때, 장점이 부각된다.
    RabbitMQ는 데이터 처리보단 Manage UI를 제공하는 만큼 관리적인 측면이나, 다양한 기능 구현을 위한 서비스를 구축할 때, 장점이 부각된다.

 

 

https://velog.io/@cho876/%EC%B9%B4%ED%94%84%EC%B9%B4kafka-vs-RabbitMQ

 

카프카(kafka) vs RabbitMQ

오늘은 kafka와 RabbitMQ의 차이에 대해 알아보도록 하겠다. 1. 이해 1-1. 메시지 큐(MessageQuque : MQ) kafka와 rabbitMQ를 이해하기 위해선 우선 메시지 큐에 대한 이해가 선제적으로 필요하다. >메시지 큐(Mes

velog.io

  • 둘다 쩌는 솔루션이다.  -  RabbitMQ 가 좀더 성숙하다. (Written 12 Sep, 2012)
  • 철학은 좀 다른데, 기본적으로 RabbitMQ 는 브로커 중심적이며, 생산자와 소비자간의 보장되는 메세지 전달에 촛점을 맞추었다.  
  • 반면 Kafka 는 생산자 중심적이며, 엄청난 이벤트 데이터을 파티셔닝하는데 기반을 둔다. 배치 소비자를 지원하며, 온라인, 오프라인에 저 지연율(Low latency)을 보장하며 메세지를 전달해준다. 
  • RabbitMQ 는 브로커상에서 전달 상태를 확인하기위한 메세지 표식을 사용한다. 카프카는 그런 메세지 표식이 없으며 컨슈머가 전달(배달) 상태를 기억하는것을 기대한다.
  • 둘다 클러스터간의 상태를 관리하기위해 Zookeeper 를 사용한다. 
  •  RabbitMQ 는 커다란 크기의 데이터를 위해 디자인되지 않았으며 만약 컨슈머가 매우 느리다면 실패할것이다.그러나 post 2.0 에  RabbitMQ  는 느린 배치 컨슈머를 핸들링 되는게 요청되어졌다. 
  • Kafka 은 오직 토픽같은 exchanges 를 사용한다. RabbitMQ 는 다양한 exchanges 를 사용한다.
  • Kafka 는 파티션들 안에서 메세지 순서를 제공하며, 파티션들간에 엄격한 순서를 가진다. 카프카 컨슈머들은 충분히 스마트해야하며 , 그들 스스로 파티션간의 순서를 해결(resolve) 해야한다.
  • Kafka 는 디스크상에서 메세지를 저장하고 데이타 손실을 막기위해 클러스터로 그들을 복제한다. 각각의 성능에 큰 문제없이 브로커는 테라바이트를 핸들링할수있다. Kafka 는 쓰기에 초당  200k 메세지를 , 읽는데는 3M 메세지를  제공되도록 테스트되었다.

https://hamait.tistory.com/403?category=138704 

 

카프카(Kafka) vs RabbitMQ

생산자 (Sender) 테스트 결과 이유 카프카 생산자는 브로커로 부터의 ack 를  기다리지 않고 메세지를 보낸다.브로커가 핸들링 할수있는 만큼 빠르게 메세지를 마구 보낸다. 카프카는 좀더 효율

hamait.tistory.com

 

 

 

 

 

참고:

더보기

https://sg-choi.tistory.com/406

 

[RabbitMQ] RabbitMQ란?

RabbitMQ란? RabbitMQ는 AMQP를 따르는 오픈소스 메세지 브로커 AMQP는 클라이언트가 메시지 미들웨어 브로커와 통신할 수 있게 해주는 메세징 프로토콜 주요 개념으로 Producer, Exchange, Binding, Queue, Consu..

sg-choi.tistory.com

https://jin2rang.tistory.com/entry/RabbitMQ%EB%9E%80

 

RabbitMQ란?

RabbitMQ란? 서버간 메세지를 전달해주는 오픈소스 메세지 브로커이다. A → B에게 또는 A → B,C,D,E,F ... 등 메세지를 보내려고 할 때 RabbitMQ가 이 메세지를 받아서 전달 해주는 것으로 이해하면 된다

jin2rang.tistory.com

https://velog.io/@cckn/%EB%B2%88%EC%97%AD-RabbitMQ-%EB%B0%8F-Node.js%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-%EB%B9%84%EB%8F%99%EA%B8%B0-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C-%EC%84%9C%EB%B9%84%EC%8A%A4

 

(번역) RabbitMQ 및 Node.js를 사용한 비동기 마이크로 서비스

메세지와 이벤트를 통한 확장성과 내결함성.이 글은 Asynchronous Microservices with RabbitMQ and Node.js를 번역한 글입니다

velog.io

https://daily-coding-diary.tistory.com/13

 

[Node.js] RabbitMQ

목표 : RabbitMQ를 이용하여, 데이터를 임시로 저장하고 확인해봅시다. RabbitMQ는 디비 또는 데이터 처리에 문제가 생겼을때 데이터를 임시로 처리해줄때 많이 사용합니다. 예를 들면, 채팅방에 한

daily-coding-diary.tistory.com

https://www.joinc.co.kr/w/man/12/%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90/Message#sid_1

 

스타벅스로 살펴보는 Message 아키텍처

소프트웨어 시나리오

www.joinc.co.kr

 

nodejs로 개발을 하던 중 데이터베이스 테이블에 컬럼을 추가할 일이 있었다.

그때 사용한 방법을 기록해볼까 한다.


1. 데이터베이스 마이그레이션?

 

 

어디서 많이 들어보긴했지만 정확한 의미는 잘 몰랐다.

우선 마이그레이션이란 큰 의미로

"한 운영환경으로부터 다른 운영환경으로 옮기는 작업"

 

이라고 할 수 있다. Migration을 사전에 검색하면 나오는 '이주, 이송'의 개념이다.

하드웨어, 소프트웨어, 네트워크 등 넓은 범위에서 마이그레이션의 개념이 사용되고 있다. 

 

 

데이터베이스에서 한 예시로,

개발환경에서 스키마와 운영환경에서 스키마에 차이가 있을때 마이그레이션을 진행한다고 할 수 있다.

작게는 테이블 생성과 수정부터 하나의 애플리케이션 혹은 전체 시스템을 옮기는 것까지를 마이그레이션이라고 한다.

나는 데이터베이스에서 한 테이블에 컬럼을 추가하고자 한다.

 

 

 

 


2. 실습

현재 Nodejs에서 Sequelize 라이브러리로 데이터베이스를 조작하고 있다.

개발단계에서는 sync({force: true})를 통해서 컬럼을 강제로 추가할 순 있겠지만 운영단계에서는 불가능하다.

따라서 Sequelize에서 지원하는 마이그레이션으로 컬럼을 추가해볼까 한다.

 

 

0. Sequelize CLI 설치

# using npm
npm install --save-dev sequelize-cli
# using yarn
yarn add sequelize-cli --dev

 

 

 

1. init 명령

# using npm
npx sequelize-cli init
# using yarn
yarn sequelize-cli init

init 명령을 실행하면

  • config, 데이터베이스에 연결하는 방법을 CLI에 알려주는 구성 파일이 포함돼 있다.
  • models, 프로젝트의 모든 모델을 포함한다.
  • migrations, 모든 마이그레이션 파일 포함한다.
  • seeders, 모든 시드 파일을 포함한다.

이 4개의 폴더가 생성된다.

 

 

 

2. 데이터베이스 설정

config/config.json 파일에서 각 환경별로 나의 데이터베이스 정보를 입력해두어야 한다.

{
  "development": {
    "username": "mydb",
    "password": "mydb-password",
    "database": "postgres",
    "host": "mydb-dev.1234.ap-northeast-2.rds.amazonaws.com",
    "post": 5432,
    "dialect": "postgres"
  },
  "test": {
    "username": "root",
    "password": null,
    "database": "database_test",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "production": {
    "username": "root",
    "password": null,
    "database": "database_production",
    "host": "127.0.0.1",
    "dialect": "mysql"
  }
}

개발환경만 설정했다.

Sequelize CLI는 기본값이 mysql로 설정돼있다. 다른 데이터베이스를 사용하는 경우 "dialect"옵션의 내용을 수정해줘야한다.

또 Sequelize는 각 데이터베이스에 대한 포트로 기본값들을 사용한다.(postgres의 경우 5432포트)

다른 포트를 지정해야 하는 경우 "port"필드를 추가하여 설정할 수 있다.

 

 

 

3. 마이그레이션 파일 생성

sequelize migration:create --name test-migration

이 명령어로 마이그레이션 설정 파일을 생성한다. --name 옵션으로 파일의 이름을 지정한다.

그러면 migration 폴더에 20220318184913-test-migration.js 이라는 파일이 생성될 것이다.(날짜-이름.js)

 

 

이렇게 생긴 파일이다.

"use strict"

module.exports = {
  up: function (queryInterface, Sequelize) {
    /*
      Add altering commands here.
      Return a promise to correctly handle asynchronicity.

      Example:
      return queryInterface.createTable('users', { id: Sequelize.INTEGER });
    */
  },

  down: function (queryInterface, Sequelize) {
    /*
      Add reverting commands here.
      Return a promise to correctly handle asynchronicity.

      Example:
      return queryInterface.dropTable('users');
    */
  },
}

up 메소드에는 마이그레이션 할 내용을 기재하고 

down 메소드에는 롤백할 내용을 기재한다.

 

 

컬럼을 추가하는 up메서드를 작성해보자.

'use strict';

module.exports = {
  up: async (queryInterface, Sequelize) => {
    return queryInterface.addColumn('product', 'url', {
      type: Sequelize.STRING,
      allowNull: false,
      defaultValue: 'https://myProduct.s3.ap-northeast-2.amazonaws.com/default.png',
    });
  },

  down: async (queryInterface, Sequelize) => {
    return queryInterface.removeColumn("product", "url")
  },
};

up 메소드에는 product테이블에 상품의 사진을 저장하는 url 컬럼을 추가한다.

down 메소드에는 up 메소드에 대한 롤백 내용을 기재해보았다.

 

 

 

4. 마이그레이션 진행

sequelize db:migrate --env development

를 통해서 up 메소드에 기재한 마이그레이션을 실행할 수 있다.

--env는 default로 development로 설정돼있긴하다.

 

 

 

5. 롤백

롤백은 이렇게 해주면 된다.

sequelize db:migrate:undo --env development

 

 

 

6. 다중 마이그레이션 

해보진 않았지만 잘 정리된 블로그의 내용을 참고했다.

 

만약 컬럼을 여러개 추가할 때는 어떻게 해야할까.

up() 함수는 프라미스를 리턴하게 되어있는데 promise로 구성된 배열을 반환해도 된다.

컬럼을 여러개 추가할 것이라면 addColumn()을 배열에 담아 리턴하면 된다.

물론 롤백할 때도 동일하게 removeColumn()를 배열에 담아서 반환한다. 

module.exports = {
  up: function (queryInterface, Sequelize) {
    return [
      queryInterface.addColumn("product", "url", {
        type: Sequelize.STRING,
      }),
      queryInterface.addColumn("product", "price", {
        type: Sequelize.STRING,
      }),
    ]
  },

  down: function (queryInterface, Sequelize) {
    return [
      queryInterface.removeColumn("product", "url"),
      queryInterface.removeColumn("product", "price"),
    ]
  },
}

물론 직접 raw 쿼리를 사용할 수도 있다.

module.exports = {
  up: function (queryInterface, Sequelize) {
    var sql = "ALTER TABLE product ADD COLUMN url varchar(255) NOT NULL"

    return queryInterface.sequelize.query(sql, {
      type: Sequelize.QueryTypes.RAW,
    })
  },

  down: function (queryInterface, Sequelize) {
    var sql = "ALTER TABLE product DROP COLUMN url"

    return queryInterface.sequelize.query(sql, {
      type: Sequelize.QueryTypes.RAW,
    })
  },
}

 

 

 

 

 

 

 

참고:

더보기

최근 코틀린을 처음으로 사용해보고 있다.

처음 언어를 배울때 일단 부딪혀가며 습득하자는 생각이라 주먹구구식으로 일단은 개발했던것 같다.

그 과정에서 정리가 필요한 부분들을 느꼈고 간단하게나마 적어보려한다.

 


1. 기본생성자

코틀린 클래스는 다음과 같이 생성한다.

class TestClass {
}

이 클래스는 생성자도 프로퍼티(얘도 상당히 중요한 개념인것 같다. 다음 글에서 정리해봐야겠다)도 없는 클래스다

여기에 기본생성자를 추가해보자면,

 

 

 

 

class TestClass(name: String, age: Int){
}

이런 모양이 된다. 이름과 나이를 초기화할 수 있는 기본생성자를 추가해보았다.

 

 

 

 

만약, 자바였다면 다음과 같았을 것이다.

class TestClass {    
        String name;
        int age;
    
        // 생성자
        public TestClass(String n1, int n2) {
                this.name = n1;
                this.age = n2;
        }
}

 

코틀린이 훨씬 간결하다.

 

 

 


2. init

바로 위의 코드처럼 자바에서는 생성자에서 바로 멤버변수들을 초기화 해줄 수 있다.

하지만 코틀린에서는 그러지 못하고 다음과 같은 방법을 사용할 수 있다.

class TestClass(name: String, age: Int){
    val name: String = "kim"
    var age: Int = 0
    init {
        this.age = age
    }
}

init은 기본생성자 호출이후 바로 다음에 호출되는 키워드이다.

 

 

 

하지만 우린 굳이 init 안쓰고 받아온 매개변수를 사용할 수 있다.

class TestClass(var name: String, val age: Int){
    fun introduce() {
        println("Hi! I'm $name and I'm $age years old")
    }
}

기본생성자의 매개변수 옆에 var과 val가 슬쩍 생긴것을 볼 수 있다.

얘네들을 붙혀주면 생성자의 매개변수를 클래스 내부에서 사용할 수 있다.

 

 

 

 


3. 보조생성자 constructor()

클래스 이름 옆에서만 생성자를 만들 수 있는걸까?

생성자를 여러개 만들고 싶을때는 어떻게 하면될까..

 

우리는 constructor 키워드를 사용할 수 있다.

class TestClass{
    var name = ""
    var age = 0
    constructor(name: String, age: Int){
        this.name = name
        this.age = age
    }
}

참고로 var name = ""에서 자동으로 String으로 타입추론이 되기에 :String이라고 명시해주지 않아도 된다.

 

 

하지만 이렇게 constructor로 생성자를 선언해주면 똑똑한 인텔리제이에서는 이러지 말라고 권유한다.

"Convert to primary constructor"를 누르면

 

 

이렇게 다시 정리가 된다.

 

 

아무튼 constructor 키워드로 생성자를 여러개 선언해 줄 수 있다.

class TestClass{
    var name = ""
    var age = 0
    
    constructor(name: String, age: Int){
        this.name = name
        this.age = age
    }

    constructor(age: Int){
        this.age = age
    }

    constructor(name: String){
        this.name = name
    }
}

 

 

하지만,

constructor 키워드를 사용할 때 한가지 주의할 것이 있다.

기본 생성자를 선언하고 constructor를 사용하면 다음과 같이 에러가 발생한다.

 

 

인텔리제이가 하라는 대로 this()를 추가해주면 에러는 사라진다.

왜 이럴까?

 

 

 

constructor로부터 생성된 생성자는 기본 생성자를 상속받아야 한다.

그렇기 때문에 기본 생성자를 상속받고 난 이후에는 에러가 사라진 것이다.

물론 기본 생성자를 상속 받는 것이니 constructor로 만든 생성자들은 반드시 기본 생성자가 갖고 있는 인자들을 갖고 있어야 한다.

 

 

 

이 경우에는 기본생성자에 name이 있지만 보조생성자에는 없다.

this()에 name을 넣어주도록 하자

 

 

 

에러 해결

 

 

 

기본생성자에는 존재하는 name이 보조생성자에는 없기에 에러가 발생한 모습이다.

 

 

 

생성자들의 호출 흐름을 보자면 다음과 같다.

https://jaejong.tistory.com/50

 

 

 

 

참고:

더보기

+ Recent posts