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

[29주차 1] 의존관계 제거 기법 1.md #249

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
246 changes: 246 additions & 0 deletions jin/[Refactoring] 의존관계 제거 기법 1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
# 25. 의존관계 제거 기법

테스트를 작성하기 위해, 혹은 더욱 좋은 설계를 위해 의존관계를 제거하는 기법을 소개한다.

## 25.1 매개변수 적응

- ~~mocking 라이브러리~~

임의로 인터페이스 파라미터에 대한 추상화 ~~계층을~~ 만드는 것이다.

예를 들어 아래 메서드의 파라미터 `HttpServletRequest` 는 사실상 메서드 안에서 파라미터를 전달하는데에만 쓰인다. 호출되는 메서드는 `getParameterValues`

- 테스트를 위한 Fake 구현체 → 23개 힘들다 ㅠㅠ

```java
public class SomeDispatcher {

public void doSomething(HttpServletRequest request) {
String[] values = request.getParameterValues(foo);

...생략

}
}
```

- 짖기 물기 혀 내밀기 → 강아지 추상화 하기가 쉽네

이 때, HttpServletRequest는 인터페이스인데, 약 23개의 메서드를 제공하므로 구현체를 만들기가 쉽지 않다. 따라서 `getParameterValues()` 를 갖는 인터페이스를 만들고 구현체를 전달 해보자는 아이디이다.

필요에 따라 해당 메서드에서 Call 하는 파라미터 메서드들을 추상화 하면 될 것이다.

그리고 구현체를 전달하면 테스트 상황에서 쉽게 테스트 할 수 있다.

물론 이는 큰 변화에 대당한다. 충분한 테스트가 필요하다.

## 25.2 메소드 객체 추출

인스턴스화가 어려운 객체에 적용한다.

인스턴스화가 어려운 객체의 경우, 리팩토링시 실제로 변경하는 부분은 적은데에 비해 테스트 작성을 위해 많은 작업이 필요하게 된다.

1. 메소드의 크기가 크거나
2. 인스턴스 변수 및 메소드를 사용하는 경우

→ 메소드 객체 추출 기법을 사용할 수 있다.

→ **대규모 메서드를 새로운 클래스로 추출하는 것**

이는 메소드 객체라고 부른다.

많은 경우 테스트 루틴 작성이 용이해짐

예를 들어 GDIBrush 클래스가 가진 draw 메서드를

Renderer라는 클래스로 빼낸다. Renderer의 생성자에는 brush가 있고, 이것을 사용한다.

(추출한 클래스가 기존 클래스를 참조할 수 있어야 한다.)

```java
브러쉬 클래스
private final 화가;

-> 그리기(화가) {
화가_인스턴스.그리기()
}

Fake_화가 {
그리기() {
내용물
}
}
```

기존 클래스의 인스턴스 변수나 메소드를 사용하고 있다면, 이는 내부적으로 참조한 기존 클래스를 호출해 사용한다. 그리고 기존 클래스의 메서드 (draw)는 이제 내부적으로 Renderer를 만들어 draw()를 호출하는 방식으로 바꾼다.

**정리하자면, 어떤 클래스가 가진 메서드의 책임을 아예 다른 클래스로 돌리기 위해, 새로운 클래스를 정의한다. 그리고 기존 클래스가 새로운 클래스를 의존해 그 클래스에게 책임을 위임 하는 것이다.**

## 25.3. 정의 완성

일부 언어에서는 어떤 타입을 선언한 후 다른 곳에서 정의할 수 있다.

→ java에선 가능한가? pass

## 25.4. 전역 참조 캡슐화

전역적인 요소에 대한 의존 관계에 문제가 있는 코드를 테스트 할 때 3개의 선택지가 있다.

1. 테스트할 때 전역 요소가 다르게 동작하도록 하는것

→ 생각나는 예시) Config 를 환경별로 설정, 전역 요소를 갖는 클래스나 인터페이스 만들어 환경에 따라 다른 구현체 사용하도록 하기.

2. 다른 전역 요소와 연결 시키는 것 (통테?)

→ 그냥 의존성 있는 객체들을 연결해서 써 버리는 것일까?

3. 전역 요소를 캡슐화해 다른 것들과 분리하는 것

→ ~~상수 클래스를 만든다던지..~~


여기서 마지막 방법을 전역 참조 캡슐화라고 한다.

이후 인스턴스를 만들어 해당 인스턴스에서 맴버 변수들을 가져다 쓴다.

```java
Config 클래스 -> 얘가 전역 정보들
public static Config CONFIG_INSTANCE -> 얘를 사용
getter,

public static final 어떤_클래스 어떤_맴버 = ??;

방어적 복사를 한다던지...
```

책에 나온 내용은 아니지만, 생성자를 막고, 인스턴스를 public으로 만들던지 getter를 제공할 수 있을 것이다. 혹은 Read Only로 사용할 수 있도록 변수 getter들만 제공하는 방법도 있을 것이다.

## 25.5 정적 메서드 드러내기

이번에도 인스턴스화가 어려운 클래스를 다루는 방법에 해당.

```java
이진호 클래스
이진호_패기();
이진호 HP -- 몸이 다칠거 -> 기록 상태가 필요
인스턴스가 필수

public static String get_출생년도(); ->
언제 static을 써야 하는가? 그리고 언제 쓰지 X 그리고 트레이드 오프는?

유틸_클래스 상태 less
뭐 더하기 -> 애초에 static
```

