애너테이션 기반 DI 직접 해보자 with 리플렉션

노력을 즐기는 사람·2021년 11월 20일
2
post-thumbnail

서론

최근 이펙티브 자바를 읽고 있습니다.
아이템 65 리플렉션 보다는 인터페이스를 사용하라 읽었는데요.
아이템 65를 읽으면서 리플렉션에 관심을 가지게 되었습니다.

리플렉션이라는 녀석을 듣기는 많이 들었는데 뭐하는 녀석인지 몰랐습니다.
흑마법을 부릴 수 있고 잘못쓰면 성능 저하를 야기한다 정도만 알고있었습니다.
그리고 Spring Framework 에서는 리플렉션이 여기저기 사용되고 있다고 들었습니다.
그래서 이 참에 리플렉션을 공부해보자~ 는 마음을 먹고 케케묵은 강의를 꺼내 들었습니다.

바로바로 백기선님의 더 자바, 코드를 조작하는 다양한 방법 강의입니다.
강의 내용 중 나만의 DI 프레임워크를 만들어보자! 라는 내용이 있는데요.
이번 포스팅에서는 백기선님이 제시한 DI 프레임워크 만들기를 구현해보도록 하겠습니다.
(제가 작성할 코드는 백기선님이 강의에서 제시한 코드는 아닙니다)

이제 시작하겠습니다!

리플렉션이 뭘까?

자바에서 리플렉션은 java.lang.reflect 에 정의되어 있는데요.
Oracle에서 제공하는 java docs 에서는 리플렉션을 이렇게 소개하고 있습니다.

Provides classes and interfaces for obtaining reflective information about classes and objects.

간단히 말해서 클래스나 객체의 정보를 얻어내는 기능을 제공한다는 뜻입니다.

여기서 클래스나 객체의 정보는 무엇을 말하는걸까요?
그 아래 문장을 읽어보면 알 수 있습니다.

Reflection allows programmatic access to information about the fields, methods and constructors of loaded classes, and the use of reflected fields, methods, and constructors to operate on their underlying counterparts, within security restrictions.

또 간단히 말해보겠습니다. 리플렉션과 함께 한다면 클래스에 정의되어 있는 필드, 메서드, 생성자, 애너테이션, 클래스 간의 관계 등 모두 알아 낼 수 있습니다.

리플렉션은 정보를 알아내는 것 뿐만 아니라 알아낸 녀석들을 조작할 수 있습니다.
그러니까 클래스가 가진 메서드를 호출해버릴 수 있습니다.
게다가 생성자를 호출해서 객체를 생성할 수도 있습니다.

자.. 이제 포스팅 주제를 다시 살펴볼까요?
저는 리플렉션으로 DI 프레임워크를 만들어보려고 합니다.
그리고 리플렉션은 다른 클래스의 메서드, 생성자를 호출 할 수 있습니다.

이제 제가 무엇을 할지 감이 오시나요?
그럴리가요. 감이 안오실 것 같아서 리플렉션 예제를 준비해봤습니다.

Reflection Play Ground

이 섹션에서는 리플렉션으로 할 수 있는 것들을 보여드리려고 합니다.
어떻게 동작하는지 보다는 어떤 동작을 하는지에 집중해주시면 좋겠습니다.

리플렉션으로 필드 정보 조회하기

리플렉션은 클래스에 정의된 필드의 정보들을 확인할 수 있습니다.

먼저 이런 클래스를 정의하겠습니다.

public class ParentBook {
    private String d = "PUBLIC";
    public static String e = "PUBLIC STATIC";
}

ParentBook 에는 두 개의 변수가 정의된 것을 볼 수 있습니다.
이제 리플렉션으로 정의된 두 변수의 정보를 알아내보겠습니다. 테스트 코드를 보시죠.

