TDD 예제 2 (Dependency Injection)에 대해서

 

https://snowman-seolmini.tistory.com/88

 

TDD 예제(2)

두 번째 비동기 통신 예제로 살펴보는 독립성을 유지한 간략한 테스트 코드를 작성해보자.테스트 코드를 작성할 때 어떤 측면을 검증할 지를 정하는 것이 첫 번째다.  1. 함수의 호출 여부 확인

snowman-seolmini.tistory.com

에서 테스트 코드를 작성해봤는데, 좀 더 복잡한 상태의 메서드라면 테스트 코드를 작성하는 일이 즐거운 일이 되지 않을 것 같다....이를 mock을 남발한 테스트 코드라고 한다고 한다.

const ProductService = require('../product_service.js');
const ProductClient = require('../product_client.js');
jest.mock('../product_client.js');
 
describe('ProductService', () => {
  const fetchItems = jest.fn(async () => [
    { item: 'Milk', available: true },
    { item: 'banana', available: false },
  ]);
 
  ProductClient.mockImplementation(() => {
    return {
      fetchItems,
    };
  });
  let productService;
 
  beforeEach(() => {
    productService = new ProductService();
  });
 
  it('should filter out only available items', async () => {
    const items = await productService.fetchAvailableItems();
    expect(items.length).toBe(1);
    expect(items).toEqual([{ item: 'Milk', available: true }]);
  });
});

 

위의 테스트 코드를 좀 stub 방식을 사용한다면 좀 더 간략하게 줄일 수 있다고 한다.

 

의존성 주입(Dependency Injection) 원칙은 클래스가 자신의 의존성을 직접 생성하기보다는 외부에서 주입받아야 한다. (이를 통해 클래스의 재사용성과 테스트 용이성을 높일 수 있다는데...어렵따. 하다 보면 이해하면서 익숙해지겠지. )

 

class ProductService {
  constructor() {
    this.productClient = new ProductClient();
  }

  async fetchAvailableItems() {
    const items = await this.productClient.fetchItems();
    return items.filter((item) => item.available);
  }
}

 

여기서 ProductService 클래스는 ProductClient의 인스턴스를 자신의 생성자 내에서 직접 생성하고 있다.

* 코드 분리 : ProductService는 더 이상 ProductClient를 직접 생성하지 않기 때문에, ProductClient의 변경 사항에 영향을 받지 않는다. ProductService는 오직 fetchItems 메서드가 있어야 하는 객체만 필요하다.

 

* 클래스의 책임 분리 : ProductService는 ProductClient의 생성 책임을 지지 않으며, ProductClient의 객체를 주입받아 사용하는 책임만 가진다. 이는 클래스의 책임을 명확하게 분리하는 데 도움이 된다.

 

ProductService가 ProductClient에 대한 직접적인 의존성을 가지므로, ProductClient의 변경이 ProductService에 영향을 미칠 수 있다. 즉, 두 클래스 간의 결합도가 강해진다.

 

개선 방안

- 의존성 주입을 사용한다면 생성자에서 ProductClient 인스턴스를 외부에서 주입받도록 변경할 수 있다.

class ProductService {
  constructor(productClient) {
    this.productClient = productClient;
  }

  async fetchAvailableItems() {
    const items = await this.productClient.fetchItems();
    return items.filter((item) => item.available);
  }
}

 


 

stub 형식의 의존성 주입을 받는 형태로 이를 " 의존성 주입 패턴(Dependency Injection Pattern) "이라고 부른단다.

 

이 패턴은 객체 간의 의존성을 외부에서 주입하여 모듈과의 결합도를 낮추고 유연성을 높이는 설계 방식이다.

 

각설하고 코드로 비교해보자!

 

# 기존 코드

1. ProductClient:

class ProductClient {
  async fetchItems() {
    const response = await fetch('http://example.com');
    return response.json();
  }
}

module.exports = ProductClient;

- 외부 API에서 데이터를 가져오는 메서드를 포함한다.

 

2. ProductService:

const ProductClient = require('./product_client');

class ProductService {
  constructor() {
    this.productClient = new ProductClient();
  }

  async fetchAvailableItems() {
    const items = await this.productClient.fetchItems();
    return items.filter((item) => item.available);
  }
}

module.exports = ProductService;

- ProductClient의 인스턴스를 생성하여 API 호출한다. 이로 인해 ProductService는 ProductClient에 강하게 결합되어 있다고 볼 수 있다.

 

3. 테스트 코드:

const ProductClient = require('../product_client');
const ProductService = require('../product_service_no_di');

jest.mock('../product_client');

describe('ProductService', () => {
  // ...
});

