/

핵심 요약

면접에서 I2C SPI UART 차이를 설명할 때 가장 먼저 확인해야 하는 것은 선 개수와 동기·비동기 여부입니다. I2C는 2선(SDA·SCL) 동기 방식, SPI는 4선(SCLK·MOSI·MISO·CS) 동기 방식, UART는 TX/RX 2선(+GND) 비동기 방식입니다. 면접에서는 이 기본 구조 차이를 먼저 설명하고, 장치 선택 방식과 속도, 대표 용도까지 이어서 답하는 것이 핵심입니다.

이 글은 임베디드 BSP·펌웨어 직무 면접을 준비하는 관점에서 I2C·SPI·UART의 핵심 개념과 답변 포인트를 정리한 글입니다.

I2C·SPI·UART 차이: 선 개수와 동기·비동기는?

한 줄 답: I2C = 2선(SDA·SCL) 동기 · SPI = 4선(SCLK·MOSI·MISO·CS) 동기 · UART = TX/RX 2선(+GND) 비동기

  1. 동기 방식은 클럭 선을 함께 보내며, 비동기 방식은 클럭 없이 양쪽이 같은 baud rate를 미리 약속합니다.
  2. I2C는 오픈 드레인 + 풀업 저항이 필요하다는 점이 면접 단골 질문입니다.

  1. 세 버스의 선 수, 클럭, 듀플렉스를 비교하면 다음과 같습니다.
    항목I2CSPIUART
    선 수2선 (SDA, SCL)4선 (SCLK, MOSI, MISO, CS)2선 (+GND) (TX, RX 교차)
    동기 여부동기식 (클럭 있음)동기식 (클럭 있음)비동기식 (클럭 없음)
    듀플렉스반이중 (Half-duplex)전이중 (Full-duplex)전이중 (Full-duplex)

속도·주소 지정·다중 장치 연결은 어떻게 다른가?

한 줄 답: I2C는 주소로 2선에 여러 장치를 묶고, SPI는 장치마다 CS 선으로 골라 공유 클럭으로 전송하며, UART는 1:1 연결에 맞춰 양쪽 baud 속도를 약속합니다.

  1. I2C는 주소로 장치를 선택합니다(7비트, "can be expanded to get a 10 bit I2C address"). 같은 2선에 여러 장치를 연결할 수 있습니다. Rd/Wr bit(Rd=1, Wr=0)로 읽기·쓰기를 구분하고, A/NA(ACK/NACK)로 응답을 확인합니다. 속도는 모드명 수준(Standard-mode, Fast-mode, High-speed mode)으로 구분하고, Linux 커널 문서는 "typically up to 400 kHz, high speed modes up to 5 MHz"라고 설명합니다. (예시 주소: 0x48 / 0x68)
  2. SPI는 주소가 없어 장치마다 CS(Chip Select) 선을 1개씩 둡니다. SCK·MOSI·MISO는 공유합니다. 클럭 모드는 0–3이 있으며, CPOL은 초기 클럭 극성(initial clock polarity), CPHA는 위상(phase)을 뜻합니다(CPHA=0이면 leading edge에서 샘플링). 모드 0 = CPOL=0, CPHA=0입니다. SPI는 "simple de facto standard, not complicated enough to acquire a standardization body"이며, 장치 자동 탐색(automatic discovery/enumeration)은 거의 지원하지 않습니다. 속도는 칩·보드 데이터시트가 정하며, 커널 문서는 SCK를 "often on the order of 10 MHz"라고만 설명합니다.

  1. UART는 기본적으로 1:1 연결이며, start/stop 비트로 프레임을 구분합니다. 워드 길이·parity·stop 비트·baud rate 같은 포트 파라미터(커널 serial 드라이버 문서 기준)를 양쪽이 같게 설정해야 하며, 속도도 양쪽 baud rate 설정으로 맞춥니다(예: 115200 같은 관용 값).
  2. 통신 특징별 세부 비교는 다음과 같습니다.
    항목I2CSPIUART
    장치 선택주소 (7비트/10비트 확장)CS (Chip Select) 선기본 1:1 연결
    다중 장치2선에 여러 장치 공유CS 선 추가로 여러 장치1:1
    응답 확인ACK/NACK없음없음
    속도 결정통신 모드 사양에 따름칩/보드 한계가 정함양쪽 baud rate 약속

센서·플래시·디버그 콘솔, 무엇을 고르나? 면접 답변 틀

한 줄 답: 핀을 아껴야 하는 센서에는 I2C를, 빠른 연속 전송이 필요한 플래시에는 SPI를, 부트 로그나 단순 통신에는 UART를 사용합니다.

  1. I2C 용도: 센서·EEPROM 여러 개를 연결하며 핀을 아낄 때 사용합니다.
  2. SPI 용도: SPI 플래시·디스플레이·ADC처럼 빠른 연속 전송에 사용합니다. 커널 문서에 따르면 모든 MMC/SD 카드가 SPI를 지원하며, 일부 PC는 BIOS 코드용으로 SPI 플래시를 사용합니다.
  3. UART 용도: 부트 로그·디버그 콘솔·GPS/BT 같은 모듈 통신에 사용합니다.
  4. 면접 답변 템플릿: 「선 수와 동기/비동기 여부를 먼저 언급한 후 → 장치 선택 방식과 속도 차이를 비교하고 → 각 통신 방식에 맞는 대표적인 용도 예시를 순서대로 답합니다.」
면접용 한 줄: I2C는 2선 동기+주소, SPI는 4선 동기+CS, UART는 2선 비동기+baud rate라는 구조 차이를 먼저 짚고, 핀 절약·속도·단순 통신이라는 각각의 쓰임새로 이어서 답변합니다.

