프로젝트/계정 관리 프로그램

C++로 계정 관리 프로그램 개발하기

Junheehee 2022. 8. 14. 13:50

사이트마다 아이디, 비밀번호 생성 규칙도 다르고 다 똑같으면 보안이 취약하니, 가입할 때 계정을 다 다르게 했다.

모든 계정을 외울 수 없어서 메모장에 계정 정보를 적어놨었다.

그런데 보기에 지저분하고 사용하기도 불편해서, c++로 아주 간단하게 계정 관리 프로그램을 개발해보자.

 

 

 

 

기획

 

인터페이스는 GUI가 아니라, 터미널에서 명령어로 동작하는 CLI로 만들 것이다.

만들 명령어는

 

  1. account / account --all

    모든 계정을 출력한다.

  2. account 사이트도메인

    해당 사이트의 계정을 출력한다.

  3. account --help

    계정 관리 프로그램의 모든 명령어에 대한 설명을 출력한다.

  4. account --add

    계정을 추가한다.
  5. account --update

    계정을 수정한다.

  6. account --delete

    계정을 삭제한다.

 

이때 사이트 도메인은 www.naver.com 같이 도메인 전체가 아닌 도메인 이름 naver를 저장한다.

서브 도메인을 입력하지 않는 이유는 보통 서브 도메인마다 따로 계정을 관리하지 않아서다.

 

 

위 명령어는 c++ 파일을 컴파일해서 생긴 실행파일을 실행시킨다.

account 뒤에 붙는 도메인 이름, --help 등의 명령어는 실행파일에 인수로 전달해준다.

예를 들어 --help가 인수로 들어오면 명령어에 대한 설명을 출력하는 함수를 호출도록 하자.

 

 

계정은 json 파일에 저장할 거다.

 

{
  "name": "계정 관리 프로그램",
  "version": "1.0.0",
  "accounts": [
    {
      "domain": "naver",
      "id": "네이버 아이디",
      "password": "네이버 비밀번호",
      "memo": "메모가 필요하면 작성"
    }, ...
  ]
}

 

acounts가 계정 정보들을 리스트로 저장한다.

 

 

 

 

개발

 

우선 main 함수를 만들어보자.

실행파일에서 전달한 인수를 main 함수에서 접근하기 위해선 argc와 *argv[]를 파라미터로 받으면 된다.

argc는 ARGument Count의 약자로 인수의 개수를 의미하고, argv는 ARGument Vector로 인수 배열을 의미한다.

테스해보자.

 

argc와 argv를 출력하는 코드를 간단하게 만들었다.

 

#include <iostream>
using namespace std;

int main(int argc, char *argv[]) {
  cout << "argc: " << argc << "\n";
  for (int i = 0; i < argc; i++) {
    cout << i << "번째 argv: " << argv[i] << "\n";
  }

  return 0;
}

 

인수들이 잘 전달되는 것을 확인할 수 있다.

 

파라미터 argc와 argv로 인수 사용

 

 

이제 진짜 본격적으로 main함수를 만들어보자.

main 함수에는 전달 받은 인수에 따라 알맞은 함수를 호출하는 기능을 넣을 것이다.

 

먼저 인수의 개수로 구분한다.

  0개: 모든 계정 출력

  2개 이상: 인수가 너무 많다는 안내 메시지 출력

 

인수가 1개일 때는 두 경우로 구분된다.

 

첫번째는 naver, google처럼 검색하고 싶은 도메인 이름인 경우,

두번째는--help, --add처럼 특정 기능을 나타내는 경우이다.

두번째 경우에 --를 붙인 이유는 도메인 이름과 구별하기 위해서다.

아마 없겠지만 혹시 help나 add가 도메인 이름이면 중복되기 때문이다.

 

 

// main 함수

int main(int argc, char *argv[]) {
  if (argc == 1) {
    searchAllAccounts();
  } else if (argc == 2) {
    string arg = argv[1];
    if (arg.size() > 2 && arg.substr(0, 2) == "--") {
      string command = arg.substr(2);
      if (command == "all") {
        searchAllAccounts();
      } else if (command == "help") {
        helpCommand();
      } else if (command == "add") {
        addAccount();
      } else if (command == "update") {
        updateAccount();
      } else if (command == "delete") {
        deleteAccount();
      } else {
        printStartMessage("main");
        handleError("wrong command");
      }
    } else {
      searchAccount(argv[1]);
    }
  } else {
    printStartMessage("main");
    handleError("too lot arguments");
  }

  return 0;
}

 

 

