코드의 구린내

Updated:

코드의 구린내

  • 리펙토링을 해야하는 정확한 시점은 없다.
  • 많은 연습을 통한 자신만의 기준을 통해서 감을 잡는 방법밖에 없다.

1. 중복코드

  • 똑같은 구조가 두 군데 이상 있을 때는 그 부분을 하나로 통일하면 개선된다.
  1. 한 클래스의 두 메서드 안에 같은 코드가 있는 경우이다.
    • 이럴 때는 메서드 추출 기법으로 중복을 없애고 메서드 상향 기법을 적용.
    • 코드가 똑같지 않고 비슷하다면 메서드 추출 기법을 적용하여 같은 부분과 다른 부분을 분리. 그런 다음 경우에 따라 템플릿 메서드 형성 기법을 적용.
  2. 두 메서드가 알고리즘만 다르고 기능이 같다면
    • 두 알고리즘 중에서 더 간단한것을 택해서 알고리즘 전환을 적용하면 된다.
  3. 중복 코드가 메서드 가운데에 있다면 주변 메서드 추출을 적용하면 된다.

  4. 서로 상관없는 두 클래스 안에 중복 코드가 있을 때는 한 클래스 안의 중복 코드를 클래스 추출이나 모듈 추출을 적용해 제 3의 클래스나 모듈로 떼어낸 후 그것을 다른 클래스에서 호출하는 방법이 있다

2. 장황한 메서드

  1. 메서드의 기능을 한 눈에 알 수 있는 메서드명을 사용해라.
    • 이를 위해서 메서드를 훨신 과감하게 쪼개야 한다.
    • 주석을 달아야 할 것 같은 부분에 주석을 넣는 대신 메서드를 작성한다.
    • 메서드 안에 주석을 단 코드를 넣고, 그 메서드명은 기능 수행 방식이 아니라 목적(즉, 기능 자체)을 나타내는 이름으로 정한다.
    • 메서드 호출이 원래 코드보다 길어져도, 메서드 명은 그 코드의 의도를 잘 반영하는 것으로 정해야 한다.
    • 메서드 길이가 아니라 메서드 기능과 수행 방법이 서로 다른 의미임을 먼저 이해해야 한다.
    • 메서드 추출 기법을 사용하면 메서드가 줄어든다.
    • 그러나 매개변수와 임시 변수가 많으면 메서드 추출이 어려워진다.
    • 임시 변수를 메서드 호출 전환 기법 or 메서드 체인 전환 기법으로 제거한다.
    • 매개 변수는 객체 전환 기법과 객체를 통째로 전달 기법을 적용한다.
    • 여전히 임시, 매개 변수가 많다면 메서드를 메서드 객체 전환 기법을 적용한다.
  2. 코드를 여러 덩어리로 분리하려면 어떻게 할까?

    • 주석을 보면 이런 의지적 차이를 구분할 수 있다.

    • 기능 설명이 주석으로 처리된 코드 구간을 메서드로 만들면 된다.

    • 이때 메서드명은 주석에 설명된 기능을 참고해서 정하면 된다.

    • 한 줄 밖에 안되는 코드라도 별도의 주석이 달려 있을 정도로 다른 기능을 수행한다면 메서드로 추출해야 한다.

  3. 조건문과 루프도 역시 메서드로 빼야 한다. 조건문을 추출하려면 조건문 쪼개기기법을 사용해야 한다.

  4. 루프를 컬렌션 클로저 메서드로 전환을 실시한 후 그 클로저 메서드 호출과 클로저 자체에 메서드 추출을 실시하면 된다.

