POST

All
Product
Team
Tech
DocVLM: Make Your VLM an Efficient Reader
  • 최윤진
  1. Tech
Python 3.10 신규 문법 : Parenthesized context managers와 PEG Parser
  • S
    seunghoChoe
  1. Tech
UReader: Universal OCR-free Visually-situated Language Understanding with Multimodal Large Language Model
  • 최윤진
  1. Tech
[팀 소개편] KPMG Lighthouse는 어떤 팀인가요?
  • L
    Lighthouse
  1. Team
[챕터 소개편] Backend Chapter를 소개합니다
  • L
    Lighthouse
  1. Team
[챕터 소개편]Frontend Chapter를 소개합니다
  • L
    Lighthouse
  1. Team
[챕터 소개편] AI Chapter를 소개합니다
  • L
    Lighthouse
  1. Team

Python의 Decorator

Created by
  • 김원준
Created at
Category
  1. Tech

0. 들어가며

이번 시간에는, 파이썬의 데코레이터에 대해서 발표해보도록 하겠다.

1. Decorator 란?

파이썬을 사용하다보면, 함수 혹은 메서드 위에 “@” 골뱅이가 붙은 특이한 이름을 본 적이 있을 것이다.
이러한 특이한 문법을 decorator라고 하는데, 영어 사전에서 decorator는 “장식자”이라는 뜻을 가지고 있다.
파이썬의 decorator 역시 동일한 의미로 사용된다. 어떤 함수가 있을 때, 해당 함수를 직접 수정하지 않고, 함수에 기능을 추가 /변경 / 확장 하고자 할 때 decorator를 사용한다.
decorator는 함수를 인자로 받고, 또 다른 함수를 반환하는 고차함수 이다.
💬
고차함수 ?

함수를 인자로 받아서, 함수를 반환하는 함수
파이썬에서 함수는 일급 객체로 취급되기 때문에 가능하다.

일급객체 ?
→ 아래에서 살펴보자.
그렇다면, 도대체 어떤 기능 이길래 추가 /변경 / 확장 모두 가능한 것일까?
지금부터 알아보자.

1.1. 데코레이터 기본 예시

def say_hello_to_lh_team(): print('안녕하세요. 좋은 아침입니다.') print('고생하셨습니다. 내일 뵐게요.') say_hello_to_lh_team() --- 안녕하세요. 좋은 아침입니다. 고생하셨습니다. 내일 뵐게요.
팀원들에게 아침 / 저녁 인사를 하는 함수가 있다.
def work_in_gfc(): print('10시에 스크럼 진행하시죠.') print('잠시 미팅 가능하실까요?') print('ESG 관련해서 질문이 있습니다 !') work_in_gfc() --- 10시에 스크럼 진행하시죠. 잠시 미팅 가능하실까요? ESG 관련해서 질문이 있습니다 !
또, GFC에서 업무를 진행하는 (자주 하곤 하는) 함수가 있다.
그렇다면, 아침 / 저녁 인사 사이에 업무에 관한 대화를 출력하고 싶다면 어떻게 해야할까 ??
def say_hello_to_lh_team(work_func): print('안녕하세요. 좋은 아침입니다.') work_func() print('고생하셨습니다. 내일 뵐게요.') def work_in_gfc(): print('10시에 스크럼 진행하시죠.') print('잠시 미팅 가능하실까요?') print('ESG 관련해서 질문이 있습니다 !') say_hello_to_lh_team(work_in_gfc) --- 안녕하세요. 좋은 아침입니다. 10시에 스크럼 진행하시죠. 잠시 미팅 가능하실까요? ESG 관련해서 질문이 있습니다 ! 고생하셨습니다. 내일 뵐게요.
위에서 말한 것 처럼, 파이썬은 함수를 함수의 인자로, 혹은 리턴값으로 지정할 수 있는 고차함수 이기에,
업무를 진행하는 함수를, 아침 / 저녁 인사를 하는 함수에 인자로 주면 쉽게 구현할 수 있다.
하지만, 팀원들에게 전하는 아침 / 저녁 인사는 매번 동일할 수 있지만, 업무는 매일 다르다. 그러면 매일매일 업무에 따라 work 함수를 정의 할텐데,
그때마다
say_hello_to_lh_team(work_in_gfc_monday)
say_hello_to_lh_team(work_in_gfc_tuesday)
say_hello_to_lh_team(work_in_gfc_wednesday)
say_hello_to_lh_team(work_in_gfc_thursday)
이렇게 표현해야한다.
하지만 데코레이터로 표현하자면,
def say_hello_to_lh_team(work_func): def print_hello(): print('안녕하세요. 좋은 아침입니다.') result = work_func() print('고생하셨습니다. 내일 뵐게요.') return print_hello @say_hello_to_lh_team def work_in_gfc_monday(): print('안녕하세요. 좋은 아침입니다.') ... print('고생하셨습니다. 내일 뵐게요.') ... @say_hello_to_lh_team def work_in_gfc_tuesday(): ... @say_hello_to_lh_team def work_in_gfc_wednesday(): ... work_in_gfc_monday() work_in_gfc_tuesday() work_in_gfc_wednesday()
데코레이터만 추가해준다면, 더이상 say_hello_to_lh_team(work_in_gfc_thursday) 이런식으로 함수를 감싸지 않아도 된다.
즉 데코레이터가 붙은 함수의 호출 Flow는
함수 호출 → 데코레이터 진입 → 데코레이터가 가미된(?) 함수 호출
이러한 흐름을 보이게 된다.

