백엔드 개발자
[디자인 패턴] 구조 패턴 본문
구조 패턴(Structural Patterns)은 객체나 클래스를 조합하여 더 큰 구조를 만드는 데 초점을 둔다.
그래서 유지보수성과 확장성을 고려하여 코드의 결합도를 낮추고 재사용성을 높이는 역할을 한다.
1. 어댑터 패턴(Adapter Pattern)
어댑터 패턴은 서로 다른 인터페이스를 가진 클래스들이 함께 동작할 수 있도록 중간에 변환기를 두는 패턴이다.
즉, 기존 코드의 변경 없이 새로운 기능을 추가할 때 유용하다.
구현 방법
- 기존 인터페이스와 호환되지 않는 클래스를 새 인터페이스에 맞게 변환하는 역할을 한다.
- 인터페이스 기반의 어댑터 또는 객체 기반의 어댑터를 사용할 수 있다.
특징
- 코드 변경 없이 다른 클래스와 연동 가능.
- 기존 시스템을 수정하지 않고 확장 가능.
- 클래스 어댑터(상속 사용)와 객체 어댑터(위임 사용) 두 가지 방식이 있다.
사용 예시 (Java)
// 기존 인터페이스
interface OldSystem {
void oldMethod();
}
// 새로운 인터페이스
interface NewSystem {
void newMethod();
}
// 기존 시스템 구현
class OldSystemImpl implements OldSystem {
@Override
public void oldMethod() {
System.out.println("Old system method.");
}
}
// 어댑터 클래스 (NewSystem을 구현하면서, OldSystem을 필드로 가지고 있다.)
class Adapter implements NewSystem {
private OldSystem oldSystem;
public Adapter(OldSystem oldSystem) {
this.oldSystem = oldSystem;
}
@Override
public void newMethod() {
oldSystem.oldMethod(); // 기존 메서드를 변환하여 호출
}
}
public class AdapterPatternExample {
public static void main(String[] args) {
OldSystem oldSystem = new OldSystemImpl();
NewSystem newSystem = new Adapter(oldSystem);
newSystem.newMethod();
}
}
실제 사용 예시
- Java의 InputStreamReader: InputStream(바이트 기반)을 Reader(문자 기반)로 변환할 때 사용됨.
- Spring의 Resource 인터페이스: 다양한 리소스(File, URL)를 통합하여 처리하는 데 사용됨.
2. 브리지 패턴(Bridge Pattern)
브리지 패턴은 추상적인 부분과 구현을 분리하여 독립적으로 확장할 수 있도록 하는 패턴이다.
즉, 인터페이스와 구현을 분리하여 유지보수성을 높이고 결합도를 낮춘다.
구현 방법
- 추상 클래스는 기능을 정의하고, 구현체는 별도의 클래스로 분리한다.
- 인터페이스를 활용하여 다형성을 유지하며, 여러 구현체와 조합할 수 있도록 한다.
특징
- 추상화와 구현을 독립적으로 확장 가능.
- 계층 구조가 복잡한 경우 유용.
- 유지보수가 쉬워지고 결합도가 낮아짐.
사용 예시 (Java)
// 구현체 인터페이스
interface Color {
String fill();
}
// 구체적인 구현체
class RedColor implements Color {
@Override
public String fill() {
return "Color: Red";
}
}
class BlueColor implements Color {
@Override
public String fill() {
return "Color: Blue";
}
}
// 추상 클래스
abstract class Shape {
protected Color color;
public Shape(Color color) {
this.color = color;
}
abstract void draw();
}
// 구체적인 추상 클래스
class Circle extends Shape {
public Circle(Color color) {
super(color);
}
@Override
public void draw() {
System.out.println("Drawing Circle with " + color.fill());
}
}
// 사용 예시
public class BridgePatternExample {
public static void main(String[] args) {
Shape redCircle = new Circle(new RedColor());
Shape blueCircle = new Circle(new BlueColor());
redCircle.draw(); // Drawing Circle with Color: Red
blueCircle.draw(); // Drawing Circle with Color: Blue
}
}
실제 사용 예시
- JDBC 드라이버: Connection 인터페이스와 각 데이터베이스 벤더의 구현 분리.
- GUI 라이브러리: OS별 UI 렌더링을 독립적으로 구현.
3. 프록시 패턴(Proxy Pattern)
프록시 패턴은 특정 객체에 대한 접근을 제어하기 위해 대리자(Proxy)를 사용하는 패턴이다.
구현 방법
- 실제 객체 대신 중간에 Proxy 클래스를 두고 요청을 위임한다.
- 접근 제어(보안, 로깅, 캐싱 등)를 적용할 수 있다.
특징
- 객체 생성 비용을 줄일 수 있음.
- 접근 제어 및 로깅 기능을 쉽게 추가 가능.
- 원본 객체를 보호할 수 있음.
사용 예시 (Java)
// 원본 인터페이스
interface Service {
void operation();
}
// 실제 서비스 클래스
class RealService implements Service {
@Override
public void operation() {
System.out.println("Real Service is performing an operation.");
}
}
// 프록시 클래스 (RealService 접근 제어)
class ProxyService implements Service {
private RealService realService;
@Override
public void operation() {
if (realService == null) {
realService = new RealService(); // 지연 초기화
}
System.out.println("Proxy: Logging before operation.");
realService.operation();
}
}
// 사용 예시
public class ProxyPatternExample {
public static void main(String[] args) {
Service proxy = new ProxyService();
proxy.operation();
}
}
실제 사용 예시
- Spring AOP: 프록시를 사용하여 트랜잭션, 로깅, 보안 기능 추가.
- 가상 프록시: 이미지 로딩 최적화(예: 지연 로딩).
=> 연예인 매니지먼트를 통해 섭외, 협찬, 선물을 전달하는 것처럼 연예인한테 다이렉트로 요청을 전하는 것이 아닌 프록시(대리인)를 통해 접근 제어를 할 수 있다.
4. 데코레이터 패턴(Decorator Pattern)
데코레이터 패턴은 기존 객체에 새로운 기능을 추가하는 패턴이다.
상속이 아닌 객체 조합(composition) 을 사용하여 기능을 확장한다.
구현 방법
- 기본 클래스를 상속받은 데코레이터 클래스를 만들고, 해당 클래스에서 기존 기능을 감싸서 확장한다.
- 실행 중에 동적으로 객체의 기능을 변경할 수 있다.
특징
- 기존 코드를 수정하지 않고 기능을 추가 가능.
- 중첩을 통해 여러 기능을 조합 가능.
- OCP(개방-폐쇄 원칙)를 만족.
사용 예시 (Java)
// 기본 인터페이스
interface Coffee {
String make();
}
// 기본 클래스
class SimpleCoffee implements Coffee {
@Override
public String make() {
return "Simple Coffee";
}
}
// 데코레이터 클래스
class MilkDecorator implements Coffee {
private Coffee coffee;
public MilkDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public String make() {
return coffee.make() + ", with Milk";
}
}
// 사용 예시
public class DecoratorPatternExample {
public static void main(String[] args) {
Coffee coffee = new SimpleCoffee();
Coffee milkCoffee = new MilkDecorator(coffee);
System.out.println(coffee.make()); // Simple Coffee
System.out.println(milkCoffee.make()); // Simple Coffee, with Milk
}
}
클래스 다이어그램
실제 사용 예시
- Java I/O (BufferedReader, InputStreamReader)
- Spring의 BeanPostProcessor
5. 컴포지트 패턴(Composite Pattern)
컴포지트 패턴은 객체를 트리 구조로 구성하여 계층적인 표현을 가능하게 한다.
즉, 단일 객체와 복합 객체를 동일하게 다룰 수 있도록 설계한다.
구현 방법
- 공통 인터페이스를 만들어 개별 객체와 복합 객체를 동일하게 다룰 수 있도록 한다.
- 재귀적인 구조를 가질 수 있도록 설계한다.
특징
- 클라이언트가 단일 객체와 복합 객체를 동일하게 다룰 수 있음.
- 트리 구조를 쉽게 관리 가능.
- 계층적인 데이터 표현에 적합.
사용 예시 (Java)
import java.util.ArrayList;
import java.util.List;
// 공통 인터페이스
interface Component {
void showDetails();
}
// 개별 객체 (Leaf)
class File implements Component {
private String name;
public File(String name) {
this.name = name;
}
@Override
public void showDetails() {
System.out.println("File: " + name);
}
}
// 복합 객체 (Composite)
class Folder implements Component {
private String name;
private List<Component> components = new ArrayList<>();
public Folder(String name) {
this.name = name;
}
public void addComponent(Component component) {
components.add(component);
}
@Override
public void showDetails() {
System.out.println("Folder: " + name);
for (Component component : components) {
component.showDetails();
}
}
}
// 사용 예시
public class CompositePatternExample {
public static void main(String[] args) {
Folder root = new Folder("Root");
Folder subFolder = new Folder("SubFolder");
File file1 = new File("file1.txt");
File file2 = new File("file2.txt");
root.addComponent(subFolder);
root.addComponent(file1);
subFolder.addComponent(file2);
root.showDetails();
}
}
실제 사용 예시
- 파일 시스템 (디렉토리 - 파일 구조)
- HTML 요소 구조
6. 퍼사드 패턴(Facade Pattern)
퍼사드 패턴은 복잡한 서브시스템을 단순한 인터페이스로 감싸는 패턴이다.
즉, 클라이언트가 복잡한 내부 구현을 몰라도 쉽게 사용할 수 있도록 한다.
구현 방법
- 서브시스템을 감싸는 단일 클래스를 만든다.
- 클라이언트는 이 단일 클래스를 통해서만 기능을 사용하도록 한다.
특징
- 사용자가 내부 구현을 몰라도 쉽게 사용 가능.
- 서브시스템 간의 결합도를 낮출 수 있음.
- 코드의 가독성과 유지보수성이 향상됨.
사용 예시 (Java)
// 복잡한 서브 시스템
class CPU {
void start() { System.out.println("CPU started."); }
}
class Memory {
void load() { System.out.println("Memory loaded."); }
}
class HardDrive {
void read() { System.out.println("Hard Drive reading."); }
}
// 퍼사드 클래스
class ComputerFacade {
private CPU cpu;
private Memory memory;
private HardDrive hardDrive;
public ComputerFacade() {
this.cpu = new CPU();
this.memory = new Memory();
this.hardDrive = new HardDrive();
}
public void startComputer() {
cpu.start();
memory.load();
hardDrive.read();
System.out.println("Computer started successfully.");
}
}
// 사용 예시
public class FacadePatternExample {
public static void main(String[] args) {
ComputerFacade computer = new ComputerFacade();
computer.startComputer();
}
}
실제 사용 예시
- Spring의 JdbcTemplate (복잡한 DB 작업을 단순화)
- SLF4J 로깅 프레임워크
=> 너무 당연하게 느껴지지만 우리가 라이브러리나 프레임워크를 사용하는 것도 이런 퍼사드 패턴 기반으로 발전해왔기 때문에 그렇게 느껴질 수 있다.
7. 플라이웨이트 패턴(Flyweight Pattern)
플라이웨이트 패턴은 객체를 공유하여 메모리 사용량을 최적화하는 패턴이다.
구현 방법
- 공유 가능한 상태를 가진 객체를 캐싱하여 재사용한다.
- 다수의 객체 생성 비용을 줄인다.
특징
- 객체 재사용을 통해 성능 최적화 가능.
- 상태를 내부(공유)와 외부(비공유)로 나눔.
- 메모리 사용량 감소.
사용 예시 (Java)
import java.util.HashMap;
import java.util.Map;
// 공유 객체
class Character {
private final char symbol;
public Character(char symbol) {
this.symbol = symbol;
}
public void display() {
System.out.println("Character: " + symbol);
}
}
// 플라이웨이트 팩토리
class CharacterFactory {
private static final Map<Character, Character> cache = new HashMap<>();
public static Character getCharacter(char symbol) {
cache.putIfAbsent(symbol, new Character(symbol));
return cache.get(symbol);
}
}
// 사용 예시
public class FlyweightPatternExample {
public static void main(String[] args) {
Character a1 = CharacterFactory.getCharacter('A');
Character a2 = CharacterFactory.getCharacter('A');
a1.display();
a2.display();
System.out.println("Same object: " + (a1 == a2)); // true
}
}
실제 사용 예시
- Java String Pool (String interning)
- 그래픽 엔진에서 객체 재사용 (예: 문자 렌더링)