본문 바로가기

개발 이야기/java

[Java][Spring Boot]어노테이션(Annotation)

회사에서 spring boot 전공 서적을 구입하여 스터디를 진행 중인데, 기존에 ppt를 만들어 발표하는 형식에서 자료를 오랫동안 보존하고 싶은 마음에 블로그에 정리를 시작해볼까 한다. 비록 어느 정도 진행된 스터디이나 앞부분은 추후 시간이 될 때마다 발표했던걸 다시 공부할 겸 정리해서 올릴 계획이다. 글이 다소 정리가 안되어 있을 수 있다. 

 

어노테이션(Annotation)은 java1.5 버전부터 지원되는 기능으로 일종의 메타데이터(metadata)다. 사전적인 의미는 주석인데, 주석처럼 코드에 추가해서 사용할 수 있으며 컴파일 또는 런타임 시에 해석된다. 


1. 어노테이션 선언 형식

어노테이션은 @interface를 붙여 선언하고 적용될 대상과 동작 방식을 지정할 수 있다.

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

@Target은 어노테이션이 적용되는 위치를 의미하며 전체 목록은 아래와 같다. 

요소명 적용 대상
TYPE 클래스 및 인터페이스
FIELD 클래스의 멤버 변수
METHOD 메서드
PARAMETER 파라미터
CONSTRUCTOR 생성자
LOCAL_VARIABLE 지역변수
ANNOTATION_TYPE 어노테이션 타입
PACKAGE 패키지
TYPE_PARAMETER 타입 파라미터
TYPE_USE 타입 사용

 

@Retention은 어노테이션이 적용될 범위로 어떤 시점까지 어노테이션이 영향을 미치는지를 결정한다. 

  • Class : 어노테이션 작성 시 기본값으로 클래스 파일에 포함되지만 JVM이 로드하지 않는다.
  • Runtime : 클래스 파일에 포함되고, JVM이 로드해서 리플렉션 API로 참조 가능하다. 
  • Source : 컴파일 때만 사용되고, 클래스 파일에 포함되지 않는다.

2. 어노테이션 생성

문자열과 숫자 타입의 값을 세팅할 수 있는 어노테이션을 만들어 보자

package annotest.demo;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
    String strValue();
    int intValue();
}

 

위의 TestAnnotation을 이용하는 서비스 클래스를 만들어 보자.

package annotest.demo;

public class MyService {
    @TestAnnotation(strValue = "hello", intValue = 1234)
    public void printSomething() {
        System.out.println("test my annotation");
    }
}

TestAnnotation@Target METHOD로 설정하였기에 MyService의 printSomething 메서드에 해당 어노테이션을 선언한다. 그럼 해당 어노테이션을 실행해 값을 확인해 보자.

package annotest.demo;

import java.lang.reflect.Method;

public class AnnotationApp {
    public static void main(String ar[]) throws Exception {
        //TestService 클래스에서 메소드 들을 가져온다
        Method[] methods = Class.forName(TestService.class.getName()).getMethods();

        for (int i = 0; i < methods.length; i++) {
            //TestAnnotation이 존재하는지 확인
            if (methods[i].isAnnotationPresent(TestAnnotation.class)) {
                TestAnnotation an = methods[i].getAnnotation(TestAnnotation.class);
                System.out.println("str:" + an.strValue());
                System.out.println("int:" + an.intValue());
            }
        }
    }
}


3. 스프링 부트 어노테이션

1) ImportSelector

java로 작성된 많은 설정 클래스들이 있는데 이런 설정 클래스들이 어노테이션의 값에 따라서 로딩 여부가 결정되는데, 이럴 때 사용되는 게 ImportSelector 인터페이스이다.  간단한 사용예를 만들어 보자.

 

동작을 확인하기 위한 msg 필드를 가지고 있는 MyBean 클래스

package annotest.demo.importSelector;

class MyBean {
    private String msg;
    public MyBean(String msg) {
        this.msg = msg;
    }
    public String getMsg() {
        return msg;
    }
}

 

MyBean 클래스를 사용하는 UseMyBean 클래스

package annotest.demo.importSelector;

import org.springframework.beans.factory.annotation.Autowired;

public class UseMyBean {
    @Autowired
    private MyBean myBean;
    public void printMsg() {
        System.out.println(myBean.getMsg());
    }
}

UseMyBeanprintMsg 메서드를 통하여 myBean의 메세지 내용을 출력한다. 위에서 myBeanAutowired로 의존주입 해주기 위해선 myBeanbean으로 등록해주어야 한다.

 

AConfigBConfig 클래스는 각각 다른 메시지 값으로 MyBean 클래스를 빈으로 등록해준다.

package annotest.demo.importSelector;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AConfig {
    @Bean
    MyBean myBean() {
        return new MyBean("from A");
    }
}
package annotest.demo.importSelector;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class BConfig {
    @Bean
    MyBean myBean() {
        return new MyBean("from B");
    }
}

어노테이션 값에 따라서 AConfigBConfig를 선택하는 MyImportSelector

package annotest.demo.importSelector;

import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;