3. 방대한 클래스

  • 기능이 지나치게 많은 클래스에는 보통 엄청난 수의 인스턴스 변수가 들어 있다. 이는 중복 코드가 반드시 존재한다.
  • 서로 연관된 변수를 골라서 클래스 추출을 실시하면 수많은 인스턴스 변수를 하나로 묶을 수 있다.
  • 만약 추출할 클래스가 대리자로 부적절할 것 같으면 모듈 추출을 실시하면 된다.
  • 인스턴스 변수를 계속해서 모두 사용하지 않는 클래스, 인스턴스 변수가 너무 많은 클래스, 변수가 많이 들어 있는 클래스, 코드 분량이 너무 방대한 클래스는 모두 클래스 추출, 모듈 추출, 하위클래스 추출 중 하나를 여러 번 적용하는 방법도 있다.
  • GUI 클래스라면 데이터와 기능을 서로 다른 도메인 객체로 옮겨야 할수도 있다. 이를 위해 두 곳에 있는 일부 중복 데이터는 놔두고 그 데이터와 싱크를 유지해야 할 수 도 있다. 이때 관측 데이터 복제 기법을 실시하면 해결된다.

4. 과다한 매개변수

  • 이미 알고 있는 객체에 요청하여 한 매개변수에 들어 있는 데이터를 가져올 수 있을 때는 매개변수 세트를 메서드로 전환을 적용하면 된다.
  • 이 객체는 인스턴스 변수일 수도 있고 다른 매개변수 일 수도 있다.
  • 객체에 있는 데이터 세트를 가져온 후, 데이터 세트를 그 객체 자체로 전환하려면 통째로 전달을 적용하면 된다.
  • 여러 데이터 항목에 논리적 객체가 없다면 매개변수 세트를 객체로 전환을 적용하면 된다.
  • 다만 호출되는 객체가 호출 객체에 의존하면 적용하지 말아야 한다.

5. 수정의 산발 (Divergent Change)

  • 수정할 때 개발자는 시스템의 분명한 위치로 곧장 가서 수정할 수 없다면 문제가 있는 것이다.
  • 수정의 산발은 한 클래스가 다양한 원인 때문에 다양한 방식으로 자주 수정될 때 일어난다.
  • 무언가를 추가할 때마다 한 클래스에 여러 메소드를 수정해야 한다면 하나의 클래스를 여러 개의 변형 객체로 분리하는 것이 좋다.
  • 그러면 각 객체는 한 종류의 수정에 의해서만 변경된다.
  • 특정 원인으로 인해 변하는 모든 부분을 찾은 후 클래스 추출을 적용해서 그 부분들을 합쳐 한 클래스로 빼내야 한다.

6. 기능의 산재

  • 수정할 때마다 여러 클래스에서 수많은 자잘한 부분을 고쳐야 한다면 이 문제를 의심할 수 있다.
  • 메서드 이동과 필드 이동을 적용해서 수정할 부분들을 전부 하나의 클래스에 넣어야 한다.
  • 기존의 클래스 중 어느 것에 넣기에도 부적절해 보이는 것은 새 클래스를 만들어야 한다.
  • 클래스 내용 직접 삽입을 적용해서 별도 클래스에 분산되어 있던 모든 기능을 한 곳으로 가져와도 된다.
  • 수정의 산발은 한 클래스에 여러 수정이 발생하는 문제이고, 기능의 산재는 하나의 수정으로 여러 클래스가 바뀌게 되는 문제이다.
  • 둘 중 어느 것이든 수정과 클래스가 일대일 대응되게 깔끔히 정리해야 한다.

7. 잘못된 소속

  • 객체의 핵심은 데이터와 그 데이터에 사용되는 프로세스를 한 데 묶는 기술이라는 점이다.
  • 어떤 메소드가 자신이 속하지 않은 클래스에 더 많이 접근한다면 잘못된 소속이다.

8. 데이터 뭉치

  • 동일한 3~4개의 데이터 항목이 여러 위치에 몰려있는 경우, 객체로 만든다.
  • 데이터 뭉치를 클래스 추출 기법으로 객체로 만든다.
  • 그리고 메서드 시그너처를 대상으로 매개변수 세트를 객체로 전환기법과 객체를 통째로 전달기법을 적용하여 간결하게 만들어야 한다.
  • 이렇게 하면 매개변수가 적어져서 부수적으로 메서드 호출 코드가 간결해지는 효과도 누릴 수 있다.
  • 새로 생긴 객체의 속성들 중 일부만 이용하는 데이터 뭉치라 해도 이 방법으로 효과를 볼 수 있다.
  • 둘 이상의 필드를 객체로 전환하면 코드가 개선된다.
  • 그러한 효과는 여러 데이터 값 중 하나를 삭제해보면 확실히 알 수 있다.
  • 그렇게 했을 때 나머지 데이터 값들이 제대로 돌아가지 않는다면 그 객체를 없애고 새로 만들어야 한다.

