[리팩터링] 11장 코드 구조 따르기

공효은·2023년 6월 2일
0

refactoring

목록 보기
11/12
post-thumbnail

11장 너무 이해하기 어렵다...

이번 장에서 다룰 내용

  • 제어 흐름에 행위 인코딩하기
  • 행위를 데이터 구조로 옮기기
  • 데이터에 행위 인코딩하기
  • 코드에서 활용되지 않는 구조 식별

11.1 범위와 출처에 따른 구조 분류

  • 소프트 웨어 개발에서는 여러 유형의 구조(인색 가능한 패턴)를 다룬다.
    이런 구조는 유사한 두개의 메서드가 될 수도 있고 사람들이 매일 하는 행위가 될 수도 있다.

  • 구조를 4가지 범주로 나눈다. 하나의 기준은 구조가 한 팀이나 (팀 내의) 사람에게 영향을 미치는지, 아니면 여러 팀이나(팀 간의) 여러 사람에게 영향을 미치는지 이다. 다른 기준은 구조가 코드에 있는지, 사람에게 있는지 이다.

    팀 간팀 내
    코드에 있는 경우외부 API데이터와 함수, 대부분의 리팩터링
    사람에 있는 경우조직도, 프로세스행위 및 도메인 전문가

메크로 아키텍처

  • 팀 간 구조에 관한 것이다.
  • 제품이 무엇이며 다른 코드가 그것과 어떻게 상호작용하는가에 대한 것이다.
  • 이것은 외부 API가 어떻게 보여야 하는지, 각 팀이 어떤 데이터를 소유하는 지를 보여주며 소프트웨어 플랫폼을 정의한다.

마이크로 아키텍처

  • 팀 내 구조에 관한 것이다. 즉, 팀이 가치를 제공하기 위해 무엇을 할 수 있는지, 어떤 서비스를 사용하는지, 데이터를 어떻게 구성할지, 그리고 코드를 어떻게 작성할지 등이다.

우리는 조직에서 정의한 프로세스와 계층, 즉 팀 간 커뮤티케이션 방식 안에서 작업한다.
여기서 프로세스는 스크럼(Scrum), 칸반(Kanban), 프ㄹ젝트 모델 등을 의미하며 계층은 누구와 이야기 해야하는지를 정의하는 조직도 또는 그와 유사한 것들이다.

도메인 전문가가 정의 하는 구조

  • 도메인 전문가는 패턴에 익숙한데, 도메인의 패턴은 특정 행위를 반복하기 때문에 전문가들은 이런 패턴에 익숙하다.
  • 전문가들은 소프트웨어가 어떻게 동작하는지 정의한다.
  • 즉, 시스템은 전문가의 행위를 반영한다.

사용자의 행위는 코드 구조에 영향을 미친다.

  • 일부 코드 구조의 변경은 사용자 행위의 변경을 필요로한다. 사용자는 코드의 또 다른 부분으로 볼 수 있다.
  • 리팩터링 관점에서 사용자를 신경 써야하는 데, 이것은 리팩터링을 제약한다. 사용자를 재교육할 수 있다면 사용자는 리팩터링 범위에 포함된다.
  • 우선 모든 비효율성을 포함해 사용자 행위를 있는 그대로 모델링 한 다음, 훈련 및 교육과 함꼐 점차적으로 더 효율적인 기능을 제공해서 사용자의 행위를 리팩터링 하는 것이 유용하다.

11.2 행위를 코드화하는 세 가지 방법

행위를 코드에 반영하는 데는 세 가지 방법이 있다.

  • 제어 흐름
  • 데이터 구조
  • 데이터 자체

각 접근 방식에 대해 알아본다. 차이점을 나타내기 위해 유명한 FizzBuzz 프로그램을 서로 다른 방법을 사용해 보여준다.

