디자인 패턴

ghltjd369·2023년 8월 21일
0

1. 싱글톤 패턴

  • 쉽게 얘기해서 매번 새로운 것을 생성하는 것이 아니라 하나를 만들어놓고 필요할 때마다 그것을 가져와서 사용하는 방식
class Singleton {
	private static class singleInstanceHolder {
    	private static final Singleton INSTANCE = new Singleton();	//Singleton 객체를 하나 만들음.
    }
    public static synchronized Singleton getInstance() {
    	return singleInstanceHolder.INSTANCE;
    }
}

public class HelloWorld {
	public static void main(String[] args) {
    	Singleton a = Singleton.getInsatnce();
        Singleton b = Singleton.getInstance();
        System.out.println(a.hashCode());
        System.out.println(b.hashCode());
        if (a == b) {
        	System.out.println(true);
        }
    }
}
/*
783929123
783929123
trie
*/
  • 위 코드처럼 Singleton 객체인 a를 생성할 때 new Singleton() 이런 식으로 생성하는 것이 아니라 이미 만들어져있는 Singleton 객체(INSTANCE)를 가져오도록 하면 매번 똑같은 객체를 가져오게 된다.
  • 이 패턴은 주로 데이터베이스 연결 모듈에 많이 사용된다.

단점

  • TDD를 할 때 걸림돌이 된다.
    • TDD는 단위 테스트를 주로 하는데, 단위 테스트는 각 테스트가 서로 독립적이어야 하며 테스트를 어떤 순서로든 실행할 수 있어야 한다.
    • 하지만 싱글톤 패턴은 미리 생성된 하나의 인스턴스를 기반으로 구현하는 패턴이므로 각 테스트마다 '독립적인' 인스턴스를 만들기 어렵다.
  • 모듈 간의 결합을 강하게 만든다.
    • 이는 의존성 주입(DI, Dependency Injection)을 통해 해결할 수 있다.

의존성 주입

  • 메인 모듈이 '직접' 의존성을 주는 것이 아니라 중간에 의존성 주입자가 이 역할을 대신하여 '간접'적으로 의존성을 주입하는 방식

의존성 주입의 장점

  • 모듈들을 쉽게 교체할 수 있는 구조가 되어 테스팅하기 쉽고 마이그레이션 하기 쉽다.
  • 구현할 때 추상화 레이어를 넣고 이를 기반으로 구현체를 넣는다.
    • 애플리케이션 의존성 방향이 일관된다.
    • 애플리케이션을 쉽게 추론할 수 있다.
    • 모듈 간의 관계들이 조금 더 명확해진다.

의존성 주입의 단점

  • 모듈들이 분리되어 클래스 수가 늘어나 복잡성이 증가될 수 있다.
  • 약간의 런타임 페널티가 생길 수 있다.

의존성 주입 원칙

  • 상위 모듈은 하위 모듈에서 어떠한 것도 가져오지 않아야 한다.
  • 둘 다 추상화에 의존해야 한다.
    • 이 때 추상화는 세부 사항에 의존하지 말아야 한다.

2. 팩토리 패턴

  • 상위 클래스로 공장을 하나 만들어놓고 하위 클래스로 레시피들을 만들어놓는 패턴
  • 이 패턴의 장점이 뭘까 하면 우선 상위 클래스와 하위 클래스가 분리되어 느슨한 결합을 가진다.
  • 상위 클래스는 인스턴스 생성 방식에 전혀 알 필요가 없으므로 더 많은 유연성을 가지게 된다.
  • 객체 생성 로직이 따로 떼어져 있어 코드가 바뀌어도 한 곳 (하위 클래스에 있는 레시피)만 고치면 돼서 유지 보수성이 증가한다.
  • 예를 들어서 커피 공장(상위 클래스)가 있다고 할 때 커피 공장 쪽에다가 커피들의 레시피를 적어놓는 것이 아니라 별도로 레시피들(하위 클래스)을 따로 보관한다.
abstract class Coffee { 
    public abstract int getPrice(); 
    
    @Override
    public String toString(){
        return "Hi this coffee is "+ this.getPrice();
    }
}

class CoffeeFactory { 
    public static Coffee getCoffee(String type, int price){
        if("Latte".equalsIgnoreCase(type)) return new Latte(price);
        else if("Americano".equalsIgnoreCase(type)) return new Americano(price);
        else{
            return new DefaultCoffee();
        } 
    }
}
class DefaultCoffee extends Coffee {
    private int price;

