F-Lab
🚀
깊이 있는 개발자 커뮤니티, 데브클럽에 함께 하세요

성장하는 Python 코드베이스에서 피해야 할 10가지 안티 패턴

writer_thumbnail

F-Lab : 상위 1% 개발자들의 멘토링

안녕하세요. F-Lab  'Python Backend' 과정 멘토 Jacob 입니다. 저는 센드버드 출신의 엔지니어링 조직 문화와 생산성에 관심이 많은 개발자입니다. 오늘은 Python 코드베이스가 성장하는 과정에서 타협하게 되는 지점들과 그러한 타협들이 왜 나쁜지 알아보려고 합니다.

 

 

개요

성장하는 회사는 코드베이스도 빠르게 변화합니다. 특히 다양한 고객 요구사항을 만족시키기 위해 기능개발이 많이 일어나는 경우 코드베이스의 크기가 빠른 속도로 커지는것을 쉽게 볼 수 있습니다. 

 

이러한 기능 개발 과정에서 개발자는 더 쉽고 빠르게 기능 개발을 하고 싶은 유혹을 느낄 수 있습니다. 가령 기존에 존재하는 API 에 기능을 하나 더하거나 새로운 함수를 만드는 대신 이미 사용중인 함수를 확장하는 등의 방식으로 기능 개발에 드는 시간과 노력을 줄일 수 있다면 큰 이득이라고 느낄 수 있을 것 같습니다. 하지만, 코드는 작성하는 것 만큼이나 유지보수하는데도 많은 시간과 노력이 들어갑니다. 당시에는 빠른 길이라 생각했던 방법들이 결과적으로는 가독성을 떨어트리고 유지보수를 어렵게 만드는 경우가 많습니다. 

 

이 블로그 글에서는 필자가 발견했던 성장하는 Python 코드베이스에서 흔히 나타나는 10가지 안티 패턴을 들여다보고 각각을 왜 피해야 하는지 설명합니다.

 

 

10가지 안티 패턴

 

1. 복수 타입의 인자

2. 복수 타입의 반환값

3. 함수 인자 늘리기

4. 스코프 밖의 변수에 접근하는 함수

5. 구조화된 데이터를 dictionary 로 표현하기

6. 동적 import

7. `except Exception:` 으로 모든 예외 잡기

8. assert 문으로 검사하기

9. classmethod 만 존재하는 클래스

10. 과도한 상속

 

 

1. 복수 타입의 인자

존재하는 함수나 메소드에 다수의 동일한 인자를 전달해야 하는 경우에 같은 이름의 인자를 list 나 set 으로 바꿔서 전달하면 코드를 쉽게 확장 가능할것 같다는 아이디어를 떠올릴 수 있습니다. 

 

하지만 이런 방식으로 코드를 확장하는 것은 함수나 메소드 내부에서 타입 검사와 처리를 해줘야 하기 때문에 함수의 크기가 늘어나는 문제점이 생기며 코드 가독성을 떨어뜨립니다. 다시 말해서 복수 타입의 인자를 받는 것은 동적 타입 언어에서 허용된 일이지만 코드의 가독성과 유지보수를 어렵게 만들 수 있습니다. 

 

인자의 타입이 명확하지 않으면 코드를 이해하기 어려워지며, 디버깅이 더 복잡해집니다. 아래의 코드는 인자 data 를 string 과 dictionary 의 두가지 타입으로 받을 때 어떤 일이 일어나는지 잘 보여줍니다.

 

def process_data(data: Union[str, dict]) -> None:
   if isinstance(data, str):
       # …
   elif isinstance(data, dict):
       # …
   else:
       raise TypeError('Unsupported type {}'.format(type(data)))

 

함수 안에서 분기문을 통해서 각각 타입에 맞는 동작을 수행해야 하기 때문에 함수의 크기가 커지고 지원되지 않는 타입의 경우 예외를 발생시키기 때문에 함수를 사용할때 예외 처리가 필요한것을 알 수 있습니다. 이 대신에 한가지 타입을 사용해서 함수나 메서드의 인터페이스를 명확하게 정의하는 것이 좋습니다.

 

from json import loads

def process_data_string(data: str) -> None:
   process_data(loads(data))

def process_data(data: dict) -> None:
   # …

 

 

2. 복수 타입의 반환값

마찬가지로 함수나 메서드가 복수 타입의 값을 반환하는 것은 예상치 못한 동작을 야기할 수 있습니다. 아울러서 함수를 사용하는 쪽에서 타입 검사를 통해서 각각의 타입에 맞는 동작을 구현해야 하기 때문에 복잡도가 증가합니다.

 

def get_data(value):
   if value < 0:
       return 'value must be greater than 0'
   else:
       return 42

 