1.2. 장단점

1.2.1. 장점

코드의 재사용성을 높일 수 있다.
데코레이터를 사용하면, 코드의 일부분을 여러 함수에서 공유할 수 있다. 이로 인해 코드의 재사용성이 향상되며, 유지보수도 용이해진다.
코드의 가독성이 향상된다.
데코레이터를 사용하면, 중복 사용되는 부분의 코드가 데코레이터 부분으로 분리되기 때문에, 가독성이 향상된다.
관심사의 분리가 가능해진다.
예를들어, 권한 검증이나 로깅과 같은. 어떻게 보면 API의 핵심 기능과는 동떨어진 로직을 분리함으로써, 함수의 핵심 기능에만 집중할 수 있다.

1.2.2. 단점

코드의 복잡도를 높일 수 있다.
하지만 무분별한 데코레이터의 사용은 코드의 복잡도를 높인다. 다수의 데코레이터가 중첩되어 사용될 경우 코드의 해석이 어려워진다. 이는 버그 발생의 원인이 되기도 하며, 유지보수에 악영향을 미치게된다. 그렇기 때문에, 데코레이터는 필요한 경우에만 적절히 사용하는 것이 좋다.

1.3. 데코레이터 사용시 주의사항

1.3.1. 함수의 메타데이터 변경

데코레이터를 사용하면, 원래 함수의 메타데이터 (함수 이름, 주석 문자열 등등…)이 변경될 수 있다. 그러한 이유는, 데코레이터가 원래 함수를 감싸는 새로운 함수를 반환하기 때문이다. 이로 인해, 디버깅이나 문서화 도구에서 원래의 함수 이름이나 문서화(주석) 문자열을 제대로 인식하지 못할 수 있다.
궁금하다.😒  한번 코드를 통해 알아보자.
def my_decorator(func): def func_in_deco(): #데코레이터 내부 함수 return func() return func_in_deco @my_decorator def original_func(): #원본 함수 """함수 원본입니당.""" #함수 설명 print("Hello!") print(original_func.__name__) print(original_func.__doc__) --- func_in_deco None
original_func의 메타데이터를 호출 해본 결과, 데코레이터 내부 함수의 이름 / doc 이 사라진걸 볼 수 있다.
위에서 언급한 것 처럼, 이러한 상황은 예상치 못한 상황에서 에러를 발생 시킬 수 있다.
이러한 현상은 파이썬의 functools 모듈에 있는 @functools.wraps 데코레이터를 이용하여 해결할 수 있다.
@functools.wraps는 원래 함수의 메타데이터를 유지하면서, 데코레이터를 적용할 수 있도록 도와준다.
import functools def my_decorator(func): @functools.wraps(func) # 추가한 구문 def func_in_deco(): return func() return func_in_deco @my_decorator def original_func(): """함수 원본입니당.""" pass print(original_func.__name__) print(original_func.__doc__) --- original_func 함수 원본입니당.
이처럼 메타데이터가 유지되는 것을 볼 수 있다.

1.3.2. 실행 순서에 주의

여러 데코레이터를 사용할 때, 데코레이터가 적용되는 순서에 따라 최종 값이 변질될 수 있으니, 데코레이터를 올바른 순서로 적용해야한다.

2. 실용적인 Decorator 사용 방안

무분별한 데코레이터 사용은 유지보수 및 코드의 가독성에 악영향을 미칠 수 있다고 했다.
그렇다면, 실 업무 환경에서 데코레이터는 언제 사용하는게 좋을까?

2.1. 권한

프로덕트 내에서 권한에 따라 실행 여부가 결정되는 함수 (API도 함수이다.)가 존재하는 경우가 있다.
이때, 대부분의 함수(API)에서 권한을 검증하는 프로세스가 진행되기 때문에, 각 함수에 중복되는 코드가 존재할 수 밖에 없다. 이때 데코레이터를 사용하면, 효율적이다.
def auth_decorator(func): # 권한을 검증하는 공통 함수 def wrapper(user): if user.is_authenticated: return func(user) # delete_user 함수 실행 else: raise PermissionError("권한이 없습니다.") return wrapper @auth_decorator def delete_user(user, ...): # 프로덕트 내부 함수 pass
해당 함수와 같이, 유저를 삭제하는 꽤나 크리티컬한 함수에 권한 검증 프로세스가 붙어있지 않다면 운영에 큰 차질을 겪을 수 있다. (단순 url호출만으로도 유저가 삭제되니..)
그렇기에 대부분의 프로덕트에서는 권한 검증이 필수로 들어가있고, 이러한 검증 행위는 대부분 같은 함수를 사용하기에 데코레이터를 사용하곤한다.

