Spring + Java

[Spring] 자바 리플렉션과 생성자 주입의 관계

Stark97 2023. 11. 19. 19:02
반응형

이번 포스트에서는 Spring 프레임워크가 자바의 리플렉션 기능을 어떻게 활용하여 생성자 주입을 수행하는지에 대해 살펴보자

 

이번 포스트에서는 특히, 스프링의 AutowiredAnnotationBeanPostProcessor 클래스가 생성자 주입 과정에서 어떤 역할을 하는지 집중적으로 알아볼 것이다. 이 글에서는 리플렉션이 스프링 내부에서 어떻게 사용되는지에 집중하여 설명한다. 이를 통해, 스프링의 내부 동작 방식에 대한 더 깊은 이해를 얻을 수 있을 것이고 자바 리플렉션의 중요성도 알 수 있을 것이라고 생각한다.

 

 

AutowiredAnnotationBeanPostProcessor 클래스는 아래와 같다.

AutowiredAnnotationBeanPostProcessor 클래스

 

 

스프링은 왜 리플렉션을 사용해서 의존성 주입을 할까?


1. 스프링이 리플렉션을 통해 하는 일은 무엇일까?

스프링에서 @Autowired를 사용할 때 AutowiredAnnotationBeanPostProcessor를 통해 자바의 리플렉션을 사용하는 이유는 여러 가지가 있다. 일단 리플렉션은 자바에서 런타임에 클래스의 메타데이터를 읽거나 수정할 수 있는 강력한 기능이다. 스프링은 이 기능을 이용해서 다음과 같은 일들을 한다.
  1. 유연성과 동적 바인딩
    • 리플렉션을 사용하면 스프링은 런타임에 객체의 필드, 메서드, 생성자 등에 접근하고, 그에 따라 의존성을 주입할 수 있다. 이렇게 되면 컴파일 시간에 모든 것을 결정하지 않아도 되고, 더 유연한 애플리케이션 구조를 만들 수 있다.

  2. 구성의 외부화
    • XML이나 어노테이션을 통해 빈(Bean) 설정을 외부에서 정의할 수 있게 한다. 이렇게 하면 코드 변경 없이 애플리케이션의 행동을 바꿀 수 있다. 리플렉션 덕분에 스프링은 이런 설정 정보를 읽고, 해당 객체에 적용할 수 있다.

  3. 코드의 간결성
    • 리플렉션을 사용하면 스프링 프레임워크가 많은 부분을 자동으로 처리해 준다. 개발자가 직접 의존성을 관리하는 코드를 적을 필요가 줄어들고, 결과적으로 코드가 더 깔끔하고 관리하기 쉬워진다.
하지만 리플렉션 사용에는 단점도 있다. 성능 저하가 있을 수 있고, 컴파일 시점에 오류를 잡기 어렵게 만들 수 있다. 그래서 스프링은 리플렉션을 효율적으로 사용하려고 많은 노력을 하고 있는 상황이다. 예를 들어, 스프링 5에서는 리플렉션 사용을 줄이기 위해 주로 '컴파일 타임 위빙(Compile Time Weaving)'과 '함수형 프로그래밍 지원'을 도입했다. 이 기능들은 런타임에 리플렉션을 사용하는 대신에 컴파일 시점에 의존성을 처리하거나, 더 명시적이고 성능이 좋은 코드 작성 방법을 제공한다. 

 

🤔그럼 스프링6가 적용된 스프링부트3.x.x부터는 리플렉션을 사용하지 않는가?

그것은 아니다. 스프링 프레임워크, 스프링 6 그리고 스프링 부트 3.x.x 모두 여전히 @Autowired로 생성자 주입을 할 때 리플렉션을 사용한다. 스프링 5에서 리플렉션 사용을 줄이기 위해 도입한 '컴파일 타임 위빙(Compile Time Weaving)'과 '함수형 프로그래밍 지원'은 리플렉션 사용을 최소화하는 데 도움을 주지만, 완전히 대체하지는 않는다.

예를 들어, 함수형 프로그래밍 접근 방식을 사용하면, 개발자가 더 명시적으로 의존성을 정의하고 관리할 수 있다. 하지만 @Autowired와 같은 어노테이션 기반의 의존성 주입은 여전히 리플렉션을 이용해야 하는 경우가 많다. 
즉, 스프링 5는 리플렉션에 대한 의존도를 줄이긴 했지만, 여전히 @Autowired를 비롯한 기존의 어노테이션 기반 의존성 주입 방식에서는 리플렉션을 사용하고 있다.

 

 

이제 스프링의 의존성 주입과 리플렉션을 실생활로 예시를 들어서 쉽게 이해해 보도록 하자

 

 

2. 스프링의 의존성 주입과 리플렉션을 이해하기 쉽게 설명하겠다.

