본문 바로가기
핌팩토링

7. 캡슐화

by AsCE_hyunseung 2022. 4. 27.

7. 캡슐화

  • 모듈을 분리하는 가장 중요한 기준은 아마도 시스템에서 각 모듈이 자신을 제외한 다른 부분에 드러내지 않아야 할 비밀을 얼마나 잘 숨기느냐에 있다.
  • 클래스는 내부 정보뿐 아니라 클래스 사이의 연결 관계를 숨기는 데도 유용하다.
  • 가장 큰 캡슐화 단위는 클래스와 모듈이지만 함수도 구현을 캡슐화한다.

7.1 레코드 캡슐화하기

배경

  • 레코드는 연관된 여러 데이터를 직관적인 방식으로 묶을 수 있어서 각각을 따로 취급할 때보다 훨씬 의미 있는 단위로 전달할 수 있게 해준다.
    • 단점으로는 계산해서 얻을 수 있는 값과 그렇지 않은 값을 명확히 구분해 저장해야 하는 점이 번거롭다.
  • 가변 데이터를 저장하는 용도로는 레코드보다 객체를 선호한다.
    • 객체를 사용하면 어떻게 저장했는지를 숨긴 채 세 가지 값을 각각의 메서드로 제공할 수 있다.
    • 사용자는 무엇이 저장된 값이고 무엇이 계산된 값인지 알 필요가 없다.
    • 캡슐화하면 이름을 바꿀 때도 좋다.
  • 가변 데이터일 때 객체를 선호하고 불변이면 레코드를 선호한다.
  • 중첩된 리스트나 해시맵을 받아서 다른 포맷으로 직렬화할때, 캡슐화하면 나중에 포맷을 바꾸거나 추적하기 어려운 데이터를 수정하기 수월해진다.

절차

  1. 레코드를 담은 변수를 캡슐화한다.
    • 레코드를 캡슐화하는 함수의 이름은 검색하기 쉽게 지어준다.
  2. 레코드를 감싼 단순한 클래스로 해당 변수의 내용을 교체한다. 이 클래스에 원본 레코드를 반환하는 접근자도 정의하고, 변수를 캡슐화하는 함수들이 이 접근자를 사용하도록 수정한다.
  3. 테스트한다.
  4. 원본 레코드 대신 새로 정의한 클래스 타입의 객체를 반환하는 함수들을 새로 만든다.
  5. 레코드를 반환하는 예전 함수를 사용하는 코드를 전 단계에서 만든 새 함수를 사용하도록 바꾼다. 필드에 접근할 때는 객체의 접근자를 사용한다. 적절한 접근자가 없다면 추가한다. 한 부분을 바꿀 때마다 테스트한다.
    • 클라이언트가 데이터를 읽기만 한다면 데이터의 복제본이나 읽기전용 프록시를 반환할지 고려해본다.
  6. 클래스에서 원본 데이터를 반환하는 접근자와 1단계에서 레코드를 캡슐화하는 함수들을 제거한다.
  7. 테스트한다.
  8. 레코드의 필드도 데이터 구조인 중첩 구조라면 레코드 캡슐화하기와 컬렉션 캡슐화하기를 재귀적으로 적용한다.

예시

const organization = {name: "애크미", country: "GB"}

result += `${organization.name}`// 읽기
organization.name = newName // 쓰기
  1. 상수를 캡슐화한다(변수 캡슐화하기)
function getRawDataOfOrganization() {
    return organization;
}

result += `${getRawDataOfOrganiztion().name}`// 읽기
getRawDataOfOrganiztion().name = newName // 쓰기
  1. 레코드를 클래스로 바꾸고, 새 클래스의 인스턴스를 반환하는 함수를 새로 만든다.
class Organization {
    constructor(data) {
        this._data = data;
    }
}

const organization = new Organization({name: "애크미", country: "GB"});
function getRawDataOfOrganization() {return organization._data;}
function getOrganization() {return organization;}
  1. 레코드를 갱신하던 코드는 모두 세터를 사용하도록 고친다.
// Organization 클래스
set name(aString) {this._data.name = aString;}

