Skip to main content

MCP가 뭐길래🤔

· 8 min read
eunsung shin
Software Engineer

최근 AI 개발 커뮤니티에서 **MCP(Model Context Protocol)**가 큰 주목을 받고 있습니다. 2024년 11월 Anthropic이 오픈소스로 공개한 이 프로토콜은 처음에는 큰 반응을 얻지 못했지만, 2025년 초부터 관심이 급격히 증가하고 있습니다. MCP는 LLM에게 부족한 파일 시스템, 데이터베이스, API 등 기존 데이터 소스를 컨텍스트로써 통신할 수 있는 표준을 제공합니다. HTTP 통신이 처음 세상에 나왔을 때처럼, LLM 엔진에 붙일 수 있는 도구를 정의하는 규격을 제공함으로써, LLM이 가진 가능성을 무한대로 확장시켜줍니다.

MCP란 무엇인가?

MCP는 대규모 언어 모델(LLM)이 외부 데이터와 시스템을 효과적으로 활용할 수 있도록 설계된 개방형 표준 프로토콜입니다. Anthropic은 MCP를 USB-C 포트에 비유하는데, USB-C가 다양한 기기와 주변 장치를 표준화된 방식으로 연결하듯이, MCP는 AI 모델이 다양한 데이터 소스와 도구에 표준화된 방식으로 연결될 수 있도록 합니다.

MCP의 주요 특징

  1. 개방형 표준: 누구나 자유롭게 사용하고 개선할 수 있습니다.
  2. 양방향 연결: AI 모델과 데이터 소스 간의 지속적인 통신을 지원합니다.
  3. 범용성과 표준화: 다양한 데이터 소스와 도구를 하나의 프로토콜로 연결합니다.
  4. 보안 및 신뢰성: 안전하고 신뢰할 수 있는 연결을 제공합니다.

MCP의 동작 방식

MCP는 다음과 같은 프로세스를 거칩니다.

1. 연결 설정

  • 호스트 애플리케이션(예: Claude Desktop, Cursor)이 필요한 기능을 가진 MCP 서버에 대한 클라이언트를 생성합니다.

2. 초기화 프로세스

  • 클라이언트가 서버와 연결되면 JSON-RPC 기반 메시지를 통해 프로토콜 버전과 지원 가능한 기능을 확인합니다.
  • 이 과정에서 서버의 능력과 제약 사항이 결정됩니다.

3. 기능 탐색

  • 클라이언트는 서버에 tools/list 등의 요청을 보내 사용 가능한 도구, 리소스, 프롬프트 목록을 수집합니다.
  • 이 정보는 호스트를 통해 LLM에게 전달됩니다.

4. 요청 처리 과정

  1. 사용자가 호스트에 질문을 입력하면 LLM이 이를 분석합니다.
  2. LLM은 적절한 서버와 도구를 선택해 구조화된 요청을 생성합니다.
  3. 호스트는 해당 요청을 관련 클라이언트에 전달합니다.
  4. 클라이언트는 요청을 서버가 이해할 수 있는 형식으로 변환하여 전송합니다.
  5. 서버는 요청된 작업을 수행하고 결과를 반환합니다.
  6. 결과는 클라이언트를 통해 호스트로 전달되며, 최종적으로 LLM에게 제공됩니다.

5. 통신 형식

  • JSON-RPC 2.0 프로토콜을 기반으로 데이터 교환이 이루어집니다.
  • 텍스트 데이터는 UTF-8 인코딩, 이미지 및 바이너리 데이터는 Base64 인코딩을 사용하여 전송됩니다.

MCP 구조

mcp구조

MCP는 호스트(Host), 클라이언트(Client), 서버(Server) 세 개의 컴포넌트로 이루어져 있습니다.

1. 호스트(Host)

호스트는 AI 애플리케이션의 컨테이너이자 조정자 역할을 합니다.

호스트의 주요 역할

  • 여러 클라이언트 인스턴스를 생성하고 관리
  • 클라이언트 연결 권한과 생명 주기 제어
  • 보안 정책과 동의 요구사항 시행
  • AI/LLM 통합 및 샘플링 조정
  • 대화 컨텍스트 관리 및 클라이언트 간 컨텍스트 집계

2. 클라이언트(Client)

클라이언트는 호스트에 의해 생성되며 서버와의 독립적인 1:1 연결을 유지합니다.

클라이언트의 주요 역할

  • 서버당 하나의 상태 유지 세션 설정
  • 프로토콜 협상 및 기능 교환 처리
  • 양방향으로 프로토콜 메시지 라우팅
  • 구독 및 알림 관리
  • 서버 간 보안 경계 유지

3. 서버(Server)

서버는 특정 컨텍스트와 기능을 제공하는 독립적인 프로그램입니다.

서버의 주요 역할

  • 리소스, 도구, 프롬프트 등의 기능 노출
  • 독립적으로 작동하며 특정 책임 수행
  • 클라이언트 인터페이스를 통해 샘플링 요청 처리
  • 보안 제약 준수
  • 로컬 프로세스 또는 원격 서비스로 구현 가능

클라이언트가 서버를 호출하는 방식

1. stdio 방식 (표준 입출력)

  • 클라이언트가 서버를 하위 프로세스로 실행합니다.
  • 서버는 표준 입력(stdin)으로 명령을 받고 표준 출력(stdout)으로 응답합니다.
  • JSON-RPC 2.0 형식으로 메시지를 주고받습니다.
  • 로컬 환경에서 설정이 간단하고 빠르게 실행 가능합니다.

2. HTTP+SSE 방식 (Server-Sent Events)

  • 클라이언트는 HTTP POST 요청으로 명령을 서버에 보냅니다.
  • 서버는 SSE(Server-Sent Events)를 사용하여 클라이언트에게 응답이나 이벤트를 스트리밍합니다.
  • 웹 애플리케이션 및 분산 시스템에서 유용합니다.

마무리

MCP는 AI 모델이 외부 데이터와 도구에 접근할 수 있는 표준화된 방법을 제공함으로써 AI 애플리케이션의 가능성을 크게 확장시키고 있습니다. 호스트, 클라이언트, 서버로 구성된 아키텍처는 복잡한 시스템을 효율적으로 관리할 수 있도록 돕습니다.

안쓰는 노트북으로 개인 서버 만들기💻-배포관리 편

· 13 min read
eunsung shin
Software Engineer

이전 글에서는 홈서버의 네트워크를 셋팅했다. 이번 글에서는 본격적으로 홈서버에서 사이드 프로젝트를 배포하고 관리하기 위해서 필요한 설정들을 하려고 한다. 글의 구성은 다음과 같다. 필요한 부분만 찾아서 확인해보셔도 좋겠다.

  • 리눅스 설치
  • ssh 설치
  • 서버용 설정
  • docker-registry 설정
  • github-action runner 설정
  • 쿠버네티스 환경 구성

리눅스 설치

macos의 버젼이 오래 돼서 도커 데스크탑 설치가 안 된다. os를 업데이트하려고 했지만, 왠지 모르는 이유로 소프트웨어 업데이트 버튼 클릭 시, 무한로딩에 걸려버려서 리눅스를 설치했다.

리눅스를 설치한 맥북


ssh 설치

다음은 외부에서 홈서버의 콘솔에 접속할 수 있도록 ssh를 설치한다. ssh는 컴퓨터 간 암호화되어 안전한 통신을 가능하게 해주는 프로토콜이다. ssh에 대해 처음 들어본다면, 검색해보자. 자세히 잘 설명해준 글들이 많다.

비밀키와 공개키를 통해 컴퓨터간 통신을 하는 ssh id_rsa를 발급받고 id_rsa.pub을 서버에 심는다. 그리고, 공유기 관리자 페이지에서 포트 포워딩을 해준다.
이제 외부에서 로컬 서버로 접속이 가능하다.
ssh 접속 확인! 현재는 내부 네트워크에서 접속하는 거지만, 외부에서 접속하고 싶다면, 마찬가지로 ssh key를 발급받고, 서버에 심은 다음, 이전 글에서 설정한 dDNS 주소로 접속하면 된다.


서버용으로 노트북 설정 변경하기

이 노트북은 24/7 돌아가기에 적합한 서버용은 아니지만, 필요한 설정들을 해주자. 절전 모드를 비활성화하고, 덮개가 닫혀도 돌아갈 수 있도록 설정한다.

sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target

노트북의 절전 모드를 끈다 이제 이 서버(노트북이였던)는 더 이상 절전모드에 들어가지 않는다. 그리고 이어서 노트북 덮개를 덮어도 꺼지지 않도록 설정한다. etc 디렉토리의 logind.conf 에 들어가서 그림 아래 두 설정들을 ignore로 바꿔준다.

sudo nano /etc/systemd/logind.conf

노트북이 덮여도 꺼지지 않도록 구성한다.

노트북이 덮여도 꺼지지 않도록 구성한다.

그리고 서버가 재부팅되었을 때, 시작할 프로그램들을 설정해준다.

systemd 서비스에 직접 등록하거나, systemctl enable [your-service]로 설정할 수 있다. 나는 docker registry와 github action runner가 자동으로 시작되도록 설정했다.


docker-registry 구성

다음은 docker-registry 구성했다. docker-registry는 도커 이미지 저장소이다. docker-registry를 처음 들어봤다면, AWS의 ECR이나 dockerhub를 생각하면 된다. 도커 컨테이너로 docker-registry를 띄우고, 컨테이너가 내려가도 이미지들은 그대로 저장되어 있을 수 있도록 volume을 지정해두었다.


github action runner 구성

그리고 github action runner를 local-hosted runner로 구성했다. github action은 CI/CD를 무료로 제공하는 편리한 서비스다. github action은 기본적으로 github에서 제공하는 runner를 사용하는데, 이 runner는 무료인만큼 속도가 비교적 느리다는 단점과 따로 캐시를 설정하지 않는다면, 매번 CI/CD에 설정된 빌드 과정을 처음부터 실행하여 시간이 오래 걸린다는 단점이 있다. 이 또한, 설정하는 방법은 github action 페이지의 local-hosted runner 섹션에 친절히 설명되어 있으므로 자세한 과정은 생략한다.

github action 페이지에 local-hosted runner를 프로젝트별로 설정해야하길래, 나는 Organization을 만들어 사이드 프로젝트들을 다 옮기고, 이 organization을 관리하는 local-hosted runner로 등록했다.


쿠버네티스 환경 구축하기

쿠버네티스가 과연 필요한가?

마지막은 대망의 쿠버네티스 환경 구축이다. 사실 이 부분은 환경적으로 정말 필요해서라기보다는 쿠버네티스 운영을 맛보고 싶어서다.

