Backend Β· Python Β·

[AI Assignment - Python] πŸŽ€ λ°μ½”λ ˆμ΄ν„° μ™„μ „ 정볡 β€” timer, retry, validate_types 직접 κ΅¬ν˜„

🎯 과제: μ‹€λ¬΄ν˜• λ°μ½”λ ˆμ΄ν„° 3개 κ΅¬ν˜„ #

  1. @timer β€” ν•¨μˆ˜ μ‹€ν–‰ μ‹œκ°„ μΈ‘μ •
  2. @retry(max_attempts=3) β€” μ‹€νŒ¨ μ‹œ μžλ™ μž¬μ‹œλ„
  3. @validate_types β€” νƒ€μž… 힌트 기반 λŸ°νƒ€μž„ νƒ€μž… 검증

λͺ¨λ“  λ°μ½”λ ˆμ΄ν„°μ— functools.wrapsλ₯Ό μ μš©ν•˜λŠ” 것이 μš”κ΅¬μ‚¬ν•­μ΄μ—ˆμŠ΅λ‹ˆλ‹€.


πŸ“ λ°μ½”λ ˆμ΄ν„° κΈ°λ³Έ ꡬ쑰 #

κ³Όμ œμ— λ“€μ–΄κ°€κΈ° 전에 λ¨Όμ € κΈ°λ³Έ ꡬ쑰λ₯Ό μ •λ¦¬ν–ˆμŠ΅λ‹ˆλ‹€.

 1def my_decorator(func):
 2    def wrapper(*args, **kwargs):
 3        # ν•¨μˆ˜ μ‹€ν–‰ 전에 ν•  일
 4        result = func(*args, **kwargs)
 5        # ν•¨μˆ˜ μ‹€ν–‰ 후에 ν•  일
 6        return result
 7    return wrapper
 8
 9# @my_decoratorλŠ” 사싀 이것과 κ°™μŠ΅λ‹ˆλ‹€:
10# say_hello = my_decorator(say_hello)

인자λ₯Ό λ°›λŠ” λ°μ½”λ ˆμ΄ν„°λŠ” ν•œ κ²Ή 더 κ°μŒ‰λ‹ˆλ‹€:

1def repeat(n: int):           # ← λ°μ½”λ ˆμ΄ν„° νŒ©ν† λ¦¬
2    def decorator(func):      # ← μ‹€μ œ λ°μ½”λ ˆμ΄ν„°
3        def wrapper(*args, **kwargs):
4            ...
5        return wrapper
6    return decorator

1️⃣ @timer #

1μ°¨ μ‹œλ„ #

 1def timer(func):
 2    def running_check(*args, **kwargs):
 3        start_time = time.time()
 4        result = func(*args, **kwargs)
 5        end_time = time.time()
 6        if result == 'done':
 7            text = f"좜λ ₯: slow_function μ‹€ν–‰ μ‹œκ°„: {end_time - start_time:.2f}초"
 8        return text
 9    print(running_check().__repr__())
10    return running_check

지적받은 점 3κ°€μ§€ #

if result == 'done' ν•˜λ“œμ½”λ”©: λ°μ½”λ ˆμ΄ν„°λŠ” μ–΄λ–€ ν•¨μˆ˜λ“  κ°μŒ€ 수 μžˆμ–΄μ•Ό ν•©λ‹ˆλ‹€. 'done'을 λ°˜ν™˜ν•˜λŠ” ν•¨μˆ˜μ—μ„œλ§Œ μž‘λ™ν•˜λ©΄ λ²”μš©μ„±μ΄ μ—†μŠ΅λ‹ˆλ‹€.

textλ₯Ό λ°˜ν™˜ν•˜κ³  resultλ₯Ό μ†Œμ‹€: μ›λž˜ ν•¨μˆ˜μ˜ λ°˜ν™˜κ°’μ„ λŒλ €μ€˜μ•Ό ν•˜λŠ”λ°, 좜λ ₯ λ¬Έμžμ—΄μ„ λ°˜ν™˜ν•˜κ³  μžˆμ—ˆμŠ΅λ‹ˆλ‹€. ifλ₯Ό 타지 μ•ŠμœΌλ©΄ textκ°€ λ―Έμ •μ˜λ˜μ–΄ NameError도 λ°œμƒν•©λ‹ˆλ‹€.