// 클라이언트
getOrganization().name = newName;
  1. 임시 함수를 제거한다.
  2. 완성
class Organization {
    constructor(data) {
        this._name = data.name
        this._country = data.country
    }
    get name() {return this._name;}
    set name(aString) {this._name = aString;}
    get country() {return this._country;}
    set country(aCountryCode) {this._country = aCountryCode}
}

7.2 컬렉션 캡슐화하기

배경

  • 컬렉션 게터가 원본 컬렉션을 반환하지 않게 만들어서 클라이언트가 실수로 컬렉션을 바꿀 가능성을 차단하는게 낫다.

두 가지 방식이 있다.

  1. 컬렉션에 접근하려면 컬렉션이 소속된 클래스의 적절한 메서드를 반드시 거치게 한다.
  2. 컬렉션을 읽기 전용으로 제공할 수 있다.
  • 위의 방식 중 한 가지만 택해서 컬렉션 접근 함수의 동작 방식을 통일해야 한다.

절차

  1. 아직 컬렉션을 캡슐화하지 않았다면 변수 캡슐화하기부터 한다.
  2. 컬렉션에 원소를 추가/ 제거하는 함수를 추가한다.
    • 컬렉션 자체를 통짜로 바꾸는 세터는 제거한다.
  3. 컴파일
  4. 컬렉션을 참조하는 부분을 모두 찾는다. 컬렉션의 변경자를 호출하는 코드가 모두 앞에서 추가한 추가/제거 함수를 호출하도록 수정한다.
  5. 컬렉션 게터를 수정해서 원본 내용을 수정할 수 없는 읽기전용 프록시나 복제본을 반환하게 한다.
  6. 테스트

예시

class Person {
    get courses() {return this._courses;}
    set courses(aList) {this._courses = aList;}
}
------------------------------------------------------------
class Person {
    get courses() {return this._courses.slice();}
    addCourse(aCourse) {...}
    removeCourse(aCourse) {...}
}

7.3 기본형을 객체로 바꾸기

배경

  • 단순한 출력 이상의 기능이 필요해지면, 데이터를 표현하는 전용 클래스를 정의하는게 낫다.

절차

  1. 변수를 캡슐화하지 않았다면 캡슐화한다.(게터, 세터를 만든다)
  2. 단순한 값 클래스를 만든다. 생성자는 기존 값을 인수로 받아서 저장하고, 이 값을 반환하는 게터를 추가한다.
  3. 값 클래스의 인스턴스를 새로 만들어서 필드에 저장하도록 1에서 만든 세터를 수정한다. 이미 있다면 필드의 타입을 적절히 변경한다.
  4. 새로 만든 클래스의 게터를 호출한 결과를 반환하도록 1에서 만든 게터를 수정한다.
  5. 함수 이름을 바꾸면 원본 접근자의 동작을 더 잘 드러낼 수 있는지 검토한다.

예시

class Order {
    constructor(data) {
        this.priority = data.priority;
    ...
    }
}

// 클라이언트
highPriorityCount = orders.filter(o => "high" === o.priority
    || "rush" === o.priority)
    .length
  1. 변수를 캡슐화한다.
    • 이렇게 필드를 자가 캡슐화하면 이름을 바꿔도 클라이언트 코드는 유지할 수 있다.
class Order {
    constructor(data) {
        this.priority = data.priority;
    ...
    }

    get priority() {
        return this._priority;
    }

    set priority(aString) {
        this._priority = aString
    }
}
  1. 우선순위 속성을 표현하는 값 클래스 Priority를 만든다.
class Priority {
    constructor(value) {
        this._value = value;
    }
    toString() {
        return this._value;
    }
}
  1. 전 단계에서 만들은 Priority 클래스를 사용하도록 접근자들을 수정한다.
class Order {
    constructor(data) {
        this.priority = data.priority;
		    ...
    }

    get priority() {
        return this._priority.toString();
    }

    set priority(aString) {
        this._priority = new Priority(aString)
    }
}
  1. get priority()를 의미에 맞게 함수 이름을 바꾼다.
