디자인 패턴

유요한·2024년 2월 23일
0

기술면접

목록 보기
3/27
post-thumbnail

디자인 패턴이란?

프로그램 설계시 자주 발생하는 문제들을 해결할 수 있는 프로그램 설계 방법이다. 디자인 패턴은 자주 사용하는 설계 형태를 정형화해서 이를 유형별로 설계 템플릿을 만들어둔 것을 말한다. 객체지향 언어를 이용해 프로그래밍 할 때 디자인 패턴을 사용한다면 보다 효율적이고 재사용성이 높은 설계를 할 수 있다. 이에따른 유지보수가 용이해지고 객체의 생성과 참조 과정을 추상화하여 개발 시 부담을 줄여줄 수 있다.

생성패턴:
객체와 생성에 사용되는 패턴으로, 객체를 수정해도 호출부가 영향을 받지 않게 합니다.

구조패턴:
객체를 조합하여 더 큰 구조를 만드는 패턴

행위패턴:

  • 객체 사이의 알고리즘이나 책임 분배에 관련된 패턴
  • 객체 하나로는 수행할 수 없는 작업을 여러 객체를 이용해 작업을 분배합니다. 결합도 최소화를 고려할 필요가 있습니다.

생성패턴

프로토타입

  • 원본 객체를 복제해서 새로운객체를 생성하는 패턴
  • 기본은 얕은 복사
  • Cloneable의 clone메소드를 오버라이딩하여 사용

팩토리 패턴

팩토리 패턴은 객체를 사용하는 객체를 사용하는 코드에서 객체 생성 부분을 떼어내 추상화한 패턴이자 상속 관계에 있는 두 클래스에서 상위 클래스가 중요한 뼈대를 결정하고 하위 클래스에서 객체 생성에 관한 구체적인 내용을 결정하는 패턴입니다.

상위 클래스와 하위 클래스가 분리되기 때문에 느슨한 결합을 가지며 상위 클래스에서는 인스턴스 생성 방식에 대해 전혀 알 필요가 없기 때문에 더 많은 유연성을 갖게 됩니다. 그리고 객체 생성 로직이 따로 떼어져 있기 때문에 코드를 리팩터링하더라도 한 곳만 고칠 수 있게 되니 유지 보수성이 증가됩니다. 예를들어 라떼 레시피와 아메리카노 레시피, 우유 레시피라는 구체적인 내용이 들어있는 하위 클래스가 컨베이어 벨트를 통해 전달되고 상위 클래스인 바리스타 공장에서 이 레시피들을 토대로 우유 등을 생산하는 생산 공장이라고 생각하면 됩니다.

package Factory;

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 Factory {
    public static void main(String[] args) {
        Coffee latte = CoffeeFactory.getCoffee("Latte", 4000);
        Coffee ame = CoffeeFactory.getCoffee("Americano", 1500);
        System.out.println("Factory Latte : " + latte);
        System.out.println("Factory Americano : " + ame);
    }
}

싱글톤 패턴

  • 전역변수를 사용하지 않고 객체를 하나만 생성하여, 어디서든지 참조할 수 있도록 하는 패턴
  • LazyHolder을 사용하는 싱글톤 패턴이 가장 이상적

싱글톤 패턴은 하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴입니다. 하나의 클래스를 기반으로 여러 개의 개별적인 인스턴스를 만들 수 있지만 그렇게 하지 않고 하나의 클래스를 기반으로 단 하나의 인스턴스를 만들어 이를 기반으로 로직을 만드는 데 쓰이며, 보통 데이터베이스 연결 모듈에 많이 사용합니다. 하나의 인스턴스를 만들어 놓고 해당 인스턴스를 다른 모듈들이 공유하며 사용하기 때문에 인스턴스를 생성할 때 드는 비용이 줄어드는 장점이 있습니다.

package Singleton;

class Singleton {
    private static class singleInstanceHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() {
        return singleInstanceHolder.INSTANCE;
    }
}


public class HelloWorld {
    public static void main(String[] args) {
        Singleton a = Singleton.getInstance();
        Singleton b = Singleton.getInstance();
        System.out.println(a.hashCode());
        System.out.println(b.hashCode());

        if (a == b) {
            System.out.println(true);
        }
    }
}

싱글톤 패턴을 사용하는 이유

인스턴스를 오직 한 개만 생성하여 사용한다면 메모리 낭비가 방지가 된다. 그리고 이미 생성된 인스턴스를 활용함으로써 속도 측면에서도 장점이 있습니다. 그리고 데이터 공유가 쉽습니다. 싱글톤으로 생성된 객체는 전역성을 띄기 때문에 다른 객체와 공유가 용이합니다. 하지만 만약 여러 클래스의 인스턴스에서 싱글톤 인스턴스의 데이터에 동시에 접속하게 된다면 동시성 문제가 생길 수 있습니다. 인스턴스가 한 개만 존재하는 것을 보증하고 싶은 경우 싱글톤 패턴을 사용합니다.

싱글톤 패턴 단점

싱글톤 패턴은 TDD(Test Driven Development)를 할 때 걸림돌이 됩니다. TDD를 할 때 단위 테스트를 주로 하는데, 단위 테스트는 테스트가 서로 독립적이어야 하며 테스트를 어떤 순서로든 실행할 수 있어야 합니다. 하지만 싱글톤 패턴은 미리 생성된 하나의 인스턴스 기반으로 구현하는 패턴이므로 각 테스트마다 독립적인 인스턴스를 만들기 어렵다.

또한 싱글톤 패턴은 사용하기 쉽고 실용적이지만 모듈 간의 결합을 강하게 만들 수 있다는 단점이 있습니다. 이 때 의존성 주입(DI)을 통해 모듈 간의 결합을 조금 더 느슨하게 만들어 해결할 수 있습니다.

의존성 주입 장점

