이번 장에서는 캐시에 대해서 기술하며 책의 5번째 여정입니다. 다시 복기하자면 컴파일러 -> 프로세스/스레드 -> 메모리 -> CPU에 이은 장이며, 컴퓨터가 캐싱을 하기 위한 방법과 문제에 대해서 서술합니다.
1. 캐시, 어디에나 존재하는 것
CPU는 메모리에서 정보를 가져와 업무를 수행하며, 이를 더 효율적으로 수행하기 위해서 캐시를 발명
도서관으로 비유하면 캐시는 책상 메모리는 서고
캐시는 L1,L2,L3로 나눠어져 있으며 점차 그 크기가 커짐
가상메모리의 경우 실제 메모리보다 더 크게 설정될 수 있는데 이 경우 하드디스크에 데이터를 저장
저장 형태의 계층 구조
맥북의 경우 활성 상태 보기에서 캐시에 대한 정보를 확인할 수 있음
하단 Cached Files
이전에 인텔 맥을 사용하면서 16기가 중에 거진 15기가를 점유하고 사용하고 있었다. 맥북을 새로 구입하면서 48기가로 구입했는데 오히려 사용량이 31기가로 늘었다(거의 비슷한 환경 구성임에도) 이로 미루어 볼때, 이전에 메모리가 부족해서 로컬 스토리지(SSD)에서 끌어다 쓰고 있다는 사실을 알게되었다.
궁금증1) 파이썬 데이터 처리 메소드 pd.read_csv는 메모리에 저장할까 캐시에 저장할까?
메모리에 저장한다. `info()`, `memory_usage()`로 확인 가능하다
궁금증2) 최근에 채점 서비스를 개발하면서 streamlit 프레임워크(파이썬 프론트엔드)에서 데이터를 저장하는 것을 캐싱으로 제안받았는데 이게 뭘까?
이번 글은 NVIDIA의 certificate 중 하나인 Prompt Engeering 자격증에 대한 취득 과정을 기술합니다. 또한, 학습과정에서 NotebookLM, GPTs, 노션AI를 활용해서 효과적으로 학습한 후기를 함께 남깁니다. 추후 다른 자격증을 공부하실 때 활용할 수 있는 좋은 레퍼런스가 될 수 있길 바랍니다.
인증 자격 대상이 된다면 다음 링크에 지원서를 작성합니다. Application 링크 저는 소속 기업이 있어서 스폰을 받아 지원했고, experience와 delivery plan을 집중적으로 적는게 권장됩니다. 내가 어떤 경험이 있고 이 자격을 획득하여 어떻게 사용할 것인지 기술하는 과정입니다. 정상적으로 접수되면 수 일 내 등록된 이메일로 연락이 오며, 보통 Monthly하게 학습 코호트가 시작되어 저는 바로 6월에 참여할 수 있었습니다. 해당 참여 코호트 안에 교육 이수와 최종 인터뷰까지 하는 것이 필수적으로 요구됩니다.
Student Assesment: 자체적으로 운영하는 Jupyter notebook 플랫폼에서 실습를 하는 과정입니다. 2주간 소요됩니다.
Review Instructor video: (선택사항) Assesment를 이수하면 제공되는 해설 강의입니다.
Certification Interview: 최종 통과를 위한 화상 면접(영어)이 진행됩니다.
2. 이수 진행
2.1. Student Assement 개요
Student Assessment가 가장 투자 시간을 많이 하는 부분입니다. 기본적인 프롬프트 엔지니어링 기술과 함께 LCEL, Pydantic, Gradio 와 같은 프레임 워크를 알려줍니다. 하지만 정작 평가는 간단히 자연어로 프롬프팅하면 통과가 되는 쉬운 문제로 구성되어 있습니다.
교안 자체는 Jupyter notebook으로 코드가 작성되어 실행만하면 대부분 원하는 결과를 얻을 수 있지만 이 학습 방식이 생각보다 굉장히 지루했습니다. 사실 영어라서 눈에 잘 안들어오기도 하고, 이미 작성된 코드 + 추상화된 Langchain 코드의 특성상 안에 들어가는 원리를 파악하는데는 큰 도움이 되지 않았습니다. 서버에서 이미 패키징된 NVIDIA api를 사용해서 실습하는 방식인데, 저는 로컬에서 직접하고 싶어서 Container에 가입하고 했는데, 세상에 NVIDIA에 한국 verification이 없습니다 :< 통신사 이슈인 것 같은데, 그래서 그냥 gemini로 변경해서 진행했습니다.
API key를 얻기 위해서 Verification 필요
그 과정에서 Langchain의 공식 Docs는 물론 Teddynote님이 작성하신 랭체인노트도 참고를 했습니다. 프롬프트 엔지니어링부분만 본다면 사실 많은 클래스와 메소드를 사용하지는 않지만, 워낙 Langchain이 클래스와 그 클래스를 상속한 다른 클래스가 많아서 가독성이 좀 좋진 않았습니다.
2.2. 학습 방법: NotebookLM
추가적으로 이번에 NotebookLM을 사용해봤는데, 학습에 필요한 링크나 자료(pdf)등을 업로드 하면 RAG를 자체적으로 구축하여 질의응답을 할 수 있는 시스템입니다. 다 넣어놓고 핵심 부분을 뽑아달라고 하거나 아니면 개인적으로 궁금한 것들을 대화형으로 공부하니까 훨씬 집중이 잘되었습니다. 개인적으로 진짜 학습하기 좋은 환경인 것 같네요.
자료가 올라간 NotebookLM
3. 면접
3.1. 영어 면접 준비 - GPTs
Assessment는 충분한 시간을 가지고 문제를 풀 수 있지만 문제는 인터뷰 입니다. 교육에 관련하여 일정이나 Timeline을 알려주는 DLI 담당자가 미국에 있어서 새벽에 볼 줄 알았는데 다행이 대만에 Principle Instructor가 있어서 주간 시간 내에 볼 수 있었습니다. 프롬프트 엔지니어링과 그 기반의 이론들은 한국어로 설명하려면 어렵진 않지만 영어로 표현이 잘 안되어서 GPTs에 자연어로 빌드해놓고 공부했습니다. 확실히 다른 LLM과 달리 ChatGPT에서 지원하는 GPTs 만큼은 음성으로 대화도 편리하게 잘되고 LLM에 대한 프로토 타이핑 관점에서 유용하게 쓸만한 것 같아서 여전히 ChatGPT가 매력적인 포지션을 가지고 있는 것 같습니다. 또 면접 시작 전 30분 정도는 ChatGPT 어플과 영어로 대화하면서 입을 풀었습니다.
3.2. 면접 과정
프롬프트 엔지니어링의 수행 방법은 간단하고 쉽지만 그 기반에 대한 질문들이 많이 나왔습니다. 처음부터 토크나이징, 벡터화, one-hot encoding 의 정의와 차이점을 기술하라 등 부터 시작하여, Runnable, Tempature와 같은 Langchain와 LLM에 관련된 핵심 개념들을 질의응답 했습니다. 생각보다 Langchain의 핵심 메소드나 클래스에 대한 질문은 덜 했고 이론을 위주로 티키타카하는 시간이였습니다. 30분간 진행해서 처음에는 집중력이 좋았지만, 후에 갈수록 질문을 잘 못듣고 놓치는 상황이 발생해서 조금 아쉬웠습니다.
3.3. 면접 후 정리: 노션 AI
요즘 Notion에서 노트AI라고 회의 내용을 저장하고 요약해주는 기능을 제공합니다. 면접과정에 이걸 켜놓고 복기하기 위한 수단으로 사용했습니다. 처음 당시는 무료로 시간제한도 없고 실시간 streaming하게 적어나가는 모습이 보였는데, 서버 부하 때문인지 녹음 시간이 제한이 되기 시작했습니다. 그래도 접근성도 좋고 편의성도 좋은 툴이라 복기할 때 잘 썼습니다.
4. 후기
미루고 미루던 Langchain을 접할 기회가 있어서 좋았습니다. 일과 병행하느라 처음에는 적잖이 집중을 못했는데 다양한 AI툴과 나의 학습방법을 갖춰나가니 조금 더 정리하는 시간이 되어서 매우 좋았습니다. 관련하여 더 괜찮은 자격증이 있으면 추후 학습 기회로 삼아볼 생각입니다. 프롬프트 엔지니어링에 대한 공부 내용과 레퍼런스는 다음 글에 작성해놓았으니 참고하시길 바랍니다!
이번 글에서는 NVIDA DLI Course에서 수행한 과정에서 LLMs 특성과 그를 보완하기 위한 방법에 대해서 작성합니다. 프롬프트엔지니어링이 무엇인지 그리고 Langchian으로 어떤 클래스와 메소드를 이용해서 구현하는지 알아보겠습니다.
1. LLM이 무엇인가?
LLM은 2017년 Google 발표한 Transformer를 기반으로하는 딥러닝 아키텍처. 핵심은 문맥을 이해하기 위해 단어 간의 관계를 계산하여 토크나이징과 벡터화를 이용하여 정보를 추상화
딥러닝은 행렬과 가중치로 데이터를 저장. 음악 믹서로 비교하자면, 보컬,드럼,베이스에 대한 소리를 조절하는 슬라이더가 있고, 그 슬라이더의 위치(가중치)를 바꾸면서 음악을 만들어내는 것과 같음. 수십개의 슬라이더의 조합이 행렬이라고 할 수 있음
이 가중치는 사전 학습(Pretrain)때 정해지므로 내부 규칙이자 고정된 노하우. 내가 프롬프트를 한다고 해서 이 고정된 규칙이 변하는게 아니라 context window가 계속 추가하고 변화되기 때문에 결과값이 변하는 것처럼 느껴지는 것
LLM의 특징
장점: 강력한 기능과 광범위한 활용성
단점: 할루시네이션에 대한 문제
2. LLM의 단점의 보완 대책
프롬프트 엔지니어링: 원하는 응답을 얻기 위해 효과적으로 소통하는 방식
Chain of Thought: LLM이 복잡한 문제를 중간 단계로 분해하여 생각하도록 유도하는 복잡한 추론 능력을 향상시킴. 명시적인 예시 / 단계별로 생각을 유도 / 작업 과정을 보여주게 하여 보완
검색 증강 생성(RAG): LLM의 지식 한계를 보완하기 위해 외부 지식과 LLM을 결합하는 기술, 문맥적으로 관련성 높은 응답을 제공. 이 때 코사인 유사도를 통해 의미 유사도를 벡터 검색함
가드레일: 신뢰있는 LLM을 구축하기 위한 제약조건과 규칙. 부적절한/유해한 응답을 방지
3. What is Langchain?
LangChain은 이러한 기반 어플리케이션 개발을 간소화하고 유연하게 만드는오픈소스 프레임워크. 상당한 추상화를 제공하여 초보자도 쉽게 접근할 수 있는 장점이 있으나, 지나친 추상화로 인한 디버깅이 힘든 단점이 존재하기도 함
핵심 개념
Runnable: LLM의 인스턴스로, 기본 작업 단위. RunnableLambda를 이용해 사용자정의함수를 Runnable로 변환 가능
LCEL: LangChainExpressionLanguage Runnable을 체인으로 구성하는 선언적인 방ㅇ식 제공
표준화된 메소드: invoke, batch, stream 표준 호출 메소드를 제공합니다.
프롬프트 엔지니어링 기법 지원
역할 기반 메시지: ChatPromptTemplate.from_messages 메소드를 사용하여 Human, AI, System 와 같은 역할 기반 메시지를 명시적으로 정의
퓨삿 프롬프팅: FewShotChatMessagePromptTemplate 모델 동작에 영향에 미치는 예시를 사전에 제공할 수 있음
프롬프트 엔지니어링 관련 Lanchain 디렉토리
#langchain_core
runnables.base # Runnable
├── Runnable : 기본 단위
├── RunnableLambda: 함수를 Runnable로 변환
├── RunnableParallel:
│
langchain_core.prompts.chat
├── ChatPromptTemplate
│ ├── from_messages(): reate a chat prompt template from a variety of message formats.
│ # 튜플 형식, "SystemMessage" 등의 키워드 사용 가능
├── few_shot.FewShotPromptTemplate: fewshot 제공
│
output_parsers.string
├──StrOutputParser: llm이 반환하는 결과를 문자형으로 변환
#llm api class
langchain_google_genai
├──ChatGoogleGenerativeAI
│ ├──invoke()
│ ├──stream()
│ ├──batch()
4. 용어집(Glossary)
대분류
소분류
설명
Prompt Engineering
Prompt Engineering
LLM의 가진 본질적인 특성 할루시네이션을 제어하고 원하는 결과를 도출하기 위한 방방법
Prompting
LLM에게 작업을 지시하는 행위 그 자체
Prompt Template
프롬프트의 구조를 미리 만들어 놓고 변수를 주입항여 재사용성을 높이는 도구
System Messages and System Context
모델의 역할, 성격, 행동 지침을 설정하는 시스템 레벨의 지시어
Chain-of-Thought
모델이 복잡한 문제를 풀 때, 생각 과정을 단계별로 서술하게 항여 정확도를 높이는 프롬프트 기법
Few-shot Classification
몇 가지 예시(few-shot)을 프롬프트에 포함하여 모델이 특정 패턴이나 분류 기준을 학습하게 하는 기법
Prompt Injection
악의적인 사용자가 프롬프트를 조작하여 개발자가 의도하지 않은 동작을 유도하는 보안 공격
개발 프레임워크 및 도구
LangChain
LLM을 외부 데이터 및 다른 서비스와 쉽게 연동하여 어플리케이션을 만들도록 돕는 개발 프레임워크
LCEL
Langchain의 여러 컴포넌트(Runnable)을 파이프라인으로 직관적으로 연결할 수 있게 하는 선언형 문법
Runnables
LCEL을 구성하는 기본 단위
Gradio and Chatbots
머신러닝 모델의 UI를 빠르게 만들어 프로토 타이핑하고 공유할 수 있는 도구
Pydantic
Python에서 데이터 유효성검사와 구조화를 쉽게 해주는 라이브러리
고급 어플리케이션 패턴
Agents
LLM과 RAG를 포함한 다양한 기술(플러그인, API)을 활용하여 사용자의 질문을 처리하고 실제 행동을 수행할 수 있는 인공지능 시스템
Retrieval Augmented Generation (RAG)
LLM의 추상화된 벡터에 의존하지 않고 외부 데이터베이스에서 정보를 검색하여 답변을 생성(Generation)하는 기술
Structured Data Generation
LLM이 단순 텍스트가 아닌 JSON/XML 등 정해진 형식의 구조화된 데이터를 생성하도록 하는 작업
LLM 모델 및 동작 제어
SOTA LLM models (ChatGPT, LlamaX, etc.)
현재 최고 성능을 보이는 모델들
Temperature
모델 답변 생성시 무작위성을 조절하는 파라미터, 높을수록 창의적
Instruction fine tuning
특정 작업이나 스타일에 맞게 응답하도록 사전 학습된 LLM을 명령어 데이터셋으로 학습시키는 과정. zero-shot에도 잘 작동할 수 있게됨
Hallucinations
다음 단어를 예측하는 LLM의 특징 때문에 일어나는 현상이며, 실제로 사실이 아닌데 사실 인 것처럼 말하는 현상
데이터 처리 및 인프라
JSON
JavaScript Object Notion.key-value 쌍으로 이루어진 데이터 교환 포맷.
Streaming vs. Batching
스트리밍은 결과가 생성되는 즉시 실시간으로 전송하는 방식, 배치는 한 번에 처리하는 방식
Nvidia NIM
NVIDIA Inference Microservice, 쿠버네틱스 기반 마이크로서비스 세트
자체적으로 채점자동화를 위한 웹 서비스를 만든 후기를 작성해봅니다. 소규모 인원을 대상으로 GCP 서비스를 조합해서 편하게 채점할 수 있도록 만들었고 그 과정에서 개발, 배포, 서비스 운영에 대한 전반적인 내용을 다룹니다. 그 과정에서 일어난 시행 착오를 공유하여 독자들이 서버리스 자동화를 설계할 때 사전에 고려해야할 점 등을 예습하는 글이 되길 바랍니다.
1. 채점 서비스의 필요성
1.1. 기존 채점 프로세스의 문제점
문제1) 과제 제출 -> 피드백 까지의 시간 지연
기존에는 학생들이 과제를 제출하면 이를 받아 채점 튜터들이 채점하고 리뷰를 하는 구조로 진행 되어있습니다.
학생들의 답안 제출 -> 교육 시스템에 적재 -> 튜터에게 할당(10명 등) -> 각자의 로컬 컴퓨터에서 코드를 실행 & 리뷰를 작성
답안 제출로부터 학생들이 피드백을 받을 때까지 최소 1주 - 최대 2주의 시간이 소요되었습니다. 문제를 푸는데 이렇게 시간이 오래 걸린다면 코드를 작성할때의 기억들이 휘발되어 학습효과가 떨어집니다. 따라서 위 프로세스 중에 채점하는 시간을 줄이고자 했습니다.
문제2) 환경 의존성 문제
데이터 분석을 포함한 프로그래밍은 환경 설정에 따라서 그 결과값의 차이가 있을 수 있습니다. 예컨데 SQL로 데이터를 불러온다고 해도 각자 데이터를 불러오는 DDL/DML이 다르거나 환경이 다르면 다른 결과물을 산출할 수 있습니다. 과제를 풀 때나, 채점을 할 때나 각자의 로컬 환경에 MySQL-DBeaver로 설치를 하는 환경이 문제였습니다.
따라서, 위 문제를 해결하기 위해서 하나의 서비스를 만들게 되었습니다. 제출된 코드에 대한 결과는 채점 자동화는 물론, 코드에 대한 정성적 피드백도 Gemini로 받았으면 좋겠다고 생각했습니다.
2. 채점 시스템 설계
2.1. 기존 프로세스 개선
사실 서비스를 시작하기에 앞서 이 서비스를 전체적으로 알리고 프로세스를 개선하는 필요성이 있다고 생각 했습니다. 따라서 과제를 제출하는 과정부터 프로세스를 개선하기 위해서 교육업체 담당자에게 설명하기 시작했습니다.
보통 과제가 나가는 과정은 튜터의 과제 제작 -> 학생들이 풀이 -> 튜터들의 과제 채점 -> 결과 송부 의 형식으로 진행됩니다. 일관된 채점을 위해서는 먼저 과제 제작에 대한 설계도 필요했습니다. 그래서 과제를 제출할 시 몇가지 기준을 세웠습니다.
과제에 대한 의도, 목적 등을 작성할 것
과제에 채점 기준을 명시할 것
모범 답안으로 해설지를 작성할 것(정답 코드 포함)
이렇게 되면 일관된 과제 제출도 가능해지고 추후 채점을 Gemini에 요청할 때 Prompt 의 기반이 될 것이므로 이 형식을 같이 잡아나갔습니다.
2.2. 채점 서비스 설계
위 업무 프로세스 개선과 동시에 서비스 설계를 위한 서비스와 스택을 정리해나갔습니다. 가장 중점적인 것은 GCP의 on-demand 서비스인 Cloud run으로 서버리스 구조를 선택하게 되었습니다. 서버를 관리하고 올리기엔 관리포인트가 클 것이고 간단하게 만들 수 있을 것 같아 선택했습니다.(하지만 이 선택을 후회하게됩니다) 웹사이트는 Streamlit으로 정했고, . 또한 과제는 크게 SQL와 Python으로 나뉘는데 SQL의 경우 실제 MySQL 8.0 버전이 필요해서 GCP의 Cloud SQL을 사용하였습니다. 파이썬의 경우 내장함수으로 exec 를 이용해서 채점 기능을 구현했습니다.
과제는 확장될 수 있지만 기본적으로 SQL, Python, Pandas에 대한 문제를 기본으로 결과를 만들어 갔습니다. 크게 기능은 모범 정답과 학생 정답을 채점하는 기능 1번과 정성 평가를 하는 기능2가 있습니다.
기능 1) Gemini를 이용한 정성평가( 코드의 효율성과 개선점을)을 함께 평가
사전에 선정해놓은 기준으로 Prompt engineering에 따라 점수를 측정
기능 2) 학생들의 답과 모범 정답 비교
SQL) Cloud상의 SQL 서비스에 직접 쿼리를 날려 가져와 출력
Python) exec 내장함수를 이용해서 테스트 케이스마다의 출력
Pandas) 코드저장소의 csv파일을 불러와 데이터 가공한 결과를 출력
3. 핵심 기능 구현 상세
기능1) 정성 평가
구현 내용
정성 평가는 몇가지 기준에 따라 채점됩니다. 2.1. 기존 프로세스 변경 제안에서 말한 것처럼 과제에 대한 의도, 채점 기준, 모범 답안 등의 대한 정보와 사전 프롬프트를 받아 결과를 뱉어냅니다. 다음은 `prompt_builder.py` 의 결과물이며 Langchain의 from_template 메소드처럼 사전에 변경 가능한 구조로 설계하여 확장정을 높였습니다.
당신은 {category} 과제 채점 전문가입니다.
- 문제: {question}
- 모범답안: {model_answer}
- 학생답안: {student_answer}
- 평가기준: {evaluation_criteria}
- 쿼리 상태: {query_status}
{grading_criteria_str}
학생의 답안이 모범답안과 다르더라도, 논리적/문법적 오류가 없다면 높은 점수를 주세요.
아래 조건을 꼭 지켜서 평가해 주세요.
1. 각 평가기준(정확도, 가독성, 효율성, 분석력 등)에 대해 **5점 단위(0, 5, 10, ..., 100)로만** 부분 점수를 각각 매겨주세요. (예: 정확도 50점, 가독성 20점, 효율성 15점)
2. 각 기준별 점수의 합이 100점이 되도록 최종 점수도 함께 출력해 주세요.
3. 각 평가기준별로 '참고할 점'을 한두 문장으로 구체적으로 작성해 주세요. (예: 잘한 점, 부족한 점, 개선점 등)
4. 마지막 피드백은 평가자가 참고할 수 있도록, 학생에게 직접 전달하는 친근한 말투가 아니라 틀린 부분, 부족한 점, 개선점 등 객관적이고 구체적인 정보 위주로 작성해 주세요.
결과는 아래 형식으로 출력해 주세요.
[기준별 점수]
정확도: 45 / 50
가독성: 15 / 20
효율성: 10 / 15
분석력: 10 / 15
[기준별 참고사항]
정확도: WHERE 조건에서 'Attrited Customer' 필터가 누락됨.
가독성: 쿼리 구조는 명확함.
효율성: CTE 활용이 부족함.
[최종 점수] 80
[피드백] 전반적으로 쿼리 구조는 명확하나, 일부 필터 조건과 효율성에서 아쉬움
문제: 프롬프트에 대한 불확실성
결국 LLM에 응답한 결과는 매번 결과가 달라지는 문제가 있습니다. 특히 이따금 결과값이 json 형태로 뱉어내어서 기대한 형태의 출력이 안되는 경우도 많았는데, 추후에 Langhain 스택을 추가하여 StructuredOuputParser 사용하는게 좀 더 나을 것 같습니다.
기능2) 채점 기능 - SQL
구현 내용
SQL은 MySQL로 문제를 냈다면 해당 클라우드가 있어야합니다. 이를 위해 Cloud SQL에 인스턴스를 하나 파서 DDL을 이용하여 데이터를 삽입하였습니다. 문제는 공통된 DDL을 코드로 제시했음에도 불구하고 각자의 환경에서 알아서 데이터를 불러오다보니 미묘하게 결과 데이터가 다른 문제가 있었습니다. 예를 들어 정수/정수 나누기에 대한 결과값 계산 과정에서 decimal 사용 유무에 따라 결과 값이 달라서 결국 소수점 2째짜리 이내는 정답으로 처리하도록 임시 방편을 택했습니다.
문제: 비용 이슈
반면 cloud 스펙에 대한 이슈도 있었는데, 원본 데이터가 그렇게 크지 않지만 초기 스펙은 램 2기가에 2CPU를 기본값을 배정했는데 매일 5천원씩 너무 많이 과금이 되는 문제가 있었습니다. 샌드박스 옵션인데도 불구하고 과제에 적합한 사이즈는 아니 였으며, DB의 완정성을 보장하기 위한 백업이나 관리의 비용이 많이 나가는 것을 확인했습니다. 이를 확인하고 다운 그레이드 절차를 진행했습니다. 결과적으로 일 5000원 유지비 -> 850원 정도의 유지비로 절감할 수 있었습니다.
데이터 백업 기능 해제(운영 DB가 아니므로)
CPU 2코어 수 -> 1 햐향 조정
메모리 2GB -> 600 MB 하향 조정
기능2) 채점 기능 - Python / Pandas
구현 내용
파이썬의 경우내장 함수 exec를 통해 채점을 구현했습니다. 문제를 dictionary 형식으로 저장하여 추후 불러와 사용했습니다. python의 결과는 모범정답의 key값에 따라 다양한 체크포인트를 거쳐 최종 정답으로 처리됩니다. 예컨데 문제1번이 단순하게 평균을 내는 함수라면 model_answer의 코드를 exec 내장함수에 넣고 test_cases의 input값을 전달인자로 실행하여 expected와 같은지 확인하면 됩니다.
QUESTIONS = {
"PYTHON_1": {
"title": "숫자 리스트의 평균 계산하기",
"content":
"""
- **배경**: 한 소매점에서 재고를 계산해야 합니다. 주어진 재고의 평균을 계산해보세요.
- **문제 의도**
- 리스트의 자료형을 이해
- 내장 함수의 활용
- **요구 사항**
- 함수명: `calculate_stock`
- 해당 함수는 리스트의 전달 인자를 받음
""",
"model_answer":
"""
def calculate_stock(numbers):
return sum(numbers) / len(numbers)
""",
"function_name": "calculate_stock",
"test_cases": [{"input": [10, 20, 30, 40, 50], "expected": 30.0}],
"evaluation_criteria": [
{"id": "P1", "description": "리스트의 합과 길이를 이용해 평균을 계산한다."},
{"id": "P2", "description": "내장 함수(sum, len) 사용"}
]
}
Pandas의 경우 자료형과 결과값 두개를 동시에 비교해야 하기때문에 "test_cases"에 자료형을 저장하는 "expected_type"과 "expected"를 동시에 넣어 채점하게 됩니다. passed 변수를 True로 초기화 해놓고 각 기준을 통과하지 못하면 False으로 Flag를 세워 최종적으로 True로 살아남는 코드를 정답으로 처리하였습니다.
"PYTHON_7": {
"title": "결측치 확인",
"content":
"""
- **배경:** 데이터를 불러 왔을 때 각 컬럼에 결측치 유무를 확인하는 것은 중요합니다. 컬럼의 결측치를 확인해보세요.
- **문제 의도**
- DataFrame의 함수를 활용
- **요구 사항**
- 함수명: `get_missing`
- 컬럼별 결측치 수를 예시 결과와 같이 출력
""",
"model_answer":
"""
def get_missing(df):
return df.isnull().sum()
""",
"function_name": "get_missing",
"test_cases": [ {"input": "df_sample",
"expected_type": 'Series',
"expected": {
"Route": 1,
"Total_Stops": 1,
"Airline": 0, "Date_of_Journey": 0, "Source": 0,
"Destination": 0, "Dep_Time": 0, "Arrival_Time": 0,
"Duration": 0, "Additional_Info": 0, "Price": 0
}}
],
"evaluation_criteria": [{"id": "P1", "description": "DataFrame의 isnull().sum()을 활용한다."}]
}
또한, 데이터를 불러와야하는 문제가 있었는데, 매번 문제마다 데이터를 불러오면 in-ouput관점에서 비효율적일 것 같아서 찾아보니 stremlit에서 캐시 기능을 구현할 수 있는 데코레이터를 발견했습니다. 이를 사용해서 효율적인 저장을 구현했습니다.
@st.cache_data
def load_sample_dataframe():
"""
이 함수는 처음 호출될 때 단 한 번만 실행되고 캐싱됩니다.
이후 호출에서는 캐시된 데이터를 반환합니다.
"""
# print('데이터로드 시작')
df = pd.read_csv('./data/7th/flight_data.csv', sep=';')
# print(df.head(3))
# print('데이터로드 종료')
return df
이슈: 데이터 로딩 문제
문제는 Pandas에 있었는데, 데이터 분석에 관련하기 때문에 외부에서 데이터를 불러와서 채점하게 됩니다. uci 레포나 github 등일수도있고 mysql 서버일 수 도 있습니다. 하지만 cloud service은 외부 데이터를 가져옴에 있어서 인증을 요구하다보니 이걸로 3일 정도 삽질을 했습니다. 사실 별거아닌 문제이지만, cloud run 서비스 특성상 에러를 찾기가 매우 불편합니다. 로그를 뒤져보면 되긴하지만 내가 실제로 서버를 띄우는 것 처럼 bash형태로 로그를 찾는게 아니다 보니 데이터 로딩에서 문제가 일어난다는 사실 자체를 찾는데도 오래 걸렸습니다. 사실 데이터가 7MB 이였어서 큰 문제는 없었지만, 대량의 데이터를 불러온다면 Cloud SQL 에 적재해서 가져오는 방향이 나을 것 같습니다.
4. 개발 과정의 교훈과 팁
각 디렉토리를 정확하게 명시
― core: 핵심 모듈
┕ service: 외부 호출되는 서비스( Ex Cloud SQL 등)
┕ streamlit_app: 프론트 엔드 streamlit 을 위한 디렉토리
┕ answer: 정답 저장
┕ data : 데이터 저장
하나의 디렉토리는 하나의 기능 혹은 구현하려고자하는 결과물이 명확하게 정의되면 정리하기가 좋습니다. 혼자 일하던 여럿이 일하던 명확한 디렉토리 설계와 README.md 작성은 업무 시작 시 로딩 속도를 줄여줍니다.
생성된 모듈의 import 하는 방법
# module import 문제 없음. 같은 경로
/ main.py
/ module.py
# module import 문제 없음. 같은 경로
┖ core
┖ ─ main.py
┖ dir
┖ ─ __init__.py
┖ ─ module.py
위처럼 모듈을 기능에 따라 나누다보면 결국 타 디렉토리에 모듈을 가져올 상황이 생깁니다. 같은 경로면 import module.py 를 스크립트 상단에 올리면되지만 디렉토리가 서로 다른 경우 문제가 생깁니다. 이럴때 `__init__.py` 라는 초기화 모듈을 해당 디렉토리에 넣어주면 경로를 쉽게 인식할 수 있게 해줍니다.
Docker를 적극적으로 사용하자
streamlit 을 이용하면 localhost:8501 포트에 자동으로 실행되지만 여기서 재현성이 된다고 Cloud run에서 잘 작동하는 것 은 아닙니다. 기본적으로 로컬은 윈도우 환경이고 cloud 서비스는 linux 환경이기 때문입니다. 그래서 실제로 컨테이너를 띄워서 테스트해보는게 cloud run에 배포했을 때도 오류를 줄일 수 있는 좋은 테스트 환경입니다. Docker 짱장맨!
# base 디렉토리로 이동
cd base
# Docker 이미지 빌드
# 맥이라면 arm 64 아키텍쳐로 빌드하고 테스트하며, 배포 시에는 arm64로 변경필요
docker build --platform linux/amd64 -t {서비스명} .
# 빌드된 이미지로 Docker 컨테이너 실행하기
docker run -d -p 8501:8080 -m 1g --name {개발 이미지명} {서비스명}
[중요] build후 로컬 테스트에서 윈도우에서는 amd64, 맥에서는 arm64 아키텍쳐를 명시해야하나, cloud run 배포시는 amd64로 통일 필수(이것 때문에 단순배포 2시간동안 삽질함)
.env: 환경 변수에 대한 정보를 담습니다. 보통 api나 db의 접속 정보 등을 가지고 있으나 credential이기 때문에 `.env .example` 파일을 github에 올리고 환경변수 명을 제외한 정보를 제거하여 형태만 보존합니다.
# .env.example
GEMINI_API_KEY=
SLACK_BOT_TOKEN=
`.dockerignore`: 도커나이징 할때 굳이 필요 없는 파일을 저장합니다. `.gitignore`와 비슷한데 보통 python 가상환경에 대한 디렉토리나 cache 기타 파일들을 작성합니다. 이걸 명시하지 않으면 가상환경 디렉토리 내용이 통째로 docker화 되기 때문에 작업 효율이 늦어집니다.
# 파이썬 가상환경 폴더
.venv
# 파이썬 캐시 파일
__pycache__/
*.pyc
# Git 폴더
.git
# 기타 운영체제 파일
.DS_Store
배포 코드 .sh 화: 일련의 deploy를 위한 코드를 매번 치지말고 .sh 파일을 만들어서 실행시킵니다.
#!/bin/bash
# 스크립트 실행 중 오류 발생 시 즉시 중단
set -e
# 2. Docker 이미지 빌드 (linux/amd64 플랫폼)
echo "Building Docker image..."
docker build --platform linux/amd64 -t {서비스명} .
# 3. Artifact Registry에 맞게 이미지 태그 지정
echo "Tagging image for Artifact Registry..."
docker tag {서비스명} {이미지명}.pkg.dev/{프로젝트}/cloud-run-source-deploy/{서비스명}
# 4. Artifact Registry로 이미지 푸시
echo "Pushing image to Artifact Registry..."
docker push {이미지명}.pkg.dev/{프로젝트}/cloud-run-source-deploy/{서비스명}
# 5. Cloud Run에 배포 (us-central1)
echo "Deploying to Cloud Run..."
gcloud run deploy grading-app \
--image={이미지명}.pkg.dev/{프로젝트명}/cloud-run-source-deploy/{서비스명} \
--region=us-central1 \
--platform=managed \
--vpc-connector={vpc 커넥터} \
--vpc-egress=all \
--allow-unauthenticated \
--memory=2Gi \
--min-instances=1 \
--cpu=2
echo "Deployment complete! 🚀"
requirements
requirement는 가상환경을 쓴다면 자주 사용하는데 이게 나중에 docker로 배포할때 설치할 라이브러리가 되기 때문에 필수적입니다.
#가상환경 만들기
py -3.12 -m venv {가상환경명}
'''
라이브러리 설치 ...
'''
#패키지 저장
pip freeze > -r requirements.txt
#설치
pip install -r requirments.txt
mono repo전략
잠시 파이썬 채점자동화시 clodu run function을 고민했던 적이 있었습니다. 코드저장소 측면에서보면 하나의 리포지토리에 두 개이상의 배포 포인트가 있어야하는건데 이럴 땐 그냥 간단히 디렉토리를 나누면 됩니다.
5. 개발 후기
5.1. 그래서 cloud run 다음에도 쓸까?
상황에 따라 다를 것 같은데, 그래도 서버를 파는게 나을 것 같습니다. on-demand서비스라는건 비용 측면에서 좋지마 로그를 찾기가 너무 불편합니다. 데이터 로딩 과정에서 계속 에러가 났는데 확인하기 불가능한 경우가 많았습니다. 더욱이 로컬 Docker에서는 기능을 잘 하는 것이 배포하는 환경에서는 에러 재현이 안되니 정말 답답했습니다. 사실상 단일 스크립트로 구현해야하는 서비스의 경우에는 cloud run function을 사용하는게 나을 것 같고, 조금 복잡한 서비스에는 직접 GCE를 이용해서 구현하는게 디버깅 측면에서는 더 나을 것 같습니다.
여기서 에러를 찾아보시오..
5.2. 개선할 점
구조설계의 효율화
뒤돌아보니 개선할 몇가지가 보이긴 합니다. 일단 가장 큰 것은 함수의 파편화입니다. 일례로 gemini가 정성 평가하는 방식만 봐도 main -> local_grader -> generate_content -> prompt_builder 의 스택 구조가 필요 이상으로 만들어져 있다는 느낌이 들었습니다. 이러면 디버깅 관점에서도, 확장 측면에서도 파악이 어려워서, 다음에는 하나의 스크립트로 통합하고 prompt_builder 와 같은 지속적으로 변경하고 수정이 필요한 부분만 따로 분리하는 것이 좋을 것 같습니다. 또한, 함수형 스크립트보다는 문제의 기능을 통합하여 class 설계를 하는 것도 좋겠습니다.
비용 효율 관점
처음에는 비용을 크게 생각하지 않았는데, 매일 몇천원씩 과금되는 Cloud - SQL이 점차 비용이 많이 증대되는게 느껴졌습니다. 좀 더 저렴한 사설 cloud를 쓸 수 도 있겠지만, 관리포인트가 많아지고 난이도가 높아질 것 같아 최대한 GCP안에서 해결하려고 했습니다. 하지만 그 과정에서도 자원과 비용을 효율적으로 사용할 설계를 하는 것이 좋겠습니다. 항상 채점 서비스가 올라가야하는 것은 아니라 상시 올려져있는 cloud-sql와 같은 서비스도 on-off 자동화가 필요해보입니다.
채점 서비스 기능 추가
현재 채점 서비스는 따로 로깅이나 데이터 수집을 하고 있진 않습니다. Gemini를 단순히 사용하는 것에 넘어서 개선하는 결과를 만들고 싶은데 그러러면 로깅 시스템이 있어야해서 설계해볼 것 같습니다. 또한, 현재는 정량적인 결과를 채점하고 있는데, 추후 통계 그래프나 시각화 이미지 등을 채점하는 기능을 추가 개발할 여지가 있습니다. 그리고 현재는 Prompt Engineering이 직접 api를 call 하고 있는데 langchain을 도입하여 좀 더 구조적인 설계를 해볼 수 있습니다.
5.3. 결과
결과적으로 총 2번의 과제에 지원 서비스를 무사히 마칠 수 있었고, 하나의 서비스를 개발해보는 좋은 경험이 되었습니다. 정량적으로는 기존에 1명에 채점이 20분 정도 걸리는 시간에서 10분정도로 빠르게 채점할 수 있었고, 개별적으로 튜터들이 시스템을 구축하여 채점하는 사전 준비 시간도 없앨 수 있었습니다. 사실 본 서비스는 누가 시킨것도 아니고 스스로 편하게 일해보자고 시작했지만 그래도 잘 해보자 나에게 남을 것이라는 마음으로 시작했는데, 결과가 나름 만족스럽게 나와서 좋았습니다. 다음에도 이렇게 스스로 추진하고 결과를 만드는 프로덕트를 하나씩 만들고 남겨보도록 하겠습니다.
from dotenv import load_dotenv
import getpass
import os
load_dotenv(verbose = True)
# os.getenv("GOOGLE_API_KEY")
if "GOOGLE_API_KEY" not in os.environ:
os.environ["GOOGLE_API_KEY"] = getpass.getpass("Enter your Google AI API key: ")
NVIDA에서 모델 불러올 수 있는데 황당한건 모델 컨테이너 사이트에서 한국인증이 아직 안됩니다.. 어째서..?
2. 본문 불러오기
from langchain_google_genai import ChatGoogleGenerativeAI
llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash")
result = llm.invoke('What is OMSCS?')
print(result.content)
"""
OMSCS stands for **Online Master of Science in Computer Science** offered by the **Georgia Institute of Technology (Georgia Tech)**.
Here's a breakdown of what makes it significant:
* **Reputable Institution:** It's a fully online master's degree from a top-ranked computer science program. Georgia Tech is highly respected in the field.
* **Affordable:** Compared to traditional on-campus master's programs, OMSCS is significantly more affordable. This makes it accessible to a wider range of students.
* **Flexible:** The online format allows students to study at their own pace and on their own schedule, making it ideal for working professionals.
* **Rigorous Curriculum:** The program covers a wide range of computer science topics and maintains a high academic standard. It is not a "watered down" version of the on-campus program.
* **Diverse Specializations:** Students can choose to specialize in areas such as:
"""
3. 스트리밍과 배치
invoke와 대응되게 대답을 받는 방법이며, streaming은 동기적으로 batch는 비동기 형식으로 대답을 반환
# llm이 뱉어내는 결과를 스트리밍하게 밷어낼 수 있다.
# invoke는 동기식, stream은 비동기식으로 결과를 받아온다.
prompt = "What is OMSCS?"
for chunk in llm.stream(prompt):
print(chunk.content, end='')
#batch 식
prompts = [
'What is OMSCS?',
'What is the capital of France?',
'What is the largest mammal?'
]
results = llm.batch(prompts)
for result in results:
print(result.content)
4. 템플릿을 이용한 재사용
def translate_to_french(text):
prompt = f"Translate the following English text to French: '{text}'"
return llm.invoke(prompt).content
english_statsments = [
"Hello, how are you?",
"What is your name?",
"I love programming.",
"The weather is nice today."
]
prompts = [translate_to_french(statement) for statement in english_statsments]
prompts
"""
['Here are a few options for translating "Hello, how are you?" into French, with slightly different nuances:\n\n* **Bonjour, comment allez-vous ?** (Formal, polite. Use with people you don\'t know well, or in professional settings.)\n\n* **Salut, comment vas-tu ?** (Informal, friendly. Use with friends, family, and people you know well.)\n\n* **Bonjour, ça va ?** (Informal, but very common. A general greeting, like "Hi, how\'s it going?")\n\n* **Salut, ça va ?** (Even more informal, like "Hey, what\'s up?")\n\nThe best choice depends on the context and your relationship with the person you\'re speaking to.',
'The most common and polite translation of "What is your name?" in French is:\n\n**Comment vous appelez-vous ?**\n\nHere are a few other options, with slightly different connotations:\n\n* **Quel est votre nom ?** (More formal, literally "What is your name?")\n* **Tu t\'appelles comment ?** (Informal, used with people you know or are on familiar terms with)\n* **Comment tu t\'appelles ?** (Also informal, but slightly more direct than "Tu t\'appelles comment ?")\n\nThe best choice depends on the context and your relationship with the person you\'re asking. However, "Comment vous appelez-vous ?" is generally the safest and most appropriate option.',
'The most common and direct translation of "I love programming" in French is:\n\n**J\'adore la programmation.**\n\nHere are a few other options, with slightly different nuances:\n\n* **J\'aime la programmation.** (This is also very common and means "I like programming," but can also imply love in some contexts)\n\n* **J\'aime beaucoup la programmation.** (This emphasizes the liking, "I like programming a lot")\n\n* **Je suis passionné(e) par la programmation.** (This means "I am passionate about programming." Use "passionné" if you are male, and "passionnée" if you are female.)\n\n* **La programmation, c\'est ma passion.** (This means "Programming is my passion.")\n\nThe best choice depends on the specific context and the degree of enthusiasm you want to convey. However, **J\'adore la programmation** is generally the safest and most natural translation for "I love programming."',
'There are a few ways to translate "The weather is nice today" into French, depending on the nuance you want to convey:\n\n* **Il fait beau aujourd\'hui.** (This is the most common and straightforward translation. It means "The weather is good today.")\n\n* **Le temps est agréable aujourd\'hui.** (This is a slightly more formal way of saying it, and means "The weather is pleasant today.")\n\n* **On a du beau temps aujourd\'hui.** (This is a more colloquial way of saying it, and translates to "We have nice weather today.")\n\nSo, the best option depends on the context, but **Il fait beau aujourd\'hui** is generally the safest and most widely understood.']
"""
translations = llm.batch(prompts)
for translation in translations:
print('---')
print(translation.content)
"""
---
This is a great and accurate breakdown of the different ways to say "Hello, how are you?" in French! You've covered the key distinctions of formality and provided helpful context for each option.
Here are a few minor additions that could further enhance this explanation:
"""
#template 으로 프롬프트 엔지니어링 하기
from langchain_core.prompts import ChatPromptTemplate
#사전에 템플릿을 만들어 runnable을 만드는 방식
english_to_spanish_template = ChatPromptTemplate.from_template("""Translate the following English text to Spanish Provide only the translated text: {english_statement}""")
prompt = english_to_spanish_template.invoke('Hello, how are you?')
print(llm.invoke(prompt).content)
6. 예시
다음 문장들을 가지고 3가지(감성분석, 메인 토픽 추출, 다음에 올 질문 만들기)
statements = [
"I had a fantastic time hiking up the mountain yesterday.",
"The new restaurant downtwon serves delicious vegeterian dishes.",
"I am feeling quite stressed about the upcoming project deadline.",
"Watching the sunsset at the beach was a calming exmperience.",
"I recently started reading a fascinating book about space exploration.",
]
sentiment_template = ChatPromptTemplate.from_template("In a single word, either 'positive' or 'negative' provide the overall sentiment of " \
"following piece of text: {text}")
main_topic_template = ChatPromptTemplate.from_template("Identify main topic with one sentence about following piece of text: {text}")
followup_template = ChatPromptTemplate.from_template("what is followup just one question about following piece of text: {text}")
sentiment_prompts = [sentiment_template.invoke({"text":statement}) for statement in statements ]
main_prompts = [main_topic_template.invoke({"text":statement}) for statement in statements ]
followup_prompts = [followup_template.invoke({"text":statement}) for statement in statements ]
sentiments = llm.batch(sentiment_prompts)
main_topics = llm.batch(main_prompts)
followups = llm.batch(followup_prompts)
for statement, sentiment, main_topic, followup in zip(statements, sentiments, main_topics, followups):
print(
f"Statement : {statement}\n"
f"Overall sentiment : {sentiment.content}\n"
f"Main topic : {main_topic.content}\n"
f"Followup question : {followup.content}"
)
2025년 데이터야 놀자 발표자로 참가했습니다. 2019년부터 참여를 드문드문 해오다가 발표자로 참가하려니 새롭네요. 참가자의 후기는 많을테니 저는 발표자로 참가하된 계기와 그 과정에서 얻는 깨달음 등을 정리해보는 글을 작성해봅니다 :) 발표에 관심이 있으시다면 2번 단락의 Lesson Learn을 집중적으로 보시면 도움이 되실 것 같습니다
1. 발표 계기
사실 퇴사한 2023년에도 운영진에게 발표 제안이 들어왔었지만, 항상 미루고 미뤘습니다. 발표라는게 되게 웅장하고 높은 목표일 것 같고 높은 상아탑을 만든 사람만 얘기할 수 있는 자리처럼 느껴졌거든요. 하지만 미룬다고 대단한 사람이 되는 건 아니였습니다.🤣 그래서 이번에 인과추론을 공부한 김에 같은 맥락인 A/B 테스트가 왜 중요한지 정리하는 시간을 가져보았습니다. 그리하야 제목 "프로덕트 팀은 왜 A/B Test를 사랑할까?(feat 인과추론)"입니다. 사실 동명의 글이 이 블로그에 남아있으며 제 블로그에서 반응이 2번째로 좋은 글 중 하나입니다.
위 내용이 이미 정리가 되어 있었어서 발표 개요도 쉽게 제출 할 수 있었고, Linkedin에도 홍보할 수 있었습니다. 제목이 어그로(?)를 제대로 끌어서 그런지 그렇게 유명인사가 아닌데도 반응이 너무 좋아서 '이거 사람들 진짜 많이 몰리겠는데' 하는 생각은 했지만... 발표장의 터져나갈정도로 사람들이 들어올줄이야... 역시 사람은 마음 대비를 해야합니다
2. Lesson Learn
유독 또 2025년 데이터야놀자가 발표의 퀄리티들이 좋았던 것 같습니다. 타 발표자에게 배울 점이 안 밖으로 많았았습니다. 이를 정리해보겠습니다.
Lesson Learn 1: 발표의 목적을 명확히 하자
청중들에게 어떤 내용을 전달할지 그리고 이 발표를 듣고서 어떤 Action Point를 도출할지에 대한 명확한 목표가 있어야 효능감을 느낄 것 같아 그 부분을 발표 끝 마무리에 한 장 정리를 했습니다.
Lesson Learn 2: 대외적으로 이 발표를 위해 내가 얻을 것을 명확히 하자
저는 그냥 발표 그 자체에 의미를 두고 나아가서 지식 공유에 대한 발판으로 삼는 다는 목적을 가지고 있었습니다. 타 발표자들은 인프런에서 시기에 맞춰 강의를 출시하시고 지식공유하는 시간을 가지는 한편, 당일 발표자료를 올리는 등 치밀하게 계획하시는 부분을 보면서 '내가 너무 발표 그 자체에만 목적을 두었구나'라는 생각이 들었습니다. 역시 사람은 행동에 목적성이 뚜렷해야합니다(!) 부라부랴 제가 추진하는 월간 데이터 노트의 홍보를 넣었지만 QR도 망가지고 어필도 못해서 너무 아쉽습니다 ㅠㅠ 다음 링크로 나마 공유합니다.https://monthlydatanote.oopy.io/
세번째는 아쉬웠던 점이기도 한 점입니다. 저는 점심시간 이후 발표였는데, 크게 3가지 문제가 있었습니다. ppt 발표를 하는 과정에서 서식이 깨져 일부 디자인이 무너졌습니다. 물론 제가 더블 체크했어야 했는데 역시 발표자료는 pdf로 하는게 좋겠습니다. 두 번째는 음향문제였습니다. 방송 쪽에서 어떤 문제가 있었던 건지 계속 OBS 마이크 음향 테스트를 하더라구요. 결국 시간이 지연되고 발표 도중에 잡음이 계속 들어가서 청중들이 불편함을 느꼈을 것 같습니다. 이때 잠시 숨을 고르고 문제를 해결하고 가는 것도 방법일 것 같습니다. 세번째는 Q&A입니다. 발표를 하는 것에 집중하느라 마무리가 '감사합니다'라고 하고 끝나버렸는데 사실 Q&A시간이 있었습니다. 화면에 띄워놓고 보면서 질의응답을 받아야했는데 사전에 Q&A 웹페이지가 띄워져있지 않았고, 발표가 모두 끝난 줄 알고 나가시는 분들이 많더라구요(아마 시간 지연으로 인한 타 발표를 들으러 가신 건지도..) 그래서 아차 싶었습니다. 사실 이런 변수들을 운영진에게 맞지만 최종적으로 발표자가 체크를 해야하는 부분이라고 생각되어서 저도 다음 발표에는 이 부분을 신경써야겠다고 느꼈습니다.
3. Action Point
이번에 발표를 사전 점검하면서 OBS를 통해 유튜브로 녹화하는 경험도 해봤습니다. 원래 강의를 좀 했어서 그런지 어렵지는 않았는데 정말 쉽게 영상을 찍을 수 있다는 걸 알고 '이제 더이상 인프런에 강의 올리는걸 미루지 말자'라는 생각이 들었습니다. 주제가 뭘할지 고민이지만 너무 힘들이지 않고 간단한 무료 영상부터 시작해서 해볼 생각입니다.
4. 나가며
사실 제 발표이 외에도 좋은 발표가 정말 많았습니다. 다이나믹 프라이싱을 하는 과정에서 얻은 교훈과 절차에 대해서 소개해주신 발표도 좋았고(AI 없이 다이나믹 프라이싱 설계하는 법, 박성원님), 데이터 마트 모델링을 Fact/Demension을 하나씩 설명하고 CTR을 계산하며 사용성/확장성/효율성 측면 3 가지를 모두 포괄하며 정말 기막히게 설명하신 발표도(데이터 마트 모델링 후기, 김진석님) 있었습니다. 정말 세상에는 똑똑하고 멋진 분들이 많다는 걸 느꼈고 저도 성장해야겠다는 동기 부여가 많이 되었습니다.
또한, 컨퍼런스가 으레 커뮤니티의 장이긴하지만 이번에 진짜 제 데이터 생활(?)에서 거의 모든 분들을 만난 것 같습니다 ㅋㅋ 서울대 부트캠프 친구, 모두연/스파르타 코딩클럽 학생들, 스코 동료 튜터분들, 글또 분들까지 모든 유니버스가 합쳐지는 신기한 세계여서 앞으로도 이런 재미난 행사는 자주 참여하는게 좋겠습니다. 끝으로 이번에 발표했던 자료를 공유합니다. Linkedin 포스팅 링크도 첨부드립니다.
이전 단원에서는 CPU가 하는 일과 그와 함께 등장하는 스레드, 코루틴, 콜백함수와 동기/비동기 등에 대해서 알아봤습니다. 이번 장에서는 CPU가 데이터를 끌어오는 장소 메모리에 대해서 어떻게 활용하는지에 대해서 논의합니다. 특히, 함수의 호출에 대한 스택영역과 동적 할당을 위한 힙 영역에서 활용되는 예시를 아주 상세하게 설명하고 있어서 개인적으로 이해가 잘되었습니다. 마무리로는 Python에서 자동으로 메모리할당/제거 해주는 가비지 컬렉션 외에도 관리할 수 있는 몇가지 방법에 대해서 작성하며 마무리합니다.
처음 C언어를 배울 때, 포인터는 많이 좌절하는 구간입니다. C에서는 포인터에 대해서 중요하게 다루는 반면 java와 Python에서는 참조라는 이름으로 다르게 불리웁니다. 그 차이가 무엇일까요?
일단 그 전에 메모리 정의 부터 해야할 것 같습니다. 메모리는 1,0 숫자를 저장하는 컴퓨터의 '사물함'이라고 표현하고 있습니다. 보통 8개의 사물함을 묶어 표현하며 이를 1Byte라고 합니다. 쉽게 말해 부호 없는 정수(unsigned interger)르 2^8까지 즉 0 ~ 255까지 저장할 수 있는 공간입니다. 흔히 Panas로 데이터를 불러 올때, int64 데이터 형식으로 데이터를 불러오게되는데 이는 CPU 처리 형식과 관련이 있는 것임을 알 수 있었습니다.
이런 메모리에 값이 저장이 되고 변수는 그 메모리의 저장 위치를 가리킨다는 직관적인 이해를 할 수 있습니다. 반면 동일한 값을 저장하는 경우에는 굳이 중복되게 메모리에 저장할 필요가 없습니다. 이때 해당하는 데이터의 주소를 저장하므로 포인터의 개념이 등장합니다. 책에서는 포인터는 메모리 주소를 더 높은 수준으로 추상화 한 것이라고 말합니다.
포인터는 C와 같이 직접적으로 주소에 접근 할 수 있어 메모리를 직접 프로그래머가 알 수 있고 수정할 수 있습니다. 하지만 자바나 파이썬은 이를 이런 작업이 불가능합니다. 이런 이유 때문에 C언어가 저수준의 계층을 제어하고 강력한 힘을 가지며 시스템, 임베디드 프로그래밍에 적합한 언어임을 할 수 있습니다. 반면 이런 자율성이 오히려 난이도를 높이는 결과를 가져오기도 합니다.
반면 적은 힘에는 적은 책임이 따르는(?) 포인터가 없는 언어의 경우 "참조(reference)"라고 통칭 됩니다. 참조는 포인터를 한번 더 추상화 한 것이라고 표현합니다.
2. 프로세스의 모습과 스택 영역
왼쪽은 메모리의 프로세스의 모습입니다. 코드 영역과 데이터 영역은 Chapter1에서 설명했던 것 처럼 링커가 만든 실행파일을 초기화 할 때 생성되는 영역입니다. 힙 영역은 동적 메모리 할당에 사용 되는데 C언어에서는 malloc 함수가 요청하고 관리합니다. 주로 프로그래머가 직접 관리하는 영역입니다. 마지막으로 스택 영역은 함수 호출에 사용되며 매개 변수, 반환 주소, 레지스터 정보 등 을 포함한 함수 실행 시 저장하는데 사용하며 필요에 따라 중간의 유휴 영역에 메모리를 사용할 수 있습니다.
스택 영역에서 일어나는 일은 자료 구조의 Stack을 이해했다면 꽤나 쉬워서 간단하게 그림으로 넘어갑니다.
함수 실행에 대한 Stack 형식 도식화
그림엔 표현되어있지 않지만 함수 실행 관점에서는 2가지가 중요한데, 어디서 왔는지에 대한 반환(return)과 어디로 가는지에 대한 정보(jump)의 정보가 필요합니다. 함수A가 다른 함수 B를 호출한다면 이 연계를 통해 CPU에게 끊이지 않고 프로그램 실행을 요청해야하기 떄문입니다. 함수의 스택 프레임은 어디로 가야할지에 대한 정보를 하단에 추가합니다. 반환 주소 외에도 지역변수, 매개 변수를 전달하거나 반환값을 가져오면 역시 스텍 프레임에 추가됩니다. 이로서 기존 유휴 공간이 줄어듭니다.
지역 변수와 달리 전역변수는 모든 모듈에 노출이 되어있으며 접근 가능합니다. 이를 프로그래머가 직접 제어하는 것이 중요합니다. 왠면 지역 변수는 함수에 종속되어 있어서, 메모리의 사용이 끝나면 메모리를 무효화하기 때문에 해당 정보가 언제까지 유지될지 모르기 때문입니다. 따라서 직접 제어할 수 있는 메모리 영역이 필요해졌고 이를 힙 영역을 활용하기로 하였습니다.
이런 힙 영역은 C언어에서 malloc 함수로 활용하며 여기서 고민할 가지는 4가지로 다음과 같습니다.
메모리 유휴 여부를 확인하기
메모리 사용 우선 순위
할당 후 남은 메모리 처리 방안
반환된 메모리의 처리 방안
1. 메모리의 유휴 여부를 확인하는 것은 메모리의 header 영역과 payload 영역을 따로 설정하여 정보를 나타나게 하였습니다. free/allocated 를 뜻하는 f/a 영역에 메모리의 사용 여부를, 조각의크기를 저장하여 메모리의 사용정보를 나타냈습니다.
메모리의 사용 정보 저장
2. 메모리 사용 우선 순위: 메모리는 파편화되어 있을 수 있기 때문에 어떤 메모리를 먼저 할당하여 효율적으로 사용할지는 상황에 맞는 알고리즘을 써야하며, 최초 적합 / 다음 적하 / 최적 적합 방식을 쓸 수 있습니다. 이 부분에서 쏘카에서 했던 자동차 대여 테트리스 블로그 글이 생각났는데, 메모리는 full scan을 할만큼 고정되어있는 정보이고 자동차 대여는 미래를 알 수 없는 점이라는게 다르네요.
3. 남은 유휴 메모리 처리 방안: 메모리가 해제 될때 즉시 병합하는 것은 어려운 일은 아니나, 해제될 때 마다 병합하는 것은 새로운 메모리 저장 요청이 들어왔을 때 다시 분할의 과정을 거쳐야하는 이슈가 있을 수 있으므로 대부분의 경우에 '연기'를 하는 방식을 선택하고 있다고 합니다. 이것도 사실 상황에 따라 달라질 수 있는 것이라 생각됩니다.
4. 그러면 남은 메모리를 어떻게 효율적으로 병합할 수 있을까요? 그전에 사실 header에는 해당 메모리의 할당 여부와 크기 밖에 없기 때문에 그 전 메모리에 대한 정보를 알 수 없습니다. 따라서 연결 리스트의 개념을 본따 이중 연결 리스트로 해결할 수 있습니다.
x86 CPU는 4가지 단계의 특권 단계(previlege level)을 제공하며, Level 0은 커널 상태 가장 수준 높은 상태. 반면 Level 3는 사용자 상태에서 사용할 수 있는 응용 프로그램이 접근 할 수 있는 단계입니다. 커널모드가 그럼 윈도우에서 관리자 실행 모드 인가 라는 질문에는 No 입니다.
일부 상황에서 응용 프로그램이 운영체재에 서비스를 요청할 수 있는데 이를 시스템 호출(System call)이라고 합니다. 이를 호출하는 단계를 표현하면 다음과 같습니다.
시스템 호출 과정에서 운영체제에 종속되지 않고 범용적으로 사용하기 위한 중간 계층이 있는데 이를 표준 라이브러리라고 합니다.표준 라이브러리는 사용자 상태에서도 실행되며 실행 중인 운영 체제에 따라 대응되는 시스템 호출을 선택합니다. C언어에서 malloc과 같은 메모리 할당자가 바로 이 표준 라이브러리에 존재합니다.
5. 파이썬에서 메모리 관리
c언어에서는 메모리 관리에 대해서 매우 딥하게 배우지만 파이썬은 그렇지 않습니다. del 과 같은 명령어로 객체를 삭제하여 명시적으로 메모리를 확보할 수 있습니다.
가비지 컬렉션(Garbage Collection)
프로그램이 할당했지만 더 이상 사용하지 않는 메모리를 자동으로 회수해주는 프로그램입니다. 다음 코드로 강제 실행을 할 수 있습니다.
import gc
# 가비지 컬렉션 강제 실행
gc.collect()
제너레이터 사용: 함수의 결과를 한번에 반환하지 않고 yield를 통해 하나씩 리턴
# 리스트: 메모리를 한 번에 많이 사용
nums = [i * i for i in range(1000000)]
# 제너레이터: 하나씩 생성 -> 메모리 절약
def generate_nums():
for i in range(1000000):
yield i * i
nums_gen = generate_nums()
# 사용 예시
for num in nums_gen:
if num > 100:
break
로컬 함수에 변수 선언
함수 내부에서 정의된 변수는 전역 변수보다 접근이 빠르고 함수 실행이 끝나면 자동으로 메모리에서 해제됨!
# 전역 변수 (권장되지 않음)
temp = 0
def compute_sum_global(n):
global temp
for i in range(n):
temp += i
return temp
# 로컬 변수 (권장)
def compute_sum_local(n):
temp = 0 # 로컬 변수
for i in range(n):
temp += i
return temp
print(compute_sum_local(1000000))
구현된 함수나 라이브러리 사용
표준, 외부 라이브러리는 C로 구현되어있어서 빠르고 메모리 효율적
# 직접 구현한 합계 함수 (비효율)
def custom_sum(lst):
result = 0
for num in lst:
result += num
return result
# 내장 함수 sum 사용 (권장)
nums = list(range(1000000))
print(sum(nums)) # 훨씬 빠르고 메모리 효율적
반복문에 itertools 사용
import itertools
# 일반 for문으로 두 리스트의 모든 조합 생성 (메모리 낭비 가능)
list1 = [1, 2, 3]
list2 = ['a', 'b', 'c']
product_manual = [(x, y) for x in list1 for y in list2]
# itertools.product 사용 (lazy evaluation)
product_lazy = itertools.product(list1, list2)
for pair in product_lazy:
print(pair)
메모리 타입 최적화
import pandas as pd
import numpy as np
# 비효율적인 타입
df = pd.DataFrame({
'id': range(10000),
'category': ['A'] * 5000 + ['B'] * 5000
})
# 메모리 최적화
df['id'] = df['id'].astype(np.int16)
df['category'] = df['category'].astype('category')
print(df.info(memory_usage='deep'))
-- 코드를 입력하세요
SELECT HISTORY_ID, CAR_ID, DATE_FORMAT(START_DATE,'%Y-%m-%d') AS START_DATE, DATE_FORMAT(END_DATE,'%Y-%m-%d') AS END_DATE,
CASE WHEN END_DATE - START_DATE >= 90 THEN "장기 대여"
ELSE "단기 대여" END AS RENT_TYPE
FROM CAR_RENTAL_COMPANY_RENTAL_HISTORY
WHERE (MONTH(START_DATE) = 9) AND (YEAR(START_DATE) = 2022)
ORDER BY HISTORY_ID DESC
이번 장에서는 운영체제, 프로세스, 스레드의 개념을 기반으로 동기/비동기, 블로킹/논블로킹에 대해서 다룹니다. 기본적으로 컴퓨터가 일을 처리하는 방식에서의 추상화된 개념을 배울 수 있는 좋은 장이였습니다.
1. 용어 정리
신규 개념
프로그램 카운터
레지스터
진입 포인트(Entry Point)
구조체(structure)
스레드(thread)
스레드 풀(thread pool)
동기/비동기
콜백지옥
블록/논블록
컨테이너
1. CPU가 하는일
1. 메모리에서 명령어 가져오기
2. 이 명령어를 실행하기
CPU는 프로그램 카운터(PC), 레지스터에 명령어 주소를 저장함. 소스 파일을 컴파일러가 실행파일로 변환시키고, 이를 디스크에 저장하며 실행 당시 메모리에 올리면 CPU가 이를 확인하여 실행시킴. 다중프로세스 프로그랭을 통해서 한 번에 여러 연산이 가능하게 하지만, 다음의 부담이 있음
프로세스를 생성할 때의 부담(overhead)
프로세스 간 통신으로 인한 복잡성 증가
따라서 스레드의 개념이 등장
2. 스레드 등장
프로세스는 메모리에서 실행할 적절한 영역을 찾을 후 CPU의 PC 레지스터를 main함수로 지정하는 흐름. 따라서 단 하나의 실행으름만 존재 스레드는 운영체제 계층에서 구현되며 동일한 프로세스에서 동시에 진행되는 개념. 이때 스레드는 긴 작업과 짧은 작업이 존재 할 수 있으며 매번 스레드를 열었다 닫았다하면 부하가 걸리므로 짧은 작업은 스레드 풀(thread pool)을 만들어 처리할 작업이 생기면 스레드에 요청하고 그 외에는 휴무. 이때 생성자와 소비자의 개념이 등장스레드는 전용 리소스와 공유 리소스를 구분해야 스레드 안전을 추구할 수 있게됨
3. 코루틴의 등장
붉은색 푸른색~ 그 사이 짧은 "코루틴" 이무진의 <신호등> 각색
코루틴은 Python 에서 yield로 구현되며, 함수 실행 중간 일시중지와 재개의 기능을 할 수 있게됨
# 일반
def func():
print("a")
print("b")
print("c")
def foo():
func()
# a
# b
# C
이때 yield함수를 통해서 일시정지와 재개의 개념을 넣을 수 있음. 코루틴은 자신이 일시 중지될 때 실행 중인 상태를 저장했다가 다시 시작 하여 계속 되는 구조를 만들 수 있음
# 코루틴
def func():
print("a")
yield
print("b")
yield
print("c")
def A():
co = func()
next(co)
print("in function A")
next(co)
# a
# in fucntion A
# b
함수는 코루틴의 특별한 예이며, 반대로 코루틴은 함수의 일반화
4. 콜백 함수
콜백 함수는 다른 코드에 매개변수로 전달되는 실행 가능한 코드. 다른 함수가 반환 될때 까지 기다리는 동기 콜백(블로킹 콜백)과 콜백 함수와 주 프로그램이 동시에 진행될 수 있는 비동기 콜백 이 있음. 하지만 비동기 콜백은 복잡한 서비스에서 수십 수백개의 콜백함수가 비동기 콜백으로 처리하여 콜백 지옥에 빠질 수 있는 단점이 있음.
5. 동기/비동기
동기: 작업이 순차적으로 진행되며, 한 작업이 완료될 때까지 기다려야함(Ex 전화통화)
비동기: 작업이 독립적으로 실행되며, 다른 작업을 동시에 수행 가능(Ex 이메일 통신)
블로킹: 실행 중인 스레드나 프로세스가 작업 완료될 때 까지 일시 중지됨, 필시적으로 동기작업은 블로킹
논블로킹: 실행중인 스레드나 프로세스가 중지되지 않고 계속적으로 실행
하지만 논블로킹이 반드시 비동기를 의미하는 것은 아님. 피자를 주문해놓고 매번 호출자가 피자 완료를 확인한다면 비동기이긴 하지만 논블로킹하지 않음. 갑자기 흑백요리사의 리조또 100인분 짤이 생각남
논블로킹 - 준동기식 리조또 100인분
5. 컨테이너와 가상 머신 기술
CPU가 능동적으로 일시중지하고 다음에 함수가 호출 될 때, 앞에서 중단된 지점에서 다시 실행이 가능할 때 함수를 코루틴
함수의 일시 중지와 재개가 커널 상태에서 구현된는 경우를 스레드
스레드에 주소 공간처럼 종속된 실행시 리소스를 결합한 것이 프로세스
구성, 라이브러리처럼 프로그램이 의존하는 실행 환경과 함께 묶인 것을 컨테이너
컨테이너는 운영 체재 계층 수준에서 소프트웨어 리소스를 가상화함, Docker로 인하여 빠르게 대중화된 기술
case when 으로 비트 마스킹 후 min/max함수를 이용해 이용 가능한 차량 출력하기
3. 풀이
# car_id 기준으로 한번이라도 0이 등장하면 못빌리는 자동차
select car_id, case when possible = 1 then '대여 가능' else '대여중' end as AVAILABILITY
from(
select car_id,
min(possible) as possible
from
(
select car_id,
DATE_FORMAT(start_date,'%Y-%m-%d'), DATE_FORMAT(end_date,'%Y-%m-%d'),
case when start_date <= '2022-10-16' and end_date >= '2022-10-16' then 0
else 1 end as possible # 해당하는 기간에 16일이 껴있으면 0(불가), 1(가능)
from car_rental_company_rental_history
order by car_id asc
) t1
group by car_id
) t2
order by car_id desc