λ°μ½”λ ˆμ΄ν„° μ•ˆμ—μ„œ 직접 μ‹€ν–‰: print(running_check()) 이 쀄 λ•Œλ¬Έμ— @timerκ°€ λΆ™λŠ” μˆœκ°„ ν•¨μˆ˜κ°€ λ°”λ‘œ μ‹€ν–‰λ©λ‹ˆλ‹€. λ°μ½”λ ˆμ΄ν„°λŠ” 감싼 ν•¨μˆ˜λ₯Ό λ°˜ν™˜λ§Œ ν•΄μ•Ό ν•©λ‹ˆλ‹€.

1# μ‹€ν–‰ 타이밍 이해
2@timer                    # ← 이 μ‹œμ μ— timer(slow_function) μ‹€ν–‰
3def slow_function():      #    β†’ wrapperλ₯Ό λ°˜ν™˜λ§Œ ν•΄μ•Ό 함
4    ...
5
6slow_function()           # ← 이 μ‹œμ μ— wrapper() μ‹€ν–‰

2μ°¨ μ‹œλ„ (μ™„μ„±) #

1def timer(func):
2    @wraps(func)
3    def wrapper(*args, **kwargs):
4        start = time.time()
5        result = func(*args, **kwargs)
6        elapsed = time.time() - start
7        print(f"{func.__name__} μ‹€ν–‰ μ‹œκ°„: {elapsed:.2f}초")
8        return result
9    return wrapper

Tip: 핡심 νŒ¨ν„΄ β€” wrapperλŠ” resultλ₯Ό λ°˜ν™˜, λ°μ½”λ ˆμ΄ν„°λŠ” wrapperλ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€.


2️⃣ @retry #

1μ°¨ μ‹œλ„ #

1def retry(max_attempts: int):
2    def decorator(func):
3        def wrapper(*args, **kwargs):
4            for _ in range(max_attempts):
5                result = func(*args, **kwargs)
6            return result
7        return wrapper
8    return decorator

μž¬μ‹œλ„ λ‘œμ§μ„ λ°μ½”λ ˆμ΄ν„° μ•ˆμ΄ μ•„λ‹ˆλΌ __main__에 λ”°λ‘œ μž‘μ„±ν–ˆμŠ΅λ‹ˆλ‹€:

1# __main__μ—μ„œ 직접 μž¬μ‹œλ„ β€” λ°μ½”λ ˆμ΄ν„°μ˜ μ˜λ―Έκ°€ 없어짐
2for i in range(max_attempts):
3    try:
4        check = unstable_api_call()
5        if check['status'] == 'ok':
6            break
7    except Exception as e:
8        print(f"μ‹œλ„ {i+1}/{max_attempts} μ‹€νŒ¨: {e}")

⚠️ λ°μ½”λ ˆμ΄ν„°μ˜ κ°€μΉ˜λŠ” ν˜ΈμΆœν•˜λŠ” μͺ½μ΄ μž¬μ‹œλ„ λ‘œμ§μ„ λͺ°λΌλ„ 되게 λ§Œλ“œλŠ” κ²ƒμž…λ‹ˆλ‹€. try/except + for 루프가 wrapper μ•ˆμ— μžˆμ–΄μ•Ό ν•©λ‹ˆλ‹€.

2μ°¨ μ‹œλ„ β€” return vs raise 문제 #

 1def wrapper(*args, **kwargs):
 2    last_exception = None
 3    for i in range(max_attempts):
 4        try:
 5            result = func(*args, **kwargs)
 6            print(f"{func.__name__} μ‹œλ„ {i+1}/{max_attempts} 성곡")
 7            return result
 8        except Exception as e:
 9            last_exception = e