내가 새 집으로 이사를 갔고, 집안 꾸미기를 해야 하는 상황이라고 가정해 보자. 이때 가구 배치는 내가 결정해야 하는데, 가구는 아직 고르지 않은 상황이다. 이 상황에서 '의존성 주입'은 가구를 배치하는 것과 비슷하다. 나는 어디에 어떤 가구가 들어갈지는 알고 있지만, 실제 가구는 아직 없다. 가구는 나중에 고르기로 한 것이다.

이제 '컴파일 시간' '런타임'을 집 꾸미기에 비유해 보자. 컴파일 시간은 내가 이사 계획을 세우는 시간이다. 이때는 가구의 정확한 종류나 색깔은 결정하지 않는다. 단지 '여기에는 소파가 들어갈 거야, 저기에는 책상이 필요해' 같은 계획만 세운다. 가구의 정확한 세부 사항은 나중에 결정할 것이다. 이게 바로 '컴파일 시간에 모든 것을 결정하지 않는다'는 의미이다.

그다음 '런타임'은 실제로 가구를 구매하고 배치하는 시간이다. 이때 나는 계획했던 위치에 맞춰서 실제 가구를 고르고 배치한다. 이 과정은 프로그램이 실행되는 동안 의존성을 연결하는 것과 비슷하다. 스프링은 '런타임'에 내가 미리 정해놓은 위치(코드에서 @Autowired 어노테이션으로 표시한 부분)에 맞는 '가구'(의존성 객체)를 찾아서 배치한다.

즉, 스프링은 프로그램이 실행되는(런타임) 동안 미리 정해둔 위치에 필요한 객체를 배치해 준다. 그리고 이 모든 것이 가능한 것은 자바의 리플렉션 덕분이다. 리플렉션은 스프링이 '런타임'에 클래스의 구조를 파악하고, 어디에 무엇을 배치해야 할지 결정하는 데 도움을 주기 때문이다.

 

 

마지막으로 런타임에 의존성 주입하는 것이 어떤 장점이 있는지 알아보자

 

3. 스프링에서 의존성을 런타임에 주입하는 것이 갖는 장점

1. 유연성과 확장성
스프링에서 런타임에 의존성을 주입하면, 애플리케이션의 구성 요소들(예: 서비스, 레포지토리 등)을 쉽게 교체할 수 있다. 이는 어플리케이션이 다양한 환경이나 요구사항의 변화에 유연하게 대응할 수 있게 해 준다.

2. 설정과 코드의 분리
설정을 통해서 어플리케이션의 동작을 조정할 수 있다. 예를 들어, 개발 환경에서는 메모리 기반의 데이터베이스를 사용하고, 운영 환경에서는 실제 데이터베이스를 사용하는 식이다. 이렇게 하면 코드를 변경하지 않고도 어플리케이션의 설정을 바꿀 수 있다.

3. 쉬운 유닛 테스트
런타임에 의존성을 주입하면 테스트를 위해 모의 객체나 대체 구현을 쉽게 사용할 수 있다. 예를 들어, 실제 외부 서비스에 의존하는 클래스를 테스트할 때, 그 서비스의 모의 구현을 주입해서 테스트를 단순화할 수 있다.

4. 의존성 관리의 단순화
의존성 주입을 사용하면 개발자가 직접 객체를 생성하고 관리하는 복잡성을 줄일 수 있다. 클래스는 필요한 의존성이 자동으로 주입될 것이라고 기대할 수 있고, 이로 인해 클래스가 더 명확하고 관리하기 쉬워진다.

 

 

지금까지 자바 리플렉션이 스프링에서 어떻게 사용되는지 간단히 알아봤으니 이제는 예시 코드와 내부에서 동작하는 코드를 직접 분석해 보면서 스프링이 어떻게 자바 리플렉션을 활용하여 의존성 주입을 진행하는지 알아보도록 하자!

 

 

1. 의존성 주입을 설명하기 위해 간단한 예시 클래스를 만들었다.


1-1. 먼저, JinanController 클래스와 JinanService 클래스를 생성한다.

  • 여기서 JinanControllerLombok이 지원하는 @RequiredArgsConstructor를 사용하여 JinanService에 대한 의존성을 생성자 주입받도록 작성했다.
@RestController
@RequiredArgsConstructor
public class JinanController {
    private final JinanService jinanService;

    // 컨트롤러 로직...
}

@Service
public class JinanService {
    // 서비스 로직...
}

 

 

생성자 주입에서 AutowiredAnnotationBeanPostProcessor이 어떻게 사용되는지 알아보기 전에 스프링의 생성자 주입에 대해서 간략하게 알아보고 넘어가도록 하자


2. 스프링은 어떻게 생성자 주입을 하는가?


스프링 프레임워크에서 생성자 주입은 @Autowired 어노테이션이 있을 때와 없을 때 모두 동작한다. @Autowired 어노테이션의 사용 여부에 따라 주입 과정의 세부 사항은 아래와 같이 나눠진다.

 