9. 강박적 기본 타입 사용

  • 객체의 주요 장점 중 하나가 바로 기본 카입 클래스와 응용 클래스 간의 경계를 허문다는 점이다.
  • 언어에 내장된 기본 타입과 구별하기 힘든 작은 클래스를 손쉽게 작성할 수 있다.
  • 자바에는 문자열과 날짜가 클래스로 존재한다.
  • 우편번호, 전화번호 같은 특수 문자열 클래스 등의 사소한 작업에 작은 객체를 잘 사용하지 않으려는 경향이 있다.

  • 데이터 값을 객체로 전환을 실시한다.

  • 데이터 값이 분류 부호일 땐 그 값이 기능에 영향을 주지 않는다면 분류 부호를 클래스로 전환을 실시하자.
  • 조건문에 분류 부호가 사용될 땐 분류 부호를 하위클래스로 전환 기법이나 분류 부호를 상태/전략 패턴으로 전환 기법을 적용하자.
  • 뭉쳐 다녀야 할 여러 개의 필드가 있다면 클래스 추출 기법을 적용
  • 이런 기본 타입이 매개변수 세트에 들어 있다면 매개변수 세트를 객체로 전환 기법을 적용
  • 배열 때문에 불편하다면 배열을 객체로 전환 기법을 적용

10. Switch 문

  • switch의 단점은 반드시 중복이 생긴다는 점이다. 이를 위해 다형성, 즉 재정의를 이용하는 것이다.
  • 문제는 재정의를 넣을 위치이다.
  • switch 문에는 분류 부호가 흔히 사용되는데, 그럴 땐 분류 부호 값이 들어 있는 메서드나 클래스가 있어야 한다.
  • 이럴 때는 메서드 추출을 실시해서 switch문을 메서드로 빼낸 후 메서드 이동을 실시해서 그 메서드를 재정의해야 할 클래스에 옮겨 넣으면 된다.
  • 그와 동시에 분류 부호를 하위 클래스로 전환 기법과 분류 부호를 상태/전략 패턴으로 전환 기법 중 어느 것을 적용할지 판단해야 한다.
  • 상속구조를 만들었다면 조건문을 재정의로 전환 기법을 적용

  • 하나의 메서드에 영향을 미치는 case 문이 2~3개 밖에 없고 나중에 그 모든 case 문을 수정할 일이 없을 것 같으면, 재정의로 전환하는 것은 과하다.
  • 그럴 때는 매개 변수를 메서드로 변환을 적용하는 편이 낫다.
  • 여러 case 문 중 하나가 null일 때는 null 검사를 널 객체에 위임을 실시하면 된다.

11. 평행 상속 계층

  • 기능의 산재의 특수한 상황이다.
  • 이 문제점이 있으면 한 클래스의 하위클래스를 만들 때마다 매번 다른 클래스의 하위 클래스도 만들어야 한다.
  • 서로 다른 두 상속 계층의 클래스명 접두어가 같으면 이 문제를 의심할 수 있다.
  • 중복 코드 부분을 제거하려면 보통은 한 상속 계층의 인스턴스가 다른 상속 계층의 인스턴스를 참조하게 만들면 된다.
  • 메서드 이동과 필드 이동을 실시하면 참조하는 클래스에 있는 계층이 제거된다.

12. 직무유기 클래스

  • 효율이 떨어지는 클래스는 제거해야 한다.
  • 하위 클래스 모듈인 경우 계층 병합을 실시한다.
  • 쓸모 없는 구성 요소에는 클래스 내용 직접 삽입 or 모듈 내용 직접 삽입 기법을 적용해야 한다.

