Java Annotation

정찬's avatar
Sep 05, 2024
Java Annotation
 

어노테이션이란?

 
사전적 정의로 어노테이션은 '주석'을 의미합니다.
그러나 어노테이션은 단순한 주석 이상의 기능을 합니다. 실제로 프로그램 실행에 도움을 주는 역할을 합니다.
어노테이션은 일반적인 주석과 다릅니다. Java에서 사용하는 일반 주석(// , / /)은 컴파일 시 무시되어 바이트코드에 남지 않습니다. 이러한 주석은 코드를 읽는 사람들에게만 정보전달이 가능했습니다.
반면, 어노테이션은 런타임 이후까지 생명주기를 지정할 수 있어 컴파일러가 인식할 수 있습니다.
그리고 JVM에서 리플렉션으로 접근하여 어노테이션을 읽을 수 있습니다. 따라서 어노테이션은 실제 프로그램의 로직에 도움을 줄 수 있습니다.
 
정리하자면, 어노테이션은 "런타임 시점에도 남아있는 구조화된 주석"이라고 볼 수 있습니다.
어노테이션과 리플렉션을 활용한다면 코드 구조상의 불편함을 해결할 수 있는데요, 지금부터 알아보겠습니다.
 

리플렉션이란?

 
Reflection은 Java에서 실행 시간에 클래스, 메서드, 필드 등의 메타데이터를 검사하고 조작할 수 있는 기능을 제공하는 API입니다.
리플렉션을 사용하면 프로그램 실행 중에 클래스의 구조를 동적으로 검사하고, 객체를 생성하거나 메서드를 호출하는 등의 작업을 할 수 있습니다.
 
자바는 리플렉션 기능을 지원하기 때문에 어노테이션을 효과적으로 활용할 수 있습니다.

리플렉션 사용 상황

  1. 동적 객체 생성
  1. 동적 메서드 호출
  1. 동적 필드 접근
  1. 어노테이션 처리
 
public class Reflection { public static void main(String[] args) throws Exception { // Class for the class named "A" Class<?> clazz = Class.forName("A"); for (Field field : clazz.getDeclaredFields()) { System.out.println(field.getName()); Object instance = clazz.getDeclaredConstructor().newInstance(); field.setAccessible(true); System.out.println(field.getName() + ": " + field.get(instance)); } } } class A { // Public fields for demonstration public int a = 10; public int b = 20; }
 
코드를 보면 리플랙션을 사용하는 상황을 알 수 있습니다.
 
클래스의 이름으로 동적으로 객체를 생성하고, 필드에 접근하고 있습니다.
리플랙션을 사용하면 클래스들의 메타데이터를 확인하여 해당 타겟의 연결 방법이나 구조를 변경할 수 있습니다.
 
리플랙션으로 어노테이션을 접근하는 상황은 다음과 같습니다.
 
public class Reflection { public static void main(String[] args) throws Exception { Class<?> clazz = Class.forName("A"); for (Method method : clazz.getDeclaredMethods()) { for (Annotation annotation : method.getAnnotations()) { if (annotation.annotationType().equals(Deprecated.class)) { System.out.println("Deprecated Method: " + method.getName()); } } } } } class A { private int a = 10; private int b = 20; @Deprecated public void method1() { } }
 
어노테이션을 활용하면 다음처럼 메타데이터를 추가할 수 있어, 리플렉션으로 가져올 수 있는 데이터가 더 많아집니다.
 

어노테이션 사용을 고려해볼 상황

 
코드를 작성할 때 어노테이션을 도입하면 좋은 상황을 알아봅시다.

어노테이션의 장점

 
어노테이션의 장점은 다음과 같습니다:
 
  • 코드 가독성 향상:
    • - 어노테이션은 메타데이터를 명확하고 간결하게 표현합니다.
      - 코드의 의도와 기능을 직관적으로 파악할 수 있게 해줍니다.
      - 예를 들어, @Override 어노테이션은 메서드가 상위 클래스의 메서드를 오버라이드한다는 것을 명확히 나타냅니다.
  • 설정의 간소화:
    • - XML 설정 파일 대신 어노테이션을 사용하면 설정을 더 간단하고 직관적으로 할 수 있습니다.
      - 코드 내에서 직접 설정을 관리할 수 있어 유지보수가 용이합니다.
      - 예를 들어, Spring Framework에서 @Component, @Autowired 등의 어노테이션으로 빈 설정을 간소화할 수 있습니다.
  • 컴파일 시점 검사:
    • - 특정 어노테이션을 사용하면 컴파일러가 코드의 정확성을 검증할 수 있습니다.
      - 런타임 오류를 사전에 방지하여 프로그램의 안정성을 높입니다.
      - 예를 들어, @FunctionalInterface 어노테이션은 해당 인터페이스가 함수형 인터페이스의 요구사항을 충족하는지 컴파일 시점에 확인합니다.
  • 런타임 처리 지원:
    • - 어노테이션은 런타임에 리플렉션을 통해 처리될 수 있어, 동적인 기능 구현이 가능합니다.
      - AOP(Aspect-Oriented Programming)를 구현하는 데 유용하게 사용됩니다.
      - 예를 들어, Spring의 @Transactional 어노테이션은 메서드 실행 시 트랜잭션 처리를 동적으로 수행합니다.
  • 코드 생성 및 자동화:
    • - 어노테이션 프로세서를 이용해 컴파일 시점에 코드를 자동으로 생성할 수 있습니다.
      - 반복적인 작업을 줄이고 개발 생산성을 향상시킵니다.
      - 예를 들어, Lombok 라이브러리의 @Getter, @Setter 어노테이션은 getter와 setter 메서드를 자동으로 생성합니다.
 

어노테이션 단점

 
어노테이션을 사용하면 오버헤드가 발생하고, 특히 런타임 시점에 어노테이션이 에러를 발생시킨다면 오류를 확인할 수 없다는 단점이 있습니다.
 

어노테이션 종류

 

1. 표준 어노테이션

 
표준 어노테이션은 Java 표준 라이브러리에 포함된 기본 제공 어노테이션입니다.
 
  • @Override
    • 이 어노테이션은 메서드가 슈퍼클래스에 있는 메서드를 오버라이드 하고 있음을 명시적으로 나타냅니다. 이 어노테이션을 사용하면 실수로 잘못된 메서드 식별자를 사용했을 때 컴파일러가 경고를 제공합니다.
  • @Deprecated
    • 더 이상 사용되지 않는 메서드나 클래스에 붙이며, 해당 요소가 곧 제거될 예정임을 나타냅니다. 컴파일러는 이 어노테이션이 붙은 코드를 사용할 때 경고를 발생시킵니다.
  • @SuppressWarnings
    • 컴파일러 경고를 무시하도록 지시하는데 사용됩니다.
  • @SafeVarargs
    • 가변 인수를 사용하는 메서드 또는 생성자가 타입에 안전하다는 것을 보장하기 위해 사용합니다. 주로 제네릭 가변 인수 메서드에서 경고를 억제하는데 사용합니다.
  • @FuncionalInterface
    • 인터페이스가 함수형 인터페이스임을 명시합니다. 함수형 인터페이스는 하나의 추상 메서드만을 가지는 인터페이스로, 람다 표현식에서 사용됩니다.
 

2. 메타 어노테이션

 
메타 어노테이션은 다른 어노테이션을 정의할 때 사용되는 어노테이션입니다.
 
  • @Retention
    • 어노테이션의 유지 정책을 정의합니다.
    • Option
      • RetentionPolicy.SOURCE : 소스 코드에만 존재
      • RetentionPolicy.CLASS : 컴파일 된 바이트코드에 존재. 런타임 시점에 소멸
      • RetentionPolicy.RUNTIME : 런타임에도 유지됨
  • @Target
    • 어노테이션이 적용될 수 있는 위치를 정의합니다.
    • Option
      • ElementType.TYPE : 클래스, 인터페이스, 열거형에 어노테이션을 적용할 수 있습니다.
      • ElementType.FIELD : 필드에 어노테이션을 적용할 수 있습니다.
      • ElementType.METHOD : 메서드에 어노테이션을 적용할 수 있습니다.
      • ElementType.PARAMETER : 메서드 매개변수에 어노테이션을 적용할 수 있습니다.
      • ElementType.CONSTRUCTOR : 생성자에 어노테이션을 적용할 수 있습니다.
      • ElementType.LOCAL_VARIABLE : 로컬 변수에 어노테이션을 적용할 수 있습니다.
      • ElementType.ANNOTATION_TYPE : 다른 어노테이션에 적용할 수 있습니다.
      • ElementType.PACKAGE : 패키지에 어노테이션을 적용할 수 있습니다.
      • ElementType.TYPE_PARAMETER : 제네릭 타입 파라미터에 적용할 수 있습니다.
      • ElementType.TYPE_USE : 타입 선언에 어노테이션을 적용할 수 있습니다.
  • @Inherited
    • 자식 클래스가 부모 클래스에 선언된 어노테이션을 상속받도록 합니다. 클래스 레벨에서만 적용되며 메서드나 필드에는 적용되지 않습니다.
  • @Documented
    • 어노테이션은 특정 어노테이션이 Javadoc으로 문서화될 때 포함되도록 합니다. 이 어노테이션은 주로 API 문서를 작성할 때 유용합니다.
  • @Repeatable
    • 동일한 어노테이션을 같은 대상에 여러 번 적용할 수 있도록 해줍니다. Java 8부터 지원됩니다. @Repeatable 어노테이션을 사용하려면, 이 어노테이션의 인자로 컨테이너 어노테이션을 지정해야 합니다.
 

사용자 정의 어노테이션 도입해보기

 

문제 상황

 
public class JokBoCalculator { //... public static JokBo calculateJokBo(List<Card> cards) { Collections.sort(cards); Collections.reverse(cards); Optional<JokBo> optionalJokBo; optionalJokBo = JokBoCalculator.evaluateStraightFlush(cards); if (optionalJokBo.isPresent()) { return optionalJokBo.get(); } optionalJokBo = JokBoCalculator.evaluateFourOfAKind(cards); if (optionalJokBo.isPresent()) { return optionalJokBo.get(); } optionalJokBo = JokBoCalculator.evaluateFullHouse(cards); if (optionalJokBo.isPresent()) { return optionalJokBo.get(); } optionalJokBo = JokBoCalculator.evaluateFlush(cards); if (optionalJokBo.isPresent()) { return optionalJokBo.get(); } optionalJokBo = JokBoCalculator.evaluateStraight(cards); if (optionalJokBo.isPresent()) { return optionalJokBo.get(); } optionalJokBo = JokBoCalculator.evaluateTwoPair(cards); if (optionalJokBo.isPresent()) { return optionalJokBo.get(); } optionalJokBo = JokBoCalculator.evaluateOnePair(cards); if (optionalJokBo.isPresent()) { return optionalJokBo.get(); } optionalJokBo = JokBoCalculator.evaluateHighCard(cards); if (optionalJokBo.isPresent()) { return optionalJokBo.get(); } return JokBoCalculator.evaluateHighCard(cards).get(); } }
 
위의 코드는 Nhn 아카데미에서 과제로 작성한 포커 쇼다운 로직입니다.
제가 짰던 위의 함수는 몇가지 문제가 있습니다.
 
  1. 코드 반복:
    1. 사용자의 카드를 받아 알맞은 족보를 검출하여 반환하는 로직을 수행하는데 코드를 보시면 총 9개의 족보를 처리하는 로직이 순서에 따라 반복되고 있습니다.
  1. 가독성 저하 :
    1. if문의 많은 반복으로 코드의 가독성이 떨어집니다.
  1. 유연성 및 확장성 부족 포커는 많은 종류가 있고, 같은 포커 게임에서도 많은 룰이 있습니다.
    1. 예를들어 함수의 input으로 전달받는 card의 개수가 달라질 수 있습니다 ( 플레이어가 소유하고 있는 카드의 숫자나 커뮤니티 카드의 유무에 따라 달라질 수 있음). 또한 많은 룰이 있는 포커의 경우를 생각해봤을 때 확장성을 생각하지 않고 작성한 코드입니다.
       
 

사용자 정의 어노테이션을 사용하여 문제 해결

 
위 3가지 문제를 어노테이션을 사용하여 해결해보겠습니다.
문제 코드를 보면 실행되는 메서드 순서만 다를 뿐, 같은 로직을 반복하고있습니다.
따라서 족보를 검출하는 함수에 사용자 정의 어노테이션을 정의하여 실행 순서 메타데이터를 추가해 봅시다.
 

1. 사용자 정의 어노테이션 생성

 
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface JokBoEvaluator { int order(); }
 
순서를 인자로 받는 JokBoEvaluator 어노테이션을 생성하였습니다.
 

2. 함수에 어노테이션 추가

 
@JokBoEvaluator(order = 1) private static Optional<JokBo> evaluateStraightFlush(List<Card> cards) { Optional<JokBo> flushJokBo = evaluateFlush(cards); if (flushJokBo.isPresent()) { Optional<JokBo> straightJokBo = evaluateStraight(cards); if (straightJokBo.isPresent()) { return Optional.of(new JokBo(JokBoTitle.STRAIGHT_FLUSH, flushJokBo.get().getStrengthContext())); } } return Optional.empty(); }
 
함수에 @JokBoEvaluator 어노테이션을 추가하고 순서를 넘겨줍니다.
 

3. 족보 검출 방법 설정

 
public class JokBoCalculator { private static final List<Method> evaluators; static { evaluators = new ArrayList<>(); for (Method method : JokBoCalculator.class.getDeclaredMethods()) { if (method.isAnnotationPresent(JokBoEvaluator.class)) { evaluators.add(method); } } evaluators.sort(Comparator.comparingInt(m -> m.getAnnotation(JokBoEvaluator.class).order())); } }
 
  1. 9가지 족보를 검산하는 함수들을 찾아냅니다. (JokBoEvaluator 어노테이션)
  1. order 인자에 따라 순서대로 정렬합니다.
 
비즈니스로직 내에 실행 순서와 족보 검출 로직이 담겨있는 전 함수와는 달리, 클래스가 로드될 때 evaluators를 설정하는 방식으로 변경하였습니다.
 

4. 결과 (비즈니스 로직)

 
public static JokBo calculateJokBo(List<Card> cards) { for (Method evaluator : evaluators) { try { Optional<JokBo> result = (Optional<JokBo>) evaluator.invoke(null, cards); if (result.isPresent()) { return result.get(); } } catch (Exception e) { throw new RuntimeException("Error while evaluating JokBo", e); } } return new JokBo(JokBoTitle.NONE, new ArrayList<>()); }
 
결과적으로, 어노테이션과 리플렉션을 활용하여 코드의 반복을 줄이고 가독성을 향상시켰습니다.
또한 새로운 족보를 추가하거나 순서를 변경하는 것이 더욱 용이해져 유연성과 확장성이 크게 개선되었습니다.
 

Reference

 
 
Share article

lushlife99