인수에 "--" 유무를 판단하기 위해 string 클래스의 문자열을 자르는 substr 메서드를 사용했다.

substr의 첫번째 인수는 시작 인덱스, 두번째 인덱스는 문자열 길이이다.

만약 두번째 인수가 없으면 끝까지 자른다.

즉 "--help"의 substr(0, 2)는 "--"이고, substr(2)는 "help"다.

 

올바르지 않은 인수를 전달하는 경우는 handleError 함수로 처리했다.

 

이제 main 함수에서 호출하는 6가지 함수를 만들자.

 

 

이 함수들은 계정 정보가 들어있는 json 파일을 읽거나 수정할 수 있어야 하므로,

json용 라이브러리가 필요하다.

여러가지가 있는데 나는 오픈소스인 nlohmann/json을 이용했다.

설치는 brew를 통해서 했고,

사용법은 아래 깃허브 페이지에 잘 나와있다.

 

 

GitHub - nlohmann/json: JSON for Modern C++

JSON for Modern C++. Contribute to nlohmann/json development by creating an account on GitHub.

github.com

 

nlohmann-json

Homebrew’s package index

formulae.brew.sh

 

그리고 include 해서 사용하면 되는데, 여기서 엄청 고생했다;;;;;;;;;;

 

brew로 설치한 c++ library를 사용하기 위해선 따로 path 설정을 해줘야한다.

.zshrc 파일에 아래 코드를 추가했다.

(homebrew 설정마다 경로가 다를 수 있다.)

 

export CPATH=/opt/homebrew/include
export LIBRARY_PATH=/opt/homebrew/lib

 

 

그리고 c++에서 다른 파일(여기선 json 파일)을 불러오기 위해 fstream를 사용했다.

 

즉 계정 정보가 들어 있는 외부 json 파일을 제어하기 위해 fstream을 사용했고,

json으로 저장된 계정 정보를 처리하기 위해 nlohmann/json을 사용했다.

아래는 예시다.

 

using json = nlohmann::json;

ifstream f("data.json");
json accounts = json::parse(f);

cout << accounts["name"]; // "계정 관리 시스템"

 

 

이를 이용해 main함수에서 호출하는 함수들을 만들었다.

 

만들 때 가장 집중한 건 일어날 수 있는 모든 경우를 헨들링하는 것이었다.

예를 들어 정보를 입력하지 않으면 다음으로 넘어가지 않도록 만들었다.

또, 계정을 추가할 때 이미 계정이 존재하는 도메인이면 추가가 안되게 하기도 했다.

 

그리고 좀 프로그램답게 만들고 싶어 시작과 종료시 안내 메시지도 출력했다.

 

 

최종 c++ 코드다!!!

 

#include <fstream>
#include <iostream>
#include <nlohmann/json.hpp>
#include <string>
using namespace std;
using json = nlohmann::json;

ifstream i("data.json");
json accounts = json::parse(i);

void updateDataFile(void) {
  ofstream o("data.json");
  o << accounts;
}

void printStartMessage(string func) {
  string programName = accounts["name"];
  string programVersion = accounts["version"];
  cout << "_________________________________________\n";
  cout << "-----------------------------------------\n";
  cout << "        " << programName << " " << programVersion << "\n";
  if (func == "add") {
    cout << "----------------계정 추가----------------\n";
  } else if (func == "update") {
    cout << "----------------계정 수정----------------\n";
  } else if (func == "delete") {
    cout << "----------------계정 삭제----------------\n";
  } else if (func == "search") {
    cout << "----------------계정 검색----------------\n";
  } else if (func == "searchAll") {
    cout << "----------------모든 계정----------------\n";
  } else if (func == "help") {
    cout << "----------------사용 방법----------------\n";
  } else if (func == "main") {
    cout << "-----------------------------------------\n";
  }
  cout << "\n";
  return;
}

void printEndtMessage(string func) {
  cout << "\n";
  if (func == "add") {
    cout << "----------------추가 완료----------------\n";
  } else if (func == "update") {
    cout << "----------------수정 완료----------------\n";
  } else if (func == "delete") {
    cout << "----------------삭제 완료----------------\n";
  } else if (func == "search" || func == "searchAll") {
    cout << "----------------검색 완료----------------\n";
  } else if (func == "error") {
    cout << "----------------에러 발생----------------\n";
  }
  cout << "-----------------------------------------\n";
  return;
}

int checkAccountIndex(string domain) {
  for (int iter = 0; iter < (int)accounts["accounts"].size(); iter++) {
    if (accounts["accounts"][iter]["domain"] == domain) {
      return iter;
    }
  }
  return -1;
}

