리팩터링캡슐화

[ Programming > Refactoring ]

[리팩터링] 캡슐화

 Carrot Yoon
 2025-08-18
 8

[리팩터링] 캡슐화

리팩터링 저자에 따르면 모듈을 분리하는 가장 중요한 기준은 시스템에서 각 모듈이 자신을 제외한 다른 부분에 드러내지 않아야 할 비밀을 얼마나 잘 숨기느냐에 있다.

클래스는 본래 정보를 숨기는 용도로 설계되었고, 클래스는 내부 정보뿐 아니라 클래스 간의 연결 관계를 숨기는 데도 유용하다. 그리고 캡슐화의 가장 큰 단위는 클래스와 모듈이지만, 함수도 구현을 캡슐화한다.

오늘은 캡슐화 방법을 통해 어떻게 리팩터링해서 깔끔한 코드를 만들 수 있을지 함께 공부해서 좋은 코드를 만드는 것이 목표다.

1. 레코드 캡슐화

레코드는 연관된 여러 데이터를 직관적인 방식으로 묶을 수 있어서 각각 따로 취급할 때보다 훨씬 의미있는 단위로 전달할 수 있게 해준다.그런데 단순한 레코드에는 단점이 있다. 계산해서 얻을 수 있는 값과 그렇지 않은 값을 명확히 구분해 저장해야 해서 번거롭다. 예를 들면 최종 세금같이 여러 상수들의 곱을 통해 최종 산출되는 값이 필요한 경우 번거로워 진다.

레코드 구조는 두가지로 구분할 수 있다. 필드명을 노출하는 형태와 필드를 외부로부터 숨겨서 원하는 이름을 쓰는 형태다. 예를들면 전자는 클래스같이 필드명이 노출되는 형태이고, 후자는 Map, HashMap, dictionary처럼 Map.get("키")와 같이 필드명이 노출되지 않는 형태다. 그런데 필드명을 노출하지 않는 형태는 불분명함이 크기때문에 필드명을 노출하는 형태가 선호된다.

1.1 레코드 캡슐화 절차

  • 절차

    1. 레코드를 담은 변수를 캡슐화한다. (기본 리팩터링 6.1 변수 캡슐화 참고)

    2. 레코드를 감싼 단순한 클래스로 해당 변수의 내용을 교체한다. 이 클래스에 원본 레코드를 반환하는 접근자도 정의하고, 변수를 캡슐화하는 함수들이 이 접근자를 사용하도록 수정한다.

    3. 테스트한다.

    4. 원본 레코드 대신 새로 정의한 클래스 타입의 객체를 반환하는 함수들을 새로 만든다.

    5. 레코드를 반환하는 예전 함수를 사용하는 코드를 (4) 에서 만든 새 함수를 사용하도록 바꾼다. 필드에 접근할 때는 객체의 접근자를 사용한다. 적절한 접근자가 없다면 추가한다. 한 부분을 바꿀 때마다 테스트한다.

    6. 클래스에서 원본 데이터를 반환하는 접근자와 원본 레코드를 반환하는 함수들을 제거한다.

    7. 테스트한다.

    8. 레코드의 필드도 데이터 구조인 중첩 구조라면 레코드 캡슐화하기와 컬렉션 캡슐화하기를 재귀적으로 적용한다.

1.2 레코드 캡슐화 예시

// 단순한 레코드 변수const organization = {name: "어피치", country: "KR"};// 변수 캡슐화, organization.js 파일let default = {name:"초기값", country: "KR"};export function getOrganization() {return Object.assign({}, default);}...// 레코드 캡슐화class Organization {    constructor(data) {        this._name = data.name;        this._country = data.country;    }    get name() {return this._name;}    set name(name) {this._name = name;}    get country() {return this._country;}    set country(country) {this._country = country;}    get familyName() {return this._name.slice(0,1)};}

2. 컬렉션 캡슐화

리팩터링 저자는 가변 데이터를 모두 캡슐화하는 편이라고 한다. 그러면 데이터가 언제 어떻게 수정되는지 파악하기 쉬워서 필요한 시점에 데이터 구조를 변경하기도 쉬워지기 때문이다. 자바스크립트에서 컬렉션을 캡슐화할 때 주의할 점은 게터가 컬렉션 자체를 반환하도록 하는 경우다. 컬렉션을 수정할 때 원본 컬렉션이 수정되어 예상치 못한 상황이 발생할 수 있기 때문이다.(불변성)