    public DefaultCoffee() {
        this.price = -1;
    }

    @Override
    public int getPrice() {
        return this.price;
    }
}
class Latte extends Coffee { 
    private int price; 
    
    public Latte(int price){
        this.price=price; 
    }
    @Override
    public int getPrice() {
        return this.price;
    } 
}
class Americano extends Coffee { 
    private int price; 
    
    public Americano(int price){
        this.price=price; 
    }
    @Override
    public int getPrice() {
        return this.price;
    } 
} 
public class HelloWorld{ 
     public static void main(String []args){ 
        Coffee latte = CoffeeFactory.getCoffee("Latte", 4000);
        Coffee ame = CoffeeFactory.getCoffee("Americano",3000); 
        System.out.println("Factory latte ::"+latte);
        System.out.println("Factory ame ::"+ame); 
     }
} 
/*
Factory latte ::Hi this coffee is 4000
Factory ame ::Hi this coffee is 3000
*/

이렇게 하면 만약에 커피를 만들 때 필요한 인자가 변경된다면,

    public static Coffee getCoffee(String type, int price){
        if("Latte".equalsIgnoreCase(type)) return new Latte(price);
        else if("Americano".equalsIgnoreCase(type)) return new Americano(price);
        else{
            return new DefaultCoffee();
        } 
    }
}

이 부분만 필요에 맞게 변경하면 된다.
해당 부분은 Enum 또는 Map을 이용하여 if문을 쓰지 않고 매핑해서 할 수도 있다.

3. 전략 패턴

  • 말 그대로 필요에 따라 전략을 수정하는 패턴이다.
  • 캡슐화된 전략들을 만들어놓고 필요에 따라 교체해주는 방식이다.
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
interface PaymentStrategy { 
    public void pay(int amount);
} 

class KAKAOCardStrategy implements PaymentStrategy {
    private String name;
    private String cardNumber;
    private String cvv;
    private String dateOfExpiry;
    
    public KAKAOCardStrategy(String nm, String ccNum, String cvv, String expiryDate){
        this.name=nm;
        this.cardNumber=ccNum;
        this.cvv=cvv;
        this.dateOfExpiry=expiryDate;
    }

    @Override
    public void pay(int amount) {
        System.out.println(amount +" paid using KAKAOCard.");
    }
} 

class LUNACardStrategy implements PaymentStrategy {
    private String emailId;
    private String password;
    
    public LUNACardStrategy(String email, String pwd){
        this.emailId=email;
        this.password=pwd;
    }
    
    @Override
    public void pay(int amount) {
        System.out.println(amount + " paid using LUNACard.");
    }
} 

class Item { 
    private String name;
    private int price; 
    public Item(String name, int cost){
        this.name=name;
        this.price=cost;
    }

    public String getName() {
        return name;
    }

    public int getPrice() {
        return price;
    }
} 

class ShoppingCart { 
    List<Item> items;
    
    public ShoppingCart(){
        this.items=new ArrayList<Item>();
    }
    
    public void addItem(Item item){
        this.items.add(item);
    }
    
    public void removeItem(Item item){
        this.items.remove(item);
    }
    
    public int calculateTotal(){
        int sum = 0;
        for(Item item : items){
            sum += item.getPrice();
        }
        return sum;
    }
    
    public void pay(PaymentStrategy paymentMethod){
        int amount = calculateTotal();
        paymentMethod.pay(amount);
    }
}  

public class HelloWorld{
    public static void main(String []args){
        ShoppingCart cart = new ShoppingCart();
        
        Item A = new Item("kundolA",100);
        Item B = new Item("kundolB",300);
        
        cart.addItem(A);
        cart.addItem(B);
        
        // pay by LUNACard
        cart.pay(new LUNACardStrategy("kundol@example.com", "pukubababo"));
        // pay by KAKAOBank
        cart.pay(new KAKAOCardStrategy("Ju hongchul", "123456789", "123", "12/01"));
    }
}
/*
400 paid using LUNACard.
400 paid using KAKAOCard.
*/
  • 위 코드는 어떤 item을 구매할 때 LUNACard로 살 지 KAKAOCard로 살 지 정해서 그에 따른 결제 방식이 달라지는 코드이다.
  • PaymentStrategy 인터페이스를 상속받아 LUNACardStrategy와 KAKAOCardStrategy를 만들었다.
  • 이제 필요에 따라 자신에 맞는 전략을 다음과 같이 선택해서 사용하면 된다.
