Call chain analyzer 소개

안녕하세요, SK 플래닛에서 Store 플랫폼을 개발하고 있는 정화수입니다.

남이 만든 코드를 읽으려 하면 어디서부터 시작해야 하는지 참 막막한 경우가 많습니다. 코드가 잘 설계되어 있고 깔끔하더라도, 그 구조의 파악은 꽤나 어렵습니다. 잘 분해된 클래스들 사이로 로직을 따라가다 보면 구글 없이 웹서핑하는 기분을 간접적으로나마 느낄 수 있습니다.

주어진 문제를 해결할 수 있는 코드의 단편을 드디어 발견했습니다. 득의양양하던 것도 잠시… 이내 이를 수정했을 때 미칠 파장에 대해 끝도 없는 고민을 시작해야 합니다. 수십 만 줄의 코드 더미 속에서  변경 영향도를 파악하는 일은 마치 쓰레기장을 헤매는 느낌입니다. 꼼꼼하게 만들어진 단위테스트 그물만 있다면 어디선가 에러가 터져 변경에 따른 부작용을 자동으로 찾아줄 수 있을 텐데…  이를 사전에 준비하지 않았던 전임자들이 원망스럽습니다.

하지만 이런 회귀테스트 그물은 공짜가 아니라서, 현실에서는 단순 커버리지 100% 도 확보가 쉽지 않습니다. 방대한 소스 안에서 변경 영향도를 파악하는 일은 개발자 혼자서 감당하기에 벅찬 일입니다.

문제가 발생하는 부분은 디버깅을 통해 쉽게 발견할 수 있지만, 수정에 따른 변경 영향도를 파악하기 위해서는 각 요소 별 호출관계를 분석할 필요가 있습니다.

SQL의 department 컬럼명 수정으로 인해 발생하는 변경 영향도 파악

그림1. SQL 의 department 컬럼명 수정으로 인해 발생하는 변경 영향도 파악

위와 같은 관계도를 저희는 Call chain diagram (호출관계도) 이라고 가칭하였습니다. 코드 중심의 Activity diagram이라고 볼 수 있는데요. Sequence diagram과의 차이점은 다음과 같습니다.

  1. 모듈간 작동 흐름을 기술하는 것에 중점이 맞춰져 있습니다.
  2. 다양한 모듈(API, Java, SQL) 간 호출관계를 한 눈에 보여줍니다.

Call chain diagram 은 코드 기반의 산출물인 만큼, 이를 문서로 관리하는 것은 실효성이 낮습니다.

상시 변경되는 코드에 대한 호출관계를 사람이 수작업으로 문서화하는 것은, 정확성이 낮아 실효성도 의문이며 작업량이 높은 편이라 비용 측면에서도 바람직하지 않습니다. 과거 사례를 비교해 드리면 다음과 같습니다.

작업방식 수작업으로 분석 도구에 의한 분석
작업시간 요건당 1주일 수십 초
정확도 50% 미만 95% 이상

현재 T store는 Call chain analyzer 라는 자체 제작한 분석도구를 통해 호출관계를 손쉽게 확인하여 작업에 활용하고 있습니다.

  1. 남이 만든 소스의 구조 전반을 파악하는 것도 용이하지만
  2. 변경 영향도 파악이 손쉬워, 개발비용 및 일정을 합리적으로 산정할 수 있게 되었습니다.
  3. 이로 인해 다음과 같이 일하는 방법의 변화를 가져올 수 있었습니다.
As-Is 요건접수 → 감으로 개발범위 파악개발착수
→ Risk 관리 실패 → 일정지연 및 (사업) 기회비용 손실
To-Be 요건접수 → 영향도 분석 → 비용 및 일정 도출 → 일정 협의 → 개발착수
→ 일정 준수

개발과정

