[CS] 디자인 패턴

young-gue Park·2023년 8월 2일
0

CS

목록 보기
1/12
post-thumbnail

자바와 자바스크립트를 모두 사용하여 설명한다.

사용 교재) 면접을 위한 CS 전공지식 노트 (주홍철, 2022)


⚡ 디자인 패턴


🔷 프로그램을 설계할 때 발생했던 문제점들을 객체 간의 상호 관계 등을 이용하여 해결할 수 있도록 하나의 '규약' 형태로 만들어 놓은 것

  • 라이브러리나 프레임워크의 기본이 되는 것이 디자인 패턴이다.

💡 라이브러리: 공통으로 사용될 수 있는 특정한 기능들을 모듈화한 것, 폴더명이나 파일명에 대한 규칙이 없고 프레임워크에 비해 자유롭다. 배운 것 중에는 React가 대표적인 라이브러리.

💡 프레임워크: 공통으로 사용될 수 있는 특정한 기능들을 모듈화한 것을 의미한다. 폴더명, 파일명데 대한 규칙이 있고 라이브러리에 비해 더 엄격하다. 배운 것 중에는 Spring이 대표적인 프레임워크.

📌 싱글톤 패턴 (singleton pattern)

🔷 하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴

  1. 사용처: 데이터베이스 연결 모듈

  2. 장점: 인스턴스를 생성할 때 드는 비용이 줄어든다.

  3. 단점: 의존성이 높아진다.

의존성이 왜 위험한가?
의존 관계에서 하나가 변경되면 그것을 의존하는 다른 것들에게까지 영향을 끼친다. 특히 순환 의존의 경우에는 자기 자신까지도 영향이 돌아올 수 있어 위험성이 높다. 또한 의존성이 높으면 모듈을 따로 떼어내어 테스트하기 어려워지며 모듈의 재사용성을 떨어뜨린다. (SOLID)

  • 이러한 단점 해결을 위해 의존성 주입을 이용할 수 있다.
    • 마이그레이션 하기 수월하고 테스팅하기 쉬워지며 모듈 간의 관계들이 더 명확해진다.
    • 다만 클래스 수가 늘어나 더 복잡해지며 런타임 페널티가 생길 수 있다.

💡 의존성 주입
메인 모듈이 직접 다른 하위 모듈에 의존성을 주는게 아니라 의존성 주입자(dependency injector)가 대신 주게 만들어 간접적으로 의존성을 주입하는 방식, 이로써 모듈 간의 결합을 느슨하게 만들며 이를 '디커플링 되다' 라고 한다.

⭐ 자바스크립트

  • 자바스크립트에서는 리터럴 {} 또는 new object로 객체를 생성하면 다른 어떤 객체와도 같지 않기 때문에 이 자체만으로 싱글톤 패턴을 구현할 수 있다.
const obj = {
	num: 1
}

const objOther = {
	num: 1
}

console.log(obj===objOther); // false

근데 뭔가 많이 없어보인다. 검증 치고는 허술하다.
그래서 보통 이런 방식을 사용한다.

class Singleton {
	constructor() {
        if(!Singleton.instance) {
            Singleton.instance = this;
        }
        return Singleton.instance;
    }

    getInstance() {
        return this.instance;
    }
} 

const a = new Singleton();
const b = new Singleton();

console.log(a===b);

  • Node.js에서 MongoDB를 연결할 때 쓰는 mongoose 모듈에서 볼 수 있다.
// connect() 함수
Mongoose.prototype.connect = function(uri, options, callback) {
    const _mongoose = this instanceof Mongoose ? this : mongoose;
    const conn = _mongoose.connection;

    return _mongoose_promiseOrCallback(callback, cb => {
        conn.openUri(uri, options, err => {
            if(err != null) {
                return cb(err);
            }
            return cb(err);
        });
    });
};
  • 그 외에도 MySQL 등 DB와의 연결 시에 주로 쓰이는 편이다.

⭐ 자바

  • 자바로는 이전에 다뤘으니 넘어간다.

이전 포스팅


📌 팩토리 패턴 (factory pattern)

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

  • 상위 클래스와 하위 클래스가 분리되어 결합이 느슨해지며 상위 클래스에서 인스턴스 생성 방식에 대해 알 필요가 사라져 유연성이 늘어난다. 또한 객체 생성 로직이 분리되어 있기 때문에 코드 리팩터링 시에도 고칠 곳이 줄어들어 유지 보수성이 증가된다.
  • 들어오는 레시피(하위 클래스)에 따라 다른 물건을 생산하는 공장(상위 클래스)