class Order {
    constructor(data) {
        this.priority = data.priority;
		    ...
    }

    get priorityString() {
        return this._priority.toString();
    }

    set priority(aString) {
        this._priority = new Priority(aString)
    }
}

// 클라이언트
highPriorityCount = orders.filter(o => "high" === o.priorityString
    || "rush" === o.priorityString)
    .length

7.4 임시 변수를 질의 함수로 바꾸기

배경

  • 임시 변수는 아예 함수로 만들어 사용하는 편이 나을 때가 많다.
  • 변수 대신 함수로 만들어두면 비슷한 계산을 수행하는 다른 함수에서도 사용할 수 있어 코드 중복이 줄어든다.
  • 이 리팩토링 기법은 클래스 안에서 적용할 때 효과가 좋다
    • 클래스는 추출할 메서들에 공유 컨텍스트를 제공하기 때문이다.
  • 스냅숏 용도로 쓰이는 변수에는 이 리팩터링을 적용하면 안된다.

절차

  1. 변수가 사용되기 전에 값이 활실히 결정되는지, 변수를 사용할 때마다 계산 로직이 매번 다른 결과를 내지는 않는지 확인한다.
  2. 읽기전용으로 만들 수 있는 변수는 읽기전용으로 만든다.
  3. 변수 대입문을 함수로 추출한다.
  4. 변수 인라인하기로 임시 변수를 제거한다.

예시

const basePrice = this._quantity * this._itemPrice;

if (basePrice > 1000)
    return basePrice * 0.95;
else
    return basePrice * 0.98;

----------------------------------------------------
get basePrice() {this._quantity * this._itemPrice;}

if (this.basePrice > 1000)
    return this.basePrice * 0.95;
else
    return this.basePrice * 0.98;

 

7.5 클래스 추출하기

배경

  • 메서드와 데이터가 너무 많은 클래스는 이해하기가 쉽지 않으니 잘 살펴보고 적절히 분리하는 것이 좋다.
  • 일부 데이터와 메서드를 따로 묶을 수 있다면 어서 분리하라는 신호다
  • 함께 변경되는 일이 많거나 서로 의존하는 데이터들도 분리한다.
  • 제거해도 다른 필드나 메서드 들이 논리적으로 문제가 없다면 분리할 수 있다는 뜻이다.

절차

  1. 클래스의 역할을 분리할 방법을 정한다.
  2. 분리될 역할을 담당할 클래스를 새로 만든다.
    • 원래 클래스는 남은 역할에 따라 이름을 적절히 바꿔준다.
  3. 원래 클래스의 생성자에서 새로운 클래스의 인스턴스를 생성하여 필드에 저장해둔다.
  4. 분리될 역할에 필요한 필드들을 새 클래스로 옮긴다. 하나씩 옮길 때마다 테스트한다.
  5. 메서드들도 새 클래스로 옮긴다. 이때 저수준 메서드. 즉 호출을 당하는 일이 많은 메서드부터 옮긴다.
  6. 양쪽 클래스의 인터페이스를 살펴보면서 불필요한 메서드를 제거하고, 이름도 새로운 환경에 맞게 바꾼다.
  7. 새 클래스를 외부로 노출할지 정한다. 노출하려거든 새 클래스에 참조를 값으로 바꾸기를 적용할지 고민한다.

예시

class Person {
    get officeAreaCode() {
        return this._officeAreaCode;
    }

    get officeNumber() {
        return this._officeNumber;
    }
}
------------------------------------

class Person {
    get officeAreaCode() {
        return this._telephoneNumber.areaCode;
    }

    get officeNumber() {
        return this._telephoneNumber.number;
    }
}

class TelephoneNumber {
    get areaCode() {
        return this._areaCode;
    }

    get number() {
        return this._number;
    }
}

7.6 클래스 인라인하기

배경

  • 클래스 추출하기를 거꾸로 돌리는 리팩터링이다.
  • 제 역할을 못 해서 그대로 두면 안되는 클래스를 인라인한다.
  • 두 클래스의 기능은 지금과 다르게 배분하고 싶을 때도 클래스를 인라인한다.
    • 클래스를 인라인해서 합친 다음 추출하는게 더 쉬울때도 있기 때문

