반응형

이번 2024년도 1회 차 필기, 실기를 응시하고 자격을 취득했습니다.

 

제가 전공을 바꾸고 주변에서 가장 많이 들었던 말이 "개발자로 취업하는데 정보처리기사는 필요 없어."였습니다.

그런 안일한 말만 듣고 준비하지 않고 있었고, 어느 순간 깨닫게 되었습니다.

정보처리기사가 필요 없다는 말을 하는 사람들 중에 정보처리기사를 갖고 있지 않은 사람은 없었습니다.

뒤 늦게 깨닫게 된 저의 표정과 비슷하네요.

 

그래서 정보처리기사는 일단 기본적으로 따야겠다는 생각으로 단기간 빠르게 준비했습니다.

 

1. 필기는 CBT, CBT, CBT 그리고 또 CBT

저는 개정된 이후의 필기 교재를 구매해서 일단 1 회독을 시작했습니다.

처음 목표는 사실 필기가 널널하니깐 실기까지 잡으면서 간다는 마인드로 공부를 시작했습니다.

하지만 책에 나오는 모든 내용을 다 정독하면서 암기하기에는 정보처리기사에만 쓸 수 있는 시간은 없었습니다.

그래서 실기는 필기 붙은 다음에 생각하자는 마인드로 2주 안에 필기 끝내기를 시작했습니다.

 

1주일 동안은 CBT 사이트에서 2020년도부터 2022년도까지 문제를 풀었습니다.

 

하루에 하나씩만 풀어도 8일이면 다 풀 수 있습니다. 요약본을 읽고 나서 푸는 것이 안 본 것보다는 도움이 많이 되지만, 그냥 풀어도 상관없습니다.

 

1회 차 풀고, 맞은 문제, 틀린 문제 상관없이 해설을 읽으면서 모두 정독했습니다. 계속 이렇게 반복하다 보면 자주 나오는 유형들이 있습니다. 이런 문제는 절대 틀리시면 안 됩니다.

 

1주일 동안 문제를 풀었다면, 이제는 1주일 동안 선택 연도 말고, 아래 "모의고사"라는 모드가 따로 있습니다. 지정한 연도의 범위에서 문제가 출제되고, 막 섞여서 나옵니다. (풀었던 건데, 다 맞출 수 있지라고 생각하시지 말고 한번 풀어보세요.)

 

이렇게 2주만 CBT를 돌려보고, 필기를 봐도 무난하게 통과하실 수 있으실 거 같습니다.

 

 

2. 실기는 공부, 공부, 공부

실기는 사실 요령이라고 하면서 말씀드릴 수 있는 부분이 없을 것 같습니다.

필기처럼 CBT 문제를 계속 돌려볼 수도 없고, 기출문제가 있다고 한들 그대로 나오지 않기 때문입니다.

 

제가 사용했던 교재는 수제비입니다. 1, 2권으로 나누어져 있어서 따로 들고 다니기도 편했습니다.

https://www.yes24.com/Product/Goods/117206088

 

2023 수제비 정보처리기사 실기 1권+2권 합본세트 - 예스24

