강의

멘토링

로드맵

Inflearn brand logo image

인프런 커뮤니티 질문&답변

조태준님의 프로필 이미지
조태준

작성한 질문수

스프링 핵심 원리 - 기본편

@Configuration과 바이트코드 조작의 마법

이 경우는 왜 그런 것일까요?

작성

·

123

1

아래와 같은 AppConfig를 설정 정보로 컨테이너를 생성했을 때

@Configuration
class AppConfig {
    
    @Bean
    public BeanA beanA1() {
        return new BeanA();
    }

    @Bean
    public BeanA beanA2() {
        return beanA1();
    }

    @Bean
    public BeanA beanA3() {
        return new BeanA();
    }
}

빈 이름이 beanA1, beanA2 인 것 끼리는 같은 객체고 beanA3 혼자 다른 객체로 빈이 등록되었는데요

결국에는 메서드 beanA2() 의 return문에 쓰인 beanA1()

메서드 beanA3() 의 return문에 쓰인 new BeanA() 과 다를 게 없다고 생각하였으나 결과는 다르게 나오더라고요.

 

@Configuration 을 적용함으로써

beanA2 가 등록될 때, return문에 있는 beanA1() 를 따라갔을 때 있는 new BeanA() 를 만나도

CGLIB 를 통해 조작된 바이트코드로 인해 정말 new를 다시 하지 않고 (원래 자바 코드라면 new를 했겠지만)

메서드 beanA1() 에서 이미 등록된 빈을 참조하여 등록됩니다.

 

여기서 의문인 점은 beanA3 가 등록될 때는 왜 원래 자바 코드 그대로 new 를 실행하게 되나요?

바이트코드를 조작할 때, return 문에 들어가는 게 '메서드'인지 'new ~' 인지 등의 분기가 또 들어가는 건가요?


만약 제 테스트가 틀린 거라면 아래 테스트 코드를 참고해주시기 바랍니다.

public class AppConfigTest {

    @Test
    void test1() {

        ApplicationContext ac = new AnnotationConfigApplicationContext(Appconfig.class);

        BeanA beanA1 = ac.getBean("beanA1", BeanA.class);
        BeanA beanA2 = ac.getBean("beanA2", BeanA.class);
        BeanA beanA3 = ac.getBean("beanA3", BeanA.class);

        System.out.println(beanA1); // BeanA@e54303
        System.out.println(beanA2); // BeanA@e54303
        System.out.println(beanA3); // BeanA@e8df99a (혼자만 다름)
    }

    @Configuration
    static class Appconfig {

        @Bean
        public BeanA beanA1() {
            return new BeanA();
        }

        @Bean
        public BeanA beanA2() {
            return beanA1();
        }

        @Bean
        public BeanA beanA3() {
            return new BeanA();
        }
    }
}

 

답변 2

2

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. 조태준님

스프링의 @Configuration 어노테이션과 CGLIB를 통한 바이트코드 조작의 동작 방식에 관한 질문으로 이해했습니다.

질문하신 내용을 정리하자면:

  1. beanA1과 beanA2는 같은 인스턴스를 참조하고 있음

  2. beanA3는 다른 인스턴스임

  3. 왜 beanA2()에서는 beanA1()을 호출할 때 새 인스턴스를 생성하지 않고, beanA3()에서는 new BeanA()로 새 인스턴스를 생성하는지에 대한 의문

이 현상이 발생하는 이유는 다음과 같습니다:

스프링에서 @Configuration 클래스는 CGLIB 프록시로 감싸집니다. 이 프록시는 @Bean 메서드의 호출을 가로채서 싱글톤 레지스트리에서 빈을 찾거나 생성하는 역할을 합니다.

중요한 차이점은 메서드 호출직접 객체 생성입니다:

  1. beanA2() 메서드에서 return beanA1();다른 @Bean 메서드를 호출합니다.

    • 이 호출은 CGLIB 프록시에 의해 가로채져서 이미 생성된 빈을 반환합니다.

    • 따라서 beanA1과 beanA2는 같은 인스턴스를 참조합니다.

  2. beanA3() 메서드에서 return new BeanA();직접 객체를 생성합니다.

    • 이것은 메서드 호출이 아니라 객체 생성 코드이므로 CGLIB 프록시가 가로채지 않습니다.

    • 따라서 새 인스턴스가 생성되어 beanA3는 다른 인스턴스를 참조합니다.

CGLIB 프록시는 메서드 호출을 인터셉트할 수 있지만, 클래스 내부의 new 키워드를 사용한 객체 생성은 인터셉트하지 않습니다.

