TIL/[TIL]

[TIL]Spring Core: DI, Spring Container, Bean, 계층 구조, @Autowired와 컴포넌트 스캔

namerong 2026. 5. 26. 15:16

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. 핵심 정리

  1. Spring Container는 Spring이 객체를 만들고 보관하고 관리하는 공간이다.
  2. Bean은 Spring Container가 관리하는 객체이다.
  3. XML 설정, Java Config의 @Bean, Component Scan의 @Component 계열은 모두 Bean을 등록하는 방식이다.
  4. @Configuration 클래스는 Spring 설정을 담는 클래스이다.
  5. @Bean은 메서드가 반환한 객체를 Spring Bean으로 등록한다.
  6. chap02-di / section03은 Spring이 Controller -> Service -> Repository 객체를 대신 조립하는 흐름을 보여준다.
  7. Controller -> Service -> Repository 구조는 역할을 분리해 유지보수성을 높이기 위한 구조다.
  8. 인터페이스에 의존하면 구현체를 바꾸기 쉬워지고 결합도가 낮아진다.
  9. chap03은 @Repository, @Service, Component Scan으로 Bean 등록을 자동화하는 단계다.
  10. @Autowired는 필요한 의존 객체를 Spring이 자동으로 주입하게 만드는 어노테이션이다.
  11. 생성자 주입은 실무에서 가장 많이 쓰이고 가장 권장되는 방식이다.
  12. 필드 주입과 setter 주입도 가능하지만, 필수 의존성에는 생성자 주입이 가장 안정적이다.