의존성 주입(Dependency Injection, DI)
의존성 주입은 원칙을 설계하기 위한 구체적인 기법이다.
목차
- "의존"이란
- 의존성 생성
- 의존성 주입 방법
- 불변 VS 가변
- 불변 vs 상수
"의존" 이란?
A 클래스가 다른 클래스의 기능이 필요해서 다른 클래스를 호출하여 사용하는 경우, A 클래스는 그 클래스에 "의존"한다고 말한다.
의존성 생성
어쨌든, A 클래스는 다른 클래스의 기능이 필요하기 때문에 의존을 할 수 밖에 없다.
그렇다면 어떠한 방법으로 의존성을 생성할 수 있을까?
1. 의존성의 명시적 생성
개발자가 직접 의존성을 생성한다는 의미에서 이렇게도 부른다.
이러한 경우, A 클래스는 다른 클래스의 생성에 민감하기 때문에 강한 의존성 또는 강한 결합을 의미한다.
- 다른 클래스가 바뀌면 A 클래스도 수정해야 할 가능성이 높다.
- 테스트하려고 해도 다른 클래스를 대체(Mocking)하기 어렵다.
UserService userService = new UserService(); // 직접 생성
2. 의존성 주입
반대로 외부에서 의존성을 대신 주입해달라고 한다.
이러한 경우, A 클래스는 다른 클래스의 생성에는 관심이 없고 오직 필요한 기능(동작)만 기대할 뿐이다.
- 인터페이스 클래스로의존성을 주입하면 더 느슨해짐
⇒ 개방-폐쇄 원칙(Open-Closed Principle, OCP)와 관련이 있다.
⇒ 구현 클래스를 유연하게 변경할 수 있기 때문이다.
// 생성자 주입 예시
private final B b;
public A(B b) { // 외부에서 주입
this.b = b;
}
의존성 주입 방법
의존성을 주입하는 방법에는 아래와 같이 세 가지 방법이 있다.
1. 생성자 주입
- 가장 권장되는 방식
- 불변성(immutable) 보장 ⇒ final 키워드 사용 가능
- 테스트 용이성 높음
public class A {
private final OtherClass b;
public A(OtherClass b) {
this.b = b;
}
public void doSomething() {
b.run();
}
}
2. 필드 주입
클래스 내부에 선언된 변수(멤버 변수)를 필드라고 한다.
- 코드가 간결함
- 테스트 어려움, Mock 주입 불가
- final 사용 불가 ⇒ 불변 객체 만들기 어려움
public class A {
@Autowired
private OtherClass b;
public void doSomething() {
b.pay();
}
}
3. setter 주입
- 선택적 의존성에 유리 (null 허용 가능)
- 객체 생성 후 의존성 주입
- 테스트 시 Mock 객체 주입에 유리
public class A {
private otherClass b;
@Autowired
public void setOtherClass(otherClass b) {
this.b = b;
}
}
불변 VS 가변
의존성 주입 방법에서 불변(Immutable)과 가변(Mutable)에 대해 자주 언급이 된다.
두 성질(불변과 가변)이 붙은 객체에 대해 알아보자.
불변 객체
객체가 생성된 이후, 내부 상태(필드 값)가 바뀌지 않는 객체
가변 객체
객체가 생성된 이후에도, 내부 상태를 바꿀 수 있는 객체
불변(final) vs 상수(static final)
자바에서는 변수를 불변으로 만들기 위해 final 을 사용한다.
그리고 final이 붙으면 상수라고 생각할 수 있는데 상수와 final은 약간 다르다.
final
변수에 final을 붙이면 한 번만 값이 할당될 수 있도록 제한한다.
즉, 값을 변경할 수 없게 만드는 키워드이다.
final int x = 10;
x = 20; // ❌ 컴파일 에러 – final 변수는 재할당 불가
상수(static final)
클래스 차원의 상수 (공통값 + 불변성) ⇒ 상수라고 부름
public class Constants {
public static final int MAX_USER = 100; // 진짜 상수
}
불변을 사용하면 장점
- 예측 가능성 증가
⇒ 생성 후 객체의 상태가 변하지 않으니 **사이드 이펙트(side effect)**를 걱정할 필요 없음. - 테스트 용이
⇒ 테스트 시 객체가 바뀌지 않으니 신뢰성 높은 테스트가 가능. - 스레드 안전성(Thread Safety)
⇒ 멀티스레드 환경에서도 동기화 없이 안전하게 사용할 수 있음. - 버그 방지
⇒ 필드가 나중에 갑자기 null로 바뀌는 등의 예외 상황이 원천 차단됨.