FizzBuzz 소개
FizzBuzz는 구구단을 가르치는 어린이용 게임이다. 두 개의 숫자를 선택하면 플레이어가 차례대로 숫자를 말한다. 다음 순서의 숫자가 첫 번쨰 숫자로 나누어 떨어지는 경우 'Fizz' 라고 외친다. 두 번째 숫자로 나눌 수 있는 경우 'Buzz' 라고 외친다. 두 숫자로 모두 나눌 수 있으면 'FizzBuzz' 라고 외친다.
코드로 이 게임을 구현할때는. 숫자 N을 입력 받아 0에서 N까지의 모든 숫자를 출력하는 프로그램을 작성한다. 이때 숫자가 3으로 나누어떨어지면 'Fizz'를 출력한다. 5로 나누어 떨어지면 'Buzz'를 출력한다. 둘 다로 나눌 수 있으면 'FizzBuzz'를 출력한다.

11.2.1 제어 흐름에 행위 코드화 하기

제어 흐름은 제어 연산자, 메서드 호출, 또는 단순히 열거된 코드의 줄을 통해 행위를 텍스트로 표현한다.
다음은 세 가지 가장 일반적인 제어 흐름 유형을 사용한 동일한 반복이다.

// 제어 연산자
let i = 0;
while (i < 5){
  foo(i);
  i++;
}

// 메서드 호출
function loop (i:number) {
  if(i < 5){
    foo(i);
    loop(i + 1)
  }
}

// 코드의 줄
foo(0);
foo(1);
foo(2);
foo(3);
foo(4);

코드 중복에 대해 이야기할 때마다 행위를 코드화한 이 세 가지 범주 사이를 오가면서 일반적으로 가장 오른쪽 유형인 열거된 반복적인 코드 줄의 유형을 지양하는 것에 대해 이야기한다.

이 세가지 범주는 묘하게 다르다. 메서드 호출과 열거된 줄은 비지역적 구조를 표현할 수 있지만 루프는 지역적으로만 동작할 수 있다.

//메서드 호출
function insert(data:object) {
  let db = new Database();
  let normalized = normalize(data);
  db.insert(normalized);
}

function a() {
  // ...
  insert(obj1);
  // ...
}

function b() {
   // ...
  insert(obj2);
  // ...
}

// 열거된 코드 줄
function a() {
  // ...
  let db = new Database();
  let normalized = normalize(obj1);
  db.insert(normalized);
  // ...
}
function b() {
  // ...
  let db = new Database();
  let normalized = normalize(obj2);
  db.insert(normalized);
  // ...
}

다른 관점에서, 제어 연산자와 메서드 호출은 일련의 반복적인 줄로는 할 수 없는 작업, 바로 무한 루프를 생성할 수 있다.

// 제어 연산자
for (;;) { }

// 메서드 호출
function loop() {
  loop();
}

제어 흐름 안에 행위를 기술하면 단순히 여기저기 문장을 이동하는 것만으로도 흐름을 변경할 수 있기 때문에 커다란 변화를 만들기 쉽다.
흔히 우리는 안정성과 작은 변화를 선호하기 때문에 제어 흐름을 사용해 리팩터링하지 않는다.
그러나 어떤 상황에서는 큰 조정이 필요하다. 이런 경우 동작을 제어 흐름으로 리팩터링 한 다음, 이를 다시 리팩터링 하는 것이 유용할 수 있다.

대부분의 사람은 제어 흐름 안에 FizzBuzz를 인코딩해서 구현한다.

// 제어 흐름을 사용한 FizzBuzz
function fizzBuzz(n:number){
  for(let i = 0; i < n; i++){
    if(i % 3 === 0 && i % 5 === 0){
      console.log("FizzBuzz");
    }else if(i % 5 === 0){
      console.log("Buzz");
    }else if(i % 3 === 0){
      console.log("Fizz");
    }else{
      console.log(i);
    }
  }
}

11.2.2 데이터 구조에 행위 코드화하기

for, while 같은 제어 연산자 및 재귀 함수를 대신해서 타입을 이용해 무한 루프를 만드는 방법을 살펴보자. 이 예에서는 재귀 데이터 구조를 사용한다.
Rec 에는 타입이 Rec인 필드 f가 있다. 즉, 재귀 데이터 구조이다.

필드 f가 함수이기 때문에 Rec 객체를 취해서 그 안에 있는 함수 f를 가져와 동일한 Rec 객체를 인자로 호출하는 helper 함수를 정의할 수 있다.