2-1. @Autowired가 있을 때의 의존성 주입

  • 스프링은 명시적으로 @Autowired가 붙은 생성자를 찾아 의존성을 주입한다. 이 경우, AutowiredAnnotationBeanPostProcessor는 내부적으로 @Autowired 어노테이션이 붙은 생성자를 우선적으로 사용하도록 설계되어 있다.
@Controller
public class MyController {
    private final DependencyService dependencyService;

    @Autowired
    public MyController(DependencyService dependencyService) {
        this.dependencyService = dependencyService;
    }
    // 컨트롤러 로직...
}

@Service
public class DependencyService {
    // 서비스 로직...
}

 

2-2. @Autowired가 없을 때의 의존성 주입

1. 단일 생성자의 경우
  • 만약 클래스에 단 하나의 생성자만 존재한다면 스프링은 이 생성자를 자동으로 사용하여 의존성을 주입한다. 이때 @Autowired 어노테이션은 필요하지 않다. 왜냐하면 앞에서 말했듯이 스프링이 내부적으로 자동으로 생성자를 사용하여 의존성 주입을 처리하기 때문이다.
@Controller
public class MyController {
    private final DependencyService dependencyService;

    public MyController(DependencyService dependencyService) {
        this.dependencyService = dependencyService;
    }
    // 컨트롤러 로직...
}

@Service
public class DependencyService {
    // 서비스 로직...
}

 

2. @RequiredArgsConstructor를 사용하는 경우
  • Lombok의 @RequiredArgsConstructor를 사용하면 롬복이 필요한 의존성에 대한 생성자를 자동으로 생성해 준다. 이렇게 롬복을 사용하는 경우 스프링은 생성자 주입을 할 때 내부적으로 롬복이 생성한 생성자를 자동으로 사용하게 된다.
@RestController
@RequiredArgsConstructor
public class MyController {
    private final DependencyService dependencyService;

    // 컨트롤러 로직...
}

@Service
public class DependencyService {
    // 서비스 로직...
}

 

2-3. 여러 개의 @Autowired가 붙은 생성자가 존재하는 경우

  • 스프링 프레임워크에서 @Autowired 어노테이션이 붙은 생성자가 '필수'(required)로 선택되는 기준은 다음과 같다.
1. 개발자가 지정

개발자는 @Autowired(required = true) 또는 @Autowired(required = false)를 사용하여 생성자가 '필수'인지 여부를 명시적으로 지정할 수 있다. required 속성이 true(기본값)인 경우, 해당 생성자는 필수로 간주된다. false로 설정하면, 해당 생성자는 필수가 아니며, 의존성이 주입되지 않을 수 있다.

2. 스프링의 내부 규칙에 의한 지정

개발자가 required 속성을 명시적으로 설정하지 않은 경우, 스프링은 내부 규칙에 따라 생성자를 선택한다. 예를 들어, 클래스에 단일 생성자만 있으면 스프링은 그것을 필수 생성자로 간주한다. 여러 생성자가 있고 모두 @Autowired로 표시되어 있다면, 스프링은 오류를 발생시킬 수 있다.

 

  • 알아둬야 할 것은 @Autowired는 required 값이 기본적으로 true로 설정되어 있다.
spring의 autowired 어노테이션의 구성
spring의 autowired 어노테이션의 구성
1. 단일 '필수' 생성자
  • 스프링은 @Autowired 어노테이션이 붙은 생성자들 중에서 '필수'(required)로 지정된 생성자를 우선적으로 선택한다. 만약 클래스 내부에 하나의 생성자만이 '필수'로 표시되어 있다면, 그 생성자가 자동 주입을 위해 선택된다. 아래의 예시에서는 위에 있는 생성자가 사용될 것이다.
@Controller
public class MyController {
    private final DependencyService1 dependencyService1;
    private final DependencyService2 dependencyService2;

    @Autowired
    public MyController(DependencyService1 dependencyService1) {
        this.dependencyService1 = dependencyService1;
        // dependencyService2는 주입되지 않음
    }

    @Autowired(required = false)
    public MyController(DependencyService1 dependencyService1, DependencyService2 dependencyService2) {
        this.dependencyService1 = dependencyService1;
        this.dependencyService2 = dependencyService2;
    }
    // 컨트롤러 로직...
}
2. 중복 어노테이션 문제
  • 만약 여러 생성자가 모두 '필수'(required)로 표시되어 있다면, 스프링은 이를 오류로 간주하고 예외를 발생시킨다. 스프링은 한 클래스 내에서 단 하나의 '필수(required)' 생성자만을 지원한다.
스프링은 @Autowired 어노테이션이 붙은 여러 생성자를 발견하면, 어떤 생성자를 선택해야 할지 결정하기 어렵다. required 표시는 개발자가 @Autowired(required = true) 또는 @Autowired(required = false)를 사용하여 명시적으로 설정할 수 있지만, 아래의 예시코드에는 이러한 표시가 없다. 이 경우, 스프링은 생성자 중 하나를 선택해야 하는데, 이는 오류나 예기치 않은 동작을 유발할 수가 있다.
@Controller
public class MyController {
    private final DependencyService1 dependencyService1;
    private final DependencyService2 dependencyService2;

    @Autowired
    public MyController(DependencyService1 dependencyService1) {
        this.dependencyService1 = dependencyService1;
    }

    @Autowired
    public MyController(DependencyService1 dependencyService1, DependencyService2 dependencyService2) {
        this.dependencyService1 = dependencyService1;
        this.dependencyService2 = dependencyService2;
    }
    // 컨트롤러 로직...
}
  • 따라서, 일반적인 사용 사례에서는 한 클래스 내에 여러 @Autowired 생성자가 있더라도, 하나의 '필수' 생성자를 명시적으로 지정하거나, 모든 생성자를 '선택적'(required=false)으로 설정하여 스프링이 올바른 생성자를 선택할 수 있도록 해야 한다.

 

3. 위에서 선언한 JinanCotroller가 생성자 주입을 하는 과정은 다음과 같다.


이 설명은 스프링이 클래스의 생성자에 어떻게 의존성 주입(DI)을 처리하는지에 대한 간략한 설명이다. 그래서 실제 스프링이 내부적으로 사용하는 AutowiredAnnotationBeanPostProcessor 클래스가 어떤 방식으로 이 과정을 진행하는지를 설명한다.

참고로 하단의 3-1~3-5번까지의 설명에 적혀있는 메서드들은 전부 AutowiredAnnotationBeanPostProcessor 내부에 존재하는 메서드이다.
@RestController
@RequiredArgsConstructor
public class JinanController {
    private final JinanService jinanService;

    // 컨트롤러 로직...
}

 

3-1. JinanController (@Autowired 또는 @RequiredArgsConstructor)

JinanController 클래스는 @Autowired 또는 @RequiredArgsConstructor를 사용하여 필요한 의존성을 명시한다.

 

3-2. 메타데이터 생성 (findAutowiringMetadata 메서드)

JinanController에 대한 메타데이터 생성은 findAutowiringMetadata() 메서드를 통해 진행된다. 이 메서드는 클래스 내부를 리플렉션을 사용하여 검사하고, @Autowired 또는 다른 관련 어노테이션이 적용된 필드와 메서드를 찾는다. 찾아낸 필드와 메서드에 대한 정보 (예: 필드 타입, 메서드 시그니처)는 메타데이터로 저장되어 후속 주입 과정에서 사용된다.

 

3-3. 후보 생성자 결정 (determineCandidateConstructors 메서드)

JinanController의 생성자 중에서 의존성 주입에 적합한 후보를 결정하는 과정이다. @RequiredArgsConstructor로 인해 생성된 생성자는 JinanService 타입의 파라미터를 받는 생성자이다. 스프링은 이 생성자를 자동 주입을 위한 적절한 후보로 선택한다.

 

3-4. 실제 의존성 주입 (postProcessProperties 메서드)

이 단계에서는 앞서 생성된 메타데이터를 기반으로 실제 의존성 주입을 진행한다. JinanController의 jinanService 필드에 JinanService 인스턴스가 주입된다.

 

3-5. 추가적인 의존성 주입 (processInjection 메서드)

필요한 경우, 이 단계에서 추가적인 의존성 주입이 이루어진다.

 

 

지금부터 나는 스프링이 내부적으로 AutowiredAnnotationBeanPostProcessor 클래스를 사용하여 이 클래스 안에 작성되어 있는 메서드를 사용해서 의존성 주입을 하는 방법을 설명하겠다.

 

이 글을 작성한 이유는 스프링에서는 자바 리플렉션을 생성자 주입에 어떻게 사용하는지 알아보기 위한 것이라는 걸 잊지 말자

지금부터 내가 설명할 내용에 나오는 메서드들은 모두 스프링이 내부적으로 의존성 주입을 하기 위해 사용하는 AutowiredAnnotationBeanPostProcessor 클래스 내부에 작성되어 있는 메서드에 대한 설명이다. 스프링은 내부적으로 의존성 주입을 처리할 때 이 메서드들을 사용한다. (참고로 이 클래스는 내가 만든 클래스가 아니라 이미 스프링이 만들어 놓고 내부적으로 사용하는 클래스다.)

우리가 확인해 볼 메서드의 순서는 다음과 같다. -> 이것은 의존성 주입이 동작하는 순서를 그대로 따라가는 것이다.

1. findAutowiringMetadata()
2. determineCandidateConstructors()
3. postProcessProperties()

여기서 독특한 건 만약 스프링의 자동 주입 방식이 아니라 개발자가 직접 빈을 추가적으로 주입해주고자 한다면 아래의 4번 메서드가 사용된다. 스프링이 처리하는 일반적인 의존성 주입은 위의 1~3 단계 안에서 다 끝난다.

4. processInjection()

 

 

4. 의존성 주입을 할 때 가장 먼저 1.findAutowiringMetadata 메서드가 동작한다.


4-1. findAutowiringMetadata 메서드

findAutowiringMetadata() 메서드는 주로 캐시 된 의존성 주입 메타데이터를 관리한다. 이 메서드는 먼저 빈(Bean) 이름이나 클래스 이름을 키(key)로 사용해서 캐시에서 메타데이터를 조회한다. 만약 메타데이터가 존재하지 않거나 새로고침이 필요한 경우, 새로운 메타데이터를 생성하여 캐시에 저장한다. 이 과정은 동시성을 고려하여 동기화된 상태에서 이루어진다. 따라서, 이 메서드는 주입할 필드와 메서드에 대한 메타데이터를 찾고 생성하는 중요한 역할을 한다.

 

findAutowiringMetadata 메서드
findAutowiringMetadata 메서드

 

이렇게만 보면 도대체 어디서 자바의 리플렉션이 사용되는지 의문이 들것이다.
  • 아래 사진에 체크해 놓은 buildAutowiringMetadata() 메서드에 들어가서 코드를 확인해 보면 답이 나올 것이다.
buildAutowiringMetadata메서드 내부의 buildAutowiringMetadata 메서드
buildAutowiringMetadata메서드 내부의 buildAutowiringMetadata 메서드

 

  • 위의 findAutowiringMetadata() 메서드 내부에서 호출되는 buildAutowiringMetadata(clazz) 메서드 안에서 자바의 리플렉션이 사용된다. 아래의 코드를 보면서 설명하겠다.
private InjectionMetadata buildAutowiringMetadata(Class<?> clazz) {
    // 주어진 클래스가 의존성 주입 대상인지 확인
    if (!AnnotationUtils.isCandidateClass(clazz, this.autowiredAnnotationTypes)) {
        return InjectionMetadata.EMPTY;
    }

    List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
    Class<?> targetClass = clazz;

    do {
        final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();

        // 클래스의 모든 필드 순회 - 여기서 리플렉션이 사용된다.
        ReflectionUtils.doWithLocalFields(targetClass, field -> {
            MergedAnnotation<?> ann = findAutowiredAnnotation(field);
            if (ann != null) {
                // 정적 필드는 무시
                if (Modifier.isStatic(field.getModifiers())) {
                    return;
                }
                boolean required = determineRequiredStatus(ann);
                // 필드 주입 요소 추가
                currElements.add(new AutowiredFieldElement(field, required));
            }
        });

        // 클래스의 모든 메서드 순회 - 여기서 리플렉션이 사용된다.
        ReflectionUtils.doWithLocalMethods(targetClass, method -> {
            Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
            if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
                return;
            }
            MergedAnnotation<?> ann = findAutowiredAnnotation(bridgedMethod);
            if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
                // 정적 메서드는 무시
                if (Modifier.isStatic(method.getModifiers())) {
                    return;
                }
                boolean required = determineRequiredStatus(ann);
                PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
                // 메서드 주입 요소 추가
                currElements.add(new AutowiredMethodElement(method, required, pd));
            }
        });

        // 현재 클래스의 주입 요소를 전체 목록에 추가
        elements.addAll(0, currElements);
        // 상위 클래스로 이동
        targetClass = targetClass.getSuperclass();
    } while (targetClass != null && targetClass != Object.class);

    // 수집된 요소들로 메타데이터 생성
    return InjectionMetadata.forElements(elements, clazz);
}

 

위의 코드를 살펴보면 ReflectionUilts를 사용하는 부분이 중간중간 있는데 이 과정에서 리플렉션이 사용된다.
이 코드 내용을 보면 doWithLocalFields() 메서드는 주어진 클래스의 모든 필드를 리플렉션을 통해서 접근하고, 필드에 @Autowired 어노테이션이 적용되었는지 확인한다. doWithLocalMethods() 메서드도 마찬가지로 모든 메서드에 대해 리플렉션을 사용하여 접근하고, @Autowired 어노테이션이 적용된 메서드를 찾아낸다.

 

리플렉션 doWithLocalFields
리플렉션 doWithLocalFields
리플렉션 doWithLocalMethods
리플렉션 doWithLocalMethods

 

 

 

5. 어떤 생성자를 사용하여 의존성을 주입할지 결정하는 2. determineCandidateConstructors 메서드가 동작한다.


5-1. determineCandidateConstructors 메서드 알아보기