소스로부터 설계를 복원해 모듈간 호출관계를 시각적으로 보여주는 솔루션은 이미 시장에 다수 존재합니다.

  • Enterprise Architect (SPARX)
  • Umodel (Altova)
  • UML Modeller (Umbrello)
  • UML2, (Soyatech)
  • Source Insight (Source Dynamics)
  • Doxygen
  • Visual Paradigm for UML
  • Architexa

하지만 이런 솔루션들은 ② Java의 method간 호출관계만 파악해 줄 뿐,  ① URI → Java,  ③ Java → SQL 간 호출관계까지 수집해주진 않습니다.

그림 2. Call Chain Diagram 모식도
그림 2. Call chain diagram 모식도

 ① URI → Java,  ③ Java → SQL 간 호출관계는 API를 구성하는 필수 요소로, 이를 모르면 영향도 파악이 중간에 끊어집니다.

    • API URI가 어떤 Java 프로그램과 매핑되는지
    • Java 프로그램이 결국 어떤 DB 테이블 및 컬럼을 억세스하고 있는지
    • 또 이를 수정했을 경우 어떤 다른 모듈에 영향을 미치는지 등

하지만 이런 호출관계는 특정 framework또는 패턴에 종속적인 일종의 설정이며 표준도 아니기 때문에, 이를 범용 솔루션에서 바라는 것은 어려워 보였습니다.

물론 Byte code injection 등을 통해 runtime시 호출 stack을 수집하는 방법도 있지만, 이는 다음과 같은 아쉬움이 있어 가급적 소스로부터 호출관계를 직접 파악해내고 싶었습니다.

  • 100% 테스트 커버리지가 확보되지 않는다면 호출관계 파악에 결손이 발생합니다.
  • 100% 테스트 커버리지 확보는 현실적으로 어렵다는 가정 하에,  Call Chain이 끊어졌다는 정보를 신뢰하기 어려워집니다. (정말 끊어진 건지, 아니면 단순히 자료가 수집되지 않은 건지)

Reverse engineering 을 통한 호출관계 파악

URI → Java 호출관계는 프레임워크 설정을 통해 비교적 손쉽게 mapping 관계를 도출할 수 있습니다.

Java의 개별 클래스 구조도 reflection을 이용해 파악이 가능했습니다.

하지만 Java의 method → method, method → SQL 호출관계는 소스를 통해 도출해내지 않으면 파악이 불가능했습니다.

처음에는 소스 구문 분석을 통해 Call chain 관계를 도출해 보았습니다. 그러나 구문 분석으로 찾아내지 못하는 패턴을 확인해 보완할수록, 이는 컴파일러를 제작하는 것과 동일한 일이라는 것을 깨닫게 되었습니다.

보다 손쉬운 방안은 class 파일을 역공학으로 직접 읽어 들여 관계를 수집하는 거였는데요.

