취업 준비를 하면서 혼자 톰캣도 직접 자바 코드로 짜보고 (블로그에 작성하진 않았지만..) 알고리즘 문제도 종종 풀고 있다.
하지만 어떤 프로젝트를 해야할지 너무 막막했지만 이전에 톰캣을 직접 구현해본 것처럼 이번에는 자바 스프링 프레임워크를 구현해보고 싶었다.
사실 이미 잘 만들어져있는 프레임워크나 소스들을 왜 굳이 내가 하나씩 다시 구현할까 싶겠지만 나는 주변 사람들한테 cs 관련 개념이 매우 중요하다는 사실을 알게 되었고, 나 자신도 이에 대해 생각해봤을 때 부족하다는 사실을 누구보다 잘 알고 있었다.
그래서 cs공부를 제대로 해보고자 이러한 개념들을 좀 딥하게 파보려고 한다.
우선 클로드를 활용하여 전체적인 로드맵을 잡았다
전체 로드맵
- 프로젝트 생성 & 핵심 어노테이션
BeanDefinition— 빈의 메타정보 표현- 클래스패스 스캐너 —
@Component붙은 클래스 찾기 BeanFactory— 리플렉션으로 인스턴스 만들기 & 싱글톤 관리- 의존성 주입 —
@Autowired처리 ApplicationContext— 위 전부를 묶어 부트스트랩BeanPostProcessor— 빈 생성 전후 훅- AOP — 동적 프록시,
@Aspect,@Around - MVC —
DispatcherServlet,@Controller
각 단계마다 왜 필요한지? 어떻게 동작하는지를 깊게 파볼 생각이다.
프로젝트 생성
바로 프로젝트 생성부터 진행하였다.

프로젝트 이름은 mini-spring으로 지정하였고, 시스템 빌드는 Gradle, JDK는 자바 17버전을 선택했다.
그리고 Gradle DSL은 Groovy를 선택했다
그룹 ID와 아티팩트ID는 각각 com.mini, mini-spring으로 지정했다.

프로젝트를 생성한 다음 실행하면 다음과 같은 화면을 볼 수 있다.

일단 코드 실행을 하게 되면 정상적으로 결과가 출력되는 것을 확인할 수 있다.
기본 세팅을 위해서 build.gradle 파일을 열어서 한 번 확인해보자.

이와 같이 구성되어있는 것을 확인할 수 있다. 이를 수정할 것이다.

다음과 같이 수정했다.
의존성에 보면 spring 관련 내용이 하나도 없는 것을 확인할 수 있다.
왜냐하면 내가 직접 만들 것이기 때문이다.reflections는 단순히 특정 패키지 아래 모든 클래스 목록을 가져오는 유틸일 뿐, 스프링과 관련 없는 일반 자바 라이브러리다.