2.1 컬렉션 캡슐화 절차

  • 절차

    1. 아직 컬렉션을 캡슐화하지 않았다면 변수 캡슐화하기부터 한다.

    2. 컬렉션에 원소를 추가/제거하는 함수를 추가한다.

    3. 정적 검사를 수행한다.

    4. 컬렉션을 참조하는 부분을 모두 찾는다. 컬렉션의 변경자를 호출하는 코드가 모두 앞에서 추가한 추가/제거 함수를 호출하도록 수정한다. 하나씩 수정할 때마다 테스트한다.

    5. 컬렉션 게터를 수정해서 원본 내용을 수정할 수 없는 읽기전용 프락시나 복제본을 반환하게 한다.

    6. 테스트한다.

2.2 컬렉션 캡슐화 예시

//  컬렉션 캡슐화 적용 전class Person {    constructor(name) {        this._name = name;        this._courses = [];    }    get name() {return this._name;}    get courses() {return this._courses;}    set courses(courses) {this._courses=courses;}}class Course {    constructor(name, isAdvanced) {        this._name = name;        this._isAdvanced = isAdvanced;    }    get name() {return this._name;}    get isAdvanced() {return this._isAdvanced;}}// 클라이언트 사용 코드 예시const basicCourses = ["국어", "영어"];const student1 = new Person("어피치");basicCourses.forEach((course)=>{student1.courses.push(new Course(course,true))});
// 컬렉션 캡슐화 적용 후class Person {    constructor(name) {        this._name = name;        this._courses = [];    }    get name() {return this._name;}    addCourse(course) {        this._courses.push(course);    }    removeCourse(course) {        const idx = this._courses.indexOf(course);        if(idx === -1) throw new RangeError();        this._courses.splice(index,1);    }    get courses() {return this._courses.slice();} // 불변성 고려    set courses(courses) {this._courses=courses.slice();}}class Course {    constructor(name, isAdvanced) {        this._name = name;        this._isAdvanced = isAdvanced;    }    get name() {return this._name;}    get isAdvanced() {return this._isAdvanced;}}// 클라이언트 사용 코드 예시const basicCourses = ["국어", "영어"];const student1 = new Person("어피치");basicCourses.forEach((course)=>{student1.addCourse(new Course(course,true))});

3. 기본형을 객체로 바꾸기

개발 초기에는 단순한 정보를 숫자나 문자열 같은 간단한 데이터 항목(enum)으로 표현할 때가 많다. 그런데 개발을 하다보면, 이 정보들이 더이상 간단하지 않게 변한다. 예를 들면 처음에는 전화번호가 문자열로 표현되지만, 나중에는 포맷팅이나 지역 코드 추출같은 동작이 더해진다. 그리고 이런 코드들은 중복이 늘어고 유지 보수성도 안좋아지게 된다.

리팩터링 저자는 단순한 출력 이상의 기능이 필요해지는 순간 그 데이터를 표현하는 전용 클래스를 정의한다고 한다. 처음에는 기본형 데이터를 감싸기만한 수준이지만, 특별한 동작이 추가될수록 점점 유용한 도구가 된다. 대단해 보이지 않겠지만, 코드베이스에 미치는 효과가 놀라울 만큼 크다고 한다. 초보 프로그래머에게는 직관에 어긋나 보이지만, 경험 많은 개발자들은 가장 유용한 리팩터링으로 꼽는다.

3.1 기본형을 객체로 바꾸기 절차

  • 절차

    1. 아직 변수를 캡슐화하지 않았다면 캡슐화한다.

    2. 단순한 값 클래스를 만든다. 생성자는 기존 값을 인수로 받아서 저장하고, 이 값을 반환하는 게터를 추가한다.

    3. 정적 검사를 수행한다.

    4. 값 크래스의 인스턴스를 새로 만들어서 필드에 저장하도록 세터에 수정한다. 이미 있다면 필드의 타입을 적절히 변경한다.

    5. 새로 만든 클래스의 게터를 호출한 결과를 반환하도록 게터를 수정한다.

    6. 테스트한다.

    7. 함수 이름을 바꾸면 원본 접근자의 동작을 더 잘 드러낼 수 있는지 검토한다.

3.2 기본형을 객체로 바꾸기 예시

// 우선순위에 따라 분류하고 실행이 달라지는 코드class Order {    constructor(data) {        this.priority = data.priority;    }}const orders = [Order1, Order2 ...]const highPriorityCount = orders.filter(order=>order.priority === "high" || order.priority === "rush").length;// 이런 코드가 많아질 것으로 예상(medium등도 추출할거니까)하여 기본형을 객체로 바꾼 예시class Priority {    static legalValues() {return ['low', 'normal', 'high', 'rush'];}    constructor(value) {        this._value = value;    }    get index() {return Priority.legalValues().findIndex(s=>s===this.value);}    toString() {return this._value;}    higherThan(otherPriority) {return this.index > otherPriority.index;}}const orders = [Order1, Order2 ...]const highPriorityCount = orders.filter(order=>order.priority.higherThan(new Priority("normal"))).length;

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

