CS50 Probelm set 2 - 카이사르 암호

dondonee·2023년 1월 31일
0

CS50

목록 보기
6/12
post-thumbnail

카이사르 암호

카이사르는 기밀문서의 알파벳 문자를 일정 자릿수만큼 떨어진 다른 알파벳 문자로 바꾸는 방식으로 암호문을 만들었던 것으로 추정된다. 예를 들면 A는 B, B는 C, C는 D로 … 그리고 다시 Z는 A로 바꾸는 식이다. 이렇게 암호화된 메세지를 받은 수신인은 암호화에 사용된 것과 같은 자릿수만큼 반대 방향으로 변환하여 해독했다.

일반적으로 암호화되지 않은 글을 plaintext(평문)라고 하고, 암호화된 글은 ciphertext(암호문), 그리고 약속된 비밀은 key(암호키)라고 부른다. 좀 더 공식적으로 표현하자면, 카이사르의 알고리즘은 plaintext의 각 문자를 일정 자릿수만큼 회전시켜서 암호화를 하는 것이다. 이것을 공식으로 만들어보면 아래와 같다.

ciphertext = (plaintext + key) % 26


Caesar 과제

사용자에게 평문 텍스트와 암호키를 입력받고, 카이사르의 암호 알고리즘에 따라 암호문으로 변환하여 출력하는 프로그램을 만든다.

$ ./caesar 1
plaintext:  HELLO
ciphertext: IFMMP
$ ./caesar 13
plaintext:  hello, world
ciphertext: uryyb, jbeyq

사용자가 잘못된 값을 입력하는 경우 아래와 같은 메세지를 출력한다.

$ ./caesar HELLO
Usage: ./caesar key
$ ./caesar
Usage: ./caesar key
$ ./caesar 1 2 3
Usage: ./caesar key

