무료 LLM API 리스트 (Last updated 2026.09)

어디까지가 진짜 공짜인가

돈 안 쓰고 LLM API를 붙여 보고 싶은 사람용 목록이다. “무료”라는 말에 끝까지 공짜인 것크레딧 다 쓰면 청구되는 것이 섞여 있어서 표를 아예 둘로 갈랐다. 급하면 이것만 — 끝까지 공짜로 굴릴 거면 Google Gemini나 OpenRouter, 임베딩·검색이면 Cohere. Cerebras 와 Together AI 는 무료로 적혀 있어도 아니다.

기준은 뭐 개인 사이드 프로젝트 수준이다.

마지막 확인: 2026-09-07 — 아래 값은 전부 이 날짜에 각 제공사 공식 문서에서 다시 확인했습니다.

1. 항상 무료

한도를 넘으면 요청이 거절될 뿐, 청구서는 오지 않는다. 여기서 시작하면 돈 나갈 일이 없다.

# 제공사 키 받는 곳 무료로 할 수 있는 것
1 Google Gemini AI Studio Flash 계열부터 2.5 Pro까지 무료 티어가 붙는다. 공짜로 만질 수 있는 모델 폭이 가장 넓어 주력으로 삼을 만하다
2 OpenRouter API Keys 무료 모델 20 RPM, 하루 50건. 크레딧 10달러면 하루 1,000건. 키 하나로 여러 모델을 갈아 끼운다
3 Cloudflare Workers AI 시작 가이드 하루 10,000 Neurons, 매일 00:00 UTC 리셋. 일부 모델은 결제수단을 요구
4 Cohere Dashboard 평가용 키로 월 1,000콜. 임베딩 분당 2,000 inputs라 검색·리랭크에 좋다
5 Groq Console 무료 티어에서 그대로 빠른 응답을 쓴다. 한도는 문서가 어긋나 있어 응답 헤더로 확인한다

2. 항상 무료가 아닌 곳 — 부분 유료

영어권 목록이 위 표와 이 표를 한 덩어리로 묶어 놓는 경우가 많다.

# 제공사 무료의 종류 키 받는 곳 알고 가야 할 것
6 Mistral 월 갱신 크레딧 Quickstart Free 플랜에 월 10달러 크레딧. 카드 불필요라고 문서가 명시한다
7 Hugging Face 월 갱신 크레딧 Tokens 무료 계정 월 0.10달러. 리셋되지만 실용 한도는 거의 없다
8 SambaNova 확인 필요 Cloud 공식 문서와 요금제 페이지가 서로 어긋난다
9 Cerebras 1회성 체험 크레딧 Cloud 카드를 등록해야 5달러가 나오고 30일 뒤 소멸. 영구 무료 티어는 없다고 FAQ가 답한다
10 Together AI 무료 아님 API Keys 무료 체험 자체가 없다. 최소 5달러 선불
11 NVIDIA NIM 미검증 build.nvidia.com 사이트가 자바스크립트로만 그려져 무료 조건을 못 읽었다
12 GitHub Models 종료됨 종료 공지 2026년 7월 30일 완전 종료. 지금 호출하면 410이 온다

무료라고 다 같은 무료가 아님.

무료 LLM API의 세 가지 종류를 항상 무료, 월 갱신 크레딧, 1회성 체험 크레딧으로 나눈 판단 흐름도. 각 갈래마다 카드 요구 여부와 소진 후 결과, 대표 제공사가 적혀 있다.

갈림길은 하나다. 소진된 뒤에 청구서가 오는가. 공짜라는 말만 보고 줄을 세우면 맨 오른쪽 갈래가 섞여 들어온다. 영어권 목록 상당수가 Cerebras를 “free tier”로 묶어 놓는 게 딱 그 경우다.

한국에서 막히는 곳은 없었다

한국 가정용 회선(LG U+, 고양시)에서 위 제공사 엔드포인트에 키 없이 요청을 던져 봤다. 지역 차단으로 막힌 곳은 하나도 없었다. 오히려 한국이 유리하다. Gemini 약관은 유럽경제지역·스위스·영국 사용자에게 API를 제공할 때 유료 서비스만 쓰라고 못박아 뒀는데, 한국은 지원 지역 목록에 그냥 들어 있어 이 제약에 안 걸린다.

다만 호출이 도달한다고 가입까지 된다는 뜻은 아니다. 지역 제한은 보통 가입·결제 단계에 걸리는데, 나는 계정이 없어 SMS 인증이나 해외 카드 요구 여부는 확인하지 못했다.

국산 모델은 지금 어디까지 ?

영어권 목록에는 아예 없는 칸이다. 키만 받아서 바로 부를 수 있는 국산 LLM은 두 곳뿐이고, 둘 다 항상 무료가 아니다.

# 제공사 지금 키 받나 무료인가 바로가기 메모
1 CLOVA Studio (HyperCLOVA X) 가능 아니오 (종량제) 콘솔 신청만으로 이용, 토큰 사용량만큼 과금. 한국어와 국내 리전이 강점
2 Upstage Solar 가능 미확인 API Keys solar-mini부터 solar-pro-4까지. 무료 크레딧 유무는 미확인
3 FriendliAI 가능 아니오 Suite 서버리스 7개 전부 종량제, 0원짜리 0개
4 LG EXAONE 불가 오픈웨이트만 모델 받기 한 달 한시 무료가 2026년 1월 28일에 끝났다
5 KT 믿:음 (Mi:dm) 불가 오픈웨이트만 모델 받기 믿:음 K 2.0은 MIT 라이선스라 상업 이용까지 된다
6 SKT A.X 불가 제공 방식 심사로 대상을 선정하는 방식이라 개인 창구가 아니다
7 Kakao Kanana 불가 API 문서 초청제 베타 신청 페이지가 닫혔다

행을 하나도 안 비웠다. “안 된다”도 답이기 때문이다.

국내 요금을 확인하려다 한 번 제대로 막혔다. 네이버 클라우드 가이드 문서는 브라우저로는 멀쩡히 열리는데 curl에는 문을 안 열어 준다. 우회해도 요금표가 자바스크립트로 채워지는 구조라 로그인 없이는 숫자 자체가 없었다. Upstage도 똑같았다. 그래서 “미확인”으로 뒀다.

언제까지 쓸수있을까?

무료 티어는 반드시 죽는다. 다시 올 때 이 다섯 가지만 의심하면 된다.

  1. 무료 티어에 카드 등록이 붙는다 (Cerebras 형)
  2. 무료 체험이 사라지고 최소 선불이 생긴다 (Together AI 형)
  3. 특정 모델만 조용히 유료로 빠진다 (Cloudflare 형)
  4. 서비스가 아예 종료된다 — GitHub Models가 2026년 7월 30일에 이렇게 사라졌다
  5. 한시 무료가 기한이 되면 닫힌다 — K-EXAONE이 2026년 1월 28일에 이렇게 끝났다. “출시 기념 무료”는 무료 티어가 아니다

그래서 판정 기준 하나. 아직 GitHub Models를 무료로 올려둔 목록이라면 최소 한 달 이상 묵은 것이다.

한도가 모자라거나 공짜로 쓰겠다면 선택지는 내 GPU다. 그쪽은 로컬 LLM 환경 구축에 정리해 뒀다. 아직 무료 한도로 버틸 만하다면, 아래에 네 곳만 골라 예제 코드까지 붙여 뒀다.

무료 LLM API 추천

위 표에서 항상 무료 + 한국에서 도달 확인 둘을 다 만족하는 곳만 남기고, 용도별로 하나씩 골랐다.

  • Google Gemini — 범용 대화. 무료 티어가 붙는 모델 폭이 제일 넓어서 공짜로 굴릴 수 있는 범위가 가장 크다. 하나만 고르라면 여기다.
  • OpenRouter — 모델 갈아 끼우기. 키 하나로 여러 회사 모델을 돌려 볼 수 있어서, 어느 모델이 내 작업에 맞는지 아직 모를 때 가장 싸게 헤맬 수 있다.
  • Groq — 응답이 빨라야 할 때. 대화형 UI처럼 첫 토큰이 늦으면 티가 나는 쪽에 붙인다.
  • Cohere — 임베딩·검색. 채팅용으로 고른 게 아니다. RAG 인덱스를 만들어 보려는데 임베딩 비용이 부담될 때 쓴다.

아래 코드는 전부 각 제공사 공식 문서의 현행 예제를 근거로 썼다. 앞에서 밝혔듯 나는 어느 제공사에도 계정이 없어서 직접 실행해 보지는 못했다. 그래서 응답 예시를 지어내지 않고, 참고한 문서를 항목마다 링크로 달아 뒀다.

예제는 전부 파이썬이다. 키는 코드에 적지 말고 터미널에서 이렇게 넣어 둔다.

export GEMINI_API_KEY="여기에_발급받은_키"

다른 곳도 이름만 다르고 방식은 같다. 어떤 이름을 읽는지는 아래 예제마다 코드에 그대로 적혀 있다. 네 예제 모두 골격이 같다 — 키 읽기 → 클라이언트 만들기 → 물어보기 → 출력.

범용 대화라면 Google Gemini

공식 SDK를 쓴다. genai.Client()GEMINI_API_KEY 환경변수를 알아서 읽으므로 인자로 키를 넘길 필요가 없다.

# pip install google-genai
from google import genai

client = genai.Client()  # GEMINI_API_KEY 를 알아서 읽는다
answer = client.interactions.create(
    model="gemini-3.8-flash", input="좀비 프로세스가 왜 생기지?")
print(answer.output_text)

문서의 REST 예제에는 Api-Revision: 2026-05-20 헤더가 붙는다. API 동작을 그 날짜 버전에 고정하는 헤더라, 없으면 늘 최신 동작을 따라가서 구글이 응답 형식을 바꾸는 날 코드가 조용히 깨진다. 다만 파이썬 SDK 에서 이걸 어떻게 넘기는지는 문서에서 확인하지 못했다 — 문서의 파이썬 예제도 genai.Client() 만 쓰고 리비전을 따로 지정하지 않는다. 버전을 못 박아야 하는 상황이라면 REST 로 직접 부르는 쪽이 확실하다.

근거: Gemini API 퀵스타트

여러 모델을 갈아 끼우려면 OpenRouter

여기는 모델 id를 먼저 골라야 한다. 무료 모델 목록은 수시로 바뀌니 지금 살아 있는 것부터 뽑는다. 이 코드는 키가 없어도 돈다.

# pip install requests
import requests

models = requests.get("https://openrouter.ai/api/v1/models").json()["data"]
for model in models:
    if model["id"].endswith(":free"):
        print(model["id"])

거기서 나온 id 하나를 그대로 끼우면 된다. 호출은 OpenAI SDK를 그대로 쓰고 base_url 만 갈아 끼운다.

