어노테이션이란?
사전적으로는 “주석”이라는 의미를 가지고 있습니다.
자바에서는 @ 를 이용하여 주석처럼 달아 특수한 의미를 부여합니다.
자바의 어노테이션은 소스코드를 추가해서 사용할 수 있는 메타데이터의 일종입니다.
⭐ 메타데이터란?
어떤 목적을 가지고 만들어진 데이터
출처: 위키백과
JDK 1.5 버전 이상에서부터 사용가능하며, 자바 어노테이션은 클래스 파일에 임베드되어 컴파일러에 의해 생성된 이후 JVM에 포함되어 동작합니다.
자바 어노테이션을 이용하면 다음을 할 수 있습니다.
아래에서 하나씩 자세히 알아보도록 하겠습니다.
어노테이션은 컴파일 단계에서 코드의 유효성을 검사하는데 사용될 수 있습니다. 대표적인 예로 @Override 가 있습니다.
이 외에도 메소드를 사용하는 애플리케이션을 컴파일 할 경우 경고를 발생시키는 @Deprecated 와 컴파일러 경고를 출력하지 않도록 하는 @SupperessWarnings 가 있습니다.
abstract class BaseEntity {
public void print() {
System.out.println("I'm BaseEntity");
}
@Deprecated
public void fly() {
System.out.println("I'm flying");
}
}
public class MemberEntity extends BaseEntity {
public static void main(String[] args) {
MemberEntity memberEntity = new MemberEntity();
memberEntity.fly();
// Note: /src/main/java/com/seikim/annotationstudy/annotation2/MemberEntity.java uses or overrides a **deprecated API.**
}
@Override
public void print() {
System.out.println("I'm MemberEntity");
}
@Override // 컴파일에러 발생
public void walk() {
System.out.println("Oops!");
}
}
어노테이션 프로세스(Annotation Processor)를 이용하면 컴파일 시점에 코드를 자동 생성할 수 있습니다. 대표적인 라이브러리로 Lombok이 있으며, @Getter , @AllArgsConstructor 를 통해 자동으로 코드를 생성합니다.
@AllArgsConstructor
@Getter
public class ProductEntity {
private String name;
private int quantity;
public static void main(String[] args) {
ProductEntity productEntity = new ProductEntity("과자", 10);
System.out.println(
MessageFormat.format("제품 이름: {0}, 수량: {1}",
productEntity.getName(), productEntity.getQuantity()));
// Output: "제품 이름: 과자, 수량: 10"
}
}
런타임에 동작하는 어노테이션을 통해 리플렉션을 사용하여 특정 동작을 수행할 수 있습니다. 스프링 프레임워크에서 흔히 사용되는 @Transactional 와 @Service 등이 이런 역할을 합니다.
@Service
public class ProductService {
@Transactional
public void save(String name, int quantity) {
ProductEntity product = new ProductEntity(name, quantity);
// ETC...
}
}
커스텀 어노테이션을 만들 때 사용하는 메타 어노테이션도 있습니다.
@Retention어노테이션의 리텐션 기간을 명명합니다.
| Type | 설명 |
|---|---|
| RetentionPolicy.Class | 바이트 코드 파일까지 어노테이션 정보를 유지합니다. 리플렉션을 이용해서 어노테이션 정보를 얻을 수 없습니다. |
| RetentionPolicy.Runtime | 바이트 코드 파일까지 어노테이션 정보를 유지하면서 리플랙션을 이용해 런타임에 어노테이션 정보를 가져올 수 있습니다. |
| RetentionPolicy.Source | Compile 이후에는 삭제됩니다. |
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME) // 해당 부분
@Documented
@Component
public @interface Service {
@AliasFor(
annotation = Component.class
)
String value() default "";
}
위에는 @Service 어노테이션을 확인해본 결과 Runtime 으로 동작하는 것을 확인할 수 있습니다.
@Documented자바 문서에도 어노테이션 정보가 표현됩니다.
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented // 해당 부분
@Component
public @interface Service {
@AliasFor(
annotation = Component.class
)
String value() default "";
}
해당 어노테이션에 대한 정보는 Service (Spring Framework 6.1.13 API) 에서 확인할 수 있습니다.

