객체지향 디자인 패턴

싱글톤 패턴, 전략 패턴, 상태 패턴, 커맨드 패턴, 프록시 패턴에 대해 배워봅니다.


객체지향 디자인 패턴

복잡한 구조를 한 단어로 정의함으로서 협업을 할 때에 의사소통을 효율적으로 할 수 있다
기존 코드의 문제점을 해결할 수 있어 효율적으로 코드를 개선할 수 있다

싱글톤 패턴

한 클래스에서 단 하나의 객체만을 생성하게 강제하는 패턴

// 외부에서 인스턴스 생성을 막기위해 생성자를 private 으로 만든다
// static 으로 선언 시 데이터 영역에 저장되어 객체 생성 없이 접근할 수 있다
// spring을 사용할 때 @Autowired 어노테이션을 설정하면 싱글톤 객체가 생성된다

public class Singleton {
    private Singleton() {};
    private static Singleton object = null;

    public static Singleton getInstance() {
        if (object == null) {
            object = new Singleton();
            System.out.println("싱글톤 객체 최초 생성됨");
        }else {
            System.out.println("싱글톤 객체 이미 생성됨");
        }
        return object;
    }
}

public class Main {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance(); //싱글톤 객체 최초 생성됨
        Singleton singleton2 = Singleton.getInstance(); //싱글톤 객체 이미 생성됨 
        Singleton singleton3 = Singleton.getInstance(); //싱글톤 객체 이미 생성됨
        Singleton singleton4 = Singleton.getInstance(); //싱글톤 객체 이미 생성됨
        Singleton singleton5 = Singleton.getInstance(); //싱글톤 객체 이미 생성됨
    }
}

싱글톤 패턴 특징

어떤 클래스의 객체가, 해당 프로세스에서 딱 하나만 만들어져 있어야 할 때 사용한다
예를 들어 다크 모드를 적용할 경우 모든 페이지에서 반드시 같은 객체를 사용해야 한다

전략 패턴

옵션들마다의 행동을 모듈화해서 독립적이고 상호 교체 가능하게 만드는 패턴

public interface PaymentService {
    public void pay(int price);
}

public class OrderService {
    public void payment(PaymentService paymentService, int price) {
        paymentService.pay(price);
    }
}

public class CardService implements PaymentService{
    @Override
    public void pay(int price) {
        System.out.println(price + " 원이 카드로 결제되었습니다");
    }
}

public class MoneyService implements PaymentService {
    @Override
    public void pay(int price) {
        System.out.println(price + " 원이 현금으로 결제되었습니다");
    }
}

public class Main {
    public static void main (String[] args) {
        OrderService orderService = new OrderService();
        PaymentService cardService = new CardService();
        PaymentService moneyService = new MoneyService();
        orderService.payment(moneyService, 3000); //3000 원이 현금으로 결제되었습니다
        orderService.payment(cardService, 5000); //5000 원이 카드로 결제되었습니다 
        orderService.payment(moneyService, 8000); //8000 원이 현금으로 결제되었습니다 
        orderService.payment(cardService, 10000); //10000 원이 카드로 결제되었습니다
    }
}

전략 패턴 특징

일관된 동작이 각 전략에 따라 변경될 때 사용한다
코드에서 ‘결제’라는 행위는 동작이고 ‘결제 타입’이 전략이라 볼 수 있다

상태 패턴

객체가 상태에 따라 다른 행동을 할 수 있도록 위임하는 패턴

public interface PowerState {
    public void setPowerState(Power power);
}

public class Power {
    PowerState powerState = new PowerOff();
    public void switchPowerState(PowerState powerState) {
        this.powerState = powerState;
    }
    public void onSwitch() {
        powerState.setPowerState(this);
    }
}

public class PowerOn implements PowerState {
    @Override
    public void setPowerState(Power power) {
        System.out.println("전원이 꺼졌습니다");
        power.switchPowerState(new PowerOff());
    }
}

public class PowerOff implements PowerState {
    @Override
    public void setPowerState(Power power) {
        System.out.println("전원이 켜졌습니다");
        power.switchPowerState(new PowerOn());
    }
}