이제 Rec 객체를 인스턴스화하고 helper 함수에 전달할 수 있다.
이 예제에서는 어떤 함수도 직접 호출하지 않는다. helper함수는 Rec 데이터 구조를 통해 스스로를 호출한다.

//재귀 데이터 구조
class Rec {
  constructor(public readonly f: (_:Rec) => void) { }
}

function loop() {
  let helper = (r: Rec) => r.f(r);
  helper(new Rec(helper));
}

제어 흐름에서의 행위와 비교해서 이 접근 방식을 사용하면 기존의 변형이 발생하는 지점(변형점)과 일치하지 않는 한 큰 변경을 수행하기가 더 어렵다.
그러나 작은 변경은 더 쉽고 안전하다. 이것은 더 많은 타입 안전성과 지역성을 얻기 때문이다.

11.2.3 데이터에 행위 코드화하기

마지막 접근 방식은 데이터에 행위를 코드화 하는것이다. 이것은 도구와 컴파일러의 정지 문제 사각지대에 빠르게 맞닥 뜨리기 떄문에 가장 어렵다.

중복된 데이터로 데이터를 구조화 하는 것을 흔히 볼 수 있다. 이것은 특히 데이터가 변경 가능할 경우 일관성 문제로 이어질 수 있다. 성능 향상은 이런 문제를 정당화 할 수 있지만 오류의 원인이 된다.

데이터로 무한 루프를 만들기 위해서는 타입스크립트, 자바 및 C# 배열의 참조를 사용해야 하는데, 객체는 참조로 처리한다. 원리는(자기 자신의) 참조를 찾아 호출하는 함수를 메모리에 넣는것이다. 함수는 직접이 아닌 간접으로 힙을 통해 다시 자신을 호출한다.

// 재귀 데이터
function loop() {
  let a = [() => { }];
  a[0] = () => a[0]();
  a[0]();
}

이 방법은 컴파일러의 지원을 받지 못하기 떄문이 안전하게 사용하기 어렵다.
한 가지 해결책은 데이터를 검색해서 데이터에서 데이터 구조를 생성하는 도구를 사용하는 것이다.
결과적으로 동작을 복제하고 도구를 직접 유지보수하거나 외부 라이브러리를 사용해야한다.

11.3 구조 노출을 위한 코드 추가

  • 기본 구조에 대한 확신이 없다면 리팩터링 노력을 줄이고 먼저 정확성에 집중해야한다.
  • 새로운 기능이나 하위 시스템을 구현할 떄는 불확실성이 있다. 이런 상황에서는 빠르게 변경할 수 있도록 클래스보다는 열거형이나 루프를 사용하는 것이 좋다.

11.4 예측 대신 관찰, 그리고 경험적 기술 사용

  • 변경 범위를 예측하려는 시도는 코드베이스에 도움이 되기보다 손상을 준다.

  • 코드를 추측하지 말고 경험적인 기술을 사용해야한다.

  • 강력한 도구가 있더라도 항상 사용해서는 안된다. 코드가 어떻게 변경될지는 관찰이 필요하다.

  • 변경 되지 않으면 아무것도 하지말라

  • 예측할 수 없이 변경되는 경우 취약성을 피하기 위해서 리팩터링하라

  • 그렇지 않으면, 과거에 발생한 변경 유형을 적용해 리팩터링하라.

11.5 코드를 이해하지 않고도 안전성을 확보하는 방안

리팩터링은 제어 흐름, 자료구조, 데이터 사이에서 동작을 이동시킨다. 구조가 코드에 있기 때문에 기본 도메인이나 구조에 관계없이 이것은 사실이다. 이미 코드에 있는 구조를 따르고 실수 없이 믿을 만한 리팩터링 패턴을 사용하는 한 작업을 위해 코드를 이해할 필요가 없다.

11.5.1 테스트를 통한 안전성 확보

  • 안전을 확보하기 위한 가장 일반적인 접근 방식은 코드를 테스트 하는 것이다.
  • 사용자 입장에서 공감해보기 위해 그렇게 한다.
  • 테스트를 자동화 하라!

