public class OrderService {
private MySQLOrderRepository repository = new MySQLOrderRepository(); // 직접 생성
public void createOrder(Order order) {
repository.save(order);
}
}
OrderService 가 MySQLOrderRepository를 직접 new로 생성하고 있다.OrderService에 작성된 코드를 직접 수정해야 한다. 서비스가 10개, 100개 늘어나면 유지보수시간은 기하급수적으로 상승한다.핵심 : 클래스가 구체적인 구현체에 '작접' 의존하면, 그 변경은 연쇄적으로 퍼진다.
자바의 다형성이란, 부모 타입의 변수로 자식 타입의 객체를 다룰 수 있는 것이다.
// 여기서의 부모 타입 : Animal 타입
Animal animal = new Dog(); // Dog는 Animal을 상속
animal.speak(); // "멍멍!" — Dog의 메서드가 실행됨
animal = new Cat();
animal.speak(); // "야옹!" — Cat의 메서드가 실행됨
같은 Animal 타입 변수이지만, 실제로 어떤 객체가 들어있느냐에 따라 행동이 달라진다. 이 원리가 결합도를 낮추는 핵심 도구가 된다.
인터페이스는 "무엇을 할 수 있는가"만 정의하고, "어떻게 하는가"는 정의하지 않는 계약서다.
// 인터페이스에 포함되는 것은 리턴 타입, 메서드 이름, 파라미터 종류 정도만 정의
// 이를 '추상 메서드'라고 표현함
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 인터페이스) 에 의존
결국 인터페이스를 사이에 두면 양쪽 모두 구체적인 상대를 모르게 되므로, 어느 한쪽을 바꿔도 다른 쪽에 영향을 주지 않는 것
아직 문제가 남아있다. - 실제 구현체를 누가, 어떻게 넣어주느냐?
주입이란 표현은 거창하지만, 그 본질은 단순하다.
객체가 필요로 하는 의존성을 스스로 만들지 않고, 외부에서 넣어주는 것
예를 들면, 다음과 같다
@Service
public class OrderService {
@Autowired
private OrderRepository repository; // 필드에 직접 주입
}
간결하지만 권장되지 않는다. 그 이유는 아래와 같다.
@Service
public class OrderService {
private OrderRepository repository;
@Autowired
public void setRepository(OrderRepository repository) {
this.repository = repository;
}
}
선택적 의존성에 사용할 수 있지만, 객체 생성 후 의존성이 변경될 수 있어 안정성이 떨어진다.
@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; // 자동으로 생성자 주입
}
전통적인 프로그래밍에서 제어의 흐름은 아래와 같았다 :
// 내가(개발자가) 직접 제어
OrderRepository repo = new MySQLOrderRepository(); // 내가 생성
OrderService service = new OrderService(repo); // 내가 조립
service.createOrder(order); // 내가 호출
@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들의 생명주기와 의존관계를 전부 관리하기 때문에, 개발자는 비즈니스 로직에만 집중할 수 있게 된다.