determineCandidateConstructors 메서드의 역할은 스프링 컨테이너가 어떤 생성자를 사용하여 의존성을 주입할지 결정하는 것이다. 다시 말해, 이 메서드는 클래스의 생성자들을 검토하여 의존성 주입에 적합한 생성자들을 '후보'로 선정하는 과정을 수행한다. 실제 의존성 주입은 이후 단계에서 이루어지며, 이때 determineCandidateConstructors 메서드에서 선정된 생성자가 사용된다.

 

determineCandidateConstructor 메서드 - 생략이미지
determineCandidateConstructor 메서드 - 생략이미지
@Override
@Nullable
public Constructor<?>[] determineCandidateConstructors(Class<?> beanClass, final String beanName)
        throws BeanCreationException {

    checkLookupMethods(beanClass, beanName);

    // 먼저 캐시에서 후보 생성자를 빠르게 확인
    Constructor<?>[] candidateConstructors = this.candidateConstructorsCache.get(beanClass);
    if (candidateConstructors == null) {
        // 동기화된 구조에서 후보 생성자 확인...
        synchronized (this.candidateConstructorsCache) {
            candidateConstructors = this.candidateConstructorsCache.get(beanClass);
            if (candidateConstructors == null) {
                Constructor<?>[] rawCandidates;
                try {
                    rawCandidates = beanClass.getDeclaredConstructors();
                } catch (Throwable ex) {
                    // 클래스 로더로부터 생성자 확인 실패 시 예외 발생
                    throw new BeanCreationException(beanName, "Resolution of declared constructors failed", ex);
                }
                List<Constructor<?>> candidates = new ArrayList<>();
                Constructor<?> requiredConstructor = null;
                Constructor<?> defaultConstructor = null;
                Constructor<?> primaryConstructor = BeanUtils.findPrimaryConstructor(beanClass);
                int nonSyntheticConstructors = 0;
                for (Constructor<?> candidate : rawCandidates) {
                    // ...
                    // 생성자 후보 확인 로직
                    // ...
                }
                // 후보 생성자 결정 로직
                // ...
            }
        }
    }
    return (candidateConstructors.length > 0 ? candidateConstructors : null);
}
determineCandidateConstructors 메서드는 클래스의 생성자들 중 스프링의 자동 주입(autowiring)에 적합한 후보를 선택하는 역할을 한다. 이 과정은 다음과 같이 진행된다.

 

5-2. 메서드 동작 1: getDeclaredConstructors() 메서드로 모든 생성자 가져오기

  • beanClass.getDeclaredConstructors()를 사용하여 해당 클래스에 정의된 모든 생성자를 가져온다.
  • getDeclaredConstructors 메서드의 동작은 클래스에 정의된 모든 생성자들을 가져오는 작업이다. 이는 자바 리플렉션 API의 일부로, 주어진 클래스에 정의된 모든 생성자를 Constructor<?>[] 배열 형태로 반환한다.
beanClass.getDeclaredConstructors 메서드
beanClass.getDeclaredConstructors 메서드
getDeclaredConstructors 메서드
getDeclaredConstructors 메서드

 

5-3. 메서드 동작 2: determineCandidateConstructors 메서드 내부에서 @Autowired 어노테이션이 붙은 생성자들 찾기

  • 위의 getDeclaredConstructors() 메서드를 통해 모든 생성자를 가져왔다면 이제 determineCandidateConstructors() 메서드는 이 가져온 생성자들 중에서 @Autowired 어노테이션이 붙은 생성자들을 찾는다. @Autowired 어노테이션은 스프링 프레임워크가 해당 생성자에 의존성을 자동으로 주입하도록 지시하는 데 사용된다. getDeclaredConstructors() 메서드는 이러한 생성자들을 후보로 선정한다.
만약 한 클래스 안에 @Autowired가 붙은 생성자가 여러 개 있다면, 스프링은 그중에서 하나를 선택해야 하는데, 이는 일반적으로 '필수'(required)인 생성자 또는 가장 많은 의존성을 충족시킬 수 있는 생성자가 된다. @Autowired 어노테이션이 없는 경우에는 기본 생성자나 하나만 존재하는 생성자가 후보로 고려된다.

 

한 클래스 안에는 여러 개의 생성자를 선언할 수 있기 때문에 클래스에 정의된 모든 생성자를 가져와서 비교한 다음 결정하게 된다.
// 여러개의 생성자 예시코드
public class ExampleClass {

    // 기본 생성자
    public ExampleClass() {
        // ...
    }

    // @Autowired 애노테이션이 붙은 생성자
    @Autowired
    public ExampleClass(DependencyClass dependency) {
        // ...
    }

    // 파라미터가 다른 생성자
    public ExampleClass(AnotherDependency anotherDependency) {
        // ...
    }
}

 

 

6. 마지막으로 3. postProcessProperties 메서드가 동작해서 실제로 의존성 주입을 수행한다.


