컴파일 시점의 위빙

Always·2025년 3월 21일

Backend&Devops

목록 보기
11/15

🧱 OOP (Object-Oriented Programming)

자바와 스프링은 기본적으로 객체 지향 프로그래밍(OOP)을 기반으로 하여,
각 객체에 책임을 부여하고, 그 책임에 맞는 로직을 클래스 내에서 구현합니다.

✅ SOLID 원칙을 잘 활용한 OOP의 장점

  • 객체는 실생활 개념과 매칭되기 때문에 직관적으로 설계 가능
  • 응집도는 높이고, 결합도는 낮춰서 유지보수에 유리
  • 클래스마다 책임이 분리되어 중복이 줄어듦
  • 변경에 유연: 하나의 기능을 수정해도, 다른 기능에 영향이 적음
    📌 즉, 유지보수성과 확장성이 뛰어난 코드 구조를 만들 수 있다.

🛠 실생활 비유

작업자들이 각각 일1, 일2, 일3을 나누어 수행한다고 생각해보자.
여기서: 일은 하나의 클래스 각 일에 대한 처리는 객체의 책임으로 볼 수 있다.

그런데 이런 상황을 생각해보자!

모든 작업자가 일을 시작하기 전의 준비 과정과
일이 끝난 후의 정리 과정이 모두 동일하다고 해보자.

이러한 공통 로직을 각 작업자(클래스)들이 전부 가지고 있다면?

  • 중복 코드가 생긴다
  • 핵심 작업 외에 부가적인 일까지 하게 된다
  • 코드 유지보수가 어려워진다

🌀 AOP (Aspect-Oriented Programming)

이런 상황이라면, 공통 작업(준비, 정리)을 하나의 기계가 대신 처리하고
작업자는 자신의 일에만 집중할 수 있다면 훨씬 효율적이지 않을까?

이것이 바로 AOP의 핵심 아이디어다!

✨ AOP란?

AOP는 클래스 안에 존재하던 공통적인 부가 로직을 분리하여,
하나의 로직으로 따로 관리함으로써:

기존 클래스는 핵심 관심사(Core Concern)에만 집중하고 부가 관심사(Cross-Cutting Concern)는 AOP가 관리

AOP의 장점

  • 공통 로직 분리로 중복 제거
  • 핵심 기능에 집중 가능
  • 유지보수와 확장성 향상

🔧 AOP의 구현 방식

AOP는 보통 두 가지 방식으로 구현됩니다.

1. AspectJ (바이트코드 조작 방식)

  • 컴파일 타임에 바이트코드를 조작
  • 복잡도 면에서 부담이 있으나, 속도면에서 이득임.

2. Spring AOP (프록시 기반 방식)

  • 대부분의 스프링 컴포넌트는 이 방식을 사용
  • 메서드 호출 시 프록시 객체가 가로채서 부가기능 처리
  • 구현이 간단하고 실행 성능 부담이 적음
  • Runtime weaving이 여기에 존재한다.

각 방식에 대해서 보다 자세히 알아보자.

Aspectj방식의 바이트 조작 방식

앞에서 말했다 싶이 컴파일 타임에 JVM이 바이트 코드 조작을 통해서 실제 로직에 적용하는 방식이다.

Spring Aop와 달리 프록시 방식이 아니다

프록시 패턴이 아니므로, 일을 위임하지 않으므로, Spring Aop에 비해서 빠르다는 장점을 가지고 있다.

Aspectj에서 Weaving은 부가 로직이 핵심 로직에 적용되는 시점에 따라서 크게 3개로 나뉜다.

Weaving은 Pointcut에 의해서 결정된 타겟의 Join Point에 부가기능(Advice)를 삽입하는 과정을 뜻한다

  • Compile-Time 위빙: 이름 그대로 컴파일 시점에 위빙을 실행하는 것이다.
    이 때는 AspectJ 전용 컴파일러를 이용해서 Aop가 적용된 코드를 하나의 바이트 코드(.class)형태로 생성한다.
    즉 .class->.java로 컴파일 되는 시점에 위빙이 되는 것이다.

  • Post-Compile위빙:이미 compile된 파일을 수정해서 적용시키는 것이다. 외부 라이브러리를 Weaving 할 때 사용한다

외부 라이브러리의 경우 이미 .class혹은 .jar형태로 주는 경우가 있다. 따라서 Post-comile방식을 이용한다.

  • Load-time 위빙: .class파일이 실제 JVM에 올라갈 때, ClassLoader가 바이트 코드를 조작하는 위빙 방식이다.
    객체를 로드할 때 위빙이 일어나는 거라 앱 성능의 하락이 우려되므로, 잘 권장되지 않는다.

