[Spring] Bean 생명주기와 메서드 실행 시점 제어

슈퍼대디·2024년 12월 23일

CS면접대비

목록 보기
8/13

Spring의 메서드 실행 시점 제어하기

목차

  1. 스프링 컨테이너와 빈 생명주기
  2. 초기화/소멸 메서드
  3. AOP를 이용한 실행 시점 제어
  4. 실행 순서 제어하기
  5. 면접 예상 질문

1. 스프링 컨테이너와 빈 생명주기

스프링 컨테이너 시작과 종료

1. 스프링 컨테이너 생성
2. 스프링 빈 생성
3. 의존관계 주입
4. 초기화 콜백
5. 사용
6. 소멸 콜백
7. 스프링 종료

빈 생명주기 콜백의 종류

  1. 인터페이스(InitializingBean, DisposableBean)
  2. 설정 정보에 초기화/소멸 메서드 지정
  3. @PostConstruct, @PreDestroy 애노테이션

2. 초기화/소멸 메서드

@PostConstruct

스프링 컨테이너가 빈의 의존관계 주입을 마친 후 실행됩니다.

@Component
public class DatabaseInitializer {
    
    private final DataSource dataSource;
    
    public DatabaseInitializer(DataSource dataSource) {
        this.dataSource = dataSource;
        // 이 시점에는 아직 초기화가 완료되지 않음
    }
    
    @PostConstruct
    public void init() {
        // 모든 의존성 주입이 완료된 후 실행
        try (Connection conn = dataSource.getConnection()) {
            // 데이터베이스 초기화 작업 수행
        }
    }
}

@PreDestroy

스프링 컨테이너가 빈을 제거하기 전에 실행됩니다.

@Component
public class ResourceCleaner {
    
    private final ExecutorService executorService;
    
    public ResourceCleaner() {
        this.executorService = Executors.newFixedThreadPool(10);
    }
    
    @PreDestroy
    public void cleanup() {
        // 컨테이너가 종료되기 전에 리소스 정리
        executorService.shutdown();
        try {
            if (!executorService.awaitTermination(1, TimeUnit.SECONDS)) {
                executorService.shutdownNow();
            }
        } catch (InterruptedException e) {
            executorService.shutdownNow();
        }
    }
}

InitializingBean, DisposableBean 인터페이스

@Component
public class NetworkClient implements InitializingBean, DisposableBean {
    
    @Override
    public void afterPropertiesSet() throws Exception {
        // 초기화 작업
        connect();
        call("초기화 연결 메시지");
    }
    
    @Override
    public void destroy() throws Exception {
        // 소멸 전 작업
        disconnect();
    }
}

3. AOP를 이용한 실행 시점 제어

Pointcut 표현식

Pointcut은 어드바이스가 적용될 조인 포인트를 선별하는 표현식입니다.

  1. 기본 표현식 문법
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {}
  1. 주요 Pointcut 지시자
@Aspect
@Component
public class SystemArchitecture {
    // execution: 메서드 실행 조인 포인트 매칭
    @Pointcut("execution(public * *(..))")
    public void anyPublicMethod() {}
    
    // within: 특정 타입 내의 모든 메서드 매칭
    @Pointcut("within(com.example.service.*)")
    public void inService() {}
    
    // @annotation: 특정 애노테이션이 붙은 메서드 매칭
    @Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
    public void transactionalMethod() {}
    
    // args: 특정 타입의 파라미터를 가진 메서드 매칭
    @Pointcut("args(String, ..)")
    public void stringArgumentMethod() {}
    
    // bean: 특정 이름의 빈 매칭
    @Pointcut("bean(*Service)")
    public void beanPointcut() {}
}
  1. 복합 Pointcut 표현식
@Aspect
@Component
public class ComplexPointcutExample {
    // AND 조건
    @Pointcut("execution(* com.example.service.*.*(..)) && @annotation(Transactional)")
    public void transactionalServiceMethod() {}
    
    // OR 조건
    @Pointcut("within(com.example.service.*) || within(com.example.repository.*)")
    public void businessLayer() {}
    
    // NOT 조건
    @Pointcut("execution(* com.example.*.*(..)) && !within(com.example.aspect.*)")
    public void notInAspect() {}
}

실제 Pointcut 활용 예시

@Aspect
@Component
public class PerformanceAspect {
    
    // 모든 Service 클래스의 public 메서드
    @Pointcut("execution(public * com.example.service.*Service.*(..))")
    private void serviceOperation() {}
    
    // 모든 Repository 메서드
    @Pointcut("within(com.example.repository.*Repository)")
    private void repositoryOperation() {}
    
    // 위의 두 Pointcut을 조합
    @Around("serviceOperation() || repositoryOperation()")
    public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        try {
            return joinPoint.proceed();
        } finally {
            long endTime = System.currentTimeMillis();
            String className = joinPoint.getTarget().getClass().getSimpleName();
            String methodName = joinPoint.getSignature().getName();
            System.out.println(className + "." + methodName + ": " + (endTime - startTime) + "ms");
        }
    }
}

세밀한 Pointcut 제어

@Aspect
@Component
public class DetailedPointcutExample {
    
    // 특정 애노테이션이 있는 메서드만 대상
    @Pointcut("@annotation(com.example.annotation.Auditable)")
    public void auditableMethod() {}
    
    // 특정 파라미터 타입을 가진 메서드
    @Pointcut("args(org.springframework.security.core.Authentication, ..)")
    public void authenticatedMethod() {}
    