11.5.2 숙달을 통한 안전성 확보

  • 리팩터링을 작은 단계로 분할 한다.
  • 안전한 환경에서 반복적인 리팩터링을 수행해서 기계처럼 할 수 있게 숙달한다.

11.5.3 도구의 지원을 통한 안전성 확보

  • IDE에 있는 리팩터링 기능을 활용하라.
  • 도구에 버그가 있을 수 있지만 버그가 빠르게 패치되어 위험이 줄어든다.
    => IDE 잘 활용해보기!

11.5.4 공식 인증을 통한 안전성 확보

  • 증명 보조기를 사용해서 프로그램의 논리적인 명제가 올바른지 기계적으로 확인할 수 있다.

11.5.5 내결함성을 통한 안전성 확보

오류가 발생하더라도 자체 수정하도록 코드를 작성한다.
실패 시 자동 롤백하도록 토글 기능을 추가할 수 있따. 이렇게 하면 리팩터링 하는 동안 실수로 인해 코드가 실패하더라도 기능 전환 시스템이 자동으로 이전 코드로 되돌린다.

11.6 활용되지 않은 구조 이용

코드에서 활용 가능한 구조를 찾는 가장 일반적인 위치와 사용 방법

11.6.1 추출 및 캡슐화에 공백 활용

공백을 넣어 그룹화된 문장을 볼때마다 메서드 추출 패턴을 생각해봐야한다.

다음 예제에서 함수는 배열 내의 최솟값을 찾아내어 배열의 모든 요소에서 해당 값을 빼는 작업을 한다. 빈 줄로 구분된 두 개의 부분이 존재한다.

//변경 전
function subMin(arr:number[]){
  let min = Number.POSITIVE_INFINITY;
  for(let x = 0; x < arr.length; x++){
    min = Math.min(min, arr[x]);
  }
  
  for(let x = 0; x < arr.length; x++) {
    arr[x] -= min;
  }
}

// 변경 후
function subMin(arr:number[]){
  let min = findMin(arr)';
  subtractFromEach(min, arr);
}

function findMin(arr:number[]){
  let min = Number.POSITIVE_INFINITY;
  for(let x = 0; x < arr.length; x++){
    min = Math.min(min, arr[x]);
  }
  return min;
}

function subtractFromEach(min:number, arr: number[]){
  for(let x = 0; x < arr.length; x++) {
    arr[x] -= min;
  }
}

공백은 필드를 그룹화 하는데도 사용된다. 이 경우 공백은 어떤 데이터 요소가 더 관련이 있는지(즉, 함께 사용)를 나타낸다. 이 또한 데이터 캡슐화 리팩터링 패턴을 통해 이 구조를 활용하는 연습을 할 수 있다.

다음 예에는 x,y 및 color 필드를 가진 Particle 클래스가 있다. 공백을 통해 x와 y가 color보다 더 밀접하게 연결되어 있음을 추론할 수 있고, 이를 활용한다.

//변경 전
class Paricle {
  private x: number;
  private y: number;

  private color: number;
}

//변경 후
class Vector2D {
  private x: number;
  private y: number;
}

class Paricle {
  private position: Vector2D;
  private color:number;
  // ...
}

11.6.2 통합에 중복 코드 활용

서로 가까이, 또는 서로 다른 클래스의 여러 메서드에 걸쳐 중복된 문장을 가질 수 있다. 두 경우 모두 기본적인 메서드 추출 리팩터링 패턴으로 시작한다.
이 예에는 두개의 포매터(Formater)가 있다. 두 클래스 모두에 나타나는 result += 문의 처리가 다르므로 이를 메서드로 추출한다.

// 변경 전
class XMLFormatter {
  format(vals: string[]) {
    let result = "";
    for (let i =0; i < vals.length; i++){
      result += `<Value>${vals[i]}</Value>`
    }
    return result;
  }
}
class JSONFormatter {
  format(vals: string[]){
    let result = "";
    for (let i =0; i < vals.length; i++){
      if(i > 0) result += ",";
       result += `{value: "${vals[i]}"}`;
    }
    return result;
  }
}

//변경 후
class XMLFormatter {
  format(vals: string[]){
    let result = "";
    for (let i =0; i < vals.length; i++) {
      result += this.formatSingle(vals[i);
    }
    return result;
  }
  formatSingle(val: string){
    return `<Value>${vals[i]}</Value>`
  }
}

class JSONFormatter {
  format(vals: string[]){
    let result = "";
    for (let i =0; i < vals.length; i++){
      if(i > 0) result += ",";
       result += `{value: "${vals[i]}"}`;
    }
    return result;
  }
  formatSingle(val: string){
    return `{value: "${vals[i]}"}`;
  }
}

추출된 메서드가 여러 클래스에 분산되어 있을 경우 이번에는 메서드에 데이터 캡슐화를 사용해서 한 곳으로 모을 수 있다.

// 변경 전
class XMLFormatter {
  formatSingle(val: string){
    return `<Value>${vals[i]}</Value>`
  }
  // ...
}

class JSONFormatter {
  formatSingle(val: string){
    return `{value: "${vals[i]}"}`;
  }
  // ...
}

// 변경 후
class XMLFormatter {
  formatSingle(val: string){
    return new XMLFormatSingle()
      .format(val);
  }
  // ...
}

class JSONFormatter {
  formatSingle(val: string){
    return new JSONFormatSingle()
      .format(val);
  }
  // ...
}

class XMLFormatSingle {
  format(val: string) {
    return `<Value>${vals[i]}</Value>`
  }
}

class JSONFormatSingle {
  formatSingle(val: string){
    return `{value: "${vals[i]}"}`;
  }
}

보유한 메서드가 동일하면 해당 클래스도 동일하기 때문에 하나만 남기고 삭제할 수 있다. 캡슐화한 클래스들이 단순히 유사한 경우나 클래스들이 중복된 것을 볼 때마다 유사 클래스 통합 패턴을 사용해 통합할 수 있다.

// 변경 전
class XMLFormatSingle {
  format(val: string) {
    return `<Value>${vals[i]}</Value>`
  }
}

class JSONFormatSingle {
  formatSingle(val: string){
    return `{value: "${vals[i]}"}`;
  }
}

// 변경 후
class XMLFormatter {
  formatSingle(val: string){
    return new FormatSingle("<Value>","</Value>")
      .format(val);
  }
  // ...
}

class JSONFormatter {
  formatSingle(val: string){
    return new FormatSingle("{value: '","'}")
      .format(val);
  }
  // ...
}

class FormatSingle {
 constructor(
   private before: string,
   private after: string
  ){}
  format(val:string){
    return `${before}${val}${after}`;
  }
}

문장의 제어 흐름은 유사한데, 문장 자체가 다른 경우 전략 패턴의 도입을 사용해서 동일 하게 할 수 있다.

// 변경 전
class XMLFormatter {
  format(vals: string[]){
    let result = "";
    for (let i = 0; i < vals.length; i++) {
      result += new FormatSingle("<Value>","</Value>").format(vals[i]);
    }
    return result;
  }
}

class JSONFormatter {
  format(vals: string[]){
    let result = "";
    for (let i = 0; i < vals.length; i++) {
     if(i > 0) result += ",";
     result += new FormatSingle("{value: '","'}").format(vals[i]);
    }
    return result;
  }
}

//변경 후
class XMLFormatter {
  format(vals:string[]){
    return new Formatter (
      new FormatSingle("<Value>","</Value>"),
      new None()
      ).format(vals);
    )
  }
}

class JSONFormatter {
  format(vals:string[]){
    return new Formatter (
      new FormatSingle("{value:'","'}"),
      new Comma()
      ).format(vals);
    )
  }
}

class Formatter {
  constructor(
  	private single: FormatSingle,
    private sep: Separator
  ){}
  format(vals: string[]){
    let result = "";
    for (let i = 0; i < val.length; i++){
      result = this.sep.put(i, result);
      result += this.single.format(vals[i]);
    }
    return result;
  }
}
interface Separator {
  put(i:number, str:string):string;
}
class Comma implements Separator {
  put(i:number, result:string){
    if(i > 0) result += ',';
    return result;
  }
}
class None implements Separator {
  put(i:number,result:string){
    return result;
  }
}