모듈들을 쉽게 교체할 수 있는 구조가 되어 테스팅하기 쉽고 마이그레이션하기도 수월합니다. 또한 구현할 때 추상화 레이어를 넣고 이를 기반으로 구현체를 넣어 주기 때문에 애플리케이션의 의존성 방향이 일관되고 애플리케이션을 쉽게 추론할 수 있으며, 모듈 간의 관계들이 조금 더 명확해집니다.

의존성 주입 단점

모듈들이 더욱더 분리되므로 클래스 수가 늘어나 복잡성이 증가될 수 있으며 약간의 런타임 패널티가 생기기도 합니다.

의존성 주입 원칙

의존성 주입은 상위 모듈은 하위 모듈에서 어떠한 것도 가져오지 않아야 합니다. 또한 둘 다 추상화에 의존적이여야 하며 이 때 추상화는 세부 사항에 의존하지 말아야 합니다.라는 의존성 주입 원칙을 지켜주며 만들어야 합니다.

빌더

  • 객체의 생성과 표현을 분리해 객체를 생성하는 패턴
  • 점층적 생성자 패턴과 자바 빈 패턴 결합

구조패턴

어댑터

  • 어느 클래스의 인터페이스를 다른 클래스에서 사용하고자 할 때 다른 클래스에서 사용할 수 있도록 호환성을 제공하는 패턴

  • 클래스 어댑터는 다중상속이 안되고 실행중간에 변경할 수 없으므로, 객체 어댑터 사용이 나음

브리지

추상화의 기능을 분리하여 각자 독립적으로 변형할 수 있게 하는 패턴

컴포지트

  • 여러개의 객체들로 구성된 복합객체와 단일 객체를 클라이언트에서 구별없이 다루게 해주는 패턴

  • 파일 디렉토리와 같은 관계를 갖는 객체들 사이에 유용

  • 클라이언트는 전체와 부분을 구별하지 않고 동일한 인터페이스 사용 가능

플라이웨이트

  • 동일하거나 유사한 객체들 사이에 가능한 많은 데이터를 공유하여 메모리 사용을 최소화하는 패턴

  • 생성되는 객체의 수가 많거나, 생성된 객체가 오래도록 메모리에 상주 등에 적합

  • Integer.valueOf 메서드도 플라이웨이트 패턴을 사용하여 127~127사이의 값은 캐싱하여 같은 참조값을 갖도록 만듦

프록시 패턴

  • 실제 기능을 수행하는 객체 대신 가상의 객체를 사용해 로직의 흐름을 제어하는 패턴
  • 가상의 객체의 내부 코드가 실제 객체의 수행을 감싸고 있는 형태

프록시 패턴(proxy pattern)은 대상 객체에 접근하기 전 그 접근에 대한 흐름을 가로채 대상 객체 앞단의 인터페이스 역할을 하는 디자인 패턴입니다.

이를 통해 객체의 속성, 변환 등을 보완하며 보안, 데이터 검증, 캐싱, 로깅에 사용합니다. 이는 앞서 설명한 프록시 객체로 쓰이기도 하지만 프록시 서버로도 사용됩니다.

프록시 서버에서의 캐싱

캐시 안에 정보를 담아두고 캐시 안에 있는 정보를 요구하는 요청에 대해 다시 저 멀리 있는 원격 서버에 요청하지 않고 캐시 안에 있는 데이터를 활용하는 것을 말한다. 이를 통해 불필요하게 외부와 연곃하지 않기 때문에 트래픽을 줄일 수 있는 장점이 있다.

프록시 서버

프록서 서버는 서버와 클라이언트 사이에서 클라이언트가 자신을 통해 다른 네트워크 서비스에 간접적으로 접속할 수 있게 해주는 컴퓨터 시스템이나 응용 프로그램을 가리킵니다.

버퍼 오버플로우

버퍼는 보통 데이터가 저장되는 메모리 공간으로 메모리 공간을 벗어나는 경우를 말한다. 이때 사용되지 않아야 할 영역에 데이터가 덮어씌어져 주소, 값을 바꾸는 공격이 발생하기도 한다.

gzip

LZ77과 Huffman 코딩의 조합인 DEFLATE 알고리즘을 기반으로 한 압축 기술이다. gzip 압축을 하면 데이터 전송량을 줄일 수 있지만, 압축을 해지했을 때 서버에서의 CPU 오버헤드도 생각해서 gzip 압축 사용 유무를 결정해야 한다.

프록시 서버로 쓰는 CloudFlare

CloudFlare는 전 세계적으로 분산된 서버가 있고 이를 통해 어떠한 시스템의 콘텐츠 전달을 빠르게 할 수 있는 CDN 서비스입니다. 웹 서버 앞단에 프록시 서버로 두어 DDOS공격 방어HTTPS 구축에 쓰입니다. 또한, 서비스를 배포한 이후에 해외에서 무언가 의심스러운 트래픽이 많이 발생하면 이 때문에 많은 클라우드 서비스 비용이 발생할 수도 있는데 이때 CloudFlare가 의심스러운 트래픽인지를 먼저 판단해 CAPTCHA 등을 기반으로 이를 일정부분 막아주는 역할도 수행합니다.

DDOS 공격 방어

DDOS는 짧은 기간 동안 네트워크에 많은 요청을 보내 네트워크를 마비시켜 웹 사이트의 가용성을 방해하는 사이버 공격 유형입니다. CloudFlare는 의심스러운 트래픽, 특히 사용자가 접속하는 것이 아닌 시스템을 통해 오는 트래픽을 자동으로 차단해서 DDOS 공격으로 부터 보호합니다. CloudFlare의 거대한 네트워크 용량과 캐싱 전략으로 소규모 DDOS 공격은 쉽게 막아낼 수 있으며 이러한 공격에 대한 방화벽 대시보드도 제공합니다.

HTTPS 구축

서버에서 HTTPS를 구축할 때 인증서를 기반으로 구축할 수도 있습니다. 하지만 CloudFlare를 사용하면 별도의 인증서 설치 없이 좀 더 쉽게 HTTPS를 구축할 수 있습니다.

