프로젝트 생성에 이어서 이번에는 BeanDefinition의 개념에 대해 다뤄볼 것이다.BeanDefinition를 직역하면 "빈 정의"이다.
그렇다면 여기에서 사용하는 "빈"이란 무엇일까?
빈의 개념부터 짚고 넘어가보자.
빈(Bean)
빈(Bean) : "스프링 컨테이너가 관리하는 자바 객체"를 의미한다.
그냥 객체랑 다른 점
자바 객체와 빈을 코드로 비교해보자면 아래와 같다.
// 그냥 객체 — 내가 직접 만들고 직접 관리
HelloService a = new HelloService();
a.hello();
여기에서 a는 내가 new로 만들고, 내가 다 쓰고 나면 알아서 가비지 컬렉션이 된다.
생성과 소멸을 우리가 책임진다.
// 빈 — 컨테이너가 만들고 컨테이너가 관리
ApplicationContext context = new MiniApplicationContext("com.mini.spring.sample");
HelloService b = context.getBean(HelloService.class);
b.hello();
반면에 빈으로 만든 b는 내가 new로 만든게 아니다. 컨테이너가 어딘가에서 만들어 보관하고 있다가, 내가 getBean()으로 달라고하면 꺼내준다.
즉, 생성과 보관을 컨테이너가 책임진다.
같은 HelloService 클래스의 인스턴스인데, 누가 관리하느냐의 차이다.
"관리한다" 라는 것은
컨테이너가 빈에 대해 해주는 일들은 아래와 같다.
- 생성 — 적절한 시점에
new해줌 - 의존성 주입 — 다른 빈이 필요하면 자동으로 꽂아줌 (
@Autowired) - 싱글톤 보장 — 같은 빈은 항상 같은 인스턴스 반환
- 생명주기 콜백 — 생성 직후, 소멸 직전에 특정 메서드 호출
- AOP 적용 — 필요하면 프록시로 감싸서 부가 기능 추가
만약 내가 그냥 객체를 만들었다면 이 모든 걸 내가 직접 해야한다.
하지만 빈으로 만들면 컨테이너가 해준다.
"빈"이 되는 방법
평범한 클래스가 빈이 되는 방법은 두 가지다.
@Component(또는 그 변형:@Service,@Repository,@Controller) 붙이기
@Component
public class HelloService { // ← 이제 이 클래스의 인스턴스는 빈이 됨
public String hello() { return "Hello!"; }
}
@Configuration클래스 안에서@Bean메서드로 등록
@Configuration
public class AppConfig {
@Bean
public HelloService helloService() {
return new HelloService();
}
}
내가 만들 미니 스프링 프로젝트에서는 1번 방식만 다룰 것이다.
비유로 이해하기
호텔을 생각해보자.
- 그냥 객체 : 내가 직접 빌린 에어비앤비이다. 청소도 내가 하고, 침구도 내가, 체크아웃도 내가 한다.
- 빈 : 호텔 객실이다. 들어가면 청소가 되어있고, 룸서비스 부르면 오고, 나갈 때 정리도 호텔이 해준다.
호텔(컨테이너)이 모든 객실(빈)을 관리해준다. 손님(내 코드)은 그냥 "방 키 주세요"(getBean())만 하면 된다.
왜 BeanDefinition이 필요한가
그렇다면 본격적으로 왜 BeanDefinition이 필요한가에 대해서 알아보도록 하자.
스프링은 왜 Class<?> 객체를 직접 다루지 않고, BeanDefinition이라는 한 단계를 더 둘까?
단순히 아래와 같이 하면 안될까?
Map<String, Class<?>> beans = new HashMap<>();
beans.put("helloService", HelloService.class);
// 필요할 때 newInstance()로 만들면 끝 아닌가?
지금 당장은 된다. 하지만 이런 정보들이 더 필요해질 수 있다.
- 이 빈이 싱글톤인가, 매번 새로 만드는(프로토타입) 빈인가?
- 생성자에 어떤 인자를 넣어줘야 하는가? (3단계 이후)
- 초기화 메서드(
@PostConstruct)나 소멸 메서드는 뭔가? - 지연 로딩인가, 컨텍스트 시작 시 바로 만드는가?
- 어떤 다른 빈에 의존하고 있는가?
Class<?> 객체는 "이 클래스의 구조"만 알려준다. 위 정보들은 클래스 자체에 없다.
그래서 빈을 만들기 위한 레시피를 따로 표현하는 객체가 필요하다.
그게 바로 BeanDefinition이다.
요약하자면 아래와 같다.
Class<?> = "이 클래스가 뭔지" (자바가 주는 정보)
BeanDefinition = "이 빈을 어떻게 만들지" (프레임워크가 정의)
이 분리 덕분에 나중에 XML 설정, 어노테이션 설정, Java Config 같은 서로 다른 소스에서 빈 정의를 읽어들여도 내부적으로는 동일한 BeanDefinition 구조로 통일해서 다룰 수 있다.
실제 스프링도 이렇게 동작한다.
BeanDefinition 설계
지금 단계에서는 최소 정보만 담아볼 것이다. 나중에 필요하면 필드를 그 때 추가하는 식으로 진행할 것이다.
지금 필요한 것:
- 빈 이름 (
name) - 컨테이너에서 빈을 찾을 키 - 빈 클래스 (
beanClass) - 인스턴스를 만들 클래스 정보 - 스코프 (
scope) - 싱글톤이냐 프로토타입이냐
스코프는 일단 문자열 상수로 두고, enum이나 별도 클래스로의 분리는 나중에 필요해지면 할 것이다
코드
package com.mini.spring.beans;
public class BeanDefinition {
public static final String SCOPE_SINGLETON = "singleton";
public static final String SCOPE_PROTOTYPE = "prototype";
private final String name;
private final Class<?> beanClass;
private String scope = SCOPE_SINGLETON; // 기본값 : 싱글톤
public BeanDefinition(String name, Class<?> beanClass) {
this.name = name;
this.beanClass = beanClass;
}
public String getName() {
return name;
}
public Class<?> getBeanClass() {
return beanClass;
}
public String getScope() {
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
public boolean isSingleton() {
return SCOPE_SINGLETON.equals(scope);
}
public boolean isPrototype() {
return SCOPE_PROTOTYPE.equals(scope);
}
@Override
public String toString() {
return "BeanDefinition{name='" + name + "', beanClass=" + beanClass.getSimpleName() + ", scope='" + scope + "'}";
}
}
name과beanClass는final: 빈 이름과 클래스는 한 번 정해지면 바꾸면 안된다. 도중에 바뀌면 컨테이너 상태가 깨진다. 생성자에서만 주입하고setter는 두지 않는다.scope는mutable: 스캔하면서 일단 기본값인 싱글톤으로 만든 다음,@Scope같은 어노테이션을 읽어서 나중에 바꿔주는 식으로 쓸 것이다. 그래서setter가 있다.isSingleton(),isPrototype()편의 메서드 : 호출하는 쪽에서 매번def.getScope().equals("singleton")를 적으면 깔끔하지 않다. 그래서 의도를 드러내는 메서드로 감싼다.toString(): 디버깅용이다.
테스트
package com.mini.spring.beans;
import com.mini.spring.sample.HelloService;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class BeanDefinitionTest {
@Test
void 빈정의를_생성하면_이름과_클래스를_보관한다() {
BeanDefinition def = new BeanDefinition("helloService", HelloService.class);
assertEquals("helloService", def.getName());
assertEquals(HelloService.class, def.getBeanClass());
}
@Test
void 기본_스코프는_싱글톤이다() {
BeanDefinition def = new BeanDefinition("helloService", HelloService.class);
assertTrue(def.isSingleton());
assertFalse(def.isPrototype());
}
@Test
void 스코프를_프로토타입으로_변경할_수_있다() {
BeanDefinition def = new BeanDefinition("helloService", HelloService.class);
def.setScope(BeanDefinition.SCOPE_PROTOTYPE);
assertFalse(def.isSingleton());
assertTrue(def.isPrototype());
}
}
실행
맥은 터미널에서 ./gradlew test, 윈도우는 gradlew.bat test를 입력하면 된다.

BUILD SUCCESSFUL in 2s
이렇게 나오면 성공이다.
만약 테스트 결과까지 자세히 보고 싶으면
./gradlew test --info
를 입력하면 된다.
정리
BeanDefinition은 "빈을 만들기 위한 레시피"이다. 빈 자체(인스턴스)와는 다르다.- 진짜 스프링의
BeanDefinition인터페이스는 필드가 훨씬 많지만, 본질은 동일하다. "이 빈을 어떻게 만들지에 대한 모든 메타 정보를 한 객체에 모은 것"이다. - 진짜 스프링은
BeanDefinition이 인터페이스고,RootBeanDefinition,GenericBeanDefinition같은 구현체가 있다. 나는 단순화하기 위해 클래스를 하나로 가지만, 나중에 추상화가 필요해지면 그 때 인터페이스로 리팩토링할 것이다.
다음 단계
다음에는 클래스패스 스캐너를 만들 것이다. 사용자가 @Component를 붙인 클래스들을 자동으로 찾아내서, 각 클래스에 대해 BeanDefinition을 만들어주는 컴포넌트이다. 여기에서 1단계에 추가했던 reflections 라이브러리가 등장한다.
'cs > Spring Framework 구현' 카테고리의 다른 글
| [Spring Framework] 나만의 스프링 프레임워크 만들기 - 1 (프로젝트 생성) (0) | 2026.05.19 |
|---|