관련 글: 인터럽트와 폴링, 임베디드 면접에서 어떻게 고르나? · DMA란? CPU 복사와 차이, 임베디드·펌웨어 면접에서 어떻게 답하나?

참고 문서: I2C summary · I2C protocol · SPI summary · Serial driver API

자주 헷갈리는 질문(FAQ)은 무엇입니까?

Q. I2C 통신에는 왜 풀업 저항이 필요합니까?

I2C 선은 오픈 드레인 구조라 장치가 선을 Low로 당기기만 하므로, 놓았을 때 High로 되돌릴 외부 풀업 저항이 필요합니다.

Q. SPI 통신 시 모드가 맞지 않으면 어떤 증상이 나타납니까?

샘플링 에지가 어긋나 비트가 밀리거나 값이 깨져 보일 수 있습니다. 칩 데이터시트의 타이밍도에서 CPOL·CPHA를 확인합니다.

Q. UART 통신 시 데이터가 쓰레기 값(garbage)으로 나올 때 가장 먼저 확인할 것은 무엇입니까?

양쪽의 baud rate 설정, GND 공통 연결, TX와 RX의 교차 연결 여부를 확인해야 합니다.

한 줄 요약

프로그래머스 AI 활용평가 체험을 직접 풀어 봤더니 절반 조금 넘게 맞혔습니다. 쉽게 생각했는데, 이러다간 떨어지겠다는 생각이 들었습니다. 코딩할 때처럼 처음부터 AI에게 물어본 게 문제였고, 이제는 "AI는 손이고 머리는 나다"라는 원칙으로 준비하고 있습니다.

SK하이닉스 AI 해커톤 2026이란? 서류 합격 후기와 1차 예선 각오에 이어지는 글입니다.

프로그래머스 AI 활용평가는 어떤 문제가 나오나요?

프로그래머스의 AI 활용평가는 "AI Assist를 활용하여 실제 업무와 유사한 과제를 해결하는 실전형 평가"입니다. 체험 목록에는 비개발 직군 대상 과제 세 개가 있고, 각각 4문항, 객관식/주관식, AI Assist 활용으로 되어 있습니다.

  • 간식 선호도 설문조사 분석
  • 구독 비용 최적화
  • 식품/화장품 규정 위반 식별

아래는 실제 체험 목록 화면입니다. 문제 내용과 정답은 쓰지 않고, 주제와 제가 푼 방식만 정리하겠습니다.

직접 풀어보니 결과는?

체험 모드라 점수는 나오지 않고, 문항별로 맞았는지 틀렸는지만 나옵니다. 세 과제 12문항 중 7문항을 맞혔습니다. 절반 조금 넘게 맞힌 셈입니다.

  1. 구독 비용 최적화 (4문항): 1문항 정답, 1문항 오답, 2문항 미제출입니다. 앞 문제에서 AI 질문 횟수를 다 써 버렸고, 표 하나면 풀 수 있는 문제에서 AI가 낸 답을 그대로 골랐습니다.
  2. 간식 선호도 설문조사 분석 (4문항): 4문항 모두 정답입니다. AI는 10번 중 4번만 썼습니다. 간단한 부분은 직접 풀고, 컨텍스트를 좁혀서 주고, 단계별로 지시하고, 출력 형식을 정해 줬습니다. 다만 AI가 13명이라고 해 놓고 16명을 나열하는, 자기 말끼리 안 맞는 답을 냈는데 저는 검증하지 않고 넘어갔습니다.
  3. 식품/화장품 규정 위반 식별 (4문항): 4문항 중 2문항 정답입니다. 일반 기준을 묻는 문제에 EU 기준 문서를 참고 문서로 골랐고, 조건 하나를 놓쳤고, 대상 45개 중 일부만 첨부했습니다.

가장 크게 배운 점은 AI를 쓰기 전에 문제를 끝까지 읽고 조건을 내가 먼저 정리해야 한다는 것입니다. AI는 손이고 머리는 나입니다.

코딩할 때처럼 AI를 쓰면 왜 막힐까요?

문제는 코딩할 때 습관대로 처음부터 AI에게 물어본 것입니다. 코딩은 문제가 명확하고, 컨텍스트가 코드베이스 그 자체라서 "코드베이스 조사 → 수정 계획 → 실행" 딸깍이 통했습니다.

그런데 이런 문제는 코드베이스처럼 주어지지 않습니다. 내가 문제를 이해하고, 무엇이 필요한지, 어디에 AI를 더 쓸지 고민해야 합니다. 이론은 공부해 둔 것과 사용 경험으로 괜찮을 것 같은데, 자료를 기반으로 문제를 푸는 건 약하다는 걸 알았습니다.

앞으로 AI를 어떻게 쓰기로 했나요?

  1. 먼저 고민하고 일을 시킵니다: 문제를 읽고 무엇을 풀어야 하는지, AI를 어떻게 효율적으로 쓸지 고민한 뒤에 일을 시킵니다. 조건이 여러 개면 ①②로 쪼개서 정리합니다.
  2. AI에게 정보를 가공해서 줍니다: 계획 문서를 세우듯 정보를 가공해서 주는 연습을 합니다. 필요한 숫자만 표로 모으고, AI에게는 계산만 시킵니다.
  3. 모르면 필요한 것부터 묻습니다: 이해가 안 되면 AI에게 "답을 내려면 뭐가 필요한지" 먼저 물어서, 필요한 정보를 제대로 줍니다.

여기에 실전 팁 두 가지를 더했습니다. 문제별로 AI 질문 횟수를 미리 나눠 두고, AI 답에는 검증 프롬프트를 붙여 자기 말끼리 맞는지 확인하게 합니다.

