diff --git "a/jin/[Refactoring] \354\235\230\354\241\264\352\264\200\352\263\204 \354\240\234\352\261\260 \352\270\260\353\262\225 1.md" "b/jin/[Refactoring] \354\235\230\354\241\264\352\264\200\352\263\204 \354\240\234\352\261\260 \352\270\260\353\262\225 1.md" new file mode 100644 index 0000000..2e6b71c --- /dev/null +++ "b/jin/[Refactoring] \354\235\230\354\241\264\352\264\200\352\263\204 \354\240\234\352\261\260 \352\270\260\353\262\225 1.md" @@ -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. 인터페이스 추출 + +- 결국 얻으려는 것은 구현체 추출과 동일하다. +- 어떤 클래스의 일부 동작을 인터페이스화 시켜 구현체를 주입해주는 것