2.2. 로깅

권한만큼 데코레이터를 이용해서 호출되곤 하는것이 바로 로깅이다. 로깅은 간혹 가벼이 여겨질 수는 있지만, 프로그램의 실행 흐름을 추적하거나 성능 문제를 진단하는데 도움이 될 수 있는 수단이다.
하지만, 이러한 로깅도 대부분의 로직/코드의 형태가 프로덕트 내에서 동일하기에, 데코레이터를 사용하여 표현하곤 한다.
def log_decorator(func): # 실행 시간 및 실행 함수를 로깅하는 decorator def wrapper(*args, **kwargs): # 여러곳에서 사용되기 때문에, 유연한 형태로 인자를 수령 start_time = time.time() end_time = time.time() logging.info(f'{func.__name__} / 실행 시각 : {start_time} 실행 시간: {end_time - start_time} 초') # logging 코드 추가 # 추가로 필요로하는 값을 유연하게 추가. return func() return wrapper @log_decorator def my_func(): pass

2.3. API Router !!

실용적인 방안이라고 하기엔 조금 뭐하지만, FastAPI와 Flask에서 api를 정의할 때 사용하는 (url과 method를 적곤하는) Router역시 데코레이터다.
모든 Rest API에는 필수로 경로(Path)와 메서드(Method)가 명시되어야 한다.
또, 개발자들 끼리 사전에 약속한 프로덕트 Rule (tag를 붙인다든가, response_model을 명시한다든가..)은 거의 모든 API에 적용되기 때문에, 코드의 중복이 발생할 가능성이 크다. 그렇기 때문에, API 내부에서 Router 데코레이터는 개발자에게 편리함을 제공함과 동시에, 유지보수 및 코드의 가독성에도 큰 도움을 준다.
즉, 데코레이터는 반복되는 (코드의 중복 여지가 다분한) 로직에서, 또 비교적 간단한 로직에서 사용되는것이 좋다.

3. Decorator 원리

3.1. Object

파이썬에서 가장 기본이 되는 형(Type)은 객체(Object)라고 볼 수 있다.
이는, 숫자도 문자도 심지어 함수도 모두 객체에서 파생되는 데이터 형이라는 말이 된다.
이는 정말 간단하게도, isinstance를 이용하여 가시적으로 확인할 수 있다.
print(isinstance(1, object)) print(isinstance('str', object)) print(isinstance(original_func, object)) --- True True True
즉, 모든 파이썬의 기본 데이터는 object이며, 다시말해 파이썬은 object를 통해 작동한다. 라고 생각하면 된다.
💬
중간 save point

🤔 파이썬의 모든 Type은 Object이다. (Object를 상속받아 이뤄졌구나.)
🤔 함수 역시 Object 구나

3.2. 일급객체(First-Class Object) / 일급함수(First-Class Function)

일급객체? 영어 단어에서도 볼 수 있듯이, 한우로치면 투쁠한우처럼 보인다.
wikipedia에서는 일급객체를
→ 다른 객체들에 일반적으로 적용 가능한 연산을 모두 지원하는 객체
라고 정의한다.
여기서 일반적으로 적용 가능한 연산 이란, 데이터를 할당하거나 반환하거나 전달하는 것을 의미한다.
즉, 변수나 데이터 구조에 할당이 가능하며, 리턴 값으로 반환할 수 있거나, 파라미터로 전달할 수 있다면, 그 객체는 일급객체 이다.
💬
일급객체 조건

1. 변수 혹은 데이터 구조 안에 담을 수 있다.
2. 파라미터로 전달할 수 있다.
3. Return 값으로 사용할 수 있다.
그럼 의문이 생긴다. 그럼 평범한 정수, 문자열도 일급 객체인가?
→ 맞다. 위의 3가지 조건을 모두 만족하면서 동시에 객체이기 때문에 일급객체가 맞다.
그렇다면, 일급함수 (First-Class Function)는 무엇일까?
너무 간단하다. 일급객체의 조건을 만족하는 함수가 일급함수이다.
근데 위에서, 함수는 객체라고 했다.
💬
아! 함수 역시 일급객체구나!

즉, 파이썬에서는 함수 마저도 int, str, float 등과 같은 기본적인 변수들 처럼 일급객체로 취급한다.

3.3. 결론

데코레이터의 형태를 리마인드 하기 위해, 위의 코드를 다시 보고오자.
“””
변수안에 함수를 담았고
함수를 파라미터로 받아서
리턴값으로 함수를 리턴한다.
“””
즉 데코레이터는, 파이썬의 이러한 기본 원리를 바탕으로 이뤄진 문법이다.
1.
파이썬의 모든 타입은 객체를 기반으로 한다.
2.
특정한 조건을 만족하는 객체를 일급 객체로 한다.
3.
파이썬은 함수 역시 객체 취급을 한다.

4. 마무리

이번 시간에는, 파이썬의 데코레이터에 대해서 알아보는 시간을 가졌다.
다음 시간에는 최근 ESG 개발을 하면서 많이 애를 먹은 부분인 Python의 Meta Class 에 대해서 발표해보려고 한다.