6-1. postProcessProperties 메서드 알아보기

postProcessProperties 메서드는 식별된 의존성들을 빈의 필드와 메서드에 주입하는 중요한 단계이다. 이전 단계인 determineCandidateConstructors 메서드에서 적절한 생성자를 선정하는 역할을 한다면, 이번 단계인 postProcessProperties는 그 선정된 생성자를 사용하여 실제 의존성 주입을 수행하는 단계이다.

 

postProcessProperties 메서드
postProcessProperties 메서드
@Override
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
	InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
	try {
		metadata.inject(bean, beanName, pvs);
	}
	catch (BeanCreationException ex) {
		throw ex;
	}
	catch (Throwable ex) {
		throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex);
	}
	return pvs;
}

 

6-2. postProcessProperties 메서드는 스프링 빈의 프로퍼티 처리 및 의존성 주입을 담당한다.

  1. 메타데이터 검색
    • 먼저, findAutowiringMetadata 메서드를 호출하여 빈의 클래스에 대한 메타데이터를 찾는다. 이 메타데이터에는 의존성을 주입해야 할 필드와 메서드에 대한 정보가 포함되어 있다.

  2. 의존성 주입 수행
    • 찾은 메타데이터를 바탕으로, InjectionMetadata.inject 메서드를 호출하여 실제 의존성을 빈의 필드와 메서드에 주입한다. 이 과정에서 빈의 인스턴스(bean), 빈 이름(beanName), 그리고 프로퍼티 값(pvs)이 인자로 사용된다.
InjectionMetadata 클래스
InjectionMetadata 클래스
InjectionMetadata.inject 메서드
InjectionMetadata.inject 메서드
public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
    // 주입할 요소들을 담은 컬렉션을 확인합니다.
    Collection<InjectedElement> checkedElements = this.checkedElements;
    Collection<InjectedElement> elementsToIterate =
            (checkedElements != null ? checkedElements : this.injectedElements);

    // 주입할 요소들이 비어있지 않은 경우
    if (!elementsToIterate.isEmpty()) {
        // 각 요소에 대하여 반복합니다.
        for (InjectedElement element : elementsToIterate) {
            // 각 요소에 대하여 주입을 수행합니다. 여기서 target은 주입 대상 객체, beanName은 빈 이름, pvs는 프로퍼티 값들입니다.
            element.inject(target, beanName, pvs);
        }
    }
}

 

InjectionMetadata.inject 메서드는 스프링 빈의 프로퍼티 처리 및 의존성 주입 과정에서 중요한 역할을 한다.

 

  1. 메타데이터 활용
    • findAutowiringMetadata 메서드를 통해 얻은 메타데이터는 빈에 주입해야 할 필드와 메서드에 대한 정보를 담고 있다. 이 메서드 내에서 checkedElements 또는 injectedElements 컬렉션을 통해 주입할 요소들을 확인한다.

  2. 의존성 주입 실행
    • 메타데이터에 포함된 정보를 바탕으로, 각 InjectedElement에 대해 inject 메서드를 호출한다. 이때, target은 주입 대상 객체, beanName은 빈의 이름, pvs는 프로퍼티 값들을 나타낸다.

  3. 주입 대상 식별 및 접근
    • InjectedElement는 주입해야 할 필드나 메서드를 식별하고, 이들에 접근하여 의존성을 설정한다. 이 과정은 리플렉션을 통해 수행된다.

  4. 오류 처리
    • inject 메서드는 의존성 주입 중 발생할 수 있는 예외나 오류를 처리한다. 문제가 발생하면 적절한 예외를 발생시키는 역할도 한다.
요약하자면, InjectionMetadata.inject 메서드는 주입할 요소들의 컬렉션을 관리하고, 각 요소에 대해 의존성 주입을 수행하는 중심적인 역할을 한다. 이러한 과정을 통해, postProcessProperties 메서드는 스프링 빈에 필요한 의존성을 효과적으로 식별하고, 적절한 위치에 주입하는 중요한 역할을 수행한다.

 

 

7. 이미 스프링의 의존성 주입은 끝난 상황이고 이 4.processInjection 메서드는 특정 상황에만 동작한다.


7-1. processInjection 메서드 알아보기

processInjection 메서드는 주로 프로그래밍적인 방식으로 특정 인스턴스에 의존성을 주입할 필요가 있을 때 사용된다. 이 메서드는 스프링의 표준 자동 의존성 주입 프로세스와는 별개로, 개발자가 특정 상황에서 명시적으로 의존성 주입을 수행하고자 할 때 활용된다.
예를 들어, 특정 빈 인스턴스에 대해 필드와 메서드에 의존성을 주입하고자 할 때
processInjection이 사용될 수 있다. 이 과정에서 findAutowiringMetadata 메서드를 통해 해당 빈의 클래스에 대한 메타데이터를 찾고, InjectionMetadata.inject 메서드를 사용하여 실제 의존성을 주입한다. 이는 일반적인 스프링 빈의 자동 주입 과정과는 다르게, 개발자가 직접 제어하는 경우에 해당한다.

 