보통 쿠버네티스 환경 구성은 최소 3개의 마스터 노드와 여러 개의 워커 노드들로 이루어져있다. 나는 단지 맥북 하나 있을 뿐이고, 이 환경이 얼마만큼의 부하를 견딜 수 있을지도 모른다.

그리고 쿠버네티스의 기능은 컨테이너 오케스트레이션, 즉 여러 개의 컨테이너들을 한 번에 관리하는 것이다.이 여러 개의 컨테이너가 필요한 이유는 서버가 받는 트래픽을 분산시키기 위해서(또는 아키텍쳐 구성상 예를 들면 MSA)인데, 내가 진행하고 있는 사이드 프로젝트들 중에서 서버량이 여러 개의 인스턴스가 필요할 정도의 트래픽은 있는 건 아니다.

정리하자면 노드도 한 개이고 서비스 규모도 크지 않아서 쿠버네티스의 필요성은 의문이다. 하지만, 뭐 이렇게 시간이 남아돌 때 해봐야지 언제 해보겠나. 간단한 서비스를 띄워보고, 실제 트래픽이 많은 서비스 구성과는 어떻게 다를지 가늠하는 식으로 진행해보자.

쿠버네티스를 어떻게 구성하면 좋을까?

쿠버네티스에는 여러 종류의 배포판이 있다. 단일 노드 환경에 적합한 경량화된 쿠버네티스 배포판에는 minikube, k3 , kind 등이 있는데, 그 중에 k3를 선택했다. k3를 선택한 이유는 상태와 구성 정보를 저장하는 키-값 저장소인 etcd 대신 sqlite3를 사용하여 리소스를 절약할 수 있기 때문이다. minikube와 kind는 etcd를 사용한다. 우리는 단일 노드를 사용하기 때문에 etcd의 분산 시스템이 필요가 없다.

내 서버는 i5-7360U, cpu 4코어, 8gb RAM, 228gb 디스크를 가지고 있다. 이 노드 안에서 구성을 어떻게 할지 고민됐다.

서버 안에 가상 머신으로 노드들을 몇 개 구성하면, 좀 더 실제와 가까운 쿠버네티스 셋팅을 맛볼 수 있을 것 같다는 생각이 들었다. 예를 들어, taint나 tolerate 같은 노드 라벨링, 노드 간의 네트워크 통신 설정, 드레이닝이나 코든 같은 노드 유지보수 과정들을 실습해볼 수 있을 것 같다.

하지만 결과적으로는 가상 머신으로 분리하지 않고, 단일 노드에서 마스터 노드가 워커 노드 역할을 겸하는 구성으로 가기로 했다. 왜냐하면, 첫번째로 가상 머신을 여럿 띄울만큼의 리소스가 없다는 점. 그리고 두번째로 마스터 노드와 워커 노드의 분리 목적이 애초에 클러스터의 안정성을 위해서라는 점에서, 가상머신을 띄운 맥북이 다운되면 노드 전부가 꺼져버릴 게 당연하기 때문이다.

앱을 띄워보자

이 단일 노드에 쿠버네티스에 띄울 앱과 상태를 적은 쿠버네티스 manifest를 작성했다. 최대한 간단하게 앱을 구성했다. fastapi, react, mysql, 그리고 nginx로 돌아가는 앱이다. 이름을 등록하면, '이름 hello world'를 리턴하는 간단한 앱이다.(코드는 여기)

이 앱은 이름도 프린트해준다!

이 앱은 이름도 프린트해준다!

Details

소소한 트러블 슈팅 docker registry에 이미지를 push할 때, github action이 계속 https로 요청을 보내서 오류가 난다. 우선은 docker daemon config에 insecure-registries 목록에 해당 registry 주소를 추가하여 (우회)해결했다. 근본적으로는 registry에도 https로 통신하는 게 맞기 때문에 후에 추가로 설정을 바꿔봐야겠다.

그리고 k8s에 올리기 위해 필요한 manifest들을 작성한다. 앱의 라벨링을 담당할 namespace, 각 요소(backend, frontend, nginx)의 deployment와 서비스, 그리고 각종 config들을 담을 configmap과, secret, 그리고 요청의 라우팅을 처리해줄 ingress(이쪽은 손을 더 봐야할듯)를 작성한다. 이후에는 self-hosted runner에서 돌아갈 workflow를 작성해준다. (몇 문장으로 정리가 가능하지만, 이 과정에 거의 세시간 정도 소요된 것 같다…) 드디어 이제, 로컬 서버 쿠버네티스에서 실행되는 앱을 확인할 수 있다….!

쿠버네티스에 올라간 앱 동작 확인

이번 시간에는 홈 서버에 작성한 앱을 배포하기 위한 배포 환경을 구성했다.

양이 생각보다 길어져서 다음 편으로 이어서 쓰겠다. 다음 편에는 이 앱을 가지고, 여러 설정을 추가(ingress 설정, 파드 증설 등)하고 실험을 해보면서 모니터링을 해보려고 한다.

안쓰는 노트북으로 개인 서버 만들기💻-네트워크편

· 10 min read
eunsung shin
Software Engineer

얼마전 의도치 않은 AWS 요금 폭탄을 맞고(미리 요금 모니터링을 안 한 내 잘못이 맞ㄷㅏ…), 개인용 pc로 배포 서버를 구성하기로 마음 먹었다. 마침 중고로 팔려고 언젠가부터 창고 한켠에 쳐박아두었던 맥북 프로가 생각났다. 오랫동안 쓰지 않은 터라, 부팅 속도가 느리고, 프로그램 하나만 실행해도 팬 돌아가는 소리가 시끄럽긴 했지만, 꽤 잘 동작한다.

macbook.png

이 맥북을 서버로 사용하기 위해 필요한 작업을 크게 세가지로 정리했다.

  1. 서버 셋팅
  2. 외부에서 접속 가능하도록 네트워크 설정
  3. 앱 배포

서버 셋팅과 앱 배포 부분은 우선 네트워크를 구성해놓은 후 천천히 진행해도 될 것 같아서, 이번 글에서는 2번 네트워크 설정한 후기에 대해서 적어보려고 한다.


진행 방식은 다음과 같다.

  1. 홈 네트워크 구성 파악하기
  2. 외부 IP와 내부 IP 파악하기
  3. DHCP 서버에서 고정 IP 할당받기
  4. 포트 포워딩하기
  5. dDNS 등록하기

홈 네트워크 구성 파악하기

우선 맥북이 연결된 네트워크의 구성을 파악할 필요가 있었다.

네트워크 구성을 살펴보면 이렇게 그림으로 정리될 수 있다.

network_composition.png

네트워크 구성 트러블 슈팅

우리집 네트워크는 회선이 두 개다. 하나는 내 방에 랜선으로 데스크탑에 연결되어있고, 다른 하나는 거실에 WiFi를 연결할 수 있는 공유기로 연결되어 있다. 회선을 2개를 사용(아니 애시당초 왜 2개 회선인 거지…?)하고 있기 때문에 당연히 나는 구성이 데스크탑과 WIFI가 서로 별개의 네트워크 구성이라고 생각했다. 그림으로 그려보자면,

wrong_network_composition.png

이런 식으로 말이다.

결론적으로 알게 된 구성의 형태는

network_composition.png

이런 식이였다.

35.1 공유기의 포트 포워딩 설정 후에도 외부에서 접속이 불가한 이슈가 있었는데, 한참을 헤매다가 traceroute 8.8.8.8 커맨드로 35.1 공유기를 지나친 패킷이 55.1을 거쳐간다는 걸 확인하고 35.1이 55.1에 속해있다는 걸 파악할 수 있었다.

traceroute.png

그래서 이 35.1 공유기를 bridge모드로 설정하여 서브넷팅 없이 패킷만 전송하도록 설정하니 외부에서 접속이 가능해졌다.

아무튼 내 소소한 삽질이였고, 본인의 네트워크 구성도 skbroad밴드에서 회선을 두 개 사용하는 분이라면, 위에 저 그림만 참고하면 좋을 것 같다.

그런데 인터넷 회선은 결국 랜선에 연결된 55.1 하나인 것 같은데, 도대체 왜 회선 2개만큼의 비용을 지불해야하는지 의문이다. 아시는 분…?


외부 IP와 맥북 IP 파악하기

외부에서 바라보는 내 네트워크(맥북이 연결된)의 공인 IP를 우선 파악한다.

이렇게 되면

ip.pe.kr에 접속하면 내 공인 IP를 확인할 수 있다.

public_ip.png

그리고 이제 맥북의 IP를 파악해야한다. 공유기 관리자 페이지(?)에서 확인할 수도 있고, ifconfig 를 통해 확인할 수도 있다.

ifconfig.png


DHCP 서버에서 고정 IP 할당받기

지금 맥북 주소는 공유기의 DHCP 서버로부터 할당받은 동적 IP이기 때문에, 공유기 관리자 페이지에서 DHCP 서버가 이 맥북에게 고정 IP를 제공할 수 있도록 설정해줘야 한다.

dhcp_static_ip.png


포트 포워딩하기

port_forwarding.png

맥북에서 실행되는 앱을 외부에서 접근할 수 있도록 하려면, 포트 포워딩이 필요하다.

실제로 앱이 접속가능한지 테스트해보기 위해 간단한 fastapi 앱을 작성해준다.

simple_app.png

그리고, 공유기 관리자 페이지에서 외부에서 해당 포트로 접속했을 경우 맥북 IP의 로컬 포트로 연결받을 수 있도록 설정을 추가해준다.

port_forwarding_page.png

그럼 이제 외부 IP의 8000번 포트로 접속이 성사되는 걸 확인할 수 있다.

ip_sample_success.png


dDNS 등록하기

마지막으로 한가지 문제가 남았다.

맥북은 와이파이를 통해 연결이 되고, 가정용 인터넷 서비스는 대부분의 경우 동적 IP를 할당한다.(고정 IP를 할당받기 위해서는 추가적인 비용이 필요하다.) 왜 동적 IP를 할당하냐면, 인터넷 서비스 업체들(ISP)이 수익을 최대화하기 위해서라고 할 수 있다. 접속을 끊은 사용자들의 IP는 거둬들이고, 접속한 사용자들에게 다시 할당하는 방식으로 한정된 IP주소들을 최대한 활용하는 방법이다.

외부에서 계속해서 바뀌는 동적 IP 주소를 접근하도록 하기 위해서는 동적 DNS가 필요하다. DNS(Domain Name System)는 서버의 이름과 IP를 매칭해서 관리하는 시스템이라고 보면된다. 서버의 이름을 설정해놓고, 서버에 DNS가 제공하는 소프트웨어를 설치해놓으면 지속적으로 변경되는 IP를 DNS에서 추적해서 업데이트해준다. (무료로 DNS를 활용할 수 있는 사이트들이 있다.)

