빈 생명주기와 확장 포인트
Spring 컨테이너는 빈을 생성하고 관리하는 과정에서 여러 단계의 생명주기를 거칩니다. 각 단계마다 개발자가 커스터마이징할 수 있는 인터페이스와 애노테이션이 제공됩니다.
생명주기 단계별 흐름
- 인스턴스 생성: 생성자나 정적 팩토리 메서드를 통해 객체 생성
- 의존성 주입: 프로퍼티 설정 및 의존 객체 주입
- 의존성 인식 콜백:
BeanNameAware,BeanFactoryAware,ApplicationContextAware순으로 콜백 실행 - 초기화 전 후처리:
BeanPostProcessor의postProcessBeforeInitialization실행 - 초기화 콜백:
InitializingBean.afterPropertiesSet()또는@PostConstruct메서드 실행 - 커스텀 초기화:
@Bean(initMethod)로 지정한 메서드 실행 - 초기화 후 후처리:
BeanPostProcessor의postProcessAfterInitialization실행 - 소멸 콜백:
DisposableBean.destroy()또는@PreDestroy메서드 실행 - 커스텀 소멸:
@Bean(destroyMethod)로 지정한 메서드 실행
실제 활용 예시
@Component
public class CustomComponent implements InitializingBean, DisposableBean {
@PostConstruct
public void setup() {
System.out.println("초기화 애노테이션 방식");
}
@Override
public void afterPropertiesSet() {
System.out.println("인터페이스 방식 초기화");
}
@PreDestroy
public void cleanup() {
System.out.println("소멸 애노테이션 방식");
}
@Override
public void destroy() {
System.out.println("인터페이스 방식 소멸");
}
}순환 의존성 해결: 3단계 캐시 아키텍처
Spring은 생성 중인 빈을 임시 보관하는 3개의 캐시를 활용하여 순환 의존성 문제를 해결합니다.
캐시 구조
| 캐시명 | 용도 | 키-값 타입 |
|---|---|---|
singletonObjects | 완전히 초기화된 싱글톤 빈 | String → Object |
earlySingletonObjects | 프록시 적용된 미완성 빈 | String → Object |
singletonFactories | 빈 생성 팩토리 (ObjectFactory) | String → ObjectFactory<?> |
순환 의존 해결 메커니즘
A → B → A 형태의 순환 의존이 있을 때:
- A 생성 시작,
singletonFactories에 A의 ObjectFactory 등록 - A의 의존성 B 주입 시도
- B 생성 시작,
singletonFactories에 B의 ObjectFactory 등록 - B의 의존성 A 주입 시도
singletonFactories에서 A의 팩토리 발견 → 프록시/원본 객체 조기 반환- B 완성,
singletonObjects에 등록 - A에 B 주입 완료, A 초기화 완료
핵심 소스 코드 분석
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 1단계: 완성된 빈 확인
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// 2단계: 미완성 프록시 빈 확인
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
// 3단계: 팩토리에서 조기 생성
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
return singletonObject;
}AOP 구현과 프록시 생성
@Aspect 기반 선언적 방식
@Aspect
@Component
public class PerformanceMonitor {
@Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
public void transactionalOperation() {}
@Around("transactionalOperation()")
public Object measureExecutionTime(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
try {
return pjp.proceed();
} finally {
long elapsed = System.nanoTime() - start;
System.out.printf("%s 실행 시간: %dms%n",
pjp.getSignature(), elapsed / 1_000_000);
}
}
}프록시 생성 전략
Spring AOP는 타겟 클래스의 특성에 따라 두 가지 프록시 방식을 선택합니다:
- JDK 동적 프록시: 인터페이스 기반 -
java.lang.reflect.Proxy활용 - CGLIB 프록시: 클래스 기반 - 타겟 클래스를 상속하여 서브클래스 생성
// CGLIB 기반 프록시 예시
public class Service$$SpringCGLIB$$0 extends DemoService {
private MethodInterceptor interceptor;
@Override
public void execute() {
return (MethodInterceptor) interceptor.intercept(
this,
executeMethod,
CGLIB$emptyArgs,
CGLIB$execute$Proxy
);
}
}트랜잭션 관리 아키텍처
선언적 트랜잭션 설정
@Service
public class OrderProcessor {
@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.READ_COMMITTED,
timeout = 30,
rollbackFor = {SQLException.class, BusinessException.class},
noRollbackFor = {NoRollbackException.class}
)
public void processOrder(Long orderId) {
// 트랜잭션 로직
}
}전파 속성 상세
| 전파 유형 | 동작 방식 | 사용 시나리오 |
|---|---|---|
| REQUIRED | 기존 트랜잭션 참여 또는 신규 생성 | 기본값, 대부분의 비즈니스 로직 |
| REQUIRES_NEW | 항상 새 트랜잭션, 기존 트랜잭션 일시 중지 | 독립적 커밋이 필요한 로직 |
| NESTED | 중첩 트랜잭션 생성 (세이브포인트) | 부분 롤백이 필요한 경우 |
| MANDATORY | 반드시 기존 트랜잭션 필요 | 트랜잭션 컨텍스트 강제 검증 |
DataSourceTransactionManager 동작 흐름
doBegin(): Connection 획득 및 auto-commit 비활성화- TransactionSynchronizationManager에 Connection 바인딩
- 비즈니스 로직 실행
- 예외 발생 시
doRollback(), 정상 시doCommit() doCleanupAfterCompletion(): Connection 반납 및 동기화 정리
고급 애노테이션 활용
환경별 빈 구성
@Configuration
public class DataSourceConfig {
@Bean
@Profile("local")
public DataSource embeddedDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.build();
}
@Bean
@Profile("production")
public DataSource pooledDataSource(
@Value("${db.url}") String url,
@Value("${db.user}") String username,
@Value("${db.pass}") String password) {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(url);
config.setUsername(username);
config.setPassword(password);
return new HikariDataSource(config);
}
}지연 초기화와 의존성 제어
@Configuration
public class LazyConfiguration {
@Bean
@Lazy // 첫 사용 시점에 초기화
public HeavyResource heavyResource() {
return new HeavyResource();
}
@Bean
@DependsOn("databaseInitializer") // 명시적 생성 순서 지정
public CacheManager cacheManager() {
return new CacheManager();
}
}다중 빈 선택 전략
public interface NotificationSender {
void send(String message, String recipient);
}
@Component
@Primary // 기본 구현체 지정
public class EmailNotificationSender implements NotificationSender { }
@Component
@Qualifier("sms") // 한정자 지정
public class SmsNotificationSender implements NotificationSender { }
@Service
public class AlertService {
@Autowired
private NotificationSender defaultSender; // EmailNotificationSender 주입
@Autowired
@Qualifier("sms")
private NotificationSender smsSender; // 명시적 선택
}예외 처리 중앙화
@RestControllerAdvice(basePackages = "com.example.api")
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationErrors(
MethodArgumentNotValidException ex) {
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.collect(Collectors.toList());
return ResponseEntity.badRequest()
.body(new ErrorResponse("VALIDATION_FAILED", errors));
}
@ExceptionHandler(DataAccessException.class)
public ResponseEntity<ErrorResponse> handleDatabaseErrors(
DataAccessException ex) {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(new ErrorResponse("DATABASE_ERROR", "일시적 오류 발생"));
}
}