객체 구조 디자인 패턴이란?
클래스와 객체 간의 관계를 조직화하여 확장성과 유지보수성을 높이는 패턴이다.
서로 다른 인터페이스를 가진 클래스를 연결하여 기존 클래스를 수정하지 않고도 새로운 기능을 추가할 수 있도록 하는 패턴이다.
// 클라이언트가 사용하는 공통 인터페이스
interface MediaPlayer {
void play(String audioType, String fileName);
}
// 기존 MP3 플레이어 클래스
class MP3Player implements MediaPlayer {
public void play(String audioType, String fileName) {
System.out.println("MP3 파일 재생 중: " + fileName);
}
}
// 기존 MP4 플레이어 (MediaPlayer 인터페이스와 호환되지 않음)
class MP4Player {
public void playMP4(String fileName) {
System.out.println("MP4 파일 재생 중: " + fileName);
}
}
// 어댑터 클래스 (MediaPlayer 인터페이스를 구현하여 MP4Player와 연결)
class MediaAdapter implements MediaPlayer {
private MP4Player mp4Player = new MP4Player();
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("mp4")) {
mp4Player.playMP4(fileName); // MP4 플레이어의 메서드를 호출하여 변환
} else {
System.out.println("지원되지 않는 형식: " + audioType);
}
}
}
// 클라이언트 코드 (어댑터 패턴을 활용하여 MP4 파일도 재생 가능)
public class AdapterPatternExample {
public static void main(String[] args) throws FileNotFoundException {
MediaPlayer mp3Player = new MP3Player();
mp3Player.play("mp3", "music.mp3");
MediaPlayer adapter = new MediaAdapter();
adapter.play("mp4", "video.mp4");
}
}
MediaAdapter가 여러 개의 다른 포맷(AVI, MKV 등)을 지원하도록 확장기존 객체에 새로운 기능을 동적으로 추가할 수 있도록 하는 패턴이다.
// 기본 커피 인터페이스
interface Coffee {
String getDescription();
double cost();
}
// 가장 기본적인 커피 클래스
class SimpleCoffee implements Coffee {
public String getDescription() {
return "기본 커피";
}
public double cost() {
return 2000.0;
}
}
// 데코레이터 (추상 클래스)
abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee; // 기존 커피 객체를 감싸는 역할
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
public String getDescription() {
return coffee.getDescription();
}
public double cost() {
return coffee.cost();
}
}
// 우유 추가 데코레이터
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
public String getDescription() {
return super.getDescription() + ", 우유 추가";
}
public double cost() {
return super.cost() + 500.0;
}
}
// 설탕 추가 데코레이터
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
public String getDescription() {
return super.getDescription() + ", 설탕 추가";
}
public double cost() {
return super.cost() + 300.0;
}
}
// 휘핑크림 추가 데코레이터
class WhipDecorator extends CoffeeDecorator {
public WhipDecorator(Coffee coffee) {
super(coffee);
}
public String getDescription() {
return super.getDescription() + ", 휘핑크림 추가";
}
public double cost() {
return super.cost() + 700.0;
}
}
// 클라이언트 코드
public class DecoratorPatternExample {
public static void main(String[] args) {
// 기본 커피
Coffee coffee = new SimpleCoffee();
System.out.println(coffee.getDescription() + " 가격: " + coffee.cost() + "원");
// 우유 추가
coffee = new MilkDecorator(coffee);
System.out.println(coffee.getDescription() + " 가격: " + coffee.cost() + "원");
// 설탕 추가
coffee = new SugarDecorator(coffee);
System.out.println(coffee.getDescription() + " 가격: " + coffee.cost() + "원");
// 휘핑크림 추가
coffee = new WhipDecorator(coffee);
System.out.println(coffee.getDescription() + " 가격: " + coffee.cost() + "원");
}
}
실제 객체에 대한 접근을 제어하기 위해 대리 객체를 사용하는 패턴이다.
// 이미지 인터페이스 (공통 인터페이스)
interface Image {
void display();
}
// 실제 이미지 객체 (비싼 리소스를 사용하는 객체)
class RealImage implements Image {
private String fileName;
public RealImage(String fileName) {
this.fileName = fileName;
loadFromDisk(); // 파일 로딩 (비용이 큰 작업)
}
private void loadFromDisk() {
System.out.println("디스크에서 " + fileName + " 로딩 중...");
}
public void display() {
System.out.println(fileName + " 화면에 표시됨");
}
}
// 프록시 객체 (실제 객체에 대한 접근을 지연시키거나 제어)
class ProxyImage implements Image {
private RealImage realImage; // 실제 이미지 객체
private String fileName;
public ProxyImage(String fileName) {
this.fileName = fileName;
}
public void display() {
if (realImage == null) { // 실제 객체가 필요할 때만 생성 (지연 로딩)
realImage = new RealImage(fileName);
}
realImage.display();
}
}
// 클라이언트 코드
public class ProxyPatternExample {
public static void main(String[] args) {
Image image1 = new ProxyImage("photo1.jpg");
// 첫 번째 호출 (이미지 로딩 발생)
image1.display();
System.out.println("-------------");
// 두 번째 호출 (이미 로딩된 이미지 사용)
image1.display();
System.out.println("-------------");
// 새로운 이미지 객체 생성
Image image2 = new ProxyImage("photo2.jpg");
image2.display();
}
}
RealImage는 파일을 디스크에서 로드하는 비용이 큰 작업을 수행하는데, ProxyImage를 사용하면 실제 이미지가 필요할 때만 객체를 생성하여 성능을 최적화 할 수 있다. ProxyImage는 display()를 호출할 때마다 if (realImage == null)을 검사하는데 이러한 추가적인 연산이 많아질 경우에는 성능에 영향을 줄 수 있다. 복잡한 시스템에 대한 간단한 인터페이스를 제공하는 패턴이다.
// 서브 시스템 1: CPU
class CPU {
void start() {
System.out.println("CPU가 시작됩니다...");
}
}
// 서브 시스템 2: 메모리
class Memory {
void load() {
System.out.println("메모리가 로드됩니다...");
}
}
// 서브 시스템 3: 하드 드라이브
class HardDrive {
void read() {
System.out.println("하드 드라이브에서 데이터를 읽습니다...");
}
}
// 퍼사드 클래스 (복잡한 서브 시스템을 단순화)
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() {
System.out.println("컴퓨터 부팅을 시작합니다...");
cpu.start();
memory.load();
hardDrive.read();
System.out.println("컴퓨터 부팅 완료!");
}
}
// 클라이언트 코드
public class FacadePatternExample {
public static void main(String[] args) {
// 퍼사드 객체를 사용하여 복잡한 과정 없이 컴퓨터를 쉽게 시작
ComputerFacade computer = new ComputerFacade();
computer.startComputer();
}
}
추상화와 구현을 분리하여 독립적으로 확장할 수 있도록 하는 패턴이다.
// 장치(Device) 인터페이스 (구현 부분)
interface Device {
void turnOn();
void turnOff();
}
// TV 클래스 (Device 인터페이스 구현)
class TV implements Device {
public void turnOn() {
System.out.println("TV 전원이 켜졌습니다.");
}
public void turnOff() {
System.out.println("TV 전원이 꺼졌습니다.");
}
}
// 라디오 클래스 (Device 인터페이스 구현, 추가 가능)
class Radio implements Device {
public void turnOn() {
System.out.println("라디오 전원이 켜졌습니다.");
}
public void turnOff() {
System.out.println("라디오 전원이 꺼졌습니다.");
}
}
// 리모컨 추상 클래스 (추상화 부분)
abstract class RemoteControl {
protected Device device; // 구현체와 연결
public RemoteControl(Device device) {
this.device = device;
}
abstract void togglePower();
}
// 기본 리모컨 (추상화 확장)
class BasicRemote extends RemoteControl {
public BasicRemote(Device device) {
super(device);
}
public void togglePower() {
System.out.println("전원 버튼을 누릅니다.");
device.turnOn();
}
}
// 고급 리모컨 (추상화 확장, 추가 가능)
class AdvancedRemote extends RemoteControl {
public AdvancedRemote(Device device) {
super(device);
}
public void togglePower() {
System.out.println("전원 버튼을 누릅니다.");
device.turnOn();
System.out.println("밝기 조정을 시작합니다.");
}
}
// 클라이언트 코드
public class BridgePatternExample {
public static void main(String[] args) {
// TV와 기본 리모컨 연결
Device tv = new TV();
RemoteControl basicRemote = new BasicRemote(tv);
basicRemote.togglePower();
System.out.println("----------------");
// 라디오와 고급 리모컨 연결
Device radio = new Radio();
RemoteControl advancedRemote = new AdvancedRemote(radio);
advancedRemote.togglePower();
}
}
객체들을 트리 구조로 구성하여 부분-전체 계층을 표현하는 패턴이다.
import java.util.ArrayList;
import java.util.List;
// 공통 인터페이스 (단일 객체와 복합 객체를 동일하게 다룸)
interface Component {
void show();
}
// Leaf(단일 객체)
class Leaf implements Component {
private String name;
public Leaf(String name) {
this.name = name;
}
public void show() {
System.out.println("Leaf: " + name);
}
}
// Composite(복합 객체, 다른 Component들을 포함할 수 있음)
class Composite implements Component {
private String name;
private List<Component> children = new ArrayList<>();
public Composite(String name) {
this.name = name;
}
public void add(Component component) {
children.add(component);
}
public void remove(Component component) {
children.remove(component);
}
public void show() {
System.out.println("Composite: " + name);
for (Component component : children) {
component.show();
}
}
}
// 클라이언트 코드
public class CompositePatternExample {
public static void main(String[] args) {
// 단일 요소(Leaf) 생성
Leaf leaf1 = new Leaf("Leaf 1");
Leaf leaf2 = new Leaf("Leaf 2");
Leaf leaf3 = new Leaf("Leaf 3");
// 루트 노드 (Composite)
Composite root = new Composite("Root");
root.add(leaf1);
root.add(leaf2);
// 서브 트리 (Composite)
Composite subTree = new Composite("SubTree");
subTree.add(leaf3);
// 루트에 서브 트리 추가
root.add(subTree);
// 트리 구조 출력
root.show();
}
}
동일한 객체를 공유하여 메모리 사용을 줄이는 패턴이다.
import java.util.HashMap;
import java.util.Map;
// 플라이웨이트 객체 (공유되는 객체)
class Character {
private final char symbol; // 변경되지 않는 내부 상태 (Intrinsic State)
public Character(char symbol) {
this.symbol = symbol;
}
public void display(String font) { // 외부 상태 (Extrinsic State)
System.out.println("Character: " + symbol + ", Font: " + font);
}
}
// 플라이웨이트 팩토리 (객체를 캐싱하여 재사용)
class CharacterFactory {
private static final Map<java.lang.Character, Character> characters = new HashMap<>();
public static Character getCharacter(char symbol) {
characters.putIfAbsent(symbol, new Character(symbol)); // 이미 존재하는 경우 기존 객체 반환
return characters.get(symbol);
}
}
// 클라이언트 코드
public class FlyweightPatternExample {
public static void main(String[] args) {
Character a1 = CharacterFactory.getCharacter('A');
Character a2 = CharacterFactory.getCharacter('A');
Character b = CharacterFactory.getCharacter('B');
// 동일한 객체를 공유하므로 a1과 a2는 같은 객체
a1.display("Arial");
a2.display("Times New Roman");
b.display("Verdana");
System.out.println("a1과 a2는 같은 객체인가? " + (a1 == a2)); // true (공유된 객체)
}
}