나는 noip.com이라는 사이트를 이용했다. 내 공인 IP와 hostname을 매칭해서 설정해준다.

noip_ddns.png

이제 dDNS에서 할당받은 hostname으로 맥북에 실행시켜둔 앱이 정상적으로 동작하는 걸 확인할 수 있다.

ddns_sample_success.png


마무리

이렇게 외부에서 접근가능한 개인서버의 네트워크 설정이 완료되었다. 이 맥북 프로를 팔았을 때 중고가격(서버로 운용한다면 사용가능한 수명이 확 떨어지지 않을까…), 24시간 운용했을 때 발생하는 전기비 등 클라우드 서비스 비용과 비교해봤을 때 개인 서버 운용이 과연 경제적인가 하는 질문에 대해서는 좀 더 지켜봐야겠지만, 서버를 구성하면서 나름 네트워크 공부도 하고 나만의 서버를 구성한다는 것 자체가 즐거웠다.

정상적으로 동작하긴 하지만, 아직 추가할 부분들이 많다. 도메인 구매, SSL설정, 도커 레지스트리 구성, CI/CD 설정 그리고 쿠버네티스 환경 구성, 모니터링, 백업 설정 등 앱 올려보면서 진행해보려고 한다. 다음 글은 여기에서 확인 가능하다!

메이플랜드의 시세는 어떻게 결정되는 걸까🍁

· 9 min read
eunsung shin
Software Engineer

이번 글은 호기심을 데이터로 구현하는 월간 데이터노트 1회차를 참여하면서 작성한 내용을 담았다.


내가 월간 데이터노트에 참여하게 된 계기

참여중인 개발자 글쓰기 모임(글또)에서 월간 데이터노트 홍보글을 봤다.

월간 데이터노트는 호기심을 데이터로 구현하여 1-2장의 결과물을 기록하는 모임이다. 예를 들어, 계절별 과일 가격은 추석이나 설날 같은 명절에 어떻게 변동하는지 추적하거나, 한강의 노벨문학상 수상을 통해 종이책 구매량은 어떻게 변했는지와 같은 일상 주제들을 데이터로써 분석해볼 수 있다.

예전부터 자주 이런 일상 주제들에 대해서 머릿속으로 나름의 가설을 세우고, 증명하기 위해서는 어떤 데이터들이 필요할지 상상해보곤 했다. 예를 들어, 퇴근시간과 같이 교통량이 몰리는 시기에 신호 패턴이 어떤 식으로 바뀌는지, 그에 따른 영향은 어떤 식으로 수치화할 수 있는지와 같은 상상.. 그래서 이런 상상을 직접 데이터를 수집해보고 결과물을 뽑아보는 실천으로 옮겨보면 재밌을 거라는 생각에 참여하게 되었다.

분석 주제 및 목적

  • 초등학교 시절 자주 하던 메이플스토리. 그 시절 메이플스토리가 최근에 다시 ‘메이플랜드’ 라는 이름으로 런칭되었다. 반가운 마음에 유튜브에 뜬 메이플스토리 플레이 영상을 보다보면 아련한 옛 추억에 잠기곤 한다.

  • 찾아보니 게임머니를 현금처럼 사용하고, 아이템을 구매하기 위해 적지 않은 금액을 기꺼이 지불하는 걸 알게 되었다.

  • 아이템 매니아(25.01.09) 기준 현재 100만메소당 5000-9000원 정도의 가격에 거래되는 것을 확인할 수 있다. 정말 비싼 아이템은 몇백만원에 거래되기도 한다.

  • mapleland.gg같은 사이트에 팝니다, 삽니다 정보를 알아보기 쉽게 정리되어있다.

    레드크리븐.png

분석 결과

“아이템 시세에 영향을 미치는 요소에는 뭐가 있을까?”

  • 시세를 어떻게 측정할 수 있을까?

    매수희망 가격을 내림차순, 매도희망 가격을 오름차순으로 정렬함.

    파는 사람은 최대한 비싸게, 사는 사람은 최대한 싸게

    순서대로 매수/매도 금액을 매칭하여 거래가 성사될 수 있는 건들에 대한 평균값을 시세로 측정.

    노작기준_매수_매도가격_비교.png

    업그레이드 없는 순정 레드 크리븐을 기준으로 시세는 2억 4889만 메소로 형성 되어있음.

    *발표 후 질문 중에 ‘실제 성사된 거래 내역을 확인해볼 수도 있을까요?’라는 질문에 깨닫게 되었는데, 시세를 확인하기 위해 먼저 고려해볼 부분은 실제 거래가 성사된 내역이라는 걸 간과하고 있었다.

  • 어떤 아이템의 시세를 살펴보면 좋을까?

    현시점(2025-01-25) 직업군 중 가장 쎈 도적의 엔드 아이템(가장 좋은 아이템) 레드 크리븐

    옵션이 좋은 매물을 기준으로 5억 메소에 매수가가 형성되어 있음. 5억 메소는 자그마치 현금 450만원(100만 메소=9000원 가정)

    레드크리븐.png

  • 아이템 스탯별 시세의 상승률을 시각화해보자

    주문서_종류.png

    • 메이플랜드에는 “주문서” 시스템이 있음. 주문서를 사용해서 아이템의 스탯을 업그레이드할 수 있는 시스템임

    • 아이템에는 보통 업그레이드횟수가 7번 정도 주어짐.

    • 주문서는 성공 확률에 따라 10%, 60%, 100%로 나뉘는데, 성공확률이 낮을수록 업그레이드할 수 있는 스탯 비중이 높아짐.

    • 공격력별 성공확률 시각화

      주문서_성공확률.png

    • 공격력별로 매수/매도 데이터를 그룹화한 후, 시세를 구한다.

    공격력별_시세비교.png

    === 시세 분석 ===
    공격력과 시세의 상관계수: 0.776
    매도 있는 최대 공격력: 12
    매수 있는 최대 공격력: 18

    === 공격력 구간별 가격 상승률 ===
    공격력 3 → 4: 0.0%
    공격력 4 → 5: 122.2%
    공격력 5 → 6: 80.0%
    공격력 6 → 7: 144.4%
    공격력 7 → 8: 11.4%
    공격력 8 → 90: 69.4%
    공격력 9 → 10: 137.3%
    공격력 10 → 11: 39.1%
    공격력 11 → 12: 9.5%
    공격력 12 → 13: 216.7%
    공격력 13 → 14: 47.4%
    공격력 14 → 15: 42.9%
    공격력 15 → 16: 6.1%
    공격력 16 → 17: 88.6%
    공격력 17 → 18: 75.0%

진행하면서 배우게 된 것/ 더 보충할 수 있을 것 같은 부분

  • 분석면에 있어서 한 문장으로 정의내릴 수 있는 명확한 결론은 나지 않은 것 같다.
  • 시세를 어떻게 정의내릴지에 대해 알아본 것은 유용했다.
  • 업그레이드 수준에 따른 시세 변화 같은 경우에 결론은 결국 ‘업그레이드가 잘 될 수록 시세는 비싸진다’ 정도에서 그친 것 같아서 좀 아쉽다. 업그레이드 횟수와 성공확률에 따라 ‘기회비용’이 얼마정도 되는지(과연 추가로 주문서 작을 하는 게 나을지에 대한 판단을 위한 수치)와 같은 분석을 추가로 진행하면 좋을 것 같다는 생각이 들었다.
  • 한 아이템에 대해서만 분석을 진행했는데, 크롤링 범위를 전체 아이템 또는 API 형식으로 확장하고 범용적으로 분석이 가능한 대쉬보드를 작성한다면 좀 더 유용한 결과물이 될 수 있을 것 같다는 생각이 들었다.

데이터출처 & 코드

  • mapleland.gg - 셀레니움으로 크롤링(코드는 chatGPT로 작성)

Django Signal 쉽게 이해하기⚡️

· 14 min read
eunsung shin
Software Engineer

Intro

django를 학습 중이다. django에서는 웹 서비스 개발에 필요한 편리한 도구들을 제공한다. 그 중 Signal은 이벤트 기반 아키텍쳐에서 강력한 도구로 활용될 수 있다. 이번 글에서는 Django Signal이란 무엇인지, 어떻게 동작하는지, 그리고 어떤 상황에서 Signal을 사용하는 것이 적절한지에 대해 심층적으로 알아보려고 한다.


Signal이 뭔가요?

signal은 django 시스템 실행 중 이벤트가 발생했을 때 실행되는 콜백 메커니즘이다. 이 시그널은 이벤트를 알리는 publisher와 알림을 받는 subscriber로 이루어진 pub/sub 구조를 통해 동작한다. publisher가 이벤트의 특정 동작 시점을 알리면, 대기하고 있던 subscriber는 그에 맞춰서 필요한 동작들을 수행한다. 이 pub/sub 구조의 장점은 느슨한 결합을 유지할 수 있다는 점인데, 예를 통해 어떤 의미인지 살펴보자.


Pub/Sub 구조

police

신호등이 없는 나라에 새로 부임받은 교통경찰이 있다. 이 교통경찰은 차량들이 지체없이 통행할 수 있도록 해야한다. 이 교통경찰은 지나가는 차량들을 일일이 붙잡고 멈춰야할 시점과 출발해야하는 시점을 알려준다. 열심히 뛰어다녔지만 혼자서는 버겁다. 그리고 간혹 말이 통하지 않는 외국인이 있는 경우에는 정보 전달을 위해 더 많은 시간이 걸렸다. 차량 퇴근시간이 되어 차량이 많아지자, 교통경찰 혼자서 차량들을 통제하는 일은 점점 더 버거워졌고, 결국은 3중 추돌 사고가 나고 말았다.

이렇게는 안 되겠다 싶었던 경찰은 신호등을 설치하기로 마음먹는다. 빨강, 노랑, 초록 싸인에 대한 설명 글귀를 큼지막하게 적어 운전자들이 확인할 수 있도록 하고, 차량들이 신호등의 신호에 따를 수 있도록 통제했다. 색깔 사인은 언어가 달라도 이해할 수 있었기 때문에 외국인도 별 무리 없이 운전이 가능했다. 만약 신호등 통제를 따르지 않는다면, 벌금을 부과하도록 단속했다. 이제 모든 차량들이 신호등의 신호에 집중해서 악셀을 밟을지 브레이크를 밟을지 결정했고, 교통 경찰은 더 이상 차량들을 하나하나 통제하지 않아도 됐다.

pubsub

