Encapsulate What Varies#
Encapsulate What Varies 는 소프트웨어 설계에서 변화가 예상되는 부분과 그렇지 않은 부분을 분리하여, 변화가 생겨도 시스템 전체에 영향을 최소화하는 원칙이다. 이 원칙은 변화 가능성이 높은 코드 부분을 추상화하고 분리함으로써 시스템의 한 부분이 다른 부분과 독립적으로 변화할 수 있도록 한다. 객체지향 설계, 디자인 패턴, 시스템 아키텍처 등에서 반복적으로 활용되며, 변화에 유연하게 대응할 수 있는 구조를 만드는데 필수적이다. 대표적으로 전략 패턴 (Strategy Pattern), 데코레이터 패턴 (Decorator Pattern) 등에서 적용되며, 실무에서는 API 버전 관리, 플러그인 시스템, 설정 관리 등 다양한 형태로 활용된다.
핵심 개념#
변하는 것을 캡슐화하라 (Encapsulate What Varies) 는 시스템에서 변화가 예상되는 부분을 식별하고 이를 안정적인 부분으로부터 분리하여 별도의 모듈로 캡슐화하여 시스템의 다른 부분에 영향을 주지 않도록 하는 설계 원칙 이다.
객체지향 프로그래밍, 함수형 프로그래밍, 시스템 아키텍처 등 소프트웨어 전반에 적용될 수 있으며, 전략 패턴, 팩토리 패턴, 플러그인 구조, 설정 파일 분리 등으로 구현할 수 있다.
핵심 아이디어#
- 변화 예측: 미래에 변경될 가능성이 높은 코드 영역을 식별
- 분리: 변화하는 부분을 안정적인 부분으로부터 독립시킴
- 추상화: 인터페이스나 추상 클래스를 통한 변화 은닉
- 독립성: 각 부분이 서로 독립적으로 진화할 수 있도록 함
이론적 기반#
- **개방 - 폐쇄 원칙 (Open-Closed Principle)**과 밀접한 관련
- 단일 책임 원칙 (Single Responsibility Principle) 지원
- **의존성 역전 원칙 (Dependency Inversion Principle)**의 구현 기반
소프트웨어 개발에서 유일한 상수는 변화라는 명제에서 출발한다. 비즈니스 요구사항, 기술 환경, 사용자 요구가 지속적으로 변화하는 현실에서 이러한 변화에 효과적으로 대응할 수 있는 설계 원칙의 필요성이 대두되었다.
Gang of Four 의 디자인 패턴 책에서 “Consider what should be variable in your design” 라는 원칙으로 제시되었으며, 이는 기존의 재설계 원인에 초점을 맞추는 접근법과는 반대로 변경 가능한 요소를 미리 고려하는 접근법을 제시한다.
목적 및 필요성#
주요 목적#
- 변화 대응력 향상: 요구사항 변화에 신속하게 대응
- 시스템 안정성 확보: 변경이 다른 부분에 미치는 영향 최소화
- 유지보수성 개선: 코드 수정 시 영향 범위 제한
- 확장성 보장: 새로운 기능 추가 시 기존 코드 최소 변경
필요성#
- 복잡성 관리: 대규모 시스템의 복잡성을 체계적으로 관리
- 위험 감소: 변경으로 인한 버그 발생 위험 최소화
- 개발 효율성: 변경 사항의 국소화를 통한 개발 생산성 향상
- 비즈니스 연속성: 비즈니스 변화에 대한 기술적 대응력 확보
주요 기능 및 역할#
- 변화가 잦은 부분을 인터페이스, 추상 클래스, 별도의 모듈 등으로 분리.
- 나머지 시스템은 변화에 영향을 받지 않도록 보호.
- 다양한 구현체 (전략, 정책, 알고리즘 등) 를 손쉽게 교체 가능.
기본 특징#
- 예측적 설계: 미래 변화를 예측하여 설계에 반영
- 계층화: 변화하는 부분과 안정적인 부분의 명확한 분리
- 유연성: 런타임 시 다양한 구현체 교체 가능
- 재사용성: 캡슐화된 컴포넌트의 다양한 컨텍스트에서 재사용
고급 특징#
- 조합 가능성: 다양한 변화 요소들의 독립적 조합
- 테스트 용이성: 모킹과 테스트가 용이한 구조
- 문서화: 변화 지점의 명확한 식별과 문서화
- 성능 최적화: 변화하지 않는 부분의 최적화 가능
핵심 원칙#
원칙명 | 정의 | 목적/핵심 의도 | 실현 방법 | 기대 효과 |
---|
변화 식별 원칙 (Change Identification) | 시스템 내에서 자주 변경될 수 있는 요소를 사전에 식별 | 무엇이 바뀌는지를 먼저 아는 것이 설계 분리의 전제 조건 | 요구사항 분석, 기술/비즈니스 규칙 검토 | 변화에 대한 대응 가능성 향상 |
분리 원칙 (Separation) | 변화하는 요소를 안정적인 부분으로부터 물리적, 논리적으로 분리 | 영향 최소화 및 독립성 확보 | 계층 분리, 모듈화, 서비스화 | 독립적 테스트/배포 가능, 영향 최소화 |
추상화 원칙 (Abstraction) | 구체 구현이 아닌, 안정적인 인터페이스로 변화 요소 은닉 | 인터페이스 교체만으로 구현 변경 가능하도록 설계 | Interface, Abstract Class, Protocol | 클라이언트 코드의 안정성 및 유연성 확보 |
결합도 최소화 원칙 (Loose Coupling) | 요소 간 직접 의존 최소화 | 한 모듈의 변화가 다른 모듈에 영향을 주지 않도록 | 의존성 주입 (DI), 이벤트, 메시지 큐 | 확장성, 유연성, 테스트 용이성 향상 |
응집도 최대화 원칙 (High Cohesion) | 유사한 기능/변화 패턴을 하나의 단위로 묶음 | 설계 명확화 및 로직 통일성 강화 | 클래스/모듈 내 기능적 유사성 기준으로 그룹화 | 유지보수성 및 코드 이해도 향상 |
주요 원리#
- 변화가 예상되는 부분을 별도의 추상화 계층으로 분리한다.
- 인터페이스 (Interface), 추상 클래스 (Abstract Class) 를 활용하여 구현부와 분리한다.
- 구체적인 구현은 캡슐 내부에 숨기고, 외부에서는 추상화된 인터페이스만 사용한다.
graph TD
A[요구사항 변화] --> B{변화 감지}
B --> C[변화 영역 식별]
C --> D[캡슐화 설계]
D --> E[인터페이스 정의]
E --> F[구현체 분리]
F --> G[클라이언트 코드]
G --> H{변화 필요?}
H -->|Yes| I[새로운 구현체 생성]
H -->|No| J[기존 동작 유지]
I --> K[기존 인터페이스 유지]
K --> L[구현체만 교체]
L --> M[시스템 안정성 유지]
J --> M
style A fill:#ffcccc
style D fill:#ccffcc
style M fill:#ccccff
subgraph "안정적인 부분"
E
G
K
end
subgraph "변화하는 부분"
F
I
L
end
기본 작동 메커니즘#
- 변화 식별: 시스템에서 자주 변경되는 코드 영역 감지
- 추상화 계층 생성: 변화하는 부분에 대한 안정적인 인터페이스 정의
- 구현 분리: 구체적인 구현을 인터페이스 뒤로 숨김
- 클라이언트 보호: 클라이언트 코드가 추상화에만 의존하도록 설계
변화 대응 프로세스#
- 변화 요청 접수: 새로운 요구사항이나 변경 사항 발생
- 영향 범위 분석: 변화가 미치는 영향 범위 분석
- 캡슐화 경계 확인: 기존 캡슐화 경계의 적절성 검토
- 구현체 수정/추가: 새로운 구현체 생성 또는 기존 구현체 수정
- 시스템 안정성 확인: 다른 부분에 영향이 없음을 확인
구조 및 아키텍처#
graph TB
subgraph "클라이언트 계층 (Client Layer)"
C1[클라이언트 A]
C2[클라이언트 B]
C3[클라이언트 C]
end
subgraph "추상화 계층 (Abstraction Layer)"
I1[인터페이스 A]
I2[인터페이스 B]
I3[추상 클래스 C]
end
subgraph "구현 계층 (Implementation Layer)"
IMPL1[구현체 A1]
IMPL2[구현체 A2]
IMPL3[구현체 B1]
IMPL4[구현체 B2]
IMPL5[구현체 C1]
end
subgraph "변화 관리 계층 (Change Management Layer)"
CF1[설정 파일]
DI1[의존성 주입 컨테이너]
FM1[팩토리 매니저]
end
C1 --> I1
C1 --> I2
C2 --> I2
C2 --> I3
C3 --> I1
C3 --> I3
I1 -.-> IMPL1
I1 -.-> IMPL2
I2 -.-> IMPL3
I2 -.-> IMPL4
I3 -.-> IMPL5
DI1 --> IMPL1
DI1 --> IMPL2
DI1 --> IMPL3
DI1 --> IMPL4
DI1 --> IMPL5
CF1 --> DI1
FM1 --> DI1
style C1 fill:#e1f5fe
style C2 fill:#e1f5fe
style C3 fill:#e1f5fe
style I1 fill:#f3e5f5
style I2 fill:#f3e5f5
style I3 fill:#f3e5f5
style IMPL1 fill:#e8f5e8
style IMPL2 fill:#e8f5e8
style IMPL3 fill:#e8f5e8
style IMPL4 fill:#e8f5e8
style IMPL5 fill:#e8f5e8
구성 요소#
구분 | 구성요소 | 기능 | 역할 | 특징 |
---|
필수 | 클라이언트 (Client) | 비즈니스 로직 실행 | 추상화에 의존해 작업 수행 | 구현체에 대해 모름 (구현 세부사항 은닉) |
| 추상화 (Abstraction) | 안정적인 인터페이스 정의 | 클라이언트와 구현체를 분리 | 인터페이스, 추상 클래스 등으로 구성 |
| 구현체 (Implementation) | 실제 로직 수행 | 추상화를 구현 | 변경 가능성이 높고 독립적으로 변경 가능 |
선택 | 팩토리 (Factory) | 구현체 생성 관리 | 생성 로직 캡슐화 | 생성 방식에 대한 유연성 제공 |
| 의존성 주입 컨테이너 (DI Container) | 의존성 자동 해결 | 런타임 의존성 주입 | 설정만으로 구현체 교체 가능, 생명주기 관리 포함 |
| 설정 관리자 (Configuration Manager) | 외부 설정 기반 구현체 결정 | 런타임 동작 제어 | 코드 수정 없이 구현체 변경 가능 (예: YAML, ENV, DB 기반 설정) |
구현 기법#
기법 | 정의 | 구성 요소 | 목적 | 장점 | 적용 상황 | 변화하는 부분 | 실제 예시 (실무 활용 사례) |
---|
인터페이스 기반 추상화 | 변화하는 기능을 인터페이스/추상 클래스로 추상화 | 공통 인터페이스, 다중 구현체, 클라이언트 코드 | 구현 은닉, 다형성 | 구현체 교체 용이, 테스트 용이, 재사용성 | 다양한 구현이 필요한 경우, 외부 교체 필요 시 | 구체적 구현 방법, 기술 스택 | 결제 시스템에서 PaymentProcessor 인터페이스로 다양한 결제 구현체 캡슐화 |
전략 패턴 | 알고리즘/정책을 인터페이스로 분리, 런타임 교체 가능 | 전략 인터페이스, 구체 전략, 컨텍스트 클래스 | 동적 교체, 유연성 | 조건문 제거, 확장성, 런타임 교체 | 비즈니스 규칙/알고리즘이 자주 바뀔 때 | 알고리즘, 정책, 비즈니스 규칙 | 할인 정책, 인증 방식, 정렬 알고리즘 등 다양한 전략 캡슐화 |
팩토리 패턴 | 객체 생성 로직을 팩토리로 분리, 생성과 사용을 분리 | 팩토리 인터페이스, 구체 팩토리, 제품 인터페이스/클래스 | 생성 로직 은닉, 확장성 | 생성 방식 변경 용이, 결합도 감소 | 객체 생성 방식이 자주 바뀌거나 다양할 때 | 객체 생성 방법, 생성할 타입 | UI 컴포넌트, DB 커넥션, 알림 채널 등 다양한 객체 생성 |
데코레이터 패턴 | 객체에 동적으로 기능을 추가하는 패턴 | 기본 컴포넌트, 데코레이터 인터페이스, 구체 데코레이터 | 유연한 확장 | 기능 조합 자유, 코드 재사용성 | 기능 확장/조합이 자주 필요한 경우 | 추가 기능, 부가 기능 | 커피/피자 옵션 추가, 웹 미들웨어 (로깅, 인증, 캐싱 등) |
플러그인 구조 | 기능 확장을 외부 모듈 (플러그인) 로 분리하는 아키텍처 | 핵심 시스템, 플러그인 인터페이스, 플러그인 모듈 | 확장성, 유연성 | 기능 추가/제거 용이, 핵심 코드 단순화 | 외부 기능 확장/모듈화가 필요한 시스템 | 확장 기능, 외부 모듈 | CMS 플러그인, 브라우저 확장, 데이터 파이프라인 등 |
의존성 주입 (DI) | 객체의 의존성을 외부에서 주입하는 설계 기법 | 의존성 인터페이스, 구현체, 주입 메커니즘, DI 컨테이너 | 결합도 감소, 테스트 | 테스트 용이, 교체/확장 쉬움, 관심사 분리 | 환경별 구현 교체, 테스트가 중요한 경우 | 의존 객체, 서비스 | 서비스 계층 분리 (Spring/Angular), 테스트 시 Mock 주입, 환경별 서비스 구현체 주입 |
인터페이스 기반 추상화 (Interface-based Abstraction)#
변화하는 부분을 인터페이스 (또는 추상 클래스) 로 추상화하여, 다양한 구현체를 통해 변화에 유연하게 대응하는 기법. 클라이언트는 인터페이스에만 의존한다.
구성:
- 인터페이스 (Interface)
- 구현체 (Implementation)
- 클라이언트 (Client)
실제 예시:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| from abc import ABC, abstractmethod
# 인터페이스
class Logger(ABC):
@abstractmethod
def log(self, msg): pass
# 구현체 1
class FileLogger(Logger):
def log(self, msg): print(f"파일 기록: {msg}")
# 구현체 2
class ConsoleLogger(Logger):
def log(self, msg): print(f"콘솔 기록: {msg}")
# 클라이언트
def do_logging(logger: Logger):
logger.log("로그 메시지")
# 사용 예시
do_logging(FileLogger())
do_logging(ConsoleLogger())
|
전략 패턴 (Strategy Pattern)#
알고리즘이나 정책을 인터페이스로 분리하여, 런타임에 동적으로 교체할 수 있게 하는 패턴. 변화하는 알고리즘을 별도의 전략 객체로 캡슐화한다.
구성:
- 전략 (Strategy) 인터페이스
- 구체 전략 (ConcreteStrategy) 클래스
- 컨텍스트 (Context) 클래스 (전략을 사용하는 주체)
실제 예시:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| from abc import ABC, abstractmethod
# 전략 인터페이스
class ShippingStrategy(ABC):
@abstractmethod
def calculate(self, order):
pass
# 구체적 전략 1
class FedExStrategy(ShippingStrategy):
def calculate(self, order):
return 10.0 # FedEx 배송비
# 구체적 전략 2
class UPSStrategy(ShippingStrategy):
def calculate(self, order):
return 8.0 # UPS 배송비
# 컨텍스트: 전략을 사용하는 주체
class ShippingCostCalculator:
def __init__(self, strategy: ShippingStrategy):
self.strategy = strategy # 전략 객체 주입
def calculate_cost(self, order):
return self.strategy.calculate(order)
# 사용 예시
order = {"weight": 2}
calculator = ShippingCostCalculator(FedExStrategy())
print(calculator.calculate_cost(order)) # 10.0
|
의존성 주입 (Dependency Injection, DI)#
객체가 직접 의존성을 생성하지 않고, 외부에서 주입받아 결합도를 낮추는 기법. 변화하는 구현체를 외부에서 주입받아 캡슐한다.
구성:
- 클라이언트 (Client)
- 서비스 (Service)
- 인터페이스 (Interface)
- 인젝터 (Injector)
실제 예시:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| # 인터페이스
class MessageSender:
def send(self, msg):
pass
# 서비스 구현체 1
class EmailSender(MessageSender):
def send(self, msg):
print("이메일 전송:", msg)
# 서비스 구현체 2
class SmsSender(MessageSender):
def send(self, msg):
print("SMS 전송:", msg)
# 클라이언트
class Notifier:
def __init__(self, sender: MessageSender): # 생성자 주입
self.sender = sender
def notify(self, msg):
self.sender.send(msg)
# 인젝터 역할
notifier = Notifier(EmailSender())
notifier.notify("환영합니다!")
|
팩토리 패턴 (Factory Pattern)#
객체 생성 로직을 별도의 팩토리 객체로 분리하여, 생성과 사용을 분리하는 패턴. 변화하는 객체 생성 방식을 캡슐화한다.
구성:
- 팩토리 인터페이스
- 구체적 팩토리 클래스
- 생성할 객체들의 공통 인터페이스
실제 예시:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| // 제품 인터페이스(여기서는 명시적 인터페이스 대신 메서드 규약)
class Notification {
send(msg) {}
}
// 구체적 제품 1
class EmailNotification extends Notification {
send(msg) { console.log("이메일:", msg); }
}
// 구체적 제품 2
class SMSNotification extends Notification {
send(msg) { console.log("SMS:", msg); }
}
// 팩토리
class NotificationFactory {
static create(type) {
if (type === "email") return new EmailNotification();
if (type === "sms") return new SMSNotification();
throw new Error("알 수 없는 타입");
}
}
// 사용 예시
const notifier = NotificationFactory.create("email");
notifier.send("가입을 환영합니다!");
|
데코레이터 패턴 (Decorator Pattern)#
객체에 동적으로 새로운 기능을 추가할 수 있도록 하는 패턴. 변화하거나 추가될 기능을 데코레이터 객체로 분리하여 캡슐화한다.
구성:
- 컴포넌트 인터페이스 (Component)
- 구체적 컴포넌트 (ConcreteComponent)
- 데코레이터 (Decorator)
- 구체적 데코레이터 (ConcreteDecorator)
실제 예시:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| # 컴포넌트 인터페이스
class Coffee:
def cost(self):
return 2000 # 기본 커피 가격
# 데코레이터 베이스
class CoffeeDecorator(Coffee):
def __init__(self, coffee):
self.coffee = coffee
def cost(self):
return self.coffee.cost()
# 구체적 데코레이터
class MilkDecorator(CoffeeDecorator):
def cost(self):
return super().cost() + 500 # 우유 추가
class SugarDecorator(CoffeeDecorator):
def cost(self):
return super().cost() + 300 # 설탕 추가
# 사용 예시
coffee = Coffee()
milk_coffee = MilkDecorator(coffee)
sugar_milk_coffee = SugarDecorator(milk_coffee)
print(sugar_milk_coffee.cost()) # 2800
|
플러그인 구조 (Plugin Architecture)#
기능 확장을 외부 모듈 (플러그인) 로 분리하여, 시스템의 핵심과 확장 기능을 분리하는 구조. 변화하는 기능을 플러그인으로 캡슐화한다.
구성:
- 코어 시스템 (Core)
- 플러그인 인터페이스 (Plugin Interface)
- 플러그인 (Plugin)
실제 예시:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| // 플러그인 인터페이스(규약)
class Plugin {
execute(data) {}
}
// 플러그인 구현
class PrintPlugin extends Plugin {
execute(data) { console.log("출력:", data); }
}
// 코어 시스템
class CoreSystem {
constructor() {
this.plugins = [];
}
register(plugin) {
this.plugins.push(plugin);
}
run(data) {
this.plugins.forEach(plugin => plugin.execute(data));
}
}
// 사용 예시
const core = new CoreSystem();
core.register(new PrintPlugin());
core.run("플러그인 테스트");
|
장점과 단점#
구분 | 항목 | 설명 |
---|
✅ 장점 | 유연성 (Flexibility) | 기능, 정책, 구현체의 변경 및 추가가 구조에 영향을 주지 않고 가능함 |
| 유지보수성 (Maintainability) | 변화가 필요한 부분만 캡슐화되어 있으므로 수정이 국소화되고 위험이 감소함 |
| 확장성 (Extensibility) | 새로운 기능이나 구현체 추가 시 기존 코드 변경 없이 시스템 확장 가능 |
| 재사용성 (Reusability) | 추상화된 컴포넌트 또는 전략을 다양한 컨텍스트에서 재활용할 수 있음 |
| 테스트 용이성 (Testability) | 의존성이 분리되어 있어 단위 테스트, Mock 테스트, A/B 테스트가 용이함 |
| 관심사의 분리 (Separation of Concerns) | 안정적인 부분과 변화하는 부분이 구조적으로 분리되어 코드의 명확성과 가독성이 향상됨 |
| 설계 일관성 (Design Consistency) | 인터페이스, 추상 클래스 기반 설계로 전체 아키텍처의 통일성과 예측 가능성 확보 |
⚠ 단점 | 초기 설계 복잡성 (Initial Design Complexity) | 캡슐화를 고려한 설계 구조 수립에 시간과 노력이 요구됨 |
| 과도한 추상화 (Over-Abstraction) | 예측되지 않는 변화까지 추상화할 경우 불필요한 인터페이스나 클래스가 증가 |
| 성능 오버헤드 (Performance Overhead) | 추상화 계층과 간접 호출로 인해 약간의 성능 저하 발생 가능 |
| 디버깅 복잡성 (Debugging Difficulty) | 추상화/위임 구조가 복잡해지면 호출 흐름 추적이 어려워질 수 있음 |
| 학습 곡선 (Learning Curve) | 디자인 패턴, DI, 전략, 추상화 개념을 팀원 모두가 충분히 이해해야 효과적 |
| 코드 스캐폴딩 증가 (Code Scaffolding Overhead) | 인터페이스, 구현체, 팩토리, 주입 설정 등 구조적 요소가 많아 초기 생산성이 낮아질 수 있음 |
도전 과제#
도전 과제 | 설명 | 해결책 |
---|
변화 예측의 어려움 | 어떤 기능이 변화할지 정확히 식별하기 어려워 잘못된 캡슐화 발생 가능 | - 과거 변경 이력 분석 - 도메인 전문가와 협업 - 점진적 리팩토링 접근 적용 |
과도한 추상화 | 필요 이상으로 추상화를 적용할 경우 오히려 복잡성이 증가 | - 실제 요구 기반 추상화 적용 - 사용 패턴 기반 검증 - 지나친 캡슐화는 피하고 YAGNI 원칙 준수 |
적절한 추상화 수준 판단의 어려움 | 추상화 수준이 지나치게 일반적이거나 구체적일 경우 유연성 저하 또는 재사용 어려움 | - 반복적 리팩토링을 통한 조정 - 유스케이스 기반 설계 - 도메인 기반 모델링 적용 |
성능과 유연성 간의 트레이드오프 | 추상화 계층으로 인한 호출 오버헤드 발생 가능 | - 중요 경로 (critical path) 선별적 최적화 - 정적 바인딩 고려 - 캐싱 및 경량 인터페이스 설계 |
팀원 간 설계 원칙 이해도 차이 | 팀마다 OOP, DI, 전략 패턴 등에 대한 숙련도 격차로 인해 일관성 부족 | - 아키텍처 가이드 문서화 - 설계 리뷰/교육/워크숍 - 코드 리뷰 및 페어 프로그래밍 운영 |
과도한 인터페이스 생성 | 단순히 추상화 원칙을 따르기 위해 의미 없는 인터페이스가 생성되는 경우 | - 역할 중심의 책임 기반 추상화 적용 - 의미 있는 경계 도출을 우선 고려 - 명확한 구현 책임이 있는 경우에만 분리 |
설계 비용 및 초기 생산성 저하 | 설계 초기 단계에서 추상화 구조를 준비하는 데 시간과 자원이 소요됨 | MVP 수준에서는 구체 구현 우선 적용 후 점진적 캡슐화 - 리팩토링 기반 캡슐화 전략 활용 |
디버깅 복잡성 증가 | 추상화 계층이 많아 호출 흐름 파악이 어려워 디버깅 난이도 상승 | - 호출 트레이싱 도구 활용 - 명확한 로깅 전략 수립 IDE 디버거와 연계한 계층 명시화 |
분류에 따른 종류#
분류 기준 | 유형/종류 | 설명 |
---|
설계 범위 기준 | 클래스 수준 캡슐화 | 클래스 내부에서 메서드 또는 속성의 변화를 은닉 (e.g. private 메서드) |
| 모듈/컴포넌트 수준 캡슐화 | 관련 클래스들을 모듈/컴포넌트로 묶어 내부 구현을 은닉 |
| 서비스 수준 캡슐화 | 마이크로서비스 단위로 책임과 변화를 분리 |
| 시스템 수준 캡슐화 | 서브시스템/계층 간 인터페이스를 정의하여 독립적 변경 가능 |
변화 대상 기준 | 행위 (알고리즘/정책) 캡슐화 | 계산 로직, 비즈니스 정책 등의 변경을 전략 객체로 분리 |
| 생성 방식 캡슐화 | 객체 생성 방법의 변경을 팩토리로 추상화 |
| 상태 캡슐화 | 객체의 상태 및 상태 전이 변화를 추상화하여 은닉 |
| 데이터 구조 캡슐화 | 내부 데이터 표현 방식의 변경을 인터페이스로 은닉 |
| 외부 통신 캡슐화 | 외부 시스템과의 연동 또는 메시징 방식의 변경을 Observer/Mediator 로 처리 |
| 설정 및 환경 의존 캡슐화 | 런타임 설정, 환경별 차이 등을 외부 설정이나 DI 로 분리 |
구현 방식 기준 | 인터페이스 기반 | 구현 세부사항을 공통 인터페이스 뒤에 숨김 |
| 상속 기반 | 추상 클래스를 통해 공통 로직 정의 후 하위 클래스에서 세부 구현 |
| 컴포지션 기반 | 객체 간 조합으로 기능 위임, 유연한 구조 제공 |
| 전략 패턴 기반 | 알고리즘을 캡슐화하여 런타임에 교체 가능 |
| 팩토리/생성자 패턴 기반 | 객체 생성을 팩토리에 위임하여 생성 시점의 유연성 확보 |
| 데코레이터 기반 | 기능을 동적으로 추가할 수 있도록 캡슐화 |
패턴 기반 분류 | Strategy (전략 패턴) | 알고리즘 또는 정책의 캡슐화 및 교체 |
| Factory / Abstract Factory | 객체 생성 방식의 캡슐화 |
| Template Method | 알고리즘 구조는 고정하되 일부 단계만 변경 |
| Observer / Mediator | 통신 방식 캡슐화, 이벤트 및 중재자 패턴 |
| State / Memento | 상태 변화 캡슐화, 상태 기반 동작 처리 |
| Adapter / Proxy | 구조 변화 또는 접근 제어의 캡슐화 |
적용 목적 기준 | 유연성 확보 | 알고리즘, 구현, 통신의 변화에 유연하게 대응 |
| 테스트 용이성 확보 | Mock 기반 테스트, 인터페이스 주입 등을 통한 독립적 테스트 가능 |
| 재사용성 확보 | 다양한 컨텍스트에서 재사용 가능한 구조 제공 |
| 유지보수성 향상 | 변경이 지역적으로 발생, ripple effect 방지 |
| 시스템 확장성 | 새로운 기능 추가 시 기존 코드 수정 최소화 |
실무 적용 예시#
도메인 | 변화 요소 | 캡슐화 방법 | 적용 기술/패턴 | 적용 효과 |
---|
결제 시스템 | 결제 수단 (카드, 페이팔, 간편결제 등) | PaymentProcessor 인터페이스 정의 | 전략 패턴 (Strategy Pattern) | 결제 방식 추가/변경 시 기존 코드 변경 없음 |
인증 시스템 | 인증 방식 (JWT, OAuth, LDAP 등) | AuthenticationProvider 인터페이스 분리 | 인터페이스 기반 추상화, DI | 인증 정책 간 유연한 전환 및 테스트 용이 |
알림 시스템 | 알림 채널 (이메일, SMS, 슬랙, 푸시 등) | NotificationChannel 인터페이스 정의 | 전략 패턴, 옵저버 패턴 | 다중 채널 대응 및 채널 교체 유연성 확보 |
로깅 시스템 | 로그 출력 방식 (파일, 콘솔, DB, 클라우드) | Logger 또는 LogAppender 인터페이스 분리 | 데코레이터 패턴, DI, 팩토리 패턴 | 로그 대상 변경 시 영향 최소화, 기능 확장 용이 |
데이터 접근 | 데이터 소스 (RDB, NoSQL, CSV, API 등) | DataSource , DAO 계층 정의 | DAO 패턴, DI | 다양한 소스 대응 및 DB 교체 시 영향 최소화 |
게임 개발 | 캐릭터 행동 (공격, 회피, 특수기 등) | Behavior 또는 Action 전략 인터페이스 정의 | 전략 패턴, 상태 패턴 | 캐릭터 클래스 재사용성 증가 및 동작 유연성 확보 |
전자상거래 플랫폼 | 배송 로직 (택배, 퀵서비스, 해외배송 등) | DeliveryStrategy 인터페이스 | 전략 패턴 | 배송 방식 확장 시 변경 최소화 |
데이터 파이프라인 | 전처리/후처리 로직 (정규화, 필터링 등) | Processor , Transformer 인터페이스 | 템플릿 메서드 패턴 + | |
활용 사례#
사례 1: 마이크로서비스 아키텍처#
마이크로서비스 아키텍처는 시스템을 독립적으로 배포 가능한 서비스로 분해함으로써 가변성 캡슐화를 대규모로 적용한다. 각 서비스는 특정 비즈니스 기능을 캡슐화하며, 서비스 간 통신은 명확한 API 를 통해 이루어진다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
| // 주문 서비스 API
@RestController
@RequestMapping("/orders")
public class OrderController {
@PostMapping
public OrderResponse createOrder(@RequestBody OrderRequest request) {
// 주문 생성 로직
}
@GetMapping("/{id}")
public OrderResponse getOrder(@PathVariable String id) {
// 주문 조회 로직
}
}
// 결제 서비스 API
@RestController
@RequestMapping("/payments")
public class PaymentController {
@PostMapping
public PaymentResponse processPayment(@RequestBody PaymentRequest request) {
// 결제 처리 로직
}
}
// 주문 서비스에서 결제 서비스 호출
@Service
public class OrderService {
private final RestTemplate restTemplate;
private final String paymentServiceUrl;
// 생략…
public Order createOrder(OrderRequest request) {
// 주문 생성
Order order = // …
// 결제 서비스 호출
PaymentRequest paymentRequest = new PaymentRequest(order.getId(), request.getAmount());
PaymentResponse response = restTemplate.postForObject(
paymentServiceUrl + "/payments",
paymentRequest,
PaymentResponse.class
);
// 결제 결과 처리
// …
return order;
}
}
|
이 아키텍처에서는 결제 처리 방식이 변경되더라도 주문 서비스에는 영향을 미치지 않는다. 결제 서비스의 내부 구현은 API 계약을 유지하는 한 자유롭게 변경될 수 있다.
사례 2: 인프라스트럭처 추상화#
현대 개발에서는 클라우드 제공업체, 데이터베이스, 메시징 시스템 등의 인프라스트럭처 의존성을 추상화하는 것이 중요하다. 이를 통해 특정 기술에 종속되지 않고 필요에 따라 인프라를 변경할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
| // 스토리지 추상화
public interface StorageService {
void store(String key, byte[] data);
byte[] retrieve(String key);
void delete(String key);
}
// AWS S3 구현
public class S3StorageService implements StorageService {
private final AmazonS3 s3Client;
private final String bucketName;
// 생략…
@Override
public void store(String key, byte[] data) {
s3Client.putObject(bucketName, key, new ByteArrayInputStream(data), new ObjectMetadata());
}
@Override
public byte[] retrieve(String key) {
S3Object object = s3Client.getObject(bucketName, key);
return IOUtils.toByteArray(object.getObjectContent());
}
@Override
public void delete(String key) {
s3Client.deleteObject(bucketName, key);
}
}
// 파일 시스템 구현
public class FileSystemStorageService implements StorageService {
private final Path rootLocation;
// 생략…
@Override
public void store(String key, byte[] data) {
try {
Files.write(rootLocation.resolve(key), data);
} catch (IOException e) {
throw new StorageException("Failed to store file", e);
}
}
@Override
public byte[] retrieve(String key) {
try {
return Files.readAllBytes(rootLocation.resolve(key));
} catch (IOException e) {
throw new StorageException("Failed to retrieve file", e);
}
}
@Override
public void delete(String key) {
try {
Files.delete(rootLocation.resolve(key));
} catch (IOException e) {
throw new StorageException("Failed to delete file", e);
}
}
}
// 서비스에서 사용
@Service
public class DocumentService {
private final StorageService storageService;
@Autowired
public DocumentService(StorageService storageService) {
this.storageService = storageService;
}
public void saveDocument(String id, byte[] content) {
storageService.store("documents/" + id, content);
}
}
|
이 예시에서는 스토리지 구현이 AWS S3 에서 파일 시스템으로 변경되더라도 DocumentService
는 수정할 필요가 없다.
사례 3: 기능 플래그 (Feature Flags)#
기능 플래그는 런타임에 기능의 활성화 여부를 제어할 수 있는 현대적인 기법이다. 이는 새로운 기능의 점진적 출시와 A/B 테스트를 가능하게 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
| // 기능 플래그 서비스
public interface FeatureFlagService {
boolean isEnabled(String featureName);
boolean isEnabled(String featureName, String userId);
}
// 구현
@Service
public class FeatureFlagServiceImpl implements FeatureFlagService {
private final FeatureFlagRepository repository;
@Autowired
public FeatureFlagServiceImpl(FeatureFlagRepository repository) {
this.repository = repository;
}
@Override
public boolean isEnabled(String featureName) {
FeatureFlag flag = repository.findByName(featureName);
return flag != null && flag.isEnabled();
}
@Override
public boolean isEnabled(String featureName, String userId) {
FeatureFlag flag = repository.findByName(featureName);
if (flag == null || !flag.isEnabled()) {
return false;
}
// 사용자별 점진적 출시 로직
if (flag.getRolloutPercentage() < 100) {
int hash = Math.abs(userId.hashCode()) % 100;
return hash < flag.getRolloutPercentage();
}
return true;
}
}
// 서비스에서 사용
@Service
public class PaymentService {
private final FeatureFlagService featureFlagService;
private final LegacyPaymentProcessor legacyProcessor;
private final NewPaymentProcessor newProcessor;
// 생략…
public PaymentResult processPayment(String userId, Payment payment) {
if (featureFlagService.isEnabled("new-payment-processor", userId)) {
return newProcessor.process(payment);
} else {
return legacyProcessor.process(payment);
}
}
}
|
이 예시에서는 새로운 결제 처리 기능의 출시를 점진적으로 제어할 수 있다. 코드 변경 없이 구성만으로 어떤 사용자가 어떤 버전의 기능을 경험할지 결정할 수 있다.
사례 4: 의존성 주입 프레임워크#
현대적인 소프트웨어 개발에서는 Spring, Guice 등의 의존성 주입 프레임워크를 사용하여 구현체의 변화를 캡슐화한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| // 구성 클래스
@Configuration
public class AppConfig {
@Bean
public PaymentGateway paymentGateway() {
if (useStripe()) {
return new StripePaymentGateway(stripeApiKey);
} else {
return new PayPalPaymentGateway(paypalClientId, paypalSecret);
}
}
private boolean useStripe() {
return "stripe".equals(environment.getProperty("payment.gateway"));
}
}
// 서비스에서 사용
@Service
public class CheckoutService {
private final PaymentGateway paymentGateway;
@Autowired
public CheckoutService(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
public OrderResult checkout(ShoppingCart cart) {
// 결제 처리
PaymentResult result = paymentGateway.processPayment(
cart.getCustomerId(),
cart.getTotalAmount()
);
// 결과 처리
// …
}
}
|
이 예시에서는 결제 게이트웨이의 구현이 Stripe 에서 PayPal 로 변경되더라도 CheckoutService
는 수정할 필요가 없다. 구성 변경만으로 다른 게이트웨이를 사용할 수 있다.
사례 5: 전자상거래 플랫폼의 결제 시스템 현대화#
시나리오: 글로벌 전자상거래 플랫폼에서 다양한 국가별 결제 방식과 새로운 핀테크 솔루션을 지원해야 하는 상황이다. 기존 시스템은 신용카드 결제만 지원했지만, PayPal, Apple Pay, 암호화폐, 지역별 결제 솔루션 (알리페이, 카카오페이 등) 을 추가로 지원해야 한다.
시스템 구성:
graph TB
subgraph "프레젠테이션 계층"
UI[웹/모바일 결제 UI]
API[결제 API Gateway]
end
subgraph "비즈니스 로직 계층"
OS[주문 서비스]
PS[결제 서비스]
VS[검증 서비스]
end
subgraph "추상화 계층 - 캡슐화된 변화 부분"
IPayment[IPaymentProcessor]
IValidation[IPaymentValidator]
INotification[INotificationService]
end
subgraph "구현체 계층 - 변화하는 부분"
CC[신용카드 프로세서]
PP[PayPal 프로세서]
AP[Apple Pay 프로세서]
CP[암호화폐 프로세서]
KP[카카오페이 프로세서]
EmailVal[이메일 검증]
SMSVal[SMS 검증]
BioVal[생체 인증 검증]
EmailNoti[이메일 알림]
SMSNoti[SMS 알림]
PushNoti[푸시 알림]
end
subgraph "외부 시스템"
Bank[은행 시스템]
PG[PG사 시스템]
Crypto[암호화폐 네트워크]
Cloud[클라우드 서비스]
end
UI --> API
API --> OS
OS --> PS
PS --> VS
PS --> IPayment
VS --> IValidation
PS --> INotification
IPayment -.-> CC
IPayment -.-> PP
IPayment -.-> AP
IPayment -.-> CP
IPayment -.-> KP
IValidation -.-> EmailVal
IValidation -.-> SMSVal
IValidation -.-> BioVal
INotification -.-> EmailNoti
INotification -.-> SMSNoti
INotification -.-> PushNoti
CC --> Bank
PP --> PG
CP --> Crypto
EmailNoti --> Cloud
style IPayment fill:#ffeb3b
style IValidation fill:#ffeb3b
style INotification fill:#ffeb3b
Workflow:
sequenceDiagram
participant C as 고객
participant UI as 결제 UI
participant PS as 결제 서비스
participant PP as IPaymentProcessor
participant PV as IPaymentValidator
participant PN as INotificationService
participant EXT as 외부 결제 시스템
C->>UI: 결제 방식 선택
UI->>PS: 결제 요청 (방식, 금액, 정보)
PS->>PP: 결제 프로세서 팩토리에서 적절한 구현체 생성
Note over PP: 런타임에 선택된 결제 방식에<br/>따라 구체적 구현체 결정
PS->>PV: 결제 정보 검증
PV->>PV: 선택된 검증 방식으로 검증 수행
alt 검증 성공
PS->>PP: 결제 처리 실행
PP->>EXT: 외부 결제 시스템 호출
EXT-->>PP: 결제 결과 응답
PP-->>PS: 처리 결과 반환
PS->>PN: 결제 완료 알림 발송
PN->>PN: 설정된 알림 방식으로 발송
PS-->>UI: 성공 응답
UI-->>C: 결제 완료 안내
else 검증 실패
PV-->>PS: 검증 실패
PS-->>UI: 오류 응답
UI-->>C: 오류 메시지 표시
end
역할 담당:
- 변화 캡슐화: 새로운 결제 방식 추가 시 기존 비즈니스 로직 변경 없이 새로운 구현체만 추가
- 독립적 개발: 각 결제 방식별 팀이 독립적으로 개발 가능
- 런타임 선택: 설정이나 사용자 선택에 따라 동적으로 결제 방식 변경
- 확장성 보장: 새로운 핀테크 솔루션이나 지역별 결제 방식 쉽게 추가 가능
실무에서 효과적으로 적용하기 위한 고려사항 및 주의할 점#
카테고리 | 고려사항 | 주의할 점 | 권장 사항 |
---|
요구사항 분석 단계 | 변화 가능성 높은 영역 사전 식별 | 변화 가능성 낮은 영역까지 과잉 설계하지 않기 | 과거 변경 이력, 도메인 전문가 협업, 변화 예측 모델링 활용 |
설계 단계 | 캡슐화 적용 범위 및 단위 결정 | 모든 기능을 무분별하게 추상화하는 일관성 없는 접근 | 실제 변화가 발생했거나 예측된 영역에만 최소한의 추상화 적용 |
| 인터페이스 또는 추상 클래스 설계 | 너무 일반적이거나 구체적인 인터페이스 설계 | 명확한 책임 분리, 단일 책임 원칙 기반 인터페이스 정의 |
구현 단계 | 구현체와 추상화 계층 간 결합 최소화 | 내부 구현 로직이 외부 추상화 계층에 노출되는 설계 | 의존성 역전 원칙 (DIP) 과 인터페이스 분리 원칙 (ISP) 적용 |
| 객체 생성 방식의 유연성 확보 | 객체 생성을 직접 하드코딩하여 캡슐화 효과 상실 | 팩토리 패턴, DI 컨테이너 (Spring, NestJS 등) 활용 |
테스트 단계 | 구현체별 독립 테스트 및 모킹 전략 | 통합 테스트 시 실제 구현체 간 복잡한 의존관계 발생 위험 | 전략 객체 Mock/Stubbing, 테스트 전용 DI 구성 활용 |
유지보수 단계 | 새로운 구현체 추가 시 기존 시스템 영향 최소화 | 인터페이스나 추상화 계층 수정 시 전파 영향 고려 부족 | 최소 인터페이스 유지, 변경 포인트 격리화, SRP 기반 리팩토링 |
운영/성능 단계 | 추상화 계층으로 인한 오버헤드 실측 | 추상화 계층 제거로 유연성과 테스트성을 포기하는 경우 | APM, 프로파일링 도구를 통해 성능 병목 식별 후 국소적 최적화 |
협업 및 커뮤니케이션 | 팀원 간 설계 원칙과 추상화 규칙 공유 | 각자 다른 추상화 기준으로 시스템 일관성 저하 | 정기 코드 리뷰, 설계 문서화, 팀 코딩 표준 수립 |
지속적 개선 및 리팩토링 | 추상화 수준은 반복적 검토 대상 | 초기에 설정한 구조가 변화에 따라 오래 유지될 것이라는 착각 | 실제 사용 패턴 분석, 리팩토링 주기 설정, 필요 시 추상화 제거 또는 단순화 |
- 적용 기준은 " 변화의 빈도와 영향도 " 로 판단
- 변화가 예상되지 않으면 추상화하지 말 것 (“YAGNI” 원칙 적용)
- 캡슐화된 객체는 독립적으로 테스트 가능해야 함
- DI 와 전략 패턴은 거의 모든 유연성 캡슐화 구조에 적합
- 추상화된 구조는 문서화 및 예제 코드로 공유되어야 함
최적화하기 위한 고려사항 및 주의할 점#
영역 | 고려사항 | 주의할 점 | 권장 사항 |
---|
런타임 객체 생성 | 전략 객체나 구현체가 매 요청마다 생성될 경우 오버헤드 유발 | 생성 시점과 범위를 고려하지 않으면 GC 와 성능 저하 발생 | 싱글톤 또는 객체 풀링 활용, DI 컨테이너에서 재사용 객체 주입 |
동적 전략 선택 | 조건문 기반 전략 선택 로직은 분기 수 증가 시 성능 저하 가능 | if-else/switch 남용 시 유지보수와 성능 모두 악화됨 | 전략 맵핑 (HashMap/Enum 기반), 전략 팩토리 패턴 적용 |
간접 호출 구조 | 인터페이스 기반 다형성은 JVM 에서 인라인 최적화가 어렵거나 지연됨 | HotSpot JIT 최적화가 제한됨 | 성능 크리티컬 경로에서는 최적화된 직접 호출 또는 inlining 가능한 구조 사용 |
메모리 사용량 | 다수 전략 구현체 또는 팩토리 등록 시 불필요한 메모리 소모 | 전 전략 객체를 한 번에 로딩하거나 캐시할 경우 사용량 과도 증가 | Lazy Initialization 적용, 자주 사용하는 전략만 사전 생성 |
캐싱 전략 | 반복 사용되는 구현체의 재사용 필요 | 멀티스레드 환경에서 비스레드 안전 캐시 사용 시 동시성 문제 | Thread-safe 캐시 구조 또는 synchronized factory method 사용 |
JIT 최적화 한계 | 동적 바인딩 시 컴파일러가 가상 메서드를 인라인하지 못할 수 있음 | 제네릭을 피하고 런타임 타입을 사용하는 방식은 최적화 방해됨 | 정적 타입 사용, sealed class, final class 등으로 JVM 최적화 유도 |
초기 로딩 비용 | DI 컨테이너 또는 팩토리가 초기 모든 구현체를 생성 시 초기 응답 지연 | 실제 사용되지 않는 객체까지 생성되며 스타트업 성능 저하 | Lazy Injection, 프리로딩 전략 혼용 (ex. Spring @Lazy, @PostConstruct preload) |
배치 처리 성능 | 동일한 로직을 반복 실행할 경우 배치 처리 최적화 필요 | 배치 단위 설정이 부적절할 경우 성능 이득 없음 또는 처리 지연 발생 | 작업 크기 기반 튜닝, Stream 처리 또는 Worker Thread 병렬 분산 적용 |
GC 영향 | 잦은 객체 생성/소멸은 GC 압박 증가 | 메모리 누수 및 불필요한 객체 생명 주기 증가 | WeakReference, Object Pool, Flyweight 패턴 활용 |
불필요한 캡슐화 | 과도한 추상화 구조는 메서드 호출/레벨 증가로 오버헤드 발생 | 단순한 연산/로직에 추상화 적용 시 오히려 성능 저하 가능 | SRP/SOC 에 위배되지 않는 선에서 단순 캡슐화 또는 함수 인라인 전략 적용 |
- 전략 객체는 가능하면 DI 컨테이너에서 싱글톤 주입받기
- 성능에 민감한 전략은 조건 분기 대신 맵핑 테이블로 대체
- 성능 병목 지점은 반드시 프로파일링을 통해 측정하고 개선
- 필요 이상으로 전략을 캡슐화하지 않으며, 메모리/생성 비용을 고려
- 성능과 유지보수 간의 트레이드오프를 명확히 판단하고 분리 적용
주제와 관련하여 주목할 내용#
분류 | 항목 | 설명 |
---|
설계 원칙/패턴 | 전략 패턴 (Strategy Pattern) | 런타임에 알고리즘 또는 정책을 동적으로 교체하기 위해 캡슐화 |
| 팩토리 패턴 (Factory Pattern) | 객체 생성 로직을 클라이언트로부터 분리하여 변화하는 생성 방식 캡슐화 |
| 템플릿 메서드 패턴 | 고정된 알고리즘 틀에 대해 가변 서브 로직만 추상화하여 캡슐화 (상속 기반) |
| 데코레이터 패턴 (Decorator Pattern) | 기능 추가/확장을 위한 객체 동적 래핑 캡슐화 |
| DI (Dependency Injection) | 의존성 주입을 통해 구현체를 외부에서 주입받아, 객체 간 결합도 낮추고 변화 유연하게 대응 |
| 인터페이스 / 추상 클래스 | 구현을 변경해도 클라이언트 코드 영향 없이 인터페이스만 유지함으로써 안정성 확보 |
| 기능 플러그인 구조화 | 핵심 모듈과 별도로 동적으로 로딩 가능한 변화 영역 모듈화 구조화 |
아키텍처 스타일 | 헥사고날 아키텍처 (Hexagonal) | 내부 로직 (도메인) 을 Port 와 Adapter 로 외부와 분리하여 외부 의존성의 변화를 캡슐화 |
| Onion Architecture | 도메인 계층을 중심으로 의존성을 안으로 모으고, 외부는 인터페이스를 통해 의존하도록 구성하여 변화 격리 |
| 플러그인 아키텍처 (Plugin Arch.) | 핵심 시스템에 영향 없이 외부 기능 추가/변경 가능하도록 캡슐화된 모듈 구조 |
프로그래밍 패러다임 | 함수형 프로그래밍 | 고차 함수, 클로저, 커링 등을 통해 로직이나 동작을 값처럼 캡슐화하여 전달 |
클라우드/인프라 | 사이드카 패턴 (Sidecar) | 마이크로서비스에서 로깅, 보안, 라우팅 등 횡단 관심사를 외부 Sidecar 로 분리하여 캡슐화 |
| 환경 설정 캡슐화 (Externalized Config) | 설정 값을 코드와 분리하여 환경 변경 시에도 코드 수정 없이 동작 가능 |
데이터/비즈니스 | CQRS | 명령 (Command) 과 조회 (Query) 를 분리하여 서로 다른 변화/요구를 독립적으로 대응 가능하도록 캡슐화 |
| Repository Pattern | 도메인과 데이터 접근 기술 (JPA, SQL 등) 을 분리하여 데이터 저장소 변경에 유연하게 대응 |
보안/정책 | Zero Trust 아키텍처 | 인증/인가/검증 등 보안 정책을 명확히 경계 지어 서비스 내부와 분리하고 캡슐화 |
| 정책 기반 라우팅/검증 | 인증, 제한 조건 등을 외부 정책 서버 또는 미들웨어로 분리하여 핵심 로직과 분리 |
성능/운영 | 캐시 어사이드 패턴 (Cache-Aside) | 읽기/쓰기 로직과 캐싱 로직을 분리하여 성능 최적화 전략 자체를 캡슐화 |
| 로깅 / 감사 / 트레이싱 분리 | 핵심 로직과 무관한 로깅, 감사, 트레이싱 등을 별도 모듈 또는 Observer 로 구성하여 캡슐화 |
- Encapsulate What Varies 는 기술 계층, 비즈니스 계층, 운영 계층 등 모든 변화 가능한 단면에 적용 가능
- 단순히 전략 패턴에만 국한되지 않으며, 구조적 캡슐화, 정책의 외부화, 인터페이스 기반 분리, 비즈니스 책임 구분 등과 깊게 연관됨
- 현대 소프트웨어 아키텍처에서는 OCP, DIP, SOC, 모듈화 원칙 등과 함께 필수적으로 적용되는 핵심 설계 원칙 중 하나
하위 주제로 분류해서 추가로 학습할 내용#
카테고리 | 주제 | 설명 |
---|
설계 패턴 (Design Pattern) | 전략 패턴 (Strategy Pattern) | 다양한 알고리즘을 런타임에 선택할 수 있도록 캡슐화 |
| 팩토리 패턴 (Factory Pattern) | 객체 생성 로직을 분리하여 생성 방식의 변화에 유연하게 대응 |
| 템플릿 메서드 패턴 (Template Method) | 상속 기반으로 고정 알고리즘과 가변 서브 로직을 분리 |
| 데코레이터 패턴 (Decorator Pattern) | 기존 객체를 수정하지 않고 새로운 기능을 동적으로 추가 |
| 상태 패턴 (State Pattern) | 객체의 상태에 따라 행동을 변경하며, 상태 전이를 캡슐화 |
아키텍처 패턴 | 의존성 주입 (Dependency Injection) | 외부에서 구현체를 주입받아 결합도를 낮추고 테스트 가능성을 높임 |
| 인터페이스 기반 설계 (Interface-Based Design) | 인터페이스를 기준으로 클라이언트와 구현체를 분리하여 유연한 구조 구성 |
| 포트 - 어댑터 패턴 (Hexagonal Architecture) | 비즈니스 로직과 외부 입출력 간의 의존성을 분리 |
| 플러그인 아키텍처 | 핵심 기능과 변경 가능 기능을 동적 모듈로 분리 |
테스트 및 품질 | Mock / Stub 기반 테스트 | 캡슐화된 로직에 대해 독립적 테스트 수행을 위한 전략 |
| 테스트 주도 개발 (TDD) | 먼저 인터페이스 및 동작을 정의하고 테스트 기반으로 개발 진행 |
| 변경 이력 기반 리팩토링 전략 | 변경 빈도, 커밋 로그 등 히스토리를 기반으로 캡슐화 대상 식별 |
소프트웨어 품질 | 변경 가능성 예측 기법 | 변경 빈도 분석, 모듈 책임 파악, 복잡도 분석 등을 통한 캡슐화 후보 도출 |
| 캡슐화 리팩토링 기법 | 기존 코드에서 관심사 분리 및 인터페이스 추출 등 리팩토링 적용 |
운영 및 확장 전략 | 설정 값 캡슐화 (Externalized Config) | 운영 환경 별 차이를 런타임 설정으로 분리하여 관리 |
| 다형성 전략 맵핑 | 전략 패턴에서 구현체 선택을 조건문 대신 맵핑 테이블이나 DI 컨테이너로 효율화 |
| 성능 최적화 기법 | 추상화 계층에서의 메모리, 오버헤드 문제 해결을 위한 캐싱/풀링/지연 로딩 전략 적용 |
실무 사례 중심 학습 | 도메인별 전략 캡슐화 (결제, 인증 등) | 서비스마다 캡슐화가 필요한 다양한 전략 로직을 구체적으로 설계하고 적용 |
| 모듈 간 책임 분리 및 재사용 | 공통 기능 (로깅, 감사, 에러 핸들링 등) 의 독립 모듈화 |
| 변화 주도 설계 기반의 캡슐화 적용 | DDD, 유비쿼터스 언어 기반의 변화 식별 및 구조 설계 전략 |
추가로 알아야 할 내용 및 관련 분야#
설명 | 관련 분야 | 학습 주제 |
---|
API 구조 변경에 유연하게 대응 | 백엔드 개발 | REST API 추상화, 버저닝 전략, 인터페이스 캡슐화 |
동적 요청 처리 및 전략별 라우팅 구현 | MSA 아키텍처 | 서비스 간 전략 선택 구조 (예: 다양한 결제 서비스 연동) |
플러그인 형태로 기능 추가 지원 | 모듈러 아키텍처 | OSGi, Microkernel 아키텍처, 메타프로그래밍 |
프론트엔드 렌더링 전략 분리 | 프론트엔드 개발 | 고차 컴포넌트 (HOC), Render Props, Composition API |
의존성 주입과 서비스 등록의 자동화 | 프레임워크 설계 | Spring, NestJS, Angular 의 DI 컨테이너 |
객체 재사용을 통한 성능 최적화 | 성능 최적화 | 객체 풀링 (Object Pooling), Lazy Initialization |
불변성과 상태 캡슐화를 통해 변화 관리 | 함수형 프로그래밍 | 고차 함수 (HOF), 불변 객체, Currying, Composition |
설계 구조의 변화 대응성 품질 측정 | 소프트웨어 품질관리 | 결합도 (Coupling), 응집도 (Cohesion), 변경 영향 분석 |
변경 가능한 비즈니스 규칙과 고정 도메인 간 분리 | 도메인 주도 설계 (DDD) | Bounded Context, Aggregate, Anti-corruption Layer |
UI 상태 및 입력 전략 캡슐화 | 프론트엔드 상태 관리 | 상태 관리 전략 (Redux, Recoil, Pinia 등) |
이벤트 기반 흐름의 전략적 분리 | 비동기 시스템 설계 | 이벤트 소싱 (Event Sourcing), Saga, CQRS |
동적 기능 로딩 및 배포 자동화를 위한 구성 캡슐화 | 클라우드 네이티브 설계 | 12-Factor App, Externalized Configuration, Sidecar Pattern |
요청 흐름, 인증, 로깅 등 횡단 관심사 분리 | 서비스 메시 (Service Mesh) | Envoy, Istio 를 통한 기능 캡슐화 및 분리 |
유연한 전략 기반 데이터 일관성 보장 | 데이터 관리 | Eventual Consistency, Outbox Pattern, Transactional Outbox |
변화 기반의 테스트 설계 전략 | 테스트 아키텍처 | 테스트 격리 (Mock, Stub), Contract Testing, 테스트 케이스 추상화 |
용어 정리#
소프트웨어 설계 원칙 (Design Principles)#
용어 | 설명 |
---|
OCP (Open-Closed Principle) | 소프트웨어는 확장에는 열려 있고, 변경에는 닫혀 있어야 한다는 원칙. |
결합도 (Coupling) | 모듈/클래스 간의 의존 관계의 강도를 나타냄. 낮을수록 유연함. |
응집도 (Cohesion) | 모듈 내부 구성 요소 간의 연관성 또는 집중도. 높을수록 유지보수에 유리. |
제어 역전 (Inversion of Control, IoC) | 프로그램의 흐름 제어를 개발자가 아닌 프레임워크나 외부 구성요소가 수행하도록 위임하는 원칙. |
객체 지향 프로그래밍 개념 (OOP Concepts)#
용어 | 설명 |
---|
캡슐화 (Encapsulation) | 데이터와 행동을 하나로 묶고, 외부에서 내부 구현을 감추는 원칙. |
추상화 (Abstraction) | 복잡한 내부 구현을 숨기고, 본질적인 특징만 드러내는 기법. |
다형성 (Polymorphism) | 동일한 인터페이스를 통해 서로 다른 구현을 사용할 수 있는 능력. |
디자인 패턴 (Design Patterns)#
용어 | 설명 |
---|
전략 패턴 (Strategy Pattern) | 알고리즘/정책을 인터페이스로 캡슐화하고, 런타임에 교체 가능하게 함. |
팩토리 패턴 (Factory Pattern) | 객체 생성 로직을 클래스 외부로 위임하여 캡슐화. |
템플릿 메서드 패턴 (Template Method Pattern) | 알고리즘의 틀은 상위 클래스에서 정의하고, 세부 구현은 하위 클래스에서 수행. |
데코레이터 패턴 (Decorator Pattern) | 객체에 기능을 동적으로 추가할 수 있도록 구성된 패턴. |
의존성 및 구성 관리 (Dependency & Configuration)#
용어 | 설명 |
---|
DI (Dependency Injection) | 객체가 필요로 하는 의존성을 외부에서 주입받는 구조. IoC 의 대표 구현 방식. |
Feature Toggle (기능 토글) | 기능을 설정으로 On/Off할 수 있게 하는 기법. A/B 테스트, 배포 전략 등에서 사용. |
테스트 및 유지보수 전략 (Testing & Maintainability)#
용어 | 설명 |
---|
모킹 (Mocking) | 실제 객체 대신 가짜 (Mock) 객체를 사용하여 테스트하는 기법. |
인터페이스 기반 개발 | 변화에 유연하게 대응하기 위해 인터페이스를 먼저 정의하고 구현체를 분리하는 개발 방식. |
아키텍처 및 시스템 설계 (Architecture & System Design)#
용어 | 설명 |
---|
MSA (Microservices Architecture) | 애플리케이션을 작은 독립 서비스 단위로 나누어 운영하는 아키텍처. |
플러그인 구조 (Plugin Architecture) | 기능을 독립된 모듈로 캡슐화하여 동적으로 로딩/교체 가능하게 함. |
참고 및 출처#
Encapsulate What Varies 원칙 관련#
디자인 패턴 (Design Patterns)#
의존성 주입 (Dependency Injection)#
아키텍처 및 MSA#
구성 및 런타임 전략#