-
4. 캡슐화(Encapsulation)소프트웨어 설계 원칙/2. 기본 원칙 2026. 5. 22. 16:06
캡슐화란?
관련된 데이터와 그 데이터를 다루는 동작을 하나의 단위로 묶는 것이다.
핵심 단어는 "묶기"다.
흩어져 있던 데이터와 동작을 한 단위(클래스, 모듈, 함수)로 모아서 함께 다루는 것이다.(보통 함께 적용되는 정보 은닉을 포함해 "내부를 감춘다"고 정의하기도 한다. 이 글에선 묶기 책임만으로 본다.)
예를 들어 장바구니 기능을 짠다면
- 캡슐화 적용 전let cartItems: Item[] = []; function addItem(items: Item[], item: Item) { items.push(item); } function getTotal(items: Item[]) { return items.reduce((sum, i) => sum + i.price, 0); } function applyCoupon(items: Item[], discount: number) { ... }
데이터(cartItems)와 동작(addItem, getTotal, applyCoupon)이 흩어져 있다.
- 캡슐화 적용 후class Cart { private items: Item[] = []; addItem(item: Item) { this.items.push(item); } getTotal() { return this.items.reduce((sum, i) => sum + i.price, 0); } applyCoupon(discount: number) { ... } }
Cart라는 한 단위 안에 데이터와 동작이 같이 들어간다.추상화/정보 은닉과의 관계
사실 추상화/정보 은닉/캡슐화는 한 사고(모듈화)의 세 측면이다.
한 단위에 이름 붙이기(추상화), 무엇을 묶을지 결정(캡슐화), 무엇을 숨길지 결정(정보 은닉) 이 셋이 동시에 일어난다.
- 추상화: 본질만 노출하고 세부 숨김 (목적/결과)
- 캡슐화: 데이터와 동작을 함께 묶기 (구조)
- 정보 은닉: 무엇을 차단할지 결정 (수단)
이 글에선 캡슐화 중심으로 무엇을 어떻게 묶을지 알아본다.
왜 해야하는가?위의 예시로 데이터와 매서드를 묶으라는 말인건 알겠다.
그런데 왜 묶어야 하는걸까?
캡슐화가 잘 적용된 코드는 자기 책임을 자기가 지는 코드가 된다.
데이터와 그 데이터를 다루는 동작이 한 곳에 있으면, 외부에서 데이터를 꺼내 처리할 필요가 없다.
객체에게 "이 일을 해줘"라고 시키면 되고, 객체가 자기 데이터로 자기 일을 처리한다.
이 결과로 얻는 가치는 세 가지로 압축된다.
1. 자기 데이터는 자기가 처리 (묻지 말고 시켜라)
데이터와 동작이 같이 있으면 외부에서 데이터를 꺼내 처리할 필요가 없다.// ❌ 캡슐화 약함 — 데이터 꺼내 외부에서 처리 const items = cart.items; const total = items.reduce((sum, i) => sum + i.price, 0); // ✅ 캡슐화 — 객체가 자기 데이터로 자기 동작 const total = cart.getTotal();
외부는 "총액 알려줘"라고 시키고, Cart가 자기 데이터로 계산해서 알려준다.
2. 일관된 상태 유지
객체가 자기 상태의 일관성을 책임진다.
데이터가 외부에 노출되면 외부 조작으로 상태가 깨질 위험이 있다.
데이터와 동작을 묶고 외부 접근을 막으면(정보은닉), 동작이 데이터를 일관되게 관리한다.// ❌ 일관성 위험 class Order { public items: Item[] = []; public total: number = 0; } // 외부에서 items만 바꾸면 total과 불일치 // ✅ 일관성 보장 class Order { private items: Item[] = []; private total: number = 0; addItem(item: Item) { this.items.push(item); this.total += item.price; // items 바뀌면 total도 같이 } }
3. 변경 격리
내부 구조를 바꿔도 외부 코드는 영향 없다.class Cart { private items: Item[] = []; // 배열로 시작 addItem(item: Item) { ... } getTotal() { ... } } // 나중에 Map으로 바꿔도 외부는 그대로 class Cart { private items: Map<string, Item> = new Map(); // 내부 구조 변경 addItem(item: Item) { ... } // 같은 인터페이스 getTotal() { ... } }
외부 호출자(cart.addItem)는 영향 받지 않는다.쉽게 말해, 관련된 데이터와 동작을 한 단위로 묶고, 객체가 자기 일을 자기가 처리하게 하면 된다.
예시 - 캡슐화가 된 것 vs 안 된 것같은 장바구니를 두 가지로 짜보자.
- 캡슐화가 안 된 코드// 데이터 let cartItems: { id: string; name: string; price: number; quantity: number }[] = []; let couponDiscount: number = 0; // 동작들이 외부 함수로 흩어짐 function addItem(item: Item) { cartItems.push(item); } function removeItem(id: string) { cartItems = cartItems.filter(i => i.id !== id); } function applyCoupon(discount: number) { couponDiscount = discount; } function getTotal() { const subtotal = cartItems.reduce((sum, i) => sum + i.price * i.quantity, 0); return subtotal * (1 - couponDiscount); } // 사용 — 데이터와 동작이 분리 addItem({ id: '1', name: '셔츠', price: 30000, quantity: 1 }); applyCoupon(0.2); const total = getTotal();
문제점
- 데이터(cartItems, couponDiscount)와 동작이 분리됨
- 어디서나 cartItems 직접 조작 가능
- 상태가 외부에 노출돼서 일관성 깨지기 쉬움
- "Cart"라는 단위가 코드 어디에도 없음 (개념만 있고 형태 없음)
캡슐화가 된 코드class Cart { private items: CartItem[] = []; private couponDiscount: number = 0; addItem(item: CartItem) { this.items.push(item); } removeItem(id: string) { this.items = this.items.filter(i => i.id !== id); } applyCoupon(discount: number) { this.couponDiscount = discount; } getTotal(): number { const subtotal = this.items.reduce((sum, i) => sum + i.price * i.quantity, 0); return subtotal * (1 - this.couponDiscount); } } // 사용 — 객체에게 시킴 const cart = new Cart(); cart.addItem({ id: '1', name: '셔츠', price: 30000, quantity: 1 }); cart.applyCoupon(0.2); const total = cart.getTotal();
개선점
- 데이터와 동작이 Cart 안에 같이 묶임
- 외부는 메서드로만 접근 (cart.addItem, cart.getTotal)
- "Cart"라는 단위가 코드에 명확히 존재
- 상태 일관성을 Cart가 책임짐
캡슐화의 일반 원칙 - "무엇을 캡슐화 할 것인가"캡슐화의 본질은 한 문장으로 요약된다.
"관련된 데이터와 동작을 한 단위로 묶는다."
이 한 문장을 실행하기 위해 두 가지 결정이 필요하다.
1. 무엇을 묶을까
같이 변경되는 것끼리 묶는다.- 같은 도메인 개념 (Cart의 items, couponDiscount는 다 장바구니)
- 같은 책임 (Cart는 장바구니, User는 사용자)
- 같이 변경될 운명 (items 변경 시 total도 같이 변경 → 같이 묶음)
"함께 변경될 운명인 것이 함께 있는가" — SoC의 본질이자 캡슐화의 기준
2. 어떤 단위로 묶을까
도메인의 자연스러운 단위로 묶는다.
- 객체 단위 (Cart, Product, User) — 가장 흔함
- 모듈 단위 (cart.ts, user.ts) — 파일로 묶기
- 함수 단위 (Custom Hook 같은 클로저) — React 환경캡슐화의 종류(도구) - "어떻게 캡슐화할 것인가"
1. 클래스
OOP의 가장 명시적인 캡슐화 도구class Cart { private items: Item[] = []; // 데이터 addItem(item: Item) { ... } // 동작 getTotal() { ... } }데이터(필드)와 동작(메서드)이 한 클래스에 묶인다.
2. 클로저
함수의 스코프를 이용한 캡슐화function createCart() { const items: Item[] = []; // 데이터 return { addItem(item: Item) { items.push(item); }, // 동작 getTotal() { return items.reduce(...); }, }; }
items가 createCart 함수 안에 갇히고, 반환된 객체의 메서드만 접근 가능.
3. 모듈
파일 단위 캡슐화// cart.ts const items: Item[] = []; // 데이터 export function addItem(item: Item) { items.push(item); } export function getTotal() { return items.reduce(...); }
파일 안에 데이터와 동작이 같이 묶이고, export한 동작으로만 외부 접근**React 환경에서의 캡슐화**
React에서는 캡슐화가 주로 Custom Hook으로 실현된다.
Custom Hook은 사실상 함수 기반 캡슐화 도구다.function useCart() { const [items, setItems] = useState<Item[]>([]); // 데이터(상태) const [couponDiscount, setCouponDiscount] = useState(0); const addItem = (item: Item) => { // 동작 setItems(prev => [...prev, item]); }; const getTotal = () => { const subtotal = items.reduce((sum, i) => sum + i.price * i.quantity, 0); return subtotal * (1 - couponDiscount); }; return { items, addItem, getTotal }; } // 컴포넌트에서 사용 function CartPage() { const { items, addItem, getTotal } = useCart(); // ... }
상태(items, couponDiscount)와 동작(addItem, getTotal)이 useCart라는 한 단위에 묶인다.
내부 setItems는 클로저 안에 갇혀 외부에서 직접 호출 못 한다.
OOP의 클래스 + private과 같은 효과를 함수형 도구로 실현한다.결론
캡슐화는 단순히 묶는 것이 아니라, 자기 책임을 자기가 지는 단위를 만드는 것이다.
데이터와 동작이 한 곳에 있어서 객체가 자기 일을 자기가 처리한다."변경 가능한 것을 모듈 내부에 두고, 제공할 인터페이스만 노출" 한다는 정보 은닉과 겹치는 느낌이 있다.
예시 코드에도 정보 은닉이 포함된 부분이 있다.
그 이유는 정보 은닉을 적용하려면 변경 가능한 것을 모듈 안에 격리해야 하고, 그러면 자연스럽게 데이터와 동작이 묶이게 된다.
즉, 정보 은닉이 캡슐화를 동반한다.
엄밀히는 다른 책임 (캡슐화는 묶기, 정보 은닉은 차단)이지만, 실전에서는 같이 일어난다.
이 때문에 캡슐화 == 정보은닉이라고 설명하는 곳도 있고 둘을 묶어서 설명하는 곳도 있다.
다만 이 글에선 각자 책임을 분리해서 본다.
각 원칙의 본질이 명확해지고, 약한 모듈 진단 시 무엇이 약한지 짚기 좋을 것이라 생각하기 때문이다.
'소프트웨어 설계 원칙 > 2. 기본 원칙' 카테고리의 다른 글
3. 정보 은닉(Information Hiding) (0) 2026.05.22 2. 추상화(Abstraction) (0) 2026.05.20 1. 관심사의 분리(SOC: Seperation Of Concern) 원칙 (0) 2026.05.19