사이트마다 아이디, 비밀번호 생성 규칙도 다르고 다 똑같으면 보안이 취약하니, 가입할 때 계정을 다 다르게 했다.
모든 계정을 외울 수 없어서 메모장에 계정 정보를 적어놨었다.
그런데 보기에 지저분하고 사용하기도 불편해서, 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를 통해서 했고,
사용법은 아래 깃허브 페이지에 잘 나와있다.
그리고 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 함수에서 인수를 다루는 것과 외부 라이브러리를 사용하고 명령어를 만드는 등 개발적으로 공부가 많이 되었다.
그리고 처음 개발한 프로그램이 필요했던 프로그램이라 그런지 더 뿌듯하다 :)