[2026.01.19] 재미나이 api 사용법 공부
[2026.01.20] 비동기 처리 / 1차 욕설필터
[2026.01.21] DjangoORM 쿼리 관련 3가지 정리
[2026.01.22]
[2026.01.23] getattr / hasattr / setattr
2026.01.19 ✅
스크립트
유저/게임/리뷰 생성
from django.utils import timezone
from apps.game.models.game import Game
from apps.community.models.reviews import Review
from apps.user.models.user import User
import random
user, _ = User.objects.get_or_create(
email="ai_tester_2@example.com",
defaults={'nickname': 'AI테스터2', 'name': '테스터2'}
)
game, _ = Game.objects.get_or_create(
name="테스트용 게임 3",
defaults={
'intro': "테스트 게임입니다.",
'released_at': timezone.now(),
'developer': "Test Dev",
'avg_score': 4.5
}
)
sample_reviews = [
"그래픽이 훌륭합니다.", "재미있어요!", "버그가 좀 있네요.", "친구랑 하기 좋아요.",
"사운드가 좋습니다.", "조작감이 별로예요.", "업데이트 좀 해주세요.", "갓겜입니다.",
"할만합니다.", "가격이 좀 비싸요.", "최적화가 아쉽네요.", "스토리가 감동적입니다.",
"멀티플레이 추천합니다.", "고인물 게임이네요.", "나쁘지 않아요."
]
for content in sample_reviews:
Review.objects.create(
game=game,
user=user,
content=content,
rating=random.randint(1, 5)
)
print(f"✅ 게임(ID: {game.id})에 리뷰 {len(sample_reviews)}개 생성 완료!")
사용가능한 모델 찾기
from google import genai
from django.conf import settings
import os
api_key = settings.GEMINI_API_KEY or os.environ.get("GEMINI_API_KEY")
print(f"Checking models for API Key: {api_key[:10]}... (masked)")
client = genai.Client(api_key=api_key)
try:
print("\n[Available Models]")
for m in client.models.list():
if "generateContent" in m.supported_actions:
print(f"- {m.name}")
except Exception as e:
print(f"\n❌ Error listing models: {e}")
사용 가능 모델(내 기준)
[Available Models]
- models/gemini-2.5-flash
- models/gemini-2.5-pro
- models/gemini-2.0-flash-exp
- models/gemini-2.0-flash
- models/gemini-2.0-flash-001
- models/gemini-2.0-flash-exp-image-generation
- models/gemini-2.0-flash-lite-001
- models/gemini-2.0-flash-lite
- models/gemini-2.0-flash-lite-preview-02-05
- models/gemini-2.0-flash-lite-preview
- models/gemini-exp-1206
- models/gemini-2.5-flash-preview-tts
- models/gemini-2.5-pro-preview-tts
- models/gemma-3-1b-it
- models/gemma-3-4b-it
- models/gemma-3-12b-it
- models/gemma-3-27b-it
- models/gemma-3n-e4b-it
- models/gemma-3n-e2b-it
- models/gemini-flash-latest
- models/gemini-flash-lite-latest
- models/gemini-pro-latest
- models/gemini-2.5-flash-lite
- models/gemini-2.5-flash-image
- models/gemini-2.5-flash-preview-09-2025
- models/gemini-2.5-flash-lite-preview-09-2025
- models/gemini-3-pro-preview
- models/gemini-3-flash-preview
- models/gemini-3-pro-image-preview
- models/nano-banana-pro-preview
- models/gemini-robotics-er-1.5-preview
- models/gemini-2.5-computer-use-preview-10-2025
- models/deep-research-pro-preview-12-2025
Plus ➕
테스트 코드(Mock)
Mocking
- 가짜 객체 만들기
- 외부 시스템(API, DB 등) 없이 로직만 테스트하기 위함
@patch
- 바꿔치기
- 테스트 동안만 진짜 객체를 가짜 객체로 교체함
함수 표기법(Naming)
| 구분 | 표기법 (Naming) | 의미 및 역할 | 문법적 특징 |
|---|
| Public | generate_summary | 공개 API. 외부에서 마음껏 호출해도 되는 대표 기능입니다. | from module import * 사용 시 임포트됩니다. |
| Internal (Protected) | _generate_summary | 내부용. 클래스나 모듈 내부에서만 쓰라고 만든 헬퍼 함수입니다. | from module import * 사용 시 임포트되지 않습니다. (직접 지정하면 가능) |
| Private (Mangling) | __generate_summary | 비공개. 자식 클래스에서도 건드리지 못하게 숨깁니다. | 이름이 _ClassName__method로 바뀌어 외부 접근이 까다로워집니다. |
함수명에 _ 를 붙이는 이유
- "이것은 내부적으로만 사용하는 함수이니, 밖에서 함부로 가져다 쓰지 마세요" 라는 뜻
2026.01.20 ✅
스크립트
리뷰 생성 스크립트(10자 이상)
from django.utils import timezone
from apps.game.models.game import Game
from apps.community.models.reviews import Review
from apps.user.models.user import User
import random
import time
user, _ = User.objects.get_or_create(
email="ai_tester_new@example.com",
defaults={'nickname': 'AI테스터New', 'name': '테스터New'})
unique_name = f"테스트용 게임 {int(time.time())}"
game = Game.objects.create(
name=unique_name,
intro="매번 새로 생성되는 테스트 게임입니다.",
released_at=timezone.now(),
developer="Test Dev",
avg_score=4.5
)
sample_reviews = [
"그래픽이 정말 훌륭하고 디테일이 살아있습니다.",
"친구들과 함께 즐기기에 부족함이 없는 게임이에요.",
"초반에는 조금 어렵지만 익숙해지면 정말 재미있습니다.",
"사운드 트랙이 너무 좋아서 계속 듣게 되네요.",
"조작감이 약간 불편하지만 패치로 고쳐질 거라 믿어요.",
"가격 대비 볼륨이 풍부해서 돈이 아깝지 않습니다.",
"최적화가 잘 되어 있어서 낮은 사양에서도 잘 돌아가요.",
"스토리가 감동적이라 엔딩 보고 눈물 흘렸습니다.",
"멀티플레이 서버가 안정적이라 쾌적하게 플레이했어요.",
"업데이트 주기가 빨라서 개발사가 신경 쓰는 게 느껴져요.",
"고인물들이 많긴 하지만 뉴비 배척은 없는 편입니다.",
"버그가 가끔 있지만 플레이에 지장은 없는 수준이에요.",
"캐릭터 디자인이 매력적이라 수집하는 재미가 쏠쏠합니다.",
"타격감이 시원시원해서 스트레스가 확 풀리네요.",
"전반적으로 완성도가 높은 수작이라고 생각합니다."
]
for content in sample_reviews:
Review.objects.create(
game=game,
user=user,
content=content,
rating=random.randint(1, 5)
)
print(f"✅ 새로운 게임 생성 완료!")
print(f" - 게임명: {game.name}")
print(f" - Game ID: {game.id}")
print(f" - 리뷰 수: {len(sample_reviews)}개")
비동기 테스트
from apps.game.models.game import Game
from apps.community.models.reviews import Review
from apps.user.models.user import User
from django.utils import timezone
import time
user, _ = User.objects.get_or_create(
email="celery_tester@test.com",
defaults={'nickname': 'Tester', 'password': 'test'}
)
game, _ = Game.objects.get_or_create(
name="Celery Test Game Check",
defaults={'released_at': timezone.now(), 'developer': "Test Dev"}
)
print(f"✅ 준비 완료! User ID: {user.id}, Game ID: {game.id}")
print("🚀 리뷰 생성을 시작합니다...")
for i in range(1, 11):
review = Review.objects.create(
game=game,
user=user,
content=f"이것은 AI 요약을 위한 {i}번째 테스트 리뷰입니다. 글자수를 채웁니다.",
rating=5
)
print(f"✅ 리뷰 {i}개 저장 완료.")
if i == 10:
print("\n✨ [EVENT] 10번째 리뷰 저장됨! Signal이 Celery를 호출했을 것입니다!")
print("👀 **지금 바로 Celery 터미널(터미널 1)을 확인하세요!**\n")
비동기 처리
client.aio (비동기 SDK)
- genai에서는 기본적으로 비동기처리를 지원함
- 장점
- Redis/Celery 설정 불필요: 인프라가 간단
- 리소스 효율: AI 응답을 기다리는 동안 서버 CPU가 놀지 않고 다른 일을 함
- 단점
- 사용자 대기(Loading)
- 서버는 편해지지만, 사용자(브라우저)는 여전히 응답이 올 때까지 기다려야 함
- Timeout
- AI 생성이 30초~1분 걸리면,
- Nginx나 브라우저에서 "응답 시간 초과(Timeout)"로 연결을 끊을 수 있음
- DRF 호환성
- Django REST Framework(DRF)의 APIView는 기본적으로 동기(Sync) 방식
- async 함수를 바로 붙이려면 adrf 같은 별도 패키지를 쓰거나 복잡한 래핑이 필요
기본구조
class ReviewSummaryService:
...
async def get_summary_async(self, game_id: int) -> dict:
... (Game 조회 등은 비동기 ORM이나 sync_to_async 필요) ...
response = await self.client.aio.models.generate_content(
model=self.model_name,
contents=user_prompt,
config=types.GenerateContentConfig(
response_mime_type="application/json",
response_schema=GameSummary,
),
)
return json.loads(response.text)
Celery (비동기 작업 큐)
- 사용자에게 "요청 받음(ID: 123)"만 바로 알려주고, 실제 작업은 뒷단(Worker)에서 처리
비동기처리 선택
client.aio 사용하게 된다면?
- 유저가 10번째 리뷰 등록 클릭
- 서버가 리뷰 10개가 되서 요약시작
- 유저화면 멈춤(로딩)
- 5~10초 후 ai응답이 도착하여 DB에 저장하고 유저에게 등록완료 응답보냄
- 유저는 리뷰 등록하는데 시간이 오래걸린다고 느낌
- 즉, 단순 비동기를 사용하면 리뷰작성자중에 억울하게 기다리는 사람이 생김
Celery를 선택하면?
- 유저가 등록 클릭
- 서버가 리뷰 등록 완료 응답 보내고 Celery에게 요약해달라고 요청함
- 유저화면에서는 즉시 등록완료 처리
- 유저가 다른 페이지를 보는동안 Celery가 일을 함
Celery를 사용하는 김에 Django Signal까지 사용
- "Signal은 '사건이 터졌다고 알리는 호루라기'
- Celery는 '그 소리를 듣고 일을 처리하는 일꾼'
Django Signal(시그널)
- 어떤 특정 행동(이벤트)이 발생했을 때
- 작업을 직접 수행하지 않고, "작업을 예약(Dispatch)"하는 역할만 수행
- 약속된 함수들에게 "야! 방금 무슨 일 생겼어!" 라고 신호를 보내는 역할
특징
- 동기(Synchronous) 방식
- Signal을 쓴다고 자동으로 백그라운드 처리가 되지 않음
- Signal에 연결된 함수(Receiver)가 10초 걸리면
- 의존성 분리 (Decoupling)
- 리뷰를 저장하는 코드(views.py)는 요약 로직을 몰라도 됨
- 그저 "저장됐다"는 신호만 보내면, 요약 로직이 알아서 듣고 반응함
- Signal 자체는 동기적으로 작동하므로 느림
- 그래서 무거운 작업은 Celery에게 넘겨야 함
구성요소
- Sender (보내는 놈): 이벤트를 발생시키는 주체
- 예: Review 모델 (리뷰가 저장될 때 신호를 보냄)
- Signal (신호 종류): 어떤 타이밍에 보낼 것인가?
- pre_save: 저장되기 직전 (데이터 조작 시 사용)
- post_save: 저장된 직후 (주로 Celery 호출할 때 사용)
- post_delete: 삭제된 직후
- Receiver (받는 놈): 신호를 받으면 실행될 함수
- 예: "리뷰 개수 체크하고 Celery 부르는 함수"
동작 흐름(내 코드 기준)
- 사용자: 리뷰 작성 -> 저장 완료! (DB 반영)
- Signal (post_save): "어? 리뷰 저장됐네? 함수 실행!"
- (여기서 조건 체크: 10개 넘었나? 30일 지났나?)
- (조건 맞으면) -> "Celery야, run_ai_summary 실행해줘! (.delay())"
- Signal 종료: Signal 함수는 Celery에게 지시만 내리고 0.01초 만에 종료
- 사용자: 화면에 바로 "리뷰 등록 성공" 뜸. (대기 없음)
- Celery: (뒷단에서 혼자) AI에게 데이터 보내고 요약 시작
Redis
- Django와 Celery는 서로 다른 프로그램(프로세스)이라서 직접 대화할 수 없음
- 중간에 메시지를 보관해줄 장소가 필요한데, 그게 바로 Redis임
ex
- 상황
- 손님(Django)이 주방장(Celery)에게 주문을 하려고 함
- Redis가 없으면
- 손님이 주방장 귀에 대고 계속 서 있어야 함 (동기 방식, 서버 멈춤)
- Redis가 있으면
- 손님이 주문서(Redis)에 주문을 붙여놓고 바로 자리로 감
- 주방장은 요리가 끝나는 대로 주문서에서 다음 주문을 가져감 (비동기 방식)
요약
| 단계 | 주체 (Component) | 동작 (Action) | 상세 설명 |
|---|
| 1 | User / Client | 리뷰 작성 (POST /reviews) | 사용자가 게임에 대한 리뷰를 작성하고 저장을 요청합니다. |
| 2 | Django View | DB 저장 (save()) | 리뷰 데이터가 DB에 저장됩니다. (아직 동기 처리) |
| 3 | Signal (ai/signals.py) | post_save 감지 | 리뷰 저장이 완료되면 시그널이 트리거되어 조건을 검사합니다. |
| 4 | Producer | Task 발행 (.delay()) | 조건(리뷰 10개 이상 등)이 맞으면, 작업을 실행하지 않고 메시지 큐(Redis)로 "작업 요청서"를 보냅니다. |
| 5 | Message Broker | 큐(Queue) 대기 | Redis는 요청받은 작업을 메모리에 담아두고 일할 사람이 올 때까지 대기합니다. |
| 6 | Celery Worker | 작업 수령 및 실행 | 별도로 떠 있는 워커 프로세스가 Redis에서 작업을 가져가 run_ai_summary를 실행합니다. |
| 7 | Service (ai/services.py) | AI API 호출 | Google Gemini API에 데이터를 보내고 요약 결과를 받습니다. (시간이 오래 걸림) |
| 8 | Database | 결과 저장 | 최종 요약 결과를 GameReviewSummary 테이블에 저장합니다. |
욕설 1차 필터링
선택지
- 시리얼라이저에서 리뷰를 등록할때 욕설이 포함되어 있으면 등록 불가하게 만들기
- 간단한데 욕설은 리뷰에 등록하지 못하는건 말이 안됨
- 욕설이 포함된 리뷰는 생략하는 방향으로 가고 싶음
- 리뷰모델에 욕설여부 컬럼을 추가하여 이 컬럼이 True인 경우
- 요약에 필요한 데이터로 가져가지 않게 하기
- 단점: 일이 너무 커짐, 수정해야할 부분이 한트럭이 됨
- AI 호출 전 Python 서비스 계층에서의 1차 필터링 ✅
정규표현식(Regex)
.*?
- "그 사이에 뭐가 들어가든(숫자, 특수문자, 공백 등) 다 무시하고 잡아라"
- 이걸 사용하면 "시간이 지나서 발견" 같은 단어도 잡힘
ex. r"시.*?발"
- 시발 (기본)
- 시1발 (숫자 섞음)
- 시...발 (특수문자)
- 시 발 (공백)
- 시@#$%발 (이상한 문자)
- 시123123발 (긴 숫자)
- "시"로 시작해서 "발"로 끝나는 모든 단어를 잡음
ex. r"시[^가-힣]*?발"
- 잡음: 시1발, 시.발, 시..발, 시 ^& 발
- 시린발, 시골발, 시간이 지나서 발견
safety_settings
- Google Gemini AI가 유해한 콘텐츠를 생성하거나 입력받았을 때
- 얼마나 엄격하게 차단할지 결정하는 안전장치 설정
4가지 유해 카테고리
HARM_CATEGORY_HATE_SPEECH (혐오 발언)
HARM_CATEGORY_HARASSMENT (괴롭힘)
HARM_CATEGORY_SEXUALLY_EXPLICIT (선정성)
HARM_CATEGORY_DANGEROUS_CONTENT (위험 콘텐츠)
threshold(차단 기준)
BLOCK_LOW_AND_ABOVE(매우 엄격)
- 조금이라도(Low) 위험해 보이면 즉시 차단 (기본값에 가까움)
BLOCK_MEDIUM_AND_ABOVE(보통)
BLOCK_ONLY_HIGH(최소 차단)
- 확실히 위험하고 심각한(High) 수준일 때만 차단, 경미한 욕설 등은 허용
BLOCK_NONE(차단 안 함)
- 필터를 끔 (일부 계정에서는 사용 불가할 수 있음)
safety_settings = [
types.SafetySetting(
category=types.HarmCategory.HARM_CATEGORY_HATE_SPEECH,
threshold=types.HarmBlockThreshold.BLOCK_ONLY_HIGH,
),
types.SafetySetting(
category=types.HarmCategory.HARM_CATEGORY_HARASSMENT,
threshold=types.HarmBlockThreshold.BLOCK_ONLY_HIGH,
),
types.SafetySetting(
category=types.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
threshold=types.HarmBlockThreshold.BLOCK_ONLY_HIGH,
),
types.SafetySetting(
category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
threshold=types.HarmBlockThreshold.BLOCK_ONLY_HIGH,
),
]
코드 리뷰
re.compile("|".join(BAD_PATTERNS), re.IGNORECASE)
- 리스트에 있는 수많은 욕설 패턴들을 하나로 합쳐서
- 한 번의 스캔으로 빠르게 잡아내는 검색 도구를 만드는 과정
"|".join(BAD_PATTERNS)
- join은 리스트 안에 있는 문자열들을 이어주는 함수
- 여기서 이어주는 역할을 하는 것이 바로
| (파이프) 기호
- 정규표현식에서
| 는 "또는(OR)" 을 의미
re.compile(...)
- re.compile을 하면
- 파이썬이 이 복잡한 패턴을 기계가 이해하기 쉬운 형태로 미리 최적화해줌
re.IGNORECASE
Plus ➕
쉘스크립트(sh)사용법
chmod +x 스크립트이름.sh
./스크립트 이름
host 처리방법
- os.environ.get('POSTGRES_HOST', 'localhost')
- 환경변수에서 'POSTGRES_HOST'라는 이름의 값을 찾음
- 만약 값을 찾으면(도커 환경 등) 그 값을 사용
- 만약 값을 못 찾으면(로컬 환경 등) 뒤에 있는 'localhost'를 기본값으로 사용
- environment: - POSTGRES_HOST=PlayType_db
- 이 부분은 도커 컨테이너가 실행될 때, 'POSTGRES_HOST'라는 환경변수를
- 'PlayType_db'로 강제 고정하겠다는 의미
- 이렇게 명시해주면, .env 파일에 'localhost'라고 적혀있어도
- 도커 내부에서는 무시하고 'PlayType_db'를 사용하게 됨
DATABASES = {
'default': {
...
'HOST': os.environ.get('POSTGRES_HOST', 'localhost'), ✅
'PORT': '5432',
}
}
services:
web:
build: .
environment:
- POSTGRES_HOST=PlayType_db ✅
- POSTGRES_DB=PlayType_db
- POSTGRES_USER=db_user
2026.01.21 ✅
정방향 참조 / 1:1, N:1
- "DB야, 어차피 연결된 거니까 JOIN해서 한 방에 다 가져와!"
- "결과가 무조건 1개만 나오는 관계"에서만 사용 가능
- SQL의 JOIN 문을 사용

