Server/Java

PCW(Post Compile Weaving) 적용법 및 테스트

suee97 2025. 8. 15. 20:06

서론

메서드 실행 전 후로 로그를 찍는 기능을 개발하고 있다.

모든 메서드에 대해서 로그 찍는 코드를 직접 작성하는 것은 매우 비효율적이므로 Aspect를 도입했다.

처음에는 Spring AOP 방식을 적용하려고 했지만, Dynamic, CGLIB Proxy 방식 모두 메서드가 실행될 때 새로운 객체를 생성한다는 점에서 메모리 사용량과 성능 측면에서 다른 방식의 필요성을 느꼈다.

그래서 컴파일 이후 바이트 코드를 추가하는 방식의 AOP 적용을 결정했다.

그 중 PCW를 결정한 이유는 CTW는 소스코드 단계에서 위빙이 적용되기 때문에 Lombok과 충돌 가능성이 매우 높으며, LTW는 설정이 매우 복잡하기 때문이다.

 

세팅 과정

1. LogAspect.java

@Aspect
public class LogAspect {

    private static final Logger log = LoggerFactory.getLogger(LogAspect.class);

    @Around("@annotation(com.ok200.AIIntheview.global.util.logging.Log)")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[AspectJ] Before: {}", joinPoint.getSignature());
        Object result = joinPoint.proceed();
        log.info("[AspectJ] After: {}", joinPoint.getSignature());
        return result;
    }

}

 

2. Log.java

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
}

 

3. LogTestClass.java

public class LogTestClass {

    @Log
    public static void testWithAnnotation() {
    }
    
}

 

4. resources/META-INF/aop.xml

<!DOCTYPE aspectj PUBLIC
        "-//AspectJ//DTD//EN"
        "https://www.eclipse.org/aspectj/dtd/aspectj.dtd">

<aspectj>
    <weaver options="-verbose">
        <include within="{aspect 적용 경로}"/>
        <exclude within="{aspect 적용x 경로}"/>
    </weaver>

    <aspects>
        <aspect name="com.ok200.AIIntheview.global.util.logging.LogAspect"/>
    </aspects>
</aspectj>

 

5. build.gradle

plugins {
	id 'java'
	... (생략)
	id "io.freefair.aspectj.post-compile-weaving" version "8.1.0"
}

... (생략)

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(17)
	}
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'

	// AspectJ (PCW)
	aspect 'org.aspectj:aspectjrt:1.9.21'
	aspect 'org.aspectj:aspectjweaver:1.9.21'

	// 빌드 시 ajc 실행을 위해 tools는 compileOnly로만
	compileOnly 'org.aspectj:aspectjtools:1.9.21'

	... (생략)
}

tasks.named('test') {
	useJUnitPlatform()
}

 

위빙 확인

weaving이 정상적으로 되었다면, LogTestClass.class는 이렇게 보여야 한다.

@Component
public class LogTestClass {
    public LogTestClass() {
    }

    @Log
    public void testWithAnnotation() {
        JoinPoint var1 = Factory.makeJP(ajc$tjp_0, this, this);
        testWithAnnotation_aroundBody1$advice(this, var1, LogAspect.aspectOf(), (ProceedingJoinPoint)var1);
    }

    static {
        ajc$preClinit();
    }
}

 

참고로 PCW가 아닌 Spring AOP 방식을 적용하면

@Component
public class LogTestClass {
    public LogTestClass() {
    }

    @Log
    public void testWithAnnotation() {
    }
}

 

그냥 이렇게 보인다. 이유는 Spring AOP는 프록시 객체를 따로 만들기 때문

 

테스트 코드 작성

@SpringBootTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class LogTest {

    @Autowired
    private LogTestClass logTestClass;

    private long totalTime = 0;

    @DisplayName("PCW")
    @RepeatedTest(100000)
    public void test_1() {
        long start = System.currentTimeMillis();
        logTestClass.testWithAnnotation();
        long end = System.currentTimeMillis();
        totalTime += (end - start);
    }

    @AfterAll
    public void printAvg() {
        System.out.println("10000회 총 실행 시간: " + totalTime + "ms");
    }
}

 

결과 : 154ms

Spring AOP의 경우 253ms가 소요

 

결론

- PCW 방식이 Spring AOP보다 약 40% 빨랐음

- 추후 JMH로 정밀한 측정이 필요함

 

느낀점, 배운점

- AOP 구현 방식이 정말 많다는 것을 배웠다. (Spring AOP - Dynamic Proxy, CGLIB / AspectJ - CTW, PCW, LTW)

- 빠른 소프트웨어를 만들기 위해서 이러한 방식의 노력이 큰 의미가 될 수도 있다고 느꼈다.