13. 막연한 범용코드

  • 아직은 필요 없는 기능을 수행하고자 만든 코드는 제거하자.
  • 별다른 기능이 없는 클래스나 모듈이 있다면 계층 병합을 실시
  • 불필요한 위임을 제거하려면 클래스 내용 직접 삽입을 실시해야 한다.
  • 메서드에 사용되지 않는 매개변수가 있으면 매개변수 제거를 실시
  • 메서드명이 이상하다면 메서드명 변경을 실시해야 한다.
  • 메서드나 클래스가 오직 테스트 케이스에만 사용된다면 테스트 케이스를 삭제하자.
  • 단, 적절한 기능을 실행하는 테스트 케이스용 헬퍼 메서드나 클래스는 당연히 삭제하지 말고 내버려둬야 한다.

14. 임시 필드

  • 어떤 객체 안에 인스턴스 변수가 특정 상황에서만 할당되는 경우 클래스 추출을 실시해야 한다.
  • 그렇게 작성한 클래스에 그 변수들과 관련된 코드를 전부 넣어야 한다.
  • null 검사를 널 객체에 위임을 실시해서 그 변수들의 값이 올바르지 않을 경우를 대비한 대체 컴포넌트를 작성하면 경우에 따라 조건문 코드를 없앨 수 있다.

15. 메시지 체인

  • 메시지 체인은 클라이언트가 한 객체에 제 2의 객체를 요청하면, 제 2의 객체가 3의 객체를 요청하고, 다시 4의 객체를 요청하는 연쇄 요청을 말한다.
  • 이런 경우 수많은 코드가 행이 든 geThis 메서드나 임시변수 세트라고 봐도 된다.
  • 이럴 때는 대리 객체 은폐를 실시해야 한다.
  • 그러나 모든 중간 객체가 중개 메서드로 변해서 과잉 중개 메서드의 구린내를 풍기는 문제가 발생한다.

  • 결과 객체가 어느 대상에 사용되는지를 알아내는 방법도 있다.
  • 그렇게 알아낸 객체가 사용되는 코드 부분을 메서드 추출을 통해 별도의 메서드로 빼낸 후 메서드 이동을 실시해서 체인 아래로 밀어낼 수 있는 지 여부를 검사해야 한다.
  • 만약 체인에 속한 객체 중 한 객체의 여러 클라이언트가 나머지 객체들에 왕래한다면 그 기능을 수행하는 메서드를 추가하면 된다.

16. 과잉 중개 메서드

  • 객체의 주요 특징 한가지는 배로 캡슐화다.
  • 캡슐화란 내분의 세부적인 처리를 외부에서 볼 수 없게 은폐하는 작업을 뜻한다.
  • 캡슐화할 때는 대게 위임이 수반된다.
  • 그러나 지나치면 문제가 된다.
  • 어떤 클래스의 인터페이스의 절반도 넘는 메서드가 기능을 다른 클래스에 위임하고 있다면, 과잉 중개 메서드 제거를 실시해서 원리가 구현된 객체에 직접 접근하자.
  • 일부 메서드에 별 기능이 없다면, 메서드 내용 직접 삽입을 실시해서 그 메서드들의 내용을 호출 객체에 직접 삽입하면 된다.
  • 부수적인 기능이 있다면 위임을 상속으로 전환 기법을 실시해서 중개 메서드를 실제 객체의 하위 클래스로 전환하면 된다. 이렇게 하면 모든 위임을 추적하지 않고 기능을 확장할 수 있다.

17. 지나친 관여

  • 클래스끼리 서로 관여하는 경우 메서드, 필드 이동을 실시한다.
  • 클래스의 양방향 연결을 단방향으로 전환 기법을 적용할 수 있는지 판단해서 만약 해당 클래스들이 공통으로 필요로 하는 부분이 있다면, 클래스 추출을 실시해서 공통 필요 부분을 별도의 안전한 클래스로 빼내면 된다.
  • 아니면 대리 객체 은폐를 실시하여 다른 클래스가 중개 메서드 역할을 하게 만들어도 된다.
  • 상속으로 인해 관여가 발생하는 경우, 하위클래스는 항상 상위클래스가 공개하는 것보다 많은 데이터를 필요로 한다.
  • 상위 클래스에서 하위 클래스를 빼내야 할 경우에는 상속을 위임으로 전환 기법을 적용해야 한다.

