Java 어노테이션 (Annotation) 파헤치기

Sei Kim·2024년 9월 25일

1. Annotation


어노테이션이란?

사전적으로는 “주석”이라는 의미를 가지고 있습니다.
자바에서는 @ 를 이용하여 주석처럼 달아 특수한 의미를 부여합니다.

1.1. 어노테이션 장점


  1. 코드 가독성: 어노테이션은 코드와 설정을 같은 위치에 배치하므로 읽고 이해하기가 쉽습니다. 클래스, 메서드, 필드, 파라미터 등 연관된 코드와 가까이 있기 때문에 흐름을 따라가기 쉽습니다.
  2. 설정의 간소화: 별도의 설정 파일 작성 없이 어노테이션 적용을 통해 설정을 간소화할 수 있습니다.
  3. 중복 코드 제거: 공통적인 코드 패턴이나 설정을 재사용할 수 있기 때문에 코드의 중복을 줄이고 효율적으로 코드를 작성할 수 있습니다.
  4. 커스텀 어노테이션: 직접 커스텀 어노테이션을 정의함으로 필요한 기능이나 제약 사항을 정의하여 사용할 수 있습니다.
  5. 프로세서를 통한 검증 및 코드 생성: 어노테이션 프로세서를 이용해 컴파일 시점에 어노테이션을 처리하고 검증할 수 있습니다. 또한 코드를 자동으로 생성하거나 수정할 수 있기에 효과적으로 기능을 구현할 수 있습니다.

1.2. 어노테이션 단점


  1. 런타임 오버헤드: 런타임 시점에 리플랙션을 사용하여 처리하는 어노테이션의 경우 성능상의 오버헤드가 발생할 수 있습니다.
  2. 컴파일 시점 제한: 어노테이션도 컴파일 시점에 오류를 확인할 수 있지만, 어노테이션 로직이 런타임에 에러를 발생시키거나 어노테이션에 잘못된 값이 할당된 경우 컴파일 시점에 오류를 확인할 수 없을 수도 있습니다.

2. Java Annotation


자바의 어노테이션은 소스코드를 추가해서 사용할 수 있는 메타데이터의 일종입니다.

⭐ 메타데이터란?

어떤 목적을 가지고 만들어진 데이터
출처: 위키백과

JDK 1.5 버전 이상에서부터 사용가능하며, 자바 어노테이션은 클래스 파일에 임베드되어 컴파일러에 의해 생성된 이후 JVM에 포함되어 동작합니다.

2.1. 자바 어노테이션을 이용하여 할 수 있는 일


자바 어노테이션을 이용하면 다음을 할 수 있습니다.

  • 컴파일러에게 코드 작성 문법 에러를 체크하도록 정보 제공
  • 소프트웨어 개발 환경이 빌드나 배포시 코드를 자동으로 생성할 수 있도록 정보 제공
  • 런타임에 특정 기능을 실행하도록 정보를 제공

아래에서 하나씩 자세히 알아보도록 하겠습니다.

2.1.1. 컴파일러에게 코드 작성 문법 에러를 체크하도록 정보 제공


어노테이션은 컴파일 단계에서 코드의 유효성을 검사하는데 사용될 수 있습니다. 대표적인 예로 @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!");
	}
}

2.1.2. 소프트웨어 개발 환경이 빌드나 배포시 코드를 자동으로 생성할 수 있도록 정보 제공


어노테이션 프로세스(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"
	}
}

2.1.3. 런타임에 특정 기능을 실행하도록 정보를 제공


런타임에 동작하는 어노테이션을 통해 리플렉션을 사용하여 특정 동작을 수행할 수 있습니다. 스프링 프레임워크에서 흔히 사용되는 @Transactional@Service 등이 이런 역할을 합니다.

@Service
public class ProductService {

	@Transactional
	public void save(String name, int quantity) {
		ProductEntity product = new ProductEntity(name, quantity);
		// ETC...
	}
}

2.2. Meta Annotation


커스텀 어노테이션을 만들 때 사용하는 메타 어노테이션도 있습니다.

2.2.1. @Retention


어노테이션의 리텐션 기간을 명명합니다.