1차 예선까지 일주일, 각오는?

1차 온라인 시험까지 약 일주일 남았습니다. 2차 온라인까지 가 보고 싶고, 2차를 보면 또 오프라인까지 가고 싶겠지요. 상반기 면접 뒤에 후회가 남았는데, 이번에는 후회 없게 열심히 해 보겠습니다. ㅎㅎ

그동안 정리한 AI 공부 시리즈도 계속 이어 가고 있습니다.

Day5~7: AI의 한계, Embedding·RAG 구조, AI Agent와 Tool Calling

SK하이닉스 AI 해커톤 2026 1차 예선(AI 기초 이해 및 활용 역량) 대비 공부 노트. Day4 프롬프트 엔지니어링에 이어지는 글.

  1. Hallucination(환각)이란? AI가 사실이 아닌 내용을 그럴듯하고 자신 있게 말하는 것. 예: 없는 논문 제목을 지어내거나, 틀린 날짜를 확신에 차서 말함.
  2. 왜 생기나? LLM은 사실을 검색하는 게 아니라 "다음에 올 그럴듯한 토큰"을 확률로 예측한다. 모르면 "모른다"고 하는 게 기본 동작이 아니라서, 말투는 자연스러운데 내용은 틀린 답이 나올 수 있다.
    • 비유: 세종대왕 발명품 3개를 묻는데 2개만 알 때, 대화를 멈추기 싫어서 하나를 지어내는 것.
    • 시험용 한 줄: LLM은 사실을 검색하는 게 아니라 그럴듯한 다음 토큰을 확률로 예측하기 때문에, 모르는 내용도 자연스럽게 지어낼 수 있다.
  3. Hallucination 줄이는 방법
    • RAG: 믿을 만한 문서를 찾아 Context에 넣어 근거 기반으로 답하게 한다.
    • Tool(웹 검색): 학습 시점 이후의 최신 정보 문제까지 해결한다.
    • 프롬프트로 "모르면 모른다고 답하라" 지시: 효과는 있지만 모델이 자기가 모른다는 걸 항상 아는 건 아니라 완벽하지 않다.
    • 사람의 검증: 출처 확인, 코드 직접 실행, 교차 확인.
  4. 잘못된 Context (Garbage in, garbage out): RAG로 넣은 문서가 오래됐거나 틀리면 AI는 그걸 믿고 틀린 답을 한다. RAG는 문서 최신화와 출처 관리가 필수다.
  5. AI의 대표 한계 네 가지
    • Hallucination: 모르는 내용도 그럴듯하게 지어낸다.
    • 최신 정보 문제: 학습 시점 이후 일은 모른다.
    • 잘못된 Context: 틀린 문서를 넣으면 틀린 답이 나온다.
    • Context 한계: 컨텍스트 윈도우에 다 못 넣고, 길어지면 앞 내용을 놓친다.

  1. AI가 써준 코드 검증하기
    • 직접 빌드하고 실행해 본다.
    • 빌드·실행이 된다고 결과가 맞는 건 아니다. 정상 입력, 경계값, 이상한 입력으로 테스트해서 예상한 결과가 나오는지 확인한다.
    • 문법 오류는 컴파일러가 잡아주니, 사람은 로직이 요구사항에 맞는지 보는 데 집중한다.
  2. Day5 결론: AI 결과는 초안이고, 검증은 사람 몫이다. 출처 확인, 테스트, 교차 확인.

Part 2. Embedding과 RAG 구조

  1. 네 가지 구분 (시험 단골)
    • Embedding: 문장을 의미가 담긴 숫자 벡터로 바꾸는 것
    • Vector DB: 그 벡터들을 저장해 두고 비슷한 걸 빨리 찾아주는 저장소
    • RAG: 질문과 관련된 문서를 찾아 LLM의 Context에 넣어주는 전체 방식
    • LLM: 그 Context를 보고 최종 답을 만드는 모델
  2. RAG 흐름: 문서 → Chunk(잘게 자르기) → Embedding → Vector DB 저장 → 질문도 Embedding → 비슷한 문서 검색 → LLM Context에 넣기 → 답변

  1. Chunk로 자르는 이유
    • 필요한 단위로 저장하면 유사 문서 검색이 더 효율적이고 정확해진다.
    • Context 한계: 통째로 넣으면 컨텍스트 윈도우를 잡아먹고, 상관없는 내용이 섞여 답이 흐려진다.
    • 주의: 너무 작으면 문맥이 끊기고, 너무 크면 쓸데없는 내용이 섞인다. 그래서 조각끼리 살짝 겹치게(overlap) 자르기도 한다.
  2. "Vector DB가 곧 RAG다"는 틀린 말: Vector DB는 RAG의 부품 하나일 뿐이고, RAG는 찾아서 Context에 넣고 LLM이 답하는 전체 방식이다. RAG가 꼭 Vector DB를 써야 하는 것도 아니다. 키워드 검색이나 웹 검색으로 찾아와도 RAG다.
    • 시험용 한 줄: Embedding은 변환, Vector DB는 저장과 검색, RAG는 검색해서 Context에 넣는 전체 방식, LLM은 답을 만드는 모델이다.