// pay by LUNACard
cart.pay(new LUNACardStrategy("kundol@example.com", "pukubababo"));
// pay by KAKAOBank
cart.pay(new KAKAOCardStrategy("Ju hongchul", "123456789", "123", "12/01"));
  • 만약 다른 계산 방법(다른 전략)을 추가하고 싶으면 똑같이 PaymentStrategy 인터페이스를 상속받는 전략 클래스를 하나 만들고 사용하면 끝이다.

4. 옵저버 패턴

  • 주체가 어떤 객체(subject)를 계속 관찰하다가 상태 변화가 있을 때마다 메서드 등을 통해 옵저버 목록에 있는 옵저버들에게 변화를 알려주는 디자인 패턴

  • 주체란 객체의 상태 변화를 보고 있는 관찰자
  • 옵저버들이란 이 객체의 상태 변화에 따라 전달되는 메서드 등을 기반으로 '추가 변화 사항'이 생기는 객체들
  • 대표적인 서비스가 트위터 (새로운 트윗을 올렸을 때 내 팔로워들에게 알림이 감)
  • 주로 이벤트 기반 시스템에 사용되며 MVC 패턴에도 사용된다.
    • 모델(주체)에서 변경 사항이 생겨 update() 메서드로 뷰(옵저버)에게 알려주고 이를 기반으로 컨트롤러가 동작
import java.util.ArrayList;
import java.util.List;

interface Subject {
    public void register(Observer obj);
    public void unregister(Observer obj);
    public void notifyObservers();
    public Object getUpdate(Observer obj);
}

interface Observer {
    public void update(); 
}

class Topic implements Subject {
    private List<Observer> observers;
    private String message; 

    public Topic() {
        this.observers = new ArrayList<>();
        this.message = "";
    }

    @Override
    public void register(Observer obj) {
        if (!observers.contains(obj)) observers.add(obj); 
    }

    @Override
    public void unregister(Observer obj) {
        observers.remove(obj); 
    }

    @Override
    public void notifyObservers() {   
        this.observers.forEach(Observer::update); 
    }

    @Override
    public Object getUpdate(Observer obj) {
        return this.message;
    } 
    
    public void postMessage(String msg) {
        System.out.println("Message sended to Topic: " + msg);
        this.message = msg; 
        notifyObservers();
    }
}

class TopicSubscriber implements Observer {
    private String name;
    private Subject topic;

    public TopicSubscriber(String name, Subject topic) {
        this.name = name;
        this.topic = topic;
    }

    @Override
    public void update() {
        String msg = (String) topic.getUpdate(this); 
        System.out.println(name + ":: got message >> " + msg); 
    } 
}

public class HelloWorld { 
    public static void main(String[] args) {
        Topic topic = new Topic(); 
        Observer a = new TopicSubscriber("a", topic);
        Observer b = new TopicSubscriber("b", topic);
        Observer c = new TopicSubscriber("c", topic);
        topic.register(a);
        topic.register(b);
        topic.register(c); 
   
        topic.postMessage("amumu is op champion!!"); 
    }
}
/*
Message sended to Topic: amumu is op champion!!
a:: got message >> amumu is op champion!!
b:: got message >> amumu is op champion!!
c:: got message >> amumu is op champion!!
*/ 
  • Subject를 implements 한 topic이 주체이자 객체가 된다.
  • topic.postMessage("아무말");을 하게 되면 이 topic에 등록된 observer들이 update() 메서드를 실행한다.

5. 프록시 패턴과 프록시 서버

  • 프록시 패턴
    • 대상 객체(subject)에 접근하기 전 그 접근에 대한 흐름을 가로채 대상 객체 앞단의 인터페이스 역할을 하는 디자인 패턴
    • 이를 통해 객체의 속성, 변환 등을 보완하며 보안, 데이터 검증, 캐싱, 로깅에 사용
  • 프록시 객체
    • 어떠한 대상의 기본적인 동작(속성 접근, 할당, 순회, 열거, 함수 호출 등)의 작업을 가로챌 수 있는 객체
  • 프록시 서버
    • 서버와 클라이언트 사이에서 클라이언트가 자신을 통해 다른 네트워크 서비스에 간접적으로 접속할 수 있게 해주는 컴퓨터 시스템이나 응용 프로그램
    • nginx, CloudFlare 등에서 사용
    • DDOS 공격으로부터 방어할 수 있고, 별도의 인증서 설치 없이 HTTPS를 구축할 수 있다.
  • 쉬운 예시로 설명하자면 유튜브가 있다.
    • 유튜브 홈 화면에 가면 다양한 영상의 썸네일이 나오고, 그 중 하나에 마우스를 올리면 영상이 재생된다.
    • 썸네일을 보여줄 때는 프록시로, 영상이 재생되는 무거운 작업은 실제 클래스가 담당하는 것이다.