10            print(f"{func.__name__} μ‹œλ„ {i+1}/{max_attempts} μ‹€νŒ¨: {e}")
11    return last_exception   # ← 문제!

3번 λ‹€ μ‹€νŒ¨ν•˜λ©΄:

result = μ„œλ²„ 응닡 μ—†μŒ
type = <class 'ConnectionError'>   ← μ˜ˆμ™Έ 객체가 정상 λ°˜ν™˜κ°’μ²˜λŸΌ λŒμ•„μ˜΄

return last_exception은 μ˜ˆμ™Έλ₯Ό 정상 λ°˜ν™˜κ°’μœΌλ‘œ λŒλ €λ³΄λƒ…λ‹ˆλ‹€. ν˜ΈμΆœν•˜λŠ” μͺ½μ—μ„œ μ—λŸ¬κ°€ λ°œμƒν–ˆλŠ”μ§€ μ•Œ μˆ˜κ°€ μ—†μŠ΅λ‹ˆλ‹€.

3μ°¨ μ‹œλ„ β€” λ°”κΉ₯ try/except μΆ”κ°€ (μ—­μ‹œ μ‹€νŒ¨) #

 1def wrapper(*args, **kwargs):
 2    last_exception = None
 3    try:
 4        for i in range(max_attempts):
 5            try:
 6                result = func(*args, **kwargs)
 7                return result
 8            except Exception as e:
 9                last_exception = e
10                break
11    except Exception as e:
12        raise last_exception

μ•ˆμͺ½ exceptκ°€ 이미 μ˜ˆμ™Έλ₯Ό μ²˜λ¦¬ν•˜κΈ° λ•Œλ¬Έμ— λ°”κΉ₯ exceptκΉŒμ§€ μ˜ˆμ™Έκ°€ μ „νŒŒλ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. 결과적으둜 None이 λ°˜ν™˜λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

4μ°¨ μ‹œλ„ (μ™„μ„±) #

 1def retry(max_attempts: int):
 2    def decorator(func):
 3        @wraps(func)
 4        def wrapper(*args, **kwargs):
 5            last_exception = None
 6            for i in range(max_attempts):
 7                try:
 8                    result = func(*args, **kwargs)
 9                    print(f"{func.__name__} μ‹œλ„ {i+1}/{max_attempts} 성곡")
10                    return result
11                except Exception as e:
12                    last_exception = e
13                    print(f"{func.__name__} μ‹œλ„ {i+1}/{max_attempts} μ‹€νŒ¨: {e}")
14            raise last_exception
15        return wrapper
16    return decorator

Tip: μ„±κ³΅ν•˜λ©΄ return으둜 ν•¨μˆ˜κ°€ μ¦‰μ‹œ μ’…λ£Œλ©λ‹ˆλ‹€. for문이 λκΉŒμ§€ λ‹€ λŒμ•˜λ‹€λŠ” 것 μžμ²΄κ°€ μ „λΆ€ μ‹€νŒ¨λ₯Ό μ˜λ―Έν•˜λ―€λ‘œ, forλ¬Έ λ°”λ‘œ 뒀에 raise만 λ†“μœΌλ©΄ λ©λ‹ˆλ‹€.


3️⃣ @validate_types #

1μ°¨ μ‹œλ„ β€” int ν•˜λ“œμ½”λ”© #

1def validate_types(func):
2    @wraps(func)
3    def wrapper(*args, **kwargs):
4        for arg in args:
5            if not isinstance(arg, int):
6                raise TypeError(f"μΈμžλŠ” μ •μˆ˜(int)μ—¬μ•Ό ν•©λ‹ˆλ‹€. 받은 νƒ€μž…: {type(arg)}")
7        return func(*args, **kwargs)
8    return wrapper