# pip install openai
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://openrouter.ai/api/v1",
    api_key=os.environ["OPENROUTER_API_KEY"])
answer = client.chat.completions.create(
    model="google/gemma-4-31b-it:free",
    messages=[{"role": "user", "content": "한 줄로 자기소개 해줘"}])
print(answer.choices[0].message.content)

근거: OpenRouter 퀵스타트

속도가 필요하면 Groq

엔드포인트 경로부터 OpenAI 호환이라 전용 SDK든 OpenAI SDK든 둘 다 붙는다. 문서 예제대로 전용 SDK를 썼다.

# pip install groq
import os
from groq import Groq

client = Groq(api_key=os.environ["GROQ_API_KEY"])
answer = client.chat.completions.create(
    model="llama-3.3-70b-versatile",
    messages=[{"role": "user", "content": "빠른 모델이 왜 중요한지 짧게"}])
print(answer.choices[0].message.content)

내 한도는 응답 헤더x-ratelimit- 로 시작해서 실려 온다. 문서의 한도 표보다 이쪽이 정확하다.

근거: Groq 퀵스타트

임베딩·검색이면 Cohere

문장을 벡터로 바꿔 준다. 검색·중복 제거·RAG 인덱스를 붙여 볼 때 쓴다.

# pip install cohere
import os
import cohere

client = cohere.ClientV2(api_key=os.environ["CO_API_KEY"])
answer = client.embed(
    texts=["좀비 프로세스가 왜 생기지?"], model="embed-v4.0",
    input_type="search_document", embedding_types=["float"])
print(answer)

근거: Cohere Embed 레퍼런스

실제로 제일 쓸모 있는 건 base_url 하나

고른 네 곳이 전부 OpenAI 호환 엔드포인트를 준다. 그러니까 파이썬 코드는 하나만 짜 두고 base_url 과 모델명만 갈아 끼우면 제공사를 갈아탈 수 있다. 한 곳이 무료 티어를 닫아도 두 줄만 고치면 된다는 뜻이다.

제공사 base_url 예제 모델 환경변수
Google Gemini https://generativelanguage.googleapis.com/v1beta/openai/ gemini-3.8-flash GEMINI_API_KEY
Groq https://api.groq.com/openai/v1 llama-3.3-70b-versatile GROQ_API_KEY
OpenRouter https://openrouter.ai/api/v1 위 목록에서 고른 :free 모델 OPENROUTER_API_KEY
Cohere https://api.cohere.ai/compatibility/v1 command-a-plus-05-2026 CO_API_KEY
# pip install openai
import os
from openai import OpenAI

# base_url 과 model 을 위 표의 다른 줄로 바꾸면 제공사가 바뀐다
client = OpenAI(
    base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
                api_key=os.environ["GEMINI_API_KEY"])
answer = client.chat.completions.create(
    model="gemini-3.8-flash",
    messages=[{"role": "user", "content": "한 줄로 인사해줘"}])
print(answer.choices[0].message.content)

Cohere 는 자체 스키마(/v2/embed)와 호환 계층 둘 다 있다. 호환 쪽은 채팅·임베딩·오디오 전사를 받는다. 그런데도 앞의 임베딩 예제를 자체 스키마로 쓴 이유가 있다. 문서가 호환 계층에서 못 쓰는 것을 따로 열거해 두기 때문이다connectors·documents·citation_options 같은 Cohere 고유 파라미터는 호환 쪽에 없다. 그러니 갈아 끼우기용으로는 호환 base_url 을, 고유 기능이 필요하면 자체 API 를 쓰면 된다.

근거: Gemini의 OpenAI 호환 엔드포인트 · Cohere 호환 API

목록 만들다 나한테 되물었던 것들

무료라고 적혀 있는데 왜 돈이 나가지?

그건 항상 무료가 아니라 1회성 체험 크레딧이기 때문입니다. 크레딧이 소진되거나 기한이 지나면 그때부터는 그냥 유료 API입니다. 시작 단계에서 결제수단부터 요구한다면 그게 구분 신호이니, 위 두 번째 표를 먼저 확인하세요.

국산 모델은 왜 무료로 못 쓰지?

넷 다 이유가 다릅니다. 위 표의 메모 열에 하나씩 적어 뒀는데, 공통점은 키만 받아서 바로 부를 창구가 없다는 겁니다. 가중치를 받아 로컬에서 돌리는 쪽이 현실적입니다.

참고 자료

로컬 LLM 환경 구축: Ollama·llama.cpp 서버까지

이 글을 따라 하면 되는 것과, 내 환경

예전에 로컬 LLM 환경 구축을 주제로 글을 하나 썼는데 “무슨 말인지 모르겠다”는 말을 들었다. 다시 열어 보니 맞는 지적이었다. 메모리 예산이 어떻고 KV 캐시가 어떻고 이론만 늘어놨지, 정작 독자가 복사해서 칠 덩어리가 없었다. 그래서 갈아엎었다. 이 글은 위에서부터 순서대로 따라 치면 8GB 짜리 그래픽카드에서 로컬 LLM 이 뜨는 걸 목표로 한다.

로컬 LLM 환경 구축 4단계 흐름도. 1 설치(ollama
version), 2 모델 받기(ollama pull llama3.1:8b), 3 확인(ollama ps 의 PROCESSOR 가 100% GPU 인지), 4 조정(num_ctx 와 KV 캐시 q8_0) 순서로 이어지고, 맨 아래에 막히는 지점은 늘 3번이라는 경고가 붙어 있는 다이어그램

명령어는 환경을 탄다. 내 쪽 조건부터 밝힌다. 아래 나오는 출력은 전부 이 머신에서 나온 값이다.

항목
GPU NVIDIA GeForce RTX 4060 Ti, VRAM 8188 MiB
드라이버 591.86 (Windows 쪽), CUDA 13.1
OS Windows 위의 WSL2, Ubuntu 24.04.1
RAM / CPU 15Gi (WSL 안에서 보이는 값), Ryzen 5 7500F, nproc 12
Ollama 0.33.3 (2026년 9월 기준 최신)
모델 llama3.1:8b (Q4_K_M, 4.9GB)

리눅스 네이티브도 흐름은 같고, macOS(Apple Silicon)는 Ollama 가 Metal 을 알아서 잡으니 설치 방법만 다르다.

어디까지 내가 직접 돌렸는지도 먼저 갈라 둔다. 명령어 글에서 이건 정확성 문제라고 생각한다.

  • 직접 실행함: 하드웨어 점검, Ollama 수동 설치, 서버 기동, 모델 다운로드, 대화, ollama ps, nvidia-smi, 컨텍스트 4096·8192·16384·32768 비교, KV 캐시 양자화, 사용자 systemd 서비스 등록, /api/generate 와 OpenAI 호환 /v1/chat/completions 호출. 아래 붙인 출력은 잘라낸 부분을 빼고 그때 나온 그대로다.
  • 직접 실행 못 함: ① llama.cpp 소스 빌드 — 이 머신에 nvcc 가 없어서 CUDA 빌드를 검증할 수 없었다. ② 루트 권한이 필요한 경로 전부 — 공식 설치 스크립트(curl | sh), 그게 만드는 ollama.service, sudo systemctl edit ollama. 사용자 서비스로 같은 구조를 만들어 확인하긴 했지만 루트 서비스 그 자체는 아니다. ③ .wslconfig 메모리 상향. 이 셋은 공식 문서와 설치 스크립트 내용을 정리한 것이고, 본문에서도 그때그때 표시해 뒀다.

0단계 — 내 카드에 뭐가 들어가나 (표 하나로 끝)

로컬 LLM 환경 구축에서 성패를 가르는 건 하나다. 모델이 메모리에 들어가느냐. 유도 과정은 접고 쓸 수 있는 형태로만 남긴다.

가중치가 먹는 메모리는 사실상 GGUF 파일 크기 그대로다. llama.cpp 가 파일을 mmap 으로 올리기 때문이다. 그래서 파일 크기표 하나면 된다. 아래는 Hugging Face 에 실제로 올라와 있는 파일 크기(GiB)를 옮긴 것이다(2026년 9월 확인).

모델 FP16/BF16 Q8_0 Q4_K_M Q4_K_M 이 편하게 들어가는 곳
Llama 3.2 3B 5.99 3.19 1.88 거의 모든 GPU, 8GB RAM 노트북
Llama 3.1 8B 14.96 7.95 4.58 8GB VRAM, CPU 16GB RAM
Qwen3 14B 27.51 14.62 8.38 12~16GB VRAM, CPU 32GB RAM
Qwen3 32B 61.03 32.43 18.40 24GB VRAM, CPU 64GB RAM

규칙은 두 줄이면 된다.

  • VRAM 에서 1.5GB 를 빼고, 남는 자리에 들어가는 Q4_K_M 을 고른다. 뺀 1.5GB 는 KV 캐시와 작업 버퍼 몫이다.
  • 컨텍스트를 8K 넘게 쓸 거면 1GB 씩 더 뺀다. 8B 모델은 컨텍스트 8,192 토큰마다 KV 캐시가 약 1GiB 붙는다.

내 8188MiB 카드면 8188 − 1500 ≈ 6.7GB 니까 8B Q4_K_M(4.58GiB)이 상한선이다. 14B 는 Q4_K_M 만으로 8.38GiB 라 가중치부터 넘친다. “8GB 카드엔 8B Q4 까지”라는 흔한 경험칙이 여기서 나온다.

자기 머신을 재 보자. 설치 전에 쳐도 되는 명령이다.

nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv
free -h
nproc
lscpu | grep -E 'Model name'
grep -o -w -E 'avx2|avx512f' /proc/cpuinfo | sort -u

내 머신에서는 이렇게 나온다. GPU 이름과 VRAM 총량, 램, 스레드 수, CPU 의 SIMD 지원 여부가 차례로 찍히면 정상이다.

WSL2에서 nvidia-smi, free -h, nproc, lscpu, /proc/cpuinfo 를 실행한 터미널 출력. RTX 4060 Ti 8188 MiB 드라이버 591.86, 메모리 total 15Gi, nproc 12, AMD Ryzen 5 7500F 6-Core, avx2 와 avx512f 지원이 차례로 표시되어 있다

free -h 의 total 이 32GB 머신인데 15Gi 로 나오는 건 램이 빠진 게 아니다. 원인과 조치는 맨 아래 ## 막혔을 때 에 적어 뒀다. GPU 만 쓸 거면 지금은 그냥 넘어가도 된다.

표를 보고 내 카드가 버거워 보이면, 굳이 여기서 붙들 필요는 없다. 무료로 쓸 수 있는 API 를 먼저 붙여 보는 쪽이 빠르다. 어디가 끝까지 공짜인지는 무료 LLM API 리스트에 정리해 뒀다. 반대로 한도가 모자라거나 데이터를 밖으로 안 보내고 싶어서 온 거라면, 그대로 아래로 내려가면 된다.