이 스토리에서 교통경찰이 일일이 차량을 통제하는 건 강한 결합, 그리고 신호등을 통해 통제하는 건 느슨한 결합이라고 볼 수 있다. SRP 원칙에 따라 한 앱(모델 또는 앱)은 자신이 맡은 역할만 수행하는 게 기본적이지만, 로직상 다른 앱의 처리과정과 연관되어 있을 경우도 자주 발생한다. (예를 들어 온라인 쇼핑몰에서 상품 결제 처리 시 재고 처리, 결제 처리, 알림 처리 등이 동작해야한다.)

pub/sub 구조는 로직을 같은 컨텍스트 안에 작성하여 강하게 결합된 코드를 만드는 대신, ‘분리되어야하는 동작을 발동시키는 로직’을 ‘이벤트’로 정의하고 큐에 넣어서 처리한다. 이렇게 하면, 모델은 본 컨텍스트에 관련된 로직만 처리하고, 신호등과 같은 역할인 큐에서 컨슈머들(차량)이 알아서 가져가서 처리할 수 있도록 할 수 있다. 퍼블리셔는 자신의 행동 외에 컨슈머가 어떤 행동을 할지에 대해서 모르고, 컨슈머도 발생한 이벤트에 맞춰 자신이 어떤 행동을 해야할지만 알 뿐이다.


정리하자면, signal을 사용함으로써 얻을 수 있는 이점은

“독립적인 컴포넌트들이 독립적으로 구분되어 서로를 모르고 동작할 수 있도록 한다.”


한 가지 주의할 점은 일반적인 pub/sub 구조는 publisher와 subscriber 가운데에 queue 또는 broker를 두어 느슨한 결합을 이루지만, django signal 같은 경우 따로 queue없이 publisher, 즉 signal 자체가 브로커 역할을 한다는 점이다.

django에서 signal을 사용하는 대표적인 예는 유저 정보를 처리할 때다. django에서 제공하는 User 모델로는 충분한 정보를 담지 못 하기 때문에 일대일 대응되는 UserProfile 같은 모델로 추가 정보를 담는 경우가 일반적인데, User 생성시에 UserProfile도 함께 생성될 수 있도록, User 생성 이벤트에 UserProfile이 생성되는 시그널을 만들어 처리한다.

@receiver(post_save, sender=User) 
def create_user_profile(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)

receiver 데코레이터는 아래 함수가 이벤트 콜백 함수임을 명시하고, 어떤 이벤트에 동작하는지 정의한다. create_user_profile이 User 모델의 post_save 시에 동작하는 콜백함수인 것을 확인할 수 있다.


signal 종류

이벤트 종류에 따라 시그널 종류가 나뉜다.

크게 ORM관련, Request와 Response 사이클 관련, Authentication 관련, 그리고 데이터베이스 연결, 테스트, 매니지먼트(manage.py 실행 관련), Admin관련, 미들웨어 관련 등으로 나눌 수 있다. 다양한 종류들이 존재하지만, ORM, Request/Response, 그리고 Authentication 관련 시그널이 많이 쓰인다고 볼 수 있다. pre, post와 같이 세부적으로 이벤트 시점 조정이 가능하고, 필요하다면 Custom Signal을 만들어 특정 이벤트에 동작할 수 있도록 구성할 수도 있다.

아래 표를 통해 어떤 signal 종류가 있는지 알아보자.


ORM 관련

Signal설명
pre_init모델 인스턴스가 초기화되기 전에 발생
post_init모델 인스턴스가 초기화된 후에 발생
pre_save모델 인스턴스가 저장되기 전에 발생
post_save모델 인스턴스가 저장된 후에 발생
pre_delete모델 인스턴스가 삭제되기 전에 발생
post_delete모델 인스턴스가 삭제된 후에 발생
m2m_changedManyToManyField관계가 변경될 때 발생
pre_migrate마이그레이션이 실행되기 전에 발생
post_migrate마이그레이션이 실행된 후에 발생

Request/Response 관련

Signal설명
request_startedHTTP 요청이 시작될 때 발생
request_finishedHTTP 요청이 끝날 때 발생합
got_request_exception예외가 발생했을 때 발생
setting_changedDjango 설정(settings.py)이 변경될 때 발생

Authentication 관련

Signal설명
user_logged_in사용자가 로그인할 때 발생합니다.
user_logged_out사용자가 로그아웃할 때 발생합니다.
user_login_failed로그인에 실패했을 때 발생합니다.
password_changed사용자의 비밀번호가 변경될 때 발생합니다.
password_reset사용자의 비밀번호가 재설정될 때 발생합니다.

어떤 식으로 사용할 수 있나요?

  1. Decorator 방식 (@receiver 사용)

가장 일반적이고 권장되는 방식이다.

# signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User

@receiver(post_save, sender=User)
def user_created_signal(sender, instance, created, **kwargs):
if created:
print(f'New user created: {instance.username}')

  1. connect 메서드 사용하기
python
# signals.py
from django.db.models.signals import post_save
from django.contrib.auth.models import User

def user_created_signal(sender, instance, created, **kwargs):
if created:
print(f'New user created: {instance.username}')

post_save.connect(user_created_signal, sender=User)

차이점:

  • connect는 동적으로 Signal을 연결할 수 있다.

c. Custom Signal 만들기

python
코드 복사
# signals.py
from django.dispatch import Signal

# Signal 정의
order_completed = Signal()

# Signal 수신기
def notify_order_completed(sender, **kwargs):
print(f"Order completed for: {kwargs['user']}")

# Signal 연결
order_completed.connect(notify_order_completed)

# Signal 발행
order_completed.send(sender=None, user='John Doe')

그럼 signal은 디렉터리 구조 중 어디에 위치하는 게 좋을까?

일반적으로 다음과 같은 구조를 사용한다.

myapp/
├── __init__.py
├── models.py
├── views.py
├── signals.py <-- Signal 정의
├── apps.py <-- Signal 등록
└── admin.py

(apps.py에 시그널을 등록해야만 활성화된다는 점도 잊지말자)

# apps.py
from django.apps import AppConfig

class MyAppConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'myapp'

def ready(self):
import myapp.signals # Signal import


어떻게 동작하나요?

signal의 동작 방식은 생각보다 단순하다. django.dispatch.dispatcher.py의 Signal의 connect 함수를 통해 확인 할 수 있다.

with self.lock:
self._clear_dead_receivers()
if not any(r_key == lookup_key for r_key, _, _ in self.receivers):
self.receivers.append((lookup_key, receiver, is_async))
self.sender_receivers_cache.clear()

lock이 걸린 상태에서 connect함수가 호출될 경우, Signal 인스턴스에 정의된 receivers를 순회하며 해당 signal에 대해 어떻게 반응해야할지 찾는다. 그리고 기본값으로 설정되어 있는 weak=True는 receiver가 호출 후, garbage collector에 의해 수거되어 메모리를 낭비하지 않아도 될 수 있도록 약한 참조를 할 수 있게 돕는다.

if weak:
ref = weakref.ref
receiver_object = receiver
# Check for bound methods
if hasattr(receiver, "__self__") and hasattr(receiver, "__func__"):
ref = weakref.WeakMethod
receiver_object = receiver.__self__
receiver = ref(receiver)
weakref.finalize(receiver_object, self._remove_receiver)

어떨 때 사용하면 좋을까요?

장점과 단점을 알고 상황에 맞게 사용하는 것이 중요하다. 위에서 언급한 것처럼 signal을 사용하면서 얻을 수 있는 장점은 느슨한 결합이다. 책임이 분리되어있기 때문에 코드가 깔끔해지고, 유지보수가 쉬워진다는 장점이 있다. 하지만 물론 단점도 있다. 코드 흐름을 알기 어렵다. two scoops of django 책에는 “그러므로 시그널을 받는 리시버가 어떤 것인지조차 알 필요가 없는 경우라면 그 때 시그널을 활용하라”라는 조언이 있다. 그리고, signal은 “동기화되고 블로킹을 일으키는 무거운 프로세스를 호출한다. 확장과 성능 면에서 어떤 장점도 찾아볼 수 없다”라는 단점이 있기도 하다. 그러므로 signal을 과도하게 사용하기보다는 정말 필요한 경우에만 사용하도록 하고, 무거운 작업일 경우, celery를 사용하는 등 비동기 처리로 옮기는 게 더 바람직하다.


Outro

이번 글에서는 django Signal에 대해 알아보았다. signal이 어떤 장점을 가지고 있는지, 어를 통해 어떤 기능을 구현할 수 있는지, 그리고 어떻게 동작하는지와 언제 사용하면 적합한지에 대해 살펴보았다. 모쪼록 django를 공부하는 분들께 도움이 되었으면 좋겠다.

오픈소스 첫 발자국 떼기🌱

· 12 min read
eunsung shin
Software Engineer

Intro

2024년이 끝나간다. 연초에 이루고 싶은 여러 목표들을 적어두었었는데, 그 중 하나는 '오픈소스 기여하기'였다. 미루고 미루다가 24년이 얼마 안 남은 시점에 그 목표를 시도해보려고 한다. 전에 해본 적이 없으니 어디서부터 시작해야할지 막연한 마음이 컸고, 이런 오픈소스 기여같은 작업은 뭔가 대단한 작업이라 내가 할 수 있을까 하는 생각 때문에 미루게 된 것 같은데, 우선 해보는 게 중요하지 않을까. 이번 글은 그래서 PR을 제출하는 과정에 있어서 어떤 생각의 흐름을 가지고 진행했는지에 대해서 써보았다.



오픈소스란?

소스 코드가 공개되어 있어 누구나 자유롭게 사용, 복사, 수정, 배포할 수 있는 소프트웨어를 말한다. 오픈소스의 장점은 모두에게 수정할 기회가 열려있다보니, 코드에 대해서 활발하게 의견 교류가 이뤄지고, 더 좋은 코드로 발전할 가능성이 높아진다는 점이다. 오픈소스 코드 작성에 기여하면서 얻을 수 있는 점은 다른 개발자들의 코드를 읽고 배우면서 개발 실력을 기를 수 있다는 점이다.



오픈소스 기여 팁

