NAVER Glace 신입 적응기: UML을 활용한 코드 리팩토링 및 코드 리뷰

Younghoon Yun
네이버 플레이스 개발 블로그
12 min readJun 15, 2022

안녕하세요, G플레이스데이터개발(줄여서 PDD)에서 근무하고 있는 윤영훈이라고 합니다. 21년 하반기 신입 공채로 들어와 두 달 간의 입문교육을 마치고 3월부터 데이터 엔지니어링의 방대한 세계를 탐험하고 있습니다.

제가 PDD에 들어와 처음 맡은 과제는 코드베이스의 code coverage 수준을 높이는 일이었습니다. 이 일을 자동화하고 PR과 커밋 단위로 반복 수행하기 위해 Code coverage reporting 시스템을 개발하였습니다.

실제 리포트 예시
동일 PR에서 outdate 된 Git revision에 대한 reporting 결과는 이렇게 숨겨집니다.
이런! coverage 테스트를 통과하지 못했습니다. 그러나 Optional test이기 때문에 merge에는 문제가 없습니다. 다만 ❌ 표시가 보기 불편하여 빨리 고치고 싶어지네요.

시스템 설계 및 구현

  • code coverage 리포트 생성
    – 언어마다 적절한 테스트 유틸리티가 있습니다. 예를 들어 파이썬 코드의 커버리지는 pytest-cov라는 pytest 플러그인을 이용해 뽑을 수 있습니다.
  • 리포트를 다듬고 사내 GitHub comment에 첨부 ⭐
  • 스크립트를 테스트 파이프라인에 연결
    – GitHub Hook을 사내 쿠버네티스 클러스터에 연결해, PR에 새 커밋이 생기면 테스트컨테이너를 자동으로 생성하도록 설정

여기서 리포트를 다듬는 로직을 더 살펴봅시다.

1. 저희 pdp repository의 구조는 대략 다음과 같습니다. packages 밑에 airflow, lambda 등 여러 패키지가 있는데, 각각의 패키지는 서로에게 거의 독립적입니다.

pdp
├── packages
├── airflow
├── lambda

패키지 간의 숨겨진 의존성을 줄이기 위해, 다음과 같이 패키지마다 Makefile goal을 정의합니다.

test.coverage.tools:  # python과 java 테스트가 정의되어 있음을 알립니다
@echo python java
test.coverage.python: # python 테스트를 수행합니다
pytest ./tests/unit --cov=./src --cov-branch --cov-report term
test.coverage.java: # java 테스트를 수행합니다
gradle test

이제 스크립트에서 packages/airflow/Makefile, packages/lambda/Makefile 등을 순회하며 필요한 모든 테스트를 수행할 수 있겠네요.

2. 한 패키지에 여러 언어가 공존한다면, 테스트 유틸리티도 여러 개가 필요할 수 있습니다.

따라서 (패키지, 테스트 유틸리티)마다 리포트 Section을 정의하고, 이것들을 조립하여 전체 리포트를 만들기로 합니다.

각각의 테스트 유틸리티마다 raw report 양식이 다를테니 Parser를 따로따로 정의해주어야 합니다.

3. 이번 PR로 인해 코드베이스의 coverage가 얼마나 증감하는지 알고 싶습니다.

원리상으로는 merge_commit 상에서 테스트를 한 번 수행하고, base_commit으로 git hard reset 후 다시 테스트를 수행하고, 두 결과를 비교하면 됩니다.

문제는 Section이 여러 개 있다는 거죠. 테스트 뽑고, base_commit 으로 hard reset 하고, 테스트 뽑고, merge_commit으로 돌아오고 … 이 과정을 매번 해주어야 합니다. 복잡성을 줄이기 위해 git hard reset 만 전문적으로 해주는 유틸리티 클래스 GitBookmark 를 정의합니다.

4. 테스트 리포트에는 아주 많은 파일이 올라옵니다. 이번 PR에서 변경된 파일들만 필터링하고 싶습니다. “변경된 파일들의 coverage 평균”을 계산할 수 있다면 더 좋겠네요.

변경된 파일들의 목록은 git diff {merge_commit} {base_commit}으로 얻을 수 있습니다.

마크다운 포맷팅을 담당하는 Table 클래스와 수치 계산을 담당하는 Record 클래스를 분리하기로 합니다.

UML 도입

요구사항이 많아질수록 객체의 종류도 점점 많아졌고, 객체 간의 역할 분담이 헷갈리기 시작했습니다. ‘분명히 어려울 일이 아니었는데’ 갈수록 모든 게 꼬이고 있었죠. 해결책이 필요했습니다.