위의 예시에서는 인자가 0보다 작은 경우에 오류 메시지 문자열을 반환합니다. 아래와 같이 복수 타입의 값을 반환하도록 코드를 작성하지 않고 예외를 이용할 수 있습니다.

 

def get_data(value):
   if value < 0:
       raise ValueError('value must be greater than 0')
   else:
       return 42

 

 

3. 함수 인자 늘리기

함수의 인자가 늘어날수록 함수 호출이 복잡해지고 가독성이 떨어집니다. 특히 많은 인자가 필요한 경우에는 코드를 이해하기 어려워집니다. 인자가 많이 필요한 경우에는 객체나 데이터 클래스를 사용하여 인자를 묶어주는 것이 좋습니다.

 

def process_data(name: str, age: int, city: str, country: str, is_student: bool):
   # ...

 

위와 같은 코드에서는 기능이 확장됨에 따라 인자가 증가할 가능성이 높습니다. 그리고 관련된 함수들에서 비슷한 인자들을 공유할 가능성이 높아 보입니다. 이런 경우 객체를 사용하여 코드를 더 간결하게 만들 수 있습니다.

 

@dataclass
class PersonInfo:
   name: str
   age: int
   city: str
   country: str
   is_student: bool

def process_data(person_info: PersonInfo):
   # ...

 

 

4. 스코프 밖의 변수에 접근하는 함수

동일한 변수를 매번 인자로 전달하는것 보다는 스코프 밖의 변수를 참조해서 함수 호출을 더 간결하게 만들고 싶은 마음이 들 수 있습니다. 하지만 함수가 스코프 밖의 변수에 의존하면 코드의 가독성과 유지보수성이 떨어집니다. 또 외부 상태에 의존하는 함수는 테스트하기 더 어려운 경향이 있습니다. 함수는 가능한한 자체적으로 모든 필요한 정보를 포함하도록 설계되어야 합니다.

 

total = 0

def add_to_total(value):
   global total
   total += value

 

위와 같은 코드에서 인자를 통해서 total 을 전달받는것이 불필요하게 느껴질 수 있습니다. 하지만 위의 코드는 읽는 사람에게 total 이라는 변수를 찾아가도록 해서 가독성을 떨어트리고 함수 외부에서 total 의 값이 바뀌는 상황에 대처할 수 없습니다. 따라서 아래와 같이 함수에 필요한 정보를 인자로 전달하고, 결과를 반환하는 것이 좋습니다.

 

def add_to_total(total, value):
   return total + value

 

 

5. 구조화된 데이터를 dictionary 로 표현하기

Python 의 dictionary 는 뛰어난 표현력을 가지고 있습니다. dictionary 를 이용해서 다양한 자료구조를 표현할 수 있고 데이터 클래스와 같은 용도로도 사용할 수 있습니다. 하지만 장기적인 관점에서 구조화된 데이터를 딕셔너리로 표현하는 것은 가독성과 유지보수성을 감소시킬 수 있습니다. 딕셔너리는 key 의 유무나 value 의 타입 등에 대한 제약을 둘 수 없고 데이터의 의미를 명확하게 전달하기 어렵습니다. Dictionary 대신에 명시적인 클래스를 사용하여 코드를 더 이해하기 쉽게 만들 수 있습니다.

 

user_info = {"name": "John", "age": 30, "city": "NEW_YORK"}

 

보다 명시적인 데이터 구조를 사용하면 데이터의 의미를 더 잘 전달할 수 있습니다.

 

class City(Enum):
   NEW_YORK = 1
   SHANGHAI = 2

@dataclass
class UserInfo:
   name: str
   age: int
   city: City

user_info = UserInfo(name="John", age=30, city=City.NEW_YORK)

 

 

6. 동적 import

PEP-8 에 따르면 import 는 항상 파일의 최상단에 위치해야 합니다. 하지만 환형 의존성이 있는 경우 함수나 메소드 안에서 import 를 수행하는 방식으로 문제를 회피하기 쉽습니다. 동적 import는 모듈이 로딩되는 시점을 알기 어렵게 만들어서 복잡도를 증가시키고 예기치 못한 오류를 일으킬 수 있습니다. 또 모듈이 없는 경우 예외가 발생할 수 있고, 코드의 가독성을 낮출 수 있습니다. 

 

필요한 모듈은 코드 상단에서 명시적으로 import 하고 모듈간의 의존성을 잘 관리하는 것이 좋습니다.

 

def calc_sqrt(value):
   import math
   return math.sqrt(value)

calc_sqrt(16)

 

명시적인 임포트를 사용하면 코드가 어떤 모듈을 사용하는지 명확하게 알 수 있습니다.

 

import math
result = math.sqrt(16)

 

 

7. `except Exception:` 으로 모든 예외 잡기