수정 후, 이 코끼리 버튼을 눌러 수정 사항을 업데이트해주면 된다.
단순히 수정을 해주는 것이 아니라 의존성 다운로드를 해주는 것이다.
패키지 구조 만들기
├── annotation/ # @Component, @Autowired 등
├── beans/ # BeanDefinition, BeanFactory
├── context/ # ApplicationContext
├── core/ # 스캐너, 유틸리티
└── sample/ # 테스트용 더미 빈
패키지 구조를 다음과 같이 만들 것이다.
@Component 어노테이션
첫 코드는 Component.java를 만든다.
우선 다음과 같이 작성했다
package com.mini.spring.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
String value() default "";
}
여기에서는 두 가지가 핵심이다
@Retention(RUNTIME)- 이게 없으면 컴파일 후 사라져서 리플렉션으로 못읽는다. 스프링이 어노테이션 기반으로 돌아가는 비밀이 여기에 있다.SOURCE/CLASS/RUNTIME세 가지 차이는 아래에서 한 번 더 알아볼 것이다.value()- 빈 이름을 직접 지정하고 싶을 때 쓴다. (예:@Component("myService")). 비워두면 4단계에서 "클래스명 첫 글자 소문자" 규칙으로 자동생성한다.
테스트용 더미 빈
다음 단계에서 스캐너가 잘 찾는지 확인할 대상이 필요하니, 샘플 빈을 하나 만든다.
package com.mini.spring.sample;
import com.mini.spring.annotation.Component;
@Component
public class HelloService {
public String hello() {
return "Hello, Mini Spring!";
}
}
빌드 확인
터미널을 프로젝트 루트에서 열고 다음 명령어를 실행해본다.
윈도우 : gradlew.bat build
맥 : ./gradlew build
결과적으로 두 운영체제 모두 BUILD SUCCESSFUL가 뜨면 성공이다.
SOURCE / CLASS / RUNTIME 의 차이점
그렇다면 이 세 가지의 차이점은 무엇일까?
자바 코드가 실행되기까지의 3단계는 아래와 같다
.java 파일 → [컴파일] → .class 파일 → [JVM 로드] → 실행 중인 객체
RetentionPolicy는 "어노테이션 정보를 이 흐름 중 어느 단계까지 살려둘 것인가"를 결정하는 설정이다.
단계마다 정보가 하나씩 버려진다고 생각하면 된다.
SOURCE - 컴파일 시점에 버려짐
.java 파일(소스)에만 존재한다. 컴파일러가 .class 파일을 만들 때 떼어내고 버린다. 그래서 바이트코드에도 없고, 당연히 런타임에도 못 읽는다.
언제쓸까?
컴파일러나 IDE에게 힌트를 주는 용도로 사용한다.
대표 예시:
@Override- 컴파일러가 "이거 진짜 부모 메서드 오버라이드 맞나?" 검증하고 끝난다. 검증 후에는 필요 없으니 버린다.@SuppressWarnings("unchecked")- 컴파일러에게 이 경고 무시하라고 알려주고 끝난다.- Lombok의
@Getter,@Setter- 컴파일 시점에 코드를 생성하고 자기 자신은 사라진다.
CLASS - .class 파일에는 있지만 런타임에는 안 읽힌다 (기본값)
.class 바이트코드 파일에는 기록되지만, JVM이 클래스를 메모리로 로드할 때 무시한다. 따라서 리플렉션으로 못 읽는다. 이 말이 어려울 수 있는데 리플렉션이란 실행 중에 클래스를 조사하고 인스턴스를 만들고 메서드를 호출하는 자바 기능이라고 알고 있으면 될 것 같다.
언제쓸까?
바이트코드를 분석하는 외부 도구가 필요로 할 때 사용한다.
대표 예시:
- 정적 분석 도구가 .class 파일을 직접 뜯어볼 때
- 일부 AOP 도구가 컴파일된 바이트코드를 수정할 때 참조한다.
일반 애플리케이션 개발에서 직접 만들 일은 거의 없다고 한다. @Retention을 안붙이면 기본값이 CLASS라는 정도로 알아두면 될 것 같다. 그래서 내가 @Component에 명시적으로 RUNTIME을 붙인 것이다. 안붙이면 못읽기 때문이다.
RUNTIME - 끝까지 살아남음
.class에도 있고, JVM이 클래스를 로드할 때도 유지된다. 따라서 리플렉션으로 읽을 수 있다.
언제쓸까?
프레임워크가 런타임에 어노테이션을 보고 동작을 결정할 때 사용한다.
대표 예시:
- 스프링의
@Component,@Autowired,@Transactional - JUnit의
@Test - JPA의
@Entity,@Column
현재 내 프로젝트에서는 @Component 붙은 클래스를 실행 중에 찾아내서 빈으로 등록할 것이기 때문에 RUNTIME을 사용할 것이다.
나의 생각
사실 지금까지 스프링 혹은 스프링 부트 프로젝트를 진행하면서 어노테이션에 대해 깊게 생각해보지 않았다.. 무슨 기능을 하는지 정확히 모르고 사용했다고 봐도 무방하다.
하지만 이제부터는 좀 더 확실히 개념을 정리하고 무슨 의미인지 알면서 코딩을 하고 싶다는 생각이 강해졌다. 그래야 나중에 유지보수 하기도 쉬워질테고, 이게 나의 무기가 될 수도 있겠다는 생각이 들었다.
'cs > Spring Framework 구현' 카테고리의 다른 글
| [Spring Framework] 나만의 스프링 프레임워크 만들기 - 2 (BeanDefinition) (0) | 2026.05.20 |
|---|