| 인터페이스 | 역할 | 호출 시점 |
|---|---|---|
InitializingBean | 빈이 생성되고 의존성 주입이 완료된 후 초기화 작업 수행 | afterPropertiesSet() 호출 |
DisposableBean | 컨테이너가 종료될 때(빈이 소멸될 때) 정리 작업 수행 | destroy() 호출 |
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
@Component
public class ExampleService implements InitializingBean, DisposableBean {
@Override
public void afterPropertiesSet() throws Exception {
// 모든 의존성 주입이 완료된 후 실행
System.out.println("✅ ExampleService 초기화 완료!");
initConnection();
}
private void initConnection() {
// DB나 외부 API 연결 초기화 로직
}
@Override
public void destroy() throws Exception {
// 애플리케이션 종료 시 실행
System.out.println("🧹 ExampleService 종료, 리소스 정리 중...");
closeConnection();
}
private void closeConnection() {
// DB 연결 종료, 파일 닫기 등
}
}
Spring 2.5 이후부터는 InitializingBean / DisposableBean 대신
JSR-250 표준 어노테이션(@PostConstruct, @PreDestroy) 사용을 권장합니다.
| 방식 | 초기화 | 소멸 | 비고 |
|---|---|---|---|
| 인터페이스 | afterPropertiesSet() | destroy() | Spring 전용 |
| 어노테이션 | @PostConstruct | @PreDestroy | 표준, 더 많이 사용됨 |
| XML 설정 | init-method | destroy-method | 구버전 XML 설정 시 |
@Component
public class ExampleService {
@PostConstruct
public void init() {
System.out.println("✅ @PostConstruct 초기화 로직");
}
@PreDestroy
public void cleanup() {
System.out.println("🧹 @PreDestroy 소멸 로직");
}
}
@Configuration
public class CacheConfig {
@Bean(initMethod = "init", destroyMethod = "destroy")
public CacheClient cacheClient() {
return new CacheClient("http://localhost:1234");
}
}
어떤 Bean을 사용할지 명시적으로 지정할 때 사용합니다. 즉, Bean 이름으로 구체적으로 선택하는 방식입니다.
@Service("dogService")
public class DogService implements AnimalService {}
@Service("catService")
public class CatService implements AnimalService {}
@Component
public class AnimalController {
private final AnimalService animalService;
@Autowired
public AnimalController(@Qualifier("dogService") AnimalService animalService) {
this.animalService = animalService;
}
}
같은 타입의 Bean이 여러 개 있을 때, 기본적으로 주입될 Bean을 우선 지정하는 방식입니다.
| 방식 | 예시 | 특징 |
|---|---|---|
| 1️⃣ 필드 주입 | @Autowired private MemberService service; | 간단하지만 테스트/불변성 문제 |
| 2️⃣ 세터 주입 | @Autowired public void setService(...) | 선택적 의존성 주입 가능 |
| 3️⃣ 생성자 주입 | public MemberController(MemberService service) | 권장 방식 ✅ |
final 필드를 사용할 수 있습니다.private final MemberService memberService; // 불변성 보장
// 누락 시 컴파일 또는 빈 주입 시점에서 오류 발생
new MemberController(null); // 🚫
“공통 기능(로그, 트랜잭션, 보안 등)을 메소드 호출 앞뒤에 자동으로 끼워 넣는 프록시 시스템”
public void order() {
long start = System.currentTimeMillis(); // 로그/측정
try {
log.info("order() 시작");
// ==== 비즈니스 로직 ====
// 주문 생성, 결제, 재고 차감...
// =======================
log.info("order() 성공");
} catch (Exception e) {
log.error("order() 실패", e); // 로그
throw e;
} finally {
long end = System.currentTimeMillis();
log.info("order() 실행 시간 = " + (end - start));
}
}
공통 기능을 끼워넣을 수 있는 지점 스프링 AOP에서는 오직 “메소드 실행”만 Join Point로 취급
==“스프링 빈 메소드 실행 시점”만 Join Point==
@Service
public class OrderService {
public void order() { ... }
public void cancel() { ... }
private void internal() { ... } // private 이라 스프링 AOP 대상 X (사실상 Join Point로 취급 안 함)
많은 Join Point 중에서, Advice를 적용할 곳을 고르는 필터 조건
// com.example.service 패키지 이하 모든 메소드
@Pointcut("execution(* com.example.service..*(..))")
public void serviceLayer() {}
실제로 끼워 넣을 공통 기능 코드
@Around 하나만 잘 써도 거의 다 해결 가능@Before : 대상 메소드 실행 직전@AfterReturning : 메소드가 정상 리턴한 후@AfterThrowing : 메소드에서 예외가 던져졌을 때@After : 정상/예외 상관 없이 메소드 종료 직후@Around : 메소드 호출을 전체 감싸서 전/후/예외 모두 직접 제어@Aspect
@Component
public class LogAspect {
// Pointcut: service 패키지 이하 모든 메소드
@Pointcut("execution(* com.example.service..*(..))")
public void serviceMethods() {}
@Before("serviceMethods()")
public void before(JoinPoint joinPoint) {
System.out.println("[BEFORE] " + joinPoint.getSignature());
}
@AfterReturning(pointcut = "serviceMethods()", returning = "result")
public void afterReturn(JoinPoint joinPoint, Object result) {
System.out.println("[AFTER RETURN] " + joinPoint.getSignature()
+ " result=" + result);
}
@AfterThrowing(pointcut = "serviceMethods()", throwing = "ex")
public void afterThrow(JoinPoint joinPoint, Throwable ex) {
System.out.println("[AFTER THROW] " + joinPoint.getSignature()
+ " ex=" + ex.getMessage());
}
@After("serviceMethods()")
public void after(JoinPoint joinPoint) {
System.out.println("[AFTER] " + joinPoint.getSignature());
}
@Around("serviceMethods()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
try {
System.out.println("[AROUND BEFORE] " + pjp.getSignature());
Object result = pjp.proceed(); // 실제 메소드 호출
System.out.println("[AROUND AFTER] " + pjp.getSignature());
return result;
} finally {
long end = System.currentTimeMillis();
System.out.println("실행시간 = " + (end - start) + "ms");
}
}
}
Pointcut + Advice의 결합체
서비스 코드에서 공통 코드 없애고, Aspect에 몰아넣는 구조
@Aspect 클래스를 하나의 Aspect@Aspect
@Component
public class TransactionAspect {
@Pointcut("execution(* com.example.service..*(..))")
public void serviceLayer() {}
@Around("serviceLayer()")
public Object txAdvice(ProceedingJoinPoint pjp) throws Throwable {
// 트랜잭션 시작
// 실제 메소드 실행
// 예외 시 롤백 / 정상 시 커밋
}
}
원래 코드 + Aspect를 실제 실행 가능한 객체로 합치는 과정
스프링 AOP는 3번, 런타임 위빙(프록시 기반)
@Aspect 클래스들을 스캔@Transactional → 트랜잭션 Aspect@Cacheable, @CacheEvict → 캐싱 Aspectpublic Object invoke(Method method, Object[] args) {
// Before
log.info("메소드 시작: " + method.getName());
long start = System.currentTimeMillis();
try {
Object result = 실제빈.method(args);
// AfterReturning
log.info("메소드 정상 종료: " + method.getName());
return result;
} catch (Throwable t) {
// AfterThrowing
log.error("메소드 예외 발생: " + method.getName(), t);
throw t;
} finally {
long end = System.currentTimeMillis();
log.info("실행시간 = " + (end - start));
// After
}
}
execution(접근제어자? 반환타입 패키지.클래스.메소드(파라미터) 예외?)
// com.example.service 패키지의 모든 메소드
"execution(* com.example.service.*.*(..))"
// com.example.service 및 하위 패키지(..)의 모든 메소드
"execution(* com.example.service..*(..))"
// 특정 클래스의 모든 메소드
"execution(* com.example.service.OrderService.*(..))"
// 메소드 이름이 get으로 시작하는 모든 메소드
"execution(* com.example..get*(..))"
// 파라미터가 없다
"execution(* com.example..*( ))"
// 파라미터가 하나 (타입 상관 X)
"execution(* com.example..*(*))"
// 파라미터가 두 개 이상
"execution(* com.example..*(*, ..))"
// 해당 패키지/클래스 범위 내의 모든 메소드
"within(com.example.service..*)"
// 특정 어노테이션이 달린 메소드에만 적용
"@annotation(패키지.어노테이션)"
인터페이스 기반 프록시, 인터페이스를 구현한 가짜 객체를 만들어서 메소드 호출을 가로챔(반드시 인터페이스가 있어야 함)
MemberService target = new MemberServiceImpl();
MemberService proxy = (MemberService) Proxy.newProxyInstance(
MemberService.class.getClassLoader(),
new Class[]{MemberService.class},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(">>> before " + method.getName());
Object result = method.invoke(target, args);
System.out.println("<<< after " + method.getName());
return result;
}
}
);
→ `proxy.join()`을 호출하면
→ `InvocationHandler.invoke()`가 먼저 호출
→ 내부에서 `target.join()`을 대신 호출
클래스를 상속해서, 바이트코드를 조작해 만든 프록시(자식 클래스)를 만들어주는 라이브러리
원본 클래스를 상속하는 새 클래스를 런타임에 동적으로 만들어서 그 안에서 메소드들을 오버라이드 → 그 오버라이드된 메소드에 AOP 코드를 끼워 넣는 방식
왜? CGLIB가 등장했는가?
- 인터페이스 없어도 되게, 그냥 클래스를 상속해서 프록시를 만들자!
- JDK 동적 프록시는 반드시 인터페이스가 있어야 함, 프록시가 인터페이스만 구현하기 때문에 “구현 클래스 타입”으로 캐스팅이 불가능
- 인터페이스 없이 그냥 클래스만 만들고 쓰는 경우도 많음, 혹은 “구현 클래스 타입”으로 뭔가 해야 하는 경우도 있음
// 스프링 컨테이너에는 **원본 MemberService가 아니라**
// `MemberService$$EnhancerByCGLIB` 객체(프록시)가 **빈으로 등록**
public class MemberService$$EnhancerByCGLIB extends MemberService {
@Override
public void join() {
// Advice (AOP) 코드
// ex) 트랜잭션 시작, 로깅 등
super.join();
// 후처리
}
}
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MemberService.class); // 상속할 대상
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println(">>> before " + method.getName());
Object result = proxy.invokeSuper(obj, args); // super.메소드() 호출
System.out.println("<<< after " + method.getName());
return result;
}
});
MemberService proxy = (MemberService) enhancer.create();
final 클래스 → 상속 불가 → 프록시 생성 불가프록시가 낀 메서드를 호출해야 트랜잭션이 동작하는데 자기 자신은 프록시를 우회해서 실제 객체를 직접 호출해버림
@Service
public class OrderService {
@Transactional
public void outer() { // `outer()`는 프록시가 감싸고 있으므로 @Transactional 적용 O
System.out.println("outer");
inner(); // ← self invocation (프록시 우회)
}
@Transactional
public void inner() { // `outer()` 내부에서 `this.inner()` 호출 → 프록시를 거치지 않음 → @Transactional(inner) 적용 X
System.out.println("inner");
}
}
외부 호출: 프록시 적용 → 트랜잭션 동작
내부 호출: this로 직접 실행 → 프록시 미작동 → 트랜잭션 미작동
// 해결 방법
// 자기 자신 빈을 DI 받아 호출하기
@Autowired
private OrderService self; // 프록시가 주입됨
@Transactional
public void outer() {
self.inner(); // 프록시를 통해 호출 → 트랜잭션 정상 적용
}
| 구분 | JDK Dynamic Proxy | CGLIB Proxy |
|---|---|---|
| 프록시 생성 방식 | Java 표준 라이브러리 기반의 인터페이스 기반 프록시 | 바이트코드 조작(ASM)으로 클래스 상속 기반 프록시 |
| 프록시 대상 | 인터페이스가 반드시 필요 | 인터페이스 없어도 클래스만 있으면 가능 |
| 동작 방식 | Reflection + InvocationHandler | 클래스 상속 + MethodInterceptor |
| 성능 | 과거엔 느렸지만 Java 1.8 이후 최적화되어 거의 CGLIB와 비슷 | 일반적으로 JDK Proxy보다 약간 빠름 |
| final 메서드 프록시 가능 여부 | 가능 (인터페이스라 final의 영향 없음) | 불가능 — final 메서드/클래스는 상속 불가 |
| private 메서드 AOP 여부 | 원칙적으로 불가 | 불가 (스프링은 private 메서드 AOP 자체 미지원) |
| Spring 기본 전략 | 인터페이스가 있으면 JDK Proxy 사용 | 인터페이스가 없으면 자동으로 CGLIB 사용 |
| 프록시 클래스 이름 | com.sun.proxy.$ProxyXX | 클래스명XXXX |
| AOP 적용 범위 | 인터페이스의 public 메서드만 | 타깃 클래스의 public/protected 메서드 |
| 호환성 | Java 표준이라 안정적 | 외부 라이브러리(CGLIB, ASM) 의존 |
| 생성 비용 | 상대적으로 저렴 | 클래스 바이트코드 조작이므로 비용 더 큼 |
| Self-invocation 문제 | 동일하게 발생 (프록시 기반이므로) | 동일하게 발생 |
| 강점 | 간단/안정적/표준 | 인터페이스 없어도 가능, 매우 유연 |
| 약점 | 인터페이스 강제 | final 제한, 외부 라이브러리 의존 |
스프링 AOP를 활성화하고 프록시 생성 방식(JDK Proxy vs CGLIB)을 제어하는 핵심 설정 어노테이션
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {}