티스토리 뷰

지난 리플렉션 시간에 이어서 리플렉션을 활용하여

Spring의 DI(Dependency Injection)을 약간 비슷하게 따라 해 보겠습니다.

일단 DI를 쓸 때, 주로 Annotation을 사용합니다.

그래서 이 애노테이션을 활용하여, DI를 구현하려고 합니다.

 

Annotation에 관하여 살펴보겠습니다.

프로젝트는 저번 시간(리플렉션)에 이어서 진행합니다.

package me.whiteship;

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

public @interface MyAnnotation {
}

Annotation을 생성합니다.

package me.whiteship;

@MyAnnotation()
class Book {
    public static String A = "A";

    private String B = "B";

    public Book() {

    }

    public Book(String b) {
        B = b;
    }

    private void c () {
        System.out.println("C");
    }

    public int sum (int left, int right){
        return left + right;
    }
}

저번 시간에 생성한 Book클래스에 Annotation을 적용합니다.

package me.whiteship;

import java.util.Arrays;

public class App 
{
    public static void main( String[] args ) {
	// getAnnotations(): 상속받은 (@Inherit) 애노테이션까지 조회
	// getDeclaredAnnotations(): 자기 자신에만 붙어있는 애노테이션 조회
        Arrays.stream(Book.class.getAnnotations()).forEach(System.out::println);
    }
}

리플렉션을 이용하여 Book 클래스에 있는 Annotation 조회하여 출력합니다.

출력 값은???

아무것도 출력되지 않습니다.

기본적으로 Annotation은 주석(코멘트)이랑 같은 취급을 받으며, 클래스 파일까지 Annotation 정보가 유지됩니다.

 

지금부터 Annotation에 대해 자세히 살펴보겠습니다.

package me.whiteship;

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

@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
}

@Retention(RetentionPolicy.속성명) : Annotation이 어느 시점까지 유지할지 정하는 Annotation입니다.

  •  속성명
    • SOURCE   : Java Source File에서만 Annotation 정보가 유지됩니다.
    • CLASS      : Class File까지 Annotation 정보가 유지되고, 리플렉션을 이용하여 Annotation 정보를 얻을 수는 없습니다.
    • RUNTIME : Class File 및 RunTime까지 Annotation 정보가 유지되고,
                         리플렉션을 이용하여 Annotation 정보를 얻을 수 있습니다. 

  - Annotation 생성 시 @Retention이 없는 경우에는 기본값으로 @Rentention(RetentionPolicy.CLASS) 입니다.

 

package me.whiteship;

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, ElementType.FIELD})
public @interface MyAnnotation {
}

@Target(ElementType.속성) : 해당 Annotation을 적용할 대상을 정하는 Annotation입니다.

  • 속성명
package me.whiteship;

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, ElementType.FIELD})
@Inherited 
public @interface MyAnnotation {

    String value() default "minkyu";

    //int number() default 100;
 
    //Class book() default Book.class;
}

@Ingerited : 상속할 경우 서브 클래스에도 Annotation이 적용되게 할 경우 이 Annotation을 씁니다.

package me.whiteship;

@MyAnnotation("minkyu")
//@MyAnnotation(value = "minkyu", number = 200, book = MyBook.class)
public class Book {

....
..

Annotation에서 변수 선언할 때, default 값을 안 주고,

해당 Annotation을 사용한 대상에서 초기값을 선언을 안 하면 컴파일 에러가 발생합니다.

변수명이 value일 경우 @MyAnnotation("minkyu") 바로 초기값 선언이 가능합니다.

변수 타입은 기본형 타입, String, Enum, Annotation, Class만 허용됩니다.

이상 Annotation에 관하여 살펴보았습니다.

이제 Annotation을 적용하면, 리플렉션을 활용하여 인스턴스가 생성되는 간단한 프로젝트를 만들어 보겠습니다.

package me.whiteship.di;

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

@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {

}

리플렉션을 활용하기 때문에 Annotation이 Runtime까지 유지되어야 합니다.

package me.whiteship.di;

import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;

public class ContainerService {

    public static <T> T getObject(Class<T> classType ) {
        T instance = createInstance(classType);
        Arrays.stream(classType.getDeclaredFields()).forEach(field -> {
            if (field.getAnnotation(Inject.class) != null) {
                Object fieldInstance = createInstance(field.getType());
                field.setAccessible(true);
                try {
                    field.set(instance, fieldInstance);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                }

            }
        });
        return instance;
    }

    private static <T> T createInstance(Class<T> classType) {
        try {
            return classType.getConstructor(null).newInstance();
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }
}

createInstance Method

Class<T> 타입에 대한 인스턴스를 생성합니다.

 

getObject Method

매개변수 Class<T>에 대한 인스턴스를 생성(createInstance)하고,

해당 T 타입의 모든 변수에 Inject Annotation이 적용되어 있는지 확인하여

적용이 되어 있는 경우에 해당 변수에 변수 타입의 인스턴스를 생성하여 주입합니다.

모든 변수가 확인이 되었으면, 리턴 값으로 매개변수 타입에 대한 인스턴스를 반환합니다.

 

제너릭 메소드

<T> T getObect(Class<T> classType) 

제네릭으로 매개변수 Class타입으로 return 한다는 내용입니다.

모든 클래스는 Class<T> Instance가 생깁니다.

자세한 내용은 https://devlog-wjdrbs96.tistory.com/201 참고 부탁드립니다.

 

package me.whiteship.di;

public class BookRepository {
}
package me.whiteship.di;

public class BookService {

    @Inject
    BookRepository bookRepository;
}
package me.whiteship.di;

import org.junit.Test;

import static org.junit.Assert.assertNotNull;

public class ContainerServiceTest {

    @Test
    public void getObject_BookRepository() {
        BookRepository bookRepository = ContainerService.getObject(BookRepository.class);
        assertNotNull(bookRepository);
    }

    @Test
    public void getObject_BookService() {
        BookService bookService = ContainerService.getObject(BookService.class);
        assertNotNull(bookService);
        assertNotNull(bookService.bookRepository);
    }
}

 

getObject_BookRepository 메소드는 ContainerService 클래스의 getOjbect를 활용하여

인스턴스를 생성하여 변수에 주입하고 해당 변수가 null값이 아닌지 확인하는 테스트 코드입니다.

 

getObject_BookService 메소드는 위와 비슷하지만,

BookService에 전역 변수 bookRepository에 Inject Annotation이 적용되어 있어

bookRepository도 인스턴스가 생성되어 주입되어 있는지 확인하는 테스트 코드입니다.

 

두 개의 메소드 테스트 결과는 당연히 성공입니다.

이렇게 리플렉션을 살펴보았습니다.

리플렉션의 기능들이 막강하지만, 주의할 점도 있습니다.

  • 지나친 사용은 성능 이슈를 야기할 수 있습니다. 
  • 컴파일 시점에 확인되지 않고 런타임 시에만 발생하는 문제를 만들 가능성이 있습니다.
  • 접근 지시자를 무시할 수 있습니다.

리플렉션 사용처는

스프링에서 의존성 주입, MVC View에서 넘어온 데이터를 객체에 바인딩 할때

하이버네이트 @Entity 클래스에 Setter가 없다면 리플렉션을 사용합니다.

 

이로써 공부한 내용을 간략히 정리해보았습니다. 

감사합니다.

 


출처

https://www.inflearn.com/course/the-java-code-manipulation