@Target생성할 어노테이션이 적용될 수 있는 위치를 나열합니다.
@Target({ElementType.TYPE, ElementType.METHOD}) // 해당 부분은 배열이라 여러개 가능합니다.
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Reflective
public @interface Transactional {
// ETC...
}
| Type | 설명 |
|---|---|
| ElementType.TYPE | 클래스, 인터페이스, 열거 타입 |
| ElementType.ANNOTATION_TYPE | 어노테이션 |
| ElementType.FILED | 필드 |
| ElementType.CONSTRUCTOR | 생성자 |
| ElementType.METHOD | 메소드 |
| ElementType.LOCAL_VARIABLE | 로컬 변수 |
| ElementType.PACKAGE | 패키지 |
@Inherited자식 클래스가 어노테이션을 상속 받을 수 있습니다.
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited // 해당 부분
@Documented
@Reflective
public @interface Transactional {
// ETC...
}
@Transactional
abstract class CustomRepository<T> {
AtomicInteger id = new AtomicInteger(1);
ConcurrentHashMap<Integer, T> map = new ConcurrentHashMap<>();
public T save(T entity) {
return map.put(id.getAndIncrement(), entity);
}
}
// `@Transactional`적용
@Repository
class ProductRepository extends CustomRepository<ProductEntity> {
}
다음과 같이 부모 클래스인 CustomRepositorty 에서 적용한 @Transactional 이 자식 클래스에도 적용이 됩니다.
@Repeatable반복적으로 어노테이션을 선언할 수 있습니다.
@Repeatable(Schedules.class)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Schedule {
String day();
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Schedules {
Schedule[] value();
}
class Event {
@SneakyThrows
public static void main(String[] args) {
Method method = Event.class.getMethod("scheduledEvent");
Schedule[] schedules = method.getAnnotationsByType(Schedule.class);
for (Schedule schedule : schedules) {
System.out.printf("Schedule: %s\n", schedule.day());
}
/**
* Output
* Schedule: Monday
* Schedule: Wednesday
* Schedule: Friday
*/
}
// 반복적으로 어노테이션을 선언
@Schedule(day = "Monday")
@Schedule(day = "Wednesday")
@Schedule(day = "Friday")
public void scheduledEvent() {
}
}
수정중...
Spring AOP를 사용하여 커스텀 어노테이션을 생성하고, 이를 메서드에 적용하는 예시입니다. 이 코드는 @MethodTimer라는 어노테이션을 사용하여, 특정 메서드의 실행 시간을 측정하고 로그로 출력하는 기능을 구현합니다.
먼저, @MethodTimer라는 어노테이션을 정의하여 특정 메서드에 적용할 수 있도록 설정합니다. 이 어노테이션은 런타임 시점에 유지되며, 메서드 레벨에서만 적용됩니다.
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface MethodTimer {
}
MethodTimerAspect 클래스는 @MethodTimer가 적용된 메서드의 실행 시간을 측정합니다. ThreadLocal을 사용하여 각 스레드마다 StopWatch 객체를 관리하고, 메서드가 실행된 시간을 계산한 후 이를 로그로 출력합니다.
@Slf4j
@Component
@Aspect
class MethodTimerAspect {
private final ThreadLocal<StopWatch> stopWatchThreadLocal = new ThreadLocal<>();
@Around("@annotation(com.seikim.annotationstudy.annotation3.MethodTimer)")
public Object process(ProceedingJoinPoint joinPoint) throws Throwable {
// StopWatch 객체를 각 스레드별로 저장
stopWatchThreadLocal.set(new StopWatch());
StopWatch stopWatch = stopWatchThreadLocal.get();
stopWatch.start();
try {
// 메서드 실행
Object result = joinPoint.proceed();
// 메서드 실행 후 시간 측정 종료
stopWatch.stop();
log.info(stopWatch.prettyPrint());
return result;
} catch (Throwable t) {
// 예외 발생 시에도 시간을 측정하고 로그 출력
stopWatch.stop();
log.error(stopWatch.prettyPrint());
throw t;
} finally {
// 스레드 로컬에서 StopWatch 제거
stopWatchThreadLocal.remove();
}
}
}
이제 @MethodTimer 어노테이션을 TimerService 클래스의 메서드에 적용하여 해당 메서드가 실행되는 동안의 시간을 측정합니다. 예를 들어, sleep() 메서드는 1초에서 3초 동안 대기하며, 해당 시간 동안의 실행 시간이 로그에 출력됩니다.
@Service
class TimerService {
@MethodTimer
public void sleep() throws InterruptedException {
// 1초에서 3초 사이의 랜덤한 시간 동안 대기
Thread.sleep(1_000L * (ThreadLocalRandom.current().nextInt(3) + 1));
}
}
@MethodTimer가 적용된 메서드를 호출하면, 메서드가 실행되는 동안의 시간이 로그로 출력됩니다. 예를 들어, TimerService.sleep() 메서드가 실행될 때마다 해당 메서드의 실행 시간이 기록됩니다.

XML을 통한 복잡한 설정은 코드의 복잡성을 증가시키고, 이는 가독성을 떨어뜨리며 유지보수를 어렵게 만드는 요인이 됩니다. 이러한 문제를 해결하기 위해 스프링에서는 어노테이션 기반 구성을 지원합니다.
어노테이션 기반 구성은 가독성, 유지보수성, 간결함, 표준화 측면에서 많은 장점을 제공합니다. 이를 통해 개발자들은 더 효율적이고 명확한 코드를 작성할 수 있습니다. 그러나 스프링은 여전히 XML 구성을 지원하므로, 프로젝트 요구사항에 맞춰 어노테이션 기반 구성과 XML 구성을 함께 사용할 수 있는 유연성을 제공합니다.
스프링은 빈을 등록하기 위해 사용하는 @Bean과, 컴포넌트로 등록하기 위한 @Component, @Service, @Repository 등 다양한 어노테이션을 제공하며, 설정 클래스를 나타내는 @Configuration 어노테이션도 지원합니다.
@Data 어노테이션을 잘 사용하지 않는 이유지금까지 어노테이션에 대해서 많이 알아봤는데 그렇다면 무조건 어노테이션은 좋지 않다는 것을 알기 위해 @Data 어노테이션을 잘 사용하지 않는 이유에 대해 알아보도록 하겠습니다.
@Data 어노테이션이란? @ToString, @Getter, @Setter, @EqualsAndHashCode, RequiredArgsConstructor 을 모두 포함하는 강력한 어노테이션 입니다.
하지만 해당 어노테이션의 사용을 지양하는 이유는 다음과 같습니다.
특정 필드가 변경을 허용하지 않기로 했다면 Setter 도 없어야 안정성을 보장할 수 있습니다. 하지만 @Data 를 사용하면 특정 필드도 Setter 가 생성되기 때문에 안정성을 보장 받을 수 없게 됩니다.
@RequiredArgsConstructor 어노테이션으로 인한 문제@RequiredArgsConstructor 어노테이션으로 인한 문제으로 문제가 발생할 수 있습니다.
예를 들어 인해 두 개의 타입 인스턴스 멤버를 선언한 상황에서 개발자가 선언된 인스턴스 멤버의 순서를 바꾸면, 개발자도 인식하지 못하는 사이에 lombok이 생성자의 파라미터 순서를 필드 선언 순서에 따라 변형하게 된다.
이때, IDE가 제공해주는 리팩토링은 전혀 동작하지 않고, 두 필드가 동일 타입이기 때문에 기존 소스에서도 오류가 발생하지 않아 아무런 문제없이 동작하는 것으로 보이지만, 실제로 입력된 값이 바뀌어 들어가는 상황이 발생합니다.
@Data
public class Member {
private final int id;
private final String name;
private final int age;
}
@Data
class BeforeMember {
private final int id;
private final int age;
private final String name;
}
class Main {
public static void main(String[] args) {
Member member = new Member(1, "김세이", 25);
BeforeMember member = new BeforeMember(1, "김세이", 25); // 컴파일 에러
}
}
위와 같이 필드의 순서가 변경되면 에러가 발생하게 되므로 사용을 지양해야 합니다.