// Developer · Debugging Reports

뉴스도 디버깅이 필요해

마주친 오류를 재현하고, 원인을 추적하고, 기록합니다. Java · Spring · DB · 실무 트러블슈팅.

debug — today.log
$ debug --trace "오늘의 뉴스"
> 헤드라인 파싱 중...
> 원인 추적: 근거 누락 · 출처 불명
> 패치 적용: 사실 확인 + 출처 보강
✓ RESOLVED
TOTAL
TODAY
YESTERDAY

Categories

웹개발/Spring

Spring IoC와 DI 개념 SI 신입을 위한 10분 정리

자바를잡아2026. 6. 30. 09:00
반응형

IoC와 DI, 말부터 풀어 보기

SI 현장에 들어가면 IoC, DI라는 말을 끝없이 듣는다. 그런데 정의만 보면 더 헷갈린다. Spring IoC DI 개념을 신입 눈높이로 풀면 이렇다. DI(Dependency Injection, 의존성 주입)는 객체가 필요로 하는 다른 객체를 직접 만들지 않고 외부에서 넣어 주는 것이다. IoC(Inversion of Control, 제어의 역전)는 그 "넣어 주는 일"을 개발자 코드가 아니라 스프링이 대신 제어한다는 더 넓은 개념이다.

둘의 관계는 간단하다. IoC가 큰 원칙이고, DI는 그 원칙을 구현하는 대표적인 방법이다. 객체 생성과 연결의 주도권을 개발자가 아니라 스프링 컨테이너가 가져간다 — 이 한 문장이 핵심이다. 왜 굳이 그렇게 하는지는 코드로 보면 바로 와닿는다.

DI를 안 쓰면 무엇이 불편한가

1. 객체를 직접 만드는 코드

회원 서비스가 회원 DAO를 사용한다고 하자. DI를 안 쓰면 서비스가 DAO를 직접 new로 만든다.

// DI를 안 쓴 경우: 서비스가 DAO를 직접 생성
public class MemberService {
    // MemberService가 MemberDao를 직접 만든다 (강한 결합)
    private MemberDao memberDao = new MemberDaoImpl();

    public MemberVO getMember(String id) {
        return memberDao.selectMember(id);
    }
}

당장은 동작한다. 그런데 MemberDaoImpl을 다른 구현(예: 테스트용 가짜 DAO, 다른 DB용 DAO)으로 바꾸려면 이 코드를 직접 고쳐야 한다. 서비스가 특정 구현 클래스에 단단히 묶여 있어서다. 이걸 "강한 결합"이라 부른다.

2. 바뀌는 순간 드러나는 문제

구현을 교체할 때마다 new 부분을 찾아 수정해야 하고, 단위 테스트에서 가짜 객체를 끼워 넣기도 어렵다. 객체 하나가 다른 객체의 생성까지 책임지니, 변경이 생기면 연쇄적으로 손이 간다. 유지보수가 많은 SI/SM에서 이런 구조는 부담이 된다.

DI를 쓰면 달라지는 것

1. 외부에서 넣어 주는 코드

DI를 쓰면 서비스는 DAO를 직접 만들지 않는다. 필요한 DAO를 외부(스프링)가 넣어 준다. 보통 생성자로 받는다.

// DI를 쓴 경우: 생성자로 주입받는다
public class MemberService {
    private final MemberDao memberDao;

    // 누가 주든 MemberDao 구현체를 받아서 쓴다 (느슨한 결합)
    public MemberService(MemberDao memberDao) {
        this.memberDao = memberDao;
    }

    public MemberVO getMember(String id) {
        return memberDao.selectMember(id);
    }
}

서비스는 이제 "MemberDao라는 역할(인터페이스)"에만 의존하고, 어떤 구현체가 들어올지는 신경 쓰지 않는다. 구현을 바꿔도 서비스 코드는 그대로다. 테스트할 때 가짜 DAO를 넣기도 쉽다. 이게 "느슨한 결합"의 이점이다.

2. 그 주입을 스프링이 대신 한다 (IoC)

그럼 누가 MemberDao 구현체를 넣어 주는가. 개발자가 직접 new MemberService(new MemberDaoImpl())을 호출할 수도 있지만, 스프링은 이 일을 컨테이너가 대신한다. 객체(빈)를 만들고 서로 연결하는 제어권이 개발자에서 스프링으로 넘어간 것 — 이게 제어의 역전(IoC)이다.

스프링이 객체를 관리하는 방식: 빈과 컨테이너

스프링이 만들고 관리하는 객체를 빈(Bean)이라 하고, 빈을 담아 두고 연결해 주는 공간을 IoC 컨테이너(ApplicationContext)라 한다. 개발자는 "이 클래스를 빈으로 등록하라"고 알려 주기만 하면, 컨테이너가 객체를 만들고 필요한 곳에 주입한다.