지시사항

  • 암호키로 한 개의, 음수가 아닌 명령행 인자를 받는다. (편의상 이 수를 k로 부른다.)
  • 만약 프로그램이 어느 인자도 없이 실행되거나, 또는 하나 이상의 인자를 입력받는다면 에러메세지를 출력하고 main 함수가 즉시 1을 반환하도록 한다.
  • 만약 명령행 인자의 문자 중 하나라도 십진 숫자가 아닌 경우 Usage: ./caesar key 에러 메세지를 출력하고 main 함수가 1을 반환하도록 한다.
  • k가 반드시 26 이하의 숫자인 것은 아니다. 프로그램은 2^31-26 보다 작은, 음수가 아닌 모든 정수값에 대해 동작해야 한다. 사용자가 int 타입에 비해 너무 큰 값을 입력하는 경우에 대해서는 고려하지 않아도 된다. 하지만 만약 k가 26보다 크다고 해도, 인풋 값의 알파벳 문자는 출력될 때도 알파벳 문자여야 한다. 예를 들어서 k가 27이고 암호화 할 문자가 A인 경우, ASCII 코드에서 [A에서 27 자리만큼 떨어져 있기는 하지만, [가 되어서는 안된다. A는 알파벳 문자 코드 안을 돌아서 는 B가 되고, ZA가 되어야 한다.
  • 프로그램은 plaintext:를 줄바꿈 없이 출력하고, 유저에게 string 타입의 인풋을 받는 프롬프트를 출력한다. (get_string 사용)
  • 프로그램은 ciphertext:를 줄바꿈 없이 출력하고, 평문의 각 알파벳 문자를 k 자릿수만큼 변환한 암호문을 출력한다. 알파벳이 아닌 문자는 변환하지 않고 출력한다.
  • 대문자는 대문자로만 변환되고, 소문자는 소문자로만 변환된다.
  • 암호문을 출력한 뒤, 줄바꿈 문자를 출력한다. 그리고 main 함수로부터 0을 반환하고 프로그램을 종료한다.


✍️ 풀이

명령행 인자 유효성 체크

bool checkArg(int argc, char **argv)
{
    if (argc != 2 || strspn(argv[1], "0123456789") != strlen(argv[1]))
    {
        return false;
    }

    return true;
}
  1. 명령행 인자 값이 단일한지 체크
    • 하나의 명령행 인자와 프로그램이 실행될 경우 main 함수의 argv[0]에는 프로그램의 이름이 저장되고, argv[1]에 명령행 인자가 저장된다. argc는 argv[]의 개수를 의미하므로 값은 2가 된다. 따라서 argc가 2가 아닌 경우 checkArg()는 false를 반환한다.
  2. argv[1]이 십진 숫자인지 체크
    1. 첫 코드는 for 루프로 argv[1]의 요소를 처음부터 하나씩 isdigit(argv[1][i])가 0인지 검사하는 방식이었다. isdigit()의 값이 0인 경우 인수가 십진 숫자가 아닌 것이므로 checkArg()는 false를 반환한다.
    2. string.h 에 정의된 strspn() 함수를 발견해서 코드를 수정했더니 훨씬 간결해졌다. 라이브러리의 함수를 사용하면 코드가 간결해지고, 가독성이 좋아지고, 실수를 방지하며, 재사용이 용이하다는 등의 장점이 있다고 한다. 라이브러리에 지나치게 의존하면 좋지 않다고 하지만 기본적인 라이브러리는 잘 활용하는 것이 좋은 것 같다.
      • strspn(string1, string2)은 string1이 string2에 사용된 문자들로 구성되어 있는지 검사하는 함수다. string1에서 string2에 없는 문자가 처음으로 발견된 위치를 반환한다. string1이 string2에 없는 문자로 시작되는 경우 함수는 0을 반환한다. string1의 모든 문자가 string2에 사용된 문자들로 구성된 경우 string1의 길이를 반환한다.

문자 암호화

과제 안내문에 제시된 카이사르의 암호화 공식은 ciphertext = (plaintext + key) % 26 였는데, 문제는 ASCII 표에서 알파벳 문자의 위치가 0부터 시작 되는 것이 아닌데다, 소문자와 대문자를 구분해야 하는 것이었다.

  1. 내 코드는 아래와 같다. if 조건문에서 인수 문자가 소문자인 경우와 대문자인 경우를 구분해서 리턴값을 다르게 했다.

    char encrypt(int key, char character)
    {
        int position;
        
        if (isupper(character))
        {
            position = (character - 65 + key) % 26;
            return 65 + position;
        }
        
        if (islower(character))
        {
            position = (character - 97 + key) % 26;
            return 97 + position;
        }
    
        return character;
    }
  2. 아래는 ChatGPT에게 encrypt()의 최적화를 요구한 코드다. isalpha()와 삼항 연산자를 이용해서 코드를 축약했다.

    char encrypt(int key, char character)
    {
        if (isalpha(character))
        {
            return (char)((character - (islower(character) ? 97 : 65) + key) % 26 + (islower(character) ? 97 : 65));
        }
        return character;
    • 코드가 획기적으로 축약되기는 했는데, 이게 좋은 코드일까? 하는 의문이 생겼다. 검색해보니 삼항연산자는 조건문을 한 줄로 압축하기 때문에 코드는 간결해지만, 같은 이유로 가독성이 떨어질 수도 있다고 한다. 또한 한 줄씩 디버깅을 할 수 없다는 단점도 있으므로, 대부분의 경우에는 사용을 권장하지 않는다는 것 같다[1].

암호화된 문자열 반환

encrypt() 함수로 암호화한 문자들을 저장한 문자열을 반환하는 getCiphertext() 함수를 만들었다. C언어는 string 타입이 없고 char의 배열로 문자열을 만든다. 즉 문자열을 저장하기 위해 고정된 크기의 데이터 타입이 없기 때문에, 사용자의 입력에 따라 문자열의 크기가 달라지는 경우에는 ‘메모리의 동적 할당’이 필요하다.

동적 메모리 할당 또는 메모리 동적 할당은 컴퓨터 프로그래밍에서 실행 시간 동안 사용할 메모리 공간을 할당하는 것을 말한다. 사용이 끝나면 운영체제가 쓸 수 있도록 반납하고 다음에 요구가 오면 재 할당을 받을 수 있다. 이것은 프로그램이 실행하는 순간 프로그램이 사용할 메모리 크기를 고려하여 메모리의 할당이 이루어지는 정적 메모리 할당과 대조적이다[2].

  • stdlib.h에 정의되어 있는 malloc()을 사용한 코드다.
    char *getCiphertext(int key, char *plaintext)
    {
        size_t len = strlen(plaintext);
        char *result = malloc(len);
    
        for (int i = 0; i < (int)len; i++)
        {
            result[i] = encrypt(key, plaintext[i]);
        }
    
        return result;
    }
  • string.h에 정의된 strdup()을 사용한 코드다.
    char *getCiphertext(int key, char *plaintext)
    {
        size_t len = strlen(plaintext);
        char *result = strdup(plaintext);
    
        for (int i = 0; i < (int)len; i++)
        {
            result[i] = encrypt(key, plaintext[i]);
        }
    
        return result;
    }
  • strdup()malloc()strcpy()를 차례로 실행하여 힙 영역에 문자열을 복사하는 것이므로, getCiphertext()에서는 필요없는 복사 작업이 수행되는 것이다. 따라서 첫 번째의 malloc() 코드를 선택했다.
  • malloc()strdup() 모두 사용 후에는 free()를 사용하여 메모리를 해제해주어 메모리 낭비를 없애야 한다.

Main()

int main(int argc, char **argv)
{
    if (checkArg(argc, argv) == false)
    {
        printf("usage: ./caesar key\n");
        return 1;
    }

    const int key = atoi(argv[1]);
    char *plaintext = get_string("plaintext:  ");
    char *ciphertext = getCiphertext(key, plaintext);

    printf("ciphertext: %s", ciphertext);
    free(ciphertext);
    printf("\n");

    return 0;
}


✍️ 메모

lldb 에서 명령형 인자와 함께 실행하는 법

  1. $ lldb
  2. $ target create *.out
  3. $ b point
  4. $ r arg

ChatGPT를 써본 소감

  • Caesar 과제를 하면서 처음으로 요즘 화제인 ChatGPT를 써보았다. 눈치가 빠른(성능이 좋은) 검색엔진처럼 사용하기에는 좋았다. 기능을 구현하기 위해 어떤 함수를 쓰면 좋을지 빠르게 찾아서 나에게 제시해주었다.
  • 오타를 잡아주거나 변수명 혹은 함수명을 제시해주는 것도 도움이 되었다.
  • 코드 최적화도 요구해 보았는데, 참고하기에는 좋았지만 그대로 쓰면 안될 것 같았다. 아예 틀린 코드를 제시해주기도 했다.
  • 질문을 최대한 좁고 명확하게 할수록 원하는 답을 얻기 좋았다. 유명한 역설인데, 질문을 잘 하려면 잘 아는 것이 중요하다. 결국 ChatGPT의 등장과 무관하게 나는 공부를 해야한다. AI가 단순히 "이런 프로그램 만들어줘."라고 해도 뚝딱 만들어주는 성능이 되기 전 까지는...
  • 단점에도 불구하고 질문할 멘토가 없던 나에게는 도움이 되는 것 같다. 게다가 AI니까 물어보기에 부담이 없다. 다른 AI챗봇도 이용해 볼 용의가 있다.



References

0개의 댓글