생성 패턴
싱글톤 패턴
클래스의 인스턴스가 하나만 생성하도록 보장하고, 그 인스턴스에 대한 전역적인 접근법을 제공한다.
public class SettingsManager {
// 1. 클래스 로딩 시점에 유일한 인스턴스를 미리 생성
private static final SettingsManager instance = new SettingsManager();
// 2. 외부에서 생성자를 호출할 수 없도록 private으로 선언
private SettingsManager() {
// 설정 로딩 등 초기화 코드
}
// 3. 외부에서 유일한 인스턴스에 접근할 수 있는 통로 제공
public static SettingsManager getInstance() {
return instance;
}
// 기타 메소드
public void printSettings() {
System.out.println("Singleton instance settings.");
}
}
프로토타입 패턴
새로운 객체를 new 키워드를 사용해 생성하는 대신, 기존에 있는 객체를 복사(clone)하여 생성하는 디자인 패턴이다.
목적: 복사할 객체를 파라미터로 전달했을 때 복사할 객체의 상태 정보를 알고 있어 캡슐화 원칙을 어기는 문제가 발생할 수 있다. 그리고 객체를 생성하는 과정에서 DB 조회 혹은 네트워크 통신으로 비용이 발생한다면 미리 만들어진 객체를 통하여 새로운 객체를 생성할 때 사용한다.
public abstract class Shape implements Cloneable {
private String id;
protected String type;
abstract void draw();
public String getType(){
return type;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
// clone 메소드를 오버라이드하여 객체 복사를 구현
@Override
public Object clone() {
Object clone = null;
try {
clone = super.clone(); // 필드 복사, 얕은 복사
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return clone;
}
}
public class Circle extends Shape {
public Circle(){
type = "Circle";
}
@Override
public void draw() {
System.out.println("Inside Circle::draw() method.");
}
}
팩토리 메서드 패턴
객체 생성을 서브클래스에 위임하는 디자인 패턴이다. 부모 클래스에서는 객체 생성에 필요한 인터페이스(메서드)만 정의하고, 어떤 클래스의 인스턴스를 만들지에 대한 결정은 서브클래스가 하도록 만든다.
목적: 구체적인 객체를 결정할 수 없을 때
// 커피숍의 기본 구조
abstract class CoffeeShop {
// 팩토리 메서드: 어떤 커피를 만들지는 서브클래스가 결정
public abstract Coffee createCoffee();
}
class LatteShop extends CoffeeShop {
@Override
public Coffee createCoffee() {
return new Latte(); // 라떼를 생성
}
}
class AmericanoShop extends CoffeeShop {
@Override
public Coffee createCoffee() {
return new Americano(); // 아메리카노를 생성
}
}
추상 팩토리 패턴
서로 관련이 있거나 의존적인 객체들의 묶음(제품군)을 생성하기 위한 인터페이스를 제공하는 디자인 패턴이다.
“테마에 맞는 제품군을 한번에”
interface FurnitureFactory {
Chair createChair();
Sofa createSofa();
}
class ModernFurnitureFactory implements FurnitureFactory {
public Chair createChair() { return new ModernChair(); }
public Sofa createSofa() { return new ModernSofa(); }
}
class VintageFurnitureFactory implements FurnitureFactory {
public Chair createChair() { return new VintageChair(); }
public Sofa createSofa() { return new VintageSofa(); }
}
팩토리 메서드 패턴과 추상 팩토리 패턴의 차이
- 객체 개수의 차이
- 구조의 차이
빌더 패턴
복잡한 객체의 생성과정과 표현 방법을 분리하여, 동일한 생성 절차로도 서로 다른 결과를 만들 수 있게 하는 생성 디자인 패턴이다.
목적
- 생성해야 할 객체의 매개변수가 많고, 대부분이 선택사항일 때
- 객체 생성 시 특정 순서에 따라 단계별로 만들어져야 할 때
- 생성 후에는 변경되지 않는 불변 객체를 만들 때
// Product: 최종적으로 만들어질 객체
class User {
private final String userId; // 필수
private final String password; // 필수
private final String name; // 선택
private final int age; // 선택
private final String address; // 선택
// Builder를 통해서만 객체를 생성할 수 있도록 생성자는 private
private User(UserBuilder builder) {
this.userId = builder.userId;
this.password = builder.password;
this.name = builder.name;
this.age = builder.age;
this.address = builder.address;
}
// Static inner Builder class
public static class UserBuilder {
private final String userId; // 필수
private final String password; // 필수
private String name; // 선택
private int age; // 선택
private String address; // 선택
// 필수 매개변수는 Builder의 생성자에서 받음
public UserBuilder(String userId, String password) {
this.userId = userId;
this.password = password;
}
// 선택 매개변수는 각각의 메서드를 통해 설정하고, Builder 자신을 반환 (Method Chaining)
public UserBuilder name(String name) {
this.name = name;
return this;
}
public UserBuilder age(int age) {
this.age = age;
return this;
}
public UserBuilder address(String address) {
this.address = address;
return this;
}
// build() 메서드가 최종적으로 User 객체를 생성하여 반환
public User build() {
return new User(this);
}
}
@Override
public String toString() {
return "User{" +
"userId='" + userId + '\'' +
", password='" + password + '\'' +
", name='" + name + '\'' +
", age=" + age +
", address='" + address + '\'' +
'}';
}
}
// 실행 코드
public class Main {
public static void main(String[] args) {
User user = new User.UserBuilder("testuser", "1234")
.name("김철수")
.age(30)
// .address("서울") // 주소는 설정하지 않음
.build();
System.out.println(user);
}
}
구조 패턴
구조 패턴은 클래스와 객체를 조합하여 더 큰 구조를 만드는 방법을 다루며, 시스템의 유연성과 효율성을 높이는 데 중점을 둔다.
어댑터 패턴
호환되지 않은 인터페이스를 가진 클래스들이 함께 작동할 수 있도록, 하나의 인터페이스를 다른 인터페이스로 변환해주는 역할을 한다.
목적: 기존 코드를 변경하지 않고 새로운 클래스를 사용하고 싶을 때, 서로 다른 API를 가진 라이브러리를 통합해야 할 때 사용한다.
// 사용할 수 없는 구형 시스템
interface OldSystem {
void legacyRequest();
}
class OldSystemImpl implements OldSystem {
public void legacyRequest() { System.out.println("구형 시스템 호출"); }
}
// 우리가 사용하려는 새로운 시스템 인터페이스
interface NewSystem {
void newRequest();
}
// 어댑터: OldSystem 인터페이스를 NewSystem 인터페이스처럼 사용하게 해줌
class Adapter implements NewSystem {
private OldSystem oldSystem;
public Adapter(OldSystem oldSystem) {
this.oldSystem = oldSystem;
}
@Override
public void newRequest() {
System.out.print("어댑터 경유 -> ");
oldSystem.legacyRequest();
}
}
// 실행
public class Main {
public static void main(String[] args) {
OldSystem oldSystem = new OldSystemImpl();
NewSystem client = new Adapter(oldSystem);
client.newRequest(); // 클라이언트는 NewSystem 인터페이스만 사용
}
}
브릿지 패턴
추상화 부분과 구현 부분을 분리하여 각각 독립적으로 확장할 수 있게 한다. 상속 대신 합성을 이용하여 기능과 구현을 연결한다.
추상화: 무엇을 하는지(코드 예시에서는 draw)
구현: 어떻게 하는지(코드 예시에서는 applyColor)
목적: 기능과 구현을 분리하여 독립적으로 변경해야 할 때
// 구현 인터페이스 (Implementation)
interface Color {
String applyColor();
}
class Red implements Color {
public String applyColor() { return "빨간색"; }
}
class Blue implements Color {
public String applyColor() { return "파란색"; }
}
// 추상화 (Abstraction)
abstract class Shape {
protected Color color; // 구현을 참조 (브리지)
public Shape(Color color) { this.color = color; }
abstract public void draw();
}
class Circle extends Shape {
public Circle(Color color) { super(color); }
public void draw() { System.out.println(color.applyColor() + " 원"); }
}
// 실행
public class Main {
public static void main(String[] args) {
Shape redCircle = new Circle(new Red());
redCircle.draw();
}
}
복합체 패턴
개별 객체와 객체의 집합을 동일한 방식으로 다룰 수 있게 한다. 객체들을 트리 구조로 구성하여 전체-부분 관계를표현한다.
// 공통 인터페이스 (Component)
interface Graphic {
void draw();
}
// 개별 객체 (Leaf)
class Dot implements Graphic {
public void draw() { System.out.println("점 그리기"); }
}
// 복합 객체 (Composite)
class CompoundGraphic implements Graphic {
private List<Graphic> children = new ArrayList<>();
public void add(Graphic child) { children.add(child); }
public void draw() {
System.out.println("복합 그래픽 시작");
for (Graphic child : children) {
child.draw(); // 자식들에게 재귀적으로 draw 호출
}
}
}
// 실행
public class Main {
public static void main(String[] args) {
CompoundGraphic all = new CompoundGraphic();
all.add(new Dot());
all.add(new Dot());
all.draw();
}
}
데코레이터 패턴
객체에 동적으로 새로운 책임(기능)을 추가할 수 있게 한다. 상속을 사용하지 않고도 객체의 기능을 확장할 수 있다.
목적: 객체의 일부 기능만 확장하고 싶을 때, 런타임에 기능을 추가하거나 제거해야 할 때 사용한다.
// 공통 인터페이스 (Component)
interface Coffee {
String getDescription();
double getCost();
}
// 기본 객체 (Concrete Component)
class SimpleCoffee implements Coffee {
public String getDescription() { return "커피"; }
public double getCost() { return 1.0; }
}
// 데코레이터 추상 클래스
abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) { this.decoratedCoffee = coffee; }
public String getDescription() { return decoratedCoffee.getDescription(); }
public double getCost() { return decoratedCoffee.getCost(); }
}
// 구체적인 데코레이터
class WithMilk extends CoffeeDecorator {
public WithMilk(Coffee coffee) { super(coffee); }
public String getDescription() { return super.getDescription() + ", 우유 추가"; }
public double getCost() { return super.getCost() + 0.5; }
}
// 실행
public class Main {
public static void main(String[] args) {
Coffee coffee = new SimpleCoffee();
coffee = new WithMilk(coffee); // 우유 기능 추가
System.out.println(coffee.getDescription() + " $" + coffee.getCost());
}
}
퍼사드 패턴
복잡한 서브시스템에 대해 단순화된 인터페이스를 제공한다. 서브시스템의 복잡한 내부 구조를 숨기고, 클라이언트가 쉽게 사용할 수 있는 창구 역할을 한다.
// 복잡한 서브시스템
class CPU { void process() { System.out.println("CPU 처리"); } }
class Memory { void load() { System.out.println("메모리 로드"); } }
class HardDrive { void read() { System.out.println("하드 드라이브 읽기"); } }
// 퍼사드
class ComputerFacade {
private CPU cpu = new CPU();
private Memory memory = new Memory();
private HardDrive hardDrive = new HardDrive();
// 단순화된 인터페이스 제공
public void start() {
hardDrive.read();
memory.load();
cpu.process();
System.out.println("컴퓨터 부팅 완료");
}
}
// 실행
public class Main {
public static void main(String[] args) {
ComputerFacade computer = new ComputerFacade();
computer.start(); // 복잡한 과정 대신 start()만 호출
}
}
플라이웨이트 패턴
다수의 유사한 객체를 생성할 때, 공유를 통해 메모리 사용량을 최소화한다. 객체의 상태를 공유 가능한 내재적 상태와 공유 불가능한 외재적 상태로 분리한다.
// 플라이웨이트 객체 (공유 대상)
class TreeType {
private String name; // 내재적 상태 (공유 가능)
public TreeType(String name) { this.name = name; }
public void draw(int x, int y) { // 외재적 상태 (공유 불가)
System.out.println(name + " 나무를 (" + x + ", " + y + ")에 그리기");
}
}
// 플라이웨이트 팩토리
class TreeFactory {
private static Map<String, TreeType> treeTypes = new HashMap<>();
public static TreeType getTreeType(String name) {
if (!treeTypes.containsKey(name)) {
treeTypes.put(name, new TreeType(name));
}
return treeTypes.get(name);
}
}
// 실행
public class Main {
public static void main(String[] args) {
TreeType type1 = TreeFactory.getTreeType("소나무");
type1.draw(10, 20);
TreeType type2 = TreeFactory.getTreeType("소나무"); // 기존 객체 재사용
type2.draw(30, 50);
}
}
프록시 패턴
어떤 객체에 대한 접근을 제어하기 위해 대리인(프록시) 객체를 제공한다. 프록시는 실제 객체에 대한 접근을 관리하며, 접근 직후에 추가적인 로직을 수행할 수 있다.
프록시 패턴 vs 데코레이터 패턴
두 패턴 사이의 구조적인 유사성 때문에 같은 패턴이라고 생각할 지 모르지만 둘의 차이는 존재한다.
두 패턴의 차이는 목적에 존재한다.
- 프록시 패턴: 객체에 대한 접근을 제어하고 관리
- 데코레이터 패턴: 새로운 기능(책임)을 동적으로 추가
// 공통 인터페이스 (Subject)
interface Image {
void display();
}
// 실제 객체 (Real Subject)
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 Main {
public static void main(String[] args) {
Image image = new ProxyImage("photo.jpg");
image.display(); // 이 시점에 RealImage 로딩 및 표시
image.display(); // 이미 로딩되었으므로 바로 표시
}
}
행동 패턴
행동 패턴은 객체들이 상호작용하는 방식과 책임을 분배하는 방법을 다룬다. 객체 간의 결합도를 낮추고 유연한 협력 관계를 만드는 데 중점을 둔다.
책임 연쇄 패턴
요청을 보내는 객체와 요청을 처리하는 개체를 분리하고, 여러 가지 객체를 사슬처럼 연결하여 요청이 해결될 떄까지 사슬을 따라 전달하게 하는 패턴이다.
목적: 요청 처리 객체와 요청 객체 간의 결합을 느슨하게 하고, 요청 처리 순서를 동적으로 변경할 수 있다.
// 클라이언트 코드
void requestApproval(int amount) {
if (amount <= 1000000) {
teamLeader.process(amount); // 클라이언트가 직접 팀장에게 보냄
} else if (amount <= 5000000) {
manager.process(amount); // 클라이언트가 직접 부장에게 보냄
} else {
// ...
}
}
// 처리기(결재자)의 공통 인터페이스
interface Approver {
void setNext(Approver next); // 다음 결재자 설정
void process(int amount); // 결재 요청 처리
}
// 팀장
class TeamLeader implements Approver {
private Approver next;
@Override
public void setNext(Approver next) {
this.next = next;
}
@Override
public void process(int amount) {
if (amount <= 1000000) { // 100만원 이하는 팀장이 처리
System.out.println("팀장이 승인했습니다.");
} else if (next != null) {
System.out.println("팀장이 처리할 수 없어 부장에게 넘깁니다.");
next.process(amount); // 처리 못하면 다음 사람에게 넘김
}
}
}
// 부장
class DepartmentManager implements Approver {
private Approver next;
@Override
public void setNext(Approver next) {
this.next = next;
}
@Override
public void process(int amount) {
if (amount <= 5000000) { // 500만원 이하는 부장이 처리
System.out.println("부장이 승인했습니다.");
} else if (next != null) {
System.out.println("부장이 처리할 수 없어 사장에게 넘깁니다.");
next.process(amount);
} else {
System.out.println("결재할 수 있는 사람이 없습니다.");
}
}
}
// 실행 코드
public class Main {
public static void main(String[] args) {
// 1. 결재 라인(사슬) 생성
Approver teamLeader = new TeamLeader();
Approver manager = new DepartmentManager();
teamLeader.setNext(manager);
// 2. 50만원 요청 (팀장이 처리)
System.out.println("50만원 경비 요청:");
teamLeader.process(500000);
System.out.println("\n---------------------------\n");
// 3. 300만원 요청 (팀장 -> 부장)
System.out.println("300만원 경비 요청:");
teamLeader.process(3000000);
}
}
커맨드 패턴
요청 자체를 객체로 캡슐화하여, 요청이 필요한 시점에 실행하거나, 취소(Undo), 로깅, 큐에 저장하는 등의 작업을 가능하게 한다.
목적: 요청을 보내는 객체와 요청을 수행하는 객체를 분리하여 서로 의존하지 않게 한다.
// 작업을 수행할 객체들
class Light {
public void on() { System.out.println("불이 켜졌습니다."); }
public void off() { System.out.println("불이 꺼졌습니다."); }
}
class Fan {
public void start() { System.out.println("선풍기가 켜졌습니다."); }
public void stop() { System.out.println("선풍기가 꺼졌습니다."); }
}
// 요청을 보내는 리모컨
class SimpleRemoteControl {
private Light light = new Light();
private Fan fan = new Fan();
// 각 버튼이 특정 기기와 그 기기의 메서드에 직접 의존
public void lightOnButtonClick() {
light.on();
}
public void fanStartButtonClick() {
fan.start();
}
}
public class Main {
public static void main(String[] args) {
SimpleRemoteControl remote = new SimpleRemoteControl();
remote.lightOnButtonClick();
remote.fanStartButtonClick();
}
}
// --- 1. 작업을 수행할 객체들 (Receiver) ---
class Light {
public void on() { System.out.println("불이 켜졌습니다."); }
}
class Fan {
public void start() { System.out.println("선풍기가 켜졌습니다."); }
}
// --- 2. 모든 커맨드의 공통 인터페이스 (Command) ---
interface Command {
void execute();
}
// --- 3. 구체적인 커맨드 객체들 (Concrete Command) ---
class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) { this.light = light; }
@Override
public void execute() {
light.on();
}
}
class FanStartCommand implements Command {
private Fan fan;
public FanStartCommand(Fan fan) { this.fan = fan; }
@Override
public void execute() {
fan.start();
}
}
// --- 4. 요청을 보내는 리모컨 (Invoker) ---
class SimpleRemoteControl {
private Command slot; // 어떤 커맨드든 담을 수 있는 슬롯
public void setCommand(Command command) {
this.slot = command;
}
public void buttonWasPressed() {
slot.execute(); // 커맨드 실행
}
}
// --- 5. 실행 ---
public class Main {
public static void main(String[] args) {
SimpleRemoteControl remote = new SimpleRemoteControl();
// 1. 불 켜기 설정
Light light = new Light();
Command lightOn = new LightOnCommand(light);
remote.setCommand(lightOn);
remote.buttonWasPressed();
// 2. 선풍기 켜기 설정
Fan fan = new Fan();
Command fanStart = new FanStartCommand(fan);
remote.setCommand(fanStart);
remote.buttonWasPressed();
}
}
개선점
- 낮은 결합도
- 높은 확장성: 다른 기능이 추가되면 객체와 커맨드만 추가하면 된다.
- 유연성: 리모컨의 버튼 기능을 런타임에 바꿀 수 있다.
반복자 패턴
컬렉션의 내부 구조를 노출하지 않고 순차적으로 접근할 수 있는 방법을 제공한다.
목적: 컬렉션의 구현 방식과 순회 방식을 분리하여, 어떤 종류의 컬렉션이든 동일한 방식으로 순회할 수 있도록 한다.
class Book {
private String name;
public Book(String name) { this.name = name; }
public String getName() { return name; }
}
// 책을 보관하는 책꽂이 클래스
class BookShelf {
private Book[] books;
private int last = 0;
public BookShelf(int maxSize) {
this.books = new Book[maxSize];
}
public Book getBookAt(int index) {
return books[index];
}
public void addBook(Book book) {
this.books[last] = book;
last++;
}
public int getLength() {
return last;
}
}
// 클라이언트 코드
public class Main {
public static void main(String[] args) {
BookShelf bookShelf = new BookShelf(4);
bookShelf.addBook(new Book("디자인 패턴"));
bookShelf.addBook(new Book("자바 기초"));
// 클라이언트가 책꽂이의 내부 구현 방식(배열, getLength)을 직접 알아야 함
for (int i = 0; i < bookShelf.getLength(); i++) {
System.out.println(bookShelf.getBookAt(i).getName());
}
}
}
import java.util.Iterator;
import java.util.ArrayList;
class Book { /* 이전과 동일 */
private String name;
public Book(String name) { this.name = name; }
public String getName() { return name; }
}
// Iterable 인터페이스를 구현하여 "순회 가능한 객체"임을 명시
class BookShelf implements Iterable<Book> {
// 내부 구현을 ArrayList로 변경해도 클라이언트 코드는 영향을 받지 않음
private ArrayList<Book> books;
public BookShelf() {
this.books = new ArrayList<>();
}
public void addBook(Book book) {
this.books.add(book);
}
// Iterator를 반환하는 메서드를 구현
@Override
public Iterator<Book> iterator() {
return books.iterator(); // ArrayList가 기본으로 제공하는 iterator를 반환
}
}
// 클라이언트 코드
public class Main {
public static void main(String[] args) {
BookShelf bookShelf = new BookShelf();
bookShelf.addBook(new Book("디자인 패턴"));
bookShelf.addBook(new Book("자바 기초"));
// 1. Iterator를 이용한 순회
Iterator<Book> it = bookShelf.iterator();
while (it.hasNext()) {
System.out.println(it.next().getName());
}
System.out.println("--------------------");
// 2. 향상된 for문 사용 (Iterable을 구현했기 때문에 가능)
// 클라이언트는 내부 구조를 전혀 몰라도 순회 가능
for (Book book : bookShelf) {
System.out.println(book.getName());
}
}
}
개선점
- 낮은 결합도: 클라이언트는 BookShelf의 내부 구현이 무엇인지 전혀 모른다.
- 높은 유연성 및 유지보수성: 컬렉션을 변경하더라도 iterator 만 변경하면 된다.
- 코드 간결성: Iterable 인터페이스를 구현하면 향상된 for 문을 사용할 수 있어 클라이언트 코드가 깔끔해진다.
중재자 패턴
여러 객체 간의 복잡한 상호작용(M:N)을 하나의 중재자 객체에 캡슐화하여 관리한다.
목적: 객체간의 직접적인 통신을 막아 결합도를 낮추고, 상호작용 로직을 한 곳에서 중앙 관리하여 유지보수를 용이하게 한다.
// 각 컴포넌트가 다른 모든 컴포넌트를 알아야 하는 상황
class Button {
private TextField textField;
private Checkbox checkbox;
public void setTextField(TextField textField) { this.textField = textField; }
public void setCheckbox(Checkbox checkbox) { this.checkbox = checkbox; }
public void click() {
System.out.println("버튼 클릭됨");
// 버튼이 클릭되면 다른 컴포넌트들의 상태를 직접 변경
textField.setText("");
checkbox.setChecked(false);
}
}
class TextField {
private String text = "";
public void setText(String text) { this.text = text; }
public String getText() { return text; }
}
class Checkbox {
private boolean checked = false;
public void setChecked(boolean checked) { this.checked = checked; }
public boolean isChecked() { return checked; }
}
// 클라이언트가 모든 객체 간의 의존성을 직접 설정해야 함
public class Main {
public static void main(String[] args) {
Button button = new Button();
TextField textField = new TextField();
Checkbox checkbox = new Checkbox();
// 모든 객체들이 서로를 직접 참조하도록 설정 (스파게티 코드)
button.setTextField(textField);
button.setCheckbox(checkbox);
button.click();
}
}
// --- 1. 중재자 인터페이스 ---
interface Mediator {
void notify(Component sender, String event);
}
// --- 2. 동료(Colleague) 클래스들 ---
// 모든 컴포넌트의 부모 클래스
abstract class Component {
protected Mediator mediator;
public Component(Mediator mediator) { this.mediator = mediator; }
}
class Button extends Component {
public Button(Mediator mediator) { super(mediator); }
public void click() {
System.out.println("버튼 클릭됨");
mediator.notify(this, "click"); // 변경 사항을 중재자에게 알림
}
}
class TextField extends Component {
private String text = "";
public TextField(Mediator mediator) { super(mediator); }
public void setText(String text) {
this.text = text;
System.out.println("텍스트 필드 초기화됨");
}
}
class Checkbox extends Component {
private boolean checked = false;
public Checkbox(Mediator mediator) { super(mediator); }
public void setChecked(boolean checked) {
this.checked = checked;
System.out.println("체크박스 해제됨");
}
}
// --- 3. 구체적인 중재자 ---
class DialogMediator implements Mediator {
private Button button;
private TextField textField;
private Checkbox checkbox;
// 중재자는 모든 컴포넌트를 알고 있음
public void setButton(Button button) { this.button = button; }
public void setTextField(TextField textField) { this.textField = textField; }
public void setCheckbox(Checkbox checkbox) { this.checkbox = checkbox; }
// 모든 상호작용 로직이 여기에 중앙 집중화됨
@Override
public void notify(Component sender, String event) {
if (sender == button && event.equals("click")) {
textField.setText("");
checkbox.setChecked(false);
}
}
}
// --- 4. 실행 ---
public class Main {
public static void main(String[] args) {
DialogMediator mediator = new DialogMediator();
Button button = new Button(mediator);
TextField textField = new TextField(mediator);
Checkbox checkbox = new Checkbox(mediator);
// 중재자에게 컴포넌트들을 등록
mediator.setButton(button);
mediator.setTextField(textField);
mediator.setCheckbox(checkbox);
button.click();
}
}
개선점
- 낮은 결합도: Button, TextField, Checkbox는 서로 전혀 모른다.오직 Mediator와만 통신한다.
- 중앙 집중화된 제어: 모든 상호작용 로직이 DialogMediator 안에 모여있어 코드를 이해하고 수정하기 훨씬 쉬워졌다.
- 높은 재사용성: 개별 컴포넌트들은 다른 중재자와도 쉽게 조합하여 재사용할 수 있다.
메멘토 패턴
객체의 내부 상태를 외부화하여 저장해두고, 나중에 이 상태로 다시 복원할 수 있다.
목적: 객체의 캡슐화를 깨지 않으면서 되돌리기(Undo) 와 같은 상태 복원 기능을 수행할 수 있다.
// 글의 상태를 저장할 객체
class Editor {
// 내부 상태가 public으로 외부에 그대로 노출됨 (캡슐화 위반)
public String content;
public void type(String words) {
this.content = (this.content == null ? "" : this.content) + words;
}
}
// Editor의 상태를 저장하고 관리하는 객체
class History {
// Editor의 상태를 저장하기 위해 Editor의 필드와 동일한 필드를 가져야 함
private String backupContent;
public void save(Editor editor) {
// Editor의 내부 상태에 직접 접근하여 저장
this.backupContent = editor.content;
}
public void undo(Editor editor) {
// Editor의 내부 상태를 직접 변경하여 복원
editor.content = this.backupContent;
}
}
public class Main {
public static void main(String[] args) {
Editor editor = new Editor();
History history = new History();
editor.type("첫 번째 문장. ");
history.save(editor); // 현재 상태 저장
editor.type("두 번째 문장. ");
System.out.println("현재 내용: " + editor.content);
history.undo(editor); // 되돌리기
System.out.println("되돌린 후 내용: " + editor.content);
}
}
// --- 2. 메멘토 (Memento): Originator의 상태를 저장하는 객체 ---
// Editor의 상태를 저장하지만, 외부에서는 상태에 접근할 수 없음 (보통 private inner class로 구현)
class EditorState {
private final String content; // 불변
public EditorState(String content) {
this.content = content;
}
public String getContent() {
return content;
}
}
// --- 1. 오리지네이터 (Originator): 상태를 저장하고 복원할 주체 ---
class Editor {
private String content; // 내부 상태는 private으로 캡슐화
public void type(String words) {
this.content = (this.content == null ? "" : this.content) + words;
}
public String getContent() {
return content;
}
// 자신의 현재 상태를 Memento 객체로 만들어 반환
public EditorState save() {
return new EditorState(this.content);
}
// Memento 객체를 받아 자신의 상태를 복원
public void restore(EditorState state) {
this.content = state.getContent();
}
}
// --- 3. 케어테이커 (Caretaker): Memento 객체를 보관하고 관리 ---
class History {
private EditorState savedState;
public void save(EditorState state) {
this.savedState = state;
}
public EditorState getSavedState() {
return this.savedState;
}
}
// --- 4. 실행 ---
public class Main {
public static void main(String[] args) {
Editor editor = new Editor();
History history = new History();
editor.type("첫 번째 문장. ");
history.save(editor.save()); // Editor가 직접 생성한 상태(Memento)를 저장
editor.type("두 번째 문장. ");
System.out.println("현재 내용: " + editor.getContent());
editor.restore(history.getSavedState()); // 저장된 상태(Memento)로 복원
System.out.println("되돌린 후 내용: " + editor.getContent());
}
}
옵저버 패턴
한 객체의 상태가 변하면, 그 객체의 의존하는 다른 객체들에게 자동으로 알림이 가고 업데이트되도록 하는 일대다(1:N) 의존 관계를 정의한다.
목적: 객체 간의 느슨한 결합을 유지하면서 상태 변화에 따른 자동 업데이트를 구현한다.
// 날씨 정보를 표시하는 디스플레이들
class CurrentConditionsDisplay {
public void update(float temperature, float humidity) {
System.out.println("현재 날씨: 온도 " + temperature + "도, 습도 " + humidity + "%");
}
}
class StatisticsDisplay {
public void showStats(float temperature) {
System.out.println("통계: 평균 온도 " + temperature + "도");
}
}
// 날씨 정보를 관리하는 주체
class WeatherStation {
private float temperature;
private float humidity;
// WeatherStation이 모든 디스플레이 종류를 직접 알고 있어야 함
private CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay();
private StatisticsDisplay statsDisplay = new StatisticsDisplay();
public void setMeasurements(float temperature, float humidity) {
this.temperature = temperature;
this.humidity = humidity;
// 데이터가 변경될 때마다 각 디스플레이의 메서드를 직접 호출
currentDisplay.update(temperature, humidity);
statsDisplay.showStats(temperature);
}
}
public class Main {
public static void main(String[] args) {
WeatherStation weatherStation = new WeatherStation();
weatherStation.setMeasurements(25.0f, 65.0f);
}
}
import java.util.ArrayList;
import java.util.List;
// --- 1. 옵서버 인터페이스 ---
interface Observer {
void update(float temperature, float humidity);
}
// --- 2. 서브젝트 인터페이스 ---
interface Subject {
void registerObserver(Observer o);
void removeObserver(Observer o);
void notifyObservers();
}
// --- 3. 구체적인 서브젝트 ---
class WeatherStation implements Subject {
private List<Observer> observers; // Observer 인터페이스에만 의존
private float temperature;
private float humidity;
public WeatherStation() {
observers = new ArrayList<>();
}
public void setMeasurements(float temperature, float humidity) {
this.temperature = temperature;
this.humidity = humidity;
notifyObservers(); // 상태 변경 시 옵서버들에게 알림
}
@Override
public void registerObserver(Observer o) { observers.add(o); }
@Override
public void removeObserver(Observer o) { observers.remove(o); }
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(temperature, humidity);
}
}
}
// --- 4. 구체적인 옵서버들 ---
class CurrentConditionsDisplay implements Observer {
@Override
public void update(float temperature, float humidity) {
System.out.println("현재 날씨: 온도 " + temperature + "도, 습도 " + humidity + "%");
}
}
class StatisticsDisplay implements Observer {
@Override
public void update(float temperature, float humidity) {
System.out.println("통계: 평균 온도 " + temperature + "도");
}
}
// --- 5. 실행 ---
public class Main {
public static void main(String[] args) {
WeatherStation weatherStation = new WeatherStation();
// 옵서버(디스플레이)들을 서브젝트(WeatherStation)에 등록
CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay();
weatherStation.registerObserver(currentDisplay);
StatisticsDisplay statsDisplay = new StatisticsDisplay();
weatherStation.registerObserver(statsDisplay);
// 날씨 정보 변경 -> 등록된 모든 옵서버에게 자동으로 알림이 감
weatherStation.setMeasurements(25.0f, 65.0f);
}
}
- 낮은 결합도: WeatherStation은 이제 Observer 인터페이스에만 의존하며, CurrentConditionsDisplay와 같은 구체적인 클래스의 존재를 전혀 모른다.
- 높은 확장성: 새로운 디스플레이를 추가하고 싶다면, Observer 인터페이스를 구현하는 클래스를 만들기만 하면 된다.
- 유연성: 런타임에 옵저버를 동적으로 추가하거나 제거할 수 있어 유연한 시스템을 만들 수 있다.
상태 패턴
객체의 내부 상태가 변경됨에 따라 객체의 행동이 바뀌도록 한다.객체가 마치 자신의 클래스를 바꾸는 것처럼 보인다.
목적: if/else나 switch 문으로 상태에 따라 분기하는 복잡한 조건문을 상태 객체로 대체하여 코드를 단순화한다.
class VendingMachine {
// 0: 동전 없음, 1: 동전 있음, 2: 매진
private int state = 0;
public VendingMachine() {
this.state = 0;
}
public void insertCoin() {
if (state == 0) {
System.out.println("동전이 투입되었습니다.");
state = 1; // 상태 변경
} else if (state == 1) {
System.out.println("이미 동전이 있습니다.");
} else if (state == 2) {
System.out.println("매진입니다.");
}
}
public void selectItem() {
if (state == 0) {
System.out.println("동전을 먼저 넣어주세요.");
} else if (state == 1) {
System.out.println("상품이 나왔습니다.");
state = 0; // 상태 변경
} else if (state == 2) {
System.out.println("매진입니다.");
}
}
}
public class Main {
public static void main(String[] args) {
VendingMachine vm = new VendingMachine();
vm.selectItem(); // 동전을 먼저 넣어주세요.
vm.insertCoin(); // 동전이 투입되었습니다.
vm.selectItem(); // 상품이 나왔습니다.
}
}
// --- 1. 상태 인터페이스 ---
interface State {
void insertCoin(VendingMachine vm);
void selectItem(VendingMachine vm);
}
// --- 2. 구체적인 상태 클래스들 ---
class NoCoinState implements State {
@Override
public void insertCoin(VendingMachine vm) {
System.out.println("동전이 투입되었습니다.");
vm.setState(new HasCoinState()); // 상태를 '동전 있음'으로 변경
}
@Override
public void selectItem(VendingMachine vm) {
System.out.println("동전을 먼저 넣어주세요.");
}
}
class HasCoinState implements State {
@Override
public void insertCoin(VendingMachine vm) {
System.out.println("이미 동전이 있습니다.");
}
@Override
public void selectItem(VendingMachine vm) {
System.out.println("상품이 나왔습니다.");
vm.setState(new NoCoinState()); // 상태를 '동전 없음'으로 변경
}
}
// --- 3. 컨텍스트 (Context) 클래스 ---
class VendingMachine {
private State currentState; // 현재 상태를 나타내는 객체
public VendingMachine() {
// 초기 상태는 '동전 없음'
this.currentState = new NoCoinState();
}
// 상태 변경을 위한 메서드
public void setState(State state) {
this.currentState = state;
}
// 요청을 현재 상태 객체에 위임
public void insertCoin() {
currentState.insertCoin(this);
}
public void selectItem() {
currentState.selectItem(this);
}
}
// --- 4. 실행 ---
public class Main {
public static void main(String[] args) {
VendingMachine vm = new VendingMachine();
vm.selectItem();
vm.insertCoin();
vm.selectItem();
}
}
전략 패턴
알고리즘군을 정의하고 각각을 캡슐화하여 서로 교체할 수 있도록 한다.
목적: 클라이언트로부터 알고리즘을 분리하여 독립적으로 변경할 수 있다. if/else 분기 대신 사용할 수 있다.
class Robot {
private String name;
private String moveMode; // "walking" 또는 "flying"
public Robot(String name) {
this.name = name;
this.moveMode = "walking"; // 기본은 걷기
}
public void setMoveMode(String mode) {
this.moveMode = mode;
}
// 이동 방식이 추가될 때마다 이 메서드에 if-else가 계속 늘어남
public void move() {
if (moveMode.equals("walking")) {
System.out.println(name + ": 걷는 중...");
} else if (moveMode.equals("flying")) {
System.out.println(name + ": 나는 중...");
}
}
}
public class Main {
public static void main(String[] args) {
Robot robot = new Robot("태권브이");
robot.move();
robot.setMoveMode("flying");
robot.move();
}
}
// --- 1. 전략(Strategy) 인터페이스 ---
interface MovingStrategy {
void move();
}
// --- 2. 구체적인 전략(Concrete Strategy) 클래스들 ---
class WalkingStrategy implements MovingStrategy {
@Override
public void move() {
System.out.println("걷는 중...");
}
}
class FlyingStrategy implements MovingStrategy {
@Override
public void move() {
System.out.println("나는 중...");
}
}
// --- 3. 컨텍스트(Context) 클래스 ---
class Robot {
private String name;
private MovingStrategy movingStrategy; // 전략 인터페이스에만 의존
public Robot(String name) {
this.name = name;
}
// 외부에서 전략을 설정(주입)
public void setMovingStrategy(MovingStrategy movingStrategy) {
this.movingStrategy = movingStrategy;
}
// 현재 설정된 전략을 실행
public void move() {
System.out.print(name + ": ");
movingStrategy.move();
}
}
// --- 4. 실행 ---
public class Main {
public static void main(String[] args) {
Robot robot = new Robot("태권브이");
robot.setMovingStrategy(new WalkingStrategy());
robot.move();
robot.setMovingStrategy(new FlyingStrategy());
robot.move();
}
}
상태 패턴과 전략 패턴의 차이는?
둘의 차이는 “누가” 행동을 “왜” 취하는지에 달려있다.
- 상태 패턴: 객체 내부의 상태가 변경됨에 따라 행동이 달라짐
- 전략 패턴: 클라이언트가 필요에 따라 행동(전략)을 바꿈
템플릿 메서드 패턴
알고리즘의 골격(뼈대)를 상위에 정의하고, 일부 단계를 서브클래스에서 구현하도록 한다.
목적: 알고리즘의 구조는 그대로 유지하면서 특정 단계만 서브클래스에서 변경할 수 있도록 하여 코드 중복을 줄인다.
class Coffee {
// 커피 만드는 법
void prepareRecipe() {
boilWater();
brewCoffeeGrinds();
pourInCup();
addSugarAndMilk();
}
private void boilWater() { System.out.println("물 끓이기"); }
private void brewCoffeeGrinds() { System.out.println("필터로 커피 내리기"); }
private void pourInCup() { System.out.println("컵에 따르기"); }
private void addSugarAndMilk() { System.out.println("설탕과 우유 추가하기"); }
}
class Tea {
// 차 만드는 법
void prepareRecipe() {
boilWater();
steepTeaBag();
pourInCup();
addLemon();
}
// Coffee 클래스와 중복되는 메서드들
private void boilWater() { System.out.println("물 끓이기"); }
private void pourInCup() { System.out.println("컵에 따르기"); }
private void steepTeaBag() { System.out.println("티백 우려내기"); }
private void addLemon() { System.out.println("레몬 추가하기"); }
}
public class Main {
public static void main(String[] args) {
Coffee coffee = new Coffee();
System.out.println("--- 커피 만들기 ---");
coffee.prepareRecipe();
Tea tea = new Tea();
System.out.println("\n--- 차 만들기 ---");
tea.prepareRecipe();
}
}
// --- 1. 알고리즘의 뼈대를 정의하는 추상 클래스 ---
abstract class CaffeineBeverage {
// 이 메서드가 바로 '템플릿 메서드'
// 알고리즘의 순서는 바뀌지 않도록 final로 선언
final void prepareRecipe() {
boilWater(); // 공통 단계
brew(); // 자식 클래스가 구현할 단계
pourInCup(); // 공통 단계
addCondiments(); // 자식 클래스가 구현할 단계
}
// 자식 클래스에서 오버라이딩 할 부분 (추상 메서드)
abstract void brew();
abstract void addCondiments();
// 모든 자식 클래스가 공통으로 사용하는 부분
private void boilWater() {
System.out.println("물 끓이기");
}
private void pourInCup() {
System.out.println("컵에 따르기");
}
}
// --- 2. 템플릿의 특정 단계를 구현하는 자식 클래스들 ---
class Coffee extends CaffeineBeverage {
@Override
void brew() {
System.out.println("필터로 커피 내리기");
}
@Override
void addCondiments() {
System.out.println("설탕과 우유 추가하기");
}
}
class Tea extends CaffeineBeverage {
@Override
void brew() {
System.out.println("티백 우려내기");
}
@Override
void addCondiments() {
System.out.println("레몬 추가하기");
}
}
// --- 3. 실행 ---
public class Main {
public static void main(String[] args) {
System.out.println("--- 커피 만들기 ---");
CaffeineBeverage coffee = new Coffee();
coffee.prepareRecipe();
System.out.println("\n--- 차 만들기 ---");
CaffeineBeverage tea = new Tea();
tea.prepareRecipe();
}
}
비지터 패턴(Visitor 패턴)
객체 구조의 요소에 대한 처리(연산)를 해당 요소의 클래스에서 분리하여 방문자(Visitor) 클래스에서 정의한다.
목적: 기존 객체 구조를 변경하지 않고 새로운 기능을 추가할 수 있다.
// 각 상품 클래스
class Book {
private int price;
public Book(int price) { this.price = price; }
// 기능이 추가될 때마다 이 클래스에 메서드를 계속 추가해야 함
public int getPrice() { return price; }
public void pack() { System.out.println("책을 포장합니다."); }
}
class Fruit {
private int pricePerKg;
private int weight;
public Fruit(int pricePerKg, int weight) { this.pricePerKg = pricePerKg; this.weight = weight; }
public int getPrice() { return pricePerKg * weight; }
public void pack() { System.out.println("과일을 포장합니다."); }
}
// 클라이언트 코드
public class Main {
public static void main(String[] args) {
Object[] items = { new Book(10000), new Fruit(2000, 3) };
int total = 0;
for (Object item : items) {
if (item instanceof Book) {
total += ((Book) item).getPrice();
((Book) item).pack();
} else if (item instanceof Fruit) {
total += ((Fruit) item).getPrice();
((Fruit) item).pack();
}
}
System.out.println("총 가격: " + total);
}
}
// --- 1. 방문자(Visitor) 인터페이스 ---
// 각 상품 종류별로 방문 메서드를 정의
interface ItemVisitor {
void visit(Book book);
void visit(Fruit fruit);
}
// --- 2. 방문을 받을 요소(Element) 인터페이스 ---
interface ItemElement {
void accept(ItemVisitor visitor);
}
// --- 3. 구체적인 요소 클래스들 ---
class Book implements ItemElement {
public int price;
public Book(int price) { this.price = price; }
@Override
public void accept(ItemVisitor visitor) {
visitor.visit(this); // 방문자를 받아들여 자신을 방문하게 함
}
}
class Fruit implements ItemElement {
public int pricePerKg;
public int weight;
public Fruit(int pricePerKg, int weight) { this.pricePerKg = pricePerKg; this.weight = weight; }
@Override
public void accept(ItemVisitor visitor) {
visitor.visit(this);
}
}
// --- 4. 구체적인 방문자 클래스들 (기능별로 분리) ---
class ShoppingCartVisitor implements ItemVisitor {
private int total = 0;
public int getTotalPrice() { return total; }
@Override
public void visit(Book book) {
total += book.price;
System.out.println("책을 카트에 담았습니다.");
}
@Override
public void visit(Fruit fruit) {
total += fruit.pricePerKg * fruit.weight;
System.out.println("과일을 카트에 담았습니다.");
}
}
// --- 5. 실행 ---
public class Main {
public static void main(String[] args) {
ItemElement[] items = { new Book(10000), new Fruit(2000, 3) };
ShoppingCartVisitor cartVisitor = new ShoppingCartVisitor();
for (ItemElement item : items) {
item.accept(cartVisitor); // 각 아이템이 방문자를 받아들임
}
System.out.println("총 가격: " + cartVisitor.getTotalPrice());
}
}
'cs > 객체지향' 카테고리의 다른 글
| 객체의 특징 (0) | 2025.03.18 |
|---|---|
| 객체지향에서의 역할, 책임, 협력 (2) | 2025.03.17 |