CDN

각 사용자가 인터넷에 접속하는 곳과 가까운 곳에서 콘텐츠를 캐싱 또는 배포하는 서버 네트워크를 말한다. 이를 통해 사용자가 웹 서버로부터 콘텐츠를 다운로드 하는 시간을 줄일 수 있다.

HTTPS
  • 사이트에 보내는 정보들을 3자가 못 보게 한다.
  • 접속한 사이트가 믿을 만한 곳인지를 알려준다.

하이퍼텍스트 전송 프로토콜 보안(HTTPS)은 웹 브라우저와 웹 사이트 간에 데이터를 전송하는 데 사용되는 기본 프로토콜인 HTTP의 보안 버전입니다. HTTPS는 데이터 전송의 보안을 강화하기 위해 암호화됩니다. 이는 사용자가 은행 계좌, 이메일 서비스, 의료 보험 공급자에 로그인하는 등 중요한 데이터를 전송할 때 특히 중요합니다.

모든 웹 사이트, 특히 로그인 자격 증명이 필요한 웹 사이트는 HTTPS를 사용해야 합니다. 크롬 등 최신 웹 브라우저에서는 HTTPS를 사용하지 않는 웹 사이트가 HTTPS를 사용하는 웹 사이트와 다르게 표시됩니다.

이 덕분에 HTTP보다 안전합니다.

포트
HTTPS는 포트 443을 사용합니다. 이는 포트 80을 사용하는 HTTP와 HTTPS를 구분합니다.

HTTP와 HTTPS의 차이
엄밀히 말하면 HTTPS는 HTTP와 별개의 프로토콜이 아닙니다. HTTPS는 단순히 HTTP 프로토콜을 통해 TLS/SSL 암호화를 사용하는 것입니다. HTTPS는 특정 공급자가 주장하는 실체가 맞는지 확인하는 TLS/SSL 인증서의 전송을 기반으로 이루어집니다.

대칭키

대칭키(Symmetric key) 방식은 암호문을 생성(암호화)할 때 사용하는 키와 암호문으로부터 평문을 복원(복호화)할 때 사용하는 키가 동일한 암호 시스템으로 일반적으로 알고 있는 암호 시스템입니다.

암복호화에 사용하는 키가 동일함

  • 장점 : 암호화방식에 속도가 빠르다. 대용량 Data 암호화에 적합하다.
  • 단점 : 키를 교환해야 하는 문제, 탈취 관리 걱정, 사람이 증가할수록 키관리가 어려워짐, 확장성 떨어짐

기밀성을 제공하나, 무결성/인증/부인방지 를 보장하지 않음

대표적 알고리즘 : 공인인증서의 암호화방식으로 유명한 SEED, DES, 3DES, AES, ARIA, 최근 주목받고 있는 암호인 ChaCha20

비대칭키

비대칭키(Asymmetric Key) 암호화는 다른 말로 공개키 암호 알고리즘라고 알려져 있습니다. 이는 하나의 키가 아닌 암호화에 쓰이는 키 값과 복호화에 쓰이는 키 값이 다른 것을 의미합니다.

비대칭키 암호화는 양방향 암호화로써, 복호화가 가능한 암호 알고리즘입니다. 공개키 암호는 대칭키 암호의 키 전달에 있어서 취약점을 해결하고자 한 노력의 결과로 탄생했습니다. 한 쌍의 키가 존재하며, 하나는 특정 사람만이 가지는 개인키(또는 비밀키)이고 다른 하나는 누구나 가질 수 있는 공개키입니다. 공개키 암호화 방식은 암호학적으로 연관된 두 개의 키를 만들어서 하나는 자기가 안전하게 보관하고 다른 하나는 상대방에게 공개합니다. 개인키로 암호화 한 정보는 그 쌍이 되는 공개키로만 복호화가 가능하고, 공개키로 암호화한 정보는 그 쌍이 되는 개인키로만 복호화가 가능합니다.

만약, 개인이 비밀통신을 할 경우 비대칭키 암호화보다 대칭키 암호를 사용할 수 있지만, 다수가 통신을 할 때에는 키의 개수가 급증하게 되어 큰 어려움이 따른다. 이런 어려움을 극복하기 위해 나타난 것이 공개키 암호이다. 공개키 암호는 다른 유저와 키를 공유하지 않더라도 암호를 통한 안전한 통신을 할 수 있다는 장점을 있다.

CORS와 프론트앤드의 프록시 서버

CORS(Cross-Origin Resource Sharing)는 서버가 웹 브라우저에서 리소스를 로드할 때 다른 오리진을 통해 로드하지 못하게 하는 HTTP 헤더 기반 메커니즘입니다.

퍼사드

일련의 서브 시스템의 인터페이스에 대한 통합된 인터페이스를 제공하여 서브 시스템 사용을 더 쉽게 할 수 있도록 하는 패턴

데코레이터

  • 객체의 결합을 통해 기능을 동적으로 유연하게 확장할 수 있게 해주는 패턴

  • 기본 기능에 추가할 수 있는 기능의 종류가 많은 경우 각 추가 기능을 Decorator클래스로 정의하고 필요한 Decorator객체를 조합하여 사용

행위 패턴

템플릿 메서드

  • 어떤 작업을 처리하는 일부분을 서브 클래스로 캡슐화하여 전체 일을 수행하는 구조는 바뀌지 않으면서 특정 단계에서 수행하는 내역을 바꾸는 패턴

  • 메인로직은 추상 클래스의 일반 메서드로 정의, 구현마다 달라질 기능은 구현 클래스에서 선언 후 호출하는 방식으로 사용

커맨드

  • 객체의 행위를 클래스로 만들어 캡슐화하는 패턴

  • 기능이 수정되거나 확장될 때 기존 코드의 수정 없이 기능에 대한 클래스를 적용하면 되므로 시스템 확장성이 있으면서 유연해짐