ForeignKey 나 OneToOneField 를 따라갈 때 사용
- ForeignKey (N:1 관계의 'N' 쪽에서 '1'을 부를 때)
- OneToOneField (1:1 관계)
사용 / 미사용 비교
class Review(TimeStampedModel):
...
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name="reviews",)
——————————————————————————————————————[비교]—————————————————————————————————————————
reviews = Review.objects.all()
for review in reviews:
print(review.user.nickname)
——————————————————————————————————————[비교]—————————————————————————————————————————
reviews = Review.objects.select_related('user').all()
for review in reviews:
print(review.user.nickname)
역방향 참조 / 1:N, M:N
- "DB야, JOIN하면 너무 복잡하니까... 그냥 따로 검색해서 내가 파이썬에서 합칠게!"
- 쿼리를 2번 날림(메인 쿼리 1번 + 연관 데이터 쿼리 1번)
ManyToOne (역참조)이나 ManyToMany 관계일 때 사용
- ManyToManyField (다대다 관계)
- 역참조 (related_name으로 접근하는 1:N 관계)
사용 / 미사용 비교
- related_name 미설정시
prefetch_related('reviewcomment_set')
class ReviewComment(TimeStampedModel):
review = models.ForeignKey(
Review,
on_delete=models.CASCADE,
related_name="comments",
)
——————————————————————————————————————[비교]—————————————————————————————————————————
reviews = Review.objects.all()
for review in reviews:
comments = review.comments.all()
——————————————————————————————————————[비교]—————————————————————————————————————————
reviews = Review.objects.prefetch_related('comments').all()
DB에서 일어나는 일 (SQL)
- 1단계 (리뷰 가져오기)
SELECT * FROM reviews; (결과: 리뷰 ID [1, 2, 3] 확보)
- 2단계 (관련 댓글 한꺼번에 가져오기)
SELECT * FROM comments WHERE review_id IN (1, 2, 3);
- 3단계 (파이썬 내부 동작)
- Django가 1단계와 2단계 결과를 메모리에서 ID 기준으로
- 조립해서 review.comments에 넣어둠
Prefetch
- "
prefetch_related를 쓸 건데... 조건(필터, 정렬)을 좀 걸어서 가져오고 싶어"
- 그냥
prefetch_related('comments') 를 하면 모든 댓글을 가져옴
- 하지만 삭제 안 된 댓글"만, 그것도 "최신순"으로 가져오고 싶다면
Plus ➕
models/init.py
- models.py 파일 하나만 사용할 때는
__init__.py를 건드릴 필요가 없음
- 하지만 models를 폴더(패키지)로 만들어서
- 여러 파일(reviews.py, comments.py 등)로 쪼개서 관리하는 경우에는 건드려야 함
from .reviews import Review
from .review_like import ReviewLike
from .comments import ReviewComment
2026.01.22 ✅
스크립트
게임 생성 스크립트
from django.utils import timezone
from apps.game.models.game import Game
from apps.community.models.reviews import Review
from apps.user.models.user import User
import random
import time
unique_name = f"테스트용 게임 {int(time.time())}"
game = Game.objects.create(
name=unique_name,
intro="매번 새로 생성되는 테스트 게임입니다.",
released_at=timezone.now(),
developer="Test Dev",
avg_score=4.5
)
Plus ➕
2026.01.23 ✅
getattr
getattr(object, name, default)
- 객체의 속성(Attribute)이나 메서드를 문자열(String) 이름으로 접근할 때 사용하는 함수
- 보통 object.attribute 형태로 접근하지만
- 속성 이름을 코드가 실행될 때(Runtime) 동적으로 결정해야 할 때 유용
장단점
| 구분 | 장점 (Pros) | 단점 (Cons) |
|---|
| 동적 접근 | 문자열을 이용해 속성에 접근하므로, 반복문이나 사용자 입력에 따라 유연하게 처리 가능합니다. | IDE의 자동 완성이나 정적 분석 도구(Linter)가 속성 존재 여부를 미리 확인할 수 없습니다. |
| 코드 간결성 | 수많은 if-else 분기 처리를 줄이고, 메타 프로그래밍(Factory 패턴 등) 구현 시 코드가 깔끔해집니다. | 코드가 직관적이지 않아, 어떤 속성을 호출하는지 한눈에 파악하기 어려워 가독성이 떨어질 수 있습니다. |
| 안정성 | default 값을 설정할 수 있어, 속성이 없을 때 AttributeError 발생을 막고 안전한 처리가 가능합니다. | 남발할 경우, 디버깅 시 어디서 잘못된 속성이 호출되었는지 추적하기가 일반 접근법보다 어렵습니다. |
코드 예시
class RPGCharacter:
def __init__(self, name, hp, mp):
self.name = name
self.hp = hp
self.mp = mp
def attack(self):
return f"{self.name} attacks!"
hero = RPGCharacter("Warrior", 100, 50)
print(f"일반 접근: {hero.name}")
print(f"getattr: {getattr(hero, 'name')}")
"""
1. hero.name과 동일한 기능을 수행
- 하지만 'name'이라는 문자열을 통해 접근하므로
- 이 문자열을 외부 입력이나 변수에서 받아올 수 있다는 점이 다릅니다.
"""
attr_to_find = 'hp'
value = getattr(hero, attr_to_find)
print(f"동적 접근 ({attr_to_find}): {value}")
"""
- attr_to_find 변수에 'hp', 'mp' 등 어떤 문자열이 들어오느냐에 따라 가져오는 값이 달라짐
- 이를 통해 유연한 코딩이 가능함
"""
———————————————————————[getattr(obj, 'key', default_value)]——————————————————————————
defense = getattr(hero, 'defense', 0)
print(f"없는 속성 접근 (기본값): {defense}")
"""
- hero 객체에는 'defense'라는 속성이 없음
- 만약 hero.defense로 접근했다면 AttributeError가 발생하여 프로그램이 멈춤
- getattr의 세 번째 인자로 0(기본값)을 주었기 때문에, 에러 없이 0이라는 값을 안전하게 반환
"""
method_name = 'attack'
if hasattr(hero, method_name):
action = getattr(hero, method_name)
print(f"메서드 실행: {action()}")
"""
- 속성뿐만 아니라 함수(메서드)도 객체이므로 가져올 수 있음
- action() 처럼 괄호를 붙여 실행하면 해당 메서드가 호출됨
- 주로 'Dispatcher' 패턴(명령어에 따라 함수 실행 분기)을 짤 때 유용
"""
hasattr
hasattr(object, name)
- 객체 안에 특정 이름의 속성이나 메서드가 존재하는지 여부(True/False)를 확인함
- 주로 에러 방지용 안전장치로 사용
장단점
| 특징 | 장점 (Pros) | 단점 (Cons) |
|---|
| 안정성 확보 | 속성이 없을 때 발생하는 AttributeError로 인해 프로그램이 비정상 종료되는 것을 막아줍니다. | 남용할 경우, 코드가 에러를 조용히 삼켜버려 나중에 논리적인 버그를 찾기 어려워질 수 있습니다. |
| 흐름 제어 | if hasattr(...): 구문을 사용하여 기능 유무에 따라 다른 로직을 실행하기 쉽습니다. | Python에서는 "일단 실행하고 에러를 잡는(EAFP)" 방식을 더 선호하는 경향이 있어, 성능상 미세하게 느릴 수 있습니다. |
사용 예시
class Plugin:
def execute(self):
return "실행 중..."
valid_plugin = Plugin()
invalid_plugin = "나는 그냥 문자열입니다"
def run_plugin(obj):
if hasattr(obj, 'execute'):
print(f"성공: {obj.execute()}")
"""
- obj 객체 내부에 'execute'라는 이름의 멤버(변수나 함수)가 존재하는지 True/False로 반환
"""
else:
print("실패: 실행할 수 없는 객체입니다.")
run_plugin(valid_plugin)
run_plugin(invalid_plugin)
"""
- 만약 이 검사 없이 invalid_plugin.execute()를 바로 호출했다면
- 'AttributeError: 'str' object has no attribute 'execute''
- 에러가 발생하며 프로그램이 멈췄을 것
- hasattr 덕분에 else 블록으로 안전하게 넘어가 예외 처리를 수행함
"""
setattr
setattr(object, name, value)
- 객체의 특정 속성에 값을 할당하거나, 새로운 속성을 동적으로 추가할 때 사용
object.name = value와 같은 역할을 하지만
- 속성 이름을 문자열로 처리한다는 점이 다름
장단점
| 특징 | 장점 (Pros) | 단점 (Cons) |
|---|
| 대량 처리 | 딕셔너리(JSON) 같은 데이터 꾸러미를 반복문을 통해 객체 속성으로 한 번에 변환하기 매우 좋습니다. | 의도치 않게 클래스의 중요 메서드나 내부 속성을 덮어써 버릴(Overwrite) 위험이 있습니다. |
| 확장성 | 클래스 정의 시점에 없던 속성도 실행 중에 필요에 따라 자유롭게 추가할 수 있습니다. | 어떤 속성이 추가되었는지 코드를 읽는 사람이나 IDE가 파악하기 힘들어 코드 추적이 어려워집니다. |
사용예시
class EmptyBox:
pass
box = EmptyBox()
item_data = {
"item_id": 101,
"name": "Apple",
"price": 3000
}
for key, value in item_data.items():
setattr(box, key, value)
"""
- EmptyBox 클래스는 아무 내용도 없지만(pass)
- setattr을 통해 'item_id', 'name', 'price'라는 속성이 인스턴스(box)에 생성됨
- 결과적으로 box.name, box.price 처럼 접근이 가능해짐
"""
print(f"상품명: {box.name}")
print(f"가격: {box.price}")
—————————————————————————[중요: 기존 메서드 덮어쓰기 주의 예시]————————————————————————————
class Calculator:
def add(self):
print("더하기 기능")
calc = Calculator()
setattr(calc, 'add', "이제 함수가 아닙니다")
try:
calc.add()
except TypeError as e:
print(f"\n에러 발생: {e}")
"""
- 원래 calc.add는 함수(메서드)였습니다.
- setattr(calc, 'add', "문자열")을 실행하는 순간, add는 더 이상 함수가 아니라 "문자열 변수"가 됨
- 이후 calc.add()를 호출하면 문자열은 호출(Call)할 수 없으므로 TypeError가 발생
- (setattr 사용 시 가장 주의할 점)
"""
Plus ➕