FE개발자의 성장 스토리 05 : 의존관계를 이용해 구조적 리팩토링 포인트 찾기

안녕하세요, FE플랫폼팀 frey 입니다. 작년 초부터 카카오 i오픈빌더(이하 오픈빌더)의 Front-end 개발을 담당하고 있습니다.

앵귤러(Angular)를 기반으로 개발된 오픈빌더는 많은 담당자의 손을 거쳐 제작, 운영되어 왔습니다. 오픈빌더 업무를 진행하면서 잘 정리된 여러 문서와 코드 구조를 통해 설계 구조와 의도를 파악할 수 있었지만, 마치 구전동화가 수많은 선조들의 입에서 입으로 전해진 것처럼, 오픈빌더 또한 여러 개발자들의 손과 손을 거쳐 업데이트되어 왔습니다.

그동안 잘 업데이트된 문서 덕분에 의도한 구조에서 크게 벗어나지 않을 수 있었지만, 문서는 가이드라인일 뿐 개개인의 개성까지 강제할 수는 없었습니다. 그래서 때로는 동작은 하지만 설계 의도와 다른 코드가 생성되기도 하고, 쉽게 발견되지 않을 때도 있었습니다.

개개인의 개성은 코딩 스타일에도 반영될 수 있지만 lint를 이용해 빠르게 발견하고 수정할 수 있습니다. 하지만 개개인의 개성이 소프트웨어 구조에 반영되어 설계 의도를 벗어난다면 어떻게 감지하고 바로 잡을 수 있을까요?

여러가지 방법이 있겠지만 이 글에서는 의존관계를 이용해 구조적으로 리팩토링 포인트를 찾은 방법을 공유해 보고자 합니다.

 


 

의존관계 정의하기

 

설계된 소프트웨어를 의존관계 관점에서 파악하기 위해 의존관계를 정의합니다. 정의된 의존관계를 기반으로 의존관계 그래프가 그려지고 설계 의도와 맞는 그래프가 그려지는지 파악합니다. 먼저 의존관계에 필요한 기준을 정합니다. 의존관계 그래프를 그리기 위해 필요한 정보는 정점(node)과 간선(edge)입니다.

 

정점과 간선

 

정점(node)

의존관계 그래프를 그리기 위해 먼저 정점을 정의합니다. UML의 클래스 다이어그램에서는 클래스를 정점으로 볼 수 있습니다. 오픈빌더에서는 정점을 앵귤러에서 제공하는 “모듈”로 정의했습니다. 

앵귤러는 자체 모듈시스템을 탑재하고 있습니다. ES6의 export / import와 달리 논리적인 코드 집합을 정의할 수 있고 앵귤러 앱은 모듈 단위로 설계/개발할 수 있습니다. 오픈빌더에 존재하는 모듈은 크게 라이브러리 모듈과 UI 모듈로 나눌 수 있습니다.

 

라이브러리 모듈과 UI모듈

 

라이브러리 모듈은 유틸성 코드, 공용으로 사용되는 컴포넌트, 서비스 등 재사용성이 높은 코드들의 집합입니다. UI 모듈은 화면에 보이는 UI 동작을 담당하는 모듈로서 하나의 목적으로 응집될 수 있는 UI 컴포넌트들이 정의되어 있습니다. 만약 UI 모듈에 라우터가 정의되어 있다면 SPA에서 페이지로 존재할 수 있는 모듈이 됩니다. 이렇게 정의된 오픈빌더의 NgModule은 의존관계 그래프에서 정점이 되게 됩니다.

 

간선(edge)

정점이 정의되었다면 정점들을 잇는 간선을 정의합니다. 간선은 정점 간의 관계를 정의합니다. 그것이 추상적이든 구체적이든 두 정점 간의 관계가 형성될 수 있다면, 간선으로 정의할 수 있습니다.  오픈빌더에서는 정점을 NgModule로 정의했으니 간선 또한 NgModule 간의 관계를 말해 줄 수 있는 무언가 되면 좋습니다. 이런 면에서 NgModule은 좋은 기능을 제공해 주고 있습니다.

 

NgModule 선언

 

앵귤러에서 모듈로 구현된 하나의 코드 집합은 다른 모듈이 사용할 수 있습니다. NgModule를 생성할 때 데코레이터의 속성값으로 imports구문을 이용해 다른 모듈을 사용(Use) 할 수 있습니다. 따라서 오픈빌더의 의존관계 그래프에서는 의존관계를 사용(Use)이라는 추상적인 개념으로 정의하고 이것을 구체화하는 도구로 NgModuleimports를 사용했습니다.

정리하면, 생성할 오픈빌더의 의존관계 그래프에서 정점은 NgModule, 그리고 정점들을 잇는 간선은 imports로 정의했습니다.

 