1단계 — Ollama 설치

sudo 를 쓸 수 있으면 한 줄이다. 공식 문서가 안내하는 방법이고, systemd 서비스까지 등록해 준다.

curl -fsSL https://ollama.com/install.sh | sh

나는 이 머신에서 sudo 비밀번호를 넣을 수 없어서 이 한 줄을 쓰지 못했다. 그래서 루트 없이 홈 디렉터리에 까는 경로로 갔다. 같은 처지면 아래를 그대로 따라 하면 된다. 결과물은 똑같이 동작한다.

먼저 배포 파일을 푸는 데 필요한 zstd 를 파이썬으로 대신한다. zstandard 는 pip 로 깔리니 루트가 필요 없다.

pip install zstandard

cat > unzstd.py <<'PY'
import sys, zstandard
zstandard.ZstdDecompressor().copy_stream(
    sys.stdin.buffer, sys.stdout.buffer, read_size=1<<20, write_size=1<<20)
PY

이제 내려받으면서 바로 풀어 홈 디렉터리에 꽂는다. 1.4GB 라 몇 분 걸린다.

mkdir -p ~/ollama-local
curl -fL --silent --show-error https://ollama.com/download/ollama-linux-amd64.tar.zst \
  | python unzstd.py \
  | tar -xf - -C ~/ollama-local

경로를 잡고 버전을 확인한다.

export PATH="$HOME/ollama-local/bin:$PATH"   # .bashrc 에도 넣어 두면 편하다
ollama --version

이렇게 나오면 설치 성공이다.

Warning: could not connect to a running Ollama instance
Warning: client version is 0.33.3

경고 두 줄이 떠서 실패한 줄 알기 쉬운데 아니다. 위 줄은 서버가 아직 안 떠 있어서 나는 경고고, 아래 줄에 client version 이 찍혔다는 게 바이너리가 제대로 깔렸다는 뜻이다. 수동 설치는 systemd 서비스를 등록하지 않으니 서버는 다음 단계에서 손으로 띄운다.

WSL2 사용자 주의. GPU 를 붙이겠다고 WSL 안에 nvidia-driver-XXX 같은 리눅스 디스플레이 드라이버를 깔면 안 된다. NVIDIA 의 CUDA on WSL 가이드가 못박아 둔 사항이다. WSL2 는 Windows 쪽 드라이버가 libcuda.so 로 자동 매핑되는 구조라, 안에서 드라이버를 깔면 그 매핑을 덮어써서 GPU 인식이 통째로 깨진다. 리눅스에 익숙할수록 반사적으로 손이 가는 자리다.

2단계 — 서버 띄우고 GPU 를 잡았는지 확인

여기서부터는 1단계에서 어느 길로 갔느냐에 따라 명령이 갈린다. 로그가 어디 쌓이는지가 다르기 때문이다.

(a) 공식 스크립트로 깐 경우 — systemd 가 ollama.service 를 이미 띄워 뒀다. 따로 실행할 게 없고, 로그는 파일이 아니라 저널에 쌓인다.

systemctl status ollama
curl -s http://localhost:11434/api/version

(b) 수동 설치한 경우 — 서비스가 없으니 직접 띄운다.

nohup ollama serve > ~/ollama-serve.log 2>&1 &
curl -s http://localhost:11434/api/version

어느 쪽이든 {"version":"0.33.3"} 가 돌아오면 서버는 산 거다.

그리고 로그를 꼭 한 번 보고 가자. GPU 를 잡았는지가 두 줄에 다 들어 있다. 여길 건너뛰면 나중에 “왜 느리지”로 돌아온다. 보는 명령은 설치 방식에 따라 다르다.

# (a) 공식 스크립트 / systemd
journalctl -u ollama -n 200 --no-pager | grep -E "inference compute|vram-based"

# (b) 수동 설치 (로그 파일)
grep -E "inference compute|vram-based" ~/ollama-serve.log

내 머신 출력이다. 한 줄이 너무 길어서 앞쪽 타임스탬프는 잘라내고 줄바꿈을 넣었다.

source=types.go:32 msg="inference compute" id=0 library=CUDA compute=8.9
  name=CUDA0 description="NVIDIA GeForce RTX 4060 Ti" libdirs=ollama,cuda_v13
  driver=13.1 type=discrete total="8.0 GiB" available="6.9 GiB"
source=routes.go:2062 msg="vram-based default context"
  total_vram="8.0 GiB" default_num_ctx=4096

읽는 법 세 가지.

  • library=CUDA — GPU 백엔드를 잡았다. 여기가 library=CPU 면 아래 단계는 다 해도 소용없다. 드라이버부터 다시 본다.
  • available="6.9 GiB" — 총 8.0GiB 중 실제로 쓸 수 있는 자리. 윈도우 데스크톱이 이미 얼마를 먹고 있어서 8GB 전부가 내 것이 아니다. 0단계에서 1.5GB 를 빼라고 한 게 이거다.
  • default_num_ctx=4096 — Ollama 가 VRAM 을 보고 정한 기본 컨텍스트. 8GB 카드에선 4,096 토큰이다. 긴 문서를 넣으면 이 창을 넘는 부분이 에러 없이 조용히 잘려 나간다.

밝혀 둘 게 있다. 나는 sudo 가 없어서 (b) 수동 설치 쪽만 직접 돌렸다. (a) 의 systemctl status ollamajournalctl -u ollama 는 루트로 깐 서비스가 있어야 확인되는 명령이라 이 머신에서 그대로 재현하지 못했다. 다만 같은 바이너리를 사용자 서비스로 등록해서 journalctl --user -u ollama 로 위와 똑같은 두 줄이 나오는 것까지는 확인했다(6단계에 그 방법을 적어 뒀다). 저널에서 읽는 내용은 같고, 다른 건 서비스를 누가 소유하느냐뿐이다.

3단계 — 모델 받고 첫 대화

0단계 표대로 8GB 카드엔 8B Q4 가 상한이니 그걸로 간다. 4.9GB 다운로드다.

ollama pull llama3.1:8b
ollama list

ollama list 가 이렇게 답하면 받아진 것이다. 태그를 생략하면 latest 가 붙고, llama3.1:8b 는 기본 양자화가 Q4_K_M 이다.

NAME           ID              SIZE      MODIFIED
llama3.1:8b    46e0c10c039e    4.9 GB    6 seconds ago

대화는 ollama run 이다. 뒤에 문장을 붙이면 한 번 답하고 빠져나오고, 안 붙이면 대화창으로 들어간다. --verbose 를 붙이면 답 끝에 속도가 붙는다.

ollama run llama3.1:8b --verbose "GGUF 가 뭔지 두 문장으로 설명해줘"
Ollama 설치부터 첫 대화까지의 터미널 출력. ollama
version 이 client version 0.33.3 을 찍고, api/version 이 0.33.3 을 돌려주고, ollama list 에 llama3.1:8b 4.9 GB 가 보이며, ollama run

내 화면에 찍힌 그대로 옮기면 이렇다.

GGUF은 한국의 게임 개발사로, 주로 PC 온라인 게임을 개발하고 있습니다.
GGUF의 대표작으로는 '신사협회'와 '던전앤파이터'가 있습니다.

total duration:       4.580796531s
load duration:        3.509825677s
prompt eval count:    24 token(s)
prompt eval duration: 109.436ms
prompt eval rate:     219.31 tokens/s
eval count:           47 token(s)
eval duration:        958.612ms
eval rate:            49.03 tokens/s

답은 보다시피 완전히 틀렸다. GGUF 는 게임 회사가 아니라 llama.cpp 계열이 쓰는 모델 파일 포맷이다. 8B 짜리를 로컬에 띄우면 이런 게 그냥 나온다는 것도 같이 알고 가는 게 좋다. 지금 확인할 건 답의 품질이 아니라 맨 아래 숫자가 찍히느냐다.

봐야 할 줄은 두 개다.

  • eval rate: 49.03 tokens/s생성 속도다. 체감을 결정하는 값이고, 아래 단계에서 계속 이 숫자를 본다.
  • load duration: 3.5s — 모델을 VRAM 에 올리는 데 걸린 시간. 처음 한 번만 나오고, 5분 안에 다시 부르면 0에 가깝게 떨어진다.

prompt eval rate 는 프롬프트를 읽는 속도라 입력이 짧으면 표본이 몇 토큰 안 돼서 들쭉날쭉하다. 여기선 24토큰짜리라 참고만 하면 된다.

4단계 — GPU 에 다 올라갔는지 확인

모델이 GPU 에 전부 올라갔는지는 nvidia-smi 가 아니라 ollama ps 로 본다. 모델은 마지막 요청 뒤 5분간 메모리에 남으니, 방금 대화했다면 바로 치면 된다.

ollama ps
nvidia-smi --query-gpu=memory.used,memory.total --format=csv
NAME           ID              SIZE      PROCESSOR    CONTEXT    UNTIL
llama3.1:8b    46e0c10c039e    5.3 GB    100% GPU     4096       4 minutes from now

memory.used [MiB], memory.total [MiB]
6022 MiB, 8188 MiB

PROCESSOR 열이 100% GPU 면 전부 VRAM 에 올라간 것이다. 여기가 34%/66% CPU/GPU 처럼 쪼개져 나오면 오프로딩이 일어난 거고, 그 순간 속도가 반토막 난다. 다음 단계에서 일부러 재현해 본다.

5단계 — 컨텍스트를 8배 올려 느려지는 걸 눈으로 보기

모델도 그대로, 양자화도 그대로 두고 컨텍스트 길이만 4096 에서 32768 로 올린다. num_ctx 는 API 요청의 options 로 넘기면 되고, 그러면 Ollama 가 모델을 그 컨텍스트로 다시 적재한다.

curl -s http://localhost:11434/api/generate -d '{
  "model": "llama3.1:8b",
  "prompt": "한 문장으로 답해줘: 캐시 미스는 왜 느린가?",
  "stream": false,
  "options": { "num_ctx": 32768 }
}' | python3 -c 'import json,sys; print(json.load(sys.stdin)["response"])'

그리고 다시 ollama ps 를 친다.

ollama ps
NAME           ID              SIZE      PROCESSOR          CONTEXT    UNTIL
llama3.1:8b    46e0c10c039e    9.5 GB    34%/66% CPU/GPU    32768      4 minutes from now
기본 컨텍스트 4096 과 num_ctx 32768 의 ollama ps 및 nvidia-smi 출력을 위아래로 비교한 터미널 화면. 위쪽은 SIZE 5.3 GB, PROCESSOR 100% GPU, CONTEXT 4096, VRAM 6022 MiB 8188 MiB 이고 아래쪽은 SIZE 9.5 GB, PROCESSOR 34%/66% CPU/GPU, CONTEXT 32768, VRAM 6952 MiB 8188 MiB 이며 맨 아래에 생성 속도가 49.16 tok/s 에서 20.82 tok/s 로 떨어졌다고 적혀 있다

같은 모델, 같은 파일, 같은 카드에서 잰 값을 나란히 놓으면 이렇다.

측정값 기본 (num_ctx 4096) num_ctx 32768
ollama ps SIZE 5.3 GB 9.5 GB
PROCESSOR 100% GPU 34%/66% CPU/GPU
nvidia-smi 사용량 6022 MiB / 8188 MiB 6952 MiB / 8188 MiB
생성 속도 (eval rate) 49.16 tok/s 20.82 tok/s

컨텍스트를 8배로 올렸더니 모델이 요구하는 메모리가 뛰었고, 8188MiB 카드에 그게 들어갈 리 없으니 Ollama 가 일부 레이어를 CPU 로 내려버렸다. 생성 속도는 그만큼 주저앉았다.

그런데 nvidia-smi 는 이 사고를 거의 안 보여준다. VRAM 사용량은 오프로딩 전후로 별로 안 변한다. 당연하다. GPU 가 꽉 차서 나머지를 시스템 RAM 으로 흘린 거니까. 그래서 VRAM 숫자만 쳐다보고 있으면 “아직 자리 남았는데 왜 느리지” 하면서 엉뚱한 데를 파게 된다. 오프로딩 판정은 ollama ps 의 PROCESSOR 열로 한다.

범인은 KV 캐시다. 지금까지 처리한 모든 토큰의 Key/Value 벡터를 레이어마다 쌓아 두는 공간이라 컨텍스트 길이에 정비례해서 커진다. Llama 3.1 8B(레이어 32, KV 헤드 8, 헤드 차원 128, FP16)면 토큰당 128KiB 니까 8K 는 1GiB, 32K 는 4GiB 다. 가중치 4.58GiB 에 4GiB 를 얹으면 8GB 카드에 들어갈 방법이 없다. 위 표에서 벌어진 일이 그거 전부다.

6단계 — 좁은 VRAM 에서 되돌리는 세 가지 손잡이

컨텍스트는 필요한데 카드는 8GB 다. 순서대로 이 세 개를 만진다.

(1) 서버 기본값을 필요한 만큼만 올린다. 8GB 카드에서 8,192 는 아직 안전한 값이다.

pkill -x ollama                       # 돌고 있던 서버를 내린다
OLLAMA_CONTEXT_LENGTH=8192 nohup ollama serve > ~/ollama-serve.log 2>&1 &

ollama ps 로 확인하면 요청에 num_ctx 를 안 줘도 CONTEXT 가 8192 로 잡히고, 적재 크기는 5.3GB 에서 5.8GB 로 0.5GB 늘었을 뿐 여전히 전부 GPU 다.

NAME           ID              SIZE      PROCESSOR    CONTEXT
llama3.1:8b    46e0c10c039e    5.8 GB    100% GPU     8192

(2) Modelfile 로 용도별 모델을 굽는다. 컨텍스트와 시스템 프롬프트를 같이 박아 둘 수 있어서 “사내 코드 리뷰어” 같은 걸 만들 때 편하다.

# Modelfile
FROM llama3.1:8b
PARAMETER num_ctx 8192
PARAMETER temperature 0.2
SYSTEM 당신은 한국어로 답하는 C/C++ 코드 리뷰어입니다. 근거 없는 추측은 하지 않습니다.
ollama create c-reviewer -f Modelfile
ollama run c-reviewer "이 코드의 문제를 한 줄로 지적해줘: char b[8]; gets(b);"

ollama create 는 마지막에 success 를 찍고 끝난다. 그리고 답은 이렇게 왔다.

gets() 함수는 사용이 금지된 함수로, 버퍼 오버플로를 유발할 수 있습니다.

ollama ps 에는 원본이 아니라 구운 모델 이름으로 잡히고, 컨텍스트도 Modelfile 에 박은 8192 가 그대로 붙는다.

NAME                 ID              SIZE      PROCESSOR    CONTEXT
c-reviewer:latest    1724e086b7ec    5.8 GB    100% GPU     8192

(3) KV 캐시를 양자화한다. 컨텍스트를 줄이기 싫을 때 제일 먼저 시도할 손잡이다. KV 캐시를 FP16 대신 8비트로 들고 있으면 그 메모리가 절반이 된다. 플래시 어텐션을 같이 켜야 한다.

pkill -x ollama
OLLAMA_FLASH_ATTENTION=1 OLLAMA_KV_CACHE_TYPE=q8_0 nohup ollama serve > ~/ollama-serve.log 2>&1 &

서버를 내릴 때 pkill -f "ollama serve" 를 쓰면 안 된다. -f 는 명령줄 전체를 보기 때문에 그 문자열이 들어 있는 자기 자신(셸)까지 같이 죽인다. 나도 이걸로 두 번 날렸다. 프로세스 이름만 정확히 보는 pkill -x ollama 를 쓰자.

systemd 로 깔았으면 환경변수를 이렇게 넣는다. 서비스로 뜨는 프로세스는 내 셸의 환경변수를 물려받지 않아서, 위 방식이 아예 안 먹는다. 드롭인 파일에 적어야 한다.

sudo systemctl edit ollama

편집기가 열리면 아래를 적고 저장한 뒤 재시작한다.

[Service]
Environment="OLLAMA_FLASH_ATTENTION=1"
Environment="OLLAMA_KV_CACHE_TYPE=q8_0"
sudo systemctl restart ollama
journalctl -u ollama -n 50 --no-pager | grep -o "OLLAMA_KV_CACHE_TYPE:[a-z0-9_]*"

마지막 줄이 OLLAMA_KV_CACHE_TYPE:q8_0 을 뱉으면 적용된 것이다.

나는 sudo 가 없어서 이 루트 서비스 경로를 그대로 돌려보지는 못했다. 대신 같은 구조를 사용자 서비스로 만들어 확인했다. 수동 설치를 해놓고 로그인할 때마다 자동으로 띄우고 싶으면 이쪽이 오히려 편하다.

mkdir -p ~/.config/systemd/user
cat > ~/.config/systemd/user/ollama.service <<EOF
[Unit]
Description=Ollama Service (user)
After=network-online.target

[Service]
ExecStart=$HOME/ollama-local/bin/ollama serve
Restart=always
RestartSec=3
Environment="OLLAMA_FLASH_ATTENTION=1"
Environment="OLLAMA_KV_CACHE_TYPE=q8_0"

[Install]
WantedBy=default.target
EOF

systemctl --user daemon-reload
systemctl --user enable --now ollama
systemctl --user is-active ollama

active 가 찍히고, 저널에서도 환경변수와 GPU 인식이 그대로 확인된다.

journalctl --user -u ollama -n 200 --no-pager | grep -E "inference compute|KV_CACHE"

이 상태에서 컨텍스트 16384 로 요청하니 5.9 GB / 100% GPU / 48.39 tok/s 가 나왔다. 셸에서 환경변수를 직접 준 앞의 측정(48.83 tok/s)과 같은 결과라, 드롭인으로 넣어도 효과는 동일하다는 걸 확인한 셈이다.

효과가 꽤 크다. 같은 32768 컨텍스트를 다시 재보니 이렇게 바뀌었다.

NAME           ID              SIZE      PROCESSOR          CONTEXT
llama3.1:8b    46e0c10c039e    7.6 GB    18%/82% CPU/GPU    32768

memory.used: 6890 MiB / 8188 MiB
eval rate:   30.22 tok/s

적재 크기가 9.5GB 에서 7.6GB 로 줄면서 CPU 로 밀려난 몫이 34%에서 18%로 내려갔고, 속도는 20.82 에서 30.22 tok/s 로 올라왔다.

더 재밌는 건 16K 다. 컨텍스트를 16384 로 잡고 KV 캐시 양자화를 껐다 켜 보면 경계가 정확히 어디인지 보인다. 내가 잰 값을 전부 모으면 이렇다. 전부 같은 카드, 같은 llama3.1:8b Q4_K_M 파일이다.

컨텍스트 KV 캐시 ollama ps SIZE PROCESSOR 생성 속도
4096 (기본) FP16 5.3 GB 100% GPU 49.16 tok/s
8192 FP16 5.8 GB 100% GPU 49.23 tok/s
16384 FP16 7.3 GB 13%/87% CPU/GPU 33.57 tok/s
16384 q8_0 5.9 GB 100% GPU 48.83 tok/s
32768 FP16 9.5 GB 34%/66% CPU/GPU 20.82 tok/s
32768 q8_0 7.6 GB 18%/82% CPU/GPU 30.22 tok/s

굵게 칠한 줄이 이 글에서 제일 쓸모 있는 발견이다. 8GB 카드에서 16K 컨텍스트는 그냥 쓰면 오프로딩이 나서 33 tok/s 로 떨어지는데, KV 캐시를 q8_0 로 바꾸면 100% GPU 로 돌아오면서 48.83 tok/s 가 나온다. 기본 컨텍스트일 때 속도(49.16)와 사실상 차이가 없다. 컨텍스트를 네 배로 쓰면서 속도는 그대로인 셈이라, 8GB 카드를 쓴다면 이 환경변수 두 개는 그냥 기본으로 켜 두는 게 낫다고 본다.

32K 는 이걸 켜도 여전히 넘친다. 그건 이 카드에서 무리라는 뜻이고, 그때는 3B 급으로 내려가는 게 맞다.

7단계 — 코드에 꽂기

서버가 떴으면 나머지는 주소 문제다. Ollama 는 자체 API(/api/generate, /api/chat) 말고 OpenAI 호환 엔드포인트도 같이 준다. 그래서 기존 코드를 거의 그대로 꽂을 수 있다.

curl -s http://localhost:11434/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "llama3.1:8b",
    "messages": [
      {"role": "system", "content": "간결하게 한국어로 답한다."},
      {"role": "user", "content": "mmap 으로 모델을 읽으면 왜 로딩이 빠른가?"}
    ],
    "temperature": 0.2
  }' | python3 -c 'import json,sys; print(json.load(sys.stdin)["choices"][0]["message"]["content"])'

받은 답이다.

mmap은 메모리 매핑을 의미하는 것으로, 파일을 메모리에 로드하지 않고
메모리 주소 공간에 파일을 매핑하는 것을 의미합니다.
(이하 줄임)

파이썬이면 openai SDK 의 base_url 만 갈아 끼우면 끝난다. api_key 는 SDK 가 빈 값을 거부하니까 아무 문자열이나 넣는다. Ollama 문서도 “필수지만 무시됨”이라고 적어 뒀다.

from openai import OpenAI