빈을 등록하는 방법은 크게 두 가지다. 공공 SI 레거시에서 흔한 XML 방식과, 최근 많이 쓰는 어노테이션 방식이다.

<!-- 방법 A: XML 설정 (전통 Spring, 공공 SI에서 흔함) -->
<bean id="memberDao" class="com.example.member.dao.MemberDaoImpl" />
<bean id="memberService" class="com.example.member.service.MemberService">
    <constructor-arg ref="memberDao" />
</bean>
// 방법 B: 어노테이션 방식
@Repository
public class MemberDaoImpl implements MemberDao { ... }

@Service
public class MemberService {
    private final MemberDao memberDao;

    @Autowired // 생성자가 하나면 생략 가능
    public MemberService(MemberDao memberDao) {
        this.memberDao = memberDao;
    }
}

XML 방식은 <bean>으로 객체와 연결을 직접 적고, 어노테이션 방식은 @Service·@Repository로 빈을 표시하고 @Autowired로 주입을 맡긴다. 결과는 같다. 컨테이너가 객체를 만들어 연결해 준다.

주입 방식 비교

주입 방식 형태 특징
생성자 주입 생성자로 받음 권장. 불변(final) 가능, 누락 시 컴파일·기동 단계에서 발견
필드 주입 필드에 @Autowired 코드는 짧으나 테스트·불변성에 불리
setter 주입 setter 메서드로 받음 선택적 의존성에 사용

요즘은 생성자 주입을 기본으로 권장한다. 의존성이 명확히 드러나고, final로 불변을 보장할 수 있으며, 의존성을 빠뜨리면 객체를 못 만들어 문제를 일찍 발견할 수 있어서다. 기존 코드에서 @Autowired가 필드에 붙어 있는 걸 자주 보게 되는데, 동작은 하지만 신규 코드는 생성자 주입으로 가는 추세다.

SI 신입이 헷갈리는 부분

  • IoC와 DI를 완전히 다른 것으로 여기는 경우. IoC가 큰 개념, DI가 그 구현 방법이라고 보면 정리된다.
  • "@Autowired가 마법처럼 알아서 한다"고만 알고, 결국 컨테이너가 빈을 만들어 연결한다는 원리를 안 보는 경우.
  • 같은 인터페이스의 구현체가 둘 이상일 때 어느 걸 주입할지 몰라 오류가 나는 경우(@Qualifier 등으로 지정).
  • XML 설정만 있는 레거시에서 어노테이션을 기대하다 빈을 못 찾는 경우. 프로젝트의 설정 방식을 먼저 확인한다.
  • 빈은 싱글톤으로 관리된다는 점을 놓치고 상태를 함부로 두는 경우. 기본 스코프가 싱글톤임을 기억한다.

FAQ

Q1. IoC와 DI는 같은 말인가요?

완전히 같지는 않다. IoC(제어의 역전)는 객체 생성·연결의 제어권을 컨테이너가 갖는다는 큰 원칙이고, DI(의존성 주입)는 그 원칙을 구현하는 대표적인 방법이다.

Q2. @Autowired는 정확히 무슨 일을 하나요?

스프링 컨테이너에 등록된 빈 중에서 타입(또는 이름)이 맞는 것을 찾아 해당 자리에 넣어 준다. 개발자가 직접 객체를 만들어 연결하지 않아도 된다.

Q3. 생성자 주입과 필드 주입 중 무엇이 좋나요?

생성자 주입이 권장된다. 의존성이 명확히 드러나고 final로 불변을 보장할 수 있으며, 누락을 일찍 발견할 수 있다.

Q4. 같은 인터페이스 구현체가 여러 개면 어떻게 되나요?

스프링이 어느 빈을 주입할지 몰라 오류가 날 수 있다. @Qualifier로 빈 이름을 지정하거나 @Primary로 우선순위를 준다.

Q5. 공공 SI는 XML 설정만 쓰나요?

프로젝트마다 다르다. 레거시는 XML 빈 설정이 많지만, 어노테이션을 함께 쓰는 경우도 흔하다. 투입 시 해당 프로젝트의 설정 방식을 먼저 파악한다.

정리

Spring IoC DI 개념은 "객체를 직접 만들지 말고, 필요한 걸 외부에서 받아라(DI), 그 일은 스프링 컨테이너가 대신한다(IoC)"로 요약된다. DI를 안 쓰면 객체가 특정 구현에 강하게 묶여 변경과 테스트가 어렵고, DI를 쓰면 역할(인터페이스)에만 의존해 느슨하게 연결된다. 빈 등록은 XML이든 어노테이션이든 결과는 같고, 주입은 생성자 방식을 기본으로 본다. SI 신입이라면 개념을 외우기보다 "DI 안 쓴 코드와 쓴 코드가 어떻게 다른지"를 직접 비교해 보는 것이 가장 빠른 이해 방법이다. 투입된 프로젝트의 설정 방식(XML/어노테이션)을 먼저 확인하는 습관도 함께 들이면 좋다.

반응형