구조 패턴 (Structural Patterns)
구조 패턴은 클래스와 객체를 결합하여 더 큰 구조를 형성하고, 시스템의 전체적인 구조를 단순화하고 유연하게 만드는 데 중요한 역할을 합니다. 이는 코드의 재사용성과 유지보수성을 높이고, 시스템의 확장성을 개선하며, 객체 간의 관계를 효과적으로 관리하는 데 도움을 줍니다.
OOP(객체 지향 프로그래밍)에서 구조 패턴(Structural Pattern)이 중요한 이유
1. 객체 간의 관계 관리
- 객체 간의 관계 캡슐화: 구조 패턴을 사용하면 객체 간의 관계를 캡슐화하여 코드의 가독성과 유지보수성을 향상시킬 수 있습니다.
- 유연한 구조: 객체 간의 관계를 유연하게 변경할 수 있어, 시스템의 확장성과 유연성을 높일 수 있습니다.
- 코드의 재사용성 향상
- 재사용 가능한 구조: 구조 패턴을 사용하면 재사용 가능한 구조를 만들 수 있어 코드 중복을 줄이고, 유지보수가 쉬워집니다.
- 공통 기능 캡슐화: 공통 기능을 캡슐화하여 여러 클래스에서 재사용할 수 있습니다.
- 시스템의 확장성 및 유지보수성 개선
- 확장성: 새로운 기능이 추가될 때 기존 코드를 최소한으로 변경하여 시스템을 확장할 수 있습니다. 예를 들어, 컴포지트 패턴을 사용하면 새로운 구성 요소를 쉽게 추가할 수 있습니다.
- 유지보수성: 객체 간의 관계를 분리하고 캡슐화함으로써, 코드 변경 시 영향 범위를 줄일 수 있습니다.
- 복잡한 구조의 단순화
- 복잡성 관리: 구조 패턴을 사용하면 복잡한 객체 구조를 단순화하여 관리할 수 있습니다. 예를 들어, 퍼사드 패턴을 사용하면 복잡한 서브시스템을 단순한 인터페이스로 감쌀 수 있습니다.
- 명확한 구조: 시스템의 구조를 명확하게 정의하여 개발자 간의 이해도를 높일 수 있습니다.
- 객체 간의 결합도 감소
- 낮은 결합도: 구조 패턴을 사용하면 객체 간의 결합도를 낮출 수 있어, 코드 변경 시 영향 범위를 최소화할 수 있습니다. 예를 들어, 어댑터 패턴을 사용하면 기존 코드의 변경 없이 새로운 인터페이스를 추가할 수 있습니다.
- 유연한 의존성 관리: 구조 패턴을 사용하면 객체 간의 의존성을 유연하게 관리할 수 있습니다.
구조 패턴 종류
1. Adapter (어댑터)
정의: 호환되지 않는 인터페이스를 가진 클래스들을 함께 동작할 수 있도록 변환합니다.
특징:
- 기존 코드를 변경하지 않고, 호환되지 않는 인터페이스를 맞추어 줍니다.
- 클래스 간의 호환성을 제공합니다.
사용 시기:
- 기존 클래스를 수정하지 않고 새로운 인터페이스에 맞추고자 할 때.
- 외부 라이브러리나 API를 프로젝트에 통합할 때.
예시:
import React from 'react';
// Target 인터페이스 정의
interface Target {
request: () => string;
}
// Adaptee 클래스 정의
class Adaptee {
specificRequest(): string {
return 'Adaptee specific request';
}
}
// Adapter 클래스 정의
class Adapter implements Target {
private adaptee: Adaptee;
constructor(adaptee: Adaptee) {
this.adaptee = adaptee;
}
request(): string {
return this.adaptee.specificRequest();
}
}
// 사용 예시
const App: React.FC = () => {
const adaptee = new Adaptee();
const adapter = new Adapter(adaptee);
return (
<div>
<h1>Adapter Pattern</h1>
<p>{adapter.request()}</p>
</div>
);
};
export default App;
2. Bridge (브리지)
정의: 추상화와 구현을 분리하여 독립적으로 변화할 수 있도록 합니다.
특징:
- 추상화와 구현을 독립적으로 확장할 수 있습니다.
- 두 클래스 계층을 연결합니다.
사용 시기:
- 추상화와 구현을 독립적으로 확장해야 할 때.
- 런타임에 구현을 변경해야 할 때.
예시:
import React from 'react';
// Implementor 인터페이스 정의
interface Renderer {
render: () => string;
}
// Concrete Implementor 구현
class SVGRenderer implements Renderer {
render(): string {
return 'Rendering with SVG';
}
}
class CanvasRenderer implements Renderer {
render(): string {
return 'Rendering with Canvas';
}
}
// Abstraction 클래스 정의
class Shape {
protected renderer: Renderer;
constructor(renderer: Renderer) {
this.renderer = renderer;
}
draw() {
return this.renderer.render();
}
}
// 사용 예시
const App: React.FC = () => {
const svgShape = new Shape(new SVGRenderer());
const canvasShape = new Shape(new CanvasRenderer());
return (
<div>
<h1>Bridge Pattern</h1>
<p>{svgShape.draw()}</p>
<p>{canvasShape.draw()}</p>
</div>
);
};
export default App;
3. Composite (컴포지트)
정의: 객체를 트리 구조로 구성하여 부분-전체 계층을 나타냅니다.
특징:
- 개별 객체와 객체의 집합을 동일하게 처리할 수 있습니다.
- 트리 구조를 통해 복잡한 계층을 구성할 수 있습니다.
사용 시기:
- 객체를 트리 구조로 구성해야 할 때.
- 개별 객체와 객체 집합을 동일한 방식으로 처리해야 할 때.
예시:
import React from 'react';
// Component 인터페이스 정의
interface Graphic {
draw: () => void;
}
// Leaf 클래스 정의
class Circle implements Graphic {
draw() {
console.log('Drawing a circle');
}
}
class Rectangle implements Graphic {
draw() {
console.log('Drawing a rectangle');
}
}
// Composite 클래스 정의
class CompositeGraphic implements Graphic {
private graphics: Graphic[] = [];
add(graphic: Graphic) {
this.graphics.push(graphic);
}
draw() {
this.graphics.forEach(graphic => graphic.draw());
}
}
// 사용 예시
const App: React.FC = () => {
const circle = new Circle();
const rectangle = new Rectangle();
const composite = new CompositeGraphic();
composite.add(circle);
composite.add(rectangle);
return (
<div>
<h1>Composite Pattern</h1>
<p>Check the console for output.</p>
</div>
);
};
export default App;
4. Decorator (데코레이터)
정의: 객체에 추가 책임을 동적으로 부여합니다.
특징:
- 기능을 추가하거나 확장할 때 유용합니다.
- 기존 객체를 수정하지 않고 새로운 기능을 추가할 수 있습니다.
사용 시기:
- 객체의 기능을 동적으로 확장해야 할 때.
- 기존 코드를 변경하지 않고 기능을 추가해야 할 때.
예시:
import React from 'react';
// Component 인터페이스 정의
interface Coffee {
cost: () => number;
description: () => string;
}
// Concrete Component 구현
class SimpleCoffee implements Coffee {
cost(): number {
return 5;
}
description(): string {
return 'Simple Coffee';
}
}
// Decorator 클래스 정의
class MilkDecorator implements Coffee {
protected coffee: Coffee;
constructor(coffee: Coffee) {
this.coffee = coffee;
}
cost(): number {
return this.coffee.cost() + 2;
}
description(): string {
return this.coffee.description() + ', Milk';
}
}
// 사용 예시
const App: React.FC = () => {
const simpleCoffee = new SimpleCoffee();
const milkCoffee = new MilkDecorator(simpleCoffee);
return (
<div>
<h1>Decorator Pattern</h1>
<p>{milkCoffee.description()} costs {milkCoffee.cost()}</p>
</div>
);
};
export default App;
5. Facade (퍼사드)
정의: 복잡한 서브시스템에 대한 간단한 인터페이스를 제공합니다.
특징:
- 서브시스템의 복잡성을 감춥니다.
- 클라이언트가 서브시스템과 상호작용하는 데 필요한 코드를 단순화합니다.
사용 시기:
- 복잡한 서브시스템을 단순화하여 사용할 때.
- 여러 서브시스템을 통합하여 단순화된 인터페이스를 제공할 때.
예시:
import React from 'react';
// 서브시스템 클래스 정의
class SubsystemA {
operationA(): string {
return 'Subsystem A operation';
}
}
class SubsystemB {
operationB(): string {
return 'Subsystem B operation';
}
}
// Facade 클래스 정의
class Facade {
private subsystemA: SubsystemA;
private subsystemB: SubsystemB;
constructor() {
this.subsystemA = new SubsystemA();
this.subsystemB = new SubsystemB();
}
operation(): string {
return `${this.subsystemA.operationA()} and ${this.subsystemB.operationB()}`;
}
}
// 사용 예시
const App: React.FC = () => {
const facade = new Facade();
return (
<div>
<h1>Facade Pattern</h1>
<p>{facade.operation()}</p>
</div>
);
};
export default App;
6. Flyweight (플라이웨이트)
정의: 다수의 객체를 효율적으로 지원하기 위해 공유를 사용합니다.
특징:
- 다수의 유사한 객체를 메모리 효율적으로 관리합니다.
- 객체의 공통 상태를 공유하여 메모리 사용을 최적화합니다.
사용 시기:
- 다수의 유사한 객체를 생성해야 할 때.
- 메모리 사용을 최적화해야 할 때.
예시:
import React from 'react';
// Flyweight 클래스 정의
class Character {
private char: string;
constructor(char: string) {
this.char = char;
}
print(): string {
return this.char;
}
}
// Flyweight Factory 클래스 정의
class CharacterFactory {
private characters: { [key: string]: Character } = {};
getCharacter(char: string): Character {
if (!this.characters[char]) {
this.characters[char] = new Character(char);
}
return this.characters[char];
}
}
// 사용 예시
const App: React.FC = () => {
const factory = new CharacterFactory();
const charA = factory.getCharacter('A');
const charB = factory.getCharacter('B');
const charA2 = factory.getCharacter('A');
return (
<div>
<h1>Flyweight Pattern</h1>
<p>{charA.print()}</p>
<p>{charB.print()}</p>
<p>{charA2.print()}</p>
</div>
);
};
export default App;
7. Proxy (프록시)
정의: 다른 객체에 대한 접근을 제어하기 위해 대리자를 제공합니다.
특징:
- 객체에 대한 접근을 제어합니다.
- 원래 객체에 접근하기 전에 추가 작업을 수행할 수 있습니다.
사용 시기:
- 객체에 대한 접근을 제어해야 할 때.
- 원래 객체에 접근하기 전에 추가 작업을 수행해야 할 때.
예시:
import React from 'react';
// Subject 인터페이스 정의
interface Subject {
request: () => string;
}
// RealSubject 클래스 정의
class RealSubject implements Subject {
request(): string {
return 'RealSubject request';
}
}
// Proxy 클래스 정의
class ProxySubject implements Subject {
private realSubject: RealSubject;
constructor(realSubject: RealSubject) {
this.realSubject = realSubject;
}
request(): string {
console.log('Proxy request');
return this.realSubject.request();
}
}
// 사용 예시
const App: React.FC = () => {
const realSubject = new RealSubject();
const proxy = new ProxySubject(realSubject);
return (
<div>
<h1>Proxy Pattern</h1>
<p>{proxy.request()}</p>
</div>
);
};
export default App;
각 구조 패턴은 특정 상황에서 유용하게 사용될 수 있으며, 코드를 더 구조적이고 유지보수하기 쉽게 만들어준다고 한다.
익숙해져서 자유자재로 사용할 수 있도록 노력해야겠다.
구조 패턴 (Structural Patterns)
구조 패턴은 클래스와 객체를 결합하여 더 큰 구조를 형성하고, 시스템의 전체적인 구조를 단순화하고 유연하게 만드는 데 중요한 역할을 합니다. 이는 코드의 재사용성과 유지보수성을 높이고, 시스템의 확장성을 개선하며, 객체 간의 관계를 효과적으로 관리하는 데 도움을 줍니다.
OOP(객체 지향 프로그래밍)에서 구조 패턴(Structural Pattern)이 중요한 이유
1. 객체 간의 관계 관리
- 객체 간의 관계 캡슐화: 구조 패턴을 사용하면 객체 간의 관계를 캡슐화하여 코드의 가독성과 유지보수성을 향상시킬 수 있습니다.
- 유연한 구조: 객체 간의 관계를 유연하게 변경할 수 있어, 시스템의 확장성과 유연성을 높일 수 있습니다.
- 코드의 재사용성 향상
- 재사용 가능한 구조: 구조 패턴을 사용하면 재사용 가능한 구조를 만들 수 있어 코드 중복을 줄이고, 유지보수가 쉬워집니다.
- 공통 기능 캡슐화: 공통 기능을 캡슐화하여 여러 클래스에서 재사용할 수 있습니다.
- 시스템의 확장성 및 유지보수성 개선
- 확장성: 새로운 기능이 추가될 때 기존 코드를 최소한으로 변경하여 시스템을 확장할 수 있습니다. 예를 들어, 컴포지트 패턴을 사용하면 새로운 구성 요소를 쉽게 추가할 수 있습니다.
- 유지보수성: 객체 간의 관계를 분리하고 캡슐화함으로써, 코드 변경 시 영향 범위를 줄일 수 있습니다.
- 복잡한 구조의 단순화
- 복잡성 관리: 구조 패턴을 사용하면 복잡한 객체 구조를 단순화하여 관리할 수 있습니다. 예를 들어, 퍼사드 패턴을 사용하면 복잡한 서브시스템을 단순한 인터페이스로 감쌀 수 있습니다.
- 명확한 구조: 시스템의 구조를 명확하게 정의하여 개발자 간의 이해도를 높일 수 있습니다.
- 객체 간의 결합도 감소
- 낮은 결합도: 구조 패턴을 사용하면 객체 간의 결합도를 낮출 수 있어, 코드 변경 시 영향 범위를 최소화할 수 있습니다. 예를 들어, 어댑터 패턴을 사용하면 기존 코드의 변경 없이 새로운 인터페이스를 추가할 수 있습니다.
- 유연한 의존성 관리: 구조 패턴을 사용하면 객체 간의 의존성을 유연하게 관리할 수 있습니다.
구조 패턴 종류
1. Adapter (어댑터)
정의: 호환되지 않는 인터페이스를 가진 클래스들을 함께 동작할 수 있도록 변환합니다.
특징:
- 기존 코드를 변경하지 않고, 호환되지 않는 인터페이스를 맞추어 줍니다.
- 클래스 간의 호환성을 제공합니다.
사용 시기:
- 기존 클래스를 수정하지 않고 새로운 인터페이스에 맞추고자 할 때.
- 외부 라이브러리나 API를 프로젝트에 통합할 때.
예시:
import React from 'react';
// Target 인터페이스 정의
interface Target {
request: () => string;
}
// Adaptee 클래스 정의
class Adaptee {
specificRequest(): string {
return 'Adaptee specific request';
}
}
// Adapter 클래스 정의
class Adapter implements Target {
private adaptee: Adaptee;
constructor(adaptee: Adaptee) {
this.adaptee = adaptee;
}
request(): string {
return this.adaptee.specificRequest();
}
}
// 사용 예시
const App: React.FC = () => {
const adaptee = new Adaptee();
const adapter = new Adapter(adaptee);
return (
<div>
<h1>Adapter Pattern</h1>
<p>{adapter.request()}</p>
</div>
);
};
export default App;
2. Bridge (브리지)
정의: 추상화와 구현을 분리하여 독립적으로 변화할 수 있도록 합니다.
특징:
- 추상화와 구현을 독립적으로 확장할 수 있습니다.
- 두 클래스 계층을 연결합니다.
사용 시기:
- 추상화와 구현을 독립적으로 확장해야 할 때.
- 런타임에 구현을 변경해야 할 때.
예시:
import React from 'react';
// Implementor 인터페이스 정의
interface Renderer {
render: () => string;
}
// Concrete Implementor 구현
class SVGRenderer implements Renderer {
render(): string {
return 'Rendering with SVG';
}
}
class CanvasRenderer implements Renderer {
render(): string {
return 'Rendering with Canvas';
}
}
// Abstraction 클래스 정의
class Shape {
protected renderer: Renderer;
constructor(renderer: Renderer) {
this.renderer = renderer;
}
draw() {
return this.renderer.render();
}
}
// 사용 예시
const App: React.FC = () => {
const svgShape = new Shape(new SVGRenderer());
const canvasShape = new Shape(new CanvasRenderer());
return (
<div>
<h1>Bridge Pattern</h1>
<p>{svgShape.draw()}</p>
<p>{canvasShape.draw()}</p>
</div>
);
};
export default App;
3. Composite (컴포지트)
정의: 객체를 트리 구조로 구성하여 부분-전체 계층을 나타냅니다.
특징:
- 개별 객체와 객체의 집합을 동일하게 처리할 수 있습니다.
- 트리 구조를 통해 복잡한 계층을 구성할 수 있습니다.
사용 시기:
- 객체를 트리 구조로 구성해야 할 때.
- 개별 객체와 객체 집합을 동일한 방식으로 처리해야 할 때.
예시:
import React from 'react';
// Component 인터페이스 정의
interface Graphic {
draw: () => void;
}
// Leaf 클래스 정의
class Circle implements Graphic {
draw() {
console.log('Drawing a circle');
}
}
class Rectangle implements Graphic {
draw() {
console.log('Drawing a rectangle');
}
}
// Composite 클래스 정의
class CompositeGraphic implements Graphic {
private graphics: Graphic[] = [];
add(graphic: Graphic) {
this.graphics.push(graphic);
}
draw() {
this.graphics.forEach(graphic => graphic.draw());
}
}
// 사용 예시
const App: React.FC = () => {
const circle = new Circle();
const rectangle = new Rectangle();
const composite = new CompositeGraphic();
composite.add(circle);
composite.add(rectangle);
return (
<div>
<h1>Composite Pattern</h1>
<p>Check the console for output.</p>
</div>
);
};
export default App;
4. Decorator (데코레이터)
정의: 객체에 추가 책임을 동적으로 부여합니다.
특징:
- 기능을 추가하거나 확장할 때 유용합니다.
- 기존 객체를 수정하지 않고 새로운 기능을 추가할 수 있습니다.
사용 시기:
- 객체의 기능을 동적으로 확장해야 할 때.
- 기존 코드를 변경하지 않고 기능을 추가해야 할 때.
예시:
import React from 'react';
// Component 인터페이스 정의
interface Coffee {
cost: () => number;
description: () => string;
}
// Concrete Component 구현
class SimpleCoffee implements Coffee {
cost(): number {
return 5;
}
description(): string {
return 'Simple Coffee';
}
}
// Decorator 클래스 정의
class MilkDecorator implements Coffee {
protected coffee: Coffee;
constructor(coffee: Coffee) {
this.coffee = coffee;
}
cost(): number {
return this.coffee.cost() + 2;
}
description(): string {
return this.coffee.description() + ', Milk';
}
}
// 사용 예시
const App: React.FC = () => {
const simpleCoffee = new SimpleCoffee();
const milkCoffee = new MilkDecorator(simpleCoffee);
return (
<div>
<h1>Decorator Pattern</h1>
<p>{milkCoffee.description()} costs {milkCoffee.cost()}</p>
</div>
);
};
export default App;
5. Facade (퍼사드)
정의: 복잡한 서브시스템에 대한 간단한 인터페이스를 제공합니다.
특징:
- 서브시스템의 복잡성을 감춥니다.
- 클라이언트가 서브시스템과 상호작용하는 데 필요한 코드를 단순화합니다.
사용 시기:
- 복잡한 서브시스템을 단순화하여 사용할 때.
- 여러 서브시스템을 통합하여 단순화된 인터페이스를 제공할 때.
예시:
import React from 'react';
// 서브시스템 클래스 정의
class SubsystemA {
operationA(): string {
return 'Subsystem A operation';
}
}
class SubsystemB {
operationB(): string {
return 'Subsystem B operation';
}
}
// Facade 클래스 정의
class Facade {
private subsystemA: SubsystemA;
private subsystemB: SubsystemB;
constructor() {
this.subsystemA = new SubsystemA();
this.subsystemB = new SubsystemB();
}
operation(): string {
return `${this.subsystemA.operationA()} and ${this.subsystemB.operationB()}`;
}
}
// 사용 예시
const App: React.FC = () => {
const facade = new Facade();
return (
<div>
<h1>Facade Pattern</h1>
<p>{facade.operation()}</p>
</div>
);
};
export default App;
6. Flyweight (플라이웨이트)
정의: 다수의 객체를 효율적으로 지원하기 위해 공유를 사용합니다.
특징:
- 다수의 유사한 객체를 메모리 효율적으로 관리합니다.
- 객체의 공통 상태를 공유하여 메모리 사용을 최적화합니다.
사용 시기:
- 다수의 유사한 객체를 생성해야 할 때.
- 메모리 사용을 최적화해야 할 때.
예시:
import React from 'react';
// Flyweight 클래스 정의
class Character {
private char: string;
constructor(char: string) {
this.char = char;
}
print(): string {
return this.char;
}
}
// Flyweight Factory 클래스 정의
class CharacterFactory {
private characters: { [key: string]: Character } = {};
getCharacter(char: string): Character {
if (!this.characters[char]) {
this.characters[char] = new Character(char);
}
return this.characters[char];
}
}
// 사용 예시
const App: React.FC = () => {
const factory = new CharacterFactory();
const charA = factory.getCharacter('A');
const charB = factory.getCharacter('B');
const charA2 = factory.getCharacter('A');
return (
<div>
<h1>Flyweight Pattern</h1>
<p>{charA.print()}</p>
<p>{charB.print()}</p>
<p>{charA2.print()}</p>
</div>
);
};
export default App;
7. Proxy (프록시)
정의: 다른 객체에 대한 접근을 제어하기 위해 대리자를 제공합니다.
특징:
- 객체에 대한 접근을 제어합니다.
- 원래 객체에 접근하기 전에 추가 작업을 수행할 수 있습니다.
사용 시기:
- 객체에 대한 접근을 제어해야 할 때.
- 원래 객체에 접근하기 전에 추가 작업을 수행해야 할 때.
예시:
import React from 'react';
// Subject 인터페이스 정의
interface Subject {
request: () => string;
}
// RealSubject 클래스 정의
class RealSubject implements Subject {
request(): string {
return 'RealSubject request';
}
}
// Proxy 클래스 정의
class ProxySubject implements Subject {
private realSubject: RealSubject;
constructor(realSubject: RealSubject) {
this.realSubject = realSubject;
}
request(): string {
console.log('Proxy request');
return this.realSubject.request();
}
}
// 사용 예시
const App: React.FC = () => {
const realSubject = new RealSubject();
const proxy = new ProxySubject(realSubject);
return (
<div>
<h1>Proxy Pattern</h1>
<p>{proxy.request()}</p>
</div>
);
};
export default App;
각 구조 패턴은 특정 상황에서 유용하게 사용될 수 있으며, 코드를 더 구조적이고 유지보수하기 쉽게 만들어준다고 한다.
익숙해져서 자유자재로 사용할 수 있도록 노력해야겠다.