client = OpenAI(base_url="http://localhost:11434/v1", api_key="local")
resp = client.chat.completions.create(
    model="llama3.1:8b",
    messages=[
        {"role": "system", "content": "간결하게 한국어로 답한다."},
        {"role": "user", "content": "KV 캐시가 컨텍스트 길이에 비례하는 이유를 한 줄로."},
    ],
    temperature=0.2,
)
print(resp.choices[0].message.content)

OpenAI 호환 API 를 받는 CLI 도구나 에이전트 프레임워크면 대개 OPENAI_BASE_URL 과 모델명만 바꿔 같은 식으로 붙는다. 다만 도구마다 function calling·스트리밍·구조화 출력 지원 범위가 다르니, 붙이기 전에 그 도구가 실제로 때리는 엔드포인트가 뭔지는 확인하고 가는 게 안전하다.

8단계(선택) — llama.cpp 로 내려가기

레이어 배치·스레드 수·KV 캐시 타입을 손으로 붙잡아야 하거나, GGUF 를 직접 양자화해야 하거나, 최소 의존성으로 컨테이너에 넣어야 하면 llama.cpp 를 직접 빌드한다.

이 섹션은 이번에 내가 빌드해 보지 않았다. 이 머신에 nvcc 가 없어서 CUDA 빌드를 검증할 방법이 없었다. 아래 명령과 옵션은 공식 빌드 문서를 정리한 것이지 내 터미널 기록이 아니다. 출력 예시를 붙이지 않은 것도 그래서다.

옵션 이름부터 짚고 간다. 오래된 글에는 CUDA 빌드가 -DLLAMA_CUBLAS=ON 으로 적혀 있는데 이 이름은 지금 안 먹는다. 현재 공식 문서 기준 CUDA 옵션은 GGML_CUDA 다. 고약한 건, 존재하지 않는 CMake 옵션을 줘도 빌드가 경고 한 줄 없이 그냥 성공한다는 점이다. 조용히 무시되고 CPU 빌드가 나오니 나중에 “빌드는 됐는데 GPU 를 안 쓴다”로 돌아온다.

sudo apt install -y build-essential cmake git
git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp

# CPU 전용 빌드
cmake -B build
cmake --build build --config Release -j "$(nproc)"

# CUDA 빌드 (WSL-Ubuntu 용 CUDA 툴킷이 깔려 있어야 nvcc 를 찾는다)
cmake -B build -DGGML_CUDA=ON
cmake --build build --config Release -j "$(nproc)"

빌드가 끝나면 build/bin/ 에 대화용 llama-cli, HTTP 서버 llama-server, 양자화 도구 llama-quantize, 벤치 llama-bench 가 생긴다.

# GGUF 를 받아서 OpenAI 호환 서버로 띄우기
# -ngl 99 는 "가능한 모든 레이어를 GPU 로", -c 는 컨텍스트 길이
./build/bin/llama-server \
  -hf bartowski/Meta-Llama-3.1-8B-Instruct-GGUF:Q4_K_M \
  -ngl 99 -c 8192

# 프롬프트 처리(pp)와 생성(tg) 속도를 갈라서 재기
./build/bin/llama-bench -m ./models/model-Q4_K_M.gguf -ngl 99 -p 512 -n 128

-c 를 생략하면 안 된다. 생략하면 llama-server 는 모델 메타데이터의 학습 컨텍스트 길이를 그대로 쓰려 드는데, Llama 3.1 은 그게 131,072 다. 0단계 규칙대로면 KV 캐시만 16GiB 를 요구하는 값이라 8GB 카드에 들어갈 방법이 없다. 8GB VRAM 이면 -c 8192 가 무난한 출발점이다.

5단계에서 본 게 이 사고의 순한 버전이다. Ollama 는 자리가 모자라면 알아서 CPU 로 내려 느려지는 선에서 끝났지만, -ngl 99 로 전부 GPU 에 올리라고 못박아 둔 상태면 자리가 없을 때 로딩 단계에서 그냥 죽는다. 어느 쪽이든 원인은 하나, 컨텍스트 길이다.

두 경로는 결국 만난다. Ollama 속에 llama.cpp/ggml 이 들어 있어서 직접 만든 GGUF 를 Modelfile 한 줄로 등록할 수 있고, 반대로 Ollama 가 받아 둔 blob 도 그냥 GGUF 라 llama-server -m 으로 바로 열린다.

그리고 이 글을 덮은 다음에 갈 데가 하나 더 있다. vLLM 이다. 위의 두 경로는 “내 기계에서 나 혼자 쓴다”가 전제인데, 여러 사용자의 요청을 동시에 받아 처리량을 뽑아야 하면 그때부터는 다른 물건이 필요하다. 대략 이렇게 갈린다.

이럴 때 쓸 것
혼자 쓰는 개발 환경, 모델을 자주 갈아 끼움 Ollama
자원이 빠듯해 레이어·스레드·KV 캐시를 손으로 통제, 직접 양자화 llama.cpp
동시 요청 다수, VRAM 24GB 이상, safetensors 원본을 그대로 vLLM

내 8GB 카드로는 vLLM 이 애초에 후보가 아니라 이 글에서는 다루지 않았다. 24GB 이상을 쥐고 여러 명에게 서빙할 상황이 되면 그때 꺼내면 된다.

막혔을 때

내가 이번에 실제로 본 에러부터, 문서가 못박아 둔 함정까지 순서대로 적는다.

curl: (22) The requested URL returned error: 404

수동 설치 URL 로 흔히 도는 .tgz 를 받으면 이게 뜬다.

$ curl -fL -o ollama-linux-amd64.tgz https://ollama.com/download/ollama-linux-amd64.tgz
curl: (22) The requested URL returned error: 404

원인: 요즘 Ollama 는 .tgz 가 아니라 .tar.zst 로 배포한다. 설치 스크립트도 .tar.zst 를 먼저 보고 없을 때만 .tgz 로 물러나는데, 0.33.3 에는 .tgz 가 아예 없다. 해결: 확장자를 바꿔서 받는다. 확인은 이렇게 한다. 앞줄이 200, 뒷줄이 404 로 찍힌다.

curl -s -o /dev/null -w "%{http_code}\n" -L -I https://ollama.com/download/ollama-linux-amd64.tar.zst
curl -s -o /dev/null -w "%{http_code}\n" -L -I https://ollama.com/download/ollama-linux-amd64.tgz

bash: zstd: command not found

원인: .tar.zst 를 풀려면 zstd 가 필요한데 기본 설치가 아니다. 설치 스크립트도 이 상황에서 이렇게 안내하고 끝낸다.

This version requires zstd for extraction. Please install zstd and try again:
  - Debian/Ubuntu: sudo apt-get install zstd

해결: sudo 가 되면 그대로 깔면 된다. 안 되면 1단계에 적은 파이썬 zstandard 로 우회한다. pip 는 루트를 안 쓴다.

sudo: a password is required

원인: 공식 설치 스크립트는 /usr/local 에 쓰고 systemd 서비스를 등록해야 해서 루트가 필요하다. 스크립트를 열어 보면 전부 $SUDO 를 달고 있다.

curl -fsSL https://ollama.com/install.sh -o install.sh
grep -n 'SUDO' install.sh | head -6
110:SUDO=
117:    SUDO="sudo"
148:            zstd -d | $SUDO tar -xf - -C "${dest_dir}"
156:        $SUDO tar -xzf - -C "${dest_dir}"
166:    $SUDO rm -rf "$OLLAMA_INSTALL_DIR/lib/ollama"
169:$SUDO install -o0 -g0 -m755 -d $BINDIR

(head -6 으로 잘랐다. 그냥 head 면 10줄이 나오고, 뒤로는 systemd 유닛과 ollama 사용자 생성까지 계속 $SUDO 가 붙는다.)

해결: 1단계의 수동 설치로 간다. 홈 디렉터리에 풀어도 기능은 동일하고, 서버만 ollama serve 로 직접 띄우면 된다.

grep: /home/사용자/ollama-serve.log: No such file or directory

원인: 공식 스크립트로 깔면 systemd 가 서버를 띄우기 때문에 로그가 파일이 아니라 저널에 쌓인다. 그 로그 파일은 2단계 (b) 수동 설치 경로에서만 생긴다. 해결: 저널을 본다.

journalctl -u ollama -n 200 --no-pager | grep -E "inference compute|vram-based"

Unit ollama.service could not be found. 가 나오면 반대 경우다. 서비스가 없는 것이니 ollama serve 를 직접 띄우고 로그 파일 쪽을 보면 된다.

환경변수를 줬는데 서버가 무시한다

원인: systemd 서비스로 뜬 프로세스는 내 셸의 환경변수를 물려받지 않는다. OLLAMA_KV_CACHE_TYPE=q8_0 ollama serve 처럼 앞에 붙여 봐야 이미 떠 있는 서비스에는 아무 영향이 없다. 해결: 드롭인에 적는다(6단계 3번). 적용됐는지는 저널에 찍히는 서버 설정 줄로 확인한다.

sudo systemctl edit ollama      # [Service] 아래 Environment= 줄 추가
sudo systemctl restart ollama
journalctl -u ollama -n 50 --no-pager | grep -o "OLLAMA_KV_CACHE_TYPE:[a-z0-9_]*"

GPU 가 있는데 CPU 처럼 느리다

원인: 십중팔구 오프로딩이다. 모델이 VRAM 을 넘으면 레이어 일부가 CPU 로 내려가고, 그 부분은 GPU 메모리보다 훨씬 느린 DDR5 에서 읽히니 전체가 같이 느려진다. nvidia-smi 로는 잘 안 보인다. 해결: ollama psPROCESSOR 열부터 본다. 100% GPU 가 아니면 컨텍스트를 줄이거나(6단계 1번), KV 캐시를 양자화하거나(6단계 3번), 더 작은 모델로 내린다.

ollama ps

서버 로그에 library=CPU 가 찍힌다

원인: GPU 를 아예 못 잡은 것이다. WSL2 라면 WSL 안에 리눅스 NVIDIA 드라이버를 깔았을 때 자주 이렇게 된다. 해결: WSL 안의 리눅스 디스플레이 드라이버를 지우고, Windows 쪽 드라이버를 최신으로 올린 뒤 nvidia-smi 가 WSL 안에서 정상 동작하는지부터 확인한다. WSL 안에는 CUDA 툴킷만(그것도 WSL-Ubuntu 전용 패키지로) 깐다.

긴 문서를 넣었는데 뒷부분이 조용히 잘린다

원인: 기본 컨텍스트가 8GB 카드 기준 4,096 토큰이다. 창을 넘는 부분은 에러 없이 버려진다. 답이 이상한데 프롬프트를 아무리 고쳐도 그대로면 여기부터 의심한다. 해결: OLLAMA_CONTEXT_LENGTH 나 요청의 num_ctx 로 명시한다. 다만 5단계에서 봤듯 올린 만큼 느려지니 필요한 만큼만 올린다.

