Spring MVC

박용민·2025년 12월 4일

Spring MVC

목록 보기
1/1

Bean 생명주기

  1. Bean 생성 (Constructor)
  2. 의존성 주입 (Setter, @Autowired 등)
  3. 초기화 단계
    • InitializingBean.afterPropertiesSet()
    • @PostConstruct 메서드
    • init-method (XML)
  4. 사용 (Service 로직 수행)
  5. 소멸 단계
    • DisposableBean.destroy()
    • @PreDestroy 메서드
    • destroy-method (XML)
인터페이스역할호출 시점
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-methoddestroy-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");
    }

}

@Qualifier

어떤 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;
    }
}

@Primary

같은 타입의 Bean이 여러 개 있을 때, 기본적으로 주입될 Bean을 우선 지정하는 방식입니다.

의존성 주입 방식 종류

방식예시특징
1️⃣ 필드 주입@Autowired private MemberService service;간단하지만 테스트/불변성 문제
2️⃣ 세터 주입@Autowired public void setService(...)선택적 의존성 주입 가능
3️⃣ 생성자 주입public MemberController(MemberService service)권장 방식 ✅

생성자 주입을 사용해야 하는 이유

불변성(Immutable) 확보

  • 생성자 주입은 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

공통 기능을 끼워넣을 수 있는 지점 스프링 AOP에서는 오직 “메소드 실행”만 Join Point로 취급

==“스프링 빈 메소드 실행 시점”만 Join Point==

@Service
public class OrderService {

    public void order() { ... }

    public void cancel() { ... }

    private void internal() { ... } // private 이라 스프링 AOP 대상 X (사실상 Join Point로 취급 안 함)

Pointcut

많은 Join Point 중에서, Advice를 적용할 곳을 고르는 필터 조건

// com.example.service 패키지 이하 모든 메소드
@Pointcut("execution(* com.example.service..*(..))")
public void serviceLayer() {}

Advice

실제로 끼워 넣을 공통 기능 코드

  • 로깅, 트랜잭션, 권한 체크, 캐싱등
  • 실무에서는 @Around 하나만 잘 써도 거의 다 해결 가능

Advice 종류

  • @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");
        }
    }
}

Aspect

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 {
        // 트랜잭션 시작
        // 실제 메소드 실행
        // 예외 시 롤백 / 정상 시 커밋
    }
}

Weaving

원래 코드 + Aspect를 실제 실행 가능한 객체로 합치는 과정
스프링 AOP는 3번, 런타임 위빙(프록시 기반)

  1. 컴파일 타임 위빙
    • .java → .class로 컴파일할 때 솔직히 바이트코드 자체를 바꿔버리는 방식
  2. 로드 타임 위빙
    • 클래스를 JVM에 로드하는 순간(클래스 로더 단계) 바이트코드를 조작
  3. 런타임 위빙
    • 프로그램 실행 중에 프록시를 만들어서 메소드 호출을 가로채는 방식

Weaving 과정

  1. @Aspect 클래스들을 스캔
  2. 각 Aspect의 Pointcut을 분석
  3. 그 Pointcut에 매칭되는 스프링 빈들을 찾음
  4. 그 빈들에 대해 프록시 객체를 생성
  5. 컨테이너에는 실제 빈 대신 프록시를 등록

Spring에서 쓰는 대표적인 내장 Aspect

  • @Transactional → 트랜잭션 Aspect
  • @Cacheable, @CacheEvict → 캐싱 Aspect
  • Spring Security의 메소드 보안 → 권한 체크 Aspect

스프링 AOP의 구현 방식(Proxy)

public 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
    }
}

Pointcut 표현식(AspectJ 표현식)

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(패키지.어노테이션)"

JDK Proxy vs CGLIB

  • 대상 빈이 인터페이스를 구현하고 있으면JDK 동적 프록시
  • 인터페이스가 없으면 → CGLIB

JDK Proxy

인터페이스 기반 프록시, 인터페이스를 구현한 가짜 객체를 만들어서 메소드 호출을 가로챔(반드시 인터페이스가 있어야 함)

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()`을 대신 호출
  • Proxy MemberService 인터페이스만 구현하는 가짜 객체
  • 진짜 구현체(MemberServiceImpl)를 안에 들고 있다가, 호출을 대신 전달
  • JDK 표준 라이브러리 → 별도 의존성 X, 인터페이스를 구현하지 않으면 사용 불가 프록시 타입이 인터페이스 타입으로만 보임
    ``

CGLIB(Code Generator Library)

클래스를 상속해서, 바이트코드를 조작해 만든 프록시(자식 클래스)를 만들어주는 라이브러리
원본 클래스를 상속하는 새 클래스를 런타임에 동적으로 만들어서 그 안에서 메소드들을 오버라이드 → 그 오버라이드된 메소드에 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();
  • 인터페이스가 없어도 됨 → 그냥 클래스만 있어도 AOP 적용 가능
  • 상속 기반이라 제약 존재:final 클래스 → 상속 불가 → 프록시 생성 불가
  • 바이트코드를 조작해서 클래스를 동적으로 만들기 때문에, 예전에는 성능/메모리 이슈가 살짝 있었음 (요즘은 거의 차이 미미)
  • 인터페이스 없으면 스프링이 알아서 CGLIB 씀, proxy-target-class=true 설정 시에는 무조건 CGLIB, final 클래스/메소드에는 AOP 안 먹는다

Self-Invocation 문제

프록시가 낀 메서드를 호출해야 트랜잭션이 동작하는데 자기 자신은 프록시를 우회해서 실제 객체를 직접 호출해버림

@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 vs CGLIB Proxy 비교 표

구분JDK Dynamic ProxyCGLIB 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클래스명EnhancerByCGLIBEnhancerByCGLIBXXXX
AOP 적용 범위인터페이스의 public 메서드만타깃 클래스의 public/protected 메서드
호환성Java 표준이라 안정적외부 라이브러리(CGLIB, ASM) 의존
생성 비용상대적으로 저렴클래스 바이트코드 조작이므로 비용 더 큼
Self-invocation 문제동일하게 발생 (프록시 기반이므로)동일하게 발생
강점간단/안정적/표준인터페이스 없어도 가능, 매우 유연
약점인터페이스 강제final 제한, 외부 라이브러리 의존

@EnableAspectJAutoProxy

스프링 AOP를 활성화하고 프록시 생성 방식(JDK Proxy vs CGLIB)을 제어하는 핵심 설정 어노테이션

@Aspect 기반 AOP 활성화

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {}

0개의 댓글