Part 3. AI Agent와 Tool Calling

  1. Agent란? 그냥 LLM은 질문을 받으면 바로 답만 한다. Agent는 LLM이 문제를 보고 스스로 판단해서 도구를 고르고, 써보고, 결과를 보고 다음 행동을 정한다. 이걸 반복해서 목표를 이룬다.
    • 흐름: 사용자 질문 → LLM 판단 → Tool 선택(검색, Python, DB, API, 파일) → 실행 결과 → LLM이 다시 판단 → … → 최종 답변

  1. 예: "오늘 서울 날씨 알려줘"
    • 그냥 LLM: 실시간 정보가 없어서 지어내거나, 모른다고 답한다. 어느 쪽이든 실제 날씨는 못 알려준다.
    • Agent: 위치 확인 → 날씨 API나 웹 검색 → 결과를 보고 답변한다.
  2. LLM / Workflow / Agent 구분 (시험 단골)
    • LLM: 질문 받으면 바로 답한다. 도구가 없다.
    • Workflow: 사람이 정해둔 순서대로 움직인다. 예: 매일 9시에 뉴스 3개 검색 → 요약 → 메일 발송.
    • Agent: LLM이 상황을 보고 다음에 뭘 할지 스스로 정한다. 예: "요즘 반도체 업계 이슈를 정리해줘"를 받으면 뭘 검색할지 고르고, 부족하면 다시 검색하고, 필요하면 표도 만든다.
    • 기준은 도구를 쓰느냐가 아니라 다음 행동을 누가 정하느냐다.
  3. Agent의 위험
    • 사용자가 원하지 않는 방향으로 작업이 진행될 수 있다.
    • 도구를 반복 호출해서 시간과 비용이 늘어난다.
    • 중간에 Hallucination이 끼면 잘못된 결과 위에 다음 행동을 쌓는다.
    • 파일 삭제나 메일 발송처럼 되돌릴 수 없는 행동을 할 수 있다.
    • 그래서 권한 제한, 중요한 행동 전 사람 승인, 결과 검증 장치를 둔다.

다음 글

머신러닝 기초와 AI로 문제 해결하기, 그리고 종합 문제 풀이

핵심 요약

C++ Insights는 컴파일러가 소스 코드 이면에서 실제로 수행하는 작업을 다시 C++ 코드로 펼쳐서 보여주는 Clang 기반 소스-투-소스 변환 도구입니다. 람다가 어떤 클래스와 operator()로 바뀌는지, range-for 루프가 begin/end 반복자 루프로 어떻게 전개되는지, 구조체 복사 시 어떤 생성자가 암시적으로 호출되는지를 어셈블리가 아니라 같은 C++ 문법으로 확인할 수 있습니다.

C++의 람다, range-for, auto 추론, 암시적 특수 멤버 함수는 모두 컴파일러가 뒤에서 코드를 만들어 내는 영역입니다. "컴파일러가 실제로 무엇을 하는지" 눈으로 확인하고 싶을 때 C++ Insights가 유용합니다. 이 글에서는 C++ Insights가 무엇인지, 어떻게 설치하고 첫 명령을 실행하는지 등 기본적인 C++ Insights 사용법과 함께 람다·range-for·암시적 생성자가 실제로 어떻게 펼쳐지는지를 검증된 예제로 정리합니다.

C++ Insights란? Compiler Explorer와 무엇이 다른가?