32GB 머신인데 free -h 가 15Gi 로 나온다

원인: 램이 빠진 게 아니라 WSL2 의 기본 동작이다. .wslconfigmemory 기본값이 호스트 RAM 의 50% 라, 32GB 머신이면 16GB 가 상한이고 커널 몫을 빼면 15Gi 로 보인다. 해결: CPU 로 큰 모델을 밀 거면 윈도우 쪽 %UserProfile%\.wslconfig 에 아래를 적고 PowerShell 에서 wsl --shutdown 을 친다. GPU VRAM 은 이 설정과 무관하다. (나는 8GB 카드에 올릴 모델만 만지는 중이라 이 상한을 그냥 두고 썼다. 이 설정은 문서 기준이고 내가 적용해 본 건 아니다.)

[wsl2]
memory=24GB

CUDA 로 빌드했는데 GPU 를 안 쓴다

원인: 옛 옵션 LLAMA_CUBLAS 를 썼을 때다. 존재하지 않는 CMake 옵션은 경고 없이 무시되고 CPU 빌드가 나온다. 해결: -DGGML_CUDA=ON 으로 다시 빌드한다.

참고 자료

C언어 함수 및 라이브러리 함수 사용하기

C언어 함수 및 라이브러리 함수 사용하기. C언어에서 함수는 코드의 재사용성과 가독성을 높이는 핵심 요소입니다. 본 글에서는 사용자 정의 함수와 표준 라이브러리 함수의 개념과 활용법을 살펴봅니다. 초보자도 쉽게 이해할 수 있도록 단계별로 설명합니다.

목차

소개

C언어에서 함수는 코드의 재사용성과 가독성을 높이는 핵심 요소입니다. 본 글에서는 사용자 정의 함수와 표준 라이브러리 함수의 개념과 활용법을 살펴봅니다. 초보자도 쉽게 이해할 수 있도록 단계별로 설명합니다.

맨 위로

1. C언어 함수의 기본 개념

C언어에서 함수는 특정 작업을 수행하는 코드 블록입니다. 함수를 사용하면 코드의 재사용성과 가독성이 향상되며, 프로그램을 모듈화할 수 있습니다. 함수는 정의(definition)와 호출(call) 두 가지 중요한 개념으로 구성되어 있으며, 매개변수를 통해 데이터를 전달하고 반환값으로 결과를 돌려줄 수 있습니다. 이 섹션에서는 함수의 기본 구조와 동작 원리를 이해하고, 매개변수와 반환값의 역할을 실용적인 예제와 함께 살펴봅니다.

1.1 함수 정의와 호출의 이해

함수 정의는 함수가 수행할 작업을 명확히 작성하는 부분입니다. 기본 구조는 다음과 같습니다:

반환형 함수이름(매개변수 목록) {
    // 함수 몸체
}
  • 반환형: 함수가 돌려주는 값의 자료형
  • 함수이름: 호출 시 사용할 이름
  • 매개변수 목록: 함수에 전달할 입력값들

함수 호출은 정의된 함수를 실제로 실행시키는 과정입니다. 예를 들어:

int add(int a, int b) {
    return a + b;
}

int main() {
    int result = add(3, 5); // 함수 호출
    printf("결과: %d\n", result);
    return 0;
}

중요: 함수 호출 시 매개변수와 인자의 순서와 자료형이 일치해야 합니다.

함수는 프로그램의 흐름을 제어하는 데 중요한 역할을 하며, 복잡한 작업을 작은 단위로 나누어 관리할 수 있게 해줍니다.

1.2 매개변수와 반환값

매개변수(Parameter)는 함수에 입력되는 값이며, 반환값(Return value)은 함수가 작업 후 돌려주는 결과입니다.

매개변수 종류

종류 설명
값에 의한 전달 (Call by Value) 매개변수로 전달된 값을 복사하여 함수 내부에서 사용. 원본 값은 변경되지 않음
주소에 의한 전달 (Call by Reference) 포인터를 이용해 변수의 주소를 전달, 함수 내부에서 원본 값을 직접 변경 가능

반환값

  • 함수는 return 키워드로 값을 반환합니다.
  • 반환형이 void인 경우, 값을 반환하지 않습니다.

예제: 매개변수와 반환값 활용

#include <stdio.h>

// 두 정수의 합을 반환하는 함수
int sum(int x, int y) {
    return x + y;
}

// 배열의 값을 모두 더하는 함수 (주소에 의한 전달)
int array_sum(int *arr, int size) {
    int total = 0;
    for (int i = 0; i < size; i++) {
        total += arr[i];
    }
    return total;
}

int main() {
    int a = 10, b = 20;
    printf("sum(a, b) = %d\n", sum(a, b));

    int nums[] = {1, 2, 3, 4, 5};
    printf("array_sum(nums, 5) = %d\n", array_sum(nums, 5));

    return 0;
}

팁: 매개변수를 적절히 사용하면 함수의 재사용성과 범용성을 높일 수 있습니다.

맨 위로

2. 사용자 정의 함수 작성법

C언어에서 사용자 정의 함수는 코드의 재사용성과 가독성을 높이는 핵심 요소입니다. 함수는 특정 작업을 수행하는 코드 블록으로, 함수 선언과 구현을 통해 정의됩니다. 적절한 함수 작성법을 익히면 복잡한 프로그램도 체계적으로 관리할 수 있습니다.

2.1 함수 선언과 구현 방법

함수는 먼저 반환형과 함수명을 명시하는 선언문으로 알려주고, 실제 동작 코드를 구현하는 정의문으로 작성합니다. 선언은 컴파일러에게 함수의 존재를 알리고, 구현은 함수가 실제로 수행할 작업을 기술합니다.

// 함수 선언
int add(int a, int b);

// 함수 구현
int add(int a, int b) {
    return a + b;
}
  • 반환형: 함수가 반환하는 값의 타입을 지정합니다. 반환값이 없으면 void를 사용합니다.
  • 매개변수: 함수가 입력받는 변수들로, 타입과 이름을 명시합니다.
  • 함수명: 함수의 이름으로, 호출할 때 사용됩니다.

함수 선언은 보통 헤더 파일에 작성하거나, 함수 구현 전에 위치시킵니다. 함수 정의는 보통 소스 파일에 작성합니다.

함수 작성 시 명확한 이름과 적절한 매개변수 사용은 유지보수에 큰 도움이 됩니다.

2.2 함수의 유용한 활용 사례

함수는 프로그램을 모듈화하고 반복되는 코드를 줄이는 데 매우 유용합니다. 아래는 함수 활용의 대표적인 사례들입니다.

  1. 코드 재사용: 동일한 작업을 여러 곳에서 수행할 때, 함수로 작성하면 중복 코드를 줄일 수 있습니다.

  2. 복잡한 문제 분할: 큰 문제를 작은 함수 단위로 나누어 구현하면 이해와 디버깅이 쉬워집니다.

  3. 매개변수를 통한 다양한 동작: 함수에 인자를 전달하여 다양한 결과를 낼 수 있습니다.

  4. 재귀 함수 사용: 자기 자신을 호출하는 함수로, 반복적인 문제를 간결하게 해결할 수 있습니다.

// 재귀를 이용한 팩토리얼 함수
int factorial(int n) {
    if (n <= 1) return 1;
    else return n * factorial(n - 1);
}
  1. 포인터를 이용한 함수 내 변수 변경: 포인터를 매개변수로 전달하면 함수 내에서 변수 값을 직접 변경할 수 있습니다.
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

함수 활용은 프로그램의 효율성과 유지보수성을 크게 향상시키므로, 다양한 형태의 함수 작성법을 익히는 것이 중요합니다.

맨 위로

3. 표준 라이브러리 함수 소개

C언어의 표준 라이브러리 함수는 프로그래밍에서 자주 필요한 기능들을 미리 구현해 놓은 함수들의 집합입니다. 입출력, 문자열 처리, 메모리 관리 등 다양한 영역에서 효율적인 개발을 가능하게 합니다. 이 섹션에서는 특히 입출력 함수와 문자열 처리 함수에 대해 실용적인 사용법과 예제를 중심으로 살펴보겠습니다.

3.1 입출력 함수 활용

C언어 표준 라이브러리에서 가장 기본이 되는 입출력 함수는 printfscanf입니다. printf는 화면에 데이터를 출력할 때, scanf는 키보드로부터 데이터를 입력받을 때 사용합니다.

printf 함수

  • 서식 지정자(format specifier)를 이용해 다양한 데이터 타입 출력 가능
  • 주요 서식 지정자 예:
서식 지정자 의미
%d 정수(int)
%f 실수(float)
%c 문자(char)
%s 문자열(char[])
int num = 10;
printf("정수 출력: %d\n", num);

scanf 함수

  • 사용자 입력을 변수에 저장
  • 입력 시 변수 주소를 전달해야 함
int age;
printf("나이를 입력하세요: ");
scanf("%d", &age);
printf("입력한 나이: %d\n", age);

중요: scanf 사용 시 입력 버퍼에 남아있는 개행 문자나 잘못된 입력으로 인해 예상치 못한 동작이 발생할 수 있으므로 주의해야 합니다.

추가 입출력 함수

  • getchar(), putchar() : 문자 단위 입출력
  • gets(), puts() : 문자열 입출력 (단, gets()는 보안 취약점으로 인해 사용 권장하지 않음)

입출력 함수들을 적절히 활용하면 사용자와의 상호작용을 원활하게 구현할 수 있습니다.

3.1.1 입출력 함수 활용

(상세 설명 내용)

3.2 문자열 처리 함수

C언어에서 문자열은 문자 배열로 표현되며, 표준 라이브러리 <string.h>에 다양한 문자열 처리 함수가 제공됩니다. 문자열 길이 측정, 복사, 연결, 비교 등 기본적인 작업을 쉽게 수행할 수 있습니다.

주요 문자열 함수

함수명 기능 예제 코드
strlen 문자열 길이 반환 strlen("hello") → 5
strcpy 문자열 복사 strcpy(dest, src)
strcat 문자열 연결 strcat(dest, src)
strcmp 문자열 비교 strcmp(str1, str2)

사용 예시

#include <stdio.h>
#include <string.h>

int main() {
    char str1[20] = "Hello";
    char str2[] = " World!";

    printf("str1 길이: %lu\n", strlen(str1));

    strcat(str1, str2);
    printf("문자열 연결 결과: %s\n", str1);

    char str3[20];
    strcpy(str3, str1);
    printf("복사된 문자열: %s\n", str3);

    if (strcmp(str1, str3) == 0) {
        printf("str1과 str3는 같습니다.\n");
    }

    return 0;
}

참고: 문자열 함수 사용 시 버퍼 크기 초과에 주의해야 하며, 안전한 함수(strncpy, strncat 등)를 사용하는 것이 좋습니다.

