ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 3. 정보 은닉(Information Hiding)
    소프트웨어 설계 원칙/2. 기본 원칙 2026. 5. 22. 13:14

    정보 은닉이란?

    정보를 외부에서 접근할 수 없게 하는 것이다.

    David Parnas가 처음 정의한 원칙으로, 표준 정의는 다음과 같다.

    "변경 가능성이 높은 결정을 모듈 안에 숨겨라."

    이게 무슨 의미일까?
    정보를 외부에서 접근할 수 없게 한다는 정보 은닉은 얼핏보면 보안에 관련된 원칙 같다.
    하지만 보안은 따라오는 효과일 뿐이다.

    이 말은 두 가지를 의미한다.

    1. 변경 가능성이 높은 것을 한 모듈 안에 가져온다 (격리)
    2. 외부에서 그것을 다룰 수 없게 차단한다 (은닉)

    변경 가능성이 높은 것이란 해당 모듈에서 다뤄야 할 것을 말한다.
    일반적으로 객체나 데이터 등 변경 가능성이 높은 것들을 모듈 안으로 가져와서 그 모듈 안에서만 다루라는 것이다.
    그리고 외부에서의 접근을 차단하여 해당 모듈이 다른 곳에서 다뤄지고 제어되는 것을 막으라는 것이다.

    변경 가능한 것이 여러 곳에 흩어져 있으면 변경 시 다 수정해야 한다.
    한 모듈에 모으고 외부에서 차단하면, 변경 시 그 모듈만 수정하면 된다.

    차단은 수단이고, 변경 격리가 목적이다.

    핵심 목적: 시스템의 변경 가능성이 높은 설계 결정을 모듈 내부로 감춰, 변경이 발생해도 다른 시스템 부분이 광범위하게 수정되지 않도록 보호

    세부 특징: 모듈 내부의 세부 구현을 외부에 노출하지 않고, 잘 정의된 인터페이스만을 통해 다른 모듈과 소통하도록 제한

     




    캡슐화/추상화와의 관계

    사실 추상화/정보 은닉/캡슐화는 한 사고(모듈화)의 세 측면이다.

    한 단위에 이름 붙이기(추상화), 무엇을 묶을지 결정(캡슐화), 무엇을 숨길지 결정(정보 은닉) 이 셋이 동시에 일어난다.

    - 추상화: 본질만 노출하고 세부 숨김 (목적/결과)
    - 캡슐화: 데이터와 동작을 함께 묶기 (구조)
    - 정보 은닉: 무엇을 차단할지 결정 (수단)

    이 글에선 정보 은닉 중심으로, 무엇을 숨기고 어떻게 숨기는지 알아본다.



     


    왜 해야하는가?

     

    위에서 말했듯,
    정보 은닉은 자주 변경되는 것을 모듈 안으로 가져오고, 외부에서 접근할 수 없게 하는 것이다.
    정보 은닉이 잘 적용된 모듈은 변경에 강한 코드가 된다.
    "변경에 강하다"는 유연하거나 확장 가능하다는 뜻이 아니다. 변경할 때 수정 범위가 작다는 뜻이다.
    한마디로 견고한 모듈화를 뜻한다.

    방법은 두 단계다.

     

    1. 변경 가능한 것을 한 모듈에 격리한다
      (여러 곳에 흩어져 있으면 변경 시 다 수정해야 함)
    2. 외부에서 못 보게 차단한다
         (외부가 디테일을 알면 디테일에 의존하게 됨)


    격리 + 차단 = 변경의 영향 범위가 모듈로 제한됨 = 변경에 강함
    이 결과로 얻는 가치는 세 가지로 압축된다.


    1. 변경 격리
    코드가 바뀔 때 영향 범위가 좁다.
    내부 구현이 외부로 새어 나오면, 내부를 바꿀 때마다 외부도 같이 바뀌어야 한다.
    내부를 한 곳에 격리하고 외부에 안 보이게 하면, 내부를 마음껏 바꿔도 외부는 영향 없다.


    2. 결합도 낮춤
    외부가 내부를 모르면, 외부 코드와 내부 코드의 결합이 약해진다.
    결합도가 낮으면 한 모듈을 다른 모듈로 교체하기 쉽고, 테스트하기도 쉽다.


    3. 인지 부하 감소
    호출자가 알아야 할 정보가 적어진다.
    내부 디테일을 다 알아야 사용할 수 있는 코드는 사용자에게 부담을 준다.
    인터페이스만 보고 쓸 수 있어야 한다.


    쉽게 말해, 사용할 객체나 변수(데이터)를 모듈 안에 두고, 제공할 기능(메서드)이나 정보(읽기 전용 값)만 노출하며
    외부에서는 조작할 수 없게 하면 된다.



     


    예시 - 정보 은닉이 된 것 vs 안 된 것


    같은 장바구니를 두 가지로 짜보자.

    - 정보 은닉이 안 된 코드

    class Cart {
        public items: Item[] = [];
        public couponDiscount: number = 0;
    }
    
    const cart = new Cart();
    cart.items.push({ id: '1', name: '셔츠', price: 30000 });
    cart.couponDiscount = 0.2;
    
    const total = cart.items.reduce((sum, i) => sum + i.price, 0) * (1 - cart.couponDiscount);

     

    문제점

     

    • items가 public이라 외부에서 직접 조작 가능
    • couponDiscount를 외부에서 임의 변경 가능
    • 총액 계산 로직이 외부에 노출 (구현시 호출자가 로직을 알아야 함)
    • 쿠폰 정책이나 계산 방식 바뀌면 외부 코드도 다 바꿔야 함

     



    - 정보 은닉이 된 코드

    class Cart {
        private items: Item[] = [];
        private couponDiscount: number = 0;
        
        private static readonly COUPONS: Record<string, number> = {
            'SUMMER20': 0.2,
            'WELCOME10': 0.1,
        };
        
        addItem(item: Item): void {
            this.items.push(item);
        }
        
        applyCoupon(code: string): boolean {
            const discount = Cart.COUPONS[code];
            if (discount === undefined) return false;
            this.couponDiscount = discount;
            return true;
        }
        
        getTotal(): number {
            const subtotal = this.items.reduce((sum, i) => sum + i.price, 0);
            return subtotal * (1 - this.couponDiscount);
        }
    }
    
    const cart = new Cart();
    cart.addItem({ id: '1', name: '셔츠', price: 30000 });
    cart.applyCoupon('SUMMER20');
    const total = cart.getTotal();
    
    cart.items;                     // ❌ 접근 불가
    cart.couponDiscount = 0.99;     // ❌ 변경 불가

     

     

    개선점

    • items, couponDiscount는 외부에서 못 봄 (private)
    • 쿠폰 코드 처리 로직이 안에 숨음 (SUMMER20이 뭔지 외부가 알 필요 없음)
    • 총액 계산 로직이 안에 숨음
    • 쿠폰 정책 바뀌어도 외부 코드는 그대로
    • 호출자는 addItem, applyCoupon, getTotal만 알면 됨

     

    - 결과
    외부에서 보이는 인터페이스가 단순해지고, 내부 변경의 영향이 Cart 안에 머문다.




    정보 은닉의 일반 원칙 - "무엇을 은닉할 것인가?"

    정보 은닉의 본질은 한 문장으로 요약된다.
    "변경 가능한 것을 안에 숨기고, 제공할 인터페이스만 외부에 노출한다."
    이 한 문장을 실행하기 위해 두 가지 결정이 필요하다.


     

    1. 무엇을 숨길까

    • 변경 가능성이 높은 것 (구현 디테일, 정책, 설정)
    • 외부가 알 필요 없는 것 (내부 계산, 헬퍼 함수)
    • 다른 책임에 속하는 것 (이건 다른 모듈로 옮길 신호)


    2. 무엇을 노출할까

    • 모듈의 본질적 동작 (addItem, getTotal 같은 의도)
    • 안정적인 인터페이스 (자주 바뀌지 않을 시그니처)
    • 호출자가 알아야 하는 것만



    "무엇을 숨길까"와 "무엇을 노출할까"는 같은 결정의 두 측면이다.
    추상화의 "묶기/버리기/노출"과 같은 사고이고, 한 모듈을 만드는 순간 동시에 결정된다.


     


    정보 은닉의 종류(도구) - "어떻게 정보 은닉을 하는가?"


    1. 객체의 필드/메서드 은닉

    객체의 내부 데이터(필드)와 동작(메서드)을 외부로부터 차단.

    class Cart {
        private items: Item[];           // 필드 은닉
        private calculateSubtotal() {}   // 메서드 은닉
        
        addItem(item: Item) { ... }      // 노출
        getTotal() { ... }
    }


    외부는 노출된 메서드로만 객체와 상호작용한다.



    2. 객체의 구체적 타입 은닉 (상위 타입 캐스팅)
    구체적인 타입을 상위 타입이나 인터페이스로 노출. 어떤 구체 타입인지 외부에서 모르게 한다.

    interface PaymentProvider {
        charge(amount: number): Promise<void>;
    }
    
    class StripeProvider implements PaymentProvider { ... }
    class TossProvider implements PaymentProvider { ... }
    
    // 사용 — 구체 타입(Stripe/Toss) 은닉
    const provider: PaymentProvider = new StripeProvider();
    provider.charge(10000);


    호출자는 Stripe인지 Toss인지 모른다. PaymentProvider라는 인터페이스만 본다.



    3. 구현 은닉

    인터페이스나 추상 클래스로 계약만 노출, 실제 구현은 숨김.

    interface Repository {
        save(data: Data): void;
        find(id: string): Data;
    }
    
    class MySQLRepository implements Repository {
        save(data) { /* MySQL 쿼리 */ }
        find(id) { /* MySQL 쿼리 */ }
    }

     

     

    호출자는 Repository 계약만 알면 된다. MySQL인지 MongoDB인지, 어떻게 저장/조회되는지 모른다.


     

    **JS/TS 환경에서의 도구 **

     

    1. 접근 제어 (private/protected)
    OOP에서 가장 명시적인 도구. 필드/메서드 은닉을 키워드로 강제

    class User {
        private password: string;
        private salt: string;
        
        login(pwd: string): boolean { ... }
    }


    Java, TypeScript 클래스에서 자주 쓴다.



    2. 클로저
    JS/TS의 함수형 정보 은닉. 함수 안 변수를 외부에서 접근 못 하게 한다.

    function createCounter() {
        let count = 0;   // 외부에서 접근 불가
        
        return {
            increment() { count++; },
            getCount() { return count; },
        };
    }
    
    const counter = createCounter();
    counter.increment();
    counter.getCount();   // 1
    counter.count;        // undefined — 못 봄


    count가 함수 안에 갇혀 외부에서 볼 수 없다.
    React의 useState도 같은 원리다. 상태가 클로저 안에 숨겨진다.



    3. 모듈 시스템
    파일 단위 정보 은닉

    // utils/cache.ts
    const cache = new Map();   // 모듈 내부에 숨김
    
    export function get(key: string) { return cache.get(key); }
    export function set(key: string, value: any) { cache.set(key, value); }
    
    // 다른 파일에서
    import { get, set } from './utils/cache';
    // cache 자체는 import 못 함


    export 안 한 것은 외부에서 접근 불가하다.



    4. 인터페이스/타입 시스템
    TS의 인터페이스로 구체 타입과 구현을 은닉

    interface PaymentProvider {
        charge(amount: number): Promise<void>;
    }
    
    function processPayment(provider: PaymentProvider) {
        provider.charge(10000);
        // provider가 어떤 구체 타입인지, 어떻게 결제되는지 모름
    }


    기본 분류의 2번(구체 타입 은닉), 3번(구현 은닉)을 TS에서 실현하는 방법



    5. 어댑터
    외부 시스템의 변경 가능성을 한 곳에 격리

    // 외부 API 응답 — 변경 가능
    interface RawApiResponse {
        user_info: { user_id: string; user_nickname: string };
    }
    
    // 내부 도메인 — 안정적
    interface User {
        id: string;
        name: string;
    }
    
    // 어댑터 — 변경 가능성을 한 곳에 격리
    function parseUser(raw: RawApiResponse): User {
        return { id: raw.user_info.user_id, name: raw.user_info.user_nickname };
    }


    외부 API 형식이 바뀌면 parseUser만 수정한다. 



     

    결론

    정보 은닉은 단순히 차단만 하는 것이 아니라, 외부와의 차단으로 한층 더 견고한 모듈화를 하는 것이다
    외부가 내부를 모르게 만들어서, 내부 변경이 외부에 영향 주지 않게 한다.
    수단(private, 클로저, 모듈, 인터페이스, 어댑터)을 외워서 적용하려고 하지 않고 목적(변경 차단)에 집중하면 자연스럽게 수단들이 보일 것이라 생각한다.

    이러한 정보은닉은 추상화와 캡슐화(데이터와 동작 묶기)를 만나면 자연스럽게 모듈화로 이어진다.

Designed by Tistory.