package proxy;

interface Thumbnail {
  public void showTitle ();
  public void showPreview ();
}

class RealThumbnail implements Thumbnail {
  private String title;
  private String movieUrl;

  public RealThumbnail (String _title, String _movieUrl) {
    title = _title;
    movieUrl = _movieUrl;

    // URL로부터 영상을 다운받는 작업 - 시간 소모 
    System.out.println(movieUrl + "로부터 " + title + "의 영상 데이터 다운");
  }
  public void showTitle () {
    System.out.println("제목:" + title);
  }
  public void showPreview () {
    System.out.println(title + "의 프리뷰 재생");
  }
}

class ProxyThumbnail implements Thumbnail {
  private String title;
  private String movieUrl;

  private RealThumbnail realThumbnail;

  public ProxyThumbnail (String _title, String _movieUrl) {
    title = _title;
    movieUrl = _movieUrl;
  }

  public void showTitle () {
    System.out.println("제목:" + title);
  }

  public void showPreview () {
    if (realThumbnail == null) {
      realThumbnail = new RealThumbnail(title, movieUrl);
    }
    realThumbnail.showPreview();
  }
}
  • 코드를 살펴보면 Thumbnail 인터페이스에 showTitle(), showPreview() 메서드가 있는 것을 확인할 수 있다.ㅣ
  • 썸네일 화면만 보여줄 때는 ProxyThumbnail을 사용한다.
    • 코드를 보면 showTitle()과 showPreivew() 모두 구현은 되어 있지만 showPreview()의 경우 RealThumbnail을 생성 후 이를 통해서 메서드를 실행시키는 것을 확인할 수 있다.
  • 마우스를 올려 영상을 재생할 때는 RealThumbnail을 사용한다.
    • 이 클래스는 무거운 작업을 담당하는 클래스로, showTitle()과 showPreivew() 모두 직접 실행하는 것을 확인할 수 있다.
  • 쉽게 말해서 프록시 패턴은 본체 앞에 분신을 하나 둬서 가벼운 작업을 시키거나, 본체를 보호하는 등의 경우에서 사용하는 패턴이다.

6. MVC 패턴

  • MVC 패턴은 모델(Model), 뷰(View), 컨트롤러(Controller)로 이루어진 디자인 패턴이다.
  • 재사용성과 확장성이 용이하다
  • 애플리케이션이 복잡해질수록 모델과 뷰의 관계가 복잡해진다.

모델

  • 애플리케이션의 데이터인 데이터베이스, 상수, 변수 등
  • 뷰에서 데이터를 생성하거나 수정하면 컨트롤러를 통해 모델을 생성하거나 갱신

  • 모델을 기반으로 사용자가 볼 수 있는 화면
  • 모델이 가지고 있는 정보를 따로 저장하지 않아야 함
  • 변경이 일어나면 컨트롤러에 전달

컨트롤러

  • 하나 이상의 모델과 하나 이상의 뷰를 잇는 다리 역할을 하며 이벤트 등 메인 로직 담당
  • 모델과 뷰의 생명주기 관리
  • 모델이나 뷰의 변경 통지를 받으면 이를 해석하여 각각의 구성 요소에 해당 내용 전달

7. MVP 패턴

  • MVC 패턴으로부터 파생된 패턴으로 MVC에서 C에 해당하는 컨트롤러가 프레젠터(presenter)로 교체된 패턴
  • 뷰와 프레젠터는 일대일 관계이므로 MVC 패턴보다 더 강한 결합을 지닌 디자인 패턴이다.

8. MVVM 패턴

  • MVC의 C에 해당하는 컨트롤러가 뷰모델(view model)로 바뀐 패턴
  • 뷰모델은 뷰를 더 추상화한 계층이다.
  • MVC 패턴과는 다르게 커맨드와 데이터 바인딩을 가진다.
  • 뷰와 뷰모델 사이의 양방향 데이터 바인딩을 지원한다.

0개의 댓글