//ImportSelector 를 상속받아 selectImports를 구현
public class MyImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        AnnotationAttributes attr = AnnotationAttributes.fromMap(
                importingClassMetadata.getAnnotationAttributes(EnableAutoMyModule.class.getName(), false));
        String value = attr.getString("value");

        //EnableAutoMyModule 어노테이션의 값이 a면 AConfig 아니면 BConfig
        if ("a".equals(value))
            return new String[]{AConfig.class.getName()};
        else
            return new String[]{BConfig.class.getName()};
    }
}

MyImportSelectorImportSelector 인터페이스를 상속받아서 selectImports 메서드를 구현한 클래스다. 해당 메서드는 @EnableAutoMyModule 어노테이션의 값이 "a"이면 AConfig 클래스를, 아니면 BConfig 클래스의 이름을 리턴한다.

 

@EnableAutoMyModule 어노테이션을 만들자

package annotest.demo.importSelector;

import org.springframework.context.annotation.Import;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MyImportSelector.class)
public @interface EnableAutoMyModule {
    String value() default "";
}
package annotest.demo.importSelector;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableAutoMyModule("a")
public class MainConfig {
    @Bean
    public UseMyBean useMyBean() {
        return new UseMyBean();
    }
}

@EnableAutoMyModule 어노테이션에 a값을 넣어줌으로서 AConfig가 로드 되도록 한다.

 

MainConfig 설정 클래스를 이용해서 UseMyBean 클래스에 printMsg 메서드를 사용하는 클래스를 만들자.

package annotest.demo.importSelector;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ImportSelectApp {
    public static void main(String ar[]) {
        //MainConfig를 context에 등록하고 생신된 context정보로 useMyBean 클래스의 빈 정보를 얻은 후 메세지 출력
        ApplicationContext context =
                new AnnotationConfigApplicationContext(MainConfig.class);
        UseMyBean bean = context.getBean(UseMyBean.class);
        bean.printMsg();
    }
}

 

 

2) Conditional

조건에 따라 자바 설정 클래스를 선택할 수 있게 해주는 어노테이션인데 Condition 인터페이스를 상속받은 클래스들과 같이 사용하는 어노테이션이다. 같은 메서드로 프로퍼티에 따라 다른 클래스가 실행되도록 하는 인터페이스를 만든다.

package annotest.demo.conditional;

public interface MsgBean {
    //java8부터 인터페이스에 default 키워드를 이용해서 몸체가 있는 메서드를 작성할 수 있다.
    default void printMsg(){
        System.out.println("My Bean default is running");
    }
}

환경 변수 키 값을 env로 하고 sitea가 value값으로 입력되었는지 체크하는 SiteAconfigCondition 클래스

package annotest.demo.conditional;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

//환경 변수를 읽어서 판별하는 클래스
public class SiteAConfigCondition implements Condition {

    //환경 변수 키값이 env를 읽어와 값이 sitea이면 true 아니면 false
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return "sitea".equals(context.getEnvironment().getProperty("env", "sitea"));
    }
}

 마찬가지 방식으로 SiteBconfigCondition 클래스를 만든다. 해당 클래스는 값이 siteb인지 체크한다.

package annotest.demo.conditional;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

public class SiteBConfigCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return "siteb".equals(context.getEnvironment().getProperty("env", "siteb"));
    }
}

 

이제 sitea, siteb 조건들을 @Conditional 어노테이션 값으로 사용하는 SiteABeanSiteBBean을 만든다.

package annotest.demo.conditional;

import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;

@Component
@Conditional(SiteAConfigCondition.class)
public class SiteABean implements MsgBean{
    //SiteAConfigCondition의 matches 메소드 반환값이 true이면 동작
    @Override
    public void printMsg() {
        System.out.printf("Site A is working");
    }
}
package annotest.demo.conditional;

import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;

@Component
@Conditional(SiteBConfigCondition.class)
public class SiteBBean implements MsgBean{
    //SiteBConfigCondition의 matches 메소드 반환값이 true이면 동작
    @Override
    public void printMsg() {
        System.out.printf("Site B is working");
    }
}

MsgBean을상속받은 클래스로 matches 메서드 반환 값이 true면 동작한다. 

MsgBean에세 메세지를 출력하는 클래스를 만들어 보자.

package annotest.demo.conditional;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ConditionApp {
    public static void main(String ar[]) {
        Package pack = ConditionApp.class.getPackage();

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.scan(pack.getName());
        context.refresh();
        MsgBean bean = context.getBean(MsgBean.class);
        bean.printMsg();
    }
}

특정 클래스 하나가 빈으로 인식되어야 하는 것이 아니라 같은 타입을 가지고 있는 클래스들이 스프링이 관리해야 할 대상이 되어야 하므로 스캔을 하고 MsgBean을 콘텍스트에 등록한다. 실행하기 전에 env값을 설정해서 전달하여야 한다.

 

VM options을 전달할 때는 -D를 앞에 붙이고 사용하여야 한다. 따라서 env값은 -Denv가 키가 되고 value는 =siteb,=sitea 이런 식으로 입력하여야 한다. 확인을 누르고 실행해보자. 

 

env값에 siteb를 설정하였으므로 SiteBBean이 동작이 되었다.