이 시점에서 두 원본 포매터는 상수 값만 다르기 때문에 쉽게 통합할 수 있다.

11.6.3 캡슐화로 공통 접사 활용

공백이나 중복, 또는 이름에 공통적인 명칭을 통해 그룹화된 것을 발견할 떄 이 구조를 견고하게 하는 방법은 데이터 캡슐화이다.
유사한 이름을 가진 클래스들을 그룹하 하는데도 이 규칙이 사용될 수 있다.
타입스크립트에는 네임스페이스나 모듈을 사용한다.

//변경 전
interface Protocol {...}
class StringProtocol implements Protocol {...}
class JSONProtocol implements Protocol {...}
class ProtobufProtoco implements Protocol {...}

let p = new StringProtocol()
///...

// 모든 클래스에 공통 접사를 사용하지 말 것 규칙을 어기는 공통 접미사 Protocol이 존재한다. 이 경우 String이 내장 클래스 명칭과 충돌하기 때문에 Protocol을 바로 제거할 수 없지만 먼저 네임스페이스에 세 개의 클래스와 인터페이스를 캡슐화 할 수 있는 경우에는 그렇지 않다.

//변경 후
namespace protocol {
  export interface Protocol {...}
  export class String implements Protocol {...}
  export class JSON implements Protocol {...}
  export class Protobuf implements Protocol {...}
}
/// ...
  let p = new Protocol.String()
///...

11.6.4 동적 실행으로 런타임 유형 활용

  • 객체지향 프로그래밍은 인터페이스를 통한 동적 실행 이라는 강력한 메커니즘이 내장되어있다. 그래서 런타임 타입을 검사하는 기능 없이 고안되었다.
  • 인터페이스를 사용해서 다양한 타입의 클래스를 변수에 넣을 수 있다. 그런 다음 변수에 대한 메서드를 호출할 때 적절한 클래스의 메서드를 호출한다. 이것은 런타임 타입 검사를 피하는 방법이다.
//변경 전
function foo(obj: any){
  if(obj instanceof A){
    obj.methodA()
  }else if(obj instanceof B){
    obj.methodB()
  }
}
class A {
  methodA() {...}
}
class B {
  methodB() {...}
}


//변경 후
function foo(obj: Foo){
  obj.foo(); // 클래스로 이관된 메서드
}
class A implements Foo {
  foo() {
    this.methodA()
  }
  methodA() {...}
}
class B implements Foo {
  foo() {
    this.methodB()
  }
  methodB() {...}
}
  
interface Foo {
  foo(): void
}

요약

  • 코드는 개발, 프로세스 및 해당 도메인과 관련된 사람들의 행위를 반영한다.
  • 제어 흐름에 인코딩된 행위(동작)는 큰 변화를 쉽게 만들 수 있다.
  • 데이터 구조에 인코딩된 행위는 타입의 안전성, 지역성, 성능 및 작은 변경의 용이성과 같은 장점을 제공한다.
  • 데이터에 인코딩된 행위는 최후의 수단으로 사용될 수 있으며 컴파일러 지원이 부족해서 안전하게 유지하기 어렵기 때문에 제한되어야한다.
  • 리팩터링은 이런 접근 방식 중 하나에서 중복을 관리하거나 한 접근 방식에서 다른 접근 방식으로 구조를 변경한다.
  • 코드를 사용해서 구조를 노출하면 리팩터링을 통해 유연하게 만든 후 더 많은 구조를 추가할 수 있다.
  • 경험적으로 공유된 기법을 사용해서 리팩터링 시 빠르게 구조를 통찰하게 하고 계속 변하는 것에 기반을 두지 마라.
  • 코드 구조를 빠르게 파악하기 위해서는 위험을 회피할 목적으로, 흔히 코드에 표시하기 위해 사용한 활용되지 않는 구조를 찾으라. 공백, 중복, 공통 접사 또는 런타임 타입 검사를 활용해 볼 수 있다.
profile
잼나게 코딩하면서 살고 싶어요 ^O^/

0개의 댓글