Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#5][2기] 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 #266

Merged
merged 3 commits into from
May 16, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
# 아이템 5 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라

---

> 클래스가 내부적으로 하나 이상의 **자원**에 의존하고,
그 자원이 클래스 동작에 영향을 준다면
싱글턴(Singleton) 과, 정적 유틸리티(static util class)는 사용하지 않는 것이 좋다.

*즉, 사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않다.*

- 정적 유틸리티를 잘못 사용한 예 - 유연하지 않고 테스트하기 어렵다

```java
// 정적 유틸리티를 잘못 사용한 예 - 유연하지 않고 테스트하기 어렵다
public class SpellChecker{
private static final Lexicon dictionary = ...; // 사전에 의존
private SpellChecker() {} // 객체 생성 방지
public static boolean isValid(String word){...}
public static List<String> suggestions(String typo){...}
}
```

- 싱글턴을 잘못 사용한 예 - 유연하지 않고 테스트하기 어렵다

```java
// 싱글턴을 잘못 사용한 예 - 유연하지 않고 테스트하기 어렵다
public class SpellChecker{
private static final Lexicon dictionary = ...; // 사전에 의존
private SpellChecker(...) {}
public static SpellChecker INSTANCE = new SpellChecker(...);

public static boolean isValid(String word){...}
public static List<String> suggestions(String typo){...}

}
```

- SpellChecker 클래스는 dictionary(자원)에 의존하고 있다
- 그런데, 언어별 사전, 특수 어휘용 사전이 필요하게 되면 클래스 내부 메서드 동작이 달라져야 한다.
- 정적 유틸리티 클래스나 싱글턴 방식은 적합하지 않다.

> 왜냐하면, 유연하지 않고, 테스트 하기 어렵기 때문이다.

- SpellChecker가 여러 사전을 사용할 수 있도록 만들기 위해 dictionary 필드에서 final 한정자를 제거하고 다른 사전으로 교체하는 메서드를 추가할 수는 있다.
- 그러나 이 방법은 어색하며, 오류를 내기 쉽다. 또한 멀티스레드 환경에서는 쓸 수 없다.

> 클래스가 여러 자원 인스턴스를 지원해야하고, 클라이언트가 원하는 자원을 사용하게 하는 방법을 사용해야 한다.
인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주자.

- **의존 객체 주입 방식** 이라고한다.

```java
// 의존 객체 주입은 유연성과 테스트 용이성을 높여준다
public class SpellChecker{
private final Lexicon dictionary;
public SpellChecker(Lexicon dictionary){
this.dictionay = Objects.requiredNonNull(dictionay);
}
public boolean isValid(String word){...}
public List<String> suggestions(String typo){...}
}
```

- 클래스의 유연성, 재사용성, 테스트 용이성을 개선 해준다.
- 불변을 보장해 해당 자원을 사용하려는 여러 클라이언트가 의존 객체들을 안심하고 공유할 수 있다.
- 생성자, 정적 팩터리, 빌더 아이템에 응용할 수 있다.

```java
public class 배달서비스{
private final 배달 치킨배달;
// 정적 유틸리티
private 배달서비스(){}; //객체 생성 방지
public static void 배달시작(){}
}
public class 배달서비스{
private final 배달 치킨배달;
private 배달서비스(..){..};
public static 배달서비스 Instance = new 배달음식서비스(..); // 싱글턴 패턴
public static void 배달시작(){}
}
// 치킨배달만 가능하다 ...
```

```java
public class 배달서비스{
private final 배달 음식배달;
public 배달서비스(배달 음식이름){
this.음식배달 = 음식이름;
}
public static void 배달시작(){}
}
// 어떤 음식도 배달할 수 있는 서비스가 되었다 !
```

> 의존 객체 주입 응용 : 생성자에 자원 팩터리(Factory) 를 넘겨 주는 방식

- 팩터리(Factory) : 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체
- 팩터리 메서드 패턴 (Factory Method pattern) : 생성자에 자원 팩터리를 넘겨주는 방식