임시 변수를 질의 함수로 바꾸기는 함수 안에서 어떤 코드의 결과값을 다시 참조할 목적으로 임시 변수를 쓰는경우 사용한다. 보통 임시 변수로 만들어 사용하면 값을 계산하는 코드가 반복되는 걸 줄이고 값의 의미도 설명할 수 있어 유용하다. 그런데 한번 더 나아가 아예 함수로 만들어 사용하는 편이 나을 때가 많다.

변수 대신 함수로 만들면 비슷한 수행을하는 다른 함수에도 사용할 수 있어 중복코드도 줄어든다. 그래서 똑같은 방식으로 계산되는 변수를 발견할 때마다 함수로 바꿀 수 있는지 살펴본다.

이 리팩터링은 클래스 안에서 적용할 때 효과가 가장 크다. 이 리팩터링 방법은 예시로만 소개하겠다.

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

// 임시 변수 basePrice, discountFactorclass Order {    constructor(quantity, item) {        this._quantity = quantity;        this._item = item;    }    get price() {        const basePrice = this._quantity * this._item.price;        let discountFactor = 0.98;        if(basePrice > 1000) discountFactor -= 0.03;        return basePrice * discountFactor;    }}// 질의 함수로 바꾼 후class Order {    constructor(quantity, item) {        this._quantity = quantity;        this._item = item;    }    // 질의 함수 1    get basePrice() {        return this._quantity * this._item.price;    }    // 질의 함수 2    get discountFactor() {        let discountFactor = 0.98;        if(this.basePrice > 1000) discountFactor -= 0.03;        return discountFactor;    }    get price() {        return this.basePrice * this.discountFactor;    }}

5. 클래스 추출하기

실무에서 클래스에 몇 가지 연산을 추가하고 데이터도 보강하면서 금방 비대해지곤 한다. 그리고 기존 클래스를 굳이 쪼갤 필요까지는 없다고 생각하여 새로운 역할을 덧씌우기 쉬운데, 점점 클래스가 복잡해지는 것을 느낄 것이다. 그리고 어느새 고치기 힘들정도의 상태가 된다.

메서드와 데이터가 너무 많은 클래스는 이해하기가 쉽지 않으니 잘 살펴보고 적절히 분리하는 것이 좋다. 특히 일부 데이터와 메서드를 따로 묶을 수 있다면 어서 분리하라는 신호다. 함께 변경되는 일이 많거나 서로 의존하는 데이터들도 분리한다. 특정 데이터나 메서드 일부를 제거해보고 어떤 일이 일어나는지 자문해보면 판단에 도움이 된다. 개발 후반으로 접어들면 서브클래스가 만들어지는 방식에서 왕왕 징후가 나타나기도 한다. 예컨대 작은 일부의 기능만을 위해 서브클래스를 만들거나, 확장해야 할 기능이 무엇이냐에 따라 서브클래스를 만드는 방식도 달라진다면 클래스를 나눠야 하는 신호다.

이 클래스 추출하기도 절차는 생략하고 예시만 보여주려고한다. 절차는 책을 참고하세요!!

5.1 클래스 추출하기 예시

// 클래스 추출하기 전class Person {    get name() {return this._name;}    get officeAreaCode() {return this._officeAreaCode;}    get officeNumber() {return this.officeNumber;}    get telephoneNumber() {return `${this.officeAreaCode}-${this.officeNumber}`;}}// 클래스 추출 후class Person {    get name() {return this._name;}    get telephoneNumber() {return this._telephoneNumber.toString();}    get officeAreaCode() {return this._telephoneNumber.areaCode;}    get officeNumber() {return this._telephoneNumber.number;}}class TelephoneNumber {    get number() {return this.number;}    get areaCode() {return this.areaCode;}    toString() {return `${this.areaCode}-${this.number}`;} // 읽기 좋게 하는 것은 전화번호 객체로.}

6. 클래스 인라인하기

클래스 인라인하기는 클래스 추출하기의 반대되는 리팩터링이다. 더이상 클래스가 제 역할을 못 해서 그대로 두면 안 되는 클래스를 인라인하는 것이다. 그래스 크게 2가지 경우 쓰인다. 리팩터링을 하고나니 특정 클래스에 남은 역할이 거의 없을 경우나 두 클래스의 기능을 지금과 다르게 배분하고 싶을 때도 클래스를 인라인 하기다. 이때 2번째 반법은 인라인으로 하나로 합쳐서 새로운 클래스를 추출하는 게 더 쉬울 수 있다.

6.1 클래스 인라인 예시

