스프링 Dependency Injection은 어떻게 동작할까?
우리는 스프링(or 스프링 부트)를 사용하면서 숨쉬듯이 어노테이션들을 사용해서 DI(Dependency Injection)를 해서 각종 서비스, 레파지토리들을 주입받아서 사용합니다.
@Autowired BookService bookService;
Java
복사
정말 간단한 Serivce, Repository를 구현해서 DI를 해봅시다.
1. BookService.java
@Service
public class BookService {
@Autowired
BookRepository bookRepository;
}
Java
복사
2. BookRepository.java
@Repository
public class BookRepository {
}
Java
복사
3. DI 확인 테스트 코드 작성 - BookServiceTest.java
@SpringBootTest
class BookServiceTest {
@Autowired
BookService bookService;
@Test
public void diTest() throws Exception{
assertThat(bookService).isNotNull();
assertThat(bookService.bookRepository).isNotNull();
}
}
Java
복사
위 테스트코드를 주입하면 테스트는 정상적으로 통과가 됩니다. 따로 생성자를 작성해주지도 않았는데 어떻게
bookService와 bookRepository 는 null이 아닌것일까요 어느 과정에서 인스턴스들이 주입된 것일까요
리플렉션 API 1부 - 클래스 정보 조회
참고: 리플렉션이란 객체를 통해 클래스의 정보를 분석해내는 프로그램 기법
Java에서는 Reflection 을 이용해 실행중인 자바 프로그램 내부의 클래스,필드,메서드의 속성을 조회,수정할 수 있습니다. 그리고 스프링에서는 런타임시에 개발자가 등록한 빈(Bean)을 애플리케이션에서 가져와 사용할 수 있게 됩니다.
리플렉션 기능은 Class<T> API를 사용하게 되는데, 클래스에 있는 필드, 인터페이스, 메서드 목록등에 접근할 수 있습니다.
좀 더 자세한 내용은 아래 링크를 참고하시면 됩니다.
1. Class<T>에 접근하는 방법
•
사전준비 - 테스트에 사용할 Book, MyBook, MyInterface 작성
Book
MyBook
MyInterface
•
모든 클래스를 로딩 한 다음 Class<T>의 인스턴스가 생긴 뒤 힙(heap) 에 저장됩니다.
Class<Book> bookClass = Book.class;
Java
복사
•
모든 인스턴스는 getClass() 메서드를 가지고 있습니다. 그렇기에 해당 메서드를 통해 접근할 수 있습니다.
Book book = new Book();
Class<? extends Book> bookClass1 = book.getClass();
Java
복사
•
클래스를 문자열로 읽어 올 수 있습니다. → 다만, 문자열은 FQCN이어야 합니다.
Class<?> aClass = Class.forName("me.hansol.Book");
Java
복사
⇒ 만약 경로가 잘못되었다면 ClassNotFoundException이 발생합니다.
2. Class<T>를 통해 할 수 있는 것.
•
필드(목록) 가져오기
Class<Book> bookClass = Book.class;
Book book = new Book();
Arrays.stream(bookClass.getFields()).forEach(System.out::println); // public한 field만 가져옵니다.
try {
Field 필드명 = bookClass.getDeclaredField("필드명");
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
Arrays.stream(bookClass.getDeclaredFields()).forEach(System.out::println);//모든 field를 가져옵니다.
Arrays.stream(bookClass.getDeclaredFields()).forEach(f ->{
try {
f.setAccessible(true);
System.out.printf("%s %s\n", f, f.get(book));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
});
Java
복사
•
메소드(목록) 가져오기
Class<Book> bookClass = Book.class;
Arrays.stream(bookClass.getDeclaredMethods()).forEach(System.out::println);
Java
복사
•
상위 클래스 가져오기
Class<? super MyBook> superclass = MyBook.class.getSuperclass();
System.out.println(superclass); //class me.hansol.Book
Java
복사
•
인터페이스 (목록) 가져오기
Arrays.stream(MyBook.class.getInterfaces()).forEach(System.out::println);
Java
복사
•
생성자 가져오기
Arrays.stream(bookClass.getDeclaredConstructors()).forEach(System.out::println);
Java
복사
•
ETC
◦
Modifier의 static method 사용해보기
Arrays.stream(Book.class.getDeclaredFields()).forEach(f->{
int modifiers = f.getModifiers();
System.out.println(f);
System.out.println(Modifier.isPrivate(modifiers)); //return type is Boolean
System.out.println(Modifier.isPublic(modifiers)); //return type is Boolean
});
Java
복사
애노테이션과 리플렉션
public @interface MyAnnotation {
String value() default "hansol";
int number() default 100;
}
@MyAnnotation
public class Book {
...
}
public static void main(String[] args) throws ClassNotFoundException {
Arrays.stream(MyBook.class.getAnnotations()).forEach(System.out::println);
}
Java
복사
Java에서는 클래스, 인터페이스 뿐 아니라 애노테이션 역시 위와같이 간단하게 만들수 있습니다.
그리고 이렇게 만든 애노테이션은 클래스나 메서드, 필드에서도 사용이 가능합니다. 리플렉션 기능을 이용해서 애노테이션의 정보나 속성들을 읽을수도 있습니다. 하지만 위에서 작성한 예제대로 getAnnotations()를 호출해 출력하면 아무것도 안나옵니다. 그 이유는 기본적으로 애노테이션은 주석과 같지만 기능이 조금 더 있는 수준인데, 이 정보들이 기본적으로 클래스나 소스까진 남지만, 바이트코드를 로딩했을 때 메모리에는 애노테이션을 생략하고 읽어오기 때문에 애노테이션 정보가 출력되지 않습니다.
이러한 부분을 해결하고 메모리에도 애노테이션을 적재하고싶다면, @Retention어노테이션을 사용해주는데 default값으로 RetentionPolicy.CLASS로 되어있기 때문에 해당 전략을 RetiontionPolicy.RUNTIME으로 바꿔준다면 런타임시에도 읽어올 수 있게 됩니다. 그리고 애노테이션을 무분별하게 붙히는게 아닌 제약을 둘 수 있는데 @Target 애노테이션을 사용하며 허용하는 타입들을 지정할 수 있습니다.
그 뿐 아니라 자손클래스에서 부모클래스의 애노테이션 정보를 얻을 수도 있는데 @Inherited 애노테이션을 붙혀주면 됩니다.
Point
1. 애노테이션에서 사용되는 중요 애노테이션
•
@Retention : 해당 애노테이션을 언제까지 유지할 것인지 지정(default RetiontionPolicy.CLASS)
•
@Inherited: 해당 애노테이션을 하위 클래스까지 전달할 것인지 여부
•
@Target: 애노테이션을 사용해도 되는 범위 지정 애노테이션
2. 리플렉션에서 제공하는 애노테이션 관련 메서드
•
getAnnotations(): 상속받은 (@Inherited) 애노테이션까지 조회
•
getDeclaredAnnotations(): 자기자신에게만 붙어있는 애노테이션 조회
3. 애노테이션 속성중 value하나만 지정하는 경우 value는 생략해도 됩니다
public @interface MyAnnotation {
String value() default "hansol";
int number() default 100;
}
@MyAnnotation("123") // 하나만 값을 세팅할경우 key값이 "value"인 경우 생략이 가능하다.
public class Book{
...
}
Java
복사
리플렉션API 2부 - 클래스 정보 수정 또는 실행
리플렉션의 각종 기능을 이용해서 클래스 생성, 정보 수정, 실행까지 해보도록 하겠습니다.
0. 테스트용 클래스 작성
Code
1. 리플렉션의 기능 소개
•
기본생성자를 통한 instance 생성
Class<?> bookClass = Class.forName("me.hansol.Book");
Constructor<?> constructor = bookClass.getConstructor(null);
Book book = (Book) constructor.newInstance();
System.out.println("book = " + book); // book = me.hansol.Book@33c7353a
Java
복사
•
인자값이 있는 생성자를 통한 instance 생성
Constructor<?> constructor1 = bookClass.getConstructor(String.class);
Book book2 = (Book) constructor1.newInstance("hansol");
System.out.println("book2 = " + book2); //book2 = me.hansol.Book@681a9515
Java
복사
◦
getConstructor()의 인자값으로 생성자의 인자값 타입을 작성합니다.
◦
newInstance()의 인자값으로 인자값을 전달합니다.
•
필드값 접근하기 & 수정하기
Field a = Book.class.getDeclaredField("A");
System.out.println(a.get(null)); //A
a.set(null, "123");
System.out.println("a = " + a.get(null)); //123
Field b = Book.class.getDeclaredField("B");
b.setAccessible(true);
System.out.println(b.get(book));
Java
복사
→ Field A는 static 변수(전역변수) 이기 때문에 instance 를 생성하지 않아도 정상적으로 출력이 됩니다.
→ 같은 이유로 변경역시 가능합니다.
→ 전역변수(static)는 Object가 따로 Field.get() 메서드의 인자값으로 들어갈 필요가 없어서 null을 전달하면 됩니다.
→ Field B는 private 변수기 때문에 setAccessible(true) 메서드를 통해 접근 권한을 풀고 접근해야 합니다. 그리고 전역변수(static)가 아니기 때문에 필드에 접근할 때(b.get(book)) 생성된 인스턴스를 인자값으로 전달해야 해당 인스턴스 내부의 Field B에 접근합니다.
•
메소드 실행하기
//파라미터가 없는 메서드 접근
Method c = Book.class.getDeclaredMethod("c");
c.invoke(book);
//파라미터가 있는 메서드 접근
Method d = Book.class.getDeclaredMethod("sum", int.class, int.class);
int invoke = (int) d.invoke(book,1,2);
System.out.println("invoke = " + invoke);
Java
복사
→ 메서드 접근 및 실행은 invoke 메서드를 통해 수행됩니다. 인자값으로는 필드와 동일하게 인스턴스를 전달합니다.
→ 파라미터가 있는 메서드는 메서드를 꺼낼 때 메서드 명 뿐 아니라 인자값의 타입도 전달해줘야 합니다.
→꺼낸 메소드를 invoke로 수행할 때 역시 인스턴스와 함께 인자값들을 전달해줘야 합니다. 반환 타입은 Object 이기 때문에 적당한 클래스로 casting 해줘야 합니다.
나만의 DI 프레임워크 만들기
목표: @Inject 애노테이션을 만들어서 필드 주입해주는 컨테이너 서비스 만들기
1. Inject.java
package me.hansol.di;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {
}
Java
복사
•
애노테이션은 @Retention 을 RUNTIME 으로 설정해줘야 합니다.
2. ContainerService.java
package me.hansol.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(f->{
if(f.getAnnotation(Inject.class) != null){
Object fieldInstance = createInstance(f.getType());
f.setAccessible(true);
try {
f.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);
}
}
}
Java
복사
•
ContainerService 에서 getObject 메서드는 클래스타입을 인자값으로 받아 instance를 생성 후 그 안에 필드들을 순회하며 getAnnotation()으로 Inject.class가 있는지 탐색 후 탐색이 될 경우 해당 필드에 인스턴스를 생성해 주입시켜줍니다.
4. 해당 프로젝트 install
→ mvn install 명령어로 ContainerService를 설치해줍니다.
5. 생성한 라이브러리 사용 - 신규 프로젝트 생성
•
의존성 추가
<dependency>
<groupId>me.hansol</groupId>
<artifactId>refactoring-example</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
XML
복사
•
테스트용 Service, Repository 구현
/* AccountRepository */
public class AccountRepository {
public void save(){
System.out.println("Repo.save");
}
}
/* AccountService */
public class AccountService {
@Inject
AccountRepository accountRepository;
public void join(){
System.out.println("Service.join");
accountRepository.save();
}
}
Java
복사
•
사용하기
AccountService accountService = ContainerService.getObject(AccountService.class);
accountService.join();
/* 실행결과 */
//Service.join
//Repo.save
Java
복사
리플렉션 정리
1. 리플렉션 사용시 주의점
•
지나친 사용은 성능 이슈를 야기할 수 있기에 반드시 필요할 때만 사용해야 합니다.
•
컴파일 타임에 확인되지 않고 런타임 시에만 발생하는 문제를 만들 가능성이 있다.
•
접근 지시자를 무시할 수 있다.
2. 스프링 활용
•
의존성 주입
•
MVC 뷰에서 넘어온 데이터를 객체에 바인딩 할 때 사용된다.
3. 하이버네이트
•
@Entity 클래스에 Setter가 없다면 리플렉션을 사용한다.