Python 코드베이스의 크기가 커지면 예외처리의 난이도가 증가합니다. 모든 예외를 잡아주는 `except Exception:` 과 같은 코드를 이용하면 코드를 당장은 실행되도록 할 수 있기 때문에 많은 개발자들이 유혹을 느낍니다. 하지만 `except Exception:` 은 디버깅을 어렵게 만듭니다. 모든 예외를 무시하면 예상치 못한 버그를 찾기 어려워지고 더 큰 문제가 발생할 수 있습니다. 

 

최대한 구체적인 예외를 처리하고, 예외가 발생했을 때 적절한 조치를 취하는 것이 중요합니다. 아래의 예시에서는 `except Exception as e:` 로 모든 예외를 잡아서 처리합니다.

 

try:
   # some code
except Exception as e:
   # handle exception

 

하지만 아래와 같이 보다 구체적인 예외를 처리하면 각각의 예외 상황에서 필요한 조치를 취할 수 있고 코드의 안정성을 향상시킬 수 있습니다. 또 어떠한 예외들이 발생할 수 있는지 기술하는 것을 통해서 디버깅을 쉽게 할 수 있습니다.

 

try:
   # some code
except ValueError as ve:
   # handle ValueError
except FileNotFoundError as fe:
   # handle FileNotFoundError
# Add more specific except blocks as needed

 

 

8. assert 문으로 검사하기

어플리케이션 내에서 assert 문을 사용하여 상태를 검사하는 것은 매우 제한적으로 이루어져야 합니다. assert 는 어플리케이션이 특정 상태로 존재하는것을 보장하기 위해서 사용되어야 하며 예외 처리의 대상이 되어서는 안됩니다. 어떠한 예외를 사용해야 좋을지 불분명한 상황에서 assert 를 무분별하게 이용하면 어플리케이션의 안정성을 떨어뜨리는 결과를 초래할 수 있습니다. 

 

예외 처리를 통해서 실행 경로가 바뀌어야 하는 경우에는 assert 대신에 명시적인 예외 처리를 사용해야 합니다.

 

assert value > 0, "Value must be greater than 0"

 

위의 예시와 같은 코드는 value 가 0과 같거나 0보다 작을때 프로그램이 실패하기를 기대한다는 의미를 내포합니다. 만약 value 의 값에 따라서 프로그램의 실행 경로가 바뀌어야 하는 것이라면 아래와 같이 보다 명시적인 예외를 사용할 수 있습니다. 명시적인 예외를 이용하면 코드의 의도가 명확해지고, 디버깅이 쉬워집니다.

 

if value <= 0:
   raise ValueError("Value must be greater than 0")

 

 

9. classmethod 만 존재하는 클래스

classmethod 만 존재하는 클래스는 일반적으로 설계상의 문제를 나타냅니다. 클래스는 객체를 생성하고 조작하는데 사용되어야 하며, 단순히 유틸리티 함수를 제공하는 클래스는 피해야 합니다. 모듈이나 함수를 사용하여 기능을 구현하는 것이 더 좋습니다.

 

class MathOperations:
   @classmethod
   def add(cls, x, y):
       return x + y

 

위와 같은 예시의 경우 클래스 대신 모듈이나 함수를 사용하여 기능을 구현하는 것이 좋습니다.

 

# math_operations.py
def add(x, y):
   return x + y

 

 

10. 과도한 상속

상속은 객체지향을 기반으로 코드 재사용성을 높일 수 있는 강력한 도구입니다. 하지만 모든 관계를 상속으로 표현하는것이 좋은 것은 아닙니다. 상속은 클래스간의 결합도를 증가시키기 때문에 반드시 함께 묶여야 하는 클래스들이 아닌 경우에는 합성을 이용하는것이 더 적절할 수 있습니다. 또 삼단 이상의 상속은 코드의 복잡성을 증가시킬 수 있습니다. 다중 상속은 의도하지 않은 동작을 야기할 수 있고, 코드를 이해하기 어렵게 만듭니다. 

 

상속을 사용할 때는 가급적 단일 상속을 이용하고 인터페이스나 믹스인을 활용하여 필요한 기능을 추가하는 것이 좋습니다.

 

class Walk:
   def get_legs(self):
       …

class Swim:
   def get_fins(self):
       …

class Mammal(Walk):
   pass

class Fish(Swim):
   pass

class Dog(Mammal):
   def move(self):
       legs = self.get_legs()
       …

class Whale(Mammal):
   def move(self):
       legs = self.get_fins()  # Mammal class does not have get_fins!
       …

 

위의 예시에서 Whale 클래스는 Mammal 클래스를 상속하지만 get_legs 대신 get_fins 메소드를 필요로 합니다. 또 Mammal 과 Fish 클래스의 존재로 인해서 불필요한 상속이 발생하는것을 확인할 수 있습니다. 이런 경우 상속을 통해서 메소드들을 재활용하기 보다는 합성을 활용하는것이 더 적절할 수 있습니다.

 