λ™μž‘μ€ ν•˜μ§€λ§Œ, λͺ¨λ“  μΈμžκ°€ intμΈμ§€λ§Œ κ²€μ‚¬ν•©λ‹ˆλ‹€. get_type_hints와 inspectλ₯Ό import 해놓고도 μ•ˆ μ“°κ³  μžˆμ—ˆμŠ΅λ‹ˆλ‹€. 이러면 greet(name: str, count: int) 같은 ν•¨μˆ˜μ—μ„œ str을 넣어도 μ—λŸ¬κ°€ λ‚©λ‹ˆλ‹€.

2μ°¨ μ‹œλ„ β€” μ—λŸ¬ λ©”μ‹œμ§€ 문제 #

1for param_name, value in zip(params, args):
2    if not isinstance(value, hints[param_name]):
3        raise TypeError(f"μΈμžλŠ” μ •μˆ˜(int)μ—¬μ•Ό ν•©λ‹ˆλ‹€. 받은 νƒ€μž…: {type(param_name)}")

λ‘œμ§μ€ λ§žμ•˜μ§€λ§Œ μ—λŸ¬ λ©”μ‹œμ§€μ— 두 κ°€μ§€ λ¬Έμ œκ°€ μžˆμ—ˆμŠ΅λ‹ˆλ‹€:

  • "μ •μˆ˜(int)"κ°€ ν•˜λ“œμ½”λ”© β€” str이어야 ν•˜λŠ” νŒŒλΌλ―Έν„°μ—λ„ “μ •μˆ˜(int)μ—¬μ•Ό ν•©λ‹ˆλ‹€"κ°€ 좜λ ₯됨
  • type(param_name) β€” param_name은 "a" 같은 λ¬Έμžμ—΄μ΄λ―€λ‘œ 항상 <class 'str'>이 λ‚˜μ˜΄. type(value)λ₯Ό 써야 함

3μ°¨ μ‹œλ„ (μ™„μ„±) #

 1def validate_types(func):
 2    @wraps(func)
 3    def wrapper(*args, **kwargs):
 4        hints = get_type_hints(func)
 5        sig = inspect.signature(func)
 6        params = list(sig.parameters.keys())
 7
 8        for param_name, value in zip(params, args):
 9            if not isinstance(value, hints[param_name]):
10                raise TypeError(
11                    f"'{param_name}'의 νƒ€μž…μ΄ {hints[param_name].__name__}μ—¬μ•Ό ν•˜μ§€λ§Œ "
12                    f"{type(value).__name__}이 전달됨"
13                )
14        return func(*args, **kwargs)
15    return wrapper

βœ… μ‹€ν–‰ κ²°κ³Ό #

=== timer ===
slow_function μ‹€ν–‰ μ‹œκ°„: 1.50초

=== retry ===
unstable_api_call μ‹œλ„ 1/3 μ‹€νŒ¨: μ„œλ²„ 응닡 μ—†μŒ
unstable_api_call μ‹œλ„ 2/3 성곡

=== validate_types ===
add(1, 2)          β†’ 3
add("1", "2")      β†’ TypeError: 'a'의 νƒ€μž…μ΄ intμ—¬μ•Ό ν•˜μ§€λ§Œ str이 전달됨
greet(123, 3)      β†’ TypeError: 'name'의 νƒ€μž…μ΄ strμ—¬μ•Ό ν•˜μ§€λ§Œ int이 전달됨
greet("hello", 3)  β†’ hellohellohello

πŸ” functools.wrapsλŠ” μ™œ ν•„μš”ν•œκ°€ #

μ²˜μŒμ—λŠ” @wraps 없이도 func.__name__이 잘 λ‚˜μ™€μ„œ ν•„μš” μ—†λ‹€κ³  μƒκ°ν–ˆμŠ΅λ‹ˆλ‹€. ν•˜μ§€λ§Œ 그건 λ°μ½”λ ˆμ΄ν„° λ‚΄λΆ€μ—μ„œ func을 직접 μ°Έμ‘°ν•˜κ³  있기 λ•Œλ¬Έμ΄μ—ˆμŠ΅λ‹ˆλ‹€. λ¬Έμ œλŠ” λ°”κΉ₯μ—μ„œ λ³Ό λ•Œμž…λ‹ˆλ‹€:

 1@timer
 2def slow_function():
 3    """느린 ν•¨μˆ˜μž…λ‹ˆλ‹€"""
 4    ...
 5
 6# @wraps 없이
 7print(slow_function.__name__)    # β†’ "wrapper"     ← μ›λž˜ 이름 μ†Œμ‹€
 8print(slow_function.__doc__)     # β†’ None           ← docstring μ†Œμ‹€
 9