이런 테스트 코드를 작성했습니다.
여기서 주목할 점은 Class<ParentBook> 입니다.
이 녀석은 java.lang 패키지에 포함되어 있는 클래스입니다.
주로 이 녀석이 시발점이 되어서 리플렉션을 사용합니다.

이 녀석의 getDeclaredField() 메서드를 호출하면 Field 객체를 알아 낼 수 있습니다.

위의 테스트 코드는 이렇게 잘 통과하는 모습을 볼 수 있습니다.

테스트 코드를 통해 알게된 사실은 다음과 같습니다.

  1. 필드의 접근제어자를 알 수 있다.
  2. 필드의 타입을 알 수 있다.
  3. 필드의 Fully Qualified Name을 알 수 있다.

리플렉션으로 메서드 호출하기

이번엔 리플렉션으로 메서드를 호출해보겠습니다.
메서드 호출 외에도 생성자 호출 등 여러 조작법이 있습니다.
대표로 메서드 호출 예제를 보여드리겠습니다.

이번에도 ParentBook 클래스부터 살펴보겠습니다. 이번에는 h() 메서드가 정의되어 있습니다.

public class ParentBook {
    public String h() {
        return "METHOD";
    }
}

h() 메서드는 "METHOD" 문자열을 리턴하는 아주 간단한 메서드입니다.
자 이제 테스트 코드를 보겠습니다.

첫번째 밑줄에서는 리플렉션을 통해 메서드의 정보를 알아내고 있습니다.
두번째 밑줄에서는 리플렉션을 통해 메서드를 호출하고 있습니다.
즉, actual 변수에는 h()의 리턴 값인 "METHOD" 가 저장될 것입니다.

자 이제 테스트 코드를 실행하면~

잘 통과하네요! 성공!

이번 테스트 코드에서는 이런 사실을 알게 되었습니다.

  1. 리플렉션은 다른 코드를 실행 시킬 수 있다.

자.. 이제 리플렉션을 어떻게 활용할지 느낌이 오시나요?
그렇다면 다행입니다!

이제 DI가 뭔지 아주 간단하게 알아볼 차례입니다.

Spring은 어떻게 DI 하고 있을까?

Spring은 XML 방식과 Java 코드 방식으로 DI를 지원하고 있습니다.
저는 Java 코드 방식 중 Annotation 기반의 DI를 많이 사용하고 있습니다.
이 포스팅에서도 Annotation 기반의 DI 프레임워크를 만들어보려고 합니다.

일단 Spring의 DI를 이해하기 위해서는 Bean을 반드시 알아야합니다.
Bean을 간단히 살펴보겠습니다.

Bean

아시다시피 Spring에서는 Bean 이라는 것을 사용합니다.
일반적으로 애플리케이션 내에서 글로벌하게 사용하고 싶은 객체를 Bean으로 등록합니다.
그리고 Bean들은 Spring에 의해서 관리됩니다. DI는 Bean 관리의 일부입니다.

Spring의 DI(의존성 주입)를 조금 더 구체적으로 설명해보자면 다음과 같습니다.

  1. Spring 애플리케이션 실행 시점에 Bean 을 인스턴스화 시킨다.
  2. Bean을 필요로 하는 곳에 Bean 인스턴스를 전달해줍니다.

Annotation 기반의 DI

SpringBoot을 사용하시면 의존성 자동 주입 기능을 기본으로 사용합니다.
@Autowired 를 사용해서 의존성 주입 받는 법을 살펴보겠습니다.

이렇게 인스턴스 변수에 @Autowired 애너테이션을 붙이면 됩니다.
이렇게 하면 인스턴스 변수들을 초기화 해주지 않아도 사용이 가능합니다.
정말 놀랍게도 초기화해주지 않아도 사용이 가능합니다.
아래 코드를 살펴보시죠.

class NoInit {
    
    @Autowired 
    HelloRepository repository;
    
    public void useRepo() {
    	repository.find(); // 초기화를 하지 않았지만 NPE 발생 안함
    }
}

