학교수업, CS/CS요약 및 정리

디자인 패턴

빨대도둑 2024. 12. 22. 20:32

Factory Pattern

Factory Pattern은 객체 생성 과정을 사용하는 코드로부터 분리하고 추상화하여 설계의 유연성과 유지 보수성을 높이는 디자인 패턴입니다. 이 패턴은 상속 관계를 활용하여 상위 클래스에서 중요한 설계 뼈대를 정의하고, 하위 클래스에서 구체적인 객체 생성 로직을 구현하는 방식으로 작동합니다.

주요 특징 및 장점

  1. 느슨한 결합 (Loose Coupling)
    • 상위 클래스와 하위 클래스 간 결합도가 낮아져 독립적인 설계가 가능합니다.
    • 상위 클래스는 객체 생성 방식에 대한 세부 정보를 알 필요가 없으므로 코드의 확장성과 유연성이 높아집니다.
  2. 유지 보수성 향상
    • 객체 생성 로직이 별도로 분리되므로, 코드 수정 시 객체 생성 관련 부분만 수정하면 됩니다.
    • 코드 리팩터링이나 확장이 필요한 경우, 최소한의 변경으로 처리할 수 있어 유지 보수가 용이합니다.

보완 설명

Factory Pattern은 특히 다양한 형태의 객체를 동적으로 생성해야 하는 경우 유용합니다. 예를 들어, 사용자가 선택한 옵션에 따라 다른 클래스의 객체를 생성하거나, 구체적인 구현을 숨기고 인터페이스만 노출해야 하는 상황에서 효과적으로 활용될 수 있습니다.

또한, 객체 생성을 단순화하면서도 코드의 재사용성을 극대화할 수 있어, 대규모 프로젝트에서 설계 품질을 유지하는 데 큰 도움이 됩니다.

예제 코드

/* Enum은 상수의 집합을 정의할 때 사용되는 타입니다. */
enum CarType {
    SantaFe,
    Avante
}

abstract class Car {
    protected String name;

    public String getName() {
        return name;
    }
}

class SantaFe extends Car {
    public SantaFe() {
        name = "SantaFe";
    }
}

class Avante extends Car {
    public Avante() {
        name = "Avante";
    }
}

class CarFactory {
    public static Car createCar(CarType type) {
        switch (type) {
            case SantaFe:
                return new SantaFe();
            case Avante:
                return new Avante();
            default:
                throw new IllegalArgumentException("Invalid Car Type: " + type);
        }
    }
}

public class factoryEX {
    public static void main(String[] args) {
        Car car = CarFactory.createCar(CarType.Avante);
        System.out.println(car.getName()); // Avante
    }
}

 


Observer Pattern

Observer Pattern은 객체의 상태 변화를 관찰하고, 해당 변화가 발생할 때 이를 **관찰자(Observer)**들에게 통지하는 디자인 패턴입니다. 주로 이벤트 기반 시스템에서 사용되며, 변경 사항을 효율적으로 관리하는 데 유용합니다.

주요 개념

  1. 주체(Subject)
    • 상태 변화를 감지하고, 옵저버들에게 그 변화를 통지하는 역할을 담당합니다.
    • 예를 들어, 특정 데이터 모델이나 상태를 추적하는 객체가 주체로 작동할 수 있습니다.
  2. 옵저버(Observer)
    • 주체로부터 변화를 통지받고, 이에 따라 특정 동작을 수행하는 객체입니다.
    • 주체와 독립적으로 작동하며, 상태 변화에 따른 추가 작업을 처리합니다.
  3. 구성 방식
    • 상태가 변경되는 개체 자체를 주체로 정의하거나, 별도의 주체와 옵저버 구조를 분리하여 구축할 수 있습니다.

활용 사례

  1. 이벤트 기반 시스템
    • 주로 GUI 이벤트 처리, 알림 시스템 등에서 활용됩니다. 예를 들어, 버튼 클릭 이벤트에 반응하여 관련 동작을 수행할 때 Observer Pattern이 사용됩니다.
  2. MVC 패턴
    • Model-View-Controller(MVC) 아키텍처에서 Model이 주체 역할을 합니다.
    • Model의 상태 변화가 발생하면, update() 메서드 등을 통해 View(옵저버)에게 알리고, View는 이를 반영하여 사용자 인터페이스를 업데이트합니다. Controller는 이 흐름을 관리하는 조정자 역할을 합니다.

보완 설명

Observer Pattern은 특히 다중 객체 간의 의존성을 줄이고, 변경 사항이 있을 때 자동으로 동기화해야 하는 시스템에서 빛을 발합니다. 예를 들어, 데이터가 갱신되면 이를 즉시 반영해야 하는 대시보드 시스템이나 실시간 알림 기능 구현 시 효과적입니다.

