inflearn logo
강의

강의

N
챌린지

챌린지

멘토링

멘토링

N
클립

클립

로드맵

로드맵

지식공유

스프링 핵심 원리 - 기본편

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

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

161

조태준

작성한 질문수 24

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();
        }
    }
}

 

spring 객체지향

답변 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가 다르게 동작하는 이유입니다.

감사합니다.

1

조태준

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

0

인프런 AI 인턴

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

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

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

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

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

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

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

0

조태준

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

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

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

코드 자료

0

8

2

구현체가 동적으로 정해질 때, 팩토리 기법을 사용하나요?

0

49

2

MemberService의 인터페이스를 왜 사용하는지 궁금합니다.

0

72

1

롬복 @Setter를 써야 하는 상황이 있는건가요?

0

87

1

빈 등록 메서드의 파라미터가 빈이 아니어도 되나요?

0

79

1

테스트 속도가 나중에 영향이 있을까요?

0

75

1

gradle 설정 안떠서 질문 남깁니다!

0

117

2

build.gradle로 프로젝트를 여는 이유

0

81

1

provider 사용하는 이유

0

85

1

다음 강의 뭘 들어야 할까요

0

123

2

프로토타입 빈, 직접 destroy 호출 안 할 경우

0

62

1

beanB

0

79

2

퀴즈다시풀기

0

63

1

Gradle로 바꿔도 오류가 똑같이 발생하네요 ㅠㅠ

0

90

2

"중복 등록과 충돌" 강의에서 강사님과 다른 에러가 발생합니다.

0

63

3

run 실행했는데 결과창이 이렇게 뜨네요 왜 그런건가요>

0

102

2

도메인의 정의?

0

57

1

ApplicationContext 질문입니다.

0

60

1

@Scope의 proxyMode를 사용할때 단위 테스트 방법

0

86

2

ai api 선정하기 관련 질문

0

116

2

생성자 자동주입 관련해서

0

61

1

생성자 직접 호출 vs 팩토리 메서드 패턴

0

93

2

Spring에서 SessionScope와 RequestScope는 함께 사용되나요?

1

63

1

12:25

0

75

2