10# @wraps 있으면
11print(slow_function.__name__)    # β†’ "slow_function"
12print(slow_function.__doc__)     # β†’ "느린 ν•¨μˆ˜μž…λ‹ˆλ‹€"

⚠️ FastAPI 같은 ν”„λ ˆμž„μ›Œν¬κ°€ ν•¨μˆ˜μ˜ __name__κ³Ό __doc__을 μ½μ–΄μ„œ API λ¬Έμ„œλ₯Ό μžλ™ μƒμ„±ν•˜λŠ”λ°, @wrapsκ°€ μ—†μœΌλ©΄ λͺ¨λ“  μ—”λ“œν¬μΈνŠΈ 이름이 “wrapper"둜 ν‘œμ‹œλ©λ‹ˆλ‹€.


πŸ’‘ Python νƒ€μž… νžŒνŠΈλŠ” λŸ°νƒ€μž„μ— κ°•μ œλ˜μ§€ μ•ŠλŠ”λ‹€ #

과제 쀑에 “νƒ€μž… 힌트λ₯Ό λ„£μœΌλ©΄ add("1", 2)μ—μ„œ λ°”λ‘œ μ—λŸ¬κ°€ λ‚˜μ§€ μ•Šλ‚˜μš”?“λΌλŠ” 의문이 μƒκ²ΌμŠ΅λ‹ˆλ‹€. 닡은 μ•„λ‹™λ‹ˆλ‹€.

1def add(a: int, b: int) -> int:
2    return a + b
3
4add("hello", "world")   # β†’ "helloworld" (μ—λŸ¬ μ—†μŒ!)
5add(1.5, 2.3)            # β†’ 3.8 (int μ•„λ‹Œλ° μ—λŸ¬ μ—†μŒ!)

Python의 νƒ€μž… νžŒνŠΈλŠ” κ·Έλƒ₯ “λ©”λͺ¨"μž…λ‹ˆλ‹€. μ‹€μ œλ‘œ νƒ€μž…μ„ κ°•μ œν•˜λ €λ©΄ mypy 같은 정적 뢄석 도ꡬλ₯Ό μ“°κ±°λ‚˜, 이번 과제의 @validate_types처럼 λŸ°νƒ€μž„μ— 직접 체크해야 ν•©λ‹ˆλ‹€. FastAPIκ°€ λ‚΄λΆ€μ μœΌλ‘œ 이런 λŸ°νƒ€μž„ 검증을 ν•΄μ£ΌλŠ” λŒ€ν‘œμ μΈ μ˜ˆμž…λ‹ˆλ‹€.


πŸ€” λ°μ½”λ ˆμ΄ν„°κ°€ μ™œ ν•„μš”ν•œκ°€ #

과제λ₯Ό ν•˜λ©΄μ„œ “ꡳ이 λ°μ½”λ ˆμ΄ν„°κ°€ ν•„μš”ν•œκ°€?“λΌλŠ” 의문이 λ“€μ—ˆμŠ΅λ‹ˆλ‹€. ν•˜μ§€λ§Œ @retry 없이 API 호좜 3개λ₯Ό λ§Œλ“ λ‹€κ³  κ°€μ •ν•˜λ©΄ 닡이 λ‚˜μ˜΅λ‹ˆλ‹€:

 1# λ°μ½”λ ˆμ΄ν„° 없이 β€” μž¬μ‹œλ„ 둜직이 맀번 반볡
 2def call_openai():
 3    for i in range(3):
 4        try: return openai.chat(...)
 5        except: ...
 6
 7def call_anthropic():
 8    for i in range(3):              # ← λ˜‘κ°™μ€ μ½”λ“œ
 9        try: return anthropic.messages(...)
