스프링은 기본적으로 제어의 역전(IoC: Inversion of Control)으로도 알려진 의존성 주입(DI: Dependency Injection), 관점 지향 프로그래밍(AOP: Aspect-oriented-Programming)을 지원한다.
스프링은 자바 외에 그루비 및 코틀린과 같은 JVM 언어도 지원한다.
프로그램의 흐름을 개발자가 아닌 프레임워크가 제어한다.
개발자는 필요한 로직만 구현하고, 나머지 흐름은 프레임워크가 관리.
스프링(Spring) 프레임워크가 객체 생성, 의존성 주입 등을 자동으로 처리.
개발자는 로직에 집중하고, 흐름 제어는 프레임워크에 맡겨 개발 효율성과 재사용성을 높일 수 있다.
객체 지향 프로그래밍(OOP)의 의존성 주입(DI)과 함께 사용되며, 현대 프레임워크에서 매우 일반적인 원칙이다.
Aspect-Oriented Programming
프로그램의 핵심 비즈니스 로직과는 별개로, 부가적인 기능(예: 로깅, 트랜잭션 관리, 보안)을 효율적으로 분리하여 모듈화하는 프로그래밍 방식이다.
AOP는 OOP와 함께 작동 하는 프로그래밍 패러다임이다.
OOP에서는 한 클래스에서 하나의 책임만 다루는 것이 좋은 관행이며, 이것을 단일 책임 원칙(SRP, Single Responsibility Principle)이라고 불린다.
AOP의 장점
중복 코드 제거
여러 곳에서 반복되는 부가 로직(예: 로그, 보안)을 한 곳에서 관리 가능.
핵심 로직과 부가 로직의 분리
코드 가독성과 유지보수성 향상.
유연성
특정 조건에 따라 부가 기능을 쉽게 추가/제거 가능.
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service..*(..))")
public void logBefore() {
System.out.println("메서드 실행 전 로그 출력");
}
}
Aspect
공통 관심사(부가기능, Advice)와 적용 대상(Pointcut)을 묶어 놓은 모듈(클래스)이다.
공통적인 기능(예: 로깅, 트랜잭션 처리 등)을 정의하고 이를 특정 지점에 적용한다.
@Aspect 어노테이션으로 정의한다.
@Component와 함께 사용하여 빈으로 등록한다.
@Before("execution(* com.example.service..*(..))")
public void logBefore() {
System.out.println("메서드 실행 전 로그 출력");
}
Advice
공통 관심사에서 실행될 실제 동작(부가기능) 코드이다.
특정 시점에서 실행될 구체적인 로직을 정의한다.
값
Before
대상 메서드 실행 전에 동작한다.
@After
대상 메서드 실행 후에 동작한다.
@AfterReturning
대상 메서드가 정상적으로 종료된 후 동작한다.
@AfterThrowing
대상 메서드에서 예외 발생 시 동작한다.
@Around
대상 메서드 실행 전후에 동작한다.
@Pointcut("execution(* com.example.service..*(..))")
public void serviceMethods() {}
Pointcut
어드바이스가 적용될 조인 포인트(Join Point)를 지정하는 표현식이다.
어드바이스를 실행할 메서드나 클래스 등 특정 위치를 정의한다.
표현식을 사용하여 클래스, 메서드, 패키지 등을 선택한다.
@Pointcut 어노테이션을 사용하여 지정한다.
Joint Point
애플리케이션 실행 중, 어드바이스가 적용될 수 있는 모든 실행 지점을 의미한다.
메서드 호출, 생성자 호출, 예외 발생 등 실행 가능한 지점을 지정한다.
public class MyService {
public void performTask() {
System.out.println("실제 비즈니스 로직");
}
}
Target
실제로 어드바이스가 적용되는 대상 객체를 의미한다.
Joint Point에서 실행되는 구체적인 클래스나 메서드를 의미한다.
위에서 MyService 클래스와 그 안의 performTask 메서드가 Target이다.
weaving
Aspect를 Target의 Join Point에 적용하는 과정을 의미한다.
런타임, 컴파일타임, 또는 클래스 로드 시점에 Aspect와 Target을 결합하는 과정이다.
Weaving 방식
런타임 위빙(Runtime Weaving)
프록시 패턴을 사용해 동적으로 결합하는 방식이다.
프록시 기반
메서드 호출 시 프록시 객체가 실제 로직 전에 어드바이스를 실행하는 방식이다.
Aspect
공통 관심사와 적용 위치를 정의한다.
Advice
Aspect의 구체적인 실행 로직이다.
Pointcut
Advice를 적용할 Join Point를 선택한다.
Join Point
실행 가능한 모든 지점(메서드 호출 등)이다.
Target
Aspect와 Advice가 실제로 적용되는 객체를 의미한다.
Weaving
Aspect와 Target을 결합하는 과정을 의미한다.
출처: https://dotnettutorials.net/lesson/spring-framework-ioc-containers/
IoC 컨테이너의 역할
스프링 프레임워크의 핵심은 IoC(Inversion of Control) 컨테이너로, 객체(Bean)의 생성, 조립, 관리를 담당.
애플리케이션에서 필요한 여러 객체를 bean으로 등록하고, 이 객체 간의 의존성 주입(DI)을 관리함.
스프링 컨텍스트에서 IoC는 DI(Dependency Injection)라고도 불림.
BeanFactory와 ApplicationContext
IoC 컨테이너는 스프링 패키지의 BeanFactory와 ApplicationContext 인터페이스로 구현됨.
BeanFactory: IoC 컨테이너의 핵심 인터페이스로, 빈 생성과 의존성 주입을 처리.
ApplicationContext: BeanFactory의 확장 버전으로, 엔터프라이즈급 기능을 제공.
ApplicationContext의 주요 기능
통합 라이프 사이클 관리.
BeanPostProcessor와 BeanFactoryPostProcessor 자동 등록.
국제화 메시지 처리(MessageSource).
이벤트 발행(ApplicationEvent).
웹 애플리케이션을 위한 특화된 기능 제공.
설정 메타데이터(Configuration Metadata)
IoC 컨테이너는 설정 메타데이터를 기반으로 객체를 생성하고 의존성을 주입.
설정 방법:
XML 설정: 전통적인 방식.
어노테이션: 현대적인 방식으로 사용 빈도 높음.
자바 기반 설정: @Configuration 클래스 사용.
스프링 컨테이너의 동작 방식
Bean의 정의
Bean은 IoC 컨테이너가 관리하는 자바 객체로, 설정 메타데이터를 기반으로 생성 및 관리된다.
각 Bean은 고유 식별자(Unique Identifier)를 가지며, 별칭(alias)으로 여러 이름을 가질 수 있다.
Bean 생성 방식
Bean은 XML, 자바 코드, 어노테이션을 통해 정의할 수 있다.
예시 코드
@Configuration
public class AppConfig {
@Bean(initMethod = "init", destroyMethod = "destroy", name = {"sampleBean", "sb"})
public SampleBean sampleBean() {
return new SampleBean();
}
@Bean
public BeanInterface beanInterface() {
return new BeanInterfaceImpl();
}
}
Bean 이름과 별칭
Bean의 기본 이름은 클래스 이름의 첫 글자를 소문자로 바꾼 형태(SampleBean → sampleBean)이다.
name 속성을 사용하여 여러 별칭(예: sampleBean, sb)을 부여 가능하다.
Bean과 Component 어노테이션
@Bean은 @Component 어노테이션 내부에서 동작하며, @Configuration이 붙은 클래스의 메서드에서 Bean을 반환하도록 설정한다.
추가 애노테이션:
설명 애노테이션
@ComponentScan의 역할
@ComponentScan은 Bean 자동 스캔을 허용하는 스프링 애노테이션이다.
지정된 패키지 내에서 @Component, @Configuration, @Controller, @Service, @Repository 등의 애노테이션이 붙은 클래스를 자동으로 검색하고, 이를 Bean으로 등록한다.
기본 스캔 동작
스프링 부트는 기본적으로 @ComponentScan을 사용하여 애플리케이션의 기본 패키지와 하위 패키지를 스캔한다.
스캔 범위를 설정하려면 basePackages 또는 basePackageClasses 속성을 사용한다.
스캔 범위 지정
basePackages 속성: 특정 패키지를 문자열로 지정
ex. @ComponentScan(basePackages = "com.example.package")
basePackageClasses 속성: 클래스 기준으로 스캔 범위를 지정
ex. @ComponentScan(basePackageClasses = AppConfig.class)
다중 패키지 스캔
여러 패키지를 스캔하려면 @ComponentScans를 사용하여 스캔 대상 패키지를 나열.
@ComponentScans({
@ComponentScan(basePackages = "com.example.package1"),
@ComponentScan(basePackageClasses = AppConfig.class)
})
public class AppConfig {
// 코드
}
스프링 컨테이너는 Bean 인스턴스를 생성할 때, 범위(scope)에 따라 인스턴스를 관리한다. 기본 범위는 싱글톤(singleton)이며, 필요에 따라 다른 범위를 설정할 수 있다.
singleton
IoC 컨테이너당 하나의 인스턴스만 생성한다.
모든 요청에서 동일한 인스턴스를 사용한다.
ex.
```
@Bean
public SingletonBean singletonBean() {
return new SingletonBean();
}
```
Prototype
요청할 때마다 새로운 인스턴스를 생성한다.
상태를 공유하지 않는 객체에 적합하다.
ex.
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public PrototypeBean prototypeBean() {
return new PrototypeBean();
}
Request (웹 요청 범위)
HTTP 요청마다 새로운 인스턴스를 생성한다.
ex.
@Bean
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public RequestScopedBean requestScopedBean() {
return new RequestScopedBean();
}
또는
@RequestScope
public RequestScopedBean requestScopedBean() {
return new RequestScopedBean();
}
Session(세션 범위)
HTTP 세션마다 새로운 인스턴스를 생성한다.
ex.
@SessionScope
public SessionScopedBean sessionScopedBean() {
return new SessionScopedBean();
}
Application (서블릿 컨텍스트 범위)
애플리케이션당 하나의 인스턴스를 생성한다.
ex.
@ApplicationScope
public ApplicationScopedBean applicationScopedBean() {
return new ApplicationScopedBean();
}
WebSocket
WebSocket 세션마다 새로운 인스턴스를 생성한다.
ex.
@Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
public WebSocketScopedBean webSocketScopedBean() {
return new WebSocketScopedBean();
}
둘 이상의 설정 클래스가 있는 경우 설정을 모듈화 하는데 사용된다.
@Import 애노테이션은 설정된 클래스들에서 bean 정의를 가져와 컨텍스트(컨테이너)를 자동으로 인스턴스화할 때 사용된다.
스프링 부트는 자동 설정을 제공하므로 일반적으로 @Import를 사용할 필요가 없다.
수동으로 컨텍스트를 구성해야 하는 경우에는 @Import를 사용하여 설정을 모듈화할 수 있다.
ex.
@Configuration
public class FooConfig {
@Bean
public FooBean fooBean() {
return new FooBean();
}
}
@Configuration
@Import(FooConfig.class)
public class BarConfig {
@Bean
public BarBean barBean() {
return new BarBean();
}
}
public static void main(String[] args) {
ApplicationContext appContext = new AnnotationConfigApplicationContext(BarConfig.class);
// 컨테이너에서 FooBean과 BarBean 가져오기
FooBean fooBean = appContext.getBean(FooBean.class);
BarBean barBean = appContext.getBean(BarBean.class);
System.out.println("FooBean: " + fooBean);
System.out.println("BarBean: " + barBean);
}
new AnnotationConfigApplicationContext(BarConfig.class)
BarConfig를 기반으로 스프링 컨테이너 생성. @Import를 통해 FooConfig도 함께 로드되므로, FooBean과 BarBean 모두 컨테이너에 등록된다.
getBean(Class requiredType)
스프링 컨테이너에서 지정된 타입의 Bean을 가져온다.
컨테이너를 인스턴스화 하는 동안 BarConfig를 제공하여 위와 같이 스프링 컨테이너 안에서 FooBean과 BarBean 정의를 모두 가져올 수 있다.
스프링 컨테이너는 Bean 초기화 순서를 자동으로 관리하지만, 특정 Bean이 다른 Bean에 의존할 경우 명시적으로 초기화 순서를 정해야 할 때가 있다.
@DependsOn은 지정된 Bean이 먼저 초기화되도록 보장하여 의존성 문제를 방지한다.
만약 초기화 순서가 올바르지 않으면 NoSuchBeanDefinitionException 예외가 발생할 수 있다.
ex.
@Configuration
public class AppConfig {
@Bean
public FooBean fooBean() {
return new FooBean();
}
@Bean
public BarBean barBean() {
return new BarBean();
}
@Bean
@DependsOn({"fooBean", "barBean"})
public BazBean bazBean() {
return new BazBean();
}
}
fooBean()과 barBean() 메서드는 각각 FooBean과 BarBean 객체를 생성하고 컨테이너에 등록한다.
@DependsOn({"fooBean", "barBean"}): bazBean() 메서드가 실행되기 전에 fooBean과 barBean이 먼저 초기화되도록 지정.
의존성 주입(DI)은 클래스 간의 결합도를 낮춰 유지보수성과 확장성을 높인다.
스프링 컨테이너(ApplicationContext)는 Bean 설정 메타데이터를 기반으로 필요한 의존성을 자동으로 주입한다.
생성자 주입 방식을 통해 의존성 분리를 구현하면 코드의 유연성과 재사용성이 크게 향상된다.
DI 적용 전
public class CartService {
private CartRepository repository;
public CartService() {
this.repository = new CartRepositoryImpl();
}
}
CartService는 CartRepository에 의존하며, 생성자 내에서 CartRepositoryImpl 객체를 직접 생성한다.
해당 방식은 모듈가 결합도가 높아 테스트나 유지보수에 어려움을 줄 수 있다.
DI 적용 후
public class CartService {
private CartRepository repository;
public CartService(CartRepository repository) {
this.repository = repository;
}
}
의존성을 분리하기 위해 생성자 주입 방식을 사용한다.
CartRepository 구현체는 외부에서 주입되므로 유연성과 테스트 용이성이 증가한다.
cf. Bean은 다른 Bean에 의존성을 가질 수 있다. 예를 들어, CartService는 CartRepository라는 객체를 필요로 하고,
이런 의존성은 생성자, 설정자 메서드(setter), 또는 클래스의 프로퍼티를 통해 정의할 수 있다.
객체가 필요한 의존성(다른 객체)을 직접 생성하지 않고 생성자로 주입받는 것을 의미한다.
이를 통해 객체 간 결합도를 낮추고, 유연하고 테스트하기 쉬운 코드를 작성할 수 있다.
ex.
@Configuration
public class AppConfig {
@Bean
public CartRepository cartRepository() {
return new CartRepositoryImpl(); // CartRepository 구현체 생성 및 반환
}
@Bean
public CartService cartService() {
return new CartService(cartRepository()); // CartRepository를 생성자 주입
}
}
생성자를 사용하는 대신, 설정자 메서드(Setter Method)를 통해 객체의 의존성을 주입하는 방식이다.
객체 생성 후, setter 메서드를 호출하여 필요한 의존성을 설정한다.
선택적 의존성 주입이나 객체 생성 후 의존성을 설정해야 할 때 유용하다.
ex.
public class CartService {
private CartRepository repository; // CartRepository 의존성
// Setter 메서드로 의존성 주입
public void setCartRepository(CartRepository repository) {
this.repository = repository;
}
}
@Configuration
public class AppConfig {
@Bean
public CartRepository cartRepository() {
return new CartRepositoryImpl(); // CartRepository 구현체 반환
}
@Bean
public CartService cartService() {
CartService service = new CartService(); // CartService 인스턴스 생성
service.setCartRepository(cartRepository()); // Setter로 CartRepository 주입
return service;
}
}
@Service
public class CartService {
@Autowired
private CartRepository repository; // CartRepository 의존성 주입
}
@Service: 해당 클래스가 비즈니스 로직을 수행하는 Service Layer임을 나타낸다.
@Autowired: Spring 컨테이너에서 CartRepository 타입의 Bean을 찾아 자동으로 주입한다.
스프링 프레임워크는 bean에 대한 메타데이터를 설정하기 위한 많은 어노테이션을 제공한다.
ex. @Autowired, @Qualifier, @ Inject, @Resource, @Primary, @Value
@Autowired는 Spring Framework에서 의존성을 자동으로 주입하기 위해 사용하는 어노테이션이다.
개발자가 별도의 설정 클래스나 XML 파일을 작성하지 않아도, 필드, 생성자, 메서드에 Bean을 자동으로 주입할 수 있도록 도와준다.
매칭 모호성을 제거하기 위해 일치하는 bean들은 타입별 일치(type matching), 한정자 일치(qualifier matching), 또는 이름 일치(name matching)를 사용하여 찾아내고 주입된다.
사용 위치
필드
필드 주입은 테스트와 유지보수에 어려움이 있을 수 있으므로 생성자 주입을 권장한다.
생성자
메서드
ex.
@Component
public class CartService {
// 필드 주입
@Autowired
private CartRepository repository;
// 생성자 주입
@Autowired
public CartService(CartRepository cartRepository) {
this.repository = cartRepository;
}
// 설정자 주입
@Autowired
public void setARepository(ARepository aRepository) {
this.aRepository = aRepository;
}
// 임의 메서드 주입
@Autowired
public void xMethod(BRepository bRepository, CRepository cRepository) {
this.bRepository = bRepository;
this.cRepository = cRepository;
}
}
ex2.
// CartRepository 인터페이스
public interface CartRepository {
void save(String item);
}
// CartRepository 구현체
@Repository // Spring 컨테이너에 Bean으로 등록
public class CartRepositoryImpl implements CartRepository {
@Override
public void save(String item) {
System.out.println("Item saved: " + item);
}
}
// CartService 클래스
@Service // Spring 컨테이너에 Bean으로 등록
public class CartService {
@Autowired // CartRepository Bean 자동 주입
private CartRepository cartRepository;
public void addItem(String item) {
cartRepository.save(item);
System.out.println("Item added: " + item);
}
}
@Configuration
public class AppConfig {
@Bean
public CartService cartService1() {
return new CartServiceImpl1();
}
@Bean
public CartService cartService2() {
return new CartServiceImpl2();
}
}
@Controller
public class CartController {
@Autowired
private CartService service1; // NoUniqueBeanDefinitionException 발생
@Autowired
private CartService service2;
}
동일한 타입의 Bean이 여러 개 존재할 경우, Spring 컨테이너는 어떤 Bean을 주입해야 할지 결정하지 못한다. 이로 인해 NoUniqueBeanDefinitionException이 발생한다.
위와 같이 CartService 타입의 Bean이 두 개(cartService1, cartService2)이므로 Spring이 어떤 Bean을 주입할지 결정하지 못한다.
@Configuration
public class AppConfig {
@Bean
public CartService cartService1() {
return new CartServiceImpl1();
}
@Bean
public CartService cartService2() {
return new CartServiceImpl2();
}
}
@Controller
public class CartController {
@Autowired
@Qualifier("cartService1") // cartService1 Bean을 주입
private CartService service1;
@Autowired
@Qualifier("cartService2") // cartService2 Bean을 주입
private CartService service2;
}
위와 같이 @Qualifier 어노테이션을 사용하여 해결한다.
@Qualifier를 사용하면 주입받을 Bean의 이름을 명시적으로 지정할 수 있다.
cf. @Bean으로 정의한 Bean의 이름은 메서드 이름과 동일하다.
ex. cartService1() 메서드의 Bean 이름은 cartService1.
Spring은 기본적으로 타입(Type)을 기준으로 Bean을 주입한다.
하지만, 필드 이름과 Bean 이름이 일치하면 이름 매칭으로도 주입이 가능하다.
이때 @Service, @Component 등의 애노테이션에서 설정한 Bean 이름이 중요하다.
다음과 같이 bean을 등록할 때 이름을 지정할 수가 있다.
@Service(value = "cartServc") // Bean 이름을 "cartServc"로 설정
public class CartService {
// 서비스 로직
}
@Controller
public class CartController {
@Autowired
private CartService cartServc; // 필드 이름과 Bean 이름이 일치하므로 자동 주입
}
@Autowired가 적용된 CartController의 cartServc 필드는 필드 이름과 동일한 Bean 이름(cartServc)을 가진 CartService Bean을 찾아 주입한다.
만약, 필드 이름과 Bean 이름이 일치하지 않으면 NoUniqueBeanDefinitionException이 발생한다.
유사 어노테이션 참고
@Autowired
타입 매칭 우선, 이름 매칭은 필드 이름과 Bean 이름이 일치할 때만 동작. (타입 -> 한정자 -> 이름)
@Inject
javax.inject 패키지에 속하며, Spring의 @Autowired와 거의 동일하게 동작. (타입 -> 한정자 -> 이름)
@Resource
이름 매칭을 우선으로 함. 이름이 없으면 타입 매칭. (이름 -> 타입 -> 한정자)
@Primary는 Spring Framework에서 제공하는 애노테이션으로, 동일한 타입의 Bean이 여러 개 존재할 때, 기본적으로 주입될 Bean을 지정하기 위해 사용한다.
@Qualifier와 유사한 역할을 하지만, 보다 간단하게 기본 Bean을 지정할 수 있다.
@Autowired를 사용할 때, @Primary가 지정된 Bean이 기본적으로 주입된다.
@Configuration
public class AppConfig {
@Bean
@Primary // 기본으로 주입될 Bean 설정
public CartService cartService1() {
return new CartServiceImpl1();
}
@Bean
public CartService cartService2() {
return new CartServiceImpl2();
}
}
@Controller
public class CartController {
@Autowired
private CartService service; // cartService1()이 기본적으로 주입됨
}
cf. @Primary vs @Qualifier
특징 @Primary @Qualifier 용도 기본으로 사용할 Bean 지정 특정 Bean을 명시적으로 지정 적용 방식 한 번 설정하면 기본값으로 동작 주입 시마다 Bean 이름을 지정해야 함 사용 편리성 간단하고 직관적 더 세밀하게 Bean을 선택할 수 있음 우선 순위 @Qualifier가 지정된 경우, @Primary는 무시됨 @Primary보다 높은 우선순위
Spring Framework에서 제공하는 애노테이션으로, 외부 프로퍼티 파일에 정의된 값을 필드, 메서드 매개변수, 또는 생성자 매개변수에 주입할 때 사용한다.
주로 application.properties 또는 application.yml 파일에 정의된 설정 값을 코드에 주입할 때 활용한다.
// application.properties
// default.currency=USD
@Configuration
@PropertySource("classpath:application.properties") // 프로퍼티 파일을 로드
public class AppConfig {}
@Controller
public class CartController {
@Value("${default.currency}") // 프로퍼티 파일의 default.currency 값을 주입
private String defaultCurrency;
public void printCurrency() {
System.out.println("Default Currency: " + defaultCurrency);
}
}
cf. 스프링 부트를 사용할 경우 @PropertySource를 사용할 필요가 없고, src/main/resource 디렉토리 아래에 application.yml 또는 프로퍼티 파일을 두기만 하면된다.
AOP(Aspect-Oriented Programming)는 횡단 관심사(Cross-Cutting Concerns)를 모듈화하여 코드 중복을 줄이고 핵심 로직과 부가 로직을 분리하는 것을 의미한다.
횡단 관심사(Cross-Cutting Concerns): 애플리케이션 전반에 걸쳐 공통적으로 적용되어야 하는 로직(예: 로깅, 트랜잭션, 성능 모니터링 등)을 횡단 관심사라고 칭한다.
코드 중복을 줄이고 핵심 비즈니스 로직과 부가적인 관심사를 분리하여 유지보수성을 향상시킨다.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TimeMonitor {
}
cf. public @interface TimeMonitor는 커스텀 애노테이션(Custom Annotation)을 정의하기 위한 구문이다.
스프링 부트에서는 기본적으로 제공되는 애노테이션(@Component, @Service, @RestController 등)을 많이 사용하지만, 특정 요구사항에 맞춘 사용자 정의 어노테이션을 만들어 사용할 수 있다.
@Target(ElementType.METHOD)
어노테이션을 어디에 사용할 수 있는지 지정한다. 즉, 어노테이션의 적용 대상을 제한하는 역할을 한다.
@Target에 설정 가능한 값은 java.lang.annotation.ElementType 열거형(enum)으로 정의되어 있다.
주요 값은 다음과 같다:
ElementType.TYPE: 클래스, 인터페이스, 열거형, 애노테이션에 사용 가능하다.
ElementType.FIELD: 필드(멤버 변수)에 사용 가능하다.
ElementType.METHOD: 메서드에 사용 가능하다.
ElementType.PARAMETER: 메서드 매개변수에 사용 가능하다.
ElementType.CONSTRUCTOR: 생성자에 사용 가능하다.
ElementType.LOCAL_VARIABLE: 로컬 변수에 사용 가능하다.
ElementType.ANNOTATION_TYPE: 다른 애노테이션에 사용 가능하다.
ElementType.PACKAGE: 패키지 선언에 사용 가능하다.
@Retention(RetentionPolicy.RUNTIME)
어노테이션의 유지 정책(Retention Policy)을 정의한다. 즉, 어노테이션이 얼마나 오래 유지되는지를 설정한다.
어노테이션 정보를 런타임까지 유지하여 리플렉션이나 AOP에서 참조할 수 있도록 설정한다.
리플렉션(Reflection): 프로그램이 실행 중에 클래스, 메서드, 필드, 어노테이션 등을 읽는 기술.
주요 값은 다음과 같다. java.lang.annotation.RetentionPolicy 열거형(enum)으로 정의되어 있다:
RetentionPolicy.SOURCE:
소스 코드에만 존재하고, 컴파일 후에는 사라진다. 컴파일러가 사용하는 어노테이션에 적합.
ex. @Override
RetentionPolicy.CLASS:
컴파일된 클래스 파일(.class)에는 남아 있지만, JVM 실행 시에는 읽을 수 없음.
해당 값이 기본값으로 설정되어 있다.
ex. 일부 내부 어노테이션.
RetentionPolicy.RUNTIME:
런타임 동안 JVM에 의해 유지된다. 리플렉션을 사용하여 런타임에 어노테이션 정보에 접근 가능하다.
ex. @Autowired, @Controller
@Aspect
@Component
public class TimeMonitorAspect {
@Around("@annotation(com.packt.modern.api.TimeMonitor)")
public Object logTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis(); // 시작 시간 기록
Object proceed = joinPoint.proceed(); // 실제 메서드 실행
long executionTime = System.currentTimeMillis() - start; // 실행 시간 계산
System.out.println(joinPoint.getSignature() + " takes: " + executionTime + " ms");
return proceed; // 메서드 결과 반환
}
}