UML(Unified Modeling Language)은 이러한 상황을 타개하고 코드 짜임새를 바로잡는 데 도움을 줄 수 있습니다. UML로 그릴 수 있는 다이어그램은 아주 많은 종류가 있는데 저는 그 중 가장 기본이 되는 클래스 다이어그램과 시퀀스 다이어그램을 그려보려고 합니다.

클래스 다이어그램

클래스가 가진 변수와 메소드를 나열하고 상속 및 호출 관계를 표현한 정적 다이어그램입니다.

https://ko.m.wikipedia.org/wiki/%ED%8C%8C%EC%9D%BC:Composite_UML_class_diagram.svg
  • 실선: Association, 호출 관계
  • 흰색 마름모: Aggregation, 구성 관계, 요소 클래스가 전체 클래스 없이 독립적으로 존재할 수 있음
    예) 학급과 학생
  • 검은색 마름모: Composition, 구성 관계, 요소 클래스가 전체 클래스 없이 독립적으로 존재할 수 없음
    예) 고양이와 고양이 꼬리
  • 흰색 삼각 화살표: Generalization
    – 실선: 부모 클래스와 자식 클래스 사이의 상속 관계
    – 점선: 추상 클래스 및 인터페이스와 구현 클래스 사이의 구현 관계

시퀀스 다이어그램

클래스들 간의 상호 호출 과정을 순서대로 나타낸 동적 다이어그램입니다.

https://en.wikipedia.org/wiki/File:CheckEmail.svg
  • 수직선: Lifeline, 객체의 생성부터 소멸하기까지의 시간선
  • 실선: 호출
  • 점선: 호출에 대한 반응
  • 사각형: 객체가 상호작용에 참여 중(active)

도구

UML은 이론상으로 파워포인트로도 가능하지만 전문 도구를 사용하는 편이 더 좋습니다. 그리기 편하기 때문이 아니라 그리기 더 불편하기 때문입니다! UML에는 정말 많은 구성 요소가 있는데 이것들을 알맞게 사용하지 않으면 읽는 사람에게 오히려 혼란을 줄 수 있습니다. 전문 도구를 사용하면 잘못된 구조를 아예 허용하지 않기 때문에 이 언어의 문법을 빠르게 익히는 데 도움이 됩니다.

  • 이 글에 나오는 다이어그램들은 StarUML을 사용해 그렸습니다.
  • Mermaid.js 를 이용하면 GitHub에서 UML로 의사소통을 할 때 굉장히 유용합니다. 다이어그램을 png 파일로 만들어 보관하면 버전 관리가 불편하다는 문제가 있는데, Mermaid 는 문자 기반이기 때문에 커밋 간 변경사항을 정확히 확인할 수 있습니다.
    – 2022년 6월 현재, 아직 GitHub이 Mermaid 렌더링을 직접 지원하지 않기 때문에 브라우저 확장앱을 설치해야 합니다.
    여기서 Mermaid 코드를 작성하고 결과를 실시간으로 확인할 수 있습니다.

어떤 프로그램들은 코드에서 UML로, 또는 UML에서 코드로의 자동 변환을 지원합니다. 파이썬과 같은 동적 언어보다는 자바와 같은 정적 언어에서 이런 변환이 더 잘 작동하는 편입니다.