노드는 NgModule로, 간선은 imports로

 

코드에서 관계 그래프로

 

의존관계가 정의되었으니 코드로부터 의존관계 그래프를 그릴 수 있는 데이터를 뽑아내고 이 데이터를 이용해 그래프를 그려봅니다. 코드에 선언된 @NgModule 데코레이터를 찾아내고 NgModule 선언부의 import 속성의 배열 값들과 NgModule로 선언된 Class의 이름을 찾아내야 합니다.

NgModule에서 imports 목록과 모듈의 이름이 필요

 

오픈빌더 코드는 typescript로 작성되어 있습니다. typescript 코드에서 문법적으로 필요한 정보를 가져오는 방법 중 하나는 AST(Abstract Syntax Tree)를 이용하는 방법입니다. 다행히 그리 멀지 않은 곳에 AST를 생성할 훌륭한 툴이 있습니다. typescript로 웹앱을 개발할 때 반드시 설치되어야 할 TSC(typescript complier)입니다.

 

tsc를 이용해 ast 를 생성하는 코드

 

TSC를 이용해 typescript 코드의 AST를 생성하고 AST를 순회하며 원하는 정보를 추출해냅니다. 추출해낸 데이터는 그래프를 그리기 좋은 형태로 가공하여 저장합니다. https://ts-ast-viewer.com/ 사이트를 이용하면 typescript 코드로부터 생성된 AST를 확인해볼 수 있습니다.

 

ts코드에서 그래프를 그리는 과정

 

그리고 AST로부터 얻어낸 데이터와  D3 라이브러리를 이용해 의존관계 그래프를 그립니다.

 

오픈빌더의 의존관계 그래프

 

이렇게 만들어진 의존관계 그래프는 소프트웨어의 전체적인 구성을 파악하는 데 도움이 됩니다. 많은 노드들이 간선으로 복잡하게 연결되어 특별한 규칙이 없어 보이지만, 하나하나 본다면 노드(모듈)들의 역할에 따라 일련의 규칙이 존재합니다.

 

SharedModule, AppModule, UI(MyAccount) Module의 그래프

 

공통 라이브러리에 해당되는 SharedModule은 거의 모든 모듈들이 사용하고 있습니다. 유틸성 코드와 공통 UI 컴포넌트 등 재사용성이 높은 코드들 모여있기 때문이죠. 반면에 AppModule은 거의 모든 모듈을 사용하고 있습니다. 엔트리 포인트에 해당되는 모듈이기 때문에  라우터를 갖는 UI 모듈들을 사용하여 앱을 구성합니다. 그리고 일반적인 UI 모듈은 SharedModule을 사용하고, 필요에 따라 다른 모듈들을 사용하고 있습니다.

각각의 모듈의 자신들의 역할에 맞게 다른 모듈들을 사용하여 의존관계를 맺고 있습니다. 이제 다시 서두에서 묻고 있는 질문을 생각해 봅니다. 이렇게 만든 의존관계 그래프에서 자신의 역할과 다른 형태의 의존관계를 보인다면, 그것은 어떻게 감지할 수 있을까요? 또 어떻게 수치화하여 파악할 수 있을까요? 

 

안정성 지표

 

우리에게 밥아저씨로 널리 알려져 있는 로버트 C. 마틴은 본인의 저서 “클린아키텍쳐” 에서 안정된 의존성 원칙(SDP : Stable Dependencies Principle)을 파악하는 방법으로 안정성 지표를 이야기하고 있습니다.

안정성 지표는 불안정성(Instability)을 측정하여 확인할 수 있습니다. 불안정성 I는 특정 코드 집합(여기서는 NgModule)이 얼마나 불안정한지를 나타내는 값입니다. 노드로 들어오는 간선(FanIn)과 노드로부터 나가는 간선(FanOut)을 측정하여 수치화합니다. 

 

FanIn, FanOut에 따른 I 값

 

불안정성은 0 ~ 1사이의 값을 가지며 0에 가까울수록 “안정”하다고, 1에 가까울수록 “불안정”하다고 칭합니다. 노드의 FanOut 값이 높고 FanIn 값이 낮으면 I는 1에 가까워집니다. 반대로 FanOut의 값이 낮고 FanIn의 값이 높으면 0에 가까워집니다. 따라서 오픈빌더의 의존관계 그래프에서 어떤 A라는 모듈의 I 값이 1에 가깝다면 A 모듈이 사용하는 모듈의 수가 A 모듈을 사용하는 모듈들의 수보다 상대적으로 많다고 볼 수 있습니다. 반대로 A 모듈의 I 값이 0에 가깝다면 그 반대가 됩니다. 이런 불안정성을 나타내는 값이 갖는 특징을 정리해보면 아래 표와 같습니다.

 

