프로브의 대상이 되는 소프트웨어 컴포넌트의 소스 코드를 직접 제어할 수 있다면, 인젝티드 프로브 대신 임베디드 프로브를 작성하는 것이 좋습니다.
인젝티드 프로브를 작성할 때 초기 작업의 대부분은 인터셉트할 메서드를 지정하고, 핸들러 메서드의 파라미터로 사용할 인젝티드 객체를 선택하는 데 사용됩니다. 임베디드 프로브의 경우, 모니터링할 메서드에서 임베디드 프로브 API를 직접 호출할 수 있으므로 이러한 과정이 필요하지 않습니다. 임베디드 프로브의 또 다른 장점은 배포가 자동으로 이루어진다는 점입니다. 프로브는 소프트웨어와 함께 배포되며, 애플리케이션이 프로파일될 때 JProfiler UI에 나타납니다. 배포해야 하는 유일한 의존성은 Apache 2.0 라이선스 하에 제공되는 작은 JAR 파일로, 주로 프로파일링 에이전트의 훅 역할을 하는 비어 있는 메서드 본문으로 구성되어 있습니다.
개발 환경
개발 환경은 인젝티드 프로브와 동일하지만, 아티팩트 이름이 jprofiler-probe-injected 대신
jprofiler-probe-embedded이고, 프로브를 별도의 프로젝트에서 개발하는 대신 JAR 파일을 애플리케이션과 함께 배포한다는 점이 다릅니다. 임베디드 프로브를
소프트웨어 컴포넌트에 추가할 때 필요한 프로브 API는 단일 JAR 아티팩트에 포함되어 있습니다. javadoc에서는 API를 탐색할 때
com.jprofiler.api.probe.embedded의 패키지 개요부터
시작하세요.
인젝티드 프로브와 마찬가지로 임베디드 프로브에도 두 가지 예제가 있습니다. api/samples/simple-embedded-probe에는 임베디드 프로브 작성을 시작할 수 있는
예제가 있습니다. 해당 디렉터리에서 ../gradlew를 실행하여 컴파일 및 실행할 수 있으며, gradle 빌드 파일 build.gradle을
참고하여 실행 환경을 이해할 수 있습니다. 제어 객체 등 더 많은 기능을 확인하려면 api/samples/advanced-embedded-probe의 예제를 참고하세요.
페이로드 프로브
인젝티드 프로브와 유사하게, 설정을 위해서는 프로브 클래스를 작성해야 합니다. 프로브 클래스는 페이로드를 수집하는지 또는 호출 트리를 분할하는지에 따라
com.jprofiler.api.probe.embedded.PayloadProbe 또는
com.jprofiler.api.probe.embedded.SplitProbe를 상속해야 합니다. 인젝티드 프로브 API에서는 핸들러 메서드에 페이로드 수집 및 분할을 위한
서로 다른 애노테이션을 사용하지만, 임베디드 프로브 API는 핸들러 메서드가 없으므로 이 설정을 프로브 클래스 자체에서 처리해야 합니다.
public class FooPayloadProbe extends PayloadProbe {
@Override
public String getName() {
return "Foo queries";
}
@Override
public String getDescription() {
return "Records foo queries";
}
}
인젝티드 프로브가 애노테이션을 통해 설정하는 반면, 임베디드 프로브는 프로브의 베이스 클래스로부터 메서드를 오버라이드하여 설정합니다. 페이로드 프로브의 경우, 유일한 추상 메서드는
getName()이며, 나머지 메서드는 필요에 따라 오버라이드할 수 있는 기본 구현을 제공합니다. 예를 들어, 오버헤드를 줄이기 위해 이벤트 뷰를 비활성화하고 싶다면
isEvents()를 오버라이드하여 false를 반환하면 됩니다.
페이로드를 수집하고 관련된 타이밍을 측정하려면 Payload.enter()와 Payload.exit() 호출을 쌍으로 사용합니다.
public void measuredCall(String query) {
Payload.enter(FooPayloadProbe.class);
try {
performWork();
} finally {
Payload.exit(query);
}
}
Payload.enter() 호출은 프로브 클래스를 인자로 받아 프로파일링 에이전트가 어떤 프로브가 호출 대상인지 알 수 있도록 합니다.
Payload.exit() 호출은 자동으로 동일한 프로브와 연결되며, 페이로드 문자열을 인자로 받습니다. exit 호출을 누락하면 호출 트리가 깨지므로, 항상 try 블록의
finally 절에서 호출해야 합니다.
측정 대상 코드 블록이 값을 반환하지 않는 경우, 페이로드 문자열과 Runnable을 인자로 받는 Payload.execute 메서드를 대신 사용할 수
있습니다. Java 8+에서는 람다나 메서드 레퍼런스를 사용해 이 메서드 호출을 매우 간결하게 만들 수 있습니다.
public void measuredCall(String query) {
Payload.execute(FooPayloadProbe.class, query, this::performWork);
}
이 경우에는 페이로드 문자열이 미리 결정되어 있어야 합니다. Callable을 인자로 받는 execute 버전도 있습니다.
public QueryResult measuredCall(String query) throws Exception {
return Payload.execute(PayloadProbe.class, query, () -> query.execute());
}
Callable을 받는 시그니처의 문제점은 Callable.call()이 체크드 Exception을 throw하기 때문에,
반드시 예외를 잡거나 포함하는 메서드에 선언해야 한다는 점입니다.
제어 객체
페이로드 프로브는 Payload 클래스의 적절한 메서드를 호출하여 제어 객체를 열고 닫을 수 있습니다. 제어 객체는 컨트롤 객체와 커스텀 이벤트 타입을 인자로 받는
Payload.enter() 또는 Payload.execute() 메서드 버전을 통해 프로브 이벤트와 연관됩니다.
public void measuredCall(String query, Connection connection) {
Payload.enter(FooPayloadProbe.class, connection, MyEventTypes.QUERY);
try {
performWork();
} finally {
Payload.exit(query);
}
}
제어 객체 뷰는 프로브 설정에서 명시적으로 활성화해야 하며, 커스텀 이벤트 타입도 프로브 클래스에 등록해야 합니다.
public class FooPayloadProbe extends PayloadProbe {
@Override
public String getName() {
return "Foo queries";
}
@Override
public String getDescription() {
return "Records foo queries";
}
@Override
public boolean isControlObjects() {
return true;
}
@Override
public Class<? extends Enum> getCustomTypes() {
return Connection.class;
}
}
제어 객체를 명시적으로 열고 닫지 않는 경우, 프로브 클래스에서 getControlObjectName을 오버라이드하여 모든 제어 객체의 표시 이름을 반환해야 합니다.
분할 프로브
분할 프로브의 베이스 클래스에는 추상 메서드가 없습니다. 호출 트리를 분할만 하고 별도의 프로브 뷰를 추가하지 않을 수도 있기 때문입니다. 이 경우, 최소한의 프로브 정의는 다음과 같습니다.
public class FooSplitProbe extends SplitProbe {}
분할 프로브에서 중요한 설정 중 하나는 재진입 가능 여부입니다. 기본적으로 최상위 호출만 분할됩니다. 재귀 호출도 분할하고 싶다면 isReentrant()를 오버라이드하여
true를 반환하면 됩니다. 또한, 분할 프로브는 isPayloads()를 오버라이드하여 true를 반환하면 프로브 뷰를 생성하고
분할 문자열을 페이로드로 게시할 수 있습니다.
분할을 수행하려면 Split.enter()와 Split.exit()를 쌍으로 호출하세요.
public void splitMethod(String parameter) {
Split.enter(FooSplitProbe.class, parameter);
try {
performWork(parameter);
} finally {
Split.exit();
}
}
페이로드 수집과 달리, 분할 문자열은 프로브 클래스와 함께 Split.enter()에 전달해야 합니다. 마찬가지로 Split.exit()도 반드시 호출되어야
하므로, try 블록의 finally 절에서 호출하는 것이 중요합니다. Split는 Runnable 및 Callable
인자를 받는 execute() 메서드도 제공하여 한 번의 호출로 분할을 수행할 수 있습니다.
텔레메트리
임베디드 프로브의 경우, 동일한 클래스패스에 있기 때문에 애플리케이션의 모든 static 메서드에 직접 접근할 수 있어 텔레메트리를 게시하기가 특히 편리합니다. 인젝티드 프로브와 마찬가지로, 프로브 설정
클래스의 static public 메서드에 @Telemetry 애노테이션을 추가하고 숫자 값을 반환하면 됩니다. 자세한 내용은 프로브 개념
챕터를 참고하세요. 임베디드와 인젝티드 프로브 API의 @Telemetry 애노테이션은 동일하며, 단지 패키지만 다릅니다.
임베디드와 인젝티드 프로브 API 모두에서 제공되는 또 다른 기능은 ThreadState 클래스를 사용하여 스레드 상태를 수정하는 기능입니다. 이 클래스 역시 두 API에 각각
다른 패키지로 존재합니다.
배포
JProfiler UI로 프로파일링할 때 임베디드 프로브를 활성화하기 위해 별도의 작업은 필요하지 않습니다. 다만, 프로브는 Payload 또는
Split에 대한 첫 호출이 발생할 때만 등록됩니다. 이 시점에 JProfiler에서 해당 프로브 뷰가 생성됩니다. 내장 및 인젝티드 프로브처럼 프로브 뷰가 처음부터 보이길
원한다면, 다음과 같이 호출할 수 있습니다.
PayloadProbe.register(FooPayloadProbe.class);
페이로드 프로브의 경우 위와 같이,
SplitProbe.register(FooSplitProbe.class);
분할 프로브의 경우 위와 같이 등록할 수 있습니다.
Payload와 Split의 메서드를 조건부로 호출할지, 즉 오버헤드를 최소화하기 위해 커맨드라인 스위치 등으로 제어할지 고민할 수 있습니다. 하지만
일반적으로 이는 필요하지 않습니다. 메서드 본문이 비어 있기 때문에 프로파일링 에이전트가 attach되지 않은 경우 페이로드 문자열을 생성하는 것 외에는 오버헤드가 발생하지 않습니다. 프로브 이벤트는
마이크로스코픽한 단위로 생성되어서는 안 되므로, 상대적으로 드물게 생성되며, 페이로드 문자열을 만드는 작업은 비교적 미미한 작업입니다.
컨테이너 환경에서는 클래스패스에 외부 의존성을 노출하고 싶지 않을 수 있습니다. 컨테이너의 사용자가 임베디드 프로브 API를 사용할 경우 충돌이 발생할 수 있습니다. 이럴 때는 임베디드 프로브 API를 자체 패키지로 shade할 수 있습니다. JProfiler는 shade된 패키지도 인식하여 API 클래스를 올바르게 계측합니다. 빌드 타임에 shading이 어렵다면, 소스 아카이브를 추출하여 클래스를 프로젝트에 포함시킬 수도 있습니다.