본격적으로 시작하기 전, 오픈소스 기여 관련 다양한 자료를 얻을 수 있었다. 다음과 같은 조언을 얻었다.

  • 어떤 오픈소스를 고를 것인가? => 대단한 게 아니여도 된다. 비교적 코드가 복잡하지 않은 리포지토리부터 시작해보자
  • 시작을 어떻게 하면 좋을까? => 자주 사용하는 라이브러리 또는 프레임워크
    => 리포지토리의 이슈 항목을 살펴보자. 보통 오픈소스는 컨트리뷰트를 어디서부터 시작하면 좋을지 안내하는 good first issue 라벨이 있다.
    => 전체 코드를 한 번에 이해하려 하지 말고, 관심있는 컴포넌트를 하나 선택해서 거기서부터 시작하는 게 좋다.
  • 어떻게 코드를 읽어야할지? => 우선 README부터 읽기
    => 시작 포인트를 찾아서 거기서부터 타고들어갈 것
    => 한줄 한줄 다 읽어볼 것
    => 리포지토리의 TODO 부분을 확인하고 기여할 수 있는 부분을 확인해볼 것
    => 테스트 코드 활용하기
    => 코드를 읽다가 이해가 안되는 부분이 있다면, 관련 PR을 찾아보는 것도 방법
    (배두식님의 PyCon KR 2023 오픈소스와 함께 성장하기를 참고했다.)


오픈소스 선정하기

위 팁들을 기반으로 요새 사용중인 웹 프레임워크인 django의 라이브러리 중 django-faker라는 라이브러리에 기여해보기로 마음먹었다.
django-faker는 django model의 값들을 랜덤으로 생성해주는 역할을 한다.
로직이 자료형에 알맞는 랜덤값만 생성하면 된다는 점에서 코드 파악을 위한 피로감이 다른 리포지토리보다 덜 하지 않을까하는 생각이 들었다. 한 가지 문제는 마지막 update가 2016년... 무려 8년전... 거의 업데이트가 더 이상 없는 라이브러리라는 점이였다. 하지만, 목표가 '오픈소스 기여하기'인만큼 이 또한 좋은 경험이 될 거라는 생각이 들었다. PR에 대한 피드백이 늦을 수 있다는 점만 감안하면 될 듯 했다.
django-faker의 커밋 히스토리



코드 파악하기

README.md를 우선 한 번 훑었다. README에는 어떻게 모듈을 사용할지에 대한 예제가 있어서 이해가 조금 더 수월해졌다. 그리고 테스트 코드를 보면서 어떻게 동작하는지 파악할 수 있었다. django-faker는 django Model의 field 타입을 예측하는 Guessor, 타입에 맞춰 값을 랜덤하게 생성하는 Generator, 생성한 값의 포맷팅을 담당하는 Formatter, 모델에 생성한 랜덤값을 주입하는 Populator로 이루어져 있다. 이에 맞춰서 테스트 코드도 각 부분들을 테스트하는 것을 확인할 수 있었다. django-faker의 동작방식



기여 목표 선정하기

django-faker는 업데이트된지 꽤 오래되었다. 하지만 해당 모듈이 의존하는 djangofaker는 계속 업데이트가 이뤄지고 있었다. 그래서 django-faker가 현재 버젼이 커버하지 못하는 새로운 django의 model field들이 있을 거라고 가정을 세우고, 이 부분에 코드를 추가해보기로 마음먹었다. 그리고 다음과 같이 목표를 세웠다.

  • django-faker에서 정해놓은 PR checks를 우선적으로 통과할 것.
  • 메인테이너 또는 오너가 내가 올린 PR을 merge시켜주는 것. (워낙 오래된 라이브러리라 리액션이 늦게 돌아올 수도 있지만, 우선은 해보자)


기여할 부분 찾기

