스프링의 Depedency Injection은 어떻게 동작할까?
@Repository
public class BookRepository {
}
@Service
public class BookService {
public final BookRepository bookRepository;
public BookService(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
}
@SpringBootTest
class BookServiceTest {
@Autowired
BookService bookService;
@Test
public void diTest(){
assertThat(bookService).isNotNull();
assertThat(bookService.bookRepository).isNotNull();
}
}
bookRepository 인스턴스는 어떻게 null이 아닌걸까?
스프링은 어떻게 BookService 인스턴스에 BookRepository 인스턴스를 넣어준 것일까? → 리플렉션
리플렉션 API1 : 클래스 정보 조회
public interface MyInterface {
}
public class Book {
private String A = "a";
private static String B = "BOOK";
private static final String C = "BOOK";
public String D = "d";
protected String E = "e";
public Book(){}
public Book(String a, String d, String e){
this.A = a;
this.D = d;
this.E = e;
}
private void f(){
System.out.println("F");
}
public void g(){
System.out.println("g");
}
public int h(){
return 100;
}
}
public class MyBook extends Book implements MyInterface{
}
Class<?> 인스턴스를 가져오는 방법
public class App {
public static void main(String[] args) throws ClassNotFoundException{
Class<Book> bookClass = Book.class;
Book book = new Book();
Class<? extends Book> bookClass2 = book.getClass();
// -> ClassNotFoundException 발생 가능
Class<?> bookClass3 = Class.forName("com.example.springdi.Book");
}
}
총 3가지 방식으로 가져올 수 있다.
- Class 타입
- 특정 Classw 인스턴스.getClass()
- Class.forName(FQCN)
FQCN( Fully Qualified Class Name ): 패키지 포함 클래스 전체 이름
만약 해당 이름으로 없으면 ClassNotFoundException이 발생한다.
이 Class 객체는 애플리케이션 로딩이 끝난 시점에 힙 영역에 저장한다.
이 클래스(Class) 객체는 클래스의 메타데이터를 관리하며, 클래스의 인스턴스를 생성하거나 리플렉션(Reflection) 같은 메커니즘을 통해 클래스 정보를 얻을 때 사용된다.
필드(변수)를 가져오는 방법
public class App {
public static void main(String[] args) throws ClassNotFoundException{
Class<Book> bookClass = Book.class;
Field[] fields = bookClass.getFields(); // 접근 제어자 public field only
Arrays.stream(fields).forEach(System.out::println);
Field[] declaredFields = bookClass.getDeclaredFields();
Arrays.stream(declaredFields).forEach(System.out::println);
}
}
// 출력
public java.lang.String com.example.springdi.Book.D
private java.lang.String com.example.springdi.Book.A
private static java.lang.String com.example.springdi.Book.B
private static final java.lang.String com.example.springdi.Book.C
public java.lang.String com.example.springdi.Book.D
protected java.lang.String com.example.springdi.Book.E
getFields(): 접근 제어자가 public인 변수만 가져온다.
getDeclaredFields(): 접근 제어자 상관없이 모든 변수를 가져온다.
두 메소드의 반환 타입은 Field [] 배열이다.
public class App {
public static void main(String[] args) throws ClassNotFoundException{
Class<Book> bookClass = Book.class;
Book book = new Book();
Arrays.stream(bookClass.getDeclaredFields()).forEach(f -> {
try {
System.out.println(f + " " + f.get(book));
}
catch (IllegalAccessException e){
e.printStackTrace();
}
});
}
}
// 출력 java.lang.IllegalAccessException
Field.get(object)를 하면 IllegalAccessException이 3번 발생한다.
private String A = "a";
private static String B = "BOOK";
private static final String C = "BOOK";
public String D = "d";
protected String E = "e";
IllegalAccessException 이유: Book의 Fields(인스턴스 변수/클래스 변수) 접근 제어자가 private인 필드가 3개 있어서
setAccessible(booean b)
public class App {
public static void main(String[] args) throws ClassNotFoundException{
Class<Book> bookClass = Book.class;
Book book = new Book();
Arrays.stream(bookClass.getDeclaredFields()).forEach(f -> {
try {
f.setAccessible(true);
System.out.println(f + " " + f.get(book));
}
catch (IllegalAccessException e){
e.printStackTrace();
}
});
}
}
// 출력
private java.lang.String com.example.springdi.Book.A a
private static java.lang.String com.example.springdi.Book.B BOOK
private static final java.lang.String com.example.springdi.Book.C BOOK
public java.lang.String com.example.springdi.Book.D d
protected java.lang.String com.example.springdi.Book.E e
f.setAccessible(true)를 주어서 접근 가능하게 만들었다.
IllegalAccessException가 발생하지 않고 모두 출력 되는 걸 확인할 수 있었다.
이렇게 리플렉션을 활용하면 접근 제어를 우회할 수 있다.
modifers 확인하기
// modifiers 확인하기
Arrays.stream(bookClass.getDeclaredFields()).forEach(f -> {
int modifiers = f.getModifiers();
System.out.println(f);
System.out.println(Modifier.isPrivate(modifiers));
System.out.println(Modifier.isStatic(modifiers));
});
private java.lang.String com.example.springdi.Book.A
true
false
private static java.lang.String com.example.springdi.Book.B
true
true
private static final java.lang.String com.example.springdi.Book.C
true
true
public java.lang.String com.example.springdi.Book.D
false
false
protected java.lang.String com.example.springdi.Book.E
false
false
메소드, 생성자... 모두 getModifiers() 가능하다.
java.lang.reflect.Modifier
메소드 가져오기
public class App {
public static void main(String[] args) throws ClassNotFoundException{
Class<Book> bookClass = Book.class;
Arrays.stream(bookClass.getMethods()).forEach(System.out::println);
}
}
// 출력
public int com.example.springdi.Book.h()
public void com.example.springdi.Book.g()
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public java.lang.String java.lang.Object.toString()
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()
현재 Book 클래스에 정의된 메소드를 출력했다.
보면 부모 클래스(Object)에 정의된 메소드들도 함께 출력되는 걸 확인할 수 있다.
해당 메소드는 접근제어자가 public인 메소드만 가져온다.
만약 현재 클래스에 정의된 메소드만 가져오고 싶다면, getDeclaredMethods()를 사용해야 한다.
getDeclaredMethods()는 접근 제어자와 상관없이 현재 클래스에 정의된 모든 메소드를 반환한다.
생성자 가져오기
// 생성자 출력
Arrays.stream(bookClass.getDeclaredConstructors()).forEach(System.out::println);
public com.example.springdi.Book()
public com.example.springdi.Book(java.lang.String,java.lang.String,java.lang.String)
부모 클래스 가져오기
// 부모 클래스 출력
System.out.println(MyBook.class.getSuperclass());
// 출력
class com.example.springdi.Book
Class<?> 타입으로 가져온다.
인터페이스 가져오기
// 인터페이스 가져오기
Arrays.stream(MyBook.class.getInterfaces()).forEach(System.out::println);
// 출력
interface com.example.springdi.MyInterface
애노테이션과 리플렉션
애노테이션은 주석과 동일하다.
메타스페이스
JVM에서 클래스 메타데이터(클래스 정보)를 저장하는 영역이다.
클래스 로딩 시 JVM은 클래스 정보뿐만 아니라 클래스의 구조, 필드, 메소드, 애노테이션 같은 메타정보를 메타스페이스에 저장한다.
힙
실제 객체가 저장되는 곳. 애노테이션 인스턴스(런타임에서 사용 가능한 애노테이션 객체)는 힙에 저장된다.
리플렉션을 통해서 애노테이션 정보를 가져올 때 JVM은 메타스페이스에서 클래스와 애노테이션의 메타데이터를 참조한다. 실제로 리플렉션 API가 반환하는 애노테이션 객체들은 힙에 위치한다. 이때 메타스페이스에 저장된 애노테이션 정보를 기반으로 힙에서 해당 애노테이션 객체가 반환된다.
@Retention
@Retention(RetentionPolicy.SOURCE): 소스 코드에서만 유지, 컴파일 시 제거
@Retention(RetentionPolicy.CLASS): 컴파일된 .class 파일에 포함되지만, JVM에서 읽지 않음 (default 값)
@Retention(RetentionPolicy.RUNTIME): 런타임까지 유지되어 메타스페이스에서 접근 가능
애노테이션 정리 내용
https://20240228.tistory.com/312
애노테이션 정의
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface MyAnnotation {
}
애노테이션 붙이기
@MyAnnotation
public class Book {
// 생략 ..
}
애노테이션 조회하기
Arrays.stream(Book.class.getAnnotations()).forEach(System.out::println);
출력 결과
@com.example.springdi.MyAnnotation()
getAnnotations(): 상속받은(@Inherited) 애노테이션까지 조회
geDeclaredtAnnotations(): 자기 자신에만 붙어있는 애노테이션만 조회
리플렉션 API2 : 클래스 정보 수정 / 실행
기본 생성자 (파라미터가 없는 생성자)
Book 클래스
public class Book {
public static String A = "A";
private String B = "B";
public Book() {}
public Book(String b){
B = b;
}
public void c(){
System.out.println("C");
}
public int sum(int left, int right){
return left + right;
}
}
Book 클래스 기본 생성자를 통해서 Book 인스턴스 생성하기
public class App {
public static void main(String[] args) throws Exception{
Class<Book> bookClass = Book.class;
Constructor<Book> constructor = bookClass.getConstructor();
Book book = constructor.newInstance();
System.out.println(book);
}
}
출력
com.example.springdi.Book@6ce253f1
기본 생성자를 생성할 때 getConstructor() API를 사용한다.
이때 매개변수를 파라미터 타입으로 넘겨준다. (매개변수가 없으면 null or 빈 값)
파라미터가 있는 생성자
파라미터가 있는 생성자
public class App {
public static void main(String[] args) throws Exception{
Class<Book> bookClass = Book.class;
Constructor<Book> constructor = bookClass.getConstructor(String.class);
Book book = constructor.newInstance("myBook");
System.out.println(book);
}
}
출력
com.example.springdi.Book@53d8d10a
static 필드
static 필드 가져오기
public class App {
public static void main(String[] args) throws Exception{
Field a = Book.class.getDeclaredField("A");
System.out.println(a.get(null));
}
}
출력
A
static 필드는 특정 object만 있는게 아니라 모든 Book 인스턴스에 존재한다.
그래서 Book 인스턴스를 넘기지 않아도 존재한다.
static 필드 수정
static 필드 수정
public class App {
public static void main(String[] args) throws Exception{
// static 필드 가져오기
Field a = Book.class.getDeclaredField("A");
System.out.println(a.get(null));
// static 필드 수정
a.set(null, "AAAAAA");
System.out.println(a.get(null));
}
}
출력
A
AAAAAA
인스턴스 변수(멤버 변수)
인스턴스 변수
public class App {
public static void main(String[] args) throws Exception{
Class<Book> bookClass = Book.class;
Constructor<Book> constructor = bookClass.getConstructor();
Book book = constructor.newInstance();
Field b = Book.class.getDeclaredField("B");
b.setAccessible(true);
System.out.println(b.get(book));
}
}
출력
B
필드 이름 B는 접근 제어자가 private인 인스턴스 변수이다.
인스턴스 변수는 각 객체마다 고유하게 가지고 있기 때문에
get을 통해서 가져오려면 매개변수로 인스턴스를 넘겨야 한다.
또한 가져오기 전에 setAccessible(true)를 통해서 접근 권한을 허용해야지 가져올 수 있다.
만약 해당 과정이 생략되고 접근 제어자가 private이면 IllegalAccessException이 발생한다.
인스턴스 변수 수정(멤버 변수)
인스턴스 변수 수정
public class App {
public static void main(String[] args) throws Exception{
Class<Book> bookClass = Book.class;
Constructor<Book> constructor = bookClass.getConstructor();
Book book = constructor.newInstance();
Field b = Book.class.getDeclaredField("B");
b.setAccessible(true);
System.out.println(b.get(book));
b.set(book, "BBBBB");
System.out.println(b.get(book));
}
}
출력
B
BBBBB
메소드 호출
// 반환타입과 매개변수가 없는 인스턴스 메소드
Method c = Book.class.getDeclaredMethod("c");
c.invoke(book); // 메소드
// 반환타입과 매개변수가 있는 인스턴스 메소드
Method sum = Book.class.getDeclaredMethod("sum", int.class, int.class);
int result = (int) sum.invoke(book, 10, 20);
System.out.println(result); // 10 + 20 -> 30
출력
C
30
마찬가지로 접근 제어자가 현재 위치에서 접근 불가능한 위치라면 setAccessible(true) 설정이 필요하다.
지금 호출하는 메소드들은 모두 접근 제어자가 public
메소드 호출은 invoke() API를 통해서 한다.
나만의 DI 프레임워크 만들기
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {
}
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){
f.setAccessible(true);
try {
f.set(instance, createInstance(f.getType()));
}
catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
});
return instance;
}
private static <T> T createInstance(Class<T> classType){
try {
return classType.getConstructor().newInstance();
}
catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e){
throw new RuntimeException(e);
}
}
}
@Inject 애노테이션과 ContainerService 이다.
스프링 컨테이너와 같이 DI 작업을 해주는 ContainerService 클래스이다.
ContainerService의 getObject(Class<?> classType)을 넘겨주면 리플렉션을 활용하여
인스턴스를 생성해서 반환해 준다.
또한 인스턴스를 생성해서 반환할 때 클래스 내부 필드에 @Inject 애노테이션이 붙어 있으면
해당 필드의 인스턴스 또한 생성해서 필드에 set 해준다.
물론 스프링 프레임워크(스프링 컨테이너)와 달리 싱글톤도 아니고 순환 참조에 대한 방지도 없지만
리플렉션을 활용하여 이렇게 DI(Dependency Injection)이 가능하다를 연습해 보았다.
public class BookRepository {
}
public class BookRepository2 {
}
public class BookService {
@Inject
BookRepository bookRepository;
BookRepository2 bookRepository2;
}
테스트 코드
class ContainerServiceTest {
@Test
public void getObject_BookRepository(){
BookRepository bookRepository = ContainerService.getObject(BookRepository.class);
assertThat(bookRepository).isNotNull();
}
@Test
public void getObject_BookService(){
BookService bookService = ContainerService.getObject(BookService.class);
assertThat(bookService).isNotNull();
assertThat(bookService.bookRepository).isNotNull();
assertThat(bookService.bookRepositor2).isNull();
}
}
BookService를 보면 2개의 필드를 가진다.
BookRepository: @Inject 애노테이션 O
BookRepository2: @Inject 애노테이션 X
BookService 인스턴스를 getObject()를 사용하여 생성해서 반환받아오면
BookRepository 타입의 필드는 null이 아니지만 BookRepository2 타입의 필드는 null이다.
리플렉션을 활용해서 필드에 값을 수정할 때 @Inject가 붙은 필드만 인스턴스를 생성하여 넣어주었기 때문이다.
리플렉션 정리
리플렉션 사용시 주의할 것
- 지나친 사용은 성능 이슈를 야기할 수 있다. 반드시 필요한 경우에만 사용할 것
- 컴파일 타임에 확인되지 않고 런타임 시에만 발생하는 문제를 만들 가능성이 있다.
- 접근 지시자를 무시할 수 있다.
스프링에서 사용
- 의존성 주입
- MVC 뷰에서 넘어온 데이터를 객체에 바인딩 할 때
하이버네이트
- @Entity 클래스에 Setter가 없다면 리플렉션을 사용한다.
JUnit
참고
참고 자료
인프런 백기선님 더 자바, 코드를 조작하는 다양한 방법
'Computer Sience > Java' 카테고리의 다른 글
[Effective Java] 아이템1: 생성자 대신 정적 팩터리 메소드를 고려해라 (0) | 2024.10.21 |
---|---|
[JAVA] 다이나믹 프록시 (5) | 2024.10.16 |
[JAVA] 바이트 코드 조작 (2) | 2024.10.10 |
[JAVA] JVM 전체 구조 정리 (1) | 2024.10.08 |
[JAVA] 자바, JVM, JDK, JRE (0) | 2024.10.08 |