class Walk:
   def get_legs(self):
       …

class Swim:
   def get_fins(self):
       …

class Dog:
   walk = Walk()

   def move(self):
       legs = self.walk.get_legs()
       …

class Whale(Mammal):
   swim = Swim()

   def move(self):
       legs = self.swim.get_fins()
       …

 

 

마무리

위의 안티 패턴들은 초기에는 코드 작성을 빠르게 하고 제품 개발 속도를 높이는 과정에서 일어나는 사소한 타협처럼 보일 수 있지만, 시간이 지남에 따라 코드베이스의 복잡성을 증가시키고 유지보수를 어렵게 만듭니다. 

 

따라서, 프로젝트를 시작할 때부터 이러한 안티 패턴을 피하고, 코드의 가독성과 유지보수성을 고려하여 개발하는 것이 중요합니다. 또 팀 내에서 코드 리뷰를 통해 이러한 안티 패턴을 발견하고 수정하는 문화와 다른 사람이 짠 코드도 필요하다면 개선을 제안하는 문화를 정착시키는 것도 중요합니다. 이를 통해 코드베이스의 품질을 높이고, 효율적인 개발을 지원할 수 있습니다. 이러한 안티 패턴들이 일으키는 문제를 피부로 느끼는 단계가 오면 이미 개선을 하는데 과도하게 많은 시간과 노력이 들어가고 그렇기 때문에 문제를 방치하게 되는 경우가 잦습니다. 

 

이 글을 읽는 독자 분들은 사소한 타협들이 쌓여서 코드베이스 전체의 품질을 하락시킬 수 있다는 점을 꼭 기억하셨으면 좋겠습니다.

ⓒ F-Lab & Company

이 컨텐츠는 F-Lab의 고유 자산으로 상업적인 목적의 복사 및 배포를 금합니다.

조회수

멘토링 코스 선택하기

  • 코스 이미지
    Java Backend

    아키텍처 설계와 대용량 트래픽 처리 능력을 깊이 있게 기르는 백앤드 개발자 성장 과정

  • 코스 이미지
    Node.js Backend

    아키텍처 설계와 대용량 트래픽 처리 능력을 깊이 있게 기르는 백앤드 개발자 성장 과정

  • 코스 이미지
    Python Backend

    대규모 서비스를 지탱할 수 있는 대체 불가능한 백엔드, 데이터 엔지니어, ML엔지니어의 길을 탐구하는 성장 과정

  • 코스 이미지
    Frontend

    기술과 브라우저를 Deep-Dive 하며 성능과 아키텍처, UX에 능한 개발자로 성장하는 과정

  • 코스 이미지
    iOS

    언어와 프레임워크, 모바일 환경에 대한 탄탄한 이해도를 갖추는 iOS 개발자 성장 과정

  • 코스 이미지
    Android

    아키텍처 설계 능력과 성능 튜닝 능력을 향상시키는 안드로이드 Deep-Dive 과정

  • 코스 이미지
    Flutter

    네이티브와 의존성 관리까지 깊이 있는 크로스 플랫폼 개발자로 성장하는 과정

  • 코스 이미지
    React Native

    네이티브와 의존성 관리까지 깊이 있는 크로스 플랫폼 개발자로 성장하는 과정

  • 코스 이미지
    Devops

    대규모 서비스를 지탱할 수 있는 데브옵스 엔지니어로 성장하는 과정

  • 코스 이미지
    ML Engineering

    머신러닝과 엔지니어링 자체에 대한 탄탄한 이해도를 갖추는 머신러닝 엔지니어 성장 과정

  • 코스 이미지
    Data Engineering

    확장성 있는 데이터 처리 및 수급이 가능하도록 시스템을 설계 하고 운영할 수 있는 능력을 갖추는 데이터 엔지니어 성장 과정

  • 코스 이미지
    Game Server

    대규모 라이브 게임을 운영할 수 있는 처리 능력과 아키텍처 설계 능력을 갖추는 게임 서버 개발자 성장 과정

  • 코스 이미지
    Game Client

    대규모 라이브 게임 그래픽 처리 성능과 게임 자체 성능을 높힐 수 있는 능력을 갖추는 게임 클라이언트 개발자 성장 과정

F-Lab
소개채용멘토 지원
facebook
linkedIn
youtube
instagram
logo
(주)에프랩앤컴퍼니 | 사업자등록번호 : 534-85-01979 | 대표자명 : 박중수 | 전화번호 : 1600-8776 | 제휴 문의 : info@f-lab.kr | 주소 : 서울특별시 강남구 테헤란로63길 12, 438호 | copyright © F-Lab & Company 2024