위빙을 적용하려고,

    // AspectJ 컴파일러
    aspectjtools 'org.aspectj:aspectjtools:1.9.20'
    aspectjweaver 'org.aspectj:aspectjweaver:1.9.20'
}

compileJava.dependsOn aspectjCompile

를 build.gradle에 넣었는데


이처럼 Spring Aop가 이미 적용되어있는 로직이 많기 때문에(@RequiredArgsConstructor, @Slf4j, @TimeTrace등)이 있기 때문에, build오류가 났다.

💭 위빙 AOP를 적용해보며 느낀 점

AspectJ의 Compile-time Weaving 방식 AOP를 적용해보는 과정에서,
기존에 사용하던 많은 코드들이 Spring AOP 기반이라는 것을 체감했다.

예를 들면:

@Slf4j → Lombok 기반 로깅
@RequiredArgsConstructor → Lombok 기반 생성자 주입
@TimeTrace → Spring AOP 기반 커스텀 애노테이션

이러한 기능들은 대부분 Spring의 프록시 기반 AOP와 Lombok Annotation Processor에 의존하고 있었다.

하지만 AspectJ의 ajc는 기본적으로 이러한 annotation processor를 지원하지 않기 때문에,
컴파일 단계에서 builder 메서드가 없거나, final 필드가 초기화되지 않았다는 오류가 발생했다.

🤔 여기서 얻은 인사이트

  • AspectJ(위빙 기반 AOP)는 런타임 오버헤드가 없고, 성능 면에서는 더 우수하다.
  • 하지만 Spring 프로젝트에서는 이미 많은 기능이 Spring AOP와 Lombok에 의존하고 있어서,
    적용 난이도가 높고, 호환성 문제가 생기기 쉽다라는 생각이 든다

Aspectj를 spring 환경에서 적용하기

Spring Aop는 일반적으로 Annotation기반인것 과 달리, Aspectj를 이용한 aop는 Annotaion을 이용하지 않는다.
또한 Aspectj를 이용하기 위해서는 아래와 같이 설정을 해줘야한다.
build.gradle파일

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.4.0'
	id 'io.spring.dependency-management' version '1.1.6'
	id 'io.freefair.aspectj.post-compile-weaving' version '8.4' 
    // ✅ post-comile-weaving 
}
....
dependency{
...
	implementation 'org.aspectj:aspectjrt:1.9.21'
...
}

}

id 'io.freefair.aspectj.post-compile-weaving' version '8.4' 를 이용하면
기존에 javac를 이용해서 컴파일 하던것을 ajc(aspectj compiler)를 이용해서 컴파일 하게된다.
ajc=javac+ (@Aspect붙은 Advice들을 실제 Joinpoint와 합쳐서 새로운 클래스 생성)의 역할을 한다.

@Aspect
@Component
public class LogAspect {

    //Advice: before
    @Before("execution(* restaurant.restaurant.aop..*(..))")
    public void logBefore(JoinPoint point){
        System.out.println("Before method: " + point.getSignature().toShortString());

    }
}

이 애스펙트는 restaurant.restaurant.aop 패키지 이하의 모든 클래스의 메서드가 실행되기 직전에, 해당 메서드의 이름을 로그로 출력하는 Before Advice이다. @Aspect와 @Component 어노테이션을 통해 스프링 빈으로 등록되며, AspectJ에 의해 위빙된다.

@Service
public class LogService {

    public void log(){
        System.out.println("Log Now!!");
    }
}

결과는 아래와 같다.


각 메서드들이 잘 실행되었음을 알 수 있다.

JDK Proxy VS GCLIB Proxy

🔍 JDK 동적 프록시 vs CGLIB

구분JDK 동적 프록시CGLIB
기반인터페이스 기반클래스 기반 (상속)
대상인터페이스가 있는 클래스만 가능인터페이스 없어도 OK
기술java.lang.reflect.Proxy바이트코드 생성 (ASM 기반)
성능조금 더 빠름 (단순 구조)약간 무거움 (바이트코드 조작)
Spring에서 사용 조건기본적으로 이게 우선인터페이스 없을 경우 자동 fallback
클래스 구조프록시 방식@Transactional 적용 여부
인터페이스 구현 OJDK Proxy (기본)O (인터페이스 메서드만)
인터페이스 구현 XCGLIB ProxyO (public 메서드만)
profile
🐶개발 블로그

0개의 댓글