책임연쇄

  • 요청을 처리하는 동일 인터페이스 객체들을 체인 형태로 연결해놓은 패턴

  • 앞의 객체가 요청을 처리하지 못할 경우, 같은 인터페이스의 다른 객체에게 해당 요청을 전달

  • 자바의 exception 처리가 대표적

중재자

  • 모든 클래스간의 복잡한 로직을 캡슐화하여 하나의 클래스에 위임하여 처리하는 패턴

  • M:N의 관계에서 M:1의 관계로 복잡도를 떨어뜨려 유지보수 및 재사용의 확장에 유리

전략 패턴

  • 행위를 클래스로 캡슐화하여 동적으로 행위를 자유롭게 바꿀 수 있게 해주는 패턴
  • 전략을 쉽게 변경할 수 있음

전략 패턴은 정책 패턴이라고도 하며 객체의 행위를 바꾸고 싶은 경우 직접 수정하지 않고 전략이라고도 부르는 캡슐화한 알고리즘을 컨텍스트 안에서 바꿔주면서 상호 교체가 가능하게 만드는 패턴입니다.

우리가 어떤 것을 살 때 네이버페이, 카카오페이 등 다양한 방법으로 결제하듯이 어떤 아이템을 살 때 여러 카드로 사는 것을 구현한 예제입니다. 결제 방식에 따라 전략만 바꿔서 두 가지 방식으로 결제하는 것을 구현했습니다.

package Strategy;

import java.util.ArrayList;
import java.util.List;

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

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

    public KakaCardStrategy(String name, String cardNumber, String cvv, String dateOfExpiry) {
        this.name = name;
        this.cardNumber = cardNumber;
        this.cvv = cvv;
        this.dateOfExpiry = dateOfExpiry;
    }

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

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

    public LunaStrategy(String emailId, String password) {
        this.emailId = emailId;
        this.password = password;
    }

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

        Item a = new Item("A", 100);
        Item b = new Item("B", 300);

        cart.addItem(a);
        cart.addItem(b);

        cart.pay(new LunaStrategy("A@example.com", "1234"));
        cart.pay(new KakaCardStrategy("John","123456789", "123", "12/01"));
    }
}

컨텍스트

프로그래밍에서의 컨텍스트는 상황, 맥락, 문맥을 의미하며 개발자가 어더한 작업을 완료하는 데 필요한 모든 관련 정보를 말한다.

옵저버 패턴

어떤 객체의 상태가 변하거나 이벤트가 발생했을 때 그와 연관된 객체들에 알림을 보내는 패턴

옵저버 패턴(observer pattern)은 주체가 어떤 객체의 상태 변화를 관찰하다가 상태 변화가 있을 때마다 메서드 등을 통해 옵저버 목록에 있는 옵저버들에게 변화를 알려주는 디자인 패턴입니다.

여기서 주체란 객체의 상태 변화를 보고 있는 관찰자이며, 옵저버들이란 이 객체의 상태 변화에 따라 전달되는 메서드 등을 기반으로 추가 변화 사항이 생기는 객체들을 의미합니다. 또한, 앞의 그림처럼 주체와 객체를 따로 두지 않고 상태가 변경되는 객체를 기반으로 구축하기도 합니다.

옵저버 패턴을 활용한 서비스로는 트위터가 있습니다.

옵저버 패턴은 주로 이벤트 기반 시스템에 사용하며 MVC 패턴에도 사용됩니다. 예를들어 주체라고 볼 수 있는 모델에서 변경 사항이 생겨 update() 메서드로 옵저버인 뷰에 알려주고 이를 기반으로 컨트롤러 등이 작동하는 겁니다.

