사이트마다 아이디, 비밀번호 생성 규칙도 다르고 다 똑같으면 보안이 취약하니, 가입할 때 계정을 다 다르게 했다.
모든 계정을 외울 수 없어서 메모장에 계정 정보를 적어놨었다.
그런데 보기에 지저분하고 사용하기도 불편해서, 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; }
인수들이 잘 전달되는 것을 확인할 수 있다.

이제 진짜 본격적으로 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 --help 명령어다.

계정 추가할 때 이미 계정이 있는 도메인을 입력하면 위첢 에러 메시지가 출력된다.
이런 식으로 에러가 발생하면 안내 메시지를 출력하도록 했다.
후기
계정 관리 프로그램을 완성했다 짝짝짝!!!
웹 개발이 아닌 프로그램 개발은 개발 공부를 시작한 이후 처음이다.
로직 자체는 간단하지만, main 함수에서 인수를 다루는 것과 외부 라이브러리를 사용하고 명령어를 만드는 등 개발적으로 공부가 많이 되었다.
그리고 처음 개발한 프로그램이 필요했던 프로그램이라 그런지 더 뿌듯하다 :)