Python

[파이썬][디자인패턴] 데코레이터 패턴을 이용한 공통 코드 처리

studioesso 2025. 4. 8. 16:51

 

개발을 하다 보면, 중복되는 코드가 있습니다. 이런 중복되는 코드들이 늘어나면 변경이 어렵고 유지보수를 힘들게 만듭니다.

 

보통 중복된 코드들은 함수나 클래스를 이용해 처리를 하는데요, 여러 함수들 내부가 비슷하게 중복되서 코드가 길어지는 경우가 있습니다.

 

대표적인 케이스가 try ~ except 문으로 에러를 처리하는 경우입니다. 함수 내부가 비슷한 try문으로 감싸져 있는 경우 디자인 패턴 중 데코레이터 패턴을 활용하여 아래처럼 작성할 수 있습니다.

from functools import wraps

def exception_deco(func):
    @wraps
    def wrapper(*args, **kwargs):
    	try:
            return func(*args, **kwargs)
        except Exception as e:
            print(e)
            raise Exception("에러가 발생했습니다.")
     
    return wrapper
    
@exception_deco
def foo():
    print("foo")
    
@exception_deco
def bar():
    print("bar")

 

이 코드들은 원래 아래처럼 작성됐을 것입니다. 함수 내부 전체를 try ~ except로 감싸서 에러가 발생 시 호출한 곳으로 에러를 던지고 있습니다. 굉장히 정형화된 패턴이기 때문에 상기처럼 디자인패턴을 이용해 중복되는 부분을 줄일 수 있습니다. 

def foo():
    try:
    	print("foo")
    except Exception as e:
    	print(e)
        raise Exception("에러 발생했습니다")
        
def bar():
    try:
    	print("bar")
    except Exception as e:
    	print(e)
        raise Exception("에러 발생했습니다")

 


 

만약 데코레이터를 적용한 함수 별로 다르게 에러를 던지고 싶다면 어떻게 할까요?

 

아래와 같이 func.__name__로 데코가 적용된 함수의 이름을 알  수 있어서 함수 별로 다르게 에러를 처리할 수 있습니다.

from functools import wraps

def exception_deco(func):
    @wraps
    def wrapper(*args, **kwargs):
    	try:
            return func(*args, **kwargs)
        except Exception as e:
            print(e)
            
            match func.__name__:
                case "foo":
                    raise Exception("foo 에러!!")
                case "bar":
                    raise Exception("bar 에러!!")
     
    return wrapper
    
@exception_deco
def foo():
    print("foo")
    
@exception_deco
def bar():
    print("bar")

 


 

또 다른 케이스입니다.

유닛 테스트 코드를 작성할 때 데이터를 가져오고 결과를 리턴하는 여러 함수들을 테스트할 때, 공통 부분을 데코레이터로 처리합니다. 

 

import unittest

from functools import wraps

def get_data(path) -> list:
    # path 경로의 파일을 읽고 리스트를 반환하는 임의의 함수.
    # 예시는 파일을 읽는 것이지만, mock db에서 데이터를 가져오거나, mock을 자체 생성하거나 알맞게 적용한다.
    return []

def exception_deco(path, func_name):
    def run_decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            data = get_data(path)
            
            match func_name:
                # 혹시 함수 별로 또 다르게 데이터를 변형해야 한다면 아래 case별로 처리
                # 그렇지 않다면 match문 없이 바로 return func(*args, data, **kwargs)
                case "run_prod_func1":
                    return func(*args, data, **kwargs)
                case "run_prod_func2":
                    return func(*args, data, **kwargs)
                    
        return wrapper
    return run_decorator

class ProdTest(unittest.TestCase):
    @exception_deco("/user/foo/test1.txt", "run_prod_func1")
    def test_run_prod_func1(self, res):
        expect = ["expect1"]
        self.assertListEqual(res, expect)
    
    @exception_deco("/user/bar/test2.txt", "run_prod_func2")
    def test_run_prod_func2(self, res):
        expect = ["expect2"]
        self.assertListEqual(res, expect)

 


 

디자인 패턴은 처음에는 익히기도 귀찮고, 새로운 코드 작성 시 적용하는 것도 귀찮지만, 막상 하나씩 배우고 익숙해지면 코드도 깔끔해지고 쏠쏠하게 잘 써먹는 것 같습니다.