void handleError(string errorMessage) {
  if (errorMessage == "existed account") {
    cout << "\n계정이 이미 존재합니다";
  } else if (errorMessage == "no account") {
    cout << "\n해당 도메인의 계정이 존재하지 않습니다";
  } else if (errorMessage == "wrong again domain") {
    cout << "\n도메인이 틀립니다";
  } else if (errorMessage == "wrong command") {
    cout << "\n잘못된 명령어입니다\n";
    cout << "\naccount --help\n  모든 명령어를 출력합니다\n";
  } else if (errorMessage == "too lot arguments") {
    cout << "\n인수가 너무 많습니다\n";
    cout << "\naccount --help\n  모든 명령어를 출력합니다\n";
  }
  printEndtMessage("error");
  return;
}

void searchAccount(string domain) {
  printStartMessage("search");
  int index = checkAccountIndex(domain);
  if (index == -1) {
    cout << "계정이 존재하지 않습니다\n";
    printEndtMessage("search");
    return;
  }
  string _domain = accounts["accounts"][index]["domain"];
  string id = accounts["accounts"][index]["id"];
  string password = accounts["accounts"][index]["password"];
  string memo = accounts["accounts"][index]["memo"];
  cout << "도메인: " << _domain << "\n";
  cout << "아이디: " << id << "\n";
  cout << "비밀번호: " << password << "\n";
  if (memo != "") {
    cout << "메모: " << memo << "\n";
  }
  printEndtMessage("search");
  return;
}

void searchAllAccounts(void) {
  printStartMessage("searchAll");
  cout << "-----------------------------------------\n";
  for (int iter = 0; iter < (int)accounts["accounts"].size(); iter++) {
    string _domain = accounts["accounts"][iter]["domain"];
    string id = accounts["accounts"][iter]["id"];
    string password = accounts["accounts"][iter]["password"];
    string memo = accounts["accounts"][iter]["memo"];
    cout << "도메인: " << _domain << "\n";
    cout << "아이디: " << id << "\n";
    cout << "비밀번호: " << password << "\n";
    if (memo != "") {
      cout << "메모: " << memo << "\n";
    }
    cout << "-----------------------------------------\n";
  }
  printEndtMessage("searchAll");
  return;
}

void addAccount(void) {
  printStartMessage("add");
  string domain = "", id = "", password = "", memo = "";
  while (domain == "") {
    cout << "도메인을 입력하세요\n";
    getline(cin, domain);
  }
  if (checkAccountIndex(domain) != -1) {
    cout << "\n이미 존재하는 도메인입니다\n";
    handleError("existed account");
    return;
  }
  while (id == "") {
    cout << "\n아이디를 입력하세요\n";
    getline(cin, id);
  }
  while (password == "") {
    cout << "\n비밀번호를 입력하세요\n";
    getline(cin, password);
  }
  cout << "\n메모를 입력하세요\n";
  getline(cin, memo);
  json newAccount = {
      {"domain", domain},
      {"id", id},
      {"password", password},
      {"memo", memo},
  };
  accounts["accounts"].push_back(newAccount);
  updateDataFile();
  printEndtMessage("add");
  return;
}

void updateAccount(void) {
  printStartMessage("update");
  string domain;
  while (domain == "") {
    cout << "도메인을 입력하세요\n";
    getline(cin, domain);
  }
  int index = checkAccountIndex(domain);
  if (index == -1) {
    handleError("no account");
    return;
  }
  cout << "\n무엇을 수정하시겠습니까\n";
  cout << "  (1) 아이디\n";
  cout << "  (2) 비밀번호\n";
  cout << "  (3) 메모\n";
  string attrNum = "";
  while (attrNum != "1" && attrNum != "2" && attrNum != "3") {
    cout << "\n번호를 입력하세요\n";
    getline(cin, attrNum);
  }
  string newData = "";
  if (attrNum == "1") {
    while (newData == "") {
      cout << "\n아이디를 입력하세요\n";
      getline(cin, newData);
    }
    accounts["accounts"][index]["id"] = newData;
  } else if (attrNum == "2") {
    while (newData == "") {
      cout << "\n비밀번호를 입력하세요\n";
      getline(cin, newData);
    }
    accounts["accounts"][index]["password"] = newData;
  } else if (attrNum == "3") {
    while (newData == "") {
      cout << "\n메모를 입력하세요\n";
      getline(cin, newData);
    }
    accounts["accounts"][index]["memo"] = newData;
  }
  updateDataFile();
  printEndtMessage("update");
  return;
}