위의 코드는 전혀 문제없이 동작합니다. 신기하네요!!
핵심은 @Autowired 애너테이션을 붙이면 변수를 초기화하지 않아도 사용이 가능하다는 사실입니다.
Spring 만세!!

나만의 DI 프레임워크

드디어 DI 프레임워크 개발을 시작해보겠습니다. 나만의 DI 프레임워크의 동작방식은 다음과 같습니다.

  1. 의존성을 주입 받고 싶은 클래스의 필드에 @Inject 애너테이션을 붙인다. (@Inject 애너테이션은 직접 정의할 예정)
  2. 리플렉션을 통해 클래스의 필드 정보를 조회하고 한다.
  3. @Inject 애너테이션이 붙은 필드를 찾으면 의존성을 주입한다.

Spring 처럼 자동으로 Bean을 스캔하는 멋진 기능은 없습니다.
그래도 Spring의 DI 방식에 대해서 감을 잡기에는 충분할 것 같습니다.

@Inject 애너테이션 정의하기

Spring에게 @Autowired 가 있다면 저에겐 @Inject 가 있습니다.
해당 필드가 의존성 주입이 필요하다는 표식을 남기기 위해서 @Inject 애너테이션을 정의해보겠습니다.

너무 간단한 코드입니다. 필드 레벨에만 붙일 수 있고, 런타임 시점까지 살아 있습니다.

BookService 정의하기

Spring의 Service 레이어와 같은 네이밍을 사용하는 클래스를 정의하겠습니다.
BookService에는 ParentBook 타입의 인스턴스 변수를 정의할 예정입니다.

이번에도 코드가 간단합니다.
주목할 점은 book 변수를 초기화하는 로직이 어디에도 없다는 사실입니다.
만약 의존성 주입이 이루어지지 않는다면 callH() 메서드를 호출 후NPE가 발생할 것 입니다.

InjectService 정의하기

이제 의존성 주입을 수행하는 InjectService 클래스를 정의하겠습니다.
InjectService의 동작 방식은 다음과 같습니다.

  1. 의존성 주입이 안된 BookService 인스턴스를 파라미터로 받는다.
  2. 리플렉션을 사용해서 BookService 클래스의 필드 중 @Inject 애너테이션이 붙어 있는 필드를 탐색한다.
  3. 리플렉션을 사용해서 @Inject가 붙어 있는 필드에 의존성을 주입한다.
  4. 리플렉션을 사용해서 BookService 인스턴스에 의존성이 주입된 필드를 넣어준다.
  5. 의존성 주입이 완료된 BookService 인스턴스를 리턴한다.

동작 방식대로 코딩을 해보겠습니다.

이렇게 짧은 코드로 의존성 주입이 가능합니다.
이제 코드가 제대로 동작하는지 확인해보겠습니다.

이런 테스트 코드를 작성했습니다.
의존성이 잘 주입이 되었다면 actual"METHOD"의 값을 저장하고 있어야 합니다.

테스트 코드를 실행해보겠습니다.

잘 성공하네요! 의존성이 잘 주입된 모습입니다.

마치며

사실 이번 포스팅의 목적은 DI 프레임워크 개발이 아니였습니다. 진짜 목적은 Spring의 DI 시스템 감 잡기였습니다.
복잡한 시스템을 학습할 때 그와 비슷한 상황을 경험해봐야 학습의 효율이 나온다고 생각합니다.
이번 포스팅을 통해서 Spring의 DI 시스템에 대해서 더욱 관심을 가지는 계기가 되었으면 좋겠습니다!

리플렉션은 더욱 다양한 기능을 가지고 있습니다. 아쉽지만 포스팅이 길어져서 다루지 못했습니다.
궁금하신 분들은 예제 코드를 확인해주세요!

profile
노력하는 자는 즐기는 자를 이길 수 없다

0개의 댓글