한 줄 답: C++ 코드를 "컴파일러가 이해한 C++ 코드"로 다시 펼쳐 보여 주는 Clang 기반 도구입니다.

  1. Compiler Explorer(https://godbolt.org)는 소스 코드를 최종 어셈블리로 보여주지만, C++ Insights는 어셈블리 대신 같은 C++ 문법으로 중간 형태를 보여주기 때문에 읽기 쉽습니다.
  2. 람다가 실제로 어떤 클래스인지, range-for가 어떻게 반복자 루프로 바뀌는지, 복사와 이동 중 어디서 무엇이 호출되는지를 확인할 때 특히 유용합니다.
  3. 구조체 복사 시 암시적 특수 멤버 함수가 어떻게 호출되는지는 별도로 다룰 예정입니다(「복사 생성자 vs 이동 생성자 차이: 언제 무엇이 호출되나?」, 발행 예정).

설치→첫 명령: 웹에서 바로 쓰고, 로컬에선 어떻게 돌리나?

한 줄 답: 웹(cppinsights.io)에서 바로 실행하거나, macOS Homebrew 및 Linux 바이너리를 통해 로컬에 설치할 수 있습니다.

  1. 가장 빠른 방법은 설치 없이 쓰는 것입니다. https://cppinsights.io에 접속해 코드를 붙여넣고, C++ 표준을 선택하면 우측 창에서 펼쳐진 결과를 바로 확인할 수 있습니다.
  2. 로컬에 설치하려면 macOS에서는 brew install cppinsights(Homebrew 포뮬러 페이지: https://formulae.brew.sh/formula/cppinsights, 2026-10-10 기준 stable 21.1)를, Linux에서는 GitHub 릴리스(https://github.com/andreasfertig/cppinsights/releases)의 바이너리 아카이브를 사용합니다. 소스에서 직접 빌드하려면 저장소(https://github.com/andreasfertig/cppinsights)를 참고하면 됩니다. Docker로 실행하는 별도 저장소(https://github.com/andreasfertig/cppinsights-docker)도 있지만, 실제 실행 명령은 이번 글에서 검증하지 못했습니다.
  3. 첫 명령은 insights <YOUR_CPP_FILE> -- -std=c++17 형태입니다. 이번 검증 환경(release v_21.1, insights-ubuntu-amd64.tar.gz, cpp-insights 21.1 / Clang 21.1.8 Ubuntu 빌드)에서 insights lambda.cpp -- -std=c++17을 그대로 실행하면 fatal error: 'stddef.h' file not found로 실패했습니다. README가 안내하는 scripts/getinclude.py도 실행해 보았지만 이 환경에서는 include 경로 외의 컴파일러 인자가 함께 섞여 나왔습니다. 그래서 g++의 include 경로를 -isystem 플래그로 직접 지정한 insights lambda.cpp -- -std=c++17 -isystem/usr/include/c++/14 -isystem/usr/include/x86_64-linux-gnu/c++/14 -isystem/usr/lib/gcc/x86_64-linux-gnu/14/include -isystem/usr/include/x86_64-linux-gnu -isystem/usr/include 명령으로 재실행하자 정상 동작했습니다. 이 로컬 21.1 바이너리의 출력은 cppinsights.io 웹 결과와 약간 달라서, 람다 클래스의 생성자를 public:으로 직접 보여주고 base 멤버 앞에 private:를 표시하며 constexpr 대신 /*constexpr */ 주석으로 표기합니다.

람다·range-for·숨은 생성자는 어떻게 펼쳐지나?

한 줄 답: 람다는 이름 붙은 클래스와 operator()로, range-for는 begin/end 반복자 루프로 변환되어 표시됩니다.

  1. 람다 변환 예시입니다. 아래 코드는 이 글에서 직접 만들어 cppinsights.io(C++17 옵션)로 변환한 것이고, README의 공식 람다 예시는 https://cppinsights.io/s/e4e19791에서 볼 수 있습니다.
    #include <cstdio>
    
    int main() {
        int base = 10;
        auto add = [base](int x) { return base + x; };
        std::printf("%d\n", add(5));
    }
    

    cppinsights.io 출력(펼친 코드):

    #include <cstdio>
    
    int main()
    {
      int base = 10;
        
      class __lambda_5_16
      {
        public: 
        inline constexpr int operator()(int x) const
        {
          return base + x;
        }
        
        // private: 
        int base;
      };
      
      __lambda_5_16 add = __lambda_5_16{base};
      printf("%d\n", add.operator()(5));
      return 0;
    }

  1. range-for 루프가 반복자 기반 루프로 전개되는 예시입니다. 아래 코드는 이 글에서 직접 만들어 cppinsights.io(C++17 옵션)로 변환한 것이고, README의 공식 range-for 예시는 https://cppinsights.io/s/0cddd172에서 볼 수 있습니다.
    #include <cstdio>
    #include <vector>
    
    int main() {
        std::vector<int> v{1, 2, 3};
        for (int x : v) std::printf("%d ", x);
        std::printf("\n");
    }
    

    cppinsights.io 출력(펼친 코드):

    #include <cstdio>
    #include <vector>
    
    int main()
    {
      std::vector<int, std::allocator<int> > v = std::vector<int, std::allocator<int> >{std::initializer_list<int>{1, 2, 3}, std::allocator<int>()};
      {
        std::vector<int, std::allocator<int> > & __range1 = v;
        __gnu_cxx::__normal_iterator<int *, std::vector<int, std::allocator<int> > > __begin1 = __range1.begin();
        __gnu_cxx::__normal_iterator<int *, std::vector<int, std::allocator<int> > > __end1 = __range1.end();
        for(; operator!=(__begin1, __end1); __begin1.operator++()) {
          int x = __begin1.operator*();
          printf("%d ", x);
        }
        
      }
      printf("\n");
      return 0;
    }
  2. auto/decltype 추론 결과가 실제 타입으로 펼쳐지는 예시입니다. 아래 코드는 이 글에서 직접 만들어 cppinsights.io(C++17 옵션)로 변환한 것이고, README의 공식 auto 예시는 https://cppinsights.io/s/6c61d601에서 볼 수 있습니다.
    #include <vector>
    
    int main() {
        std::vector<int> v{1, 2, 3};
        auto n = v.size();
        auto first = v.front() * 2.0;
        const auto& ref = v;
        return static_cast<int>(n + first + ref.size());
    }

    cppinsights.io 출력(펼친 코드):

    #include <vector>
    
    int main()
    {
      std::vector<int, std::allocator<int> > v = std::vector<int, std::allocator<int> >{std::initializer_list<int>{1, 2, 3}, std::allocator<int>()};
      unsigned long n = v.size();
      double first = static_cast<double>(v.front()) * 2.0;
      const std::vector<int, std::allocator<int> > & ref = v;
      return static_cast<int>((static_cast<double>(n) + first) + static_cast<double>(ref.size()));
    }
  3. 구조체를 복사 초기화할 때 암시적 복사 생성자가 호출되는 예시입니다. 이 지점은 앞서 언급한 「복사 생성자 vs 이동 생성자 차이: 언제 무엇이 호출되나?」 글과 이어지는 내용입니다.
    #include <string>
    
    struct Point {
        int x;
        std::string name;
    };
    
    int main() {
        Point a{1, "a"};
        Point b = a;   // 암시적 복사 생성자
        return b.x;
    }

    cppinsights.io 출력(펼친 코드):

    #include <string>
    
    struct Point
    {
      int x;
      std::basic_string<char, std::char_traits<char>, std::allocator<char> > name;
      // inline Point(const Point &) noexcept(false) = default;
      // inline ~Point() noexcept = default;
    };
    
    
    int main()
    {
      Point a = {1, std::basic_string<char, std::char_traits<char>, std::allocator<char> >("a", std::allocator<char>())};
      Point b = Point(a);
      return b.x;
    }
  4. 여기서 보이는 펼친 코드는 Clang의 해석을 기준으로 합니다. __lambda_5_16이나 __gnu_cxx::__normal_iterator 같은 내부 타입 이름은 컴파일러 버전과 표준 라이브러리(여기서는 libstdc++) 구현에 따라 달라질 수 있다는 점이 한계입니다.

이해했는지 Q&A로 확인해 볼까요?

Q1. C++ Insights와 Compiler Explorer 중 언제 무엇을 써야 합니까?

A1. 컴파일러가 코드를 어떻게 해석했는지(C++ 중간 형태)를 볼 때는 C++ Insights를, 최종 기계어나 최적화 결과를 볼 때는 Compiler Explorer를 사용합니다.

Q2. 로컬 실행 시 표준 헤더를 찾지 못하면 어떻게 해야 합니까?

A2. README는 scripts/getinclude.py로 컴파일러의 include 경로를 모으는 방법을 안내합니다. 이 글의 환경처럼 출력에 다른 인자가 섞이면, 표준 헤더 경로를 -isystem 옵션으로 직접 지정하면 됩니다.

Q3. 같은 코드면 C++ Insights 출력은 항상 똑같습니까?

A3. 아닙니다. 이 글의 람다 예제도 cppinsights.io 웹 결과와 로컬 21.1 바이너리 결과가 달랐습니다(생성자 표시, private:, constexpr 표기). 버전·옵션·표준 라이브러리에 따라 달라질 수 있습니다. std::move 자체는 C++ std::move, 복사를 줄이는 이유 글에서 다룹니다.

핵심 요약

MoE란 FFN 하나 대신 여러 전문가 FFN을 두고, 라우터가 토큰마다 일부(top-k) 전문가만 골라 계산하는 구조입니다. 토큰당 연산은 일부 전문가만 하지만 모든 전문가를 메모리에 올려 둬야 해 용량 부담이 큽니다.

이 글은 SK하이닉스 AI 해커톤 1차 예선(10/17–18)을 준비하며 정리하는 AI 공부 노트 시리즈입니다. 지난 글에서 토큰과 컨텍스트 윈도우를 정리했고, 오늘은 MoE(Mixture of Experts) 구조와 메모리 부담을 정리합니다.

MoE란? 토큰마다 일부 전문가만 쓰는 구조는?

한 줄 답: FFN 하나 대신 여러 전문가 FFN을 두고, 라우터가 토큰마다 일부(top-k)만 골라 계산하는 구조입니다.

  1. 트랜스포머 블록에서 FFN이 자리하던 위치가 MoE 레이어로 바뀌는 지점은 Day2 트랜스포머 블록 글에서 다룹니다.
  2. 라우터(Gate network/router)는 학습되는 작은 네트워크로, 토큰이 어느 전문가로 갈지 결정합니다. 토큰은 하나 이상의 전문가(top-k)로 보내질 수 있으며, Switch Transformers는 1개의 전문가로만 라우팅합니다.
  3. [비유] 종합병원 접수처(라우터)가 증상(토큰)에 맞는 진료과(전문가) 2곳으로만 안내하는 것과 비슷합니다.
  4. ST-MoE 저자들이 다국어 설정으로 학습해 관찰한 결과, 전문가가 특정 언어나 주제별로 깔끔하게 나뉘지 않는 것으로 나타났습니다("there is no single expert specialized in any given language", Hugging Face Blog). 대신 인코더 전문가는 구두점이나 고유명사 같은 얕은 개념(shallow concepts) 단위로 특화되는 경향이 있습니다.

Dense vs MoE 차이: 연산량과 전체 파라미터 수가 왜 따로 노나?

한 줄 답: 총 파라미터는 거대하지만, 토큰당 실제 연산에 참여하는 활성 파라미터는 소수의 전문가 분량이기 때문입니다.

  1. Dense 모델에서는 모든 토큰이 모든 파라미터를 지나가므로, 파라미터 수가 곧 토큰당 연산 규모와 같습니다.
  2. MoE 모델에서는 "총 파라미터"와 "활성 파라미터"를 구분해야 하며, Sparse 모델과 Dense 모델의 파라미터 수는 직접 비교할 수 없습니다("one cannot directly compare the number of parameters", Hugging Face Blog).
  3. Hugging Face 블로그는 Mixtral 8x7B를 예로 듭니다. 8x7B가 56B가 아니라 47B 파라미터를 갖는 이유는 "only the FFN layers are treated as individual experts, and the rest of the model parameters are shared"이기 때문입니다. top-2를 쓰면 14B 파라미터가 쓰이지만, attention 등이 공유되므로 "the actual number of used parameters is 12B"입니다(Hugging Face Blog).
  4. 장비가 많고 처리량(throughput)이 높은 환경은 MoE가 유리하지만, VRAM이 적고 처리량이 낮은 환경은 Dense 모델이 더 낫습니다. 또한 Sparse 모델은 과적합에 더 취약해("Sparse models are more prone to overfitting", Hugging Face Blog) 역사적으로 파인튜닝이 어려운 편이었습니다.
구분파라미터 활용 (활성 파라미터)메모리 요구 (VRAM)토큰당 연산 속도파인튜닝/학습 특성
Dense전체 파라미터 모두 연산에 참여파라미터 수에 비례파라미터 전체만큼 계산기준
MoE (Sparse)소수의 선택된 전문가 분량만 활성화큼 (모든 전문가 상주 필요)동일 파라미터 대비 훨씬 빠름과적합(overfitting)에 더 취약

MoE가 메모리 용량·대역폭(HBM)에 주는 부담은?

한 줄 답: 계산은 일부 전문가만 하더라도 모든 전문가의 가중치를 메모리에 올려 둬야 하므로 용량 부담이 큽니다.

  1. 계산에 쓰지 않는 전문가까지 모든 파라미터를 메모리에 올려야 합니다("all parameters need to be loaded in RAM, so memory requirements are high", Hugging Face Blog).

  1. 특정 전문가로 토큰이 쏠리는 것을 막기 위해 보조 손실(load balancing auxiliary loss)을 두어 "encourage giving all experts equal importance"하도록 유도합니다(Hugging Face Blog). Expert capacity를 초과한 토큰은 잔차 연결(residual connections)로 다음 레이어로 넘어가거나 버려질 수 있으며, 전문가를 여러 장치에 나눠 두는 방식을 expert parallelism이라고 부릅니다.
  2. 연산 대비 메모리 용량이 큰 구조이다 보니 HBM 용량과 대역폭이 특히 중요해지는데, 이 메모리 바운드 관점은 Day3 메모리 바운드 글에서 다룹니다.
  3. MoE의 메모리 부담을 줄이기 위한 양자화(Quantization) 연구도 있습니다. 자세한 내용은 「양자화(Quantization)란? FP16·INT8·INT4 차이와 LLM 메모리가 줄어드는 이유」 글에서 다룹니다.
[시험용 한 줄] MoE는 FFN을 여러 전문가로 나누고 라우터를 통해 토큰을 분배하여 추론 효율을 높입니다. 단, 활성화되지 않은 가중치도 메모리에 올려야 해 메모리 용량 부담이 크고(HBM 용량·대역폭이 중요), 파인튜닝 시 과적합에 더 취약합니다.

배운 내용을 퀴즈로 확인해 볼까요?

Q1. Mixtral 8x7B는 연산할 때도 47B 파라미터 전체를 사용합니까?

A1. 아닙니다. Hugging Face 블로그에 따르면 top-2에서 실제 사용 파라미터는 12B이지만, 메모리에는 47B 전체를 올려 두어야 합니다.

Q2. MoE에서는 한국어, 영어처럼 언어마다 전담 전문가가 깔끔하게 생깁니까?

A2. 아닙니다. ST-MoE 관찰 결과 언어별 전담 전문가는 없었고, 인코더 전문가는 언어나 주제별로 깔끔하게 나뉘기보다는 구두점, 고유명사 같은 얕은 개념(shallow concepts) 단위로 나뉘는 경향이 있습니다.

Q3. 한 전문가에게 토큰이 지나치게 몰리면 어떻게 됩니까?

A3. Expert capacity(전문가 용량)를 초과한 토큰은 다음 레이어로 넘어가거나(residual connections) 버려질 수 있습니다. 이를 막기 위해 보조 손실(auxiliary loss)로 균등한 분배를 유도합니다.

핵심 요약

LLM 토큰은 모델이 텍스트를 처리하는 기본 단위이고, 컨텍스트 윈도우는 한 번의 요청에서 모델이 다룰 수 있는 토큰 수의 최대 한도입니다.

이 글은 SK하이닉스 AI 해커톤 1차 예선(10/17–18)을 준비하며 정리하는 AI 공부 노트 시리즈입니다. 지난 글에서 다룬 샘플링 방식(temperature·top-p)에 이어, 오늘은 모델이 텍스트를 다루는 가장 기본 단위인 토큰과, 그 토큰을 얼마나 많이 담을 수 있는지를 정하는 컨텍스트 윈도우를 정리합니다.

토큰이란? 단어·글자와 무엇이 다른가?

한 줄 답: LLM이 텍스트를 처리하는 단위이며, 글자·단어 조각·단어 전체·문장부호가 모두 토큰이 될 수 있습니다.

  1. 텍스트는 먼저 토큰 조각으로 쪼개지고, 각 조각은 숫자로 된 토큰 ID로 바뀝니다. 토큰 ID 다음 단계인 임베딩은 Day1 Self-Attention 글에서 다룹니다.
  2. 영어 기준 대략적인 추정치로 1토큰 ≈ 4글자 ≈ 단어 3/4개, 100토큰 ≈ 단어 75개 정도입니다. 이는 정확한 값이 아니라 추정치이며, 다른 언어는 이 관계가 다를 수 있습니다.
  3. 공백과 대소문자도 토큰 구분에 영향을 줍니다. 예를 들어 red, Red, 앞에 공백이 붙은 red는 서로 다른 토큰으로 취급됩니다.
  4. 토큰 수는 글자 수나 단어 수와 같지 않습니다. 같은 텍스트라도 모델, 인코딩, 언어에 따라 토큰 수가 달라질 수 있습니다.

토크나이저(BPE)는 문장을 어떻게 쪼개나? 한국어는 왜 토큰이 더 나올 수 있나?

한 줄 답: 자주 나오는 글자 조합은 하나의 토큰으로 묶고, 드물게 나오는 단어는 여러 조각으로 쪼개서 처리합니다.

  1. 서브워드 토큰화의 직관은 이렇습니다: 자주 등장하는 글자 묶음은 하나의 토큰으로 묶이고, 드물게 등장하는 단어는 여러 개의 작은 조각으로 쪼개집니다.
  2. BPE(Byte Pair Encoding)의 직관은 이렇습니다: 자주 붙어서 나오는 글자 쌍을 반복적으로 합쳐가며 어휘를 만듭니다.
  3. 한국어는 학습 데이터와 어휘 구성에 따라 같은 뜻이라도 영어보다 토큰이 더 많이 나올 수 있습니다. 다만 정확한 비율이나 배수는 모델과 인코딩마다 달라지므로 단정할 수 없습니다.

  1. 실제로 tiktoken으로 비교해 보면, o200k_base 인코딩 기준 "I like reading books."는 5토큰, "나는 책 읽는 것을 좋아합니다."는 8토큰으로 나옵니다. 아래는 이 비교를 직접 실행한 코드와 결과입니다.
# tiktoken으로 같은 뜻의 영어·한국어 문장과 공백/대소문자 예시를 실제로 토큰화합니다.
import importlib.metadata as md
import tiktoken

TEXTS = [
    "I like reading books.",
    "나는 책 읽는 것을 좋아합니다.",
    "red",
    "Red",
    " red",
]

def piece(enc, tid):
    b = enc.decode_single_token_bytes(tid)
    try:
        return repr(b.decode("utf-8"))
    except UnicodeDecodeError:
        return f"<bytes {b.hex(' ')}>"   # 글자 중간에서 잘린 바이트 조각

print("tiktoken", md.version("tiktoken"))
for name in ("o200k_base", "cl100k_base"):
    enc = tiktoken.get_encoding(name)
    print(f"\n== {name}")
    for t in TEXTS:
        ids = enc.encode(t)
        print(f"{t!r}: chars={len(t)} tokens={len(ids)}")
        print("  ids   :", ids)
        print("  pieces:", " | ".join(piece(enc, i) for i in ids))
tiktoken 0.14.0

== o200k_base
'I like reading books.': chars=21 tokens=5
  ids   : [40, 1299, 6085, 7187, 13]
  pieces: 'I' | ' like' | ' reading' | ' books' | '.'
'나는 책 읽는 것을 좋아합니다.': chars=17 tokens=8
  ids   : [102052, 67671, 93243, 2770, 61738, 90032, 24490, 13]
  pieces: '나는' | ' 책' | ' 읽' | '는' | ' 것을' | ' 좋아' | '합니다' | '.'
'red': chars=3 tokens=1
  ids   : [1291]
  pieces: 'red'
'Red': chars=3 tokens=1
  ids   : [7805]
  pieces: 'Red'
' red': chars=4 tokens=1
  ids   : [3592]
  pieces: ' red'

== cl100k_base
'I like reading books.': chars=21 tokens=5
  ids   : [40, 1093, 5403, 6603, 13]
  pieces: 'I' | ' like' | ' reading' | ' books' | '.'
'나는 책 읽는 것을 좋아합니다.': chars=17 tokens=15
  ids   : [61415, 16969, 3396, 109, 227, 17169, 121, 16969, 72208, 18359, 66799, 233, 54059, 61938, 13]
  pieces: '나' | '는' | <bytes 20 ec> | <bytes b1> | <bytes 85> | <bytes 20 ec 9d> | <bytes bd> | '는' | ' 것' | '을' | <bytes 20 ec a2> | <bytes 8b> | '아' | '합니다' | '.'
'red': chars=3 tokens=1
  ids   : [1171]
  pieces: 'red'
'Red': chars=3 tokens=1
  ids   : [6161]
  pieces: 'Red'
' red': chars=4 tokens=1
  ids   : [2579]
  pieces: ' red'

같은 "나는 책 읽는 것을 좋아합니다."라는 문장도 인코딩에 따라 o200k_base는 8토큰, cl100k_base는 15토큰으로 다르게 나옵니다. cl100k_base에서는 일부 토큰이 한글 한 글자를 이루는 UTF-8 바이트 조각 단위로 더 잘게 쪼개지기 때문입니다. 이 숫자는 일반화된 비율이 아니라 이번에 실행한 이 문장 하나의 예시로만 참고해야 합니다. 참고로 tiktoken.encoding_for_model("gpt-4o")는 o200k_base를, tiktoken.encoding_for_model("gpt-4")는 cl100k_base를 반환합니다.

컨텍스트 윈도우란? 입력+출력 토큰 한도는 언제 문제가 되나?

한 줄 답: 한 요청에서 모델이 볼 수 있는 토큰의 총량 한도이며, 출력 한도는 따로 정해집니다.

  1. 긴 문서를 통째로 넣거나 대화 기록이 계속 쌓이면 컨텍스트 윈도우 한도를 초과할 수 있습니다. 추론(reasoning) 모델의 경우 화면에 보이지 않는 추론 토큰도 출력 쪽 공간을 차지한다는 점에 유의해야 합니다.
  2. 한도에 걸릴 때의 대처법은 다음과 같습니다: 내용을 줄이거나 다시 표현하기, 불필요하거나 중복된 컨텍스트 제거하기, 여러 부분으로 나누어 보내기, 요약·전처리 후 보내기입니다(출처: OpenAI Help: Understanding and counting tokens).
  3. 시스템 프롬프트와 대화 이력을 컨텍스트 윈도우 안에 어떻게 배치하는지는 Day4 프롬프트 엔지니어링 글에서 다룹니다.
  4. 대화가 길어질수록 KV Cache 등 메모리 부담도 함께 커진다는 점은 Day3 KV Cache 글에서 다룹니다.
[시험용 한 줄] 토큰 = 텍스트 처리 단위(단어/글자 아님); 컨텍스트 윈도우 = 1회 요청 시 처리 한도. 넘치면 줄이기·반복 제거·나누기·요약으로 대처하고, 보이지 않는 추론 토큰도 공간을 차지합니다.

배운 내용을 퀴즈로 확인해 볼까요?

Q1. 영어 단어 100개와 한국어 단어 100개는 항상 같은 수의 토큰이 됩니까?

A1. 그렇지 않습니다. 토큰 수는 언어와 모델의 인코딩에 따라 달라질 수 있습니다.

Q2. red, Red, red는 모두 같은 토큰 ID를 가집니까?

A2. 아닙니다. 위 실행 결과에서 세 문자열은 각각 1토큰이지만 토큰 ID가 모두 다릅니다(o200k_base 기준 1291, 7805, 3592).

Q3. 추론 모델에서 화면에 보이는 답은 짧은데 출력 토큰 사용량이 더 많게 나오는 이유는 무엇입니까?

A3. 화면에 보이지 않는 추론 토큰도 출력 사용량에 포함되기 때문입니다(OpenAI Help 문서 기준).

+ Recent posts