IoC와 DI를 이해해보자

StrayCat·2026년 2월 14일

1. 강한 결합 (Tight Coupling) - 문제의 시작

public class OrderService {
	private MySQLOrderRepository repository = new MySQLOrderRepository(); // 직접 생성
    
    public void createOrder(Order order) {
    	repository.save(order);
    }
}
  • OrderServiceMySQLOrderRepository를 직접 new로 생성하고 있다.
  • 이를 '강한 결합' (Tight Coupling) 이라고 부름
  • 문제는 차후 DB를 교체한다던가 하면, OrderService에 작성된 코드를 직접 수정해야 한다. 서비스가 10개, 100개 늘어나면 유지보수시간은 기하급수적으로 상승한다.

    핵심 : 클래스가 구체적인 구현체에 '작접' 의존하면, 그 변경은 연쇄적으로 퍼진다.


2. 다형성 (Polymorphism) - 해결의 열쇠

자바의 다형성이란, 부모 타입의 변수로 자식 타입의 객체를 다룰 수 있는 것이다.

// 여기서의 부모 타입 : Animal 타입

Animal animal = new Dog();  // Dog는 Animal을 상속
animal.speak(); // "멍멍!" — Dog의 메서드가 실행됨

animal = new Cat();
animal.speak(); // "야옹!" — Cat의 메서드가 실행됨

같은 Animal 타입 변수이지만, 실제로 어떤 객체가 들어있느냐에 따라 행동이 달라진다. 이 원리가 결합도를 낮추는 핵심 도구가 된다.


3. 인터페이스 (Interface) - 다형성을 설계에 적용하기

인터페이스는 "무엇을 할 수 있는가"만 정의하고, "어떻게 하는가"는 정의하지 않는 계약서다.

// 인터페이스에 포함되는 것은 리턴 타입, 메서드 이름, 파라미터 종류 정도만 정의
// 이를 '추상 메서드'라고 표현함

public interface OrderRepository {
    void save(Order order);
    Order findById(Long id);
}
  • 위 작업을 통해 이제 구현체를 여러 개 만들 수 있다 :
public class MySQLOrderRepository implements OrderRepository {
    public void save(Order order) { /* MySQL에 저장 */ }
    public Order findById(Long id) { /* MySQL에서 조회 */ }
}

public class PostgreSQLOrderRepository implements OrderRepository {
    public void save(Order order) { /* PostgreSQL에 저장 */ }
    public Order findById(Long id) { /* PostgreSQL에서 조회 */ }
}
  • 이 경우 서비스 코드를 다음과 같이 변경 가능하다 :
public class OrderService {
    private OrderRepository repository; // 인터페이스 타입으로 선언

    // 그러면 repository에 실제 객체는 누가 넣어주는가?
}
  • OrderService는 이제 OrderRepository라는 인터페이스에만 의존한다. MySQL인지 PostgreSQL인지는 알 수없고, 알 필요도 없다. 이것이 느슨한 결합 (Loose Coupling)이다.

  • 느슨한 결합은 결국, '구체적인 구현체'가 아닌, '추상화(인터페이스)'에 의존하는 상태를 의미함

  • 이를 객체지향 설계 원칙 중 DIP (의존성 역전 원칙) 이라고 표현함

  • 고수준 모듈(OrderService)이 저수준 모듈(MySQLRepository)에 의존 (X)

  • 둘 다 추상화 (OrderRepository 인터페이스) 에 의존

  • 결국 인터페이스를 사이에 두면 양쪽 모두 구체적인 상대를 모르게 되므로, 어느 한쪽을 바꿔도 다른 쪽에 영향을 주지 않는 것

  • 아직 문제가 남아있다. - 실제 구현체를 누가, 어떻게 넣어주느냐?


4. 주입(Injection) - 외부에서 실제 구현체를 넣는 작업

주입이란 표현은 거창하지만, 그 본질은 단순하다.

객체가 필요로 하는 의존성을 스스로 만들지 않고, 외부에서 넣어주는 것

