일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
- OAuth 2.0
- Volatile
- Google OAuth
- spring security
- middleware
- factory
- nestjs
- Spring
- builder
- synchronized
- java
- lombok
- 일급 객체
- 일급 컬렉션
- Dependency Injection
- Today
- Total
HJW's IT Blog
Dependency Injection 은 무엇이며 왜 사용해야 하는가? 본문
Dependency Injection(DI) 이란?
“Dependency injection is a programming technique that makes a class independent of its dependencies.”
DI 란 객체간의 의존성을 외부에서 주입해주는 설계 패턴이다. 이를 통해 객체는 의존하는 객체들을 직접 생성하는 것이 아닌 외부에서 제공된 객체를 사용하게된다. 이를 통해 개발자는 SOLID 원칙의 의존성 역전 원칙 과 단일 책임 원칙을 달성할 수 있다.
잠깐 의존성 역전 원칙과 단일 책임 원칙을 짚어보고 넘어가겠다.
의존성 역전 원칙 (Dependency Inversion Principle)
DIP 란 무엇일까?
- DIP 란 상위 모듈이 하위 모듈에 의존하지 않아야 하며, 둘 다 추상화된 인터페이스에 의존해야 한다는 원칙이다. 이를 통해, 결홥도를 낮추며 유연한 설계를 할 수 있다.
- 여기서 상위 모듈이란 복잡한 로직등을 포함하고 비즈니스 요구를 해결하는 코드이며, 하위 모듈은 DB 접근, 파일 입출력, 네트워크 요청등을 담당하는 모듈이다.
// 추상화된 인터페이스
interface MessageSender {
void send(String message);
}
// 하위 모듈 1 : email sender
class EmailSender implements MessageSender{
@Override
public void send(String message){
System.out.println("Sending email.." + message);
}
}
// 하위 모듈 2 : Sms Sender
class SmsSender implements MessageSender{
@Override
public void send(String message){
System.out.println("Sending sms..." + message);
}
}
// 상위 모듈 : 인터페이스에 의존하여 다양한 전송 지원
class UserNotification {
private MessageSender messageSender;
// DIP 적용 : 상위 모듈이 구체 클래스가 아닌 인터페이스에 의존
public UserNotification(MessageSender messageSender){
this.messageSender = messageSender;
}
public void notifyUser(String message){
messageSender.send(message);
}
}
public class Main{
public static void main(String[] args){
MessageSender emailSender = new EmailSender();
UserNotification emailNotification = new UserNotification(emailSender);
emailNotification.send("email");
MessageSender smsSender = new SmsSender();
UserNotification smsNotification = new UserNotification(smsSender);
smsNotification.send("SMS");
}
}
단일 책임 원칙 (Single Responsibility Principle, SRP)
SRP 란 하나의 클래스는 하나의 책임만 가져야 한다는 원칙이다. 즉, 하나의 클래스가 여러가지 기능을 담당하는 것이 아니라 오직 한가지 기능만 수행해야 한다는 의미이다.
class Order {
private int orderId;
private double orderAmount;
public Order(int orderId, double orderAmount){
this.orderId = orderId;
this.orderAmount = orderAmount;
}
public double calculateTotal(){
return orderAmount;
}
public void printOrder(){
System.out.println(orderId);
}
}
위 예시를 보면 Order class 는 주문관련 로직 + 출력하는 로직을 담당하기에 단일 책임 원칙을 지키지 못한것이다.
다음과 같이 수정할 경우 SRP 를 만족하게 된다
class Order {
private int orderId;
private double orderAmount;
public Order(int orderId, double orderAmount){
this.orderId = orderId;
this.orderAmount = orderAmount;
}
public double calculateTotal(){
return orderAmount;
}
public getOrderId(){
return orderId;
}
}
class OrderPrinter{
public void printOrder(Order order){
Systm.out.println(order.getOrderId());
}
}
public class Main{
public static void Main(String[] args){
Order order = new Order(1, 100);
OrderPrinter orderPrinter = new OrderPrinter();
orderPrinter.printOrder(order);
}
}
왜 Dependency Injection 이 필요한가?
하나의 객체가 다른 객체를 직접 생성하게 되면 두 객체는 강하게 결합된다. 이는 코드의 재사용성을 떨어뜨릴뿐만 아니라, 테스트 또한 하기 어렵게 만든다.
예를 들어, 클래스 A 가 클래스 B를 직접 생성한다면, B가 바뀔 때 마다, A 또한 바꾸어 주어야 한다. 뿐만 아니라, A 를 테스트 하기 위해 항상 B도 생성해야 하기 때문에 단위 테스트가 복잡해 질 수 있다.
Dependency Injection 을 사용하게 되면, A는 B에 대한 구체적인 정보를 알 필요가 없게 되며, 외부에서 B를 제공받기 때문에 B가 바뀐다 한들, A는 영향을 받지 않게 된다. 이로써, 의존성 주입을 통한 느슨한 결합을 달성할 수 있다.
주요 개념
- 의존성 : 한 객체가 다른 객체의 기능을 사용할 경우, 그 객체에 의존한다고 말한다. 예를 들어 서비스 클래스가 데이터베이스 레포지토리에 의존한다면, 이 레포지토리 객체가 서비스의 의존성이다.
- 주입 : 의존성을 직접 생성하지 않고 외부에서 전달받는 과정이다.
- 의존성 역전 원칙 : 객체는 구체적인 클래스에 의존하지 않아야 하며, 추상화된 인터페이스나 상위 클래스에 의존해야 한다.
- Inversion of Control : 객체의 생명 주기와 의존성 관리를 외부에서 제어한다는 원칙이다. Spring 의 DI 컨테이너와 같은 메커니즘을 통해 의존성을 주입하고 객체 생성의 책임을 위임한다
Dependency Injection 의 방식
Dependency Injection 은 크게 3가지 방식이 있다.
생성자 주입
의존성을 객체의 생성자를 통해 주입하는 방법으로, 의존성이 필수적일때 사용한다. 위에서 언급한 UserNotification 이 Setter Injection 의 예시이다.
class UserNotification {
private MessageSender messageSender;
//의존성 주입
public UserNotification(MessageSender messageSender){
this.messageSender = messageSender;
}
public void notifyUser(String message){
messageSender.send(message);
}
}
Setter 주입
Setter 를 통해 의존성을 주입하는 방법이다. 위의 생성자 주입의 경우, 객체가 생성됨과 동시에 주입이 되지만, Setter 주입의 경우, 개발자가 직접 해당 메서드를 통해 주입해주어야 한다.
class UserNotification {
private MessageSender messageSender;
// Setter 주입
public void setMessageSender(MessageSender messageSender) {
this.messageSender = messageSender;
}
public void notifyUser(String message) {
messageSender.send(message);
}
}
public class Main {
public static void main(String[] args) {
MessageSender emailSender = new EmailSender();
// 주입 대상 객체 생성 후, 의존성 주입
UserNotification notification = new UserNotification();
notification.setMessageSender(emailSender);
notification.notifyUser("email");
}
}
필드 주입
필드 주입은 주로 프레임워크를 사용할 때 사용되며, Spring 의 경우 @Autowired 어노테이션을 통해 DI 컨테이너가 직접 객체 필드에 의존성을 주입하는 방식이다.
interface MessageSender{
void send(String message);
}
@Component
class EmailSender implements MessageSender{
@Override
public void send(String message){
System.out.println("Sending email..." + message);
}
}
@Component
class UserNotification{
@Autowired
private MessageSender messageSender;
public void notifyUser(String message){
messageSender.send(message);
}
}
@SpringBootApplication
public class MainApplication{
public static void main(String[] args){
ApplicationContext context = SpringApplication.run(MainApplication.class, args);
UserNotification notification = context.getBean(UserNotification.class);
notification.notifyUser("Email");
}
}
Dependency Injection 의 장점
- 테스트 독립성 : Mock 객체를 통해 DB나 네트워크에 의존하지 않고 독립적인 테스트를 수행할 수 있다
- 유연한 테스트 환경 : 다양한 시나리오를 쉽게 설정하여 다양한 상황에 대한 시뮬레이션이 가능하다
- 테스트 유지보수성 : 객체간 결합도가 낮기에 코드 변경시 테스트 코드가 깨질 확률이 낮다
다음은 테스트 상황의 예시이다.
public class UserNotificationTest {
private MessageSender mockMessageSender; // Mock 객체
private UserNotification userNotification;
@Before
public void setUp() {
// MessageSender의 Mock 객체 생성
mockMessageSender = mock(MessageSender.class);
// UserNotification에 Mock 객체 주입
userNotification = new UserNotification(mockMessageSender);
}
@Test
public void testNotifyUser() {
// 테스트할 메시지
String message = "Test message";
// 유저에게 알림 전송
userNotification.notifyUser(message);
// Mock 객체의 send 메소드가 해당 메시지로 호출되었는지 검증
verify(mockMessageSender).send(message);
}
@Test
public void testNotifyUserWithDifferentMessage() {
// 다른 메시지로 테스트
String anotherMessage = "Another test message";
// 유저에게 알림 전송
userNotification.notifyUser(anotherMessage);
// Mock 객체가 호출된 횟수와 메시지 검증
verify(mockMessageSender, times(1)).send(anotherMessage);
}
}
'개발 개념' 카테고리의 다른 글
Builder Pattern 에 대한 고찰 (0) | 2025.01.10 |
---|---|
SOLID 와 TradeOff (0) | 2025.01.09 |
상속보단 합성을 사용하자 (0) | 2025.01.06 |
단일 책임 원칙, 얼마나 쪼개야 ‘적당함’일까? (1) | 2025.01.03 |
TDD : Test Driven Development (0) | 2024.10.25 |