[스프링 로드맵] B-1. DI/Bean 기본 - new 지옥에서 벗어나기

2026. 1. 19. 17:08·백엔드

스프링을 처음 배우면 @Autowired, @Component 같은 애너테이션이 먼저 보이는데, 그걸 외우기 시작하면 금방 막힌다.

핵심은 애너테이션이 아니라 “new를 누가 하느냐”다.

  • 내가 new를 하면 → 결합이 강해지고 테스트가 어려워진다.
  • 스프링이 new를 해주면 → 결합이 약해지고 교체/테스트가 쉬워진다.

이 글은 IoC/DI/Bean을 한 세트로 이해시키는 게 목표다.


1. new 지옥: “직접 만들고 직접 연결하는 방식”

예를 들어 Todo API에서 컨트롤러가 서비스, 서비스가 저장소를 쓴다고 하자.

스프링을 안 쓰고 그냥 자바로 만들면 보통 이렇게 시작한다.

class TodoController {
  private final TodoService service = new TodoService(); // 직접 생성
}

class TodoService {
  private final TodoRepository repo = new TodoRepository(); // 또 직접 생성
}

class TodoRepository {
  // DB 접근
}

 

이 방식의 문제

  1. 강한 결합
  • TodoService가 TodoRepository를 직접 생성하면
  • 저장소 구현을 바꾸는 순간(DBRepo → MemoryRepo) 코드 전체가 흔들린다.
  1. 테스트가 어려움
  • TodoService만 테스트하고 싶은데, 내부에서 new TodoRepository()를 해버리면
  • 가짜 저장소(mock/fake)를 끼우기 힘들다.
  1. 중복 생성/관리 난잡
  • 여기저기서 new가 발생하면 “객체가 언제 몇 개 만들어졌는지” 추적이 어렵다.

이걸 한 번에 해결하는 철학이 IoC/DI/Bean이다.


2. IoC Container (Inversion of Control)

객체 생성/연결의 제어권이 개발자 코드에서 ‘컨테이너’로 넘어간다.

스프링에서는 이 컨테이너를 보통 ApplicationContext(스프링 컨테이너) 라고 부른다.

컨테이너가 하는 일:

  • 객체를 만들고(생성)
  • 필요한 객체끼리 연결해주고(주입)
  • 생명주기까지 관리한다(시작/종료)

3. Bean

스프링 컨테이너가 만들어서 등록하고 관리하는 객체

  • new로 만든 객체: 그냥 객체
  • 스프링이 만들어서 관리하는 객체: 빈(Bean)

빈은 기본적으로 싱글톤 스코프가 많다.

즉, 애플리케이션 전체에서 보통 1개 만들어 공유한다.

“싱글톤 = 고유하다”가 아니라

“애플리케이션 실행 동안 같은 인스턴스를 재사용한다”가 더 정확하다.


4. DI (Dependency Injection)

객체가 의존성을 직접 만들지 않고, 외부에서 주입받는다.

스프링에서는 외부가 누구냐?

  • 바로 IoC 컨테이너다.

5. DI의 정석: 생성자 주입(기본값처럼 쓰는 이유)

스프링에서 DI 방법은 여러 개가 있지만, 기본적으로 생성자 주입이 쓰인다.

@RestController
class TodoController {
  private final TodoService service;

  public TodoController(TodoService service) {
    this.service = service;
  }
}
@Service
class TodoService {
  private final TodoRepository repo;

  public TodoService(TodoRepository repo) {
    this.repo = repo;
  }
}
@Repository
class TodoRepository { }

생성자 주입이 기본인 이유

  • 필수 의존성 강제: 필요한 게 없으면 생성 자체가 안 됨
  • final 사용 가능: 불변 구조 만들기 쉬움
  • 테스트 쉬움: 가짜 repo를 constructor로 넣으면 끝
  • 설계가 드러남: 클래스가 뭘 필요로 하는지 코드만 봐도 보임

6) “IoC/Bean/DI”를 흐름으로 묶으면

스프링 부트 앱이 뜰 때 내부에서 대략 이런 일이 벌어진다.

  1. 스프링 컨테이너(ApplicationContext) 생성
  2. 컴포넌트 스캔으로 클래스 탐색: @RestController, @Service, @Repository 같은 것들
  3. 해당 클래스의 인스턴스를 만들고 → Bean으로 등록
  4. 생성자 파라미터를 보고 의존 빈을 찾아 → DI로 주입
  5. 기동 완료

컨테이너가(IoC) 객체를 만들고(Bean) 연결한다(DI)


7. @Autowired는 어디에 끼는가(짧게만)

DI를 표현하는 방법 중 하나가 @Autowired다.

그런데 생성자 주입을 쓰면 스프링이 보통 알아서 주입해줘서 @Autowired가 생략되는 경우가 많다.

@Service
class TodoService {
  private final TodoRepository repo;

  // 생성자 1개면 보통 @Autowired 없어도 주입됨
  public TodoService(TodoRepository repo) {
    this.repo = repo;
  }
}

필드 주입처럼 쓰면 이렇게 되지만, 실제로는 지양하는 편이다.

@Autowired
private TodoRepository repo;

8. “new vs DI”를 한 문장으로 비교

  • new 지옥: 객체가 객체를 직접 만들고 직접 연결한다 → 결합이 강함
  • DI 구조: 객체는 “필요한 것”만 선언하고, 연결은 컨테이너가 한다 → 결합이 약함

'백엔드' 카테고리의 다른 글

[스프링 로드맵] B-3. DI/Bean 실전 - @Autowired, @Qualifier, Profile  (0) 2026.01.19
[스프링 로드맵] B-2. DI/Bean 내부 - 빈 등록 방식과 라이프사이클  (1) 2026.01.19
[스프링 로드맵] A-2. REST 확장 - POST/PUT/DELETE와 ResponseEntity  (0) 2026.01.19
[스프링 로드맵] A-1. REST 입문 - JSON이 응답되는 이유  (0) 2026.01.19
[스프링 로드맵] 0편. 전체 그림 – REST부터 JPA까지 한 번에 보기  (0) 2026.01.19
'백엔드' 카테고리의 다른 글
  • [스프링 로드맵] B-3. DI/Bean 실전 - @Autowired, @Qualifier, Profile
  • [스프링 로드맵] B-2. DI/Bean 내부 - 빈 등록 방식과 라이프사이클
  • [스프링 로드맵] A-2. REST 확장 - POST/PUT/DELETE와 ResponseEntity
  • [스프링 로드맵] A-1. REST 입문 - JSON이 응답되는 이유
samsam031
samsam031
samsam031 님의 블로그 입니다.
  • samsam031
    samsam031 님의 블로그
    samsam031
  • 전체
    오늘
    어제
    • 분류 전체보기
      • 디지털포렌식
      • 드림핵 문제풀이
      • 대외활동
      • 개발 실습
      • 컴퓨터 보안
      • 클라우드
      • 자격증
      • 자연어처리
      • 백엔드
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
samsam031
[스프링 로드맵] B-1. DI/Bean 기본 - new 지옥에서 벗어나기
상단으로

티스토리툴바