절차

  1. 소스 클래스의 각 public 메서드에 대응하는 메서드들을 타깃 클래스에 생성한다.
  2. 소스 클래스의 메서드를 사용하는 코드를 모두 타깃 클래스의 위임 메서드를 사용하도록 바꾼다.
  3. 소스 클래스의 메서드와 필드를 모두 타깃 클래스로 옮긴다.

예시

class Person {
    get officeAreaCode() {
        return this._telephoneNumber.areaCode;
    }

    get officeNumber() {
        return this._telephoneNumber.number;
    }
}
class TelephoneNumber {
    get areaCode() {
        return this._areaCode;
    }

    get number() {
        return this._number;
    }
}

------------------------------------
    
class Person {
    get officeAreaCode() {
        return this._officeAreaCode;
    }

    get officeNumber() {
        return this._officeNumber;
    }
}

7.7 위임 숨기기

배경

  • 모듈화 설계를 제대로 하는 핵심은 캡슐화다.
  • 캡슐화는 모듈들이 시스템의 다른 부분에 대해 알아야 할 내용을 줄여준다.
  • 서버 자체에 위임 메서드를 만들어서 위임 객체(서버 객체의 필드가 가리키는 객체)의 존재를 숨기면 클라이언트 - 위임 객체의 인터페이스 간 의존성이 사라진다.

절차

  1. 위임 객체의 각 메서드에 해당하는 위임 메서드를 서버에 생성된다.
  2. 클라이언트가 위임 객체 대신 서버를 호출하도록 수정한다.
  3. 모두 수정했다면, 서버로부터 위임 객체를 얻는 접근자를 제거한다.

예시

manager = aPerson.department.manager;

----------------------------------------
manager = aPerson.manager;

class Person {
    get manager() {
        return this.department.manager
    }
}

7.8 중개자 제거하기

배경

  • 클라이언트가 위임 객체의 또 다른 기능을 사용하고 싶을 때마다 서버에 위임 메서드를 추가해야 하는데, 이러다보면 단순히 전달만 하는 위임 메서드들이 많아 진다.
  • 이럴때 서버 클래스는 중개자 역할로 전락하여, 클라이언트가 위임 객체를 직접 호출하는 것이 낫다.

절차

  1. 위임 객체를 얻는 게터를 만든다.
  2. 위임 메서드를 호출하는 클라이언트가 모두 이 게터를 거치도록 수정한다.
  3. 모두 소정했다면 위임 메서드를 삭제한다.

예시

manager = aPerson.manager;

class Person {
    get manager() {
        return this.department.manager
    }
}

----------------------------------------

manager = aPerson.department.manager;
  • 위임 숨기기나 중개자 제거하기를 적당히 섞어도 된다. 자주 쓰는 위임은 그대로 두는 편이 클라이언트 입장에서 편하다.

7.9 알고리즘 교체하기

배경

  • 때로는 알고리즘 전체를 걷어내고 훨씬 간결한 알고리즘으로 바꿔야 할 때가 있다.
  • 문제를 더 확실히 이해하고 훨씬 쉽게 해결하는 방법을 발견했을 때 이렇게 한다.
  • 거대하고 복잡한 알고리즘을 교체하기란 상당히 어려우니 알고리즘을 간소화하는 작업부터 해야 교체가 쉬워진다.

절차

  1. 교체할 코드를 함수 하나에 모은다.
  2. 이 함수만을 이용해 동작을 검증하는 테스트를 마련한다.
  3. 대체할 알고리즘을 준비한다.
  4. 정적 검사를 수행한다.
  5. 기존 알고리즘과 새 알고리즘의 결과를 비교하는 테스트를 수행한다.

'핌팩토링' 카테고리의 다른 글

9. 데이터 조직화  (0) 2022.04.28
8. 기능 이동  (0) 2022.04.27
6. 기본적인 리팩터링  (0) 2022.03.26
4. 테스트 구축하기  (0) 2022.03.12
3. 코드에서 나는 악취  (0) 2022.02.28