I값이 0과 1일때의 특징 표

 

변경 관점에서 불안정성을 살펴보면 다음과 같습니다. 어떤 A 모듈의 I 값이 0에 가까워 “안정적” 라고 할 경우, A 모듈을 사용하는 다른 모듈이 많아서(높은 FanIn) 변경에 신중해야합니다. 만약 A 모듈에 변경이 가해진다면 A 모듈을 사용하는 다른 모든 모듈이 그 영향을 받게 되죠. 그렇기에 변경하기 “어렵다”라고 볼 수 있습니다. 또한 “안정적”이기 때문에 A 모듈이 다른 모듈을 사용하는 경우는 드뭅니다.(낮은 FanOut) 그렇기 때문에 반대로 다른 모듈에 의해 변경될 이유가 없죠. I가 1에 가까우면 그 반대가 됩니다.  

표에 정리된 내용을 기반으로 앞서 생성된 의존관계 그래프의 모듈들을 분석해보면 모듈이 의도한 역할과 실제 소프트웨어 내에서의 역할을 비교해볼 수 있습니다.

 

그래프에서 발견된 문제점 그리고 개선

 

오픈빌더 모듈들의 I값

 

I 값을 기준으로 나열된 차트를 보면 SharedModuleI 값이 전체 모듈 중 낮은 편에 속한다는 것 을 확인할 수 있습니다. SharedModule은 공통 라이브러리에 해당되는 모듈이기 때문에 다른 많은 모듈들이 사용하고 있습니다. 그렇기 때문에 안정적이길 기대하고, 실제 소프트웨어 내에서도 안정적인 I 값을 보여주고 있습니다.

반대로 AppModule은 엔트리 포인트에 해당되는 모듈로서 거의 모든 모듈을 사용하고 있습니다. 앞서 설명드렸듯이 다른 모듈들을 사용해 전체 라우터를 구현하고 웹앱을 구동시킵니다. 그렇기에 기대했던 I 값은 1에 가까운 값이었고, 실제 측정된 값은 1이 되었습니다. SharedModule, AppModule 모두 기대한 I 값과 실제 I 값이 차이가 없는 것이죠.

관점을 조금 바꿔서, “사용” 관점에서 봤을 때 모듈은 자기보다 안정적인 모듈을 사용하는 방향으로 그래프가 그려지는 것이 이상적입니다. 이러한 전제를 두고 오픈빌더의 의존관계 그래프를 본다면 문제점을 발견할 수 있습니다. 대부분의 모듈은 자기보다 안정적인 모듈을 사용하고 있습니다. 하지만 의도한 방향과 다른 방향으로 간선이 그려지는 모듈들이 존재합니다. 이 모듈들은 의도한 설계와 맞지 않은 모듈로 간주할 수 있고 리팩토링의 대상이 되는 모듈이 됩니다.

 

StandaloneTestModule

 

StandaloneTestModule

 

StandaloneTestModule은 특정 UI 동작을 독립적으로 테스트하기 위해 만든 페이지 모듈입니다. 독특하게도 FanIn이 없기 때문에 I 값은 1을 가지며 페이지 모듈임에도 불구하고 엔트리 포인트 모듈(AppModule)로부터 사용되지 않고 있습니다. 오픈빌더 코드를 살펴보면 StandaloneTestModule이 사용되는 곳은 없습니다. 모듈 이름에서 보이듯이 Test를 위해 오래전에 만들어졌던 모듈이며, 더 이상 사용되고 있지 않은 모듈입니다.

 

TutorialGuideModule

 

TutorialGuideModule

 

TutorialGuideModule은 챗봇 생성을 위한 튜토리얼 UI 모듈입니다. SharedModule 과의 관계를 보면 안정적인 모듈이 불안정한 모듈을 사용하고 있습니다. 이런 의존관계를 갖게 된 이유는 SharedModuleTutorialGuideModule을 re-export 하고 있기 때문입니다. 사실 SharedModule에서는 TutorialGuideModule을 사용하지 않습니다. 두 모듈의 목적을 보더라도 SharedModuleTutorialGuideModule을 re-export 해줄 필요는 없습니다. SharedModule은 재사용될 수 있는 공용 컴포넌트나 유틸성 코드를 포함하고 있어야 하는데, 재사용되지 않는 특정 목적의 TutorialGuideModuleSharedModule에 포함되어 있는 것입니다. 

개선해본다면 TutorialGuideModule을 독립적인 모듈로 생성하고 이 모듈을 사용하는 모듈이 imports 하여 사용하도록 개선할 수 있습니다. 하지만 아쉽게도(?) TutorialGuideModule 또한 사용되지 않는 코드이며 과거 작업의 잔재였습니다.

 

BotTestDialogModule

 

