1. 학습 주제
이번 범위는 단순히 “어노테이션 몇 개 외우기”가 아니라,
기존에 내가 new로 직접 객체를 만들던 방식을 Spring이 어떻게 대신하는지,
그리고 왜 Controller -> Service -> Repository 구조로 나누는지를 이해하는 게 중요했다.
학습 흐름은 크게 이렇게 이어진다.
- Spring Container가 무엇인지
- Bean이 무엇인지
- XML 대신 Java Config에서 @Bean으로 객체를 등록하는 방식
- 여러 객체를 계층 구조로 나눠 조립하는 DI 구조
- @ComponentScan, @Service, @Repository로 Bean을 자동 등록하는 방식
- @Autowired로 의존 객체를 주입받는 방식
- 생성자 주입, 필드 주입, setter 주입 차이
2. Spring Container란 무엇인가
적어준 문장이 핵심을 잘 잡고 있다.
Spring Container
Spring이 객체를 만들고, 보관하고, 관리하는 공간
이걸 조금 더 풀면:
- 예전에는 내가 new로 직접 객체를 만들었다
- Spring에서는 필요한 객체를 Spring Container가 대신 생성한다
- 그리고 그 객체끼리 필요한 의존관계까지 연결해준다
즉 Spring Container는 단순 저장소가 아니라:
- 객체 생성
- 객체 보관
- 객체 조립
- 객체 제공
까지 해주는 관리자이다.
3. Bean이란 무엇인가
Bean
Spring Container가 관리하는 객체
이 말이 가장 중요하다.
모든 객체가 Bean은 아니다.
그냥 new로 내가 직접 만든 일반 객체는 Bean이 아니다.
예를 들어:
- MemberDTO
- MemoController
- MemoService
- MemoRepository
- BookService
- BookDAOImpl
이런 객체가 Spring Container 안에 등록되어 관리되면 Bean이 된다.
즉 Bean은 그냥 “객체”가 아니라,
Spring이 알고 있고, 필요할 때 꺼내주고, 다른 객체와 연결까지 관리하는 객체라고 이해하면 된다.
4. Spring이 왜 필요한가
Spring을 배우기 전에는 보통 이런 식으로 객체를 만들었다.
MemoRepository repo = new MemoryMemoRepository();
MemoService service = new MemoService(repo);
MemoController controller = new MemoController(service);
이 방식은 작은 프로젝트에서는 괜찮다.
하지만 객체가 많아지면 문제가 생긴다.
- 누가 어떤 객체를 만들지 코드가 복잡해짐
- 객체 관계가 꼬이기 쉬움
- 구현체가 바뀔 때 수정 범위가 커짐
- 테스트나 유지보수가 어려워짐
Spring은 이 문제를 해결하기 위해 등장한다.
즉 Spring의 핵심은:
- 객체 생성 책임을 개발자 코드에서 분리하고
- 객체 간 의존관계를 컨테이너가 대신 연결하게 만드는 것
이다.
이게 바로 DI(Dependency Injection, 의존성 주입) 이다.
5. Java Config와 @Bean
이번 chap02-di / section02에서는 Java Config 방식으로 Bean을 직접 등록했다.
@Configuration
public class ContextConfiguration {
여기서 @Configuration은
“이 클래스는 Spring 설정 클래스야”라고 알려주는 역할이다.
그리고 안에서 @Bean 메서드로 객체를 등록한다.
@Bean
public Account accountGenerator() {
return new PersonalAccount(20, "110-234-567890");
}
@Bean
public MemberDTO memberGenerator() {
return new MemberDTO(1, "홍길동", accountGenerator());
}
이 의미는:
- accountGenerator()가 반환한 객체를 Bean으로 등록
- memberGenerator()가 반환한 객체도 Bean으로 등록
- memberGenerator() 안에서 accountGenerator()를 호출해 의존 객체를 조립
즉 Java Config는
설정 클래스 안에서 “이 객체들을 이렇게 조립해서 Bean으로 써줘”라고 직접 적는 방식이다.
6. ApplicationContext는 무엇인가
예제에서는 이렇게 컨테이너를 만들었다.
ApplicationContext context
= new AnnotationConfigApplicationContext(ContextConfiguration.class);
이 코드는:
- ContextConfiguration.class를 읽고
- 거기서 @Configuration, @Bean 정보를 확인하고
- Bean들을 생성해서 Spring Container를 구성한다
그 후에는 이렇게 Bean을 꺼낼 수 있다.
MemberDTO member = context.getBean(MemberDTO.class);
즉 ApplicationContext는
Spring Container를 다루는 대표 인터페이스라고 보면 된다.
7. chap02-di / section03 자세히 정리
여기는 꼭 흐름을 이해해야 한다.
구조는 다음과 같다.
- MemoController
- MemoService
- MemoRepository (인터페이스)
- MemoryMemoRepository (구현체)
- MemoDTO
- ContextConfiguration
이 구조는 실무에서 자주 보는 계층 구조의 아주 축소판이다.
8. 계층 구조를 왜 나누는가
적어준 문장처럼 실무에서는 보통 이런 구조를 많이 쓴다.
Controller -> Service -> Repository
이건 역할 분리 때문이다.
Controller
- 사용자의 요청을 가장 먼저 받는 계층
- 입력값을 받고
- 어떤 기능을 실행할지 결정하고
- 결과를 다시 돌려준다
Service
- 실제 비즈니스 로직을 담당
- “무슨 일을 할지”를 처리
- 여러 Repository를 조합하기도 한다
Repository
- 데이터 저장소 접근 담당
- DB 조회, 저장, 수정, 삭제 같은 역할
- 지금 예제에서는 DB 대신 메모리 리스트를 사용
즉 구조를 나누는 이유는
한 클래스가 모든 역할을 다 하지 않게 하기 위해서다.
9. MemoRepository가 인터페이스인 이유
public interface MemoRepository {
List<MemoDTO> findAll();
MemoDTO save(String content);
}
이건 아주 중요하다.
Spring이나 객체지향에서 인터페이스를 두는 이유는:
- “무엇을 할 수 있는지”를 먼저 약속하고
- “어떻게 할지는” 구현체가 결정하게 하기 위해서다
즉 MemoRepository는 계약서 역할이다.
이 말은 나중에 구현체를 바꿀 수 있다는 뜻이다.
예를 들면:
- 지금은 MemoryMemoRepository
- 나중에는 JdbcMemoRepository
- 더 나중에는 JpaMemoRepository
로 바뀔 수 있다.
그런데 MemoService는 MemoRepository 타입만 알고 있으면 된다.
구현체가 메모리인지, JDBC인지, JPA인지 몰라도 된다.
이게 결합도를 낮추는 구조다.
10. MemoryMemoRepository는 실제로 무엇을 하는가
public class MemoryMemoRepository implements MemoRepository
이 클래스는 실제 저장소 역할을 한다.
지금은 DB가 아니라 메모리 리스트를 쓴다.
private final List<MemoDTO> memos = new ArrayList<>();
그리고 생성자에서 초기 데이터도 넣는다.
save("Servlet에서 직접 만들던 객체 흐름 복습");
save("Spring Container가 객체를 대신 조립");
즉 이 클래스는 지금 단계에서는
“DB 대신 메모리 안에 데이터를 저장하는 가짜 저장소”라고 보면 된다.
중요한 건 저장 방식 자체보다,
Service가 이 구현체를 직접 의식하지 않고 인터페이스로만 의존한다는 점이다.
11. MemoService는 무엇을 하는가
public class MemoService {
private final MemoRepository memoRepository;
public MemoService(MemoRepository memoRepository) {
this.memoRepository = memoRepository;
}
}
여기서 핵심은 생성자다.
MemoService는 스스로 new MemoryMemoRepository() 하지 않는다.
대신 밖에서 MemoRepository를 넣어준다.
이게 바로 의존성 주입(DI) 이다.
즉 서비스는:
- 저장소가 필요하긴 한데
- 어떤 구현체인지는 스스로 만들지 않고
- 외부(Spring)가 넣어준다
이렇게 하면 MemoService는 훨씬 유연해진다.
12. MemoController는 무엇을 하는가
public class MemoController {
private final MemoService memoService;
public MemoController(MemoService memoService) {
this.memoService = memoService;
}
}
이것도 마찬가지다.
MemoController는 MemoService가 필요하다.
하지만 직접 new MemoService(...) 하지 않는다.
Spring이 만들어서 넣어준다.
즉 흐름은 이렇게 된다.
- Controller는 Service에 의존
- Service는 Repository에 의존
- Repository는 실제 데이터 저장 담당
이걸 한 줄로 쓰면:
Controller -> Service -> Repository
가 된다.
13. ContextConfiguration이 실제로 하는 일
이제 가장 중요한 조립 과정을 보자.
@Bean
public MemoRepository memoRepository(){
return new MemoryMemoRepository();
}
Spring에게 말한다.
- MemoRepository 역할을 할 Bean은 MemoryMemoRepository로 만들게
다음:
@Bean
public MemoService memoService(){
return new MemoService(memoRepository());
}
의미:
- MemoService Bean을 만들 때
- 방금 만든 memoRepository() Bean을 넣어서 생성할게
다음:
@Bean
public MemoController memoController(){
return new MemoController(memoService());
}
의미:
- MemoController Bean을 만들 때
- memoService() Bean을 넣어서 생성할게
즉 이 설정 클래스는
객체를 직접 new로 만들고 연결하던 일을 Spring 방식으로 옮겨놓은 설계도다.
14. section03을 가장 쉽게 이해하는 비유
이걸 레고 조립처럼 생각하면 쉽다.
예전 방식:
- 내가 레고 부품도 만들고
- 직접 끼우고
- 완성품도 관리함
Spring 방식:
- 조립 설명서를 ContextConfiguration에 적어두면
- Spring이 부품을 만들고
- 필요한 것끼리 끼워서
- 완성품 Bean을 보관해줌
즉 개발자는 “어떻게 조립할지”만 적고,
실제 조립과 보관은 Spring Container가 담당한다.
15. section03에서 왜 이해가 어려웠는가
이 부분이 어려운 이유는 보통 세 가지다.
1. 눈에 보이는 new가 줄어든다
예전에는 new MemoService(...)가 보여서 이해가 쉬웠다.
Spring에서는 그걸 설정이나 컨테이너가 대신한다.
2. 객체를 내가 만드는 게 아니라 Spring이 만든다
즉 제어권이 뒤집힌다.
3. 인터페이스와 구현체가 분리되어 있다
MemoRepository와 MemoryMemoRepository가 따로 있으니 처음엔 “왜 굳이 나눴지?” 싶다.
하지만 이 구조를 이해하면 나중에 훨씬 큰 프로그램도 관리할 수 있게 된다.
16. chap03: 어노테이션 기반 DI
이제 chap03_di-annotation은
앞에서 직접 @Bean으로 등록하던 걸 더 자동화하는 단계다.
즉 이전 단계는:
- 설정 클래스에 @Bean 메서드를 직접 작성
이번 단계는:
- 클래스에 @Repository, @Service 같은 어노테이션을 붙이고
- Spring이 스캔해서 자동 등록
하는 방식이다.
17. Component Scan이란
예제에서 컨테이너를 이렇게 만들었다.
ApplicationContext context
= new AnnotationConfigApplicationContext("com.ohgiraffers");
이건:
- com.ohgiraffers 패키지 아래를 스캔해서
- 특정 어노테이션이 붙은 클래스를 찾고
- 자동으로 Bean으로 등록하라
는 뜻이다.
즉 Component Scan은
“이 패키지 안을 뒤져서 Spring이 관리해야 할 객체를 자동으로 찾아라”는 기능이다.
18. 어떤 어노테이션이 Bean 등록 대상인가
예제에서 나온 것들:
- @Repository
- @Service
이들은 사실 @Component의 세분화 버전이다.
즉:
- @Repository : 저장소 계층 Bean
- @Service : 서비스 계층 Bean
- @Component : 일반적인 Bean
이라고 이해하면 된다.
즉 적어준 정리처럼:
- XML 설정
- Java Config의 @Bean
- ComponentScan의 @Component 계열
모두 결국은 Bean 등록 방식의 차이다.
19. BookDAOImpl는 어떻게 Bean이 되는가
@Repository("bookDAO")
public class BookDAOImpl implements BookDAO
이제 이 클래스는:
- 스캔 대상 패키지 안에 있고
- @Repository가 붙어 있으므로
- Spring이 자동으로 객체를 만들고 Bean으로 등록한다
즉 더 이상 설정 클래스에:
@Bean
public BookDAO bookDAO() { ... }
를 직접 적지 않아도 된다.
Spring이 알아서 찾아준다.
20. BookService는 어떻게 Bean이 되는가
예를 들어 생성자 주입 버전:
@Service("bookServiceConstructor")
public class BookService {
이 클래스도 마찬가지로 Component Scan 대상이 되어 Bean이 된다.
그런데 BookService는 BookDAO가 필요하다.
private final BookDAO bookDAO;
그럼 Spring은:
- 컨테이너에 있는 BookDAO 타입 Bean을 찾고
- BookService 생성자에 넣어준다
즉 이 단계에서는 Bean 등록도 자동, 의존 객체 연결도 자동으로 간다.
21. @Autowired란 무엇인가
@Autowired는
Spring아, 이 의존 객체를 자동으로 연결해줘
라는 뜻이다.
즉:
- 내가 직접 new 해서 넣는 게 아니라
- Spring이 타입을 보고 적절한 Bean을 주입해준다
이게 DI의 자동화 버전이다.
22. 생성자 주입
가장 먼저 나오는 방식이다.
public BookService(BookDAO bookDAO) {
this.bookDAO = bookDAO;
}
예제 주석처럼 실무에서 가장 기본으로 보는 방식이다.
장점:
- 객체가 생성될 때 반드시 필요한 의존성이 보장된다
- final 사용 가능
- 불변 구조 만들기 좋다
- 테스트하기 좋다
- 의존관계가 코드에 명확히 드러난다
즉 실무에서는 생성자 주입이 가장 권장되는 방식이라고 보면 된다.
그리고 생성자가 하나뿐이면 @Autowired를 생략해도 Spring이 자동으로 주입한다.
23. 필드 주입
@Autowired
private BookDAO bookDAO;
이건 코드가 짧아서 처음엔 편해 보인다.
하지만 단점도 있다.
- 외부에서 주입 구조가 잘 안 보인다
- 테스트가 불편하다
- 불변 구조 만들기 어렵다
즉 학습용으로는 이해하기 쉽지만,
실무에서는 생성자 주입보다 덜 선호된다.
24. setter 주입
@Autowired
public void setBookDAO(BookDAO bookDAO) {
this.bookDAO = bookDAO;
}
이 방식은:
- 생성 후 나중에 주입할 수 있다
- 선택적 의존성에 사용할 수도 있다
하지만 대부분의 필수 의존성은 생성자 주입으로 처리하는 경우가 더 많다.
즉 정리하면:
- 필수 의존성: 생성자 주입
- 선택적/변경 가능 의존성: setter 주입 가능
- 필드 주입: 간단하지만 실무 선호도는 낮음
25. chap03을 가장 쉽게 이해하는 방법
chap02와 chap03의 차이를 딱 하나로 줄이면 이렇다.
chap02
내가 설정 클래스에서 @Bean으로 직접 등록하고 직접 연결했다.
chap03
클래스에 @Repository, @Service를 붙여두면
Spring이 스캔해서 자동으로 Bean 등록하고, @Autowired로 자동 연결한다.
즉 chap03은
Spring이 객체 등록과 연결을 더 자동으로 해주는 단계다.
26. Controller -> Service -> Repository 구조 다시 정리
적어준 문장을 중심으로 다시 정리하면:
Controller
- 요청을 받는 입구
- 사용자 입력을 받는다
- 어떤 서비스를 호출할지 결정한다
Service
- 비즈니스 로직 담당
- 실제 규칙과 처리 흐름을 담당한다
Repository
- 데이터 저장소 접근 담당
- DB나 메모리 같은 저장소와 통신한다
이 구조로 나누는 이유는
역할별 책임을 분리해서 유지보수하기 쉽게 만들기 위해서다.
27. 이번 범위 전체 흐름 정리
1단계: Spring Container와 Bean
- Spring이 객체를 만들고 보관하는 컨테이너를 이해
- 컨테이너가 관리하는 객체가 Bean이라는 점 이해
2단계: Java Config 방식
- @Configuration
- @Bean
- 설정 클래스에서 객체를 직접 조립
3단계: 계층 구조와 DI
- Controller -> Service -> Repository
- 인터페이스와 구현체 분리
- 생성자 주입으로 의존성 전달
4단계: 어노테이션 기반 자동 등록
- @Repository, @Service
- Component Scan
- @Autowired
- 생성자/필드/setter 주입 비교
28. 핵심 정리
- Spring Container는 Spring이 객체를 만들고 보관하고 관리하는 공간이다.
- Bean은 Spring Container가 관리하는 객체이다.
- XML 설정, Java Config의 @Bean, Component Scan의 @Component 계열은 모두 Bean을 등록하는 방식이다.
- @Configuration 클래스는 Spring 설정을 담는 클래스이다.
- @Bean은 메서드가 반환한 객체를 Spring Bean으로 등록한다.
- chap02-di / section03은 Spring이 Controller -> Service -> Repository 객체를 대신 조립하는 흐름을 보여준다.
- Controller -> Service -> Repository 구조는 역할을 분리해 유지보수성을 높이기 위한 구조다.
- 인터페이스에 의존하면 구현체를 바꾸기 쉬워지고 결합도가 낮아진다.
- chap03은 @Repository, @Service, Component Scan으로 Bean 등록을 자동화하는 단계다.
- @Autowired는 필요한 의존 객체를 Spring이 자동으로 주입하게 만드는 어노테이션이다.
- 생성자 주입은 실무에서 가장 많이 쓰이고 가장 권장되는 방식이다.
- 필드 주입과 setter 주입도 가능하지만, 필수 의존성에는 생성자 주입이 가장 안정적이다.
'TIL > [TIL]' 카테고리의 다른 글
| [TIL]Spring Boot 요청 매핑, 핸들러 메서드, 세션 처리, 그리고 AOP (0) | 2026.05.28 |
|---|---|
| [TIL]Spring Bean, DI, Life Cycle, Scope, 외부 설정값, AOP 정리 (0) | 2026.05.27 |
| [TIL]쿠키, 세션, 인증/인가, 그리고 Front Controller 흐름 (0) | 2026.05.22 |
| [TIL]Servlet JSON API에서 JDBC 연동까지: 프론트엔드, 서블릿, 서비스, DAO, DB 흐름 이해하기 (0) | 2026.05.21 |
| [TIL]Servlet 기초부터 요청/응답, 파라미터 처리, JSON API까지 (0) | 2026.05.20 |