// 클래스 인라인 전class TrackingInformation {    get shippingCompany() {return this._shippingCompany;}    get trackingNumber() {return this._trackingNumber;}    get display() { return `${this.shippingCompany}: ${this.trackingNumber}`;}}class Shipment {    get trackingInfo() {return this._trackingInformation.display;}    get trackingInformation() {return this._trackingInformation;}}// 클래스 인라인 후class Shipment {    get shippingCompany() {return this._shippingCompany;}    get trackingNumber() {return this._trackingNumber;}    get display() { return `${this.shippingCompany}: ${this.trackingNumber}`;}}

7. 위임 숨기기

모듈화 설계를 제대로 하는 핵심은 캡슐화다. 캡슐화는 모듈들이 시스템의 다른 부분에 대해 알아야 할 내용을 줄여준다.무언가 변경할 때 함께 고려해야할 모듈의 수가 적어져야 코드를 변경하기가 훨씬 쉬워진다.

객체 지향을 처음 배우면 캡슐화란 필드를 숨기는 것이라 배우고, 경험이 쌓이면서 캡슐화의 역할이 그보다 많다는 사실을 깨닫는다. (필드도 숨기고, 추상화도 하고, 의존성도 생기고 등등)

위임 숨기기는 서버 객체의 필드가 가리키는 객체(위임 객체)의 메서드를 호출하는데, 위임 객체의 존재를 몰라도 되도록 만드는 것이다.

7.1 위임 숨기기 예시

// 위임 숨기기 전const manager = aPerson.department.manager;class Person {    get department() {return this._department;}}// 위임 숨기기 후const manager = aPerson.manager;class Person {    get department() {return this._department;}    get manager() {return this.department.manager;}}// 클라이언트가 사용할때 department안에 manager가 있다는 것을 몰라도 된다.

8. 중개자 제거하기

위임 숨기기를 통한 객체 캡슐화 이점을 소개했지만, 그 이점이 공짜는 아니다. 클라이언트가 위임 객체의 다른 기능도 사용하려고 한다면 그 기능들도 위임 메서드로 추가해줘야한다. 이렇게 계속 추가하다보면, 단순히 전달만하는 위임 메서드들이 점점 짜증나게된다. 그래서 해당 객체는 그냥 위임 객체의 기능을 사용하기 위한 중개자 역할밖에 못하기 때문에 그냥 객체를 직접 호출하는 것이 나을 수 있다.

데메테르 법칙(디미터의 법칙) 또는 최소 지식 원칙이라 불리는 원칙이 있다. 내부 정보를 가능한 숨기고 밀접한 모듈과만 상호작용하여 결합도를 낮추자는 원칙이다. 하지만 너무 이 원칙을 맹신하여 적용하면 너무 많은 wrapper 메서드가 늘어나는 부작용이 생길 수 있으니 적절하게 해야한다.

8.1 중개자 제거 예시

// 중개자 제거 전 (위임 숨기기 후)const manager = aPerson.manager;class Person {    get department() {return this._department;}    get manager() {return this.department.manager;}}// 중개자 제거 후 (위임 숨기기 전)const manager = aPerson.department.manager;class Person {    get department() {return this._department;}}

9. 알고리즘 교체하기

알고리즘 교체하기는 간단하다. 알고리즘을 더 나은 알고리즘으로 교체하는 것이다. 문제를 더 확실히 이해하고 훨씬 쉽게 해결하는 방법을 발견했을 때 이렇게 한다.

9.1 알고리즘 교체 예시

// 알고리즘 교체 전function foundItem(items) {    for(let i=0; i<items.length; i++) {        if(items[i]==="computer") {            return "computer";        }        if(items[i]==="tv") {            return "tv";        }        if(items[i]==="keyboard") {            return "keyboard";        }    }    return "";}// 알고리즘 교체 후function foundItem(items) {    const candidates = ["computer", "tv", "keyboard"];    return items.find(item => candiates.include(item)) || "";}

10. 마무리

리팩터링 책을 보는 이유는 리팩터링 기술들을 공부하기 위함도 있지만, 그 기술 적용 과정이나 절차도 도움이 된다. 왜냐하면 오류(사이드 이펙트) 없이 리팩터링하기 위해서 노력하는 절차들을 소개하기 때문이다. 리팩터링의 절차를 보다보면 계속 비슷 비슷하다는 느낌이 들거라고 생각한다. 모두 1단계씩 바꾸지 한번에 다 바꾸진 않는다. 왜냐하면 쓰이는 곳을 모두 확인도 해야하고 테스트도 해야하기 때문이다. 그래서 이 글에서 절차를 많이 생략했지만 리팩터링의 기본은 사이드 이펙트가 없어야 한다는 것을 꼭 명심했으면 좋겠다.