작성
·
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를 통한 바이트코드 조작의 동작 방식에 관한 질문으로 이해했습니다.
질문하신 내용을 정리하자면:
beanA1과 beanA2는 같은 인스턴스를 참조하고 있음
beanA3는 다른 인스턴스임
왜 beanA2()에서는 beanA1()을 호출할 때 새 인스턴스를 생성하지 않고, beanA3()에서는 new BeanA()로 새 인스턴스를 생성하는지에 대한 의문
이 현상이 발생하는 이유는 다음과 같습니다:
스프링에서 @Configuration 클래스는 CGLIB 프록시로 감싸집니다. 이 프록시는 @Bean 메서드의 호출을 가로채서 싱글톤 레지스트리에서 빈을 찾거나 생성하는 역할을 합니다.
중요한 차이점은 메서드 호출과 직접 객체 생성입니다:
beanA2()
메서드에서 return beanA1();
은 다른 @Bean 메서드를 호출합니다.
이 호출은 CGLIB 프록시에 의해 가로채져서 이미 생성된 빈을 반환합니다.
따라서 beanA1과 beanA2는 같은 인스턴스를 참조합니다.
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가 적용된 프록시로 감싸지며"
이 말 자체가 이해가 잘 안 되는데 참고할만한 내용이 있을까요?
지식공유자님의 답변 부탁드립니다.
와우 상세한 설명 감사합니다!