의존성 주입, 왜 필요할까?
의존성 주입의 필요성을 이야기하기 위해, 객체지향 설계의 SOLID 원칙을 간단히 언급하고 넘어가겠다.
SOLID 원칙
- SRP(Single Responsibility Principle): 단일 책임 원칙
- 한 클래스는 하나의 책임만 가져야 한다.
- OCP(Open/Closed Principle): 개방-폐쇄 원칙
- 클래스는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
- LSP(Liskov Substitution Principle): 리스코프 치환 원칙
- 하위 타입은 상위 타입을 대체할 수 있어야 한다.
- ISP(Interface Segregation Prinicple): 인터페이스 분리 원칙
- 인터페이스를 클라이언트의 용도와 목적에 맞게 작게 분리하여 제공해야 한다.
- DIP(Dependency Inversion Principle): 의존관계 역전 원칙
- 추상화(인터페이스)에 의존해야하지, 구체화(구현 클래스)에 의존하면 안된다.
다음 코드를 보면, Service 클래스에서 Repository 인터페이스의 구현 클래스를 직접 선택하고 있다.
public class Service {
private Repository repository;
public Service() {
this.repository = new MemoryRepository();
}
}
여기서 구현 클래스를 JdbcRepository로 변경한다고 하자.
public class Service {
private Repository repository;
public Service() {
// this.repository = new MemoryRepository(); //변경 전
this.repository = new JdbcRepository(); //변경 후
}
}
과연 이 코드는 OCP: 개방-폐쇄 원칙과 DIP: 의존관계 역전 원칙을 잘 지켰다고 볼 수 있을까?
이 질문에 대한 답변은 '아니다' 이다.
🚫 OCP 위반
클래스를 MemoryRepository에서 JdbcRepository로 바꾸기 위해서는 Service 클래스의 코드를 변경해야 한다.
🚫 DIP 위반
Service 클래스는 MemoryRepository 인터페이스 뿐만 아니라, MemoryRepository나 JdbcRepository 클래스와 같은 구현 클래스에도 의존하고 있다. 즉, 추상화와 구체화에 모두 의존하고 있는 것이다.
보다시피, 다형성만으로는 OCP, DIP 원칙을 지킬 수 없다.
이러한 문제를 해결하기 위한 방법이 의존성 주입(Dependency Injection)이다.
의존성 주입
의존성 주입이란 외부에서 객체 간의 의존관계를 결정하고 주입하는 것을 말한다.
어떻게 구현할 수 있을까?
1. 생성자 주입
- 생성자를 통해서 의존성을 주입하는 방식
- 스프링 빈을 만드는 동시에 의존 관계를 주입
- 생성자가 하나이고, 생성자로 주입 받을 객체가 스프링 빈으로 등록되어 있다면 @Autowired 생략 가능
@Service
public class MyService {
private MyRepository myRepository;
@Autowired //생략 가능
public MyService(MyRepository myRepository) {
this.myRepository = myRepository;
}
}
2. 수정자 주입
- Setter 메서드에 @Autowired 애노테이션을 붙이는 방법
- 단점: setXxx 메서드의 접근 제어자를 public으로 열어두어야 하기 때문에 어디서든 변경이 가능
@Service
public class MyService {
private MyRepository myRepository;
@Autowired
public void setMyRepository(MyRepository myRepository) {
this.myRepository = myRepository;
}
}
3. 필드 주입
- 필드에 @Autowired를 붙여 의존관계를 주입하는 방법
- 단점: 코드가 간결하지만, 외부에서 변경이 불가능해 테스트하기 어려움
@Service
public class MyService {
@Autowired
private MyRepository myRepository;
}
어떤 주입 방식을 사용하는게 좋을까?
Spring Framework Reference에서는 생성자 주입 방식을 권장하고 있다.
필드 주입 방식이 가장 간단한데, 왜 생정자 주입 방식을 권장할까? 이유를 알아보자.
1. 순환 참조 방지
순환 참조는 A가 B를 참조하고, B가 A를 참조하는 것이다.
다음 코드를 보면 AService에서 BService의 메서드를 호출하고, BService에서 AService의 메서드를 호출하고 있다.
@Service
public class AService {
@Autowired
private BService bService;
public void helloA() {
bService.helloB();
}
}
@Service
public class BService {
@Autowired
private AService aService;
public void helloB() {
aService.helloA();
}
}
스프링 부트 2.5 이하에서는 애플리케이션을 실행하고 AService또는 BService에서 메서드를 호출하면 서로 메서드를 호출하다가 StackOverFlow 에러가 발생하여 애플리케이션이 다운된다.
(필자는 3.xx 버전을 쓰고 있기 때문에 에러 상황을 캡처하지 못했다.)
문제는 애플리케이션 실행 중에 메서드를 호출하지 않으면 에러가 발생하지 않아, 메서드를 호출하기 전까지 개발자는 문제를 알 수가 없다는 것이다.
하지만, 생성자 주입 방식을 사용하고 애플리케이션을 실행하면 BeanCurrentlyInCreationException이 발생하여 순환 참조 문제가 있음을 알 수 있다.
생성자 주입은 필드 주입이나 수정자 주입과는 달리 빈 생성과 동시에 의존성이 주입되기 때문에 애플리케이션을 실행하자마자 오류가 발생하는 것이다.
스프링 부트 2.6 이상 부터는 이를 개선하여 필드 주입이나 수정자 주입이어도 컴파일 시점에 에러를 발생시킨다.
2. 객체의 불변성 확보
대부분의 의존관계 주입은 한 번 일어나면 애플리케이션 종료 시점까지 의존 관계를 변경할 일이 거의 없다.
생성자 주입을 사용하면 객체를 생성할 때 1번만 호출되므로 객체가 불변하게 설계할 수 있다.
3. 테스트 용이
독립적으로 인스턴스화가 가능한 POJO(Plain Old Java Object)를 사용하면 DI 컨테이너 없이도 단위 테스트에서 인스턴스화하여 의존성을 주입할 수 있다.
POJO(Plain Old Java Object)란?
Java EE에 종속되지 않은 순수한 자바 객체
Getter와 Setter로 구성된 가장 순수한 형태의 기본 클래스
@Service
public class MyService {
@Autowired
private MyRepository myRepository;
public void hello() {
}
}
class MyServiceTest {
@Test
public void testHello() {
MyRepository myRepository = new MemoryRepository();
MyService myService = new MyService(myRepository);
myService.hello();
}
}
4. final 키워드
생성자 주입을 사용하면 final 키워드를 사용하여 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에서 방지할 수 있다.
생성자 주입 외의 모든 주입 방식은 final 키워드 사용이 불가능하다.
다음과 같이 MyRepository에 대한 초기화 코드를 누락한 상태로 애플리케이션을 실행하면 에러가 발생한다.
'Spring' 카테고리의 다른 글
[Spring] Spring Boot 프로젝트 이름 변경하기 (0) | 2024.08.08 |
---|---|
[WebFlux] Reactive Streams (0) | 2023.08.09 |
[Spring] Junit5 테스트 No tests found for given includes 오류 해결 (0) | 2023.06.21 |
[Spring] 아임포트 사용한 결제 구현 + JavaScript/React 코드 (0) | 2023.06.20 |
[Spring] Webflux + RSocket, React 사용한 전체 채팅 구현 (0) | 2023.06.02 |