- ProductClient를 모킹하여 테스트를 수행하지만, 실제 의존성 주입을 사용하지 않기 때문에 유연성이 떨어지는 부분이다.

 


 

# Stub 형식의 의존성 주입 코드

1. ProductService:

class ProductService {
  constructor(productClient) {
    this.productClient = productClient;
  }

  async fetchAvailableItems() {
    const items = await this.productClient.fetchItems();
    return items.filter((item) => item.available);
  }
}

module.exports = ProductService;

- 생성자에서 ProductClient를 매개변수로 받아서 의존성을 주입받는다. 이로 인해 ProductService는 특정 구현에 종속되지 않고, 다양한 클라이언트를 사용할 수 있다.

 

2.StubProductClient:

class StubProductClient {
  async fetchItems() {
    return [
      { item: '우유', available: true },
      { item: '바나나', available: false },
    ];
  }
}

module.exports = StubProductClient;

- fetchItems 메서드가 고정된 데이터를 반환하는 stub 클라이언트이다. 이를 통해 테스트 환경에서 쉽게 제어할 수 있다.

 

3. 테스트 코드:

const ProductService = require('../product_service');
const StubProductClient = require('./stub_product_client');

describe('ProductService - Stub', () => {
  let productService;

  beforeEach(() => {
    productService = new ProductService(new StubProductClient());
  });

  it('should filter out only available items', async () => {
    const items = await productService.fetchAvailableItems();
    expect(items.length).toBe(1);
    expect(items).toEqual([{ item: '우유', available: true }]);
  });
});

- ProductService를 생성할 때 StubProductClient를 주입하여 테스트를 수행한다.

 


 

 

# 비교 및 분석

1. 의존성 주입:

- 기존 코드에서는 ProductService가 ProductClient를 직접 생성하여 의존성을 관리한다. 반면, stub 형식의 코드에서는 ProductService가 외부에서 주입된 ProductClient를 사용한다.

이로 인해 ProductService는 다양한 클라이언트(예: 실제 API 클라이언트, stub 클라이언트 등)를 사용할 수 있어 유연성이 높아진다.

 

2. 테스트의 독립성:

- stub 형식의 코드에서는 테스트에서 StubProductClient를 주입하여 API 호출 없이도 ProductService의 기능을 검증할 수 있다. 외부 의존성에서 확실히 벗어날 수 있다.

 

3. 재사용성:

- 의존성 주입을 통해 ProductService는 다양한 환경에서 재사용 가능해진다. 테스트 환경에서는 StubProductClient를, 실제 서비스 환경에서는 실제 ProductClient를 주입하여 사용할 수 있다.

 


 

 

# 추가 공부...

1. ProductService:

이 서비스는 주로 제품과 관련된 작업을 수행한다. 예를 들어, 사용 가능한 제품을 가져오는 기능말이다.

 

2. StubProductClient:

이건 테스트를 위해 만든 특별한 클라이언트이다. 실제 API 대신에 고정된 데이터를 반환한다. 즉, 실제로 서버에 요청하지 않고도 제품 데이터를 테스트할 수 있게 해준다.

 

3. 전체 코드 역할

- new StubProductClient()는 StubProductClient의 새로운 인스턴스를 생성하는 코드이다. 이 인스턴스는 fetchItems 메서드를 가지고 있으며, 이 메서드는 고저왼 제품 데이터를 반환한다.

 

- new ProductService(new StubProductClient())는 productService의 새로운 인스턴스를 생성한다.

여기서 중요한 점은 ProductService가 StubProductClient의 인스턴스를 매개변수로 받는다는 것이다.

 

# 이해를 돕기 위한 비유
친구에게 음료수를 주고 싶다고 가정해 보자.

**ProductService**는 나고,
**StubProductClient**는 내가 친구에게 주기 위해 미리 준비해 놓은 음료수이다.
여기서 new ProductService(new StubProductClient())는 내가 친구에게 음료수를 주기 위해 미리 준비한 음료수(스텁)를 사용하는 상황이다.

#요약
new StubProductClient()는 고정된 데이터를 제공하는 객체를 만든다.
new ProductService(new StubProductClient())는 그 객체를 사용하여 ProductService를 만든다.
이 방식으로 ProductService는 실제 API에 의존하지 않고도 동작할 수 있다. 즉, 테스트를 쉽게 할 수 있게 해주는 것이다

 

 

'TDD' 카테고리의 다른 글

리액트에서 테스트는?  (1) 2024.09.05
TDD - stub 방식과 mock 방식  (2) 2024.09.04
테스트 코드의 원칙을 다시 상기하자.  (0) 2024.08.29
TDD 예제(2)  (1) 2024.08.29
TDD 준비 및 예제(1)  (0) 2024.08.29