18. 인터페이스가 다른 대용 클래스

  • 기능은 같은 시그너처가 다른 메서드에는 메서드명 변경을 실시해야 한다.
  • 클래스에 여전히 충분한 기능이 구현되지 있지 않기 때문에 대체로 이 기법만 적용해선 충분하지 않다.
  • 프로토콜이 같이질 때까지 메서드 이동을 실시해서 기능을 해당 클래스로 옮겨야 한다.
  • 단, 코드를 너무 여러 번 옮겨야 한다면 상위클래스 추출을 실시하면 된다.

19. 미흡한 라이브러리 클래스

  • 자신이 직접 라이브러리 클래스를 완성하지 않는 이상 설계를 파악한다는 것이 거의 불가능하다.

  • 라이브러리 클래스에 넣어야 할 메서드가 두 개뿐이라면 외래 클래스에 메서드 추가 기법을 실시.
  • 부가 기능이 많을 때는 국소적 상속확장 클래스 사용 기법을 실시

20. 데이터 클래스

  • 데이터 클래스는 캡슐화 기법을 실시해야 한다.
  • 변경되지 않아야 하는 필드에는 쓰기 메서드 제거를 적용하자.
  • 이런 읽기/쓰기 메서드가 다른 클래스에 의해 사용되는 부분을 찾아서, 메서드 이동을 실시하여 기능을 그 데이터 클래스로 옮겨야 한다.
  • 만약 메서드 전체를 옮길 수없다면 메서드 추출을 실시해서 옮길 수 있는 메서드를 작성하면 된다.
  • 그리고 나서 읽기/쓰기 메서드에 메서드 은폐를 적용하면 된다.

21. 방치된 상속물

  • 하위 클래스는 부모 클래스의 메서드와 데이터를 상속받는다.
  • 그런데 하위클래스가 상속 받은 것 중 필요한 것 외에 방치하면 문제가 생긴다.
  • 기존에는 이 문제의 원인이 잘못된 계층구조 때문이라고 설명했다.
  • 이럴 경우, 새 대등 클래스를 작성하고 메서드 하향과 필드 하향 실시해서 사용되지 않는 모든 메서드를 그 형제 클래스에 몰아넣어야 한다.
  • 이렇게 하면 상위 클래스에는 공통 코드만 들어 있게 된다.
  • 일부 기능을 언제든 재사용하고자 하위클래스에 넣는 작업은 효과적이다.
  • 방치된 상속물로 인해 코드가 복잡해지거나 문제가 생길 때는 위에 설명한 기존 방법을 따르기 권한다.
  • 그러나 이 방법을 반드시 적용해야 하는 것은 아니다.
  • 이 문제는 심각하지 않은 경우가 대부분이기 때문에 리펙토링이 별로 필요가 없다.
  • 방치된 상속물의 구린내는 하위클래스가 기능은 재사용하지만 상위클래스의 인터페이스를 지원하진 않을 때 훨씬 심하게 풍긴다.
  • 상속구현을 거부하는 것은 상관없지만, 인터페이스를 거부하는 것은 심각한 문제다.
  • 하지만 그렇다고 계층 구조를 건드려서는 안되고, 상속을 위임으로 전환 기법을 적용해서 계층구조를 없애야 한다.

22. 불필요한 주석

  • 어떤 코드 구간의 기능을 설명할 주석이 필요할 때는 메서드 추출을 실시해야 한다.
  • 메서드가 이미 추출된 상태임에도 기능을 설명할 주석이 여전히 필요하다면 메서드명을 변경해야 한다.
  • 시스템의 필수적인 상태에 관해 약간의 규칙을 설명해야 할 때는 어설션 넣기를 실시한다.
  • 주석은 무슨 작업을 해야 좋을지 모를 때만 넣는 것이 좋다.
  • 주석을 넣으면 돌아가는 원리를 적어 둘 수도 있고 확실치 않은 부분을 표시할 수도 있다.
  • 어떤 코드를 넣은 이유를 메모해 놓을 경우에도 주석을 넣는 것이 적절하다.