예제 코드

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 observerEX {
    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("i am the champion!!");
    }
}

상속과 구현의 차이

  • 상속(extends)
    • 부모 클래스의 속성과 메서드를 자식 클래스가 물려받아 사용하며, 추가 확장 가능.
    • 일반 클래스나 추상 클래스를 기반으로 동작.
    • 코드 재사용과 중복 최소화가 목적.
  • 구현(implements)
    • 인터페이스에 정의된 메서드를 자식 클래스가 반드시 재정의(@Override)하여 구현.
    • 다중 구현 가능하며, 일관된 동작 보장이 목적.
  • 주요 차이점
    • 상속은 단일 클래스 기반, 구현은 인터페이스 기반.
    • 상속은 선택적 재정의, 구현은 모든 메서드 필수 재정의.

Proxy Pattern

Proxy Pattern대상 객체에 접근하기 전에 그 흐름을 중간에서 제어하여 접근을 필터링하거나 수정하는 계층을 제공하는 디자인 패턴입니다.

주요 특징

  • 객체의 속성 보완, 데이터 검증, 캐싱, 로깅 등의 작업을 수행.
  • 프록시 객체로 사용하거나 프록시 서버의 형태로 구현 가능.

활용 예시

  1. 프록시 객체
    • 대상 객체에 대한 접근을 간접적으로 제어하여 성능 개선 또는 보안 강화.
  2. 프록시 서버
    • 클라이언트와 서버 사이에 위치하여 네트워크 요청을 중개.
    • 대표적으로 캐싱 기능을 통해 원격 서버 요청을 줄이고 트래픽을 최적화.
    • 예: Nginx, Cloudflare 등.

Singleton Pattern

Singleton Pattern하나의 클래스에서 오직 하나의 인스턴스만 생성하도록 설계된 패턴입니다.
이는 여러 개의 개별적인 인스턴스를 만드는 대신, 단일 인스턴스를 공유하여 로직을 처리하는 데 사용됩니다.

주요 특징

  1. 인스턴스 관리
    • 단 하나의 인스턴스만 생성되며, 모든 모듈이 이를 공유합니다.
    • 데이터베이스 연결 모듈과 같은 자원 관리가 필요한 시스템에서 주로 사용됩니다.
  2. 성능 최적화
    • 추가적인 인스턴스 생성 없이 하나의 인스턴스를 활용하므로 생성 비용이 절감됩니다.

장점

  • 효율성: 인스턴스 생성과 자원 사용 비용을 줄임.
  • 글로벌 접근성: 단일 인스턴스를 통해 어디서든 접근 가능.

단점

  • 높은 의존성: 여러 모듈이 동일한 인스턴스를 공유하기 때문에 결합도가 증가할 수 있음.
  • 테스트 어려움: 전역 인스턴스가 테스트 환경에서 독립적으로 다루기 어려운 경우가 있음.

예제 코드

class Singleton {
    private static class singleInstanceHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getISingleton() {
        return singleInstanceHolder.INSTANCE;
    }
}

public class singletonEX {
    public static void main(String[] args) {
        Singleton a = Singleton.getISingleton();
        Singleton b = Singleton.getISingleton();
        System.out.println(a.hashCode());
        System.out.println(b.hashCode());
        if (a == b) {
            System.out.println(true);
        }
    }
}

 

Singleton Pattern과 의존성 주입(DI)

1. 싱글톤 패턴과 TDD의 어려움

  • 문제점:
    • 싱글톤 패턴은 하나의 인스턴스를 공유하기 때문에, 단위 테스트에서 각 테스트가 독립적이어야 하는 조건을 충족하기 어렵습니다.
    • 테스트 순서와 상관없이 실행 가능한 구조를 만들기 위해선 대안이 필요합니다.

2. 의존성 주입(DI) 개념

  • 정의:
    • 의존성 주입은 모듈 간의 결합도를 낮추기 위해, 한 모듈이 다른 모듈에 직접 의존하지 않고 중간 주입자를 통해 간접적으로 의존성을 전달받는 설계 방식입니다.
    • 이를 통해 모듈 간 **디커플링(decoupling)**이 이루어집니다.
  • 예시:
    • 메인 모듈이 직접 하위 모듈에 접근하지 않고, 주입자가 인터페이스나 추상 클래스를 통해 의존성을 전달.

3. 의존성 주입의 장단점

  • 장점:
    • 모듈 교체와 테스트가 쉬워짐.
    • 추상화를 통해 애플리케이션 의존성 방향이 일관되고, 관계 구조가 명확.
    • 코드의 유연성과 가독성 향상.
  • 단점:
    • 클래스 수가 증가해 복잡성 증가.
    • 런타임 시 약간의 성능 저하 발생.

Strategy Pattern