예를 들면, 다음과 같다

  • 주입 없음 : 요리사가 직접 농장에 가서 재료를 구해온다 (강한 결합)
  • 주입 있음 : 누군가가 요리사에게 재료를 배달해준다 (느슨한 결합)
    요리사는 "토마토"라는 스펙만 알면 되고, 어느 농장 토마토인지는 신경쓰지 않는다.

5. 주입의 세 가지 방법

1. 필드 주입 (Field Injection)

@Service
public class OrderService {
    @Autowired
    private OrderRepository repository; // 필드에 직접 주입
}

간결하지만 권장되지 않는다. 그 이유는 아래와 같다.

  • 테스트 시 Mock 객체를 넣기 어렵다. (리플렉션을 써야 함)
  • 의존성이 숨겨져서 클래스만 보고는 어떤 것이 필요한지 파악하기 어렵다.
  • final을 사용할 수 없어 불변성을 보장할 수 없다.

2. 수정자 (Setter) 주입 - 메서드를 통한 주입

@Service
public class OrderService {
    private OrderRepository repository;

    @Autowired
    public void setRepository(OrderRepository repository) {
        this.repository = repository;
    }
}

선택적 의존성에 사용할 수 있지만, 객체 생성 후 의존성이 변경될 수 있어 안정성이 떨어진다.

3. 생성자 주입 (Constructor Injection) -> 권장

@Service
public class OrderService {
    private final OrderRepository repository; // final 가능!

    @Autowired  // 생성자가 하나면 생략 가능
    public OrderService(OrderRepository repository) {
        this.repository = repository;
    }
}

Spring 공식 권장 방식이다. 이유는 :

  • final 선언으로 불변성 보장 - 한번 주입되면 바뀌지 않음

  • 의존성이 생성자에 명시되어 한눈에 파악 가능
    (이 OrderService는 OrderRepository가 있어야 동작하는구나) 를 파악

  • new OrderService(mockRepository) 형태로 테스트가 쉬움

  • 순환 의존성을 컴파일 타임에 잡을 수 있음

  • Lombok의 @RequiredArgsConstructor를 사용하면 생성자 코드도 생략 가능

@Service
@RequiredArgsConstructor
public class OrderService {
    private final OrderRepository repository; // 자동으로 생성자 주입
}

6. 제어의 역전 (IoC) - 모든 것을 관통하는 원칙

전통적인 프로그래밍에서 제어의 흐름은 아래와 같았다 :

// 내가(개발자가) 직접 제어
OrderRepository repo = new MySQLOrderRepository();  // 내가 생성
OrderService service = new OrderService(repo);       // 내가 조립
service.createOrder(order);                          // 내가 호출
  • 객체를 만들고, 조립하고, 호출하는 일련의 과정을 개발자가 모두 결정했음
  • IoC는 이 제어권을 프레임워크 (Spring 컨테이너)에 넘겨주는 것을 의미함
@Repository
public class MySQLOrderRepository implements OrderRepository { ... }

@Service
public class OrderService {
    private final OrderRepository repository;
    
    public OrderService(OrderRepository repository) {
        this.repository = repository;
    }
}

이렇게만 작성하면 Spring이 알아서:
1. MySQLOrderRepository 객체를 생성하고
2. OrderService 객체를 생성하면서
3. 생성자에 MySQLOrderRepository주입해준다

"내가 호출하는 게 아니라, 프레임워크가 나를 호출한다" — 이것이 제어의 역전이다.


전체 흐름 정리

강한 결합 (문제 인식)
    ↓
다형성 (해결 원리)
    ↓
인터페이스 (설계 도구) → 느슨한 결합 달성
    ↓
주입 (구현체를 외부에서 넣어주는 행위)
    ↓
IoC (Spring 컨테이너가 생성·주입·관리를 전부 담당)
    ↓
DI (IoC를 구현하는 구체적 패턴 = 의존성 주입)

결국 DI는 IoC를 실현하는 대표적인 방법이다. Spring 컨테이너 (ApplicationContext)가 Bean들의 생명주기와 의존관계를 전부 관리하기 때문에, 개발자는 비즈니스 로직에만 집중할 수 있게 된다.

profile
알면 좋은 것보단 잊어버리기 싫은 것들을 기록합니다.

0개의 댓글