Type설명
RetentionPolicy.Class바이트 코드 파일까지 어노테이션 정보를 유지합니다.
리플렉션을 이용해서 어노테이션 정보를 얻을 수 없습니다.
RetentionPolicy.Runtime바이트 코드 파일까지 어노테이션 정보를 유지하면서
리플랙션을 이용해 런타임에 어노테이션 정보를 가져올 수 있습니다.
RetentionPolicy.SourceCompile 이후에는 삭제됩니다.
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME) // 해당 부분
@Documented
@Component
public @interface Service {
    @AliasFor(
        annotation = Component.class
    )
    String value() default "";
}

위에는 @Service 어노테이션을 확인해본 결과 Runtime 으로 동작하는 것을 확인할 수 있습니다.

2.2.2. @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) 에서 확인할 수 있습니다.

2.2.3. @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패키지

2.1.4. @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 이 자식 클래스에도 적용이 됩니다.

2.1.5. @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() {
	}
}

3. 커스텀 어노테이션 만들기


3.1. Annotation Processor


수정중...

3.2. Spring AOP


Spring AOP를 사용하여 커스텀 어노테이션을 생성하고, 이를 메서드에 적용하는 예시입니다. 이 코드는 @MethodTimer라는 어노테이션을 사용하여, 특정 메서드의 실행 시간을 측정하고 로그로 출력하는 기능을 구현합니다.

3.2.1. 커스텀 어노테이션 정의


먼저, @MethodTimer라는 어노테이션을 정의하여 특정 메서드에 적용할 수 있도록 설정합니다. 이 어노테이션은 런타임 시점에 유지되며, 메서드 레벨에서만 적용됩니다.

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface MethodTimer {
}

3.2.2. AOP를 통한 메서드 실행 시간 측정


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();
        }
    }
}

3.2.3. 서비스 클래스에서 커스텀 어노테이션 사용


이제 @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));
    }
}

3.2.4. 실행 결과


@MethodTimer가 적용된 메서드를 호출하면, 메서드가 실행되는 동안의 시간이 로그로 출력됩니다. 예를 들어, TimerService.sleep() 메서드가 실행될 때마다 해당 메서드의 실행 시간이 기록됩니다.

4. 스프링에서 어노테이션이 많은 기능을 하는 이유


XML을 통한 복잡한 설정은 코드의 복잡성을 증가시키고, 이는 가독성을 떨어뜨리며 유지보수를 어렵게 만드는 요인이 됩니다. 이러한 문제를 해결하기 위해 스프링에서는 어노테이션 기반 구성을 지원합니다.

어노테이션 기반 구성은 가독성, 유지보수성, 간결함, 표준화 측면에서 많은 장점을 제공합니다. 이를 통해 개발자들은 더 효율적이고 명확한 코드를 작성할 수 있습니다. 그러나 스프링은 여전히 XML 구성을 지원하므로, 프로젝트 요구사항에 맞춰 어노테이션 기반 구성과 XML 구성을 함께 사용할 수 있는 유연성을 제공합니다.

스프링은 빈을 등록하기 위해 사용하는 @Bean과, 컴포넌트로 등록하기 위한 @Component, @Service, @Repository 등 다양한 어노테이션을 제공하며, 설정 클래스를 나타내는 @Configuration 어노테이션도 지원합니다.

5. @Data 어노테이션을 잘 사용하지 않는 이유


지금까지 어노테이션에 대해서 많이 알아봤는데 그렇다면 무조건 어노테이션은 좋지 않다는 것을 알기 위해 @Data 어노테이션을 잘 사용하지 않는 이유에 대해 알아보도록 하겠습니다.

@Data 어노테이션이란? @ToString, @Getter, @Setter, @EqualsAndHashCode, RequiredArgsConstructor 을 모두 포함하는 강력한 어노테이션 입니다.

하지만 해당 어노테이션의 사용을 지양하는 이유는 다음과 같습니다.

5.1. 무분별한 Setter 남용


특정 필드가 변경을 허용하지 않기로 했다면 Setter 도 없어야 안정성을 보장할 수 있습니다. 하지만 @Data 를 사용하면 특정 필드도 Setter 가 생성되기 때문에 안정성을 보장 받을 수 없게 됩니다.

5.2. @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);	// 컴파일 에러
	}
}

위와 같이 필드의 순서가 변경되면 에러가 발생하게 되므로 사용을 지양해야 합니다.

Ref


0개의 댓글