Spring 동작 원리 — 면접 대비 정리
IoC/DI, Bean 생명주기, Scope, AOP 프록시, @Transactional 동작 원리까지. Spring 내부를 이해해야 답할 수 있는 면접 질문을 정리한다.
IoC / DI
IoC (Inversion of Control): 객체 생성과 의존성 연결의 제어권을 개발자가 아닌 프레임워크(Spring)가 갖는다.
DI (Dependency Injection): IoC를 구현하는 방법. 객체가 필요한 의존성을 직접 생성하지 않고, 외부에서 주입받는다.
// DI 없이 (강한 결합)
class OrderService {
private PaymentService payment = new KakaoPayService(); // 직접 생성
}
// DI 적용 (느슨한 결합)
class OrderService {
private final PaymentService payment;
public OrderService(PaymentService payment) { // 외부에서 주입
this.payment = payment;
}
}
DI가 없으면 OrderService를 테스트할 때 실제 KakaoPayService가 필요하다. DI를 쓰면 Mock으로 교체할 수 있다.
DI 방법 3가지
// 1. 생성자 주입 (권장)
@Service
public class OrderService {
private final PaymentService paymentService;
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
// 2. Setter 주입
@Service
public class OrderService {
private PaymentService paymentService;
@Autowired
public void setPaymentService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
// 3. 필드 주입 (지양)
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
}
생성자 주입을 권장하는 이유:
final선언 가능 → 불변 보장- 순환 참조를 컴파일 타임에 감지 가능
- 테스트 시 명시적으로 의존성 전달 가능
ApplicationContext
Spring의 IoC 컨테이너. Bean을 생성하고 관리한다.
BeanFactory (기본 기능)
└── ApplicationContext (BeanFactory + 부가 기능)
├── 국제화 (MessageSource)
├── 이벤트 발행 (ApplicationEventPublisher)
├── 환경변수 (Environment)
└── 리소스 로딩 (ResourceLoader)
실무에서는 항상 ApplicationContext를 쓴다.
Bean 생명주기
컨테이너 시작
↓
클래스 스캔 & Bean 정의 등록
↓
Bean 인스턴스 생성 (생성자 호출)
↓
의존성 주입 (@Autowired, 생성자 주입 등)
↓
초기화 콜백 (@PostConstruct, InitializingBean.afterPropertiesSet())
↓
[사용]
↓
소멸 콜백 (@PreDestroy, DisposableBean.destroy())
↓
컨테이너 종료
@Component
public class DatabaseConnectionPool {
@PostConstruct
public void init() {
// 초기화: DB 커넥션 풀 세팅
System.out.println("커넥션 풀 초기화");
}
@PreDestroy
public void cleanup() {
// 소멸: 커넥션 정리
System.out.println("커넥션 정리");
}
}
Bean Scope
| Scope | 설명 | 기본값 |
|---|---|---|
| singleton | 컨테이너당 인스턴스 1개 | ✅ |
| prototype | 요청마다 새 인스턴스 | |
| request | HTTP 요청당 1개 | 웹 환경 |
| session | HTTP 세션당 1개 | 웹 환경 |
싱글톤 주의사항: 싱글톤 Bean에 상태(인스턴스 변수)를 가지면 멀티스레드 환경에서 데이터 레이스가 발생한다.
// 위험: 싱글톤인데 상태를 가짐
@Service
public class OrderService {
private int count = 0; // 모든 요청이 공유!
public void process() {
count++; // 스레드 안전하지 않음
}
}
AOP (Aspect-Oriented Programming)
횡단 관심사(로깅, 트랜잭션, 보안 등)를 핵심 비즈니스 로직에서 분리한다.
Spring AOP는 프록시 패턴으로 동작한다.
클라이언트 → [Proxy] → 실제 Bean
↓
Before Advice 실행
↓
실제 메서드 실행
↓
After Advice 실행
@Aspect
@Component
public class LoggingAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object logExecutionTime(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object result = pjp.proceed(); // 실제 메서드 실행
long duration = System.currentTimeMillis() - start;
log.info("{} took {}ms", pjp.getSignature(), duration);
return result;
}
}
프록시 생성 방식
JDK Dynamic Proxy: 인터페이스 기반. 대상 클래스가 인터페이스를 구현해야 한다.
CGLIB: 클래스 상속 기반. 인터페이스 없어도 된다. Spring Boot 기본값.
AOP 동작 안 하는 경우
같은 클래스 내 자기 호출
@Service
public class OrderService {
public void placeOrder() {
this.processPayment(); // 프록시를 거치지 않음 → @Transactional 무시
}
@Transactional
public void processPayment() { ... }
}
placeOrder()가 processPayment()를 직접 호출하면 프록시를 거치지 않는다. 트랜잭션이 적용되지 않는다.
해결: 별도 클래스로 분리하거나, self 주입(ApplicationContext에서 자신을 꺼내 호출).
@Transactional 동작 원리
@Transactional은 AOP 프록시로 구현된다.
클라이언트 → [TransactionProxy]
↓
트랜잭션 시작 (Connection.setAutoCommit(false))
↓
실제 메서드 실행
↓
성공 → commit()
예외 → rollback()
전파(Propagation)
| 옵션 | 동작 |
|---|---|
| REQUIRED (기본) | 기존 트랜잭션 있으면 참여, 없으면 새로 시작 |
| REQUIRES_NEW | 항상 새 트랜잭션 시작. 기존 트랜잭션은 일시 중단 |
| NESTED | 중첩 트랜잭션. 부모 트랜잭션 안에서 세이브포인트 |
| SUPPORTS | 트랜잭션 있으면 참여, 없으면 없이 실행 |
| NOT_SUPPORTED | 트랜잭션 없이 실행. 기존 트랜잭션은 중단 |
| NEVER | 트랜잭션 없이 실행. 트랜잭션 있으면 예외 |
@Service
public class OrderService {
@Transactional
public void placeOrder() {
orderRepository.save(order); // 같은 트랜잭션
paymentService.charge(amount); // REQUIRED → 같은 트랜잭션 참여
notificationService.send(); // REQUIRES_NEW → 별도 트랜잭션
}
}
알림 발송이 실패해도 주문/결제는 롤백하고 싶지 않을 때 REQUIRES_NEW를 쓴다.
롤백 조건
기본적으로 RuntimeException과 Error만 롤백된다. Checked Exception은 롤백하지 않는다.
@Transactional(rollbackFor = Exception.class) // Checked Exception도 롤백
@Transactional(noRollbackFor = CustomException.class) // 특정 예외는 롤백 안 함
면접에서 자주 나오는 질문
Q. 생성자 주입을 권장하는 이유는?
불변성 보장(final), 순환 참조 컴파일 타임 감지, 테스트 시 명시적 의존성 주입. 필드 주입은 리플렉션으로 주입되어 테스트에서 직접 의존성을 넣기 어렵다.
Q. @Transactional이 같은 클래스 내 호출에서 동작하지 않는 이유는?
Spring AOP는 프록시 기반이다. 같은 클래스 안에서 this.method()를 호출하면 프록시를 거치지 않고 실제 객체를 직접 호출한다. 프록시가 개입하지 않으니 어드바이스(트랜잭션 처리)가 실행되지 않는다.
Q. REQUIRED와 REQUIRES_NEW의 차이는?
REQUIRED는 기존 트랜잭션이 있으면 참여한다. 내부 메서드에서 예외가 발생하면 외부 트랜잭션도 롤백된다. REQUIRES_NEW는 항상 새 트랜잭션을 시작하고 기존을 중단시킨다. 내부 트랜잭션 롤백이 외부에 영향을 주지 않는다.
Q. Spring Bean은 기본적으로 싱글톤인데 동시성 문제가 없는가?
상태(인스턴스 변수)가 없으면 문제없다. Service, Repository 같은 Bean은 보통 상태를 갖지 않는다. 상태가 필요하면 메서드의 지역변수를 쓰거나, prototype scope를 쓰거나, ThreadLocal을 활용한다.