Strategy PatternPolicy Pattern이라고도 불리며, 객체의 행위를 직접 수정하지 않고, 이를 캡슐화된 알고리즘(전략)으로 교체할 수 있게 만드는 디자인 패턴입니다.

주요 특징

  • 행위 캡슐화: 다양한 알고리즘(전략)을 별도 클래스로 분리하여 구현.
  • 유연한 교체: 런타임 시 특정 전략을 선택해 객체의 행위를 변경 가능.
  • 코드 재사용성: 알고리즘 변경 시 기존 코드를 수정하지 않아도 됨.

예제 코드

import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;

interface PaymentStrategy {
    public void pay(int amount);
}

class kakaoPayStrategy implements PaymentStrategy {
    private String name;
    private String cardNumber;
    private String cvv;
    private String dateOfExpiry;

    public kakaoPayStrategy(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 kakao pay.");
    }
}

class tossPayStrategy implements PaymentStrategy {
    private String emailId;
    private String password;

    public tossPayStrategy(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 strategyEX {
    public static void main(String[] args) {
        ShoppingCart cart = new ShoppingCart();

        Item A = new Item("kundolA", 10000);
        Item B = new Item("kundolB", 50000);

        cart.addItem(A);
        cart.addItem(B);

        cart.pay(new tossPayStrategy("kim@example.com", "lkwjlkjsi"));

        cart.pay(new kakaoPayStrategy("lee", "92472938", "436", "12/28"));
    }
}

 


Iterator Pattern

Iterator Pattern은 **컬렉션(Collection)**의 요소들을 순차적으로 접근하는 디자인 패턴입니다. 이 패턴을 통해 다양한 자료형의 구조와 관계없이, 동일한 방식으로 순회할 수 있게 됩니다.

주요 특징

  • 이터레이터 인터페이스를 사용하여 컬렉션의 요소를 순회합니다.
  • 컬렉션의 내부 구조에 상관없이, 이터레이터를 통해 일관된 방식으로 접근이 가능합니다.

장점

  • 컬렉션의 내부 구현을 숨길 수 있어, 코드가 더 간결하고 유지보수가 용이해집니다.
  • 다양한 자료형의 컬렉션을 통일된 방식으로 순회할 수 있습니다.

단점

  • 이터레이터를 사용하는 코드에서 성능 저하가 발생할 수 있으며, 컬렉션의 크기가 클 경우 성능이 중요한 문제가 될 수 있습니다.

예제 코드

import java.util.ArrayList;
import java.util.Iterator;

public class IteratorPatternExample {
    public static void main(String[] args) {
        // 컬렉션 생성
        ArrayList<String> items = new ArrayList<>();
        items.add("Apple");
        items.add("Banana");
        items.add("Cherry");

        // Iterator 생성
        Iterator<String> iterator = items.iterator();

        // 이터레이터로 컬렉션 순회
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

Revealing Module Pattern

Revealing Module Pattern은 **즉시 실행 함수(IIFE, Immediately Invoked Function Expression)**를 사용하여 privatepublic 접근 제어를 구현하는 패턴입니다. 이 패턴은 객체 지향적 모듈을 JavaScript에서 구현할 때 사용되며, 모듈 내에서 필요한 부분만 공개하고, 나머지는 private으로 숨길 수 있습니다.

주요 특징

  • **즉시 실행 함수(IIFE)**를 활용하여 모듈을 정의.
  • private 변수와 메서드를 숨기고, 필요한 부분만 public으로 노출.
  • 모듈 내에서 캡슐화가 이루어지며, 외부에서 접근할 수 있는 인터페이스만 제공.

MVC, MVP, MVVM 패턴

1. MVC 패턴 (Model-View-Controller)

  • 구성 요소:
    • Model: 애플리케이션의 데이터와 관련된 부분. 데이터베이스, 상수, 변수 등을 관리.
    • View: 사용자 인터페이스를 담당하며, Model의 데이터를 기반으로 사용자에게 보여주는 화면.
    • Controller: ModelView를 연결하는 역할을 하며, 사용자 이벤트 처리, 로직 수행 및 ModelView 간의 데이터 전달을 담당.
  • 특징:
    • 애플리케이션의 각 구성 요소를 독립적으로 개발할 수 있어, 개발 효율성유지보수성이 높아짐.
    • Spring Framework가 MVC 패턴을 채택한 대표적인 예.

2. MVP 패턴 (Model-View-Presenter)

  • 변경된 부분:
    • ControllerPresenter로 변경되어 ViewPresenter가 일대일 관계를 가짐.
  • 특징:
    • 강한 결합: ViewPresenter는 직접적으로 연결되며, Model은 여전히 View와 분리되어 관리됩니다.
    • Presenter는 사용자 인터페이스를 처리하며, View가 이벤트를 Presenter로 전달하고, PresenterModel을 업데이트하고 결과를 View에 반영합니다.

3. MVVM 패턴 (Model-View-ViewModel)

  • 변경된 부분:
    • ControllerViewModel로 변경되어, ViewViewModel 간에 양방향 데이터 바인딩이 지원됩니다.
  • 특징:
    • ViewModelView추상화된 계층으로, 데이터 바인딩커맨드 처리 기능을 제공.
    • 뷰와 뷰모델 사이의 양방향 데이터 바인딩을 통해 UI가 자동으로 업데이트되고, 별도의 코드 수정 없이 UI 재사용이 가능합니다.
    • 단위 테스트가 용이하여, 프론트엔드 개발에서 유용합니다.
  • 대표적인 프레임워크:
    • Vue.js는 MVVM 패턴을 채택한 프레임워크입니다.

비교

  • MVCControllerModelView를 연결하는 방식이며, MVPPresenter가 더 강한 결합을 가진 일대일 관계를 가지며, MVVMViewModel을 통해 양방향 데이터 바인딩을 지원하여 UI의 재사용성과 단위 테스트의 용이성을 강화합니다.

프로그래밍 패러다임 및 설계 원칙 요약

프로그래밍 패러다임

프로그래밍 패러다임은 프로그래머가 문제를 해결하는 방식에 대한 관점을 제공합니다.

  1. 선언형 프로그래밍
    • 특징: "무엇을 풀어낼 것인가"에 집중하는 방식으로, 프로그램을 함수로 구성하는 패러다임입니다.
    • 예시: SQL 쿼리나 함수형 프로그래밍에서 사용됩니다.
  2. 함수형 프로그래밍
    • 특징: 선언형 프로그래밍의 한 형태로, 작은 함수들을 조합하여 로직을 구현합니다. 고차 함수를 사용하여 함수의 재사용성을 높입니다.
    • 핵심: 불변성, 순수 함수, 상태 변경을 최소화합니다.
  3. 객체 지향 프로그래밍 (OOP)
    • 특징: 프로그램을 객체들의 집합으로 보고, 객체 간의 상호작용을 통해 동작을 구현합니다. 데이터와 메서드를 하나의 객체 내에 캡슐화합니다.
    • 특징:
      • 추상화: 복잡한 시스템을 간략화하여 핵심적인 개념을 도출.
      • 캡슐화: 데이터와 메서드를 하나로 묶고 외부에 일부를 감추는 방식.
      • 상속: 상위 클래스의 특성을 하위 클래스가 물려받아 확장하거나 재사용하는 방식.
      • 다형성: 동일한 인터페이스나 메서드가 다양한 방식으로 동작하는 특성.
        • 오버로딩: 메서드 이름은 같지만 매개변수의 개수나 타입에 따라 다른 동작을 하게 만드는 것.
        • 오버라이딩: 상위 클래스에서 정의한 메서드를 하위 클래스에서 재정의하여 사용하는 것.

설계 원칙 (SOLID)

  1. 단일 책임 원칙 (SRP)
    • 의미: 클래스는 하나의 책임만 가져야 한다는 원칙. 각 클래스는 단일한 기능에만 집중해야 합니다.
  2. 개방 폐쇄 원칙 (OCP)
    • 의미: 기존 코드는 수정하지 않고, 확장만 가능해야 한다는 원칙. 코드 변경 없이 새로운 기능을 추가할 수 있어야 합니다.
  3. 리스코프 치환 법칙 (LSP)
    • 의미: 부모 클래스의 인스턴스를 자식 클래스의 인스턴스로 바꿔도 시스템이 정상적으로 동작해야 한다는 원칙. 상속받은 클래스는 부모 클래스를 대체할 수 있어야 합니다.
  4. 인터페이스 분리 원칙 (ISP)
    • 의미: 하나의 인터페이스보다 여러 개의 구체적인 인터페이스를 설계해야 한다는 원칙. 불필요한 의존성을 줄이고, 각 인터페이스는 특정 클라이언트에게만 필요해야 합니다.
  5. 의존 역전 원칙 (DIP)
    • 의미: 고수준 모듈은 저수준 모듈에 의존하지 않고, 두 모듈 모두 추상화된 인터페이스에 의존해야 한다는 원칙. 이는 시스템의 유연성을 높이고 변화에 강한 구조를 만듭니다.

 


정리

  • 프로그래밍 패러다임은 프로그램을 개발하는 방법론을 제시하며, 선언형 프로그래밍은 무엇을 할지, 함수형 프로그래밍은 어떻게 할지를 중심으로, 객체 지향 프로그래밍은 객체들의 상호작용을 강조합니다.
  • SOLID 설계 원칙은 소프트웨어 개발에서 유지보수성, 확장성, 유연성을 고려한 원칙들을 정의하고, 각 원칙은 코드 품질을 높이고 시스템의 결합도를 낮추는데 기여합니다.