processInjection 메서드
processInjection 메서드

 

7-2. 개발자가 직접 제어하여 직접적으로 빈(Bean)에 의존성을 주입하는 예시코드

public class CustomDependencyInjectionExample {

    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(MyConfiguration.class);
        AutowireCapableBeanFactory beanFactory = context.getAutowireCapableBeanFactory();

        MyBean myBean = new MyBean();
        AutowiredAnnotationBeanPostProcessor processor = new AutowiredAnnotationBeanPostProcessor();
        processor.setBeanFactory(beanFactory);

        // 직접적으로 의존성을 주입
        processor.processInjection(myBean);

        // 이제 myBean은 의존성이 주입된 상태
        myBean.performAction();
    }

    public static class MyBean {
        @Autowired
        private MyDependency myDependency;

        public void performAction() {
            // 의존성 사용
            myDependency.performService();
        }
    }

    public static class MyDependency {
        public void performService() {
            System.out.println("Service is performed");
        }
    }

    // ... (MyConfiguration 클래스 등 추가적인 구성 요소)
}
  • 이 예시에서는 AutowiredAnnotationBeanPostProcessor를 사용해 직접적으로 MyBean 인스턴스에 MyDependency 의존성을 주입하고 있다. 이러한 방식은 일반적인 스프링의 자동 주입 과정과는 별개로, 특정 상황에서 개발자가 직접 제어하고자 할 때 유용하게 사용될 수 있다.

 

7-3. processInjection 메서드의 동작

  1. 메타데이터 검색
    • findAutowiringMetadata 메서드를 호출하여 해당 인스턴스의 클래스에 대한 메타데이터를 찾는다. 여기서 클래스 이름과 클래스 자체가 인자로 사용된다.

  2. 의존성 주입 수행
    • 찾아낸 메타데이터에 기반하여 InjectionMetadata.inject 메서드를 호출하고, 실제로 필드와 메서드에 의존성을 주입한다. 이 단계에서 bean은 주입 대상 인스턴스로 사용되며, 나머지 두 null 인자는 주입 과정에 필요한 추가적인 정보를 제공한다.
이러한 과정을 통해 processInjection 메서드는 주입 대상 인스턴스에 필요한 의존성들을 효율적으로 주입하며, 이는 스프링 빈의 올바른 초기화와 사용을 보장하는 데 중요한 역할을 한다.

 

 

8. 결론


8-1. 이 과정들을 통해 알게 된 결과를 정리해 보자

1. findAutowiringMetadata()

이 메서드는 클래스 내부의 필드와 메서드를 리플렉션을 통해 검사하여 @Autowired 애노테이션이 적용된 필드와 메서드를 찾는다. 리플렉션을 사용하여 클래스의 메타데이터를 구성한다.

2. determineCandidateConstructors()

이 메서드는 beanClass.getDeclaredConstructors()를 호출하여 클래스에 선언된 모든 생성자를 가져온다. 이 과정에서 리플렉션이 사용되어 클래스의 생성자 정보를 추출한다.

3. postProcessProperties()

이 메서드 내에서 InjectionMetadata.inject()를 호출할 때, 실제 의존성 주입을 수행하는 과정에서 필드에 접근하거나 메서드를 호출하는데 리플렉션이 사용된다.

 

따라서, 이 세 과정 모두에서 자바의 리플렉션 기능이 중요한 역할을 한다는 것을 알 수 있었다.

 

 

8-2. 나의 생각 정리

지금까지 나는 이렇게까지 @Autowired의 동작에 대해서 알아보고자 노력했던 적이 없었다. 그냥 메서드 상단에 적기만 하면 자동으로 생성자 or 필드 주입이 되었기에 내부적으로는 어떻게 동작하는지 알아볼 생각을 하지 않았다. 이번에 자바 리플렉션을 공부하다 보니 @Autowired가 리플렉션을 사용한다는 것을 알게 되었고 이것을 스프링은 어떻게 사용했을까?라는 궁금증에 잠깐 알아보다 보니 이렇게까지 깊이 알아보게 되었다. 아직 주니어의 실력으로 작성한 글이다 보니 많이 부족한 내용이 존재하지만 누군가는 이 글을 통해 스프링이 내부적인 어떻게 생성자 주입을 처리하는지에 대한 흐름을 알아갔으면 좋겠다.

 

 

시간이 난다면 chillwave의 팀원인 "평양냉면7"님의 블로그도 방문해주세요! :)

 

하다보니 재미있는 개발

하다 보니 재미있는 개발에 빠져있는 중입니다. 문의사항: ysoil8811@gmail.com

yijoon009.tistory.com

 

반응형