비전공자를 위한 최고의 수험서!!- 2023년 합격을 위한 NCS 기반 모의고사 수록- 궁극의 암기비법(두음쌤)과 학습 Point 수록- 최적의 맞춤 학습을 위한 커뮤니티 운영- 암기비법 PDF 제공(커뮤니티 내

www.yes24.com

내용을 읽고, 바로 챕터별 문제를 풀어 볼 수 있었습니다.

수제비 카페에 궁금한 점을 올리면 고수분들이 잘 알려주시기도 합니다.

강의도 있다는데 저는 따로 듣지는 않았습니다.

 

위 교재를 보면 2권으로 나누어져 있지만, 양이 정말 방대하고 저는 실기 준비 기간에 작은 회사에.. 취업을 하게 되어서 투자할 수 있는 시간이 많지 않았습니다.

 

그래서 전략을 먼저 세웠습니다.

  1. 자신 있는 파트의 문제는 무조건 맞는다.
  2. 자주 나오는 유형에서 비어있는 점수를 채우자.

정말 간단하지만, 아주 괜찮은 전략이었습니다.

 

저는 비전공자이긴 하지만, 프로젝트와 타 전공 수업을 들으면서 C, Java, Python을 모두 다루어 봤고, 데이터베이스 전공 수업도 신청해서 들었었기 때문에 코딩과 데이터베이스를 틀리면 안 되는 파트라고 설정했습니다.

 

또한 제가 봤을 때, 자주 나왔던 부분은 OO 하는 보안방법은?, OO하는 해킹방법은? 이런 문제들이 많이 나오는 것을 확인했습니다.

 

모든 코딩 문제와 데이터베이스 그리고 추가로 2~3문제를 맞히면 합격선에 들어가게 된다는 것을 깨닫고 전략대로 집중 공략했습니다.

 

저는 도움을 많이 받았던 블로그가 있어서 올려 봅니다.

https://starrykss.tistory.com/1856

 

[정보처리기사 실기] 단원별 정리 & 예상 문제 & 기출 문제

정보처리기사 단원별 정리 & 예상 문제 & 기출 문제정보처리기사 실기 시험을 준비하면서 블로그에 올렸었던 글들을 한 페이지에 정리해본다. 개념 정리 2020년 NCS 개편 후의 내용들수험서, 인터

starrykss.tistory.com

여기에 작성자분이 정말... 엄청나게 정리를 해두셨습니다.

중간에 광고가 많이 나오긴 하는데, 그런 건 중요하지 않습니다.

오타나 오답으로 작성된 부분도 있긴 했는데 공부하면서 틀린 부분도 찾아내고 도움을 많이 받았습니다.

Java, Python, C에 대한 기본 문법을 다지고 아래 있는 블로그에 기출, 예상 문제들을 계속 풀면서 준비했습니다.

https://starrykss.tistory.com/1797

 

[정보처리기사 실기] 프로그래밍 기출 문제 정리 (2017년~2022년)

프로그래밍 기출 문제 정리 (2017년~2022년) 정보처리기사 실기 기출 문제 중에서 프로그래밍(C, Java, Python)과 관련된 문제를 정리해 본다. 이 문제들은 복원을 한 것으로, 실제 출제된 문제와 다를

starrykss.tistory.com

코딩 문제 연습해 보기 정말 좋습니다. 3~4일 동안은 퇴근 후 코딩문제에 올인을 하면서 보냈습니다.

코딩 문제를 한 바퀴 돌렸다면 바로 다음 파트 "데이터베이스" 기출문제를 풀었습니다.

(파트별로 요약집이라도 1 회독 꼭 해보시고 푸시길)

https://starrykss.tistory.com/1840

 

[정보처리기사 실기] 데이터베이스 기출 문제 정리 (2017년~2022년)

데이터베이스 기출 문제 정리 (2017년~2022년) 정보처리기사 실기 기출 문제 중에서 데이터베이스(Database)와 관련된 문제를 정리해 본다. SQL(Structed Query Language) 이 문제들은 복원을 한 것으로, 실제

starrykss.tistory.com

한 1~2주 정도 빡세게 잡고 해 보시면 코딩 문제, 데이터베이스를 한 바퀴 돌려볼 수 있을 거 같습니다.

 

시험 1주 전부터는 코딩문제, 데이터베이스 보면서 틀리거나 헷갈렸던 부분들을 다시 보고 계속 틀리는 부분은 어디 적어두면 좋습니다. (시험 당일날 도움이 많이 됩니다.)

 

이와 함께 이제 자신이 2~3문제를 맞히기로 선택한 파트를 노려줍니다. 여기는 그냥 암기.. 암기이기 때문에 어쩔 수 없이 외워줍니다.

 

 

이렇게 준비를 해서 시험을 봤고, 필단형 72점을 받고 합격했습니다.

세웠던 전략이 통해서 시험 보고 나오는 날에도 합격한 것 같은 기분이 들면서 나왔습니다.

 

이번 시험은 예상했던 대로 코딩과 데이터베이스 문제의 비중에 굉장히 컸습니다. 또한 특정 기술, 보안에 대한 문제도 적지 않게 나와서 합격할 수 있었습니다.

 

 

 

반응형
반응형

결론을 먼저 말해보면 둘 다 암호화 기법이지만, 해시(Hash)는 단방향 암호화 기법이며, 암호화(Encryption)는 양방향 암호화 기법이다.

 

즉, Hash는 평문 데이터를 암호화된 데이터로 바꿀 수 있는 단방향 암호화 기법이고, Encryption은 평문을 암호화된 데이터로 바꾸는 암호화와 암호화된 데이터를 평문 데이터로 바꿀 수 있는 복호화가 존재합니다.

 

 

 

1. Hash


  • 해시 알고리즘은 특정 입력 값에 대해서 항상 동일한 결과 값을 리턴하게 됩니다.
  • 해시 입력 값은 다르지만, 해시 결과 값은 동일할 수 있습니다. (여기서 중복이 적게 생길수록 좋은 해시 함수입니다.)

해시만 적용하는 것으로 완벽한 보안이 될까?

사실 어떻게 보안을 해도 어떤 천재 해커나 집요한 해커들에게 뚫릴 가능성이 Zero는 아니기 때문에 더 튼튼한 보안을 결정해서 사용해야 합니다.

 

하지만, 평문 데이터를 단순히 해시함수 한 번만 거쳐 암호화를 했다고 완벽한 보안이 될 수 없습니다.

 

그래서 보통 해시 함수를 여러 번 돌리거나 Salt를 넣어서 사용하는 방법을 사용하곤 합니다.

 

여기서 솔트 값은 적용하는 방식에 따라서 많이 달라질 수 있는데, 임의로 정해둔 솔트 값을 비밀번호의 앞이나 뒤 또는 앞뒤에 모두 적용해서 해시 함수에 넣어 결과 값을 얻는 방식이다.

 

이렇게 하면 어떻게 막 해시 함수에 임의의 값을 막 넣다보니 해커가 평문 데이터를 알아냈다고 했을 때, 이 평문 데이터는 임의의 값인 Salt가 섞여있는 데이터이기 때문에 해커는 이 Salt 값까지 알아내야 실제 데이터를 얻어낼 수 있습니다.

 

대표적인 해시 함수에는 MD5, bcrypt, SHA가 존재합니다.

해시 함수별 보안과 속도 비교

 

 

2. Encryption


  • 양쪽에서 같은 암호화 키를 가지고 있는 암호화 방식을 "대칭키 방식"이라고 합니다.
  • 한쪽은 개인키(private key)를 가지고, 다른 한쪽은 개인키와 한 쌍인 공개키(public key)를 가지고 암호화 복호화하는 방식을 "비대칭키 방식"이라고 합니다.
  • "대칭키"와 "비대칭키"를 둘 다 사용하는 혼합 암호화 방식도 존재합니다.
  • 암호화 방식은 해시와는 다르게 양방향성을 가지게 됩니다.

 

 

 

반응형
반응형

비동기 구현을 하려는 이유


프로젝트에서 서버에서 발생하는 5xx Exception을 모니터링하기 위해서 Slack WebHook을 사용해서 구현했습니다.

 

현재 로직은 5xx 예외가 발생 -> Slack 메시지 전송 -> 클라이언트에게 응답까지의 과정이 동기적으로 진행됩니다.

 

여기서 사용하고 있는 Slack 서버는 제가 제어할 수 없는 부분이기 때문에 만약 Slack 서버에 지연이 생겨 응답을 늦게 준다면 어떻게 될지 생각을 해봤습니다.

 

Slack 메시지 전송이 지연되어 정작 더 중요한 다음 과정인 "클라이언트에게 응답"이 제대로 수행되지 않는다면... 끔찍한 상황이 벌어질 수도 있다고 생각했습니다.

 

실제로 Slack 메시지를 보내는 push() 메서드에서 5초의 지연 시간을 주고 테스트 해보니 클라이언트에게 응답이 가는 시간도 그만큼 지연이 되는 것을 확인했습니다.

요청이 들어온 시간과 클라이언트에게 응답되는 시간 사이의 5초간의 지연 발생

 

 

그래서 Slack으로 메시지를 보내는 부분을 비동기로 처리하여 클라이언트에게 응답을 제공하는 부분에 지연이 생기지 않도록 구현하기로 했습니다.

 

 

 

🔎 Spring의 @Async


구현을 하기 전에 @Async를 조금 알아보면, 비동기 처리를 간단하게 할 수 있도록 Spring에서 제공해주는 어노테이션입니다.

 

사용 방법은 @Async를 사용하겠다고 @EnableAsyc를 붙여주고, 비동기로 처리할 메서드 위에 @Async를 붙여주면 됩니다.

 

@Async를 사용할 때는 주의 해야할 점이 2가지 있습니다.

  1. private 메서드에 붙여서는 안 됩니다. (컴파일 에러로 확인 가능)
  2. 같은 클래스 내부에서 스스로 호출(self-invocation)을 하면 안 된다. (컴파일 에러로 확인 불가)

 

1번은 private 메서드에 @Async를 붙여보면 IntelliJ에서 컴파일 에러를 확인해 볼 수 있습니다.

private 메서드에 @Async 붙였을 때

 

2번은 아래와 같이 컴파일로 확인이 되지 않습니다.

같은 클래스 내부에 있는 test() 메서드를 self invocation

 

이제 외부에서 MonitoringProvider의 push() 메서드를 호출 했을 때, 내부에 있는 test()가 당연히 비동기 처리 되겠지 하고 생각하고 놔두면 이제 디버깅 지옥을 맛볼 수 있습니다.

 

실제로 외부에서 push() 메서드를 여러 번 요청해서 비동기 처리가 되는지 확인해보면 다음과 같이 순차적으로 동기적으로 처리되는 것을 확인할 수 있습니다.

순차적으로 동기적으로 처리된 모습

 

자가 호출을 하면 동작하지 않는 이유는 Spring 컨테이너에 등록되어 있는 스프링 빈의 메서드가 호출되었을 때, @Async가 붙어있다면 AOP로 처리되어 스프링이 메서드 요청을 가로채서 다른 Thread Pool에서 실행시켜주는 구조이기 때문이라고 합니다.

 

그래서 @Async를 private에 붙이거나 내부 호출을 하게 되면 스프링이 가로채서 프록시 처리를 할 수 없기 때문에 2가지 주의점이 있습니다.

 

 

 

✨ 적용하기


이런 주의점을 확인하고, 기존 로직에 비동기 처리를 진행해줍니다.

 

먼저, @Async를 사용할 수 있도록 @EnableAsync를 붙여줍니다.

@EnableAsync
@SpringBootApplication
public class PraisePushApplication {
    public static void main(String[] args) {
        SpringApplication.run(PraisePushApplication.class, args);
    }
}

 

비동기 처리를 원했던 메시지를 보내는 push() 메서드에 @Async를 붙여줍니다.

public class SlackMonitoringProvider implements MonitoringProvider {

    @Value("${error.webhook.url}")
    private String webHookUrl;

    @Async
    @Override
    public void push(final Exception exception, final HttpServletRequest request) {
        SlackApi slackApi = new SlackApi(webHookUrl);
        slackApi.call(createSlackMessage(exception, request));
    }
    
    ... 생략 ...
}

 

요청 시간 로그를 설정하고, push 로직에서 5초의 지연을 두고 요청을 확인해 보면 메시지를 보내는 로직이 비동기적으로 처리되는 것을 확인할 수 있습니다.

 

Slack으로 메시지 보내는 로직이 비동기로 처리되어 클라이언트에게 응답되는 시간이 먼저 처리되는 것을 확인

 

 

반응형
반응형

이번에 시작한 프로젝트에서 Jenkins를 사용해서 CI/CD 파이프라인을 구성하고 있습니다.

develop 브랜치에서 작업을 하고, push를 하면 Jenkins Pipeline이 동작하면서 자동화 배포 작업이 진행됩니다.

 

우리 프로젝트에서는 다음과 같은 Pipeline이 동작해서 배포를 진행합니다.

  1. Git Clone
    • Github Repository의 develop 브랜치에 변화가 발생되면 Jenkins 동작이 시작됩니다.
    • develop 브랜치를 Jenkins 서버로 clone 해서 가져옵니다.
  2. DEV-Build
    • 이 과정에서 프로젝트의 Build와 Test를 진행합니다.
  3. Deploy
    • Jenkins 서버에서 Develop 서버로 SSH 통신을 통해 배포 스크립트를 실행합니다.
    • 이 글에서는 이 "Deploy" 과정에서 스크립트 실행 후 발생한 "ERROR: script returned exit code 255"에 대해서 다룰 예정입니다.

 

1. 기존 스크립트


기존 스크립트는 다음과 같습니다.

#!/bin/bash
source ~/.bashrc

REPOSITORY=/home/praise-push
echo "### check running application... ###"
CURRENT_PID=$(pgrep -f praise-push)
echo "### current pid = $CURRENT_PID"

if [[ -z ${CURRENT_PID} ]]; then
    echo "### Not found running application ###"
else
    echo "### Found application!! try to kill process $CURRENT_PID ###"
    kill -15 $CURRENT_PID
    sleep 3
fi
echo "### Ready to deploy ###"

JAR_NAME=$(ls $REPOSITORY | grep 'praise-push' | tail -n 1)

echo "### Source file Name: $JAR_NAME"
chmod +x $REPOSITORY/$JAR_NAME
nohup /usr/bin/java -jar $REPOSITORY/$JAR_NAME >> $REPOSITORY/nohup.out 2>&1 &
echo "success"

 

[ 실행 중인 프로세스가 있는지 확인 ]

간단하게 설명하면 현재 실행 중인 java application의 PID를 찾아서 CURRENT_PID에 저장합니다.

현재 실행중인 프로세스가 있다면 kill 명령어를 통해 중지시키고, 없다면 그냥 넘어가도록 합니다.

 

[ 새로운 프로세스를 실행합니다. ]

Jenkins 서버에서 Build, Test 과정이 끝난 jar 파일을 Develop 서버로 복사해서 가져오게 됩니다.

이 jar 파일을 nohup을 이용해서 서버를 실행합니다.

 

Jenkins에서 Deploy Pipeline은 다음과 같습니다.

stage('Deploy') {
    steps {
        sshagent(credentials: ['dev_server']) {
            sh "ssh -o StrictHostKeyChecking=no -p 9001 root@123.11.11.11 'uptime'"
            sh "scp -P 9001 /var/jenkins_home/workspace/praise-push-dev/build/libs/praise-push-0.0.1-SNAPSHOT.jar root@123.11.11.11:/home/praise-push"
            sh "ssh -t -p 9001 root@123.11.11.11 'cd /home/praise-push && ./deploy.sh'"
        }
    }
}

 

ssh으로 uptime을 요청하여 첫 SSH 연결을 진행합니다.

scp를 이용하여 Jenkins 서버에서 Build, Test 과정을 마치고 생성된 jar 파일을 Develop 서버로 복사합니다.

ssh를 이용하여 원격으로 "/home/praise-push/deploy.sh" 스크립트를 실행하여 배포를 작업을 완료합니다.

 

 

 

2. 문제 분석


먼저 script에서 어떻게 해야 255 exit code가 발생되는지 찾아봤습니다.

 

Remote(원격) 서버에 SSH가 설치되어 있지 않거나, 올바른 Connection이 아닌 경우

 

처음에는 SSH 연결이 잘 안 되고 있구나라고 판단하여 이쪽으로 찾아보기 시작했었습니다.

 

클라이언트(Jenkins)에서 Key Pair를 생성하여 공개키(pub)를 원격서버(Develop)로 보내서 서버의 "authrized_keys"에 클라이언트의 공개키를 저장해야 합니다.

 

SSH 요청 시 클라이언트와 서버 사이의 세션키(대칭키) 생성을 위한 키 교환이 시작되는데, 이때 "authrized_keys"에 있는 이 공개키가 사용됩니다.

 

그래서 저는 SSH 연결이 잘 못 된 것은 아닌지 Jenkins 서버와 Develop 서버를 다시 연결을 해봤습니다.

 

그래도 결과는 달라지지 않았습니다. 다시 한번 확인해 보니 위 스크립트에서 이 부분 이전까지는 스크립트가 실행되는 것을 확인했습니다.

echo "### Ready to deploy ###"

 

이 echo 명령어전까지는 실행이 되었다는 것은 Jenkins -> Develop의 SSH 연결에서 발생한 문제는 아니라는 것을 의미합니다.

 

정상적으로 SSH 연결을 통해 "/home/praise-push/deploy.sh" 명령까지 들어와서 실행을 했지만, 어떤 이유 때문에 중간에 script가 비정상적으로 종료되어 script가 exit 255 code를 리턴했던 것이라는 판단을 하게 되었습니다.

 

즉, 문제는 deploy.sh 스크립트에 있다!

 

deploy.sh를 하나씩 실행하며 디버깅을 해봤습니다.

 

1. 첫 번째 시도

#!/bin/bash
source ~/.bashrc
REPOSITORY=/home/praise-push

 

당연히 문제는 없겠지만, 이틀 넘게 SSH가 원인인 줄 알고 삽질을 해서 아주 꼼꼼하게... 진행해 보기 위해서 스크립트의 시작 부분부터 진행했습니다.

 

결과는 당연히 성공입니다!

 

 

2. 두 번째 시도

#!/bin/bash
source ~/.bashrc
REPOSITORY=/home/praise-push
echo "### check running application... ###"
CURRENT_PID=$(pgrep -f praise-push)
echo "### current pid = $CURRENT_PID"

 

이 부분도 별 다른 특별한 명령어는 없었기 때문에 성공했습니다!

 

 

3. 세 번째 시도

#!/bin/bash
source ~/.bashrc
REPOSITORY=/home/praise-push
echo "### check running application... ###"
CURRENT_PID=$(pgrep -f praise-push)
echo "### current pid = $CURRENT_PID"

if [[ -z ${CURRENT_PID} ]]; then
    echo "### Not found running application ###"
else
    echo "### Found application!! try to kill process $CURRENT_PID ###"
    kill -15 $CURRENT_PID
    sleep 3
fi

 

예상했던대로 세 번째 시도에서 실패했습니다. 그럼 이 if 블록 내부에 문제가 있다는 것을 의미합니다.

문법적인 오류는 아닌 것으로 보였고, 의심이 갔던 부분은 "kill -15 $CURRENT_PID" 부분이었습니다.

 

 

 

4. 네 번째 시도

#!/bin/bash
source ~/.bashrc
REPOSITORY=/home/praise-push
echo "### check running application... ###"
CURRENT_PID=$(pgrep -f praise-push)
echo "### current pid = $CURRENT_PID"

if [[ -z ${CURRENT_PID} ]]; then
    echo "### Not found running application ###"
else
    echo "### Found application!! try to kill process $CURRENT_PID ###"
    sleep 3
fi

 

의심이 들었던 kill 명령어를 제거한 후 다시 시도해봤는데, Deploy Pipeline이 성공했습니다.

역시 원인은 이 부분이었습니다.

 

 

 

3. 원인 분석


kill 명령어로 CURRENT_PID를 종료하고 있었기 때문에 Deploy Pipeline을 실행시켜 두고, Develop 서버에서 계속 "ps -ef | grep praise-push"을 실행하며 콘솔을 확인해 봤습니다.

root@praise-push-dev:/home/praise-push# ps -ef | grep praise-push
root     22623     1  3 19:01 pts/0    00:00:16 /usr/bin/java -jar /home/praise-push/praise-push-0.0.1-SNAPSHOT.jar
root     23741 16549  0 19:09 pts/0    00:00:00 grep --color=auto praise-push
root@praise-push-dev:/home/praise-push# ps -ef | grep praise-push
root     22623     1  3 19:01 pts/0    00:00:16 /usr/bin/java -jar /home/praise-push/praise-push-0.0.1-SNAPSHOT.jar
root     23789 23720  0 19:09 ?        00:00:00 bash -c                              cd /home/praise-push                             ./deploy.sh
root     23794 16549  0 19:09 pts/0    00:00:00 grep --color=auto praise-push

 

이상한 점을 확인했습니다. Deploy Pipeline이 실행되는 중간에 "praise-push"가 포함된 새로운 프로세스가 하나 실행되는 것을 보았습니다.

root     23789 23720  0 19:09 ?        00:00:00 bash -c                              cd /home/praise-push                             ./deploy.sh

 

살펴보니 Deploy Pipeline에서 실행했던 "cd /home/priase-push" 명령어에서 시작되었던 프로세스가 kill의 대상인 "pgrep -f praise-push"에 발견되어 계속 종료되고 있었습니다.

 

찾은 원인은 이렇습니다.

 

  1. Jenkins에서 SSH를 통해 Develop 서버의 cd /home/praise-push/deploy.sh를 실행 (PID 1)
  2. deploy.sh 스크립트에서 현재 실행 중인 praise-push-0.0.1.jar(PID 2)를 찾기 위해서 "pgrep -f praise-push"을 실행
  3. "pgrep -f praise-push"의 결과를 CURRENT_PID 변수에 저장합니다.
    • 여기서 CURRENT_PID에는 의도대로라면 (PID 2)만 있어야 합니다.
    • 하지만, 결과에는 (PID 1)(PID 2)가 들어가게 됩니다.
    • 이유는 "cd /home/praise-push/deploy.sh"에 praise-push가 포함되어 탐색 대상이 되었던 것입니다.
  4. "kill -15 $CURRENT_PID"에서 (PID 1) (PID 2)를 모두 죽이게 됩니다.
  5. 여기서 deploy.sh의 비정상적인 종료가 되고, exit code 255를 리턴하게 됩니다.

 

 

 

4. 해결


"pgrep -f praise-push" 스크립트를 통해 찾으려는 대상은 "praise-push-0.0.1.jar"인 jar 프로세스이기 때문에 아래와 같이 변경했습니다.

CURRENT_PID=$(pgrep -f praise-push-*.jar)

 

이 부분만 이렇게 수정한 후 Jenkins CI/CD Pipeline을 실행시켜 보니 모든 stage가 통과되었고, 정상적으로 배포에 성공했습니다!

 

 

 

생각보다 굉장히 사소한 실수로 시작된 문제인데, 꽤나 오랜 시간 잡고 있었어서 기록해 봤습니다.

 

반응형
반응형

저는 어드벤트 캘린더 형식의 크리스마스 익명 편지 서비스인 "Ginger Hotel 🎄"의 2023년 이야기를 적어볼까 합니다.

 

 

🛠️ 팀진저의 백엔드에서는 이런 기술 스택을 선택했습니다.


저는 백엔드 개발을 맡아서 진행하게 되었습니다. 가장 먼저 했던 것은 기술 스택을 선택하는 일이었습니다.

 

 

팀진저에서 선택했던 백엔드 기술 스택은 Nest.js, Typescript, PostgreSQL이었습니다.

 

"Java, Spring으로 공부하고 계신데 왜 Node 기반의 기술 스택을 선택하게 되었나요?"

 

저에게도 큰 도전이었던 것 같습니다. 팀원 한분이 풀스택으로 참여를 희망하셨어서 Front-End 개발팀에서 선택했던 언어인 Typescript에 맞추기 위함이 가장 큰 이유였습니다.

 

저도 처음 사용해 보는 기술 스택들이었기 때문에 신중하게 선택을 했었습니다.

 

Typescript의 경우에는 타입의 안정성을 고려하며 개발할 수 있어서 언어 선택에 있어서는 주저함이 없었습니다.

 

프레임워크는 Nest.js를 선택했던 이유는 모듈 단위로 나누어 개발을 할 수 있고, 확장성에 있어서도 부족함이 없다고 생각했습니다.

또한 Spring가 굉장히 유사한 구조를 가지고 있어 러닝커브를 빠르게 극복할 수 있다고 판단했습니다.

 

그리고 개발을 하며 느꼈던 점은 npm에 정말 유용한 라이브러리가 많아서 부족함 없이 개발을 했던 것 같습니다.

 

마지막으로 데이터베이스는 PostgreSQL을 선택했습니다. 가장 큰 이유는 진저호텔에서는 Member -> Hotel -> HotelWindow -> Letter -> Reply와 같이 굉장히 깊은 연관 관계의 Depth가 있었습니다.

 

그래서 Join과 같은 연산 처리 능력이 뛰어난 데이터베이스를 고려해야 했습니다. 또한 2022년 진저호텔(편지 약 650만 개)이 가지고 있는 데이터를 무시할 수는 없었기 때문에 대용량 데이터에서 성능이 좋은 데이터베이스를 찾다 보니 Oracle과 PostgreSQL을 찾게 되었습니다.

 

저희는 클라우드 플랫폼으로 AWS를 사용하기로 결정이 된 상태이기 때문에 라이센스 비용 또한 고려해야 했기 때문에 라이센스 비용이 무료였던 PostgreSQL을 최종적으로 선택하게 되었습니다.

 

 

 

☁️ 팀진저의 클라우드 이야기


제가 이번 프로젝트에서 얻어가고 싶은 경험 중 하나가 "클라우드 플랫폼"을 이용하여 서버를 운영해 보는 경험이었습니다.

그래서 개발보다도 더 많은 시간을 쏟으면서 찾아보고, 적용해보고 했던 것 같습니다.

 

 

처음 구성 했던 아키텍처는 위와 같습니다. 구글링을 통해서 구성해 봤던 아키텍처였습니다. (AWS는 맨날 EC2 만 써봐서...)

 

Application Load Balancer는 부하 분산을 위해 EC2를 증설할 계획을 가지고 있었는데, 그래서 미리 적용해 두게 되었습니다.

여기서 Cloud Front는 본래 용도는 CDN 서버로 자주 사용되는 데이터를 캐시 하여 비용을 줄이는 용도로 많이 쓰인다고 합니다.

 

하지만, 지금 현재 아키텍처에서는 Cloud Front가 캐시의 역할이 아닌 그냥 AWS Certificate Manager와의 상호작용을 통해 SSL 인증서를 확인해 주는 역할 밖에 하지 않았습니다.

 

이대로 서비스에 나간다면 Cloud Front를 거치는 비용이 계속 나갈 것으로 판단되었고, 멋쟁이 사자처럼 대학 동아리에서 인연이 있었던 높으신 분께 현재 상황에 대한 피드백을 요청했었습니다.

 

Cloud Front 제거

 

얻은 피드백에서는 Load Balancer에서도 AWS Certificate Manager와 상호작용할 수 있는 방법이 있다는 것이었습니다.

[aws] ELB Application Load Balancer를 이용한 SSL 설정

 

[aws] ELB Application Load Balancer를 이용한 SSL 설정

이번 포스트에서는 AWS ELB application load balancer를 이용해 SSL을 적용 하는 방법을 정리한다. 즉 이 포스트의 목적은 아래 Fig 1과 같은 architecture 로 서버 운영 구조를 완성하는 것이다. 이 구조를 간

pajamacoder.tistory.com

 

이렇게 되면 기존 Cloud Front를 떼어내서 아키텍처가 가벼워지고, Cloud Front에서 과금될 비용들을 다른 곳에 쓸 수 있게 되기 때문에 바로 적용하게 되었습니다.

 

 

Cloud Front를 떼어내고 난 후의 아키텍처입니다. 생각보다 빠르게 떼어낼 수 있었습니다.

비용도 줄이고, 서버 아키텍처가 더 간결해지고, 깔끔해질 수 있었습니다! ✨

 

CI/CD 파이프라인에 Docker 적용

 

기존에는 CI/CD 과정에서 Github Actions로 EC2에 접근하여 미리 적어둔 스크립트를 실행시키도록 해두었습니다.

즉, EC2에서 프로젝트 build 작업까지 하다 보니 인스턴스에 부하가 모이게 되는 것 같았습니다.

그래서 Docker Image를 이용하기로 했습니다.

관련된 내용은 따로 글에 정리를 잘해뒀습니다. 

 

https://hoonsb.tistory.com/98

 

[Github Actions x Docker] 자동화 배포 환경 도커로 변경하기

1. 기존 아키텍처와 배포 스크립트 분석 프로젝트 진행 과정에서 초반에 간단한 인프라 설계를 위처럼 진행했습니다. 먼저 EC2(Ubuntu) 내부에 Node 16 버전을 포함해서 필요한 것들을 모두 수동으로

hoonsb.tistory.com

 

EC2 인스턴스 추가, RDS Multi-AZ 다중화 배포 적용

 

진저호텔 서비스를 운영하는 데 있어서 가장 중요하게 여겼던 것은 2가지가 있습니다.

  1. 사용자들이 주고받은 편지를 안전하게 보관하자!
  2. 사용자들이 서비스를 이용하는 과정에서 끊기지 않도록 중단 없이 확장할 수 있는 서버를 만들자!

먼저 서버 중단 없이 기능을 확장하고, hotfix를 처리하기 위해서 EC2 인스턴스를 추가하여 Load Balancer의 대상 그룹에 추가했습니다.

이렇게 구성하고, 하나의 서버에 배포 변경 사항을 적용하고 Health Check가 통과되었을 때, 다른 하나의 서버를 배포하게 되었습니다.

이렇게 진저호텔은 12월 한 달간 한 번의 서버 중단 없이 서비스를 유지할 수 있었습니다.

 

그다음은 데이터 백업입니다. 기존 RDS는 Single-AZ로 하나의 Availability Zone을 사용했기 때문에 데이터 백업 중에는 데이터베이스가 중단된다는 문제점이 있었습니다. 중단 없이 백업할 수 있는 방법은 없을까 방법을 찾다가 AWS RDS에서는 "Multi-AZ 다중화 배포" 옵션을 제공했고, master DB와 standby DB가 있어서 백업의 경우에는 standby에서 진행되어 master DB는 계속 중단 없이 사용할 수 있게 됩니다. 또한 master DB에 문제가 생겼을 때, standby DB가 바로 master로 승격하게 되면 장애 대응에도 신속한 특징을 가지고 있어 운영 환경에서는 적용하게 되었습니다.

 

이렇게 변경한 후의 아키텍처는 다음과 같습니다.

 

서버 인스턴스의 상태를 모니터링하자.

 

이 부분은 운영 환경에서는 필수라고 여겨지는 부분이라 현재 팀진저에서 소통 수단으로 쓰고 있던 Discord의 Web Hook과 AWS의 CloudWatch를 연동하여 사용하기로 결정했습니다.

 

서버 모니터링이 중요했던 이유는 RDS의 경우 만약 쿼리 병목이나 슬로우 쿼리가 발견되어 과도한 부하가 걸리게 되거나, 요청에 대한 처리 속도가 늦어지게 되는 경우를 조기에 발견하고 대응해야 된다고 생각되었습니다.

 

그래서 CloudWatch에 EC2, ALB, RDS에서 감지할 옵션들의 임계값을 설정하고, 임계값을 넘어선 경우에 AWS SNS를 통해 AWS Lambda에서 Event를 받아 Discord로 알림을 바로 줄 수 있도록 구성했습니다.

 

 

이렇게 서버 인스턴스 모니터링 과정을 거치게 되면 다음과 같은 메시지(경보)를 Discord에서 확인하고, 바로 대응할 수 있습니다.

 

제가 무적 짱짱맨이어서 24시간 안 자고, 계속 서버만 모니터링할 수 있다면 좋았을 텐데 저는... 무적 짱짱맨이 아니기 때문에 다른 팀원들의 눈과 귀를 빌리고자 Discord를 적극적으로 사용하게 되었습니다.

 

 

 

✉️ 편지, 답장 차단 프로세스에 대한 고민


작년에 편지 차단에 대한 피드백이 굉장히 많았었습니다. 그래서 올해는 편지 차단 기능에 힘을 많이 쓰게 되었습니다.

차단 기능에 있어서 익명 편지 서비스이다보니 편지를 보낸 사람이 누구인지 특정할 수 없도록 하는데 고민을 많이 했었습니다.

고민 했었던 부분은 한 명이 여러 닉네임으로 편지를 보냈을 때의 상황이었습니다. 위 예시를 보면 "헤르미온느"와 "론"으로부터 받은 편지가 있습니다. "헤르미온느"와 "론"은 모두 훈섭이라는 사람이 보낸 편지입니다.

 

만약 기존에 생각하고 있던 프로세스로 편지를 받은 철수가 훈섭을 차단했을 때, block_history 테이블에 차단한 사람은 철수, 차단당한 사람은 훈섭으로 하게 되었을 때 아래와 같은 고민이 있었습니다.

 

철수가 "헤르미온느"를 차단했는데, "론"에게 받은 편지도 차단된다면, 철수는 "헤르미온느"와 "론"이 같은 사용자라는 것을 유추할 수 있게 됩니다. 그래서 차단 프로세스에서 차단 횟수를 넣게 되었습니다.

 

각 편지는 별개로 차단을 할 수 있고, 발신자에 대한 차단을 횟수로 관리하여 편지 차단을 진행합니다. "헤르미온느"에게 온 편지를 차단하면 차단한 사람은 철수, 차단당한 사람은 훈섭 그리고 차단 횟수가 1이 됩니다. 이렇게 되면 훈섭은 철수에게 편지를 보낼 수 없고, "헤르미온느"에게 온 편지를 차단했을 때, "론"에게 온 편지에는 어떠한 영향도 주지 않기 때문에 "헤르미온느"와 "론"이 같은 발신자라는 것을 유추할 수 없게 됩니다.

 

반대로 차단을 해제하게 되면 차단 횟수는 감소하게 됩니다.

 

"론"에게 받은 편지에 대한 차단을 해제했을 때는 아직 훈섭(헤르미온느)에게 받은 편지를 차단한 상태이기 때문에 훈섭에게는 편지와 답장을 받을 수 없습니다.

 

"헤르미온느"에게 받은 편지까지 차단 해제를 하면 철수가 훈섭을 차단했던 횟수가 0이 되기 때문에 해당 데이터는 사라지게 됩니다. 이제는 훈섭이 철수에게 편지를 보낼 수 있게 됩니다.

 

 

 

 

대부분 인프라 관련된 클라우드 이야기였지만, 제가 프로젝트를 진행하며 느꼈던 내용과 왜 이런 결정을 했는지에 대해서 기록해두고 싶어서 정리하게 되었습니다.

 

여기까지 2023년 진저호텔의 백엔드 개발 이야기를 풀어봤습니다. 🎄

 

반응형
반응형

1. 기존 아키텍처와 배포 스크립트 분석


 

프로젝트 진행 과정에서 초반에 간단한 인프라 설계를 위처럼 진행했습니다.

먼저 EC2(Ubuntu) 내부에 Node 16 버전을 포함해서 필요한 것들을 모두 수동으로 설치했습니다.

그 후 deploy.sh라는 스크립트 파일을 만들어 배포 스크립트를 작성했습니다.

이제 Github Actions에서 SSH 연결을 통해 EC2에 접근하여 deploy.sh를 실행하여 자동화 배포가 되도록 설정하여 완료했습니다.

 

기존의 스크립트는 아래와 같이 git 저장소에서 프로젝트 최근 파일을 가져온 후 추가된 npm 라이브러리를 확인하고 설치합니다.

프로젝트를 build 하고, 빌드가 성공하면 현재 실행 중인 프로세스를 확인하고 중지시켜 줍니다.

그 후 백그라운드 실행을 위해서 nohup을 통해서 application을 실행시켜 줬습니다.

# deploy.sh

echo "1. project root로 이동"
cd ~/server/ginger-hotel-server

echo "2. git pull"
git pull origin develop

echo "3. 추가된 라이브러리 설치"
npm install

echo "4. project build"
npm run build

if [ $? -eq 0 ]; then
  # 빌드 성공 시
  echo "5. 실행중인 프로세스 확인"
  CURRENT_PID=$(pgrep -f node)

  echo "6. 실행중인 프로세스 중지"
  sudo kill -15 $CURRENT_PID

  echo "7. 홈 경로 이동 후 프로젝트 재실행"
  nohup npm run start:prod > ~/nohup.out 2> ~/nohup.err < /dev/null &
else
  echo "프로젝트 Build 실패"
  exit 1
fi

 

이 상태로도 자동화 배포환경에는 문제가 없었고, 개발 단계에서 크게 불편한 점은 못 느꼈었습니다.

개발이 거의 마무리될 때쯤 두 가지 의문점이 들었습니다.

서버(EC2)가 증설되었을 때, EC2에 필요한 환경(특정 버전 Node)을 모두 수동으로 설치해줘야 하는 건가? Node 말고, 환경이 추가된다면?

실제 서비스를 운영중일 때, Application이 실행되고 있는 서버(EC2)에서 Git Pull을 하고, Build를 하는 과정이 부하를 주어 서버 성능에 영향을 주지는 않을까?

 

이러한 불편했던 점을 해결하기 위해서 "도커"를 활용하여 필요한 환경까지 함께 이미지로 만들고, Build 환경을 운영 서버와 분리해 보자는 생각을 하게 되었습니다.

 

 

 

2. Ubuntu에 도커 설치하기


1. ubuntu 패키지를 먼저 업데이트합니다.

sudo apt-get update

 

2. Docker 설치에 필요한 패키지를 설치합니다.

sudo apt-get install apt-transport-https ca-certificates curl gnupg-agent software-properties-common -y

 

3. Docker 공식 GPG 키를 추가합니다.

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

 

4. Docker 공식 apt 저장소를 추가합니다.

sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"

 

5. 시스템 패키지를 다시 업데이트합니다.

sudo apt-get update

 

6. Docker를 설치합니다.

sudo apt-get install docker-ce docker-ce-cli containerd.io -y

 

7. Docker의 실행 상태를 확인합니다.

sudo systemctl status docker

 

8. 도커 허브 로그인을 진행합니다.

sudo docker login
  • Docker Hub에서 가입한 이메일과 비밀번호로 로그인을 진행할 수 있습니다.
  • Docker Hub는 무료로 계정 1개당 1개의 Private 저장소를 사용할 수 있도록 제공해 줍니다.
  • 저는 이전에 Private 저장소를 쓴 적이 없기 때문에 이번 프로젝트에서 Private 저장소를 만들어 사용했습니다.

 

 

 

3. 프로젝트의 Root 경로에 DockerFile 생성


여기서 DockerFile은 도커에 존재하는 이미지를 기반으로 하여 내가 작성한 스크립트 파일을 통해 나만의 이미지를 생성할 수 있는 파일입니다. 즉, 저는 여기에 필요한 Docker 이미지를 골라 담아서 제가 만들고 있는 서버에 필요한 이미지를 생성하는 DockerFile을 생성했습니다.

# Node Base Image
FROM node:16-alpine

# RUN mkdir -p /app
WORKDIR /app

# Current Local . to /app/
ADD . /app/

# install Library
RUN npm install

# Build
RUN npm run build

# PORT
EXPOSE 8080

# Start
ENTRYPOINT npm run start:prod
  • 먼저 저는 Node 16 버전을 Base로 해서 Application을 실행시키고 있기 때문에 Base 이미지를 Node의 16 버전을 사용합니다.
  • RUN, CMD, COPY 등이 실행되는 기본 경로를 WORKDIR로 "/app"으로 설정합니다.
  • ADD 명령어를 통해 현재 프로젝트 경로(.)에 있는 소스파일을 WORKDIR로 설정한 도커의 경로(/app/)로 복사합니다.
  • 이제 순서대로 npm 라이브러리를 설치하고, build를 진행합니다.
  • EXPOSE를 통해서 도커 컨테이너가 실행되었을 때, 요청을 기다리고 있을 Listen 포트를 8080으로 지정합니다.
  • 마지막으로 ENTRYPOINT 명령어를 통해서 컨테이너가 생성되고, 최초로 실행될 때 수행하는 명령어를 설정하고 마무리합니다.

 

 

 

4. EC2 서버에서 실행할 스크립트 파일 생성


기존에 작성했던 배포 스크립트에서는 Git을 통해 EC2로 직접 Pull 하여 코드를 통합하고, Build 하는 과정도 모두 EC2에서 진행했었습니다. 하지만 이제 코드 통합과 Build는 DockerFile에 설정된 대로 Github Actions에서 진행하여 Docker Hub의 Private 저장소로 저장하게 됩니다.

 

즉, 이제 EC2에서는 이 Private Docker Hub 저장소에 있는 이미지를 가져와서 실행만 시켜 Deploy 과정을 마무리하는 스크립트를 실행하면 완료됩니다.

# start-docker.sh

echo "1. Pull Docker Image"
sudo docker pull khsrla9806/ginger-hotel:lastest

echo "2. Stop and Remove Container / Remove <none> Image"
sudo docker stop ginger-hotel
sudo docker rm -f $(sudo docker ps -qa -f status=exited)
sudo docker rmi -f $(sudo docker images -qa --filter "dangling=true")

echo "3. 새로운 이미지 실행"
sudo docker run -d --name ginger-hotel -p 80:8080 -v ./logs:/app/logs khsrla9806/ginger-hotel:lastest
  • Github Actions에서 코드 통합, Build를 거친 새로운 Docker Image를 가져옵니다.
  • 현재 실행되고 있는 컨테이너는 종료하고, 기존의 컨테이너와 이미지는 모두 삭제합니다. (계속 누적되는 것을 방지)
  • 새롭게 가져왔던 이미지를 실행시키고 배포를 완료합니다.

 

 

 

5. Github Actions WorkFlow 설정


이제 DockerFile까지 설정을 완료했기 때문에 Github Actions를 통해서 제가 설정한 DockerFile대로 이미지를 만들어 제 Private Docker Hub 저장소에 이미지를 올리고, SSH로 EC2에 접속하여 새롭게 작성한 start-docker.sh 스크립트 파일을 실행할 수 있도록 Github Actions WorkFlow를 설정해야 합니다.

name: GINGER PRODUCTION SERVER DEPLOY

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Source Code
        uses: actions/checkout@v3

      - name: Nodejs 16
        uses: actions/setup-node@v3
        with:
          node-version: 16.20.2
          cache: 'npm'

      - name: Setting Production Env
        run: echo "${{ secrets.PRODUCTION_ENV }}" >> .env.prod

      - name: Login Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_PASSWORD }}

      - name: Build and Push Docker Image
        uses: docker/build-push-action@v4.0.0
        with:
          context: .
          file: ./Dockerfile
          push: true
          tags: ${{ secrets.DOCKERHUB_REPO }}/ginger-hotel:lastest

      - name: SSH Remote Command
        uses: appleboy/ssh-action@v0.1.4
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USERNAME }}
          key: ${{ secrets.KEY }}
          port: ${{ secrets.PORT }}
          timeout: 40s

          script: ./start-docker.sh

 

위에서 ${{ secrets.USERNAME }} 이런 부분들은 Github Actions에서 사용할 수 있는 Secret 변수들을 설정해 둘 수 있습니다.

 

 

 

6. 동작 확인


이제 main 브랜치에 Merge가 되는 순간 작성해 둔 WorkFlow대로 Github Actions가 실행됩니다.

 

모든 Flow가 완료되면 자동화 배포 작업이 완료됩니다.

 

이렇게 변경한 후의 아키텍처는 다음과 같습니다.

 

 

지금까지 프로젝트의 CI/CD를 할 때, 처음에 소개했던 것 처럼 EC2 서버에게 너무 많은 책임을 부여했던 것 같아서 늘 도커로 해보자라고 생각을 했었는데 드디어 해볼 수 있게 되어 값진 경험을 가져간 것 같습니다.

 

 

 

반응형

+ Recent posts