프록시가 바이트코드를 조작할 때, return beanA1();와 같은 메서드 호출은 프록시의 로직으로 대체되어 싱글톤 관리를 처리하지만, return new BeanA();와 같은 객체 생성 코드는 그대로 실행됩니다.

실제로 생성된 프록시 클래스의 내부 동작은 대략 다음과 같을 것입니다:

// CGLIB가 생성한 프록시 클래스 (유사 코드)
class AppConfig$$EnhancerBySpringCGLIB extends AppConfig {
    private BeanFactory beanFactory; // 스프링의 빈 팩토리 참조
    
    @Override
    public BeanA beanA1() {
        // 이미 빈이 존재하는지 확인
        if (beanFactory.containsBean("beanA1")) {
            return beanFactory.getBean("beanA1", BeanA.class);
        }
        // 없으면 부모(원본) 메서드 호출하여 빈 생성
        return super.beanA1(); // 여기서 실제로 new BeanA() 실행
    }
    
    @Override
    public BeanA beanA2() {
        // 이미 빈이 존재하는지 확인
        if (beanFactory.containsBean("beanA2")) {
            return beanFactory.getBean("beanA2", BeanA.class);
        }
        // 없으면 부모 메서드 호출하여 빈 생성
        // 여기서 중요한 점: beanA1()은 프록시 메서드를 호출!
        return beanA1(); // 프록시의 beanA1() 호출, 이미 생성된 빈 반환
    }
    
    @Override
    public BeanA beanA3() {
        // 이미 빈이 존재하는지 확인
        if (beanFactory.containsBean("beanA3")) {
            return beanFactory.getBean("beanA3", BeanA.class);
        }
        // 없으면 부모 메서드 호출하여 빈 생성
        return super.beanA3(); // 여기서 실제로 new BeanA() 실행
    }
}

따라서 바이트코드 조작시 return 문에 들어가는 것이 '메서드 호출'인지 'new 객체 생성'인지에 따라 다르게 처리되며, 이것이 바로 beanA2와 beanA3가 다르게 동작하는 이유입니다.

감사합니다.

조태준님의 프로필 이미지
조태준
질문자

와우 상세한 설명 감사합니다!

0

안녕하세요, 인프런 AI 인턴입니다. 조태준님.

스프링 컨테이너에서 @Configuration@Bean 어노테이션의 동작 원리에 대해 흥미로운 질문 주셨네요. 먼저, @Configuration은 스프링의 설정 클래스를 나타내며, 스프링은 이 클래스가 프록시 객체(CGLIB)를 사용하도록 설정합니다. 이 프록시 객체는 내부적으로 @Bean 메서드를 호출할 때 싱글톤을 보장하기 위해 적용됩니다.

질문하신 부분을 설명드리자면, beanA1() 메서드가 빈으로 등록될 때 이미 CGLIB가 적용된 프록시로 감싸지며, 이 프록시가 호출될 때마다 실제로 새로운 객체를 생성하지 않고 기존의 빈을 반환하게 됩니다. 따라서 beanA2()에서 beanA1()을 호출할 때도 기존에 등록된 빈을 반환하게 됩니다.

반면에 beanA3() 메서드는 직접 new BeanA()를 호출하여 새로운 BeanA 인스턴스를 생성하고 반환합니다. 이 메서드는 내부적으로 다른 빈을 참조하지 않기 때문에 CGLIB의 영향을 받지 않고 새로운 객체를 생성하는 것입니다.

관련된 질문 링크를 몇 가지 소개해 드리겠습니다:
- @Configuration @Bean 질문입니다. - 인프런 | 질문 & 답변 (inflearn.com)
- @Configuration과 바이트코드 조작의 마법에 대해서 궁금한점이 있습니다. - 인프런 | 질문 & 답변 (inflearn.com)

이 두 링크에서는 @Configuration이 어떻게 작동하고 프록시를 통해 어떻게 빈의 싱글톤을 보장하는지를 설명하고 있습니다. 때에 따라 이해하는 데 도움이 될 수 있습니다.

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다. 현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏 추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.

조태준님의 프로필 이미지
조태준
질문자

답변 중 "beanA1() 메서드가 빈으로 등록될 때 이미 CGLIB가 적용된 프록시로 감싸지며"

이 말 자체가 이해가 잘 안 되는데 참고할만한 내용이 있을까요?

지식공유자님의 답변 부탁드립니다.

조태준님의 프로필 이미지
조태준

작성한 질문수

질문하기