- FlowTracker는 Java 프로그램이 데이터를 읽고, 조작하고, 쓰는 과정을 추적하는 Java agent로, 출력이 어떤 입력·파일·네트워크·코드 상수에서 왔는지 연결해 보여줌
- 실행 중인 프로그램을 관찰해 파일 및 네트워크 I/O를 표시하고, 특히 입력과 출력의 대응 관계를 추적해 Java 프로그램의 출력이 무엇을 의미하고 왜 생성됐는지 이해하게 해줌
- Spring PetClinic 데모에서는 HTTP 응답의 헤더, Thymeleaf 템플릿, 데이터베이스 값, SQL 삽입 스크립트까지 따라가며 소프트웨어 스택 여러 계층을 탐색할 수 있음
- 내부적으로는 JVM 로딩 시점에 바이트코드를 계측하고, JDK 메서드 훅, 데이터 흐름 분석,
ThreadLocal기반 호출 추적,ClassOriginTracker를 조합해 문자열·문자·바이트 중심의 출처 매핑을 유지함 - 현재 상태는 프로덕션 준비보다 개념 증명에 가깝고, 일부 예제 프로그램에서는 잘 동작했지만 모든 프로그램에 적합하지 않으며 큰 오버헤드로 실행 속도가 많이 느려짐
FlowTracker가 추적하는 것
- FlowTracker는 Java 프로그램 안에서 데이터가 어떻게 읽히고, 전달되고, 변형되고, 쓰이는지 추적하는 Java agent임
- 파일과 네트워크 I/O를 보여주는 데서 그치지 않고, 프로그램의 출력이 어떤 입력에서 왔는지 연결해 보여줌
- 목적은 Java 프로그램의 출력이 무엇을 의미하는지, 그리고 프로그램이 왜 그 출력을 썼는지 이해하는 데 있음
- 현재 프로젝트는 이 관점에서 프로그램 동작을 보면 어떤 통찰을 얻을 수 있는지 탐색하는 proof-of-concept임
데모: Spring PetClinic에서 HTTP 응답의 출처 추적
- FlowTracker PetClinic demo는 Spring PetClinic이 HTTP 요청을 처리하고, 템플릿과 데이터베이스 데이터를 기반으로 HTML 페이지를 생성하는 과정을 브라우저에서 볼 수 있게 함
- 화면에는 PetClinic이 네트워크로 보낸 HTTP 응답이 표시되며, 응답 본문의 일부를 클릭하면 아래쪽 뷰에서 그 부분이 어디서 왔는지 확인할 수 있음
- 왼쪽 트리나 모바일의 왼쪽 아래 버튼에서 추적된 입력·출처 또는 출력·싱크를 선택할 수 있음
-
HTTP 처리 계층
"HTTP/1.1"이나 HTTP 헤더를 클릭하면 이 응답 부분이org.apache.coyote패키지의 Apache Coyote 클래스에서 생성됐음을 볼 수 있음- FlowTracker는 어떤 코드가 어떤 출력을 만들었는지 표시함
-
Thymeleaf 템플릿 계층
"html"이나"head"같은 HTML 태그 이름을 클릭하면 해당 HTML 부분이layout.html파일에서 왔음을 볼 수 있음layout.html을 클릭한 뒤 아래쪽의 색상+버튼을 누르면 그 파일에서 온 모든 부분이 같은 색으로 표시됨- 아래로 스크롤하면 응답 일부가 다른 파일인
ownerDetails.html에서 왔음을 확인할 수 있음 <또는>문자를 클릭하면 해당 문자가 Thymeleaf 템플릿 라이브러리에 의해 쓰였음을 볼 수 있음
-
데이터베이스 값의 출처
- HTML 페이지의 테이블에는 데이터베이스에서 온 정보가 포함됨
- 테이블의
George를 클릭하면 그 값이 데이터베이스에서 왔다는 수준을 넘어, 처음 데이터베이스에 값을 삽입한 SQL 스크립트까지 추적됨 - 이 데모에서 SQL 스크립트까지 추적되는 이유는 인메모리 데이터베이스를 사용해 데이터베이스 내용이 JVM 밖으로 나가지 않았기 때문임
MySQL 데모와 프레임워크 독립성
- 같은 PetClinic 데모를 MySQL 데이터베이스로 실행하면 값은 데이터베이스 연결 지점까지 추적됨
- 이 경우 이전에 값을 만들기 위해 전송된 SQL 쿼리와 MySQL JDBC 드라이버가 데이터베이스와 통신하는 세부 사항을 볼 수 있음
- FlowTracker PetClinic mysql demo는 데이터베이스 SSL 연결을 통해 전송된 복호화된 내용을 FlowTracker가 가로챈다는 점도 보여줌
- Spring PetClinic은 예시일 뿐이며, FlowTracker는 특정 프레임워크나 라이브러리에 의존하지 않음
- javac demo는 Java 컴파일러를 관찰해 생성된 class 파일 형식과 그 안의 바이트코드를 이해하는 데 FlowTracker가 어떻게 도움을 주는지 보여줌
사용 방법과 주의사항
- 현재 FlowTracker는 프로덕션 준비 상태라기보다 proof-of-concept에 가까움
- 여러 예제 프로그램에서 잘 동작했지만, 모든 프로그램에서 잘 동작한다고 보장되지 않음
- 많은 오버헤드를 추가하므로 프로그램 실행이 훨씬 느려짐
- 사용 절차:
- Github releases pages에서
flowtracker-*.jaragent jar를 다운로드함 - Java 명령줄에
-javaagent:path/to/flowtracker.jar를 추가함 - FlowTracker를 방해하는 일부 JVM 최적화를 끄기 위해
java -jar flowtracker.jar jvmopts출력도 명령줄에 추가함 - 기본적으로 FlowTracker는 8011 포트에서 웹서버를 시작하므로 브라우저에서
http://localhost:8011/을 열면 됨
- Github releases pages에서
- 더 자세한 설정 옵션은
USAGE.md에 있음
내부 동작: 바이트코드 계측과 Tracker 모델
- FlowTracker는 JVM이 클래스를 로드할 때 class 파일, 즉 바이트코드에 코드를 주입하는 계측 agent임
- 주입된 코드는 프로그램이 데이터를 읽고 전달하고 쓰는 동안 메모리 안 데이터와 그 출처의 매핑을 유지함
- 추적 대상의 초점은
String,char,byte[]같은 텍스트·바이너리 데이터이며, 숫자·구조화 데이터·계산된 데이터가 중심은 아님 - 사용되는 방식:
- 일부 JDK 메서드 호출을 FlowTracker 버전의 메서드 호출로 대체함
- 입력과 출력을 추적하기 위해 JDK의 핵심 위치에 코드를 주입함
- 메서드 내부의 로컬 변수와 스택 값을 추적하기 위해 데이터 흐름 분석과 더 깊은 계측을 수행함
- 메서드 호출 전후와 호출된 메서드의 시작·끝에 코드를 추가해
ThreadLocal로 인자와 반환값을 추적함
-
Tracker 데이터 모델
Tracker: 추적 대상 객체의 내용과 출처 정보를 보관함content:InputStream이나OutputStream을 지나간 모든 바이트 같은 데이터source: 내용의 특정 범위를 다른 tracker의 특정 범위와 연결함TrackerRepository: 관심 있는 객체와 해당Tracker를 연결하는 큰 전역Map<Object, Tracker>를 보관함TrackerPoint: tracker 안의 한 위치를 가리키며, 하나의byte출처 같은 단일 primitive 값을 표현함
기본 계측: JDK 훅과 ASM
- FlowTracker는 특정 JDK 메서드가 호출될 때 hook 메서드 호출을 삽입해
Tracker를 최신 상태로 유지함 - 가장 단순한 예는
System.arraycopy임java.lang.System.arraycopy호출을com.coekie.flowtracker.hook.SystemHook.arraycopy호출로 대체함SystemHook은 실제arraycopy를 호출한 뒤,TrackerRepository에서 원본·대상 배열의 tracker를 가져와 대상 tracker가 원본을 가리키도록 갱신함
- 이런 계측에는 ASM 바이트코드 조작 라이브러리를 사용함
- 대부분의 hook은 호출자 쪽이 아니라 JDK 메서드 내부의 호출받는 쪽에 추가됨
- 예를 들어
FileInputStream.read(byte[])끝에FileInputStreamHook.afterReadByteArray호출을 추가함 - 이 계측은 ASM의
AdviceAdapter를 사용하는 annotation 기반 자체 마이크로 프레임워크로 구현됨
- 예를 들어
- FlowTracker는
java.io.FileInputStream,java.io.FileOutputStream,sun.nio.ch.FileChannelImpl,sun.nio.ch.IOUtil,sun.nio.ch.NioSocketImpl같은 JDK의 I/O 관련 클래스들에 hook을 추가함 - 관련 구현:
primitive 값 추적과 메서드 내부 데이터 흐름 분석
byte같은 primitive 값은 객체처럼 identity가 없으므로TrackerRepository의Map키로 안전하게 추적할 수 없음- FlowTracker는 primitive 값의 출처를 메서드 내부의 로컬 변수에 별도로 저장하도록 코드를 다시 씀
- 예를 들어
byte b = x[1]뒤에는ArrayHook.getElementTracker(x, 1)로b의 tracker를 얻고,y[2] = b때ArrayHook.setElementTracker(y, 2, bTracker)로 대상 배열에 출처를 기록하는 식임 - 이를 위해 FlowTracker는 ASM의 분석 기능 위에서 상징 해석(symbolic interpretation) 을 수행함
- 메서드의 각 지점에서 로컬 변수와 스택의 값이 어디서 왔고 어디로 가는지 모델링함
- 관련 구현:
- FlowValue와 ArrayLoadValue
- MergedValue: if 문이나 loop 같은 제어 흐름 때문에 값이 여러 위치에서 올 수 있는 상황을 처리함
- FlowInterpreter: ASM
Interpreter확장으로 바이트코드 명령을 해석하고 적절한FlowValue를 만듦 - Store와 ArrayStore
- FlowTransformer: 전체 분석과 계측 과정을 구동함
- 모든 primitive 값을 추적하지는 않으며, 초점은
byte와char이고int와long은 그보다 제한적으로 다룸
메서드 호출을 넘는 데이터 흐름
- 메서드 내부 분석만으로는 primitive 값이 다른 메서드의 인자와 반환값으로 흐르는 상황을 처리할 수 없음
- FlowTracker는
Invocation에 인자와 반환값의PointTracker를 저장하고, 메서드 호출 직전에 이를ThreadLocal에 넣음 - 호출된 메서드 시작 지점에서는
Invocation.start(...)로ThreadLocal의 정보를 꺼내 primitive 인자의 출처를 사용할 수 있음 - 이 방식으로
out.write(b)처럼 primitive 값을 메서드로 넘기는 경우에도write(byte value)내부에서value의 tracker를 이어받을 수 있음 - 관련 구현:
코드 자체를 데이터 출처로 다루기
- FlowTracker가 추적하는 주요 출처는 I/O와 코드 자체에서 온 값임
- 코드에서 온 값에는
'a',"abc"같은 primitive 및String상수가 포함됨 - 이런 상수에는 클래스마다
ClassOriginTracker를 만들고, 클래스와 상수 참조를 텍스트로 표현한 내용을 보관함 - 상수가 참조될 때 해당 값의 tracker는 이 텍스트 표현 안의 위치를 가리키게 됨
- 이 모델은 상수가 코드의 텍스트 표현에서 읽힌 것처럼 다루므로, I/O 추적 모델과 유사해짐
- 성능 때문에
constantPoint메서드가 메서드 실행마다 호출되지 않도록 ConstantDynamic (JEP 309)을 사용함 - 관련 구현:
String literal 처리와 제약
- String literal은 새
String복사본을 만들고,String.value안의byte[]를ClassOriginTracker와 연결함 String s = "abc";같은 문장은String s = StringHook.constantString("abc", 1234, 81);형태로 다시 쓰임- 이 방식은 JVM이 일반적으로 제공하는 String interning 보장을 깨뜨림
- 원래 같은 String 상수의 모든 출현은 같은 인스턴스를 참조해야 함
- instrumentation 후에는 이 보장에 의존하는 코드가 깨질 수 있음
- FlowTracker는 이 문제를 줄이기 위해 몇 가지 장치를 둠
- ConstantDynamic을 사용해 같은 줄의 같은 String literal이 여러 번 실행돼도 매번 같은 인스턴스를 반환함
- 일부
stringA == stringB표현식을Objects.equals(stringA, stringB)로 다시 써서 특정 관점에서는 같은 인스턴스처럼 보이게 함 java.lang.*같은 일부 패키지에서는 String literal 추적을 비활성화함- 이 동작은
USAGE.md의breakStringInterning으로 설정 가능함
- 관련 구현:
추적되지 않는 값의 fallback
- FlowTracker는 프로그램의 모든 값을 추적하지 않음
- 이유는 성능 우려, 아직 구현되지 않은 부분, 관련성이 낮은 값, 여러 출처의 조합으로 생기는 값을 표현하려면 더 복잡한 데이터 모델이 필요하다는 점임
- 추적되지 않던 값이 추적을 시작해야 하는 위치에 도달하면, 상수와 비슷하게
ClassOriginTracker에 연결하고 그 위치를"<?>"로 표현함 - 예를 들어 배열 길이는 추적되지 않으므로
write(array.length)가 호출되면,Invocation에는write호출 지점의 코드 위치를 가리키는PointTracker가 전달됨 - 결과적으로 바이너리 형식의 출력에서 원래 출처를 보지 못하더라도, 주변의 추적된 문자열과 코드 위치를 통해 값의 의미를 빠르게 해석할 수 있는 경우가 있음
더 다룰 수 있는 구현 주제
- MergedValue는 분기와 루프를 지나는 값의 추적을 다루며, 데이터 흐름 분석에서 가장 어려운 부분으로 꼽힘
- String concatenation은 indification (JEP 280)을 통해
StringConcatFactory가 반환하는MethodHandle에 hook을 추가해 처리함 - 소스 코드 찾기, Vineflower로 디컴파일하기, 바이트코드와 소스 라인 연결도 구현에 포함됨
- ClassLoader 구성은 bootclasspath 의존성과 애플리케이션 충돌을 피하고, shading과 nested jar 없이 빠른 개발 사이클을 유지하는 데 초점이 있음
- 필드에 저장된 primitive 값 추적도 구현에 포함됨
- 프런트엔드는 Jetty와 JAX-RS 기반 웹서버, Svelte 기반 웹 UI로 구성됨