⭐ 자바스크립트

  • 전달받은 값에 따라 다른 타입의 객체를 생성하는 자바스크립트는 간단한 선언 만으로도 패턴 구현이 가능하다.
const num = new Object(1);
const str = new Object('팩토리');
num.constructor.name; // Number
str.constructor.name; // String

인스턴스 생성 메서드를 정적 메서드로 만들면 클래스의 인스턴스 없이 호출이 가능하여 메모리 절약이 가능하고, 개별 인스턴스에 묶이지 않으며 클래스 내의 함수를 정의할 수 있다.
거기에 의존성 주입까지 함께 응용하면 이렇다.

class Sonata {
    constructor() {
        this.name = "sonata"
    }
}

class Porsche {
    constructor() {
        this.name = "porsche"
    }
}

class Genesis {
    constructor() {
        this.name = "genesis"
    }
}

class SonataFactory {
    static createCar() {
        return new Sonata()
    }
}

class PorscheFactory {
    static createCar() {
        return new Porsche()
    }
}

class GenesisFactory {
    static createCar() {
        return new Genesis()
    }
}

const factoryList = {SonataFactory, PorscheFactory, GenesisFactory}

class CarFactory {
    static createCar(type) {
        const factory = factoryList[type]
        return factory.createCar()
    }
}

const main = () => {
    const car = CarFactory.createCar("PorscheFactory")
    console.log(car.name) 
}

main()

⭐ 자바

  • 자바는 어떨까?
abstract class Car {
	public abstract int getPrice();
	
	@Override
	public String toString() {
		return "이 차는 " + this.getPrice() + "원입니다.";
	}
}

class CarFactory {
	public static Car getCar(String type, int price) {
		if("Porsche".equalsIgnoreCase(type)) return new Porsche(price);
		else if ("Sonata".equalsIgnoreCase(type)) return new Sonata(price);
		else if ("Genesis".equalsIgnoreCase(type)) return new Genesis(price);
		else return new DefaultCar();
	}
}

class DefaultCar extends Car {
	private int price;
	
	public DefaultCar() {
		this.price = -1;
	}
	
	@Override
	public int getPrice() {
		return this.price;
	}
}

class Porsche extends Car {
	private int price;
	
	public Porsche(int price) {
		this.price = price;
	}
	
	@Override
	public int getPrice() {
		return this.price;
	}
}

class Sonata extends Car {
	private int price;
	
	public Sonata(int price) {
		this.price = price;
	}
	
	@Override
	public int getPrice() {
		return this.price;
	}
}

class Genesis extends Car {
	private int price;
	
	public Genesis(int price) {
		this.price = price;
	}
	
	@Override
	public int getPrice() {
		return this.price;
	}
}


public class Main {
	public static void main(String[] args) {
		Car porsche = CarFactory.getCar("Porsche", 178000000);
		Car sonata = CarFactory.getCar("Sonata", 80000000);
		Car genesis = CarFactory.getCar("Genesis", 102000000);
		
		System.out.println("포르쉐 공장: " + porsche);
		System.out.println("소나타 공장: " + sonata);
		System.out.println("제네시스 공장: " + genesis);
	}
}

저기서 if문을 쓰지 않고 Enum 혹은 Map을 이용하여 매핑해서 로직을 구성할 수도 있다.

💡 Enum
상수의 집합을 정의할 때 사용되는 타입, 상수나 메서드 등을 집어넣어서 관리하며 코드를 리팩터링할 때 해당 집합에 관한 로직 수정 시 이 부분만 수정하면 되므로 코드 리팩터링 시 강점이 생긴다.


📌 전략 패턴 (strategy pattern)

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

💡 컨텍스트
상황, 맥락, 문맥을 의미하며 개발자가 어떠한 작업을 완료하는 데 필요한 모든 관련 정보를 말한다.

  • 쉽게 드는 예시로 온라인 쇼핑 시에 결제 수단을 선택하는 것과 같다.

⭐ 자바스크립트

  • Node.js에서 인증 모듈을 구현할 때 쓰는 미들웨어 라이브러리인 passport가 대표적인 전략 패턴 활용 사례이다. 서비스 내의 회원 가입된 아이디와 비밀번호를 기반으로 인증하는 LocalStrategy 전략과 네이버와 같은 다른 서비스를 기반으로 인증하는 OAuth 전략 등을 지원한다.
var passport = require('passport'),
    LocalStrategy = require('passport-local').strategy;

passport.use(new LocalStrategy(
    function(username, password, done) {
        username.findOne({ username: username }, function (err, user) {
            if(err) {return done(err);}
            if(!user) {
                 return done(null, false, {message: '잘못된 이름입니다.'});
                }
            if(!user.validPassword(password)) {
                return done(null, false, {message: '잘못된 비밀번호입니다.'});
            }
            return donw(null, user);
        });
    }
));
  • passport.use()에 전략을 매개변수로 넣어서 로직을 수행한다.