BotTestDialogModule

 

BotTestDialogModule은 생성한 봇을 웹에서 간단히 테스트할 수 있는 카톡 UI를 갖는 UI 모듈입니다. 이 모듈 또한 앞서 보셨던 TutorialGuideModule과 마찬가지로 SharedModule에서 re-export 해주고 있습니다. 그렇다 보니 우리가 바랬던 안정적인(낮은 I) 모듈이 불안정한(높은 I) 모듈을 사용하는 모양과는 반대의 모양을 보여주고 있습니다. BotTestDialogModule 역시 SharedModule이 re-export 해줄 이유가 없기 때문에 SharedModule 과의 관계를 끊거나 역전시키는 방향으로 개선할 수 있습니다.

 

정리하면

 

이번 글에서는 불안정성 수치를 이용해 의존 방향이 올바른가를 판단하여 리팩토링 포인트를 찾아봤습니다. 노드에 해당되는 코드 집합의 범주를 어디까지 정의할지, 노드 간의 관계를 정의하는 개념은 어떤 것을 사용할지에 따라 그래프는 다르게 그려지고 다른 문제점을 발견할 수 있습니다. 만약 노드의 범주를 “함수”로 정의하고 “호출”이라는 개념으로 노드 사이의 관계를 정의한다면 아주 자세하고 구체적인 의존관계가 그려질 것입니다. 

  불안정 수치를 보고 안정적인 코드 집합이 좋다, 불안정적인 것은 나쁘다고 말할 순 없습니다. 이 수치는 상대적인 값입니다. 코드 집합은 소프트웨어 내에서 각자의 역할이 있고 그 역할에 맞는 불안정 수치를 갖는지가 중요합니다. 그리고 코드 집합 간의 관계에 불안정 수치를 반영하여 설계 의도에 맞는 관계가 형성되고 있는지 파악이 필요합니다.

 

PS : 더 나아가서

 

의존관계 그래프와 불안정 수치를 이용해 리팩토링 포인트를 찾아보았습니다. 이 아이디어를 조금 더 발전시키면 다음과 같은 것을 해볼 수 있습니다.

모듈은 서로 연쇄적인 관계를 갖습니다. 하나의 모듈이 변경되면 그 변경이 다른 모듈에 영향을 끼칠 수  있죠. 불안정성 수치와 의존관계 그래프를 이용하면 모듈 하나가 변경되었을 때 전체 시스템에 미치는 영향을 수치화할 수 있습니다.

또한 좀 더 정확한 불안정성 수치를 계산하기 위해 FanOut, FanIn 수치에 가중치를 두어 불안정 수치를 다시 계산해볼 수 있습니다. “코드 컴플리트 2″ 책에서는 좋은 설계 원칙 중 하나로 높은 FanOut과 낮은 FanIn을 이야기하고 있습니다. 불안정성을 계산하는 공식에 의하면 동일한 I 값이어도 FanOut과 FanIn이 다를 수 있습니다. FanOut, FanIn의 절댓값을 불안정성 계산식에 적용하여 불안정성 수치를 미세조정해볼 수 있습니다.

마지막으로 테스트 적용 기준으로서의 불안정성 수치입니다. 이상적인 테스트 커버리지는 100% 지만 여건상 그렇지 못한 경우가 대부분입니다. 그럴 때 반드시 필요하다고 생각되는 코드들에 대해서만 테스트를 진행할 수 있는데, 이때 불안정 수치를 기준으로 삼을 수 있습니다. 낮은 불안정성 수치를 갖는(다른 모듈이 많이 사용하는, 변경에 신중해야 하는) 코드들에 대해서만 테스트를 진행할 수도 있고, 반대로 높은 불안정성 수치를 갖는(다른 모듈을 많이 사용하는, 변경이 쉬운) 코드들에 대해서만 테스트를 진행할 수 있습니다. 특정 I 값을 기준으로 이 기준에 만족하는 코드만 테스트를 수행하도록 테스트 작성 기준을 수치화할 수 있습니다.

긴 글 읽어주셔서 감사합니다.

카카오톡 공유 보내기 버튼

Latest Posts

제5회 Kakao Tech Meet에 초대합니다!

Kakao Tech Meet #5 트렌드와 경험 및 노하우를 자주, 지속적으로 공유하며 개발자 여러분과 함께 성장을 도모하고 긴밀한 네트워크를 형성하고자 합니다.  다섯 번째

테크밋 다시 달릴 준비!

(TMI: 이 글의 썸네일 이미지는 ChatGPT와 DALL・E로 제작했습니다. 🙂) 안녕하세요, Kakao Tech Meet(이하 테크밋)을 함께 만들어가는 슈크림입니다. 작년 5월에 테크밋을 처음 시작하고,