public class Main {
    public static void main (String[] args) {
        Power power = new Power();
        power.onSwitch(); //전원이 켜졌습니다
        power.onSwitch(); //전원이 꺼졌습니다 
        power.onSwitch(); //전원이 켜졌습니다
        power.onSwitch(); //전원이 꺼졌습니다 
        power.onSwitch(); //전원이 켜졌습니다
        power.onSwitch(); //전원이 꺼졌습니다
    }
}

상태 패턴 특징

전략 패턴과 상당히 유사하며 전략 패턴은 상속을 대체하려는 목적이라면 상태 패턴은 조건문들을 대체하려는 목적으로 사용된다

커맨드 패턴

실행될 기능을 캡슐화함으로써 여러 기능을 사용할 수 있는 재사용성이 높은 클래스를 설계하는 패턴

public interface Command {
    public void execute();
}

abstract class AccountCommand implements Command{
    protected Account account; //자식 클래스에서 사용 가능
    protected int money; //자식 클래스에서 사용 가능

    public void setAccount (Account account) {
        this.account = account;
    }
}  

class DepositCommand extends AccountCommand {
    public DepositCommand(int money) {
        this.money = money;
    }
    public void execute() {
        account.setMoney(account.getMoney() + money);
        System.out.println(money+" 원 입금, 잔액: "+account.getMoney());
    }
}

class WithDrawCommand extends AccountCommand {
    public WithDrawCommand(int money) {
        this.money = money;
    }
    public void execute() {
        if (account.getMoney() > money) {
            account.setMoney(account.getMoney() - money);
            System.out.println(money+" 원 출금, 잔액: "+account.getMoney());
        } else {
            System.out.println(money+"원 출금 실패, 출금 가능 금액: "+account.getMoney());
        }
    }
}

public class Account {
    private int money = 0;
    public int getMoney() {
        return this.money;
    }
    public void setMoney(int money) {
        this.money = money;
    }
    public void deposit(int money) {
        AccountCommand depositCommand = new DepositCommand(money);
        depositCommand.setAccount(this);
        depositCommand.execute();
    }
    public void withdraw(int money) {
        AccountCommand withdrawCommand = new WithDrawCommand(money);
        withdrawCommand.setAccount(this);
        withdrawCommand.execute();
    }
}

public class Main {
    public static void main(String[] args) {
        Account account = new Account();
        account.deposit(2000); // 2000원 입금, 잔액: 2000
        account.withdraw(1000); //1000원 출금, 잔액: 1000
        account.deposit(3000); //3000원 입금, 잔액: 4000
        account.withdraw(5000); //5000원 출금 실패, 출금 가능 금액: 4000
    }
}

커맨드 패턴 특징

기능이 변경되면 각 Command만 변경하면 되므로 호출자 클래스 수정 없이 그대로 사용할 수 있다
계좌 관련 기능이 아닌 다른 새로운 기능이 생성되더라도 새로운 기능에 관련된 여러 기능을 커맨드 패턴으로 캡슐화하여 설계하면 된다

프록시 패턴

실제 클래스 객체가 아닌 프록시 객체를 대신 사용하여 로직의 흐름을 제어하는 패턴

public abstract class Item {
    protected String title;
    protected String imageURL;
    public void showTitle() {};
    public void showImage() {};
}

class RealItem extends Item {
    public RealItem (String title, String imageURL) {
        this.title = title;
        this.imageURL = imageURL;
        System.out.println("RealItem 객체 생성됨");
    }

    public void showTitle() {
        System.out.println(title);
    }

    public void showImage() {
        System.out.println(imageURL);
    }
}

class ProxyItem extends Item {
    private RealItem realItem;

    public ProxyItem (String title, String imageURL) {
        this.title = title;
        this.imageURL = imageURL;
    }

    public void showTitle() {
        System.out.println(title);
    }

    public void showImage() {
        if (realItem == null) {
            realItem = new RealItem(title, imageURL);
        }
        realItem.showImage();
    }
}

public class Main {
    public static void main(String[] args) {
        Item item = new ProxyItem("도커", "Docker.png");
        item.showTitle(); //도커 
        item.showImage(); //RealItem객체 생성됨, Docker.png
    }
}

프록시 패턴 특징

실제 클래스 객체와 프록시 객체의 사용법이 거의 같아 사용하기 좋음
필요할 때만 실제 클래스 객체를 생성하기 때문에 효율적이고 유연한 프로그래밍이 가능함

Leave a comment