package Observer;

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<Observer>();
        this.message = "";
    }

    @Override
    public void register(Observer obj) {
        // contains() 함수는 대상 문자열에 특정 문자열이 포함되어 있는지 확인하는 함수
        // 대소문자 구분
        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 postMesseage(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 Test {
    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.postMesseage("test~");
    }
}

topic을 기반으로 옵저버 패턴을 구현했습니다. 여기서 topic은 주체이자 객체가 됩니다. class Topic implements Subject를 통해 Subject imterface를 구현했고 Observer a = new TopicSubScriber("a", topic);으로 옵저버를 선언할 때 해당 이름과 어떠한 토픽의 옵저버가 될 것인지를 정했습니다.

상태

  • 상태를 클래스로 만들어 어떤 행위를 수행할 때 상태에 행위를 수행하도록 위임하는 패턴

  • 시스템의 각 상태를 클래스로 분리하여 표현

방문자

  • 데이터 구조와 기능을 분리하여 구조를 변경하지 않고 새로운 기능을 추가할 수 있는 패턴

메멘토

객체의 생태 정보를 저장하고 필요에 의해 원하는 시점의 데이터를 복원할 수 있는 패턴

프로그래밍 패러다임

프로그래밍 패러다임은 프로그래머에게 프로그래밍의 관점을 갖게 해주는 역할을 하는 개발 방법론입니다.

예를 들어, 객체지향 프로그래밍은 프로그래머들이 프로그램을 상호 작용하는 객체들의 집합으로 볼 수 있게 하는 반면에 함수형 프로그래밍을 상태 값을 지니지 않는 함수 값들의 연속으로 생각할 수 있게 해줍니다.

프로그래밍 패러다임은 크게 선언형, 명령형으로 나누며 선언형은 함수형이라는 하위 집합을 갖습니다. 또한 명령형은 다시 객체지향, 절차지향으로 나눕니다.

선언형과 함수형 프로그래밍

선언형 프로그래밍(declarative programming)이란 무엇을 풀어내는가에 집중하는 패러다임이며 "프로그램은 함수로 이루어진 것이다." 라는 명제가 담겨있는 패러다임이기도 합니다. 함수형 프로그래밍(functional programming)은 선언형 패러다임의 일종입니다.

고차 함수

고차 함수란 함수가 함수를 값처럼 매개변수로 받아 로직을 생성할 수 있는 것을 말합니다.

일급 객체

고차 함수를 쓰기 위해서는 해당 언어가 일급 객체라는 특징을 가져야 하며 그 특징은 다음과 같습니다.

  • 변수나 메서드에 함수를 할당할 수 있습니다.
  • 함수 안에 함수를 매개변수로 담을 수 있습니다.
  • 함수가 함수를 반한할 수 있습니다.

💡옵저버 패턴을 어떻게 구현하나요?

여러가지 방법이 있지만 프록시 객체를 써서 하곤 합니다. 프록시 객체를 통해 객체의 속성이나 메서드 변화 등을 감지하고 이를 미리 설정해 놓은 옵저버들에게 전달하는 방법으로 구현합니다.


💡프록시 서버를 설명하고 사용 사례에 대해 설명해보세요

프록시 서버란 클라이언트가 자신을 통해서 다른 네트워크 서비스에 간접적으로 접속할 수 있게 해주는 서버를 말합니다. 주로 서버 앞단에 둬서 캐싱, 로깅, 데이터 분성을 서버보다 먼저 하는 서버로 쓰입니다. 이를 통해 포트 번호를 바꿔서 사용자가 실제 서버의 포트에 접근하지 못하게 할 수 있으며, 공격자의 DDOS 공격을 차단하거나 CDN을 프로식 서버로 캐싱할 수 있으며, nginx로 Node.js로 이루어진 서버의 앞단에 둬서 버퍼 오버플로우를 해결할 수도 있습니다.


Iterator 패턴

Java 언어에서 arr배열의 모든 요소를 표시하려면 다음과 같이 for문을 사용합니다.

for(int i = 0; i < arr.length; i++) {
	System.out.println(arr[i]);
  }

여기서 사용되는 루프 변수 i에 주목합니다. 변수 i는 0으로 초기화되고 1, 2, 3, ...으로 증가합니다. 그 때마다 arr[i]에 표시됩니다.

for문의 i++가 하나씩 증가시키면 현재 주목하는 요소는 다음, 그 다음으로 차례차례 진행됩니다. 이렇게 i를 늘려가다 보면 배열 arr의 요소 전체를 처음부터 순서대로 검색하게 됩니다. 여기서 사용하는 변수 i의 기능을 추상화하여 일반화한 것을 디자인 패턴에서는 Iterator 패턴이라고 합니다.

Iterator 패턴은 무엇인가 많이 모여 있을 때 이를 순서대로 가리키며 전체를 검색하고 처리를 반복하는 것입니다. iterate라는 영어 단어는 반복하다라는 뜻입니다. 그래서 iterator를 반복자라고 합니다.

  • Iterable<E> : 집합체를 나타내는 인테페이스(java.lang 패키지)
  • Iterator<E> : 처리를 반복하는 반복자를 나타내는 인터페이스(java.util 패키지)
  • Book : 책을 나타내는 클래스
  • BookShelf : 책장을 나타내는 클래스
  • BookShelfIterator : 책장을 검색하는 클래스
  • Main : 동작 테스트용 클래스
package Iterator;

import java.util.Iterator;

public interface Iterable<E> {
    // 이 메소드는 집합체에 대응하는 Iterator<E>를 만들기 위한 것입니다.
    public abstract Iterator<E> iterator();
}

잡합체에 포함된 요소를 하나하나 처리해 나가고 싶을 때는 이 iterator 메소드를 사용해 Iterator<E> 인터페이스를 구현한 클래스의 인스턴스를 만듭니다.

Iterator<E> 인터페이스는 하나하나의 요소 처리를 반복하기 위한 것으로 루프 변수와 같은 역할을 합니다.

package Iterator;

public interface Iterator<E> {
    public abstract boolean hasNext();
    public abstract E next();
    
}

여기서 선언된 메소드는 두 가지입니다. 다음 요소가 존재하는지 알아보는 hasNext 메소드와 다음 요소를 가져오는 next 메소드 입니다.

hasNext 메소드의 반환값이 boolean형인 이유는 다음 요소가 존재하면 true고 아니면 false를 반환 합니다. 즉, 마지막 요소에 도달 하면 false가 반환이 되는 것이고 루프 종료 조건으로 사용하기 위한 것입니다.

next 메소드는 집합체의 요소를 1개 반환합니다. 하지만 next 메소드가 하는 일은 그뿐만 아니라 다음 next 메소드를 호출 할 때 제대로 다음 요소를 반환할 수 있도록 내부 상태를 다음으로 진행시켜 놓는 역할이 뒤에 숨어 있습니다.

package Iterator;

public class Book {
    private String name;

    public Book(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

책을 나타내는 클래스입니다. 하지만 할 수 있는 일은 책 이름을 getName 메소드로 얻는 것 뿐입니다. 책 이름은 생성자에서 인스턴스를 초기화할 때 인수로 저장합니다.

package Iterator;
import java.util.Iterator;

public class BookShelf implements Iterable<Book>{
    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 appendBook(Book book) {
        this.books[last] = book;
        last++;
    }

    public int getLength() {
        return last;
    }

    @Override
    public Iterator<Book> iterator() {
        return new BookShelfIterator(this);
    }

}

이 책장에는 books라는 필드가 있습니다. 이 필드는 Book 배열입니다. 이 배열의 크기는 처음에 BookShelf 인스턴스를 만들 때 인수(maxsize)로 지정합니다.

그 다음 살펴볼 것은 iterator 메소드 입니다. 이 메소드는 BookShelf 클래스에 대응하는 Iterator로서 BookShelfIterator 클래스의 인스턴스를 생성하며 반환합니다. 책장에 꽂혀 있는 책을 반복해서 처리하고 싶을 때 iterator 메소드를 호출합니다.

package Iterator;
import java.util.Iterator;
import java.util.NoSuchElementException;

public class BookShelfIterator implements Iterator<Book> {
    
    private  BookShelf bookShelf;
    private int index;
    
    public BookShelfIterator(BookShelf bookShelf) {
        this.bookShelf = bookShelf;
        this.index = 0;
    }

    @Override
    public boolean hasNext() {
        if(index < bookShelf.getLength()) {
            return true;
        } else {
            return false;
        }
    }

    @Override
    public Book next() {
        if(!hasNext()) {
            throw new NoSuchElementException();
        }
        Book book = bookShelf.getBookAt(index);
        index++;
        return book;
    }
}

BookShelfIterator는 Iterator<Book> 구현하고 있으므로 Iterator<Book>형으로 다룰 수 있습니다. bookShelf 필드는 BookShelfIterator가 검색할 책장이고 , index 필드는 현재 보고 있는 책을 가리키는 첨자입니다.

생성자에서는 전달된 BookShelf 인스턴스를 bookShelf 필드에 저장하고 index를 0으로 지정합니다.

hasNext 메소드는 Iterator<Book> 인터페이스에서 선언된 메소드를 구현한 것입니다. 다음 책이 있는지 조사해서 있으면 true 아니면 false를 반환합니다. 다음 책이 있는지 없는지는 index가 책장에 꽂힌 책의 수(bookShelf.getLength() 값)보다 작은지 비교해서 판정합니다.

next 메소드는 현재 주목하는 책(Book 인스턴스)을 반환하고 다시 다음으로 진행시키는 메소드입니다. 이 메소드도 Iterator<Book> 인터페이스로 선언되어 있는데 먼저 반환값으로 돌려 줄 책을 book 변수에 저장해 두고, index를 다음으로 진행시킨 후 book을 반환합니다. index를 다음으로 진행하는 처리는 서두에서 언급한 for문의 i++에 해당하는 처리입니다. 루프 변수를 다음으로 진행한 것입니다.

package Iterator;

import java.util.Iterator;

public class Main {
    public static void main(String[] args) {
        BookShelf bookShelf = new BookShelf(4);
        bookShelf.appendBook(new Book("Around the World in 80 days"));
        bookShelf.appendBook(new Book("Bible"));
        bookShelf.appendBook(new Book("Cinderella"));
        bookShelf.appendBook(new Book("JAVA"));

        // 명시적으로 Iterator를 사용하는 방법
        Iterator<Book> it = bookShelf.iterator();
        while (it.hasNext()) {
            Book book = it.next();
            System.out.println(book.getName());
        }
        System.out.println();

//        // 확장 for문을 사용하는 방법
//        for(Book book : bookShelf) {
//            System.out.println(book.getName());
//        }
//        System.out.println();
    }
}

첫 번째는 명시적으로 Iterator을 사용하는 방법입니다. bookShelf.iterator()로 얻은 it이 책장을 검색할 때 사용할 Iterator<Book>의 인스턴스입니다. while문 조건에 it.hasNext()라고 쓰면, 검색하지 않은 책이 남아 있는 한 while 문의 루프가 돌아갑니다. 그리고 루프 안에서 it.next()로 책을 가져오 표시합니다.

두 번째는 확장 for문을 사용하는 겁니다. 확장 for문은 직전의 while문과 완전히 같은 동작을 합니다. 즉, 확장 for문을 사용하면 Iterator를 사용한 반복 처리를 간결하게 기술할 수 있습니다. 일반적으로 Java의 확장 for문은 Iterable 인터페이스를 구현한 클래스의 인스턴스에 대해 내부적으로 Iterator를 사용하여 처리합니다. 결국 확장 for문 배후에서는 Iterator 패턴이 사용된다고 볼 수 있습니다.

어떻게 구현하던 Iterator를 사용할 수 있다.

for문을 안쓰고 Iterator을 사용하는 이유는 Iterator를 사용함으로써 구현과 분리하여 반복할 수 있기 때문입니다.

   while (it.hasNext()) {
            Book book = it.next();
            System.out.println(book.getName());
        }

여기에서 사용한 것은 hasNext와 next라는 Iterator의 메소드 뿐입니다. BookShelf에서 사용된 메소드는 호출되지 않습니다. 즉, 여기 while 루프에서는 BookShelf 구현에 의존하지 않습니다.

BookShelf를 구현한 사람이 배열로 책을 관리하는 것을 그만두고 java.util.ArrayList를 사용하도록 프로그램을 변경했다고 합시다. BookShelf를 어떻게 변경을 하던 BookShelf가 iterator 메소드를 가지고 있고 올바른 Iterator<Book>을 반환하면 위의 while 루프는 변경하지 않아도 동작합니다.

이 사실은 BookShelf 사용자에게는 아주 반가운 소식입니다. 디자인 패턴은 클래스 재사용을 촉진합니다. 재사용을 촉진한다는 말은 클래스를 부품처럼 사용할 수 있게 만들어 어떤 한 부품을 수정하더라도 다른 부품을 수정할 일이 적어진다는 것입니다.

연습문제

BookShelf는 배열이라 처음 지정한 책장 크기를 넘을 수 없습니다. 그러므로 배열인 아닌 리스트로 변경하겠습니다.

package Iterator;

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

public class BookShelf implements Iterable<Book>{
    private List<Book> books;
    private int last =0;

    public BookShelf(int last) {
        this.books = new ArrayList<Book>(last);
    }

    public Book getBookAt(int index) {
        return books.get(index);
    }

    public void appendBook(Book book) {
        this.books.add(last, book);
        last++;
    }

    public int getLength() {
        return books.size();
    }

    @Override
    public Iterator<Book> iterator() {
        return new BookShelfIterator(this);
    }

}
package Iterator;

import java.util.Iterator;

public class Main {
    public static void main(String[] args) {
        BookShelf bookShelf = new BookShelf(4);
        bookShelf.appendBook(new Book("Around the World in 80 days"));
        bookShelf.appendBook(new Book("Bible"));
        bookShelf.appendBook(new Book("Cinderella"));
        bookShelf.appendBook(new Book("JAVA"));
        bookShelf.appendBook(new Book("Spring"));
        bookShelf.appendBook(new Book("SpringBoot"));
        bookShelf.appendBook(new Book("JPA"));

        // 명시적으로 Iterator를 사용하는 방법
        Iterator<Book> it = bookShelf.iterator();
        while (it.hasNext()) {
            Book book = it.next();
            System.out.println(book.getName());
        }
        System.out.println();

    }
}

Adapter

프로그램 세계에서 이미 제공된 코드를 그대로 사용할 수 없을 때 필요한 형태로 반환한 후 이용하는 경우가 자주 있습니다. 이미 제공된 것과 필요한 것 사이의 차이를 메우는 디자인 패턴이 바로 Adapter패턴입니다.

Adapter 패턴은 Wrapper 패턴이라고 불리기도 합니다. 래퍼(wrapper)는 감싸는것을 의미합니다. 무엇인가 포장해서 다른 용도로 사용할 수 있도록 변환해주는 것이 래퍼이자 어댑터입니다.

Adapter 패턴은 다음과 같은 두 종류가 있습니다.

  • 클래스에 의한 Adapter 패턴(상속을 위한 패턴)
  • 인스턴스에 의한 Adapter 패턴(위임을 사용한 패턴)

미리 제공되는 클래스

package Adapter;

public class Banner {
    private String string;

    public Banner(String string) {
        this.string = string;
    }

    public void showWithParen() {
        System.out.println("(" + string + ")");
    }

    public void showWithAster() {
        System.out.println("*" + string + "*");
    }
}

필요로 하는 인터페이스

package Adapter;

public interface Print {
    public abstract void printWeak();
    public abstract void printStrong();
}

PrintBanner은 어댑터 역할을 합니다. 준비된 Banner 클래스를 확장(extends)하여 showWithParen 메소드와 showWithAster 메소드를 상속받으며, 필요한 Print 인터페이스를 구현(implements)하여 printWeak 메소드와 printStrong이라는 두 개의 메소드가 printBanner 클래스에서 멋대로 만든게 아니라 Print 인터페이스에서 선언된 메소드를 오버라이드하여 구현한 것임을 나타냅니다.

package Adapter;

public class PrintBanner extends Banner implements Print{

    public PrintBanner(String string) {
        super(string);
    }

    @Override
    public void printWeak() {
        showWithParen();
    }

    @Override
    public void printStrong() {
        showWithAster();
    }
}

Main 클래스는 어댑터 역할을 하는 PrintBanner 클래스를 이용해 Hello 문자열을 표시합니다.

package Adapter;

public class Main {
    public static void main(String[] args) {
        Print p = new PrintBanner("Hello");
        p.printWeak();
        p.printStrong();
    }
}

여기까지는 클래스를 사용한 Adapter 패턴이였습니다. 이번에는 인스턴스를 사용한 Adapter 패턴을 살펴보겠습니다. 앞에서는 상속을 사용했지만 이번에는 위임을 사용하겠습니다.

위임

Java에서 위임은 어떤 메소드의 실제 처리를 다른 인스턴스의 메소드에 맡기는 것을 말합니다.

Main 클래스와 Banner 클래스는 위의 예제와 같으며 실행 결과도 동일합니다. 하지만 Print는 인터페이스가 아니고 클래스라고 가정합니다. 즉, Banner 클래스를 이용하여 Print 클래스와 같은 메소드를 갖는 클래스를 실현하려는 것입니다. Java에서는 두 개의 클래스를 동시에 상속할 수 없습니다. 다시 말해, PrintBanner 클래스를 Print와 Banner 양쪽의 하위 클래스로 정의할 수 없습니다.

PrintBanner 클래스는 banner 필드로 Banner 클래스의 인스턴스를 가집니다. 이 인스턴스는 PrintBanner 클래스의 생성자에서 생성합니다. printWeak 및 printStrong 메소드에서는 그 banner 필드를 통해 showWithParen, showWithAster 메소드를 호출합니다.

PrintBanner 클래스의 printWeak와 printStrong 메소드는 Print 클래스에서 선언된 메소드를 오버라이드하여 구현했으므로 메소드 앞에 @Override 어노테이션을 사용했습니다.

Adapter 패턴의 등장인물

Target(대상) 역

지금 필요한 메소드를 결정합니다. 예제 프로그램에서는 Print 인터페이스(상속)와 Print 클래스(위임)가 이 역할을 맡았습니다.

Client(의뢰자) 역

Target의 메소드를 사용해 일합니다. 예제 프로그램에서는 Main 클래스가 여기에 해당합니다.

Adaptee(적응 대상자) 역

Adapt-er(적응자)가 아니라 Adapt-ee(적응 대상자)입니다. Adaptee는 이미 준비된 메소드를 가지는 역할입니다. 예제 프로그램에서는 Banner 클래스가 이 역할을 맡았습니다. Adapter의 메소드가 Target의 메소드와 일치한다면 다음에 소개할 Adapter는 등장할 필요가 없습니다.

Adapter(적응자) 역

Adapter 패턴의 주인공입니다. Adapter의 메소드를 사용해서 어떻게든 Target을 만족시키는 것이 Adapter 패턴의 목적이며 Adapter의 임무입니다. 교류 100볼트를 직류 12볼트로 변환하는 어댑터에 해당합니다. 예제 프로그램에서는 PrintBanner 클래스가 이 역할을 수행합니다. 클래스에 의한 Adapter 패턴일 때는 Adapter는 상속하여 Adaptee를 이용합니다. 반면에 인스턴스에 의한 Adapter 패턴일때는 위임gkdu Adaptee를 이용합니다.

독자 사고를 넓혀주는 힌트

어떤 경우에 사용하는 것일까?

필요한 메소드가 있으면 그냥 프로그래밍하면 되지 왜 Adapter 패턴 같은 걸 생각해야 하느냐고 의아해할 수 있다. 프로그래밍할 때 늘 백지 상태에서 시작하는 것이 아닙니다. 이미 존재하는 클래스를 이용하는 경우도 흔합니다.

Adapter 패턴은 기존 클래스에 한겹 덧씌어 필요한 클래스를 만듭니다. 이 패턴을 사용하면 필요한 메소드군을 빠르게 만들 수 있습니다. 만약 버그가 발생하더라도 기존 클래스(Adaptee 역)에는 버그가 없는 것을 알고 있으므로, Adapter 역의 클래스를 중점적으로 살펴보면 프로그램 검사가 매우 편해집니다.

비록 소스가 없더라도

이미 만들어진 클래스가 있고 새로운 인터페이스(API)에 맞춘다고 생각하면, Adapter 패턴을 사용하는게 당연하게 느껴집니다. Adapter 패턴은 기존 클래스를 전혀 수정하지 않고 목적한 인터페이스(API)에 맞추려는 겁니다. 또한 Adapter 패턴에서는 기존 클래스의 소스 프로그램이 반드시 필요한 것이 아닙니다. 기존 클래스의 사양만 알면 새로운 클래스를 만들 수 있습니다.

버전 업과 호환성

소프트웨어는 버전 업이 필요합니다. 소프트웨어를 버전 업할 때는 구버전과의 호환성이 문제가 됩니다. 흔히 레거시 시스템(legacy system)으로도 불리는 구버전을 버리면 소프트웨어 유지 보수는 편해지지만, 항상 그럴 수 있는 것은 아닙니다. Adapter 패턴은 신버전과 구버전을 공존시키고, 유지 보수까지 편하게 하도록 도와줍니다.

예를들어, 앞으로 신버전만 유지 보수하고 싶을 때는 신버전을 Adaptee 역으로 하고, 구버전을 Target 역으로 합니다. 그리고 신버전의 클래스를 사용하여 구버전의 메소드를 구현하는 Adapter 역할 클래스를 만듭니다.

상속과 위임 어느쪽을 사용해야 할까?

상속과 위임을 사용한 두 가지 예제 프로그램을 소개했는데, 실제로는 어느 것을 사용해야 할까? 일반적으로는 상속을 사용하는 것보다 위임을 사용하는 것이 문제가 적습니다. 그 이유는 상위 클래스의 내부 동작을 자세히 모르면, 상속을 효과적으로 사용하기 어려운 경우가 많기 때문입니다.

관련 패턴

Brige 패턴

Adapter 패턴은 인터페이스(API)가 서로 다른 클래스를 연결하는 패턴입니다. Brige는 기능 계층과 구현 계층을 연결하는 패턴

Decorator 패턴

Adapter 패턴은 인터페이스(API)의 차이를 메우는 패턴입니다. Decorator 패턴은 인터페이스(API)를 변경하지 않고 기능을 추가하는 패턴


Template Method 패턴

템플릿이란 문자 모양대로 구멍이 난 얇은 플라스틱 판입니다.

Template Metohd 패턴은 탬플릿 기능을 가진 패턴입니다. 상위 클래스 쪽에 템플릿이 될 메소드가 정의되어 있고, 그 메소드 정의에 추상 메소드가 사용됩니다. 상위 클래스의 코드만 봐서는 최종적으로 어떻게 처리되는지 알 수 없습니다. 상위 클래스로 알 수 있는 것은 추상 메소드를 호출하는 방법뿐입니다.

추상 메소드를 실제로 구현하는 것은 하위 클레스입니다. 하위 클래스에서 메소드를 구현하면 구체적인 처리 방식이 정해집니다. 다른 하위 클래스에서 구현을 다르게 하면 처리도 다르게 이루어집니다. 그러나, 어느 하위 클래스에서 어떻게 구현하더라도 처리의 큰 흐름은 상위 클래스에서 구성한대로 됩니다.

이처럼 상위 클래스에서 처리의 뼈대를 결정하고 하위 클래스에서 그 구체적인 내용을 결정하는 디자인 패턴을 Template Method 패턴이라고 부릅니다.


Factory Method 패턴

인스턴스를 생성하는 공장을 Template Method 패턴으로 구성한 것이 Factory Method 패턴입니다. Factory Method 패턴에서는 인스턴스 생성 방법을 상위 클래스에 결정하되, 구체적인 클래스 이름 까지는 결정하지 않습니다. 구체적인 살은 모두 하위 클래스에서 붙입니다. 이로써 인스턴스 생성을 위한 뼈대(프레임워크)와 실제 인스턴스를 생성하는 클래스를 나누어 생각할 수 있게 됩니다.

public abstract class Product {
    public abstract void use();
}
public abstract class Factory {
    public final Product create(String owner) {
        Product p = createProduct(owner);
        registerProduct(p);
        return p;
    }

    protected abstract Product createProduct(String owner);
    protected abstract void registerProduct(Product product);
}
public class IDCard extends Product{
    private String owner;

    IDCard(String owner) {
        System.out.println(owner + "의 카드를 만듭니다.");
        this.owner = owner;
    }

    @Override
    public void use() {
        System.out.println(this + "을 사용합니다.");
    }

    @Override
    public String toString() {
        return "[IDCard : " +owner + "]";
    }

    public String getOwner() {
        return owner;
    }
}
public class IDCardFactory extends Factory{

    @Override
    protected Product createProduct(String owner) {
        return new IDCard(owner);
    }

    @Override
    protected void registerProduct(Product product) {
        System.out.println(product + "을 등록했습니다.");
    }
}
public class Main {
    public static void main(String[] args) {
        Factory factory = new IDCardFactory();
        Product card1 = factory.create("Youngjin Kim");
        Product card2 = factory.create("김민주");
        Product card3 = factory.create("김채원");

        card1.use();
        card2.use();
        card3.use();
    }
}

Abstract Factory 패턴

추상적인 공장이 등장하고 추상적인 부품을 조합하여 추상적인 제품을 만듭니다. 부품의 구체적인 구현에는 주목하지 않고 인터페이스(API)에 주목합니다. 그리고 그 인터페이스만 사용해서 부품을 조립하고 제품으로 완성하는 것입니다.

profile
발전하기 위한 공부

0개의 댓글