대략적으로 공부하고 깨달은 부분이라 정확하지 않을 수 있다. 개발 업무를 해나가면서 계속해서 개선할 내용이니, 일단 참고만 하자
문제상황
테스트 코드를 작성하는데 public 메서드 1개에 여러 개의 private 메서드가 포함되었다. 많은 참조 자료에서 private 메서드를 테스트하는 것은 바람직한 방향이 아니라고 한다.
( 참조 3. 자바에서 private 메서드를 import할 때 Reflection을 사용하게 되는데, Reflection은 컴파일 에러를 유발하지 못하므로 메서드 이름 변경 등에 취약하다. 그리고 애초에 private은 외부 참조가 안되도록 만들어놓고 결합도를 낮추어 놓은 측면이 있는데, 이를 무시하게 되는 것이 된다.)
그러나 각 메서드를 private으로 처리한 것은 정말 해당 도메인과 관련된 로직이라 외부에 공개할 필요가 없기 때문이다. 이럴 경우 아래의 논리 충돌이 발생한다.
- 최상단 public 메서드 내에 private 메서드가 많다.
- private 메서드의 로직이 복잡해서 테스트를 해봐야 코드의 안정성과 유지보수성을 확보할 수 있겠다.
- private 메서드는 테스트하지 않는다는 원칙에 따라 최상단 public 메서드만 테스트하는 경우 given 조건이 너무 많이 필요하고, 테스트 케이스마다 이 given 조건을 세세히 조정해줘야한다. (일부에서는 fixture 같이 json 파일로 조건을 주어 조정하는 방법도 있다는데, 이 역시도 json 파일을 많이 만들어야된다는 문제가 발생한다.)
수도 코드로 예시를 들자면 아래 같은 상황이다. 학교를 생성하는 public 메서드 내에 선생님, 학생 정보를 불러오고, 운동장을 만들고 세금처리를 하고 인프라를 만드는 private 메서드가 들어있다. 그러다보니 아래쪽 테스트 코드에서 최상위 create_school을 조건별로 테스트하느라 많은 given 코드가 필요하고 각각 조건을 조정해주어야되는 상황이다. 생각해볼점과 고쳐야될 부분이 많은 코드인데 주석으로 표시된 # A | B | C | D 하나씩 어떻게 해결할지 기록해놓는다.
(파이썬에서는 private 메서드의 이름 앞에 preceding single underscore (_)를 붙여서 private임을 표기한다)
def create_school(students: List[Student], people: List[Person]):
teachers = _get_teachers(students) # A
students_grouped_by_age = _group_students_by_age(students) # B
playground = _create_playground(people)
inactivated_tax = _inactive_tax_by_people(people)
infra = _create_infra(inactivated_tax)
# 여러 정보들을 바탕으로 School 생성
def _group_studens_by_age(students: List[Student]):
# 메서드 이름만 보면 이해되지 않지만 아무튼 엄청나게 복잡한 코드
# students를 순회하며 하나씩 비교해야된다거나...
...
# 테스트 코드 --------------------
def test_create_school_with_no_playground():
# given
# create teachers with lots of condition...
# create students with lots of condition...
# create playground with lots of condition...
# create tax with lots of condition...
# create infra with lots of condition...
def test_create_school_if_no_taxes():
# given
# create teachers with lots of condition...
# create students with lots of condition...
# create playground with lots of condition...
# create tax with lots of condition...
# create infra with lots of condition...
개선점
#A. 도메인과 관련되어있고, 공통적으로 사용해야될 코드는 public으로 만들고 그 메서드를 테스트한다.
teachers = _get_teachers(students)
학생들의 정보로부터 선생님들의 정보를 얻어오는 코드이다. 만약 이 메서드가 Teacher라는 모델에서만 처리할 수 있다면 Teacher 모델 부분에 public한 도메인 메서드로 넣고 도메인에서 테스트하는 것이 좋다. 단순히 students 정보로부터 DB에 접근하여 teacher 정보를 가져오는 것이라면 다른 곳에서도 사용할 가능성이 높기 때문에 더더욱 public 메서드로 빼는 것이 좋을 것이다.
만약 Teacher, Student 2개 모델과 연관이 있는 경우라면 자바의 경우 따로 Class 객체로 빼서 해당 메서드를 public으로 만들어 놓고, 파이썬의 경우에는 공통의 util 메서드를 만들어서 public으로 처리하면 된다.
위 두 가지 방향으로 public 메서드로 처리한다면 각 메서드의 디렉토리 위치에서 public 테스트를 진행하고, create_school에서는 해당 메서드들의 테스트는 굳이 진행하지 않아도 될 것이다.
#B. 해당 API에서만 쓸 것이라 private 처리된 메서드인데 복잡한 로직이 들어있어 테스트를 해보고 싶은 경우, 테스트를 하지 않거나 따로 Class, utils로 빼면서 public화 한다.
테스트가 반드시 필요한 경우인지 잘 생각해봐야한다. 그리고 해당 메서드가 너무 많은 책임을 가지고 있는지 판단해보고, 너무 많다면 여러 메서드로 분리하는 방식으로 리팩토링 해야한다. 또한 해당 메서드에 필요한 given 조건을 설정하는 것이 부담스러운지 아닌지도 판단해봐야한다.
그게 아니라면, 위 #A 케이스에서 설명했듯이 Class로 래핑 또는 util로 빼서 테스트한다.
부가적으로, 파이썬에서는 사실 _로 시작하는 private은 규약일뿐 시스템적으로 막히는 것이 아니라서 private 메서드도 테스트에서 import가 가능하긴하다(preceding double underscore로 mangling시 import 불가). 다만 _로 처리된 private 메서드를 테스트하는 것은 지양해야된다는 원리는 자바와 동일하다.
정리
필요하다면 public으로 뺄 수 있다. 다만 정말 private한 것인지, 해당 로직이 너무 많은 책임을 지고 있지는 않은지 잘 판단하자.
마지막으로 참조1)에 나온 내용에서 구현체와 테스트 코드의 관계에 대한 문구를 기록한다.
테스트 코드는 구현체의 보조 수단이 절대 아니다. 오히려 구현 설계가 잘 되었는지 판단할 수 있는 기준이 된다.
-> TDD 기반의 내용이며 DDD 에서는 테스트가 필요없다는 등 다르게 해석할 수도 있다 ^^;
참조
1) https://jojoldu.tistory.com/674
2) https://jojoldu.tistory.com/681
'Programming-[Backend] > Python' 카테고리의 다른 글
Decorator 개념과 적용, @wraps (0) | 2023.09.26 |
---|---|
[TIL] Python O(n)은 몇 초 정도일까 (Casting, converting) (1) | 2023.09.18 |
[TIL] 파이썬 단일 메서드 실행 디버깅 - shell_plus (0) | 2023.09.14 |
[TIL] PyPy, CPython과 session close(), reference counting (0) | 2023.09.13 |
[TIL] 파이썬 Retry 방식 요약 backoff_factor, status_forcelist (0) | 2023.09.08 |