    // 리턴 타입이 특정 타입인 메서드
    @Pointcut("execution(org.springframework.http.ResponseEntity *(..))")
    public void returnsResponseEntity() {}
    
    // 조합된 Pointcut 사용
    @Around("auditableMethod() && authenticatedMethod()")
    public Object auditAuthenticatedOperations(ProceedingJoinPoint joinPoint) throws Throwable {
        // 보안 감사 로직
        return joinPoint.proceed();
    }
}

@Before, @After, @AfterReturning, @AfterThrowing

메서드 실행의 특정 시점에 로직을 추가할 수 있습니다.

@Aspect
@Component
public class LoggingAspect {
    
    @Before("execution(* com.example.service.*.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        // 메서드 실행 전 로직
    }
    
    @After("execution(* com.example.service.*.*(..))")
    public void logAfter(JoinPoint joinPoint) {
        // 메서드 실행 후 로직
    }
    
    @AfterReturning(
        pointcut = "execution(* com.example.service.*.*(..))",
        returning = "result"
    )
    public void logAfterReturning(JoinPoint joinPoint, Object result) {
        // 메서드 정상 반환 후 로직
    }
    
    @AfterThrowing(
        pointcut = "execution(* com.example.service.*.*(..))",
        throwing = "ex"
    )
    public void logAfterThrowing(JoinPoint joinPoint, Exception ex) {
        // 메서드 예외 발생 시 로직
    }
}

4. 실행 순서 제어하기

@Order와 @DependsOn

빈과 AOP의 실행 순서를 제어할 수 있습니다.

@Configuration
public class AppConfig {
    
    @Bean
    @DependsOn("dataSource")  // dataSource 빈이 먼저 생성되도록 보장
    public UserService userService() {
        return new UserService();
    }
}

@Aspect
@Component
@Order(1)  // 낮은 숫자가 먼저 실행
public class SecurityAspect {
    // 보안 관련 로직
}

@Aspect
@Component
@Order(2)
public class LoggingAspect {
    // 로깅 관련 로직
}

실행 순서 예시

  1. 스프링 컨테이너 시작
  2. 빈 객체 생성 (@DependsOn 고려)
  3. 의존관계 주입
  4. 빈 초기화 콜백 (@PostConstruct)
  5. AOP 프록시 생성
  6. 실제 빈 사용
  7. 빈 소멸 전 콜백 (@PreDestroy)
  8. 스프링 컨테이너 종료

5. 면접 예상 질문

Q: Pointcut 표현식의 주요 지시자들과 그 용도를 설명해주세요.
A: Pointcut 표현식의 주요 지시자들은 다음과 같습니다:
1. execution: 가장 많이 사용되며, 메서드의 실행을 매칭합니다. 반환타입, 패키지, 클래스, 메서드, 파라미터 등을 세세하게 지정할 수 있습니다.
2. within: 특정 타입 내의 모든 메서드를 매칭합니다. 주로 특정 패키지나 클래스 내의 모든 메서드를 지정할 때 사용합니다.
3. @annotation: 특정 애노테이션이 붙은 메서드를 매칭합니다. 커스텀 애노테이션을 만들어 사용할 때 유용합니다.
4. bean: 특정 이름의 빈을 매칭합니다. 스프링 빈의 이름으로 메서드를 선택할 때 사용합니다.
이러한 지시자들은 &&, ||, ! 등의 연산자를 통해 조합하여 사용할 수 있습니다.
A: @PostConstruct와 InitializingBean은 모두 빈의 초기화 시점에 로직을 수행하기 위한 방법이지만, 몇 가지 주요 차이가 있습니다. @PostConstruct는 자바 표준 애노테이션으로, 메서드 이름을 자유롭게 지정할 수 있고 코드를 깔끔하게 유지할 수 있습니다. 반면 InitializingBean은 스프링 전용 인터페이스로, 인터페이스 구현이 필요하고 메서드 이름이 고정됩니다. 일반적으로 @PostConstruct 사용이 권장되며, InitializingBean은 레거시 코드에서 주로 볼 수 있습니다.

Q: AOP의 각 어드바이스(@Before, @After, @Around 등)의 실행 순서는 어떻게 되나요?
A: AOP 어드바이스의 실행 순서는 다음과 같습니다:
1. @Around의 시작 부분
2. @Before
3. 실제 메서드 실행
4. @AfterReturning 또는 @AfterThrowing
5. @After
6. @Around의 종료 부분

여러 개의 AOP가 적용될 경우 @Order 애노테이션으로 순서를 제어할 수 있습니다. 또한, 같은 종류의 어드바이스라면 더 구체적인 포인트컷이 우선 순위를 가집니다.

Q: 스프링의 빈 생명주기 관리가 중요한 이유는 무엇인가요?
A: 스프링의 빈 생명주기 관리는 다음과 같은 이유로 중요합니다:
1. 리소스 관리: 데이터베이스 연결, 파일 핸들러 등의 리소스를 적절한 시점에 초기화하고 정리할 수 있습니다.
2. 의존성 보장: 의존관계 주입이 완료된 후에 초기화 로직을 실행할 수 있어 안전합니다.
3. 애플리케이션 컨텍스트 관리: 애플리케이션 시작과 종료 시의 필요한 작업들을 체계적으로 관리할 수 있습니다.

참고 자료

profile
성장하고싶은 Backend 개발자

0개의 댓글