10        except: ...
1# λ°μ½”λ ˆμ΄ν„°λ‘œ β€” λΉ„μ¦ˆλ‹ˆμŠ€ 둜직만 λ‚¨μŒ
2@retry(max_attempts=3)
3def call_openai():
4    return openai.chat(...)
5
6@retry(max_attempts=3)
7def call_anthropic():
8    return anthropic.messages(...)

λ°μ½”λ ˆμ΄ν„°μ˜ κ°€μΉ˜λŠ” νš‘λ‹¨ κ΄€μ‹¬μ‚¬μ˜ λΆ„λ¦¬μž…λ‹ˆλ‹€. λ‘œκΉ…, 인증, μž¬μ‹œλ„, 캐싱, νƒ€μž… 검증 같은 건 λΉ„μ¦ˆλ‹ˆμŠ€ 둜직이 μ•„λ‹Œλ° μ—¬κΈ°μ €κΈ°μ„œ ν•„μš”ν•©λ‹ˆλ‹€. 이걸 ν•¨μˆ˜λ§ˆλ‹€ λ°˜λ³΅ν•˜λŠ” λŒ€μ‹  @ ν•œ μ€„λ‘œ λΆ™μ΄λŠ” κ²ƒμž…λ‹ˆλ‹€.


πŸ“ 이번 κ³Όμ œμ—μ„œ 배운 것 #

λ°μ½”λ ˆμ΄ν„° κΈ°λ³Έ νŒ¨ν„΄: wrapperλŠ” resultλ₯Ό λ°˜ν™˜, λ°μ½”λ ˆμ΄ν„°λŠ” wrapperλ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€. λ°μ½”λ ˆμ΄ν„° μ•ˆμ—μ„œ ν•¨μˆ˜λ₯Ό 직접 μ‹€ν–‰ν•˜λ©΄ μ•ˆ λ©λ‹ˆλ‹€.

인자 μžˆλŠ” λ°μ½”λ ˆμ΄ν„°: νŒ©ν† λ¦¬ β†’ λ°μ½”λ ˆμ΄ν„° β†’ wrapper의 3쀑 쀑첩 κ΅¬μ‘°μž…λ‹ˆλ‹€. ν‚€μ›Œλ“œ 인자둜 ν˜ΈμΆœν•  λ•ŒλŠ” 이름이 μΌμΉ˜ν•΄μ•Ό ν•˜κ³ , μœ„μΉ˜ 인자둜 ν˜ΈμΆœν•˜λ©΄ μƒκ΄€μ—†μŠ΅λ‹ˆλ‹€.

return vs raise: μ˜ˆμ™Έλ₯Ό returnν•˜λ©΄ 정상 λ°˜ν™˜κ°’μ²˜λŸΌ λŒμ•„κ°‘λ‹ˆλ‹€. μ˜ˆμ™ΈλŠ” λ°˜λ“œμ‹œ raise둜 λ°œμƒμ‹œμΌœμ•Ό ν˜ΈμΆœν•˜λŠ” μͺ½μ—μ„œ 인지할 수 μžˆμŠ΅λ‹ˆλ‹€.

@wraps의 μ—­ν• : μ›λž˜ ν•¨μˆ˜μ˜ __name__, __doc__ λ“± 메타데이터λ₯Ό λ³΄μ‘΄ν•©λ‹ˆλ‹€. FastAPI 같은 ν”„λ ˆμž„μ›Œν¬μ˜ μžλ™ λ¬Έμ„œ 생성에 영ν–₯을 μ€λ‹ˆλ‹€.

νƒ€μž… νžŒνŠΈλŠ” λ©”λͺ¨μΌ 뿐: Python은 λŸ°νƒ€μž„μ— νƒ€μž…μ„ κ°•μ œν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. get_type_hints()와 inspect.signature()둜 직접 읽어와야 ν•©λ‹ˆλ‹€.

Advertisement