본문 바로가기메뉴 바로가기


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이 담당하는 화면

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 값을 기준으로 이 기준에 만족하는 코드만 테스트를 수행하도록 테스트 작성 기준을 수치화할 수 있습니다.

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

frey.ryu
frey.ryu 남들은 해적왕, 호카게가 되고싶어할 때 설계왕이되고 싶은 FE개발자입니다.
Top Tag
2021
2021-new-krew
Ad Platform
Ad tech
adaptive-hash-index
adt
adtech
agile
agilecoach
ai
algorithm
Algorithm/ML
Algorithm/Ranking
almighty-data-transmitter
Analyzer
android
angular
anycast
App2App
applicative
Architecture
arena
ast
async
aurora
babel
babel7
Backend
BApp
bgp
big-data
ble
blind-recruitment
block
Block Chain
blockchain
bluetooth
brian
business
Cache
cahtbot
canvasapi
Caver
cch
cd
CDR
ceph
certificate
certification
cgroup
chrome
ci
cite
client
clojure
close-wait
cloud
cloudera-manager
clustered-block
cmux
cnn
code-festival
code-review
codereview
coding
coding test
competition
Compliance
component
conference
consul
container
contents
contest
cookie
core-js@3
Corporate Digital Responsibility
couchbase
COVID-19
cpp
Data
data-engineering
DB
deep-learning
Dependency
dependency-graph
dev
dev-session
dev-track
developer
developer relations
developers
devops
digitalization
digitaltransformation
dns
docker
dr
employeecard
emscripten
eslint
Feature List
Featured
Feedback
friendstime
front-end
frontend
functional-programming
funfunday
fzf
garbage-collection
gawibawibo
GC
github
globalpollution
go
graphdb
graphql
Ground X
growth
ha
hadoop
hate speech
hbase
hbase-manager
hbase-region-inspector
hbase-snashot
hbase-table-stat
hbase-tools
hri
id
if kakao
ifkakao
infrastructure
innodb
internship
ios
item
Java
javascript
javascript web-assembly
JCMM
JIRA
jsconf
jsconfkorea
json
k8s
kafka
kakao
kakao-Career-Boost-Program
kakao-commerce
kakao-games
kakaoarena
kakaocommerce
kakaocon
kakaoenterprise
kakaok
kakaokey
kakaokrew
kakaomap
kakaopage
kakaotalk
KAS
KCDC
khaiii
Klaytn
Klip
kubernetes
l3dsr
l4
License
links
Linux
load-balancing
MAB
Machine Learning
machine-learning
map
marathon
meetup
melon
mesos
message
Messaging
microservice
Microsoft TypeScript
mobil
monad
monorepo
MSA
mtre
mysql
mysql-realtime-traffic-emulator
nand-flash
network
new
new-krew
nfc
nomad
ocp
olive
onboarding
open
open source
opensource
openstack
OpenWork
OSS
page
parallel
PBA
performance
planning poker
Platform
polyfill
programming-contest
project-structure
pycon
python
quagga
react
reactive-programming
reactor
recap
recommendation
recommendation system
recruitment
redis
redis-keys
redis-scan
related-blind
rest
Rome
rubics
ruby
rxjs
s2graph
scala
scalaz
seminar
Serve
server
service
sharding
shopping
socket
spark
spark-streaming
SpringBoot
ssd
Statistics/Analysis
Stomp
storage
storm
style-guide
summer internship
support
System
talk
talkchannel
tcp
tech
Techtalk
test
Thread-Debugging
time-wait
tmux
Topic Modeling
typescript
Untact
update
User Story
vim
vim-github-dashboard
vim-plugin
vue
vue.js
WASM
web-cache
webapp
webgl
WebSocket
webworkers
weekly
work
workplatform
개인화 추천
길찾기
라이선스
연관 추천
오픈소스
오픈소스검증
의존성분석
일하는방식
협업
All Tag
2021
2021-new-krew
Ad Platform
Ad tech
adaptive-hash-index
adt
adtech
agile
agilecoach
ai
algorithm
Algorithm/ML
Algorithm/Ranking
almighty-data-transmitter
Analyzer
android
angular
anycast
App2App
applicative
Architecture
arena
ast
async
aurora
babel
babel7
Backend
BApp
bgp
big-data
ble
blind-recruitment
block
Block Chain
blockchain
bluetooth
brian
business
Cache
cahtbot
canvasapi
Caver
cch
cd
CDR
ceph
certificate
certification
cgroup
chrome
ci
cite
client
clojure
close-wait
cloud
cloudera-manager
clustered-block
cmux
cnn
code-festival
code-review
codereview
coding
coding test
competition
Compliance
component
conference
consul
container
contents
contest
cookie
core-js@3
Corporate Digital Responsibility
couchbase
COVID-19
cpp
Data
data-engineering
DB
deep-learning
Dependency
dependency-graph
dev
dev-session
dev-track
developer
developer relations
developers
devops
digitalization
digitaltransformation
dns
docker
dr
employeecard
emscripten
eslint
Feature List
Featured
Feedback
friendstime
front-end
frontend
functional-programming
funfunday
fzf
garbage-collection
gawibawibo
GC
github
globalpollution
go
graphdb
graphql
Ground X
growth
ha
hadoop
hate speech
hbase
hbase-manager
hbase-region-inspector
hbase-snashot
hbase-table-stat
hbase-tools
hri
id
if kakao
ifkakao
infrastructure
innodb
internship
ios
item
Java
javascript
javascript web-assembly
JCMM
JIRA
jsconf
jsconfkorea
json
k8s
kafka
kakao
kakao-Career-Boost-Program
kakao-commerce
kakao-games
kakaoarena
kakaocommerce
kakaocon
kakaoenterprise
kakaok
kakaokey
kakaokrew
kakaomap
kakaopage
kakaotalk
KAS
KCDC
khaiii
Klaytn
Klip
kubernetes
l3dsr
l4
License
links
Linux
load-balancing
MAB
Machine Learning
machine-learning
map
marathon
meetup
melon
mesos
message
Messaging
microservice
Microsoft TypeScript
mobil
monad
monorepo
MSA
mtre
mysql
mysql-realtime-traffic-emulator
nand-flash
network
new
new-krew
nfc
nomad
ocp
olive
onboarding
open
open source
opensource
openstack
OpenWork
OSS
page
parallel
PBA
performance
planning poker
Platform
polyfill
programming-contest
project-structure
pycon
python
quagga
react
reactive-programming
reactor
recap
recommendation
recommendation system
recruitment
redis
redis-keys
redis-scan
related-blind
rest
Rome
rubics
ruby
rxjs
s2graph
scala
scalaz
seminar
Serve
server
service
sharding
shopping
socket
spark
spark-streaming
SpringBoot
ssd
Statistics/Analysis
Stomp
storage
storm
style-guide
summer internship
support
System
talk
talkchannel
tcp
tech
Techtalk
test
Thread-Debugging
time-wait
tmux
Topic Modeling
typescript
Untact
update
User Story
vim
vim-github-dashboard
vim-plugin
vue
vue.js
WASM
web-cache
webapp
webgl
WebSocket
webworkers
weekly
work
workplatform
개인화 추천
길찾기
라이선스
연관 추천
오픈소스
오픈소스검증
의존성분석
일하는방식
협업

위로