Spring DI 정리: 왜 의존성 주입이 중요한가?

안세홍·2025년 11월 24일
post-thumbnail

📌 1. 의존성(Dependency)란?

어떤 객체가 작업을 수행하기 위해 다른 객체를 직접 생성하거나 사용해야 할 때 그 관계를 ‘의존한다’고 표현한다.

예를 들어, Roel 객체가 Galaxy 객체를 직접 생성하고 사용한다면,

Roel → Galaxy 로 의존성이 존재한다.

💡 요약
한 객체의 코드에서 다른 객체를 생성하거나 호출하면 의존성이 생긴 것이다.

📌 2. 결합도(Coupling)와 DI의 필요성

의존성의 강도를 ‘결합도(Coupling)’라고 한다.

결합도특징
강한 결합 (Tight Coupling)특정 구현 클래스에 직접 의존 → 수정 어려움
느슨한 결합 (Loose Coupling)인터페이스 기반으로 의존 → 유연하고 교체 쉬움

🔥 강한 결합의 예시


public class Roel {
    private Galaxy phone;

    public Roel() {
        this.phone = new Galaxy();
    }
}

만약 Galaxy 대신 IPhone을 사용하고 싶다면?

→ Roel 내부 전체 코드를 수정해야 한다.

이처럼 구현 클래스에 강하게 묶여 있으면 변경에 취약하다.


📌 3. 느슨한 결합을 위한 방법: 인터페이스 사용

인터페이스를 도입하면 구현(Mobile 기종)이 바뀌어도

Roel 클래스는 Phone 인터페이스만 알고 있으면 된다.

🔽 Phone 인터페이스


public interface Phone {
    void powerOn();
    void payment();
}

🔽 구현 클래스들


public class Galaxy implements Phone { ... }
public class IPhone implements Phone { ... }

🔽 느슨한 결합의 Roel 클래스


public class Roel {
    private final Phone phone;

    public Roel(Phone phone) {
        this.phone = phone;
    }
}

Roel은 더 이상 Galaxy, IPhone이 무엇인지 알 필요가 없다.

유연한 구조, 유지보수성 UP!


📌 4. DI(Dependency Injection)란?

DI는 말 그대로 의존성(Dependency)을 외부에서 주입(Injection)받는 방식이다.

즉, “필요한 객체를 스스로 new 로 만들지 않고, 외부에서 넣어주는 방식이다.”

💡 DI란?
객체가 사용할 객체의 생성 책임을 외부(스프링 컨테이너)에 넘기고
자신은 주입받은 객체만 사용한다.

이는 스프링이 지향하는 IoC(제어의 역전) 개념을 구체화한 것이다.

그래서 스프링을 IoC 컨테이너 혹은 DI 컨테이너라고도 부른다.


📌 5. DI의 장점

✔ 1) 결합도 감소 → 변경에 강한 코드

  • 구현체를 교체해도 생성자만 바뀌면 됨
  • 유지보수가 쉬움

✔ 2) 테스트 용이성 증가

  • Mock 객체를 주입할 수 있어 단위 테스트가 쉬워짐

📌 6. Spring DI 주입 방법 (3가지)

스프링에서는 다음과 같은 방식으로 객체를 주입한다.


✔ (1) 생성자(Constructor) 주입 — ⭐가장 권장


@Component
public class Roel {

    private final Phone phone;

    @Autowired
    public Roel(Phone phone) {
        this.phone = phone;
    }
}

🔍 특징

  • 불변(immutable) 의존성에 적합
  • 스프링이 객체 만들 때 한 번만 호출되므로 안정적
  • final로 선언 가능 → 값 변경 방지
  • 필수 의존성 주입에 가장 적합
  • 생성자가 하나라면 @Autowired 생략 가능
✔ 스프링 공식 권장 방식
✔ 반드시 필요하고 변경되면 안 되는 의존성에 적합
✔ 순환 참조를 애플리케이션 구동 시점에 잡아줌

✔ (2) Setter(수정자) 주입 — 선택적 의존성에 적합


@Component
public class Roel {

    private Phone phone;

    @Autowired
    public void setPhone(Phone phone) {
        this.phone = phone;
    }
}

🔍 특징

  • 나중에 값 변경이 필요한 경우 사용
  • 선택적(optional) 의존성에 적합
  • 테스트나 일부 동적 상황에서 유용
  • 단점: setter가 public 이어야 하고, 외부 변경 위험 존재

✔ (3) 필드(Field) 주입 — ❌ 비권장


@Component
public class Roel {

    @Autowired
    private Phone phone;
}

🔍 특징

  • 가장 간편하지만 재사용성과 테스트성이 낮아 비권장
  • DI 컨테이너 없이는 절대 동작하지 않음
  • 순수 자바 코드로 테스트 불가
  • 어떤 객체가 어떻게 주입되는지 코드만 보고 확인 어려움
⚠ 스프링 공식적으로 권장하지 않는 방식
(테스트 불가 + 유지보수 어려움)

📌 7. 왜 생성자 주입을 써야 할까?

✔ 1) 불변성 보장

  • 객체 생성 시 단 한 번만 호출
  • 이후 변경 불가능 → 안정적

✔ 2) 순환 참조를 구동 시점에 해결

Setter/필드 주입은 객체 생성 이후 주입되므로

순환 참조가 나중에 발생하는 반면,

생성자 주입은 애플리케이션 시작과 함께 에러를 즉시 감지한다.

✔ 3) 객체의 필수 의존성을 강제할 수 있음

필요한 의존성이 없으면 객체 생성 자체가 불가능하기 때문에

설계 오류를 예방할 수 있다.


📌 8. DI의 최종 핵심 정리

✔ 객체 간 의존성을 외부에서 주입받는 구조
✔ 강한 결합을 느슨한 결합으로 전환
✔ 유지보수성과 테스트 용이성 향상
✔ 생성자 주입이 가장 권장됨
✔ 인터페이스 기반 설계로 유연한 구조 가능

DI가 왜 중요한가?

DI는 단순히 “객체를 대신 넣어주는 기술”이 아니라, 스프링이 추구하는 설계 철학 전체를 지탱하는 핵심 원리다.

객체 간 결합을 최소화하면 시스템은 자연스럽게 더 유연해지고, 변경에 강해지고, 테스트가 쉬워진다.

이는 곧 개발 생산성 향상, 유지보수 비용 절감, 안정적인 서비스 운영으로 이어진다.

또한 DI는 인터페이스 기반 설계를 자연스럽게 유도하기 때문에, 애플리케이션 구조를 확장 가능하고 견고하게 만든다.

로직 변경, 기능 추가, 의존성 교체가 잦은 실제 개발 환경에서 DI는 개발자에게 변화에 흔들리지 않는 코드 기반을 제공한다.

결국 DI는 “스프링을 스프링답게 만드는 근본 개념”이며, 스프링을 활용하는 프로젝트에서 핵심 설계 중심축 역할을 한다.

profile
나만의 개발 일기

0개의 댓글