javap 로 컴파일 된 class 파일을 disassemble 시킨 결과에서 일말의 가능성을 확인할 수 있었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Compiled from “DisplayListProductDaoImpl.java”
public class com.omp.admin.display.listprod.dao.DisplayListProductDaoImpl
extends com.ibatis.dao.client.template.SqlMapDaoTemplate
implements com.omp.admin.display.listprod.dao.DisplayListProductDao{
public com.omp.admin.display.listprod.dao.DisplayListProductDaoImpl(com.ibatis.dao.client.DaoManager);
  Code:
   0:    aload_0
   1:    aload_1
   2:    invokespecial    #10; //Method com/ibatis/dao/client/template/SqlMapDaoTemplate.”<init>”:(Lcom/ibatis/dao/client/DaoManager;)V
   5:    return
public java.util.List getDisplayListProduct(com.omp.admin.display.listprod.dao.DisplayListProductCriteria);
  Code:
   0:    aload_0
   1:    ldc    #22; //String displayListProduct.searchDisplayListProductWithPaging
   3:    aload_1
   4:    invokevirtual    #24; //Method queryForList :  (Ljava/lang/String;Ljava/lang/Object;)Ljava/util/List;
  13:    dup
  14:    invokespecial    #39; //Method com/omp/admin/display/listprod/service/DeleteProductService.”<init>”:()V
  17:    putfield    #35; //Field deleteService:Lcom/omp/admin/display/listprod/service/DeleteListProductService;
  20:    areturn

클래스 파일의 byte 코드들은 소스가 아무리 다양한 패턴으로 작성되어 있어도 형태가 일정하기 때문에 호출관계를 파악하기가 훨씬 용이했습니다.  (시장의 호출관계 분석도구도 클래스 파일에서 연관성을 추출하는 것으로 추정됩니다.)

다만 javap 출력결과로 작업하는 것은 속도도 나쁘고 주석 형태의 참조정보를 다루기도 어려워, Object web 컨소시엄에서 만든 ASM 이란 라이브러리를 활용해 클래스 파일을 분석하였습니다.

Java의 method → method 호출관계는 다음과 같은 부분을 주의해서 추출했습니다.

  1. Method → Interface → Concreate method 호출
  2. Method → Thread → Runner → Another method 호출

그리고 URL → Java,  Java → SQL 호출관계는 특정 메서드에 세팅되는 파라미터의 문자열 값을 이용해 추출했습니다.

  1. Spring의 @RequestMapping 어노테이션에 할당되는 파라미터 값을 추출합니다.
  2. Struts의 ambiguous URL 패턴 (예를 들면 /api/select{*}) 은 관련 class의 skeleton 분석결과를 이용해 이용 가능한 public method 명칭을 조합합니다.
  3. iBatis 또는 MyBatis의 query 메서드에 할당되는 파라미터 값을 추출합니다.
  4. JDBC의 statement 메서드에 할당되는 파라미터 값을 추출합니다.
  5. 기타 특정 메서드에 할당되는 파라미터 값을 사용자 정의 호출관계로 뽑아냈습니다.(API 호출은 HttpClient를 이용한다거나 등등)

그런데, 이 파라미터 값은 단순하게 문자열로만 할당되진 않습니다. 제가 확인한 패턴은 대략적으로 다음과 같습니다.

  1. 문자열 자체가 constant pool (또는 stack) 에 할당됩니다.
  2. String 인스턴스가 할당됩니다.
  3. StringBuilder 또는 StringBuffer 의 + 연산으로 만들어 질 수 있습니다.
  4. 사칙연산으로 계산된 숫자와 결합되어 할당됩니다.사칙연산은 후위배열식으로 저장됩니다. (후위배열식을 계산할 모듈이 필요합니다.)
  5. 상수 또는 전역변수와 결합되어 할당됩니다.
  6. 분기문에 대한 고려가 필요합니다.분기문에 의해 method가 여러 SQL을 호출할 가능성을 추출할 수 있어야 합니다.
  7. Map, Collection 및 Value Object에 담긴 값의 추적성을 확보해야 합니다.단, 같은 클래스가 아닌 경우에는 이와 같이 유통되는 값을 추적하는 것이 쉽지 않습니다.극단적으로는 화면으로부터 SQL을 직접 입력 받는 안티패턴이 존재할 수 있으며, 이는 runtime 하에서만 추적이 가능해 보입니다.

위와 같은 다양한 패턴을 처리하기 위해서는 opcode를 이해하고 제어과정을 분석할 필요가 있습니다.

Call chain analyzer

Call chain analyzer는 chain 별 호출관계 및 SQL 정보를 수집 후, 특정 chain을 중심으로 Call chain diagram을 추출해 보여주는 도구입니다.

그림 3. Call Chain Analyzer 시스템 구성도

그림 3. Call chain analyzer 시스템 구성도

수정할 모듈(Root chain)을 중심으로 호출관계를 backward 및 forward 검색하여 더블트리 형태로 diagram을 구성하며, Root chain은 키워드 기반으로 AND, OR, NOT, 괄호 등을 이용해 원하는 타겟을 정확히 집어낼 수 있도록 설계되어 있습니다. 검색 가능한 키워드는 다음과 같습니다.

  1. Package (소스의 프로젝트 명)
  2. 유형 (URI, Java, SQL)
  3. Class, Method, Parameter 명
  4. iBatis 에서 사용하는 SQL id 또는 SQL 문장검색

유저가 사용하는 화면은 다음과 같습니다.

1. SQL explorer

SQL 중심으로 Root chain을 확인합니다.

Report 성격의 복잡한 SQL은 단순 키워드 검색만으로는 원하는 분석 대상인지 확신이 어려워,  문장을 직접 확인하면서 Root chain을 선택하게끔 만들었습니다.

그림 4. Sql Explorer 검색화면
그림 4. SQL Explorer 검색화면

2. Call chain analyzer

수정할 대상(Root chain)을 검색 후, 이를 기반으로 Call chain diagram을 조회합니다.

Root chain은 다음과 같은 방법으로 선택합니다.

  1. 앞서 SQL explorer 에서 선택한 SQL id
  2. 특정 키워드를 문장에 포함하고 있는 SQL
    [예] SAMPLE_TABLE 이란 키워드와 some_column 이란 키워드를 동시에 갖는 SQL
  3. 특정 키워드를 포함하는 Package (Project 명), URI, Class, Method, Parameter, SQL id
    [예1] SampleTest란 프로젝트 내에서 retrieve란 키워드를 포함하는 URI
    [예2] updateUser 란 키워드를 포함하며 UserVo란 파라미터를 사용하는 method

그림 5. Root Chain 검색화면그림 5. Root Chain 검색화면

Root chain 검색목록으로부터 다음과 같이 Call chain diagram을 추출할 수 있습니다.

그림 6. 단순 호출구조

그림 7. 복잡도가 높은 Call chain diagram

Call chain analyzer 에서 지원 가능한 분석 유형은 다음과 같습니다.

유형 지원대상
URI to Method Struts, Spring
Method to SQL iBatis, MyBatis, JDBC (소스 내 존재하는 Inline Text 포함)
Method to Method 모든 Java 소스
Method to URI User defined Pattern

분석 자체는 호출 / 피호출 chain 정보를 기반으로 수행되기 때문에, parser만 정확하다면 기타 언어도 지원 가능합니다.

맺음말

Call chain analyzer 는 현장에서 다음과 같은 용도로 활용되고 있습니다.

  1. 설계 복원을 통한 코드의 전반적인 기능 파악
  2. 사용 중인 모든 SQL 검색 및 피호출 관계 분석
  3. 변경 영향도 파악을 통한 개발 전략 수립
  4. 미사용 소스 식별 및 이를 통한 관리범위 통제

특히 2013년 T store refactoring 에서 초기 개발 전략 수립에 다음과 같이 유용하게 활용되었습니다.

  1. 기존 소스로부터 설계를 복원하여 서비스간 개발 범위를 정의
  2. 미사용 소스 식별을 통한 개발비용 예측
    – 특정 서비스는 75% 정도가 미사용되고 있어 이를 근거로 개발 예산을 다른 곳에 효과적으로 재배분할 수 있었습니다.

소스 기반의 호출관계 파악에만 머무르고 있어, 일부 runtime 환경에서만 확인 가능한 호출관계(설정 또는 UI로부터 인입되는 값에 의해 결정되는 호출 파라미터)는 보완 예정입니다.

향후 소스 기반의 분석 결과와 runtime 환경에서의 호출 빈도를 결합하여, 분석 결과에 놓치는 부분이 없도록 신뢰도를 높일 뿐 아니라 호출 빈도를 통한 모듈의 가치까지 함께 분석할 수 있도록 기능을 개선할 계획입니다.

정화수 Store기술개발팀

실용주의를 좋아하는 프로그래머입니다.

Facebook Google+ 

공유하기