우선 보통 기여할 부분을 찾는 방법은 이슈를 살펴보거나, 모듈 사용중에 불편함을 겪은 부분에 대해 개선방법을 제시하는 식으로 진행한다는 점을 기억하자. (우리는 전략적으로 django-faker가 업데이트가 오랫동안 이뤄지지 않았다는 점을 이용하는 거다.) django-faker의 마지막 업데이트 일자인 2015년을 기점으로 django에는 어떠한 모델 관련 업데이트가 있었는지 확인해보았다.(https://docs.djangoproject.com/en/5.1/releases/) 여러 변경사항들중에 2.0을 기준으로 시간 간격을 저장하는 DurationField 필드가 추가된 것을 확인할 수 있었다! 우선 내 계정으로 리포지토리를 fork하고 로컬에 clone했다.
repo_fork
실제로 django-fakerDurationField를 처리하지 못하는지 확인할 필요가 있었기 때문에, DurationField를 사용하는 모델을 하나 만들고, 테스트 코드를 작성해준다.

class GamePlayTime(models.Model):
game = models.ForeignKey(Game, on_delete=models.CASCADE)
player = models.ForeignKey(Player, on_delete=models.CASCADE)
duration = models.DurationField()
class TestDurationField(unittest.TestCase):
def testGamePlayTime(self):
generator = fake
populator = Populator(generator)
populator.addEntity(Game, 10)
populator.addEntity(Player, 10, {
'game': lambda x: Game.objects.order_by('?').first()
})
populator.addEntity(GamePlayTime, 10, {
'game': lambda x: Game.objects.order_by('?').first(),
'player': lambda x: Player.objects.order_by('?').first()
})
insertedPks = populator.execute()
self.assertEqual(len(insertedPks[GamePlayTime]), 10)

테스트가 django-faker에서 발생하는 AttributeError로 테스트를 통과하지 못 하는 걸 확인했다.


DurationField에러

새로운 코드를 반영하기 위한 브랜치를 생성한다. 브랜치 이름은 나름 비장하게 eature/enhance-django-support 그리고, 테스트 코드를 통과하기 위해 FieldTypeGuesserDurationField를 처리할 수 있도록 코드를 추가해주었다. (django와 faker의 버젼에 맞춰 코드를 업데이트해줄 필요가 있었는데, 이 부분에서 의외로 시간을 많이 사용했다.)


DurationHandling코드추가 이렇게 작성한 코드를 바탕으로 테스트를 다시 실행시켜준다.
테스트를 잘 통과한다..!
테스트를 내가 임의로 바꾸는 건 좋지 않은 방법이지만, 오랫동안 관리되지 않은 부분에 대한 업데이트를 적용하기 위한 테스트이기 때문에 우선 적용하기로 했다.



Pull Request 작성하기

테스트도 무사히 잘 통과하니, 이제 마지막으로 메인 리포지토리에 풀리퀘스트를 날려준다!! PR에는 어떤 문제를 찾았고, 그 문제를 해결하기 위해 어떤 변경사항을 적용했는지 코드를 보지 않더라도 의도와 변경사항이 명확히 전달될 수 있도록 작성하는 게 중요하다. 작성한 PR은 여기에서 확인 가능하다. PR작성



Outro

이렇게 첫 오픈소스 기여하기를 진행해보았다. 나름 단순한 코드 리포지토리였지만, 소스코드를 살펴보는 과정, 업데이트 릴리즈 노트를 확인하는 과정, 라이브러리 호환성을 맞추는 과정, PR을 잘 작성하는 과정 등 생각보다 신경쓸 것이 많았다. 하지만, 오픈소스 기여라는 것이 내가 엄두조차 못 낼 정도로 대단한 건 아니라는 걸 알게 되었다. 다음에는 버그를 찾고, 픽스해보는 PR을 날려보자! 2024년도 며칠 안 남았다. 연초에 다짐했던 목표 중 하나를 이렇게 글을 쓰면서 달성할 수 있어서 다행이다!

AI해커톤 2024 후기

· 9 min read
eunsung shin
Software Engineer

[해커톤 리포지토리 link]
항해 AI 해커톤에 참여했다. 몇 팀 참여하지 않긴 했지만, 기분 좋게 우승도 했다. 이번 글에서는 회고겸, 어떤 기능들을 구현했는지, 만약 시간이 더 주어진다면 어떤 부분들을 더 손볼 수 있을지 적어보려고 한다.

주제는 'AI를 통해 삶과 일의 균형을 찾아라'였다. 금요일 저녁 7시부터 다음날 오후 2시까지 진행되었다. 우리팀은 AI 개발자 1명, 백엔드 3명, 그리고 프론트 2명으로 구성되었다. 다들 의견 제시와 역할 분담에 적극적이여서 초반부터 끝날 때까지 신나게 진행할 수 있었다.

우리 팀이 정한 서비스 주제는 '채용담당관 입장에서 수많은 이력서를 검토하면서 느낄 수 있는 피로감을 줄이기 위해 이력서 정보를 LLM으로 요약정리해주는 서비스'였다. 시연영상.gif

메인 기능은 요약하자면 다음과 같다.

  1. 이력서 정보 추출 및 벡터DB에 저장(input: PDF file, output: 벡터DB저장 정보)
  2. 엠베딩 기반 질문 생성 기능(input: 이력서 ID, output: 이력서 관련 생성 질문)
  3. 자연어 기반 벡터DB 쿼리 기능(input: 프롬프트, output: list[이력서ID])

좀 더 자세히 설명하자면,

  1. 이력서 정보 추출 및 벡터DB에 저장
  • PDF 이력서 업로드 시, 텍스트를 추출하여 요약정리 후 벡터DB에 저장한다. 이력서에서 뽑을 수 있는 공통된 정보들을 정형화하여(지원자 이름, 직군[백엔드, AI, 프론트, 풀스택], 연차, 사용 언어) 벡터DB의 메타정보에 저장해두었다. Pydantic으로 만든 스키마가 있다면, 이에 맞춰 정보를 뽑아주는 langchain의 with_structured_output 함수를 사용했다.
  1. 엠베딩 기반 질문 생성 기능
  • 미리 저장해둔 이력서의 엠베딩 정보를 기반으로 질문을 생성한다. 질문의 종류는 직군별, 컬쳐핏, 경험, 프로젝트 질문으로 나뉜다. 예를 들자면, 이력서에 OCR 프로젝트에 대한 내용이 있다면, 해당 프로젝트에 대해 어떤 기여를 했는지, 어떤 어려움이 있었는지에 대한 질문을 생성해준다.
  1. 자연어 기반 벡터DB 쿼리 기능
  • 채용담당관이 적은 요구사항(프롬프트)을 기반으로 매칭되는 이력서ID들을 반환한다. 요구사항에 있는 메타정보로 우선적으로 필터링한 후, similarity search를 진행한다. (langchain에서는 이 과정을 query-construction chain이라고 명칭되어있다.)

생각보다 밤을 새면서 개발하는 과정은 쉽지 않았지만, 팀원들이 다 열심히 해준 덕분에 해가 뜰 무렵, 가시적인 결과물인 나오기 시작했다. 미리 MockAPI를 작성해둔 덕분에 각 도메인(프론트, 백엔드, AI)에서 따로 작업을 하긴 했지만, API 연동도 나름 순탄하게 진행되었다. 11월달에 들어 개발하면서 가장 뿌듯한 순간이였다. 그리고 해커톤 일주일이 지난 시점, 만약 시간이 더 주어진다면 어떤 부분을 더 개선해볼 수 있을까 고민했다. 구현한 기능은 LLM을 활용한다는 점에서 특수할 수는 있으나, 결국 데이터를 저장하고, 잘 읽어오는 과정이다. Store와 Retriever 객체로 추상화하여 틀을 만들어 변경사항에 유연하게 대처할 수 있도록 한 다음에 LLM을 활용해서 할 수 있는 여러 가지 시도들을 해볼 수 있을 거라는 생각이 들었다.

여러 가지 시도들을 하기 전에 가장 먼저 해야할 일들은 평가 지표를 설정하는 것이다. 시도들에 대한 객관적인 지표가 있어야만 서비스에 알맞는 선택을 할 수 있기 때문이다.

평가 프로세스 구축

현재 시스템의 성능을 객관적으로 측정할 수 있는 평가 체계가 필요하다. 현재 프로세스는 이력서를 요약하고 저장하고 쿼리하여 읽어오는 방식이다. 요약, 저장, 쿼리 각각의 과정에 대해 평가 지표를 구성할 수 있다. 그리고, 이 과정에서 사용자의 피드백을 반영할 수 있도록 한다면 더욱 좋을 거라고 생각한다.

  • 이력서 요약의 정확성 평가
  • 생성된 질문의 품질 평가
  • 검색 결과의 정확도 및 관련성 평가
  • 사용자 피드백 시스템 구축

이 밖에도 찾아보니 LLM관련 개선할 부분들이 굉장히 많았다. 시간은 좀 걸리겠지만, 천천히 이 부분들에 대해서도 알아보려고 한다.

임베딩 최적화

현재는 이력서 내용을 요약한 후 임베딩을 수행하고 있는데, 이는 중요한 정보의 손실을 초래할 수 있다.

  • 전체 이력서 내용에 대한 섹션별 임베딩 수행
  • 주요 키워드와 문맥 정보를 보존하는 청크 단위 임베딩
  • 다중 임베딩 모델의 앙상블 적용

검색 성능 향상

프롬프트 기반 검색의 정확도를 높이기 위한 방안:

  • 하이브리드 검색 구현 (키워드 + 시맨틱 검색)
  • 검색 결과 재순위화(Reranking) 도입
  • 사용자 피드백을 반영한 검색 결과 개선

데이터 저장소 최적화

메타데이터 저장 및 관리 전략:

  • RDB와 벡터DB의 하이브리드 구조 검토(어디까지 메타정보로 저장해도 될까? 어디서부터 RDB를 활용하는 게 좋을까?)
  • 메타데이터의 인덱싱 전략
  • 캐싱 레이어 도입 검토

데이터 검증 및 예외처리

안정적인 서비스 운영을 위한 개선사항:

  • 입력 데이터 유효성 검사 강화
  • PDF 파싱 예외상황 대응
  • 정형화된 데이터 스키마 검증 로직 추가
  • 에러 로깅 및 모니터링 시스템 구축

이번 글에서는 11월 15일부터 16일까지 무박 2일로 참여한 항해 AI 해커톤 회고 글을 작성해보았다. LLM을 활용한 실제 서비스를 구현해보면서, LLM의 활용 가능성에 대해 다시 한 번 감탄하지 않을 수 없었다. 그리고, 실제 사용자들이 사용하는 서비스를 만들기 위해서 많은 부분들을 개선할 수 있을 거라는 생각이 들었다.

외부 API 다루는 방법(feat. Rate Limit)

· 11 min read
eunsung shin
Software Engineer

TLDR

  • Muze라는 음악 SNS를 개발하고 있다.
  • 노래 정보 검색 기능이 필요한데, 데이터가 부족한 상황이라 검색 결과가 좋지 않다.
  • 필요한 데이터를 바로 검색 후 수집할 수 있도록 외부 API를 활용한다.
  • 이 외부 API에 문제가 생기면, 앱에도 문제가 생긴다. 그리고, API에는 Rate Limit이 걸려있어서 요청 수가 제한된다.
  • 외부 API의 의존성을 최소화하기 위해 fallback으로 사용할 API를 여러 개 두고, Token Bucket 알고리즘을 사용하여 요청을 분산시킨다.

서비스를 개발하면서 A-Z까지 전부 다 구현하기란 어렵다. 구현의 난이도 뿐만 아니라 효율면에서 부적절하다. 그래서 우리는 외부 API를 호출한다. 원하는 기능을 편리하게 사용이 가능하다. 한 가지 문제점이라면, 외부의 의존성이 생김으로써 내부의 문제가 아닌 외부의 문제로 서비스의 결함이 발생할 수도 있다는 점이다. 예를 들어, 쇼핑몰 서비스에서 외부 결제 시스템의 문제로 상품 결제가 이루어지지 않는다면, 실질적인 손해로 이어질 수 있다.

진행중인 사이드 프로젝트 Muze는 음악 취향을 공유하는 웹서비스이다. 자신이 좋아하는 노래를 게시하고 공유하는 기능이 메인 서비스다. 이 서비스를 위해서는 노래 정보들을 데이터베이스에 저장하고 검색하는 기능이 필요한데, 프로젝트의 규모상 '검색이 가능한 노래'의 범위를 정하기가 애매했다. 물론 적당히 구할 수 있는 노래들을 읽어와서 저장하는 방법도 있겠지만, 실제 사용자들이 사용하기에 매끄러운 서비스를 만들고 싶었다.

그래서 생각한 방법은 사용자가 검색 시에 음악 플랫폼(Spotify, Shazam 등)의 검색 API를 활용해서 결과를 리턴해준다. 음악 정보가 서버 내에 당장 없더라도, 정보를 반환해줄 수 있게 되는 것이다. 하지만 이런 처리는 외부 API에 대한 서버의 의존성이 생기게 된다. 우리 서버가 정상적으로 동작한다고 해도, Spotify의 서버에 문제가 생기면 고스란히 그 문제가 우리 서버의 문제로 돌아온다.

외부 API를 활용하는 것은 이미 결정된 사항이기 때문에 이 상황에서 내릴 수 있는 차선의 선택을 하는 것이 중요하다. Spotify 서버에 문제가 생긴다면, Shazam 서버를 활용하는 건 어떨까? 두 플랫폼 모두 문제가 생길 일은 극히 드물 것이다. 인풋 형식과 아웃풋 형식을 맞춰서 추상화된 RequestHandler 클래스를 작성한다.

from abc import ABC, abstractmethod

class RequestHandler(ABC):
def __init__(self, api_client):
self.api_client = api_client

@abstractmethod
def search(self, keyword, category):
return 노래정보

class SpotifyAPIRequestHandler(RequestHandler):
...

class ShazamAPIRequestHandler(RequestHandler):
...

class YoutubeAPIRequestHandler(RequestHandler):
...

노래 검색을 위해 활용할 API가 세 개나 생겼다. 노래 검색을 위해 활용할 API가 세 개나 생겼다. 그럼 이제 들어오는 요청들을 이 API들에게 어떻게 분배해줄지 설정해주어야한다. 요구사항은

  1. 결과가 나오지 않을 경우(response.status_code !=200), 다른 API로 요청을 넘긴다.
  2. 각각의 API의 Rate Limit을 초과하지 않는다.
  3. 사용자 경험을 고려하여 최대한 빠른 속도로 결과를 리턴해준다. 로 정리가 가능하다.

이 요구사항을 충족시키는 방법은 Token Bucket알고리즘을 활용하여 Rate Limiting을 적용하는 방법이다. TokenBucketAlgorithm.png

토큰 버킷 알고리즘은 토큰을 기록하여 사용량을 조절한다. 토큰은 시간에 따라 일정량씩 채워지고, 등록된 함수의 호출 시점에 토큰을 사용한다. 만약 시간당 채워지는 토큰보다 더 많은 호출이 이뤄지면 잠시동안 함수 호출이 멈추는 원리이다.

TokenBucket 클래스와 이를 관리하는 APIManager 클래스를 만들어 토큰 버킷 알고리즘을 구현해준다. 각 API당 할당된 Rate Limit 정보를 기반으로 rate(초당 버킷에 채워지는 토큰 수)과 capacity(버킷이 담을 수 있는 토큰의 최대수)을 설정한다.

그리고, 서버 접속 사용자 수에 따라 프로세스가 추가된다는 점을 상기해야한다. 프로세스 간의 APIManager 인스턴스는 독립적인데, API 호출은 공유하기 때문에 이를 인스턴스 간 정보들을 공유할 수 있도록 설정해주어야한다. 인메모리의 Redis를 사용해서 rate, capacity, token 정보를 공유하도록 설정한다.

rate, capacity 설정을 위해서 API 문서에서 Rate Limit에 대한 정보들을 살펴보았다. Spotify같은 경우에는 Rate Limit을 30초동안 쌓인 요청수를 기준으로 잡는다는 이야기만 있고 최대 요청량은 적혀있지 않았다(내가 못 찾았을수도...). 그래서 concurrent.future 모듈을 활용하여 30초동안 보낼 요청 수를 조정하여 실험한 후 최대 요청량을 가늠하였다.

# num_worker=1,2,3,4,5, ...,9,10
[{'num_requests': 158, 'num_success': 158, 'num_failures': 0}, {'num_requests': 302, 'num_success': 301, 'num_failures': 1}, {'num_requests': 533, 'num_success': 307, 'num_failures': 226}, {'num_requests': 774, 'num_success': 311, 'num_failures': 463}, {'num_requests': 992, 'num_success': 342, 'num_failures': 650}, {'num_requests': 1208, 'num_success': 356, 'num_failures': 852}, {'num_requests': 1474, 'num_success': 414, 'num_failures': 1060}, {'num_requests': 1659, 'num_success': 390, 'num_failures': 1269}, {'num_requests': 1973, 'num_success': 405, 'num_failures': 1568}, {'num_requests': 2243, 'num_success': 402, 'num_failures': 1841}]

스포티파이 API는 30초동안 대략 300 - 400개 정도의 요청을 받아준다. 대체 API인 Shazam과 Youtube도 비슷한 수량의 요청을 받아준다고 할 때, 대략 우리 서버는 30초동안 1000개의 검색 요청을 처리할 수 있다는 이야기이다. 이 얘기는 Muze 앱 운영 중에 1001명의 유저가 검색을 한 번씩 하는 경우가 30초의 시간동안 한 번이라도 발생할 경우, 다운타임이 발생할 수도 있다는 이야기이다. (물론 300명의 유저가 생길지 안 생길지조차 모르는 상황이긴 하지만, 30초동안 검색 한 번은 너무 짜다. 보통 한 유저당 한번 이상의 검색을 한다.)

이런 문제는 검색이 호출될 때 백그라운드로 해당 노래 정보를 데이터베이스에 저장하는 식으로 구성한다면, 시간이 지남에 따라 이런 문제들은 해결될 수 있을 거라고 생각했다. 한 번 이상 검색된 정보는 데이터베이스에서 호출되는 방식이기 때문에, 시간이 지날수록 외부 API에 대한 의존도가 낮아질 것이라고 기대했다. 여기에다가 추가적으로 백그라운드 스케쥴러로 최신곡에 대한 크롤링을 주기적으로 수행한다면, 외부 검색 API의 의존성을 최소화하고, 괜찮은 사용자 경험을 제공해줄 수 있게 된다.

이번 글에서는 Rate Limit이 설정되어있는 API를 다루기 위해 고려해야할 부분들과 안정적으로 처리할 수 있는 Rate Limit 기법에 대해서 알아보았다. 검색 기능은 Muze에서 꽤 중요한 기능이다. 이런 중요한 기능을 외부 API에 의존해야한다는 점이 고민되긴 했지만, 부족한 데이터를 다 커버할 수 없는 초기 개발 상황에서 차선의 선택이 필요했다. 외부 API에 의존하되, 운영 시 생길 수 있는 의존성을 최소한으로 줄이고, 데이터가 쌓임에 따라 그 의존성을 아예 없앨 수 있다면, 꽤 괜찮은 시나리오라고 생각한다.

GIL이 뭐길래

· 12 min read
eunsung shin
Software Engineer

Introduction

지난 10월 7일, 파이썬 3.13이 릴리즈되었습니다. 변경사항 중 가장 주목할만한 내용은 GIL을 키고 끌 수 있도록 변경한 부분이였습니다. 정식으로 GIL이 완전 제거 결정을 내리기까지는(제거하지 않기로 결정할 수도 있음) 약 5년정도의 시간이 걸린다고 하지만, 멀티쓰레딩이 어려웠던 기존의 파이썬을 생각하면 엄청난 변화라고 할 수 있을 것 같습니다. 이번 글에서는 GIL이 무엇인지, 왜 등장했고 왜 제거하기로 한 건지에 대해 알아보겠습니다.

GIL이 무엇일까요?

GIL이 무엇인지 이해하기 위해서 우선 우리가 작성한 파이썬 코드가 어떤 식으로 돌아가는지 알아야합니다. 컴퓨터의 기본 동작 방식은 다음과 같습니다. 보조기억장치(하드 드라이브)에 저장된 코드를 읽어 메모리 공간(RAM)에 프로세스로써 올리고, CPU는 레지스터의 도움을 받아 RAM에 있는 명령어들을 처리합니다. RAM에 적재된 코드는 컴퓨터가 이해할 수 있는 언어로 번역(소스코드 ->바이트코드->기계어)되어야합니다. 언어마다 다르지만, 파이썬은 작성한 코드를 메모리에 올릴 때마다 인터프리터가 코드 한줄 한줄 이 번역을 수행해주는 인터프리터 언어입니다. 인터프리터는 사람이 작성한 코드를 CPU에서 수행할 수 있게 번역해주는 역할을 한다고 보면 됩니다. 그리고, GIL(Global Interpreter Lock)은 프로세스당 하나의 쓰레드만이 이 인터프리터의 제어권을 갖고 명령을 수행할 수 있게 하는 락(Mutex)입니다. 동일한 프로세스라면, 여러 쓰레드가 존재해도, 이 락을 소유하기 전까지는 명령을 수행할 수 없습니다. GIL은 멀티코어로 구성되어있는 최근 CPU들을(2024년 기준 인텔 i7은 최대 20개의 코어, 애플 M3는 최대 40코어 보유) 멀티 쓰레딩으로 활용할 수 있는 이점을 제약합니다.

GIL은 왜 등장한 것일까요?

그렇다면 왜 파이썬에 GIL이 등장한 걸까요? 정답은 다수의 쓰레드를 사용하면서 생길 수 있는 Race Condition을 예방하기 위해서입니다. 더 자세히는 파이썬의 메모리 관리 방법과 연관되어있습니다. 파이썬에서는 Garbage Collection에 (Heap영역에서 더이상 사용되지 않는 객체들을 삭제) Reference Counting을 사용합니다. 어떤 객체의 Reference Count가 0이상이라면, Garbage Collection 대상에서 제외하고, 0에 도달한다면, 더 이상 사용하지 않는다고 판단하고 제거하는 식입니다. 그런데 이런 Reference Counting 방식은 같은 메모리 공간을 공유하는 쓰레드들끼리 같은 자원에 접근하는 Race Condition이 발생했을 때 문제가 생길 수 있습니다. Reference Counting에 대한 동시 접근으로 인해 삭제되어야할 객체가 삭제되지 않거나 유지되어야할 객체가 삭제되는 경우가 발생할 수 있습니다. 이런 Race Condition을 방지하기 위해 GIL이 탄생한 것입니다. (Java같은 경우, 특정 시점을 기준으로 garbage collection을 수행합니다.)

파이썬이 멀티코어를 활용하는 방법

GIL이 있다고 해서 파이썬이 멀티코어를 아예 활용하지 못 하는 건 아닙니다. 파이썬은 웹 프레임워크나 머신러닝 등 다양한 분야에서 활용되는 언어입니다. 파이썬이 멀티코어를 활용하는 방법은 크게 세 가지로 나뉠 수 있습니다.

  1. 멀티 프로세싱
  2. C나 C++로 작성한 코드
  3. CPython이 아닌 다른 인터프리터(PyPy, Jython과 같은) 사용 (추가로 GIL때문에 멀티쓰레드가 아예 의미가 없는 건 아닙니다. I/O작업시에 GIL은 자동으로 해제되기 때문에 I/O bounded 작업에서는 멀티 쓰레드가 동작한다고 볼 수 있습니다.)

파이썬 멀티코어 활용법1. 멀티프로세스

멀티프로세싱을 활용하면 멀티 코어를 온전히 사용할 수 있습니다. 그림으로 그리면 이런 식입니다. GIL에 의해 제약받지 않을 수 있도록 각 프로세스마다 별도의 인터프리터를 둡니다. 웹 게이트웨이 인터페이스인 gunicorn은 일반적으로 위에 보는 것과 같이 멀티 프로세싱 방식으로 동작합니다. 여기서 한 가지 알아야할 점은 프로세스는 개별적인 메모리 공간을 요구하기 때문에 멀티쓰레드에 비해서 메모리 공간을 더 차지합니다. 그리고 이 메모리 공간 때문에 메모리를 공유하는 쓰레드에 비해 컨텍스트 스위칭 비용이 발생합니다.

파이썬 멀티코어 활용법2. C,C++ 작성 코드

파이썬에서는 C나 C++로 작성된 코드를 수행할 수 있도록 C API를 제공합니다. 파이썬 인터프리터가 C 코드를 확인하면 제어권을 넘기는 방식입니다. 이렇게 되면 GIL 상관없이 C나 C++ 코드에서 사용하는 것처럼 멀티쓰레딩을 사용할 수 있습니다. 대표적으로 numpy나 pandas와 같은 데이터과학 모듈이 이 방법을 활용합니다. C 작성 코드의 빠름과 유연함을 활용할 수 있다는 점이 장점이지만, C, C++ 데이터 구조에서 Python 구조체로 변경하기 위해 어느 정도의 오버헤드는 발생할 것입니다.

파이썬 멀티코어 활용법3. CPython이 아닌 다른 인터프리터 사용하기

CPython이 표준 인터프리터이긴 하지만, PyPy나 Jython, IronPython 등 다른 인터프리터들도 존재합니다. GIL은 CPython 안에 존재하는 락으로, 만약 다른 인터프리터를 사용한다면 쓰레드 제약에 대해 더 이상 신경쓰지 않아도 됩니다. (물론, 다중 쓰레드 사용에 따른 공유 자원의 Race Condition을 직접 신경써줘야하겠죠) CPython이 아닌 다른 인터프리터를 직접 사용해본 적은 없지만, 필요하다면 인터프리터의 장단점을 확인하여 상황에 맞게 적절히 사용해볼 수 있을 것 같습니다.

3.13은 GIL 없이 어떻게 동작하는 걸까?

그럼 여기서 떠오르는 궁금증은 이미 잘 돌아가고 있는데, 왜 이제서야 바꾸는 걸까 입니다. 2007년에 파이썬의 창시자 귀도 반 로썸이 게시한 글에는 멀티 코어가 등장하면서, GIL을 제거하는 방안을 고민해보았지만, 딱히 성능적으로 이점이 없었다는 얘기가 있습니다. GIL을 제거하면 CPU위주의 태스크를 수행하는 멀티 쓰레딩 프로그램에서는 확실히 이점이 있겠지만, 기존의 싱글쓰레드 프로그램에서 성능적으로 손실이 있었기 때문에 정식으로 반영되지 않았다고 합니다. 실제로 GIL을 제거하려는 여러 프로젝트(대표적으로 Giletomy)들이 있었지만, 3.13까지는 정식적으로 반영될 정도로 효과적이진 않았던 것 같습니다. 하지만 GIL 없이 멀티 쓰레딩을 사용가능하다면, 확실히 개선할 수 있는 포인트들이 있을 것입니다. 쓰레드 간 메모리를 공유한다는 점을 활용하면 메모리 공간을 더 효율적으로 사용할 수 있고, 멀티프로세스 사용시 프로세스 간에 자원을 공유해야하는 부담도 덜 수 있습니다. 이 GIL 제거는 기존의 reference counting을 biased referencing, immortialization을 통해 가능해졌다고 합니다. 자세한 사항은 글이 길어져 생략하지만, 락을 걸지 않고, 어떻게 쓰레드 간의 Race Condition을 해결할지에 초점을 두고 키워드들을 살펴보시면 좋을 것 같습니다.

Outro

지금까지 GIL이 무엇인지, 왜 파이썬에 GIL이 생긴 건지, 그리고 파이썬에서는 이런 제약을 어떻게 해결해왔는지에 대하여 알아보았습니다. 기존의 파이썬 생태계에서 어느정도의 최적화가 이루어진 상황이지만, no gil 도입을 통해 어떤 식으로 더 개선될 수 있을지 기대됩니다.

DEVIEW 2023 "ML/AI 개발자를 위한 단계별 Python 최적화 가이드라인"을 보고

· 15 min read
eunsung shin
Software Engineer

영상 링크 : ML/AI 개발자를 위한 단계별 Python 최적화 가이드라인

유튜브 추천 동영상에 흥미로운 영상이 올라왔다.


"ML/AI 개발자를 위한 단계별 Python 최적화 가이드라인"

네이버 파파고 OCR팀 문주혁 님의 DEVIEW 2023 영상이다.

OCR, 이미지에서 텍스트를 탐지하는 단계에서, 텍스트 박스 갯수가 많아지면 속도가 느려지는 이슈를 어떻게 해결했는지에 대해 이야기한다. 전에 일했던 회사에서 똑같이 겪었던 이슈였기 때문에 집중해서 봤다. 영상을 보고 내 나름대로 문제 해결을 위해 발표자분이 어떤 식으로 접근했는지 정리해보았다. 기술적인 부분보다는 문제를 해결하기 위한 자세에 초점을 두고 글을 작성했다.

https://velog.velcdn.com/images/silvercity/post/5146f5e8-fe68-461a-ab63-8fd7025d6c64/image.png

  1. 문제 배경
  2. 문제 해결 방법
  3. 내가 시도해볼 것(Takeaway)

문제 배경


파이썬은 딥러닝 모델을 개발하기 위한 생태계가 잘 구성되어있다. GPU를 활용하여 딥러닝 모델을 학습/평가/서빙할 수 있는 pytorch, tensorflow 등 여러 프레임워크들과 고차원의 행렬 데이터들을 처리할 수 있는 pandas나 numpy와 같은 라이브러리들이 있다.

문제는 파이썬이 다른 언어들보다 느리다는 점이다. 모델 개발이 끝난 후, 딥러닝 모델의 앞단과 끝단에서 데이터를 처리하는 전처리나 후처리 또는 다른 부분들에서 속도가 느린 걸 발견할 수도 있다.

데이터 사이언티스트들은 보통 CS 백그라운드가 다른 개발자들보다 강한 편은 아니다. (물론 케바케겠지만) 내 첫번째 컴퓨터 언어는 파이썬이고, 비교적 최근에야 C나 C++ 같은 언어에 관심을 갖게 되었지만, 개인적으로 생각하기에 코드를 보고 '이런 부분을 고치면 조금 더 속도가 개선되겠는걸? 아 여기서 병목현상이 발생하는구나' 같은 컴퓨터 내부 동작 원리를 건드리는(또는 컴퓨터 내부 동작 원리를 알아야만 가능하다고 생각한) 생각은 많이 해보지 못한 것 같다.

그래도 완성된 딥러닝 알고리즘이 작동하는 방식을 제일 잘 이해하는 개발자는 모델을 담당한 데이터 사이언티스트 본인일 것이고, 속도가 개선되어야만 한다면 그건 데이터 사이언티스트의 임무일 것이다.

영상에서는 요새 핫한 모델 최적화 기법, 상당히 높은 지식 수준을 요구하는 고난이도 기술 대신, 우리가 시도해볼 수 있는 것 그리고 실질적인 결과물을 낼 수 있는 방법에 대해서 이야기한다.


문제 해결방법


측정 => {문제 개선방법 시도 => 측정 => 결과 분석} x repeat => 문제 개선

측정

문제를 해결하기 위해서는 우선 문제가 있다는 사실부터 인정해야한다는 말이 있다.

'속도가 느리다'라는 이슈를 해결하기 위해, 우선 속도 프로파일링을 진행한다. 문제 해결을 위한 가장 첫번째 단계는 문제 개선 여부를 측정할 수 있는 지표 설정이다.

파이썬에서 제공하는 timeit함수도 있지만, 코드 곳곳에 일일히 입력해야한다는 단점이 있다. timeit 대신 line_profiler라는 라이브러리를 사용했다고 한다. 전체 코드에 대해서 줄줄이 얼마만큼의 시간이 걸렸고, 몇 퍼센트의 비중을 차지하는지 알 수 있다고 한다.

문제 개선방법 시도

해결해야하는 문제가 '속도가 느리다'에서 '우리 코드 중 어느 부분이 특히 느리다'로 문제의 범위가 좁아졌다. 이제는 문제를 개선할 수 있는 방법들을 나열한다. 무작정 이 방법, 저 방법 해보자 보다는 여러 측면/레벨에서 시도해볼 수 있는 것들을 체계화하는 것이 중요하다. 영상에서는 속도 개선을 위해 python level, semi-c level, c/c++ level 세 가지로 나누어 접근했다.

https://velog.velcdn.com/images/silvercity/post/51584687-ccf1-401b-80a3-e973c2ac4d33/image.png

문제 개선방법 - python Level

문제 해결 방법들을 나열했다면, 가장 시도해보기 쉬운 것부터 시도해본다. 실패했을 때 비용이 적고, 가볍게 직접 시도해보면서 해결하고자 하는 문제가 조금 더 구체화된다는 장점이 있다. 문제 정의 시에 알지 못했던 부분에 대해 새롭게 알게 된다거나, 새로운 해결방법들을 추가해볼 수도 있다.

numpy나 opencv는 c와 c++로 작성된 라이브러리로 파이썬에서 작동할 수 있는 최적 속도를 어느정도 보장받는다. 그렇다고 해서 해당 라이브러리로 작성된 코드가 작성할 수 있는 최적의 속도라는 의미는 아니다. 개선할 수 있는 부분이 있을지도 모른다.

다시 한번, 문제 해결 방법은 여러가지가 있겠지만, 가장 시도해보기 쉬운 것부터 시도하는 것이 중요하다. numpy나 opencv에서 개선할 수 있는 부분을 찾으라고 해서, 안에 구동되는 동작원리를 파악하고, c++ 코드를 재작성하라는 의미가 아니다. 가장 쉽게 시도해볼 수 있는 방법은 '같은 라이브러리 내에서 코드 문맥에 조금 더 적합한 함수가 있을 수 있다. 더 알맞은 함수로 변경해보자'이다.

문제 개선방법 - Semi-c Level

파이썬 속도 개선을 위해 검색을 하다보면 항상 나오는 키워드가 있다. cython이나 numba. 결론만 말하자면, "pure python, 즉 라이브러리를 사용하지 않은 파이썬 자체 코드일 경우 눈에 띄는 성능 개선을 확인할 수 있지만, 다른 언어로 작성되어 이미 빠른 라이브러리를 사용할 경우, 그다지 좋은 성능 개선은 기대하기 힘들다"고 한다. 여기서 내가 생각하기에 중요한 부분은 '좋다고 해서 써봤는데?"라는 생각으로 시도한 방법에 대한 대응 방식이다.

https://velog.velcdn.com/images/silvercity/post/ee84945e-32af-4c3e-b1ae-e3230ec5e4ba/image.png

시도한 방법은 생각보다 결과가 좋을 수도 있고, 좋지 않을 수도 있다. 하지만, 결과의 퀄리티를 떠나, 결과를 분석하는 단계는 항상 있어야한다. cython이나 numba를 시도하는 방법에 있어서, '파이썬 자체 코드 => 성능 개선 우수, 라이브러리 코드 => 성능 개선 미미'라는 결론에 다다르기까지는 여러번의 실험과 분석이 있었을 것이다(추측이긴 하지만). 한 번에 이런 결론에 다다르면 정말 좋겠지만, 배경지식이 없는 상태에서 실험 후에 명료한 결론을 내리는 것은 생각보다 쉬운 일이 아니라고 생각한다. 여러번 걸릴 수도 있겠지만, 각 시도마다 결과를 분석하고, 나름의 결론과 경험치를 쌓아서 실용적인 결론에 다다르는 것이 중요하다.

('시도해봤는데 결과가 잘 안나왔다. 이 방법은 구리다' 같은 결론이 아니라, 영상에서처럼 '이 방법은 이럴 때는 잘 작동하지만, 저럴 때는 잘 작동하지 않을 수도 있으니 참고하세요.' 같은 실용적인 결론이다.)


문제 개선방법 - C/C++ Level

https://velog.velcdn.com/images/silvercity/post/89a1d1bb-c2e1-4118-ba69-20834eb4f066/image.png

https://velog.velcdn.com/images/silvercity/post/ef040d8e-7c41-40f9-a410-d42e1c5bf283/image.png

만약 C/C++ implementation을 적용하기로 결정했다면, '시도해보기 쉬운 것부터 시도' 원칙이 다시 한번 적용된다. 전체 코드를 바꾸는 대신, 시간이 오래 걸리는 병목 함수를 찾고 해당 부분만 C/C++ 구현을 적용한다. 여기서 내가 생각하기에 중요한 부분은 두 가지였다.

첫째는 새로운 코드 적용이 항상 공식 문서에 제공된 example과 같이 딱 들어맞지는 않을 것이기 때문에, 내 상황에 맞게 활용하는 유연성이 필요하다는 점이다. 영상에서는 numpy의 ndarray를 C에서 지원하는 cv::Mat 형식으로 변환하기 위해 Numpy C api에서 제공하는 PyArray_FromAny와 c++에서 제공하는 PyArrayObject를 거친다. '어? 내가 원하는 기능을 지원 안 하네?'하고 바로 포기해버리기보다는 조금 돌아가더라도 어떻게 해결할 수 있을지 고민해볼 필요가 있다.

두번째는 직접 해봐야 안다는 점이다. 위에서 언급했다시피, numpy와 opencv 같은 라이브러리들은 C/C++로 구현되어 있기 때문에 어느정도 최적의 속도를 보장한다. 그렇다면, 해당 라이브러리로 작성한 코드 속도가 무조건 C/C++로 구현한 속도와 동일할까? 아니다. 영상에서는 라이브러리로 작성한 파이썬 코드를 동일한 로직의 C/C++로 변환했을 때, 50%에 가까운 속도 향상을 얻었다고 한다. 개발을 하다보면 이론과는 다른 결과가 발생하기도 한다. 이론을 맹신하지말고, 직접 시도해보는 과정이 필요하다.

(문제 해결을 위해 돌아서 풀어가는 과정(workaround)에 길을 잃지 않기 위해서는 기초지식과 다루고 있는 문제의 본질에 대한 이해가 필요하다. 또, 이론이 전부가 아니다. 직접 시도해보는 과정이 필요하다.)


내가 시도해볼 것들(Takeaway)

영상을 보고 배운 점을 정리하자면 다음과 같다.

  • 최적화를 위해 컴퓨터 내부 동작 원리를 알아야만 가능한 것은 아니다.
  • 문제 해결은 측정 -> 해결 방법 시도 -> 측정 -> 결과 분석 의 반복으로 이루어진다.
  • 문제 개선 여부를 측정할 수 있는 지표 설정하기
  • 문제 범위를 좁히기
  • 여러 측면/레벨에서 시도해볼 수 있는 것들을 체계화하기
  • 각 시도마다 결과를 분석하고, 나름의 결론과 경험치를 쌓아서 실용적인 결론 내리기
  • 새로운 방법을 내 상황에 맞게 활용하는 유연성 기르기
    • 기초지식 갈고닦기
    • 해결하고자하는 문제를 이해하기
  • 이론을 맹신하지 말고, 직접 시도해보기