void deleteAccount(void) {
  printStartMessage("delete");
  string domain;
  while (domain == "") {
    cout << "도메인을 입력하세요\n";
    getline(cin, domain);
  }
  int index = checkAccountIndex(domain);
  if (index == -1) {
    handleError("no account");
    return;
  }
  string _domain = accounts["accounts"][index]["domain"];
  string id = accounts["accounts"][index]["id"];
  string password = accounts["accounts"][index]["password"];
  string memo = accounts["accounts"][index]["memo"];
  cout << "\n도메인: " << _domain << "\n";
  cout << "아이디: " << id << "\n";
  cout << "비밀번호: " << password << "\n";
  if (memo != "") {
    cout << "메모: " << memo << "\n";
  }
  cout << "\n도메인을 입력하면 삭제가 진행됩니다\n";
  string domainAgain = "";
  getline(cin, domainAgain);
  if (domain == domainAgain) {
    accounts["accounts"].erase(accounts["accounts"].begin() + index);
    updateDataFile();
    printEndtMessage("delete");
  } else {
    handleError("wrong again domain");
  }
  return;
}

void helpCommand(void) {
  printStartMessage("help");
  cout << "account \"도메인\"\n  해당 도메인의 계정을 출력합니다\n";
  cout << "\naccount / account --help\n  모든 계정을 출력합니다\n";
  cout << "\naccount --add\n  계정을 추가합니다\n";
  cout << "\naccount --update\n  계정을 수정합니다\n";
  cout << "\naccount --delete\n  계정을 삭제합니다\n";
  cout << "\naccount --help\n  모든 명령어를 출력합니다\n";
  printEndtMessage("help");
  return;
}

int main(int argc, char *argv[]) {
  if (argc == 1) {
    searchAllAccounts();
  } else if (argc == 2) {
    string arg = argv[1];
    if (arg.size() > 2 && arg.substr(0, 2) == "--") {
      string command = arg.substr(2);
      if (command == "all") {
        searchAllAccounts();
      } else if (command == "help") {
        helpCommand();
      } else if (command == "add") {
        addAccount();
      } else if (command == "update") {
        updateAccount();
      } else if (command == "delete") {
        deleteAccount();
      } else {
        printStartMessage("main");
        handleError("wrong command");
      }
    } else {
      searchAccount(argv[1]);
    }
  } else {
    printStartMessage("main");
    handleError("too lot arguments");
  }

  return 0;
}

 

 

마지막으로 계정 관리 프로그램을 실행시킬 account 명령어도 만들었다.

.zshrc에 아래 코드를 추가했다.

프로그램을 실행하면 현재 경로가 프로그램이 있는 디렉토리로 변경된다.

원래 경로로 돌아가기 위해 pwd로 프로그램 실행 전 경로를 로컬 변수에 저장한 뒤, 실행이 끝나면 원래 경로로 이동하게 했다.

$@는 모든 인수를 의미한다.

 

account() {
        local nowDir=`pwd`
        cd ~/dev/account-program
        ./main.exe $@
        cd $nowDir
}

 

 

 

 

테스트

 

프로그램이 잘 작동하는지 테스트해보자.

 

 

테스트: account

 

아직 저장된 계정이 없다.

계정을 추가해보자.

 

 

테스트: account --add

 

추가가 잘 된다.

저장된 계정을 수정해보자.

 

 

테스트: account --update

 

수정 사항이 적용되었는지 확인해보자.

 

 

테스트: account 도메인

 

수정 사항도 잘 적용되었다.

 

 

테스트: account --delete

 

삭제를 할 때는 도메인을 한번 더 입력하게 해서 확인 절차를 진행하도록 만들었다.

 

 

테스트: account --help

 

사용 방법을 확인할 수 있는 account --help 명령어다.

 

 

테스트: 에러 핸들링

 

계정 추가할 때 이미 계정이 있는 도메인을 입력하면 위첢 에러 메시지가 출력된다.

이런 식으로 에러가 발생하면 안내 메시지를 출력하도록 했다.

 

 

 

 

후기

 

계정 관리 프로그램을 완성했다 짝짝짝!!!

웹 개발이 아닌 프로그램 개발은 개발 공부를 시작한 이후 처음이다.

로직 자체는 간단하지만, main 함수에서 인수를 다루는 것과 외부 라이브러리를 사용하고 명령어를 만드는 등 개발적으로 공부가 많이 되었다.

그리고 처음 개발한 프로그램이 필요했던 프로그램이라 그런지 더 뿌듯하다 :)