<details>
<summary>팩터리 메서드 패턴</summary>
<div markdown="1">

- 팩터리 메서드 패턴
- 객체 생성을 캡슐화 하는 패턴이다
- 객체를 생성하기 위한 인터페이스를 정의하지만, 어떤 클래스의 인스턴스를 생성할지에 대한 결정은 서브 클래스가 내린다.
- Creator의 서브 클래스에 팩토리 메서드를 정의 하며, 팩터리 메서드 호출로 ConcreteProduct 인스턴스를 반환하게 한다.
- 예제코드

```java
// Product 역할
public interface Shopping {
// 쇼핑을 할 때 공통 과정
void find();
void cart();
void order();
}

// Creator (Factory)
public abstract class Shop {
public Shopping shopping(String category){
Shopping shopping = selectCategory(category);
shopping.find();
shopping.cart();
shopping.order();
return shopping;
}

// factory method
abstract Shopping selectCategory(String category);
}
```

```java
public class MarketOne extends Shop {
@Override
Shopping selectCategory(String category) {
System.out.println("THIS IS MARKET ONE.");
if (category.equals("FOOD")) return new MarketOneFoodCategory();
if (category.equals("HEALTH")) return new MarketOneHealthCategory();
return null;
}
}
public class MarketOneFoodCategory implements Shopping {
@Override
public void find() {
System.out.println("market-one: food 검색 ");
}

@Override
public void cart() {
System.out.println("market-one: food 담기 ");
}

@Override
public void order() {
System.out.println("market-one: food 주문 ");
}
}
```
</div>
</details>

- 자바 8의 Supplier<T> 인터페이스가 리소스의 생성자에 전달하는 팩터리로 쓰기에 적합하다.

```java
Mosaic create(Supplier<? extends Tile> tileFactory){..}
```

---

- 의존성이 수천개가 되는 큰 프로젝트에서는 의존 객체 주입이 코드를 어지럽게 만들기도 한다.
- 대거, 주스, 스프링 같은 의존 객체 주입 프레임워크를 사용하면 이런 어질러짐을 해소할 수 있다.

<details>
<summary>스프링 의존성 주입</summary>
<div markdown="1">

- 스프링 의존성 주입
- 스프링 핵심 기능 DI (Dependency Injection)

**외부**(IOC컨테이너) 에서 객체를 생성한 후 의존성을 주입시킨다

```java
@Service
public class ShopService{
public void findAllShop(){
ShopRepository shopRepository = new FoodShop();
shopRepository.findAll();
}
}
```

```java
@Service
public class ShopService{
private ShopRepository shopRepository;
public ShopService(ShopRepository shopRepository){
this.shopRepository = shopRepository;
}
public void findAllShop(){
shopRepository.findAll();
}
}

@Test
public class testFindAll(){
ShopRepository foodShop = new FoodShop();
ShopService shopService = new ShopService(foodShop);
shopService.findAll();
}
```

의존성 주입방법

1. 생성자 이용

```java
@Service
public class ShopService{
private ShopRepository shopRepository;
public ShopService(ShopRepository shopRepository){
this.shopRepository = shopRepository;
}
}
```

2. Field 변수 이용

```java
@Service
public class ShopService{
@Autowired
private ShopRepository shopRepository;
}
```

3. setter 이용

```java
@Service
public class ShopService{
private ShopRepository shopRepository;
@Autowired
public ShopService(ShopRepository shopRepository){
this.shopRepository = shopRepository;
}
}
```


</div>
</details>

---


### 핵심 정리

- 클래스가 내부적으로 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준다면 싱글턴과 정적 유틸리티 클래스는 사용하지 말자.
- 이 자원들을 클래스가 직접 만들게 해서도 안된다.
- 필요한 자원을 (혹은 그 자원을 만들어주는 팩터리를) 생성자에 (혹은 정적 팩터리나 빌더에) 넘겨주자. 의존 객체 주입이라 하는 이 기법은 클래스의 유연성, 재사용성, 테스트 용이성을 기막히게 개선해준다.