문자열 처리 함수는 텍스트 데이터를 다루는 프로그램에서 필수적이며, 올바른 사용법을 익히는 것이 중요합니다.

맨 위로

4. 라이브러리 함수 사용 시 주의사항

C언어에서 라이브러리 함수를 사용할 때는 헤더 파일의 올바른 포함과 링커 설정, 그리고 함수 호출 후 발생할 수 있는 오류를 적절히 처리하는 것이 중요합니다. 이를 통해 컴파일 및 실행 단계에서 발생할 수 있는 문제를 예방하고, 안정적인 프로그램을 작성할 수 있습니다.

4.1 헤더 파일 포함과 링크 문제

라이브러리 함수를 사용하려면 해당 함수가 선언된 헤더 파일을 반드시 포함해야 합니다. 헤더 파일을 포함하지 않으면 컴파일러가 함수 원형을 알 수 없어 경고나 오류가 발생할 수 있습니다.

또한, 함수가 정의된 라이브러리를 링커에 제대로 연결해야 합니다. 예를 들어, 수학 함수(sin, cos 등)를 사용하려면 <math.h>를 포함하고, 컴파일 시 -lm 옵션을 추가하여 수학 라이브러리를 링크해야 합니다.

#include <stdio.h>
#include <math.h>

int main() {
    double x = 0.5;
    double y = sin(x);
    printf("sin(%f) = %f\n", x, y);
    return 0;
}

컴파일 명령 예시:

gcc example.c -o example -lm
문제 상황 원인 해결 방법
함수 원형 미포함 헤더 파일 누락 해당 함수가 선언된 헤더 파일 포함
링커 오류 라이브러리 미링크 필요한 라이브러리 옵션 추가 (-lm 등)
중복 정의 오류 헤더 파일 중복 포함 또는 라이브러리 중복 링크 include guard 확인 및 링크 옵션 조정

헤더 파일 포함과 라이브러리 링크는 C 프로그램에서 라이브러리 함수를 올바르게 사용하는 기본 중의 기본입니다.

4.1.1 함수 사용 시 오류 처리 방법

라이브러리 함수를 호출할 때는 함수가 실패할 가능성을 항상 염두에 두고 오류 처리를 해야 합니다. 예를 들어, 파일 입출력 함수나 메모리 할당 함수는 실패 시 적절한 반환값을 통해 오류를 알립니다.

다음은 fopen 함수를 사용할 때의 오류 처리 예시입니다:

#include <stdio.h>

int main() {
    FILE *fp = fopen("nonexistent.txt", "r");
    if (fp == NULL) {
        perror("파일 열기 실패");
        return 1;
    }
    // 파일 작업 수행
    fclose(fp);
    return 0;
}

또 다른 예로, malloc 함수에서 메모리 할당 실패를 처리하는 방법:

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *arr = (int *)malloc(1000000000 * sizeof(int));
    if (arr == NULL) {
        fprintf(stderr, "메모리 할당 실패\n");
        return 1;
    }
    // 메모리 사용
    free(arr);
    return 0;
}

오류 처리는 프로그램의 안정성과 신뢰성을 높이는 필수 요소입니다. 항상 함수의 반환값을 확인하고 적절한 조치를 취하세요.

4.2 함수 사용 시 오류 처리 방법

이 하위 섹션의 내용은 자동 보정으로 채워졌습니다.

맨 위로

5. 함수 디버깅과 최적화 팁

C언어에서 함수 단위로 디버깅하는 방법과 성능 향상을 위한 최적화 기법을 소개합니다. 함수 내부의 오류를 효과적으로 찾아내고, 실행 속도와 메모리 사용을 개선하는 실용적인 팁을 제공합니다.

5.1 함수 단위 디버깅 방법

함수를 개별 단위로 디버깅하는 것은 복잡한 프로그램에서 문제를 빠르게 파악하는 데 필수적입니다. 주요 방법은 다음과 같습니다:

  1. 단위 테스트 작성: 함수별 입력과 출력을 검증하는 테스트 코드를 작성합니다. 예를 들어, assert를 이용해 함수 반환값을 확인할 수 있습니다.
#include <assert.h>

int add(int a, int b) {
    return a + b;
}

int main() {
    assert(add(2, 3) == 5);
    assert(add(-1, 1) == 0);
    return 0;
}
  1. 디버거 활용: gdb 같은 디버거를 사용해 함수 내부 변수 상태를 확인하고, 중단점(breakpoint)을 설정해 흐름을 단계별로 추적합니다.

  2. 로깅 추가: 함수 진입과 주요 변수 값을 출력하는 로그를 삽입해 실행 흐름과 상태를 파악합니다.

  3. 메모리 검사 도구 사용: Valgrind 등으로 함수 내 메모리 누수나 잘못된 접근을 점검합니다.

함수 단위 디버깅은 문제의 원인을 좁히고 코드 신뢰성을 높이는 데 매우 효과적입니다.

5.2 성능 향상을 위한 최적화 기법

함수의 성능을 높이기 위한 최적화 기법은 다음과 같습니다:

기법 설명 예제
인라인 함수 사용 작은 함수는 inline 키워드로 호출 오버헤드를 줄임 inline int square(int x) { return x * x; }
불필요한 계산 제거 반복문 밖으로 빼낼 수 있는 계산은 미리 수행 “`c
for (int i = 0; i < n; i++) {
int val = expensive_calc(); // 비효율
arr[i] = val * i;
}
// 최적화
int val = expensive_calc();
for (int i = 0; i < n; i++) {
arr[i] = val * i;
}
|
| 적절한 자료구조 선택 | 함수 내 자료구조를 효율적인 것으로 변경하여 성능 개선 | 배열 대신 해시 테이블 사용 등 |
| 루프 언롤링 | 루프 반복 횟수를 줄여 분기 비용 감소 | `for` 대신 반복문 내 명령어 중복 |
| 컴파일러 최적화 옵션 활용 | `-O2`, `-O3` 등 컴파일러 옵션으로 자동 최적화 | `gcc -O3 program.c -o program` |

```c
// 인라인 함수 예시
inline int max(int a, int b) {
    return (a > b) ? a : b;
}

최적화는 코드 가독성과 유지보수성에 영향을 줄 수 있으므로, 성능 측정을 기반으로 신중히 적용해야 합니다.

맨 위로


카테고리: 프로그래밍

태그: C언어, 함수, 라이브러리 함수, 프로그래밍, 코드 재사용

C언어 포인터의 이해

C언어 포인터의 이해

C언어 포인터의 이해. C언어에서 포인터는 메모리 주소를 직접 다루는 강력한 도구입니다. 포인터를 이해하면 메모리 관리와 효율적인 프로그래밍이 가능해집니다. 이 글에서는 포인터의 기본 개념부터 활용법까지 단계별로 살펴봅니다.

목차

소개

C언어에서 포인터는 메모리 주소를 직접 다루는 강력한 도구입니다. 포인터를 이해하면 메모리 관리와 효율적인 프로그래밍이 가능해집니다. 이 글에서는 포인터의 기본 개념부터 활용법까지 단계별로 살펴봅니다.

맨 위로

포인터의 기본 개념

포인터는 C언어에서 메모리 주소를 직접 다루는 변수로, 변수의 주소를 저장하고 이를 통해 메모리를 효율적으로 조작할 수 있게 해줍니다. 이 섹션에서는 포인터가 무엇인지, 그리고 포인터 변수를 선언하고 초기화하는 방법에 대해 실용적인 관점에서 살펴봅니다.

포인터란 무엇인가?

포인터는 메모리 주소를 저장하는 변수입니다.

포인터는 단순히 값이 아니라 메모리 상의 특정 위치(주소)를 가리키는 변수입니다. C언어에서는 변수를 선언하면 메모리의 일정 공간이 할당되는데, 포인터는 이 공간의 시작 주소를 저장합니다.

예를 들어, 정수형 변수 int a = 10;이 있을 때, a는 값 10을 저장하지만, &a는 변수 a가 저장된 메모리 주소를 가리킵니다.

int a = 10;
int *p = &a; // p는 a의 주소를 저장하는 포인터

이처럼 포인터를 통해 변수의 주소를 저장하고, 이를 이용해 변수에 간접적으로 접근하거나 조작할 수 있습니다. 포인터는 배열, 함수 매개변수 전달, 동적 메모리 할당 등 다양한 상황에서 핵심적인 역할을 합니다.


포인터의 주요 특징

특징 설명
메모리 주소 저장 변수의 메모리 주소를 저장하는 변수
간접 참조 (Dereferencing) 포인터를 통해 실제 변수의 값을 읽거나 수정 가능
타입 지정 포인터는 가리키는 데이터 타입에 따라 선언됨

포인터를 이해하는 것은 C언어를 능숙하게 다루기 위한 필수적인 단계입니다.

포인터 변수 선언과 초기화

포인터 변수를 선언할 때는 가리키는 데이터 타입을 명시해야 하며, * 기호를 사용합니다. 초기화는 보통 변수의 주소를 할당하거나 NULL로 초기화합니다.

포인터 선언 문법

타입 *포인터이름;

예를 들어, 정수를 가리키는 포인터는 다음과 같이 선언합니다:

int *p;

포인터 초기화 예시

int a = 5;
int *p = &a; // a의 주소로 초기화

int *q = NULL; // 초기화하지 않은 포인터는 NULL로 설정하는 것이 안전

중요한 점

초기화하지 않은 포인터는 쓰레기 값을 가질 수 있어, 사용 시 프로그램 오류(예: 세그멘테이션 오류)를 일으킬 수 있습니다.

포인터 사용 예

#include <stdio.h>

int main() {
    int num = 10;
    int *ptr = &num; // 포인터 선언 및 초기화

    printf("num의 값: %d\n", num);         // 10
    printf("ptr이 가리키는 값: %d\n", *ptr); // 10

    *ptr = 20; // 포인터를 통해 num 값 변경
    printf("num의 새로운 값: %d\n", num);   // 20

    return 0;
}

위 예제에서 ptrnum의 주소를 저장하고, *ptr을 통해 num의 값을 읽고 쓸 수 있습니다.


포인터 변수 선언과 초기화는 포인터를 안전하고 효과적으로 사용하는 첫걸음입니다.

포인터 변수 선언과 초기화

이 하위 섹션의 내용은 자동 보정으로 채워졌습니다.

맨 위로

포인터와 메모리

포인터는 메모리 주소를 직접 다루는 변수로, C언어에서 메모리 관리와 효율적인 데이터 조작에 핵심적인 역할을 합니다. 이 섹션에서는 메모리 주소와 변수 간의 관계를 이해하고, 포인터 연산을 실용적으로 활용하는 방법을 살펴봅니다.

메모리 주소와 변수의 관계