메서드에서 실제로 해당 클래스에 대한 인스턴스를 만들 필요가 없는 경우, 그냥 사용하는 메서드를 정적 메서드로 만드는 것이다.

잘 생각해보면 클래스의 모든 메서드들이 꼭 인스턴스화 되어야 하는 것은 아닐 수 있다.

특히 인스턴스별로 다른 상태를 갖지 않는다면? 예를 들어 어떤 상자 클래스가 있고 인스턴스를 만들려면 색깔 공을 넣어야 한다. 그리고 open 메서드를 호출하면 생성시 주입했던 색의 공을 반환한다.

이런 경우 내부 상태를 사용한다고 할 수 있다.

그리고 어떤 카운터를 내부적으로 가지고 있어서 메서드를 호출할 때마다 호출된 횟수를 세는 클래스가 있다면 내부 상태를 유지해야 한다.

하지만 어떤 클래스는 따로 의존성을 주입 받을 필요가 없고, 상태를 유지할 필요도 없다.

(불변의 진리란 없지만) 인간이라는 종은 공기로 숨을 쉰다.

인간 클래스에서 `get_숨쉬는_방법()` 을 호출 했을 때, 인스턴스와 무관하게 “공기”가 리턴 되어야 한다. 이 장에서는 이런 케이스들에 대해 정적 메서드로 만들자고 주장하는 것이다.

특히 각종 유틸 클래스에서 자주 사용할 수 있는 방법일 것이다.

단순한 계산기, 어떤 검증 클래스, 매퍼 클래스는 쉽게 static으로 선언할 수 있다.

하지만, 나는 테스트를 위해 정적 메서드가 아닌 것을 정적 메서드로 만드는 것이 옳은지는 모르겠다.

물론 테스트가 어려워서 다시 살펴보니 “원래 부터 정적 메서드로 설계 되는게 나았겠군”이라고 깨닫고 고치는 것은 좋다. 즉, 더 나은 설계에 도움이 된다면 바꾸는 것을 찬성한다.

정적 메서드는 언제, 왜 쓰는 것일까?

## 25.6 호출 추출과 재정의

앞서 나온 메서드 추출과 비슷한듯 조금 다르다.

목표는 메서드 호출의 의존 관계를 제거하는 것인데 조금 낯선 방법은 맞다.

의존성을 끊고 싶은 외부 메서드 호출부를 클래스의 메서드로 한번 래핑한다.

예를 들어 `SomeClass.callSomthing()` 이런 부분이 있다면,

아래와 같이 한번 래핑하는 것이다

```java
class MyClass {

public void test() {
/////////
callSomthing(); // 얘 떄문에 테스트 문제가 있음
}

public Something callSomething() {
return SomeClass.callSomthing();
}
}

class TestMyClass extends MyClass
```

그럼 그 다음 어떻게 활용하는가 하면..

**환경에 따라 하위 클래스를 다르게 써준다.**

마치 인터페이스를 활용한 의존성 역전의 클래스 버전 같다.

상당히 생소한데 쓰이지 않는 데는 분명 이유가 있을 것이다.

그게 무엇일까?라고 하면 나도 모르겠다.

1. 추출하고자 하는 호출을 식별하고, 메소드 선언을 찾는다.
2. 현재 클래스에 신규 메서드를 작성하고, 추출한 호출 시그니처를 옮긴다.
3. 기존의 메서드 중 호출 부분을 신규 메서드 호출로 대체한다.
4. 테스트용 서브 클래스 도입 신규 메서드 정의

## 25.7 팩토리 메서드 추출과 재정의

어떤 객체의 생성자 안에서 일어나는 일을 테스트 하려 하기는 어렵다.

**특히 생성자 안에서 다른 객체를 생성한다면**, 테스트 하기 어렵다.

```java
new 뭐();
```

이떄 생성자 안의 객체 생성을 팩토리 메서드로 추출 할 수 있다.

다른 객체로 추출하라는 것은 아니다. 메서드로 추출한 다음,

서브 클래스에서 이를 재정의 하는 것이다.

이를 통해 테스트용 팩터리 메서드를 정의해버릴 수 있다.

## 25.8. get 메서드 추출과 재정의 pass

1. get 메서드를 필요로 하는 객체를 식별한다
2. 객체 생성에 필요한 로직을 모두 추출해 get 메소드에 넣는다.
3. 객체를 사용하는 부분을 모두 get으로 대체.
4. get 메서드에 첫 호출시의 로직을 추가한다
5. 서브 클래스를 작성하고 get 메서드를 재정의해 테스트 루틴에서 사용되기 위한 대체 객치를 제공

## 25.9. 구현체 추출

- 기존 클래스를 어떤 인터페이스의 구현체로 만드는 방식
- 예를 들어 “이진호”라는 클래스만 달랑 있는데, “치킨 분쇄기”라는 인터페이스를 만들어 구현체로 두는 것
- 사실상 의존성 역전. 환경별로 다른 구현체를 사용하기 위해 추상화가 안 된 어떤 클래스를 추상화한다.
- 테스트시에 매우 용이한 대표적인 방법

![image](https://github.com/user-attachments/assets/51193151-7997-4d0a-b7d2-8ee6077899f3)


![image](https://github.com/user-attachments/assets/68ab88b8-4451-41f0-816f-73e11dacb108)


## 10. 인터페이스 추출

- 결국 얻으려는 것은 구현체 추출과 동일하다.
- 어떤 클래스의 일부 동작을 인터페이스화 시켜 구현체를 주입해주는 것