Certain classes of well-behaved programs may be diagrammable, but in the general case, it can’t be done. Python objects can be extended at run time, and objects of any type can be assigned to any instance variable. Figuring out what classes an object can contain pointers to (composition) would require a full understanding of the runtime behavior of the program. [https://stackoverflow.com/questions/260165/whats-the-best-way-to-generate-a-uml-diagram-from-python-source-code]

발전: 클래스 다이어그램

StarUML을 이용해 그려본 첫 클래스 다이어그램입니다. 솔직히 제가 봐도 난잡하네요 😮‍💨

그래서 없는 미적 감각을 발휘해서 모양새라도 좀 깔끔하게 바꾸어봅시다. 이제 SectionManager가 코드에서 핵심적인 역할을 하며 리포트 섹션을 모아 종합 리포트 coverage_report를 구성한다는 점이 보이는군요.

좀 나아졌지만 사실 진짜 문제는 따로 남아있습니다. 앞서 보여드린 클래스 다이어그램 예시와 비교해보세요. 제 그림은 시퀀스를 설명하느라 너무 공을 많이 들였고, 정작 클래스 각각의 변수와 메소드에 관한 정보가 많지 않습니다. 그마저도 박스 안에 모여있지 않고 화살표 위에 흩어져 있어 응집력이 떨어지는 모습입니다.

당시에 이 사실을 알았다면 좋았을텐데, 이 그림에서 어떻게 하면 시퀀스를 더 잘 나타낼 수 있을까 고민하느라 많은 시간을 들였습니다. 숫자 꼬리표를 달아 보기도 하고, 화살표의 방향을 좀 예쁘게 바꿔보기도 하고, … 결국 클래스 다이어그램과 시퀀스 다이어그램을 분리하자 마침내 이 난잡함을 해결할 수 있었습니다.

아래는 최종본의 모습입니다.

  • 중요하지 않은 객체를 다이어그램에서 제외했습니다. 화살표의 수가 줄어든 것이 보이시나요?
  • 시퀀스 꼬리표를 삭제했습니다. 그림이 충분히 단순한 덕분에, 꼬리표가 없어도 서로 간의 관계를 파악하는 데는 문제가 없네요.
  • 메소드 이름을 화살표 위가 아니라 객체 안에 표시했습니다. 화살표 위에 어떤 메소드가 있을지는 독자가 그림을 보고 추측하게 해도 괜찮습니다.

발전: 시퀀스 다이어그램

시퀀스 다이어그램 초안입니다. 이 다이어그램의 문제를 짚어봅시다.

  • 화살표의 개수가 너무 많아서 읽기 힘이 듭니다.
  • 화살표 내용에 파이썬 메소드 이름, 셸 명령어, 셸 출력이 섞여 있습니다.
  • 화살표가 왼쪽 그리고 오른쪽 모두에서 뻗어나오기 때문에 계층 관계를 파악하기 어렵습니다. 어느 쪽이 ‘컨트롤러’이고 어느 쪽이 ‘호출받는 쪽’일까요?

이런 문제를 보완하여, 최종본에서는

  • 중요하지 않은 객체를 다이어그램에서 제외했습니다. 제일 헷갈려 하셨던 Makefile과 git reset을 전면에 놓고 나머지는 과감하게 단순화했습니다.
  • 화살표 내용에서 메소드 이름을 삭제하고 인자반환값을 기술하는 것으로 통일했습니다.
  • 모든 호출이 왼쪽에서 오른쪽으로 뻗어나가도록 했고, 반대방향 화살표는 반환값 전달의 목적으로만 사용했습니다.

UML의 효과

UML 다이어그램은 보기에만 좋은 것이 아니라 실제로 코드 짜임새를 개선하는 데 많은 도움이 되었습니다. 저는 처음에는 리포트의 정보를 어떻게 하면 최대한 많이, 하나도 빼먹지 않고 전달해야 한다고 생각했습니다. 그래서 이렇게 길고 알아보기 힘든 표가 만들어졌죠.

그러나 데이터가 많다는 것은 다른 말로 하자면 수치 계산을 해야 할 Record 클래스가 놀고 있다는 뜻이었습니다. 마크다운 포맷팅과 수치계산을 분리하자 진짜 문제는 ‘미적 감각’이 아니라 ‘정보 압축’이라는 것이 분명해졌습니다. 열의 수를 줄이는 한편, 맨 앞에 아이콘 ✅ ❌ 를 추가하여 code coverage에 대한 배경지식이 없어도 어떤 파일을 고쳐야 할지 명확히 보이게 했습니다. 그리하여 최종적인 리포트의 형태는 아래와 같습니다.

결론

UML 다이어그램 그리기는 다음과 같은 효과가 있었습니다.

  • 다이어그램을 그리는 과정에서 제 자신의 디자인 의도를 명확하게 할 수 있었습니다.
    – 클래스 다이어그램에서 어떤 클래스에 연결된 선의 개수가 너무 많다면 그 클래스가 너무 많은 역할을 맡고 있다는 뜻입니다.
    – 시퀀스 다이어그램을 직관적으로 그릴 수 없다면 계층 구조가 명확하지 않기 때문입니다.
  • PR 중에 팀원들에게 코드 짜임새를 보다 수월하게 설명할 수 있었습니다.
    – 동료 분들이 피드백을 주시기 쉬워야 코드 수정도 빨리 하고 approve도 빨리 받습니다!
  • 다이어그램을 README에 올려 놓았기 때문에 향후 다른 분이 코드를 수정하실 때 참고하실 수도 있습니다.

UML은 그 종류나 구성 요소가 많아 처음부터 완벽하게 그리기 어렵다고 생각합니다. 만약 여러분이 이 글을 보고 UML에 관심이 생기셨다면, 문법을 완벽하게 지키는 데 너무 집착하지 말고 일단 그려보세요. ‘단지 그림일 뿐이지만’ 예상 외로 값진 성과를 거둘 수 있을 것이라 기대합니다.

더불어, 이 글이 Glace 신입 지원자 분들에게 하나의 참고할 만한 사례가 되었으면 좋겠습니다. 신입도 얼마든지 팀에 기여할 수 있답니다 :)

--

--