⭐ 자바

  • 설명했던 예시처럼 쇼핑 카트에 아이템을 담아 두 개의 결제 방식을 전략으로 결제하는 코드
import java.util.ArrayList;
import java.util.List;

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

class SamsungCardStrategy implements PaymentStrategy {
	private String name;
	private String cardNumber;
	private String cvv;
	private String dateOfExpiry;
	
	public SamsungCardStrategy(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 + "원이 삼성카드로 결제되었습니다.");
	}
}

class KAKAOPayStrategy implements PaymentStrategy {
	private String userId;
	private String password;
	
	
	public KAKAOPayStrategy(String id, String pwd) {
		this.userId = id;
		this.password = pwd;
	}
	
	@Override
	public void pay(int amount) {
		System.out.println(amount + "원이 카카오페이로 결제되었습니다.");
	}
}

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 calculate() {
		int sum = 0;
		for(Item item : items) {
			sum += item.getPrice();
		}
		return sum;
	}
	
	public void pay(PaymentStrategy paymentMethod) {
		int amount = calculate();
		paymentMethod.pay(amount);
	}
}

public class Shopping {

	public static void main(String[] args) {
		ShoppingCart cart = new ShoppingCart();
		
		Item A = new Item("팔도비빔면", 4000);
		Item B = new Item("바닐라 샴푸", 8500);
		Item C = new Item("서울우유", 2400);
		
		cart.addItem(A);
		cart.addItem(B);
		cart.addItem(C);
		
		cart.pay(new SamsungCardStrategy("Park younggue", "77777777", "123", "0525"));
		cart.pay(new KAKAOPayStrategy("dudrb5260@naver.com", "777777"));

	}

}


📌 옵저버 패턴 (observer pattern)

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

💡 옵저버
객체의 상태 변화에 따라 전달되는 메서드 등을 기반으로 '추가 변화 사항'이 생기는 객체들

  • 주체와 객체를 따로 두지 않고 상태가 변경되는 객체를 기반으로 구축하기도 한다.
  • 옵저버 패턴 대표로 트위터가 있다.
  • 주로 이벤트 기반 시스템에 사용하며 MVC 패턴에도 사용된다.

⭐ 자바

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 Shopping {
	public static void main(String[] args) {
		Topic topic = new Topic();
		Observer a = new TopicSubscriber("박영규", topic);
		Observer b = new TopicSubscriber("조강원", topic);
		Observer c = new TopicSubscriber("이승열", topic);
		
		topic.register(a);
		topic.register(b);
		topic.register(c);
		
		topic.postMessage("아무무는 쓰레기 챔피언이 맞다.");
	}
}
  • topic은 주체이자 객체가 된다.

⭐ 자바스크립트

  • 프록시 객체를 통해 구현할 수 있다.

💡 프록시(proxy) 객체
어떠한 대상의 기본적인 동작(속성 접근, 할당, 순회, 열거, 함수 호출 등)의 작업을 가로챌 수 있는 객체를 뜻하며, 자바스크립트에서 프록시 객체는 두 개의 매개변수를 가진다.
target: 프록시할 대상
handler: 프록시 객체의 target 동작을 가로채서 정의할 동작들이 정해져 있는 함수

const handler = {
	get: function(target, name) {
		return name === 'name' ? `${target.a} ${target.b}` : target[name]
    }
}

const p = new proxy({a: 'Bzeromo', b: '는 원딜 황제'}, handler)
console.log(p.name) // Bzeromo는 원딜 황제
  • Vue.js 3.0 프레임워크에서 ref나 reactive로 정의하면 해당 값이 변경되었을 때 자동으로 DOM에 있는 값이 변경되는데, 이는 앞서 설명한 프록시 객체를 이용한 옵저버 패턴을 이용하여 구현한 것이다.

📌 프록시 패턴 (proxy pattern)

🔷 대상 객체에 접근하기 전 그 접근에 대한 흐름을 가로채 대상 객체 앞단의 인터페이스 역할을 하는 디자인 패턴

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

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

⭐ 프록시 서버

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

  • Node.js서버를 운영할 때 서버 앞단의 프록시 서버로 nginx가 주로 쓰인다.
  • CloudFlare
    • 전 세계에 분산된 서버가 있고 이를 통해 어떠한 시스템의 콘텐츠 전달을 빠르게 할 수 있는 CDN 서비스

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

    • DDOS 공격 방어, HTTPS 구축에 이점이 있다.
profile
Hodie mihi, Cras tibi

0개의 댓글