C언어에서 변수는 메모리의 특정 주소에 값을 저장합니다. 포인터는 이 주소를 저장하는 변수로, 변수와 메모리 주소의 관계를 이해하면 메모리 직접 조작과 효율적인 데이터 관리가 가능합니다.

  • 변수는 메모리의 특정 위치에 데이터를 저장합니다.
  • 포인터는 그 위치(주소)를 저장하는 변수입니다.
int a = 10;       // 변수 a 선언 및 초기화
int *p = &a;      // 변수 a의 주소를 포인터 p에 저장

printf("a의 값: %d\n", a);         // 10
printf("a의 주소: %p\n", &a);      // 메모리 주소 출력
printf("p가 가리키는 값: %d\n", *p); // 10

중요: & 연산자는 변수의 주소를 얻고, * 연산자는 포인터가 가리키는 주소의 값을 참조합니다.

변수 설명
a 정수형 변수, 값 10 저장
p int형 포인터, a의 주소 저장

이 관계를 이해하면 포인터를 통해 변수의 값을 직접 조작하거나, 함수에 변수의 주소를 전달해 효율적인 메모리 사용이 가능합니다.

포인터 연산의 이해

포인터는 단순히 주소를 저장하는 것 외에도 산술 연산이 가능합니다. 포인터 연산은 메모리에서 연속된 데이터 구조(배열 등)를 다룰 때 매우 유용합니다.

  • p + 1은 포인터가 가리키는 타입의 크기만큼 주소를 증가시킵니다.
  • p - 1은 주소를 감소시킵니다.
  • 두 포인터 간의 뺄셈은 요소의 개수를 반환합니다n
int arr[3] = {10, 20, 30};
int *p = arr; // arr의 첫 번째 요소 주소

printf("첫 번째 값: %d\n", *p);       // 10
printf("두 번째 값: %d\n", *(p + 1)); // 20

p++; // 포인터를 다음 요소로 이동
printf("포인터 이동 후 값: %d\n", *p); // 20

int diff = &arr[2] - &arr[0];
printf("두 주소 사이 요소 개수: %d\n", diff); // 2

주의: 포인터 연산은 반드시 같은 배열 내에서 수행해야 하며, 배열 범위를 벗어나면 정의되지 않은 동작이 발생할 수 있습니다.

연산 설명
p + n 포인터를 n개 요소만큼 증가 (자료형 크기 단위)
p – n 포인터를 n개 요소만큼 감소
p1 – p2 두 포인터 사이 요소 수 계산

포인터 연산을 통해 배열 탐색, 동적 메모리 관리, 다양한 자료구조 구현이 가능해집니다.

맨 위로

포인터 활용 기초

포인터는 C언어에서 변수의 메모리 주소를 직접 다루는 강력한 도구입니다. 이 섹션에서는 포인터를 이용해 변수의 값을 직접 변경하는 방법과, 배열과 포인터가 어떻게 밀접하게 연결되어 있는지 실용적인 관점에서 살펴봅니다. 포인터를 이해하면 메모리 관리와 효율적인 데이터 처리에 큰 도움이 됩니다.

포인터를 이용한 값 변경

포인터를 사용하면 변수의 메모리 주소를 통해 직접 값을 변경할 수 있습니다. 이는 함수 호출 시 복사본이 아닌 원본 데이터를 수정할 때 특히 유용합니다.

#include <stdio.h>

void changeValue(int *p) {
    *p = 100;  // 포인터가 가리키는 변수의 값을 100으로 변경
}

int main() {
    int num = 10;
    printf("변경 전 num: %d\n", num);  // 10
    changeValue(&num);  // num의 주소를 함수에 전달
    printf("변경 후 num: %d\n", num);  // 100
    return 0;
}

중요 포인트*p는 포인터가 가리키는 주소의 실제 값을 의미합니다. – 함수에 변수의 주소를 전달하면, 함수 내에서 그 변수의 값을 직접 수정할 수 있습니다.

이 방법은 메모리 사용을 효율화하고, 다수의 변수를 함수에 전달할 때 복사 비용을 줄여줍니다.

배열과 포인터의 관계

배열 이름은 배열의 첫 번째 원소의 주소를 가리키는 포인터로 취급됩니다. 따라서 배열과 포인터는 밀접한 관계를 가지고 있으며, 이를 활용하면 배열 요소에 유연하게 접근할 수 있습니다.

예를 들어:

#include <stdio.h>

int main() {
    int arr[3] = {10, 20, 30};
    int *p = arr;  // 배열 이름은 첫 번째 요소의 주소

    for(int i = 0; i < 3; i++) {
        printf("arr[%d] = %d\n", i, *(p + i));  // 포인터 산술 연산 사용
    }

    return 0;
}
배열 표현 포인터 표현
arr[i] *(arr + i)

중요 포인트 – 배열 이름 arr는 상수 포인터처럼 동작합니다. – 포인터 산술 연산을 통해 배열 요소에 접근할 수 있습니다.

이 관계를 이해하면 포인터를 이용해 동적 메모리 할당, 문자열 처리 등 다양한 프로그래밍 기법을 활용할 수 있습니다.

맨 위로

포인터 심화

포인터를 단순히 변수의 주소를 저장하는 용도로만 이해하는 것을 넘어서, 함수와의 연계 사용과 다중 수준의 포인터 구조를 이해하는 것이 중요합니다. 이 섹션에서는 포인터를 함수 매개변수로 활용하는 방법과 이중 포인터의 개념 및 실용적 사용법을 다룹니다. 이를 통해 메모리 관리, 데이터 구조 조작, 함수 간 데이터 전달에 대한 깊은 이해를 도모할 수 있습니다.

포인터와 함수: 매개변수로서의 포인터

함수에 포인터를 매개변수로 전달하면, 함수 내에서 원본 변수에 직접 접근하고 수정할 수 있습니다. 이는 값 복사에 의한 오버헤드를 줄이고, 함수가 여러 값을 반환하거나 큰 데이터를 효율적으로 처리할 때 유용합니다.

#include <stdio.h>

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int x = 10, y = 20;
    printf("Before swap: x = %d, y = %d\n", x, y);
    swap(&x, &y);
    printf("After swap: x = %d, y = %d\n", x, y);
    return 0;
}

포인터 매개변수의 핵심: 함수 내에서 변수의 직접적인 변경이 가능하며, 메모리 사용을 최적화할 수 있습니다.

또한, 배열을 함수에 전달할 때 포인터를 사용하여 배열 요소에 접근할 수 있습니다. 이는 배열의 첫 번째 요소 주소를 전달하는 것과 같으며, 배열 크기 관리에 주의해야 합니다.

이중 포인터의 이해

이중 포인터는 포인터를 가리키는 포인터로, 주로 포인터 배열, 동적 메모리 할당, 다차원 배열 처리 등에 사용됩니다.

#include <stdio.h>
#include <stdlib.h>

int main() {
    int **pp;
    int *p;
    int x = 5;

    p = &x;      // p는 x의 주소를 저장
    pp = &p;     // pp는 p의 주소를 저장

    printf("Value of x: %d\n", **pp); // 이중 역참조를 통해 x 값 출력

    // 동적 메모리 할당 예
    pp = (int **)malloc(sizeof(int *));
    *pp = (int *)malloc(sizeof(int));
    **pp = 10;
    printf("Dynamically allocated value: %d\n", **pp);

    free(*pp);
    free(pp);
    return 0;
}

이중 포인터 사용 시 주의점: 메모리 할당과 해제를 명확히 하여 메모리 누수를 방지해야 합니다.

이중 포인터는 함수에서 포인터 자체를 변경해야 할 때도 사용됩니다. 예를 들어, 함수 내에서 포인터가 가리키는 메모리를 동적 할당하고자 할 때 유용합니다.

맨 위로

포인터 사용 시 주의사항

포인터는 C언어에서 강력한 기능이지만, 잘못 사용하면 프로그램의 안정성과 성능에 심각한 문제를 일으킬 수 있습니다. 이 섹션에서는 포인터 사용 시 흔히 발생하는 문제인 메모리 누수와 잘못된 포인터 사용 예제를 통해 문제의 원인과 해결법을 실용적으로 다룹니다.

포인터와 메모리 누수

포인터를 사용하여 동적 메모리를 할당할 때, 할당한 메모리를 반드시 해제하지 않으면 메모리 누수가 발생합니다. 메모리 누수는 프로그램이 점점 더 많은 메모리를 차지하게 만들어 시스템 자원을 낭비하고, 심하면 프로그램이 비정상 종료될 수 있습니다.

중요: malloc 등으로 할당한 메모리는 사용 후 반드시 free로 해제해야 합니다.

다음은 메모리 누수의 대표적인 예시입니다:

int *ptr = (int *)malloc(sizeof(int) * 10);
// ... ptr 사용
// free(ptr); // 해제를 잊으면 메모리 누수가 발생

메모리 누수를 방지하려면 다음 원칙을 지켜야 합니다:

원칙 설명
할당과 해제 쌍 유지 할당한 메모리는 반드시 해제한다
중복 해제 금지 이미 해제한 포인터는 다시 해제하지 않는다
NULL 초기화 해제 후 포인터를 NULL로 초기화하여 이중 해제를 방지
free(ptr);
ptr = NULL;

메모리 누수를 추적할 때는 도구(예: Valgrind)를 활용하는 것도 좋은 방법입니다.

잘못된 포인터 사용 예제와 해결법

포인터 사용 시 흔히 발생하는 오류와 그 해결법을 예제를 통해 살펴봅니다.

1. 초기화되지 않은 포인터 사용

int *p;
*p = 10; // p가 가리키는 메모리가 정해지지 않아 위험

해결법: 포인터를 선언할 때 반드시 유효한 주소로 초기화하거나 NULL로 초기화 후 사용 전 점검합니다.

int value = 10;
int *p = &value;
// 또는
int *p = NULL;
if (p != NULL) {
    *p = 10;
}

2. 댕글링 포인터(Dangling Pointer)

이미 해제된 메모리를 가리키는 포인터를 사용하는 경우입니다.

int *p = malloc(sizeof(int));
free(p);
*p = 5; // 댕글링 포인터 사용

해결법: 메모리 해제 후 포인터를 NULL로 설정하여 접근을 방지합니다.

free(p);
p = NULL;

3. 배열 경계 밖 접근

int arr[3] = {1, 2, 3};
int *p = arr;
int val = *(p + 3); // 배열 범위 초과 접근

해결법: 배열이나 포인터 연산 시 범위를 항상 확인합니다.


이처럼 포인터 사용 시 발생할 수 있는 문제를 미리 인지하고, 안전한 코딩 습관을 기르는 것이 중요합니다.

맨 위로


카테고리: 프로그래밍

태그: C언어, 포인터, 메모리관리, 프로그래밍, 기초, 개발