1. 클래스(class) — 코드를 하나로 묶는 설계도
AI 프로젝트에서 가장 많이 보는 구조가 클래스다. FLUX 이미지 생성기의 뼈대를 보자:
class ImageGenerator:
_gpu_lock = threading.Semaphore(1)
def __init__(self):
self.device = "cuda"
self.dtype = torch.bfloat16
self.pipe = None
def generate(self, prompt, width=768, height=768):
image = self.pipe(prompt=prompt, width=width, height=height).images[0]
return image
왜 클래스를 쓰는가?
이미지 생성기는 여러 가지를 기억해야 한다:
- 어떤 GPU를 쓰는지 (self.device)
- 어떤 모델이 로드됐는지 (self.pipe)
- 어떤 데이터 타입인지 (self.dtype)
이걸 전역 변수로 흩뿌려놓으면 코드가 엉망이 된다. 클래스는 관련된 데이터와 기능을 하나의 상자에 담는 것이다.
__init__ — 상자를 처음 열 때 실행되는 코드
def __init__(self):
self.device = "cuda" # 이 인스턴스의 GPU 설정
self.pipe = None # 이 인스턴스의 모델 파이프라인
__init__은 초기화 함수다. ImageGenerator()를 호출하면 자동으로 실행된다.
self는 **"이 인스턴스 자기 자신"**을 가리킨다:
gen = ImageGenerator()
# gen.device → "cuda"
# gen.pipe → None
# self가 가리키는 게 바로 이 gen 변수
클래스 변수 vs 인스턴스 변수
class ImageGenerator:
_gpu_lock = threading.Semaphore(1) # 클래스 변수 (모든 인스턴스가 공유)
def __init__(self):
self.device = "cuda" # 인스턴스 변수 (각자 따로)
- _gpu_lock은 클래스 변수 → ImageGenerator._gpu_lock으로 접근. 인스턴스가 100개여도 1개만 존재.
- self.device는 인스턴스 변수 → 인스턴스마다 다를 수 있음.
GPU 락은 전체에서 1개만 있어야 하니까 클래스 변수, 디바이스 설정은 인스턴스마다 다를 수 있으니 인스턴스 변수. 이 구분이 중요하다.
2. self — 가장 헷갈리는 그것
Python 클래스에서 모든 메서드의 첫 번째 인자는 self다:
def generate(self, prompt, width=768):
image = self.pipe(prompt=prompt) # self.pipe = 이 인스턴스의 파이프라인
return image
호출할 때는 self를 안 넘긴다:
gen = ImageGenerator()
gen.generate("사과 사진") # self 자리에 gen이 자동으로 들어감
쉽게 외우는 법: self는 "내 것"이라는 뜻이다.
- self.pipe → "내 파이프라인"
- self.device → "내 디바이스"
- self.generate() → "내 생성 함수"
3. 딕셔너리 — AI 프로젝트의 만능 데이터 구조
설정값, API 응답, 테마 정보 등 거의 모든 곳에서 딕셔너리를 쓴다:
CATEGORY_THEMES = {
"food": {
"bg_color": "#FFF8F0",
"text_color": "#4A3728",
"accent_color": "#E8850C",
"style_keyword": "warm, appetizing, cozy kitchen",
"design_tone": "따뜻하고 맛있어 보이는 푸드 스타일링",
},
"seafood": {
"bg_color": "#F0F8FF",
"text_color": "#1B3A54",
"accent_color": "#0077B6",
},
}
딕셔너리 접근법
theme = CATEGORY_THEMES["food"]
# → {"bg_color": "#FFF8F0", "text_color": "#4A3728", ...}
bg = theme["bg_color"]
# → "#FFF8F0"
# 키가 없으면? KeyError 에러가 난다.
# 안전한 방법:
bg = theme.get("bg_color", "#FFFFFF") # 없으면 기본값 "#FFFFFF" 반환
중첩 딕셔너리
# API 응답도 딕셔너리
response = {
"choices": [
{
"message": {
"content": "<div>상세페이지 HTML</div>"
}
}
]
}
# 실제 코드에서 이렇게 접근:
html = response["choices"][0]["message"]["content"]
OpenAI API 응답을 다룰 때 이 패턴을 매일 쓴다. response.choices[0].message.content는 이 딕셔너리 접근을 .으로 바꾼 것일 뿐이다.
4. with 문 — 열었으면 반드시 닫아라
파일, GPU 락, 브라우저 등 **"쓰고 나서 반드시 정리해야 하는 것"**에 with를 쓴다:
# 파일 처리
with open("result.html", "w", encoding="utf-8") as f:
f.write(html_content)
# with 블록이 끝나면 파일이 자동으로 닫힘
# GPU 락
with ImageGenerator._gpu_lock:
image = self.pipe(prompt=prompt).images[0]
# with 블록이 끝나면 락이 자동으로 해제됨
# Playwright 브라우저
async with async_playwright() as p:
browser = await p.chromium.launch()
# ... 작업
# with 블록이 끝나면 브라우저가 자동으로 종료됨
왜 중요한가? with 없이 쓰면 이런 일이 생긴다:
# ❌ 나쁜 예
f = open("result.html", "w")
f.write(html_content)
# 여기서 에러가 터지면? 파일이 안 닫힌다!
f.close()
# ✅ 좋은 예
with open("result.html", "w") as f:
f.write(html_content)
# 에러가 나든 안 나든 반드시 닫힘
GPU 락도 마찬가지다. with가 없으면 에러 시 락이 안 풀려서 다음 요청이 영원히 대기한다.
5. try/except — 에러를 잡아서 프로그램이 죽지 않게
AI 모델은 수시로 에러가 난다. 메모리 부족, 네트워크 끊김, 잘못된 응답 등. 이걸 처리 안 하면 전체 서비스가 죽는다:
try:
response = client.chat.completions.create(
model="gpt-5-mini",
messages=[{"role": "user", "content": prompt}],
)
raw = response.choices[0].message.content
return raw
except Exception as e:
print(f"⚠️ 섹션 작성 실패: {e}")
# 에러 나면 기본 HTML 반환 (서비스가 죽지 않게)
return f'<div style="padding:50px;">{section_name} 섹션</div>'
자주 쓰는 패턴
# 1. 구체적인 에러 잡기
try:
data = json.loads(raw_text)
except json.JSONDecodeError:
print("JSON 파싱 실패")
data = []
# 2. 여러 에러 잡기
try:
image = generator.generate(prompt)
except torch.cuda.OutOfMemoryError:
print("GPU 메모리 부족!")
torch.cuda.empty_cache()
except Exception as e:
print(f"알 수 없는 에러: {e}")
# 3. finally — 에러가 나든 안 나든 반드시 실행
try:
image = generator.generate(prompt)
except Exception as e:
print(f"에러: {e}")
finally:
torch.cuda.empty_cache() # 무조건 메모리 정리
실전 팁: except Exception as e에서 e를 반드시 출력하라. 안 그러면 뭐가 잘못된 건지 모른다.
6. f-string — 변수를 문자열에 끼워넣기
AI 프로젝트에서 프롬프트 만들 때 필수:
product_name = "유기농 모둠 쌈채소"
category = "farm"
# f-string: 중괄호 안에 변수를 넣으면 값으로 치환됨
prompt = f"제품: {product_name} ({category})"
# → "제품: 유기농 모둠 쌈채소 (farm)"
# 계산도 가능
print(f"VRAM 사용량: {torch.cuda.memory_allocated() / 1024**2:.1f} MB")
# → "VRAM 사용량: 15234.5 MB"
# 실제 프롬프트에서
prompt = f"""
역할: 대한민국 최고의 상세페이지 디자이너.
제품: {product_name} ({category})
배경색: {theme['bg_color']}
"""
f""에서 f를 빼먹으면 {product_name}이 그냥 문자열로 출력된다. 흔한 실수다.
7. os와 pathlib — 파일 경로 다루기
AI 프로젝트는 모델 파일, 이미지, HTML 등 파일을 끊임없이 다룬다:
import os
from pathlib import Path
# 환경변수 다루기
SHARED_MODEL_PATH = os.getenv('HF_HOME', '/data/shared_models/huggingface')
# HF_HOME 환경변수가 있으면 그 값, 없으면 기본값 사용
os.environ['HF_HOME'] = SHARED_MODEL_PATH # 환경변수 설정
# 경로 조합 — pathlib이 더 깔끔
BASE_DIR = Path(__file__).resolve().parent.parent.parent
# __file__ = 현재 파일 경로
# .parent = 상위 폴더 (한 번 = ai_core/, 두 번 = src/, 세 번 = Ad-Lib/)
output_dir = BASE_DIR / "output" # Ad-Lib/output/
output_dir.mkdir(parents=True, exist_ok=True)
# parents=True → 중간 폴더도 자동 생성
# exist_ok=True → 이미 있어도 에러 안 남
# 파일 존재 확인
if os.path.exists(reference_image):
print("참조 이미지 발견!")
os vs pathlib — 뭘 쓸까?
# os — 오래된 방식 (문자열 조합)
path = os.path.join("/home", "user", "output", "image.png")
# pathlib — 현대적 방식 (/ 연산자)
path = Path("/home") / "user" / "output" / "image.png"
pathlib이 읽기 쉽다. 새 코드에서는 pathlib을 쓰자.
8. 리스트 컴프리헨션 — 반복을 한 줄로
# 일반 for 루프
valid_images = []
for img_path in selected_images:
if os.path.exists(img_path):
valid_images.append(img_path)
# 리스트 컴프리헨션으로 같은 코드
valid_images = [img for img in selected_images if os.path.exists(img)]
# 실제 프로젝트에서
prompts = prompts[:num_images] # 리스트 슬라이싱: 앞에서 N개만
selected = [p.strip() for p in args.images.split(",") if p.strip()]
# "a.png, b.png, c.png" → ["a.png", "b.png", "c.png"]
읽는 법: [변환(x) for x in 리스트 if 조건]
- 리스트에서 x를 하나씩 꺼내서
- 조건에 맞으면
- 변환해서 새 리스트에 넣어라
9. threading — 동시 접근 제어
GPU는 한 번에 하나만 써야 한다. threading으로 제어한다:
import threading
class ImageGenerator:
_gpu_lock = threading.Semaphore(1) # 동시 1개만 허용
_init_lock = threading.Lock() # 초기화 보호
def generate(self, prompt):
with ImageGenerator._gpu_lock: # 여기서 대기
image = self.pipe(prompt=prompt).images[0]
return image # 락 자동 해제
Lock vs Semaphore:
- Lock() → 한 번에 1개 스레드만 (이진)
- Semaphore(1) → 한 번에 1개 스레드만 (Lock과 같음)
- Semaphore(3) → 한 번에 3개 스레드까지 (GPU가 3개면 이렇게)
10. 정규표현식(re) — 텍스트 정리
GPT 응답에서 원하는 부분만 뽑을 때 쓴다:
import re
# 파일명에서 한글/영문/숫자만 남기기
product_name = "프리미엄 망고향 샤인머스캣!! (500g)"
safe_name = re.sub(r'[^\w가-힣]', '', product_name)
# → "프리미엄망고향샤인머스캣500g"
# GPT 응답에서 JSON 추출
raw = "물론이죠! 결과는 다음과 같습니다:\n[\"prompt1\", \"prompt2\"]\n도움이 되었길..."
if "[" in raw and "]" in raw:
start = raw.find("[")
end = raw.rfind("]") + 1
data = json.loads(raw[start:end])
# → ["prompt1", "prompt2"]
# 코드블록 마크다운 제거 (GPT가 자꾸 ```html을 붙임)
raw = raw.replace("```html", "").replace("```", "").strip()
정규표현식은 전부 외울 필요 없다. re.sub(패턴, 대체, 문자열) 이 하나만 알면 80%는 커버된다.
11. *args, **kwargs — 유연한 함수 만들기
AI 라이브러리에서 자주 보이는 패턴:
# **kwargs (키워드 인자를 딕셔너리로 받기)
gen_kwargs = dict(
prompt=prompt,
num_inference_steps=28,
guidance_scale=3.5,
width=768,
height=768,
max_sequence_length=512,
)
# 조건에 따라 인자 추가
if self._ip_adapter_loaded:
gen_kwargs["ip_adapter_image"] = dummy_img
# 딕셔너리를 풀어서 함수에 넘기기
image = self.pipe(**gen_kwargs).images[0]
# 위 코드는 아래와 동일:
# self.pipe(prompt=prompt, num_inference_steps=28, guidance_scale=3.5, ...)
왜 이렇게 하나? 조건에 따라 함수 인자가 달라질 때, if/else로 함수를 두 번 호출하는 것보다 딕셔너리에 모아서 한 번 호출하는 게 깔끔하다.
12. hasattr / getattr — 속성이 있는지 확인
싱글턴 패턴에서 핵심적으로 쓰인다:
def __init__(self):
# 이미 초기화됐는지 확인
if hasattr(self, '_initialized') and self._initialized:
return # 이미 초기화된 인스턴스면 스킵
self._initialized = True
# ... 무거운 모델 로딩
- hasattr(객체, '속성이름') → 그 속성이 있으면 True, 없으면 False
- 최초 생성 시 _initialized 속성이 없으므로 hasattr이 False → 초기화 진행
- 두 번째부터는 True → return으로 스킵
실전 연습: 코드를 읽어보자
아래는 실제 프로젝트의 generate_brand_card 함수 일부다. 위에서 배운 개념이 몇 개 들어가 있는지 찾아보라:
def generate_brand_card(brand_name, product_name, category, features):
theme = get_theme(category) # (1)
print(f"🏷️ '{brand_name}' 브랜드 카드 생성 중...") # (2)
prompt = f"""
역할: 상세페이지 디자이너.
브랜드명: {brand_name}
제품명: {product_name}
""" # (3)
try: # (4)
response = client.chat.completions.create(
model="gpt-5-mini",
messages=[{"role": "user", "content": prompt}],
)
raw = response.choices[0].message.content # (5)
raw = raw.replace("```html", "").replace("```", "").strip()
return raw
except Exception as e: # (6)
return f'''<div style="width:860px; padding:80px 40px;
font-family:'Noto Sans KR';">
<div style="font-size:88px;">{brand_name}</div>
</div>''' # (7)
정답:
- 딕셔너리 반환 함수 호출 → theme["bg_color"] 등으로 접근
- f-string으로 변수 삽입
- 여러 줄 f-string (f"""...""")으로 GPT 프롬프트 구성
- try/except로 API 호출 에러 처리
- 딕셔너리(객체) 체이닝 접근 — response → choices[0] → message → content
- 에러를 e로 받아서 출력 가능
- f-string으로 폴백 HTML에 변수 삽입
'AI 엔지니어준비' 카테고리의 다른 글
| Claude 3명이 팀 개발했더니 하루 만에 앱이 배포됐다 (0) | 2026.03.25 |
|---|---|
| 코드 한 줄 안 쓰고 실제 카페에 투입한 재고관리 앱을 하루 만에 만든 방법 (0) | 2026.03.24 |
| (CUDA OutOfMemoryError) L4 GPU OOM 오류 대처 방법 (0) | 2026.02.14 |
| 📝 "L4 GPU 22GB에서 SDXL을 실전 서비스에 올리며 겪은 삽질들" (1) | 2026.02.11 |
| gcp git 설정 매번 아이디 패스워드 치기 귀찮을때 (0) | 2026.02.09 |