디자인 패턴
프로그램을 설계할 때 발생했던 문제점들을 객체 간의 상호 관계 등을 이용해 해결할 수 있도록 하나의 '규약' 형태로 만들어 놓은 것
디자인 패턴을 사용하면 상황에 맞는 올바른 설계를 더 빠르게 적용할 수 있고, 각 패턴의 장단점을 통해서 설계를 선택하는데 도움을 얻을 수 있다. 또한, 설계 패턴에 이름을 붙임으로써 시스템의 유지 보수에 도움을 얻을 수 있다.
싱글톤 패턴 | 팩토리 패턴 | 전략 패턴 | 옵저버 패턴 | 프록시 패턴 |
이터레이터 패턴 | 노출모듈 패턴 | MVC 패턴 | MVP 패턴 | MVVM 패턴 |
1. 싱글톤 패턴(Singleton pattern)
싱글톤 패턴: 하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴, 보통 데이터베이스 연결 모듈에 많이 사용함
- 하나의 인스턴스를 만들어 놓고 해당 인스턴스를 다른 모듈들이 공유하며 사용한다.
- TDD(Test Driven Development)를 할 때 단위 테스트를 주로 하는데, 단위 테스트는 테스트가 서로 독립적이어야 하며 테스트를 어떤 순서로든 실행할 수 있어야 한다. 하지만 싱글톤 패턴은 미리 생성된 하나의 인스턴스를 기반으로 구현하는 패턴이기 때문에 각 테스트마다 독립적인 인스턴스를 만들기 어렵다.
장점 | 단점 |
사용하기 쉽고 굉장히 실용적이다. | 의존성이 높아져서 모듈 간의 결합을 강하게 만들어서 단위 테스트를 할 때 좋지 않다. |
인스턴스를 생성하는 데 드는 비용이 줄어든다. |
의존성 주입(DI, Dependency Injection)을 통해 모듈간의 결합을 조금 더 느슨하게 만들어 싱글톤 패턴의 단점을 해결할 수 있다!
- 의존성 == 종속성
- A가 B에 의존성이 있다 == B의 변경사항에 대해 A도 변경해야 된다.
즉 메인 모듈이 '직접' 다른 하위 모듈에 대한 의존성을 주기 보다는 중간에 의존성 주입자(DI)를 활용해서 메일 모듈이 '간접'적으로 의존하는 방식이다.
이를 통해서 메인 모듈(상위 모듈)은 하위 모듈에 대한 의존성이 떨어지게 된다. == 디커플링이 된다.
장점 | 단점 |
모듈들을 쉽게 교체할 수 있는 구조가 되어 테스팅 하기 쉽고 마이그레이션하기도 수월하다. | 모듈들이 더욱더 분리되므로 클래스 수가 늘어나서 복잡성이 증가될 수 있다. |
구현할 때 추상화 레이어를 넣고 이를 기반으로 구현체를 넣기 때문에 애플리케이션 의존성 방향이 일관되고 애플리케이션을 쉽게 추론 가능하다. | 약간의 런타임 패널티가 생기기도 한다. |
모듈간의 관계들이 좀 더 명확해진다. |
의존성 주입 원칙
상위 모듈은 하위 모듈에서 어떠한 것도 가져오지 않아야 한다. 또한 둘 다 추상화에 의존해야 하며, 이 때 추상화는 세부 사항에 의존하지 말아야 한다.
2. 팩토리 패턴(Factory pattern)
팩토리 패턴: 객체를 사용하는 코드에서 객체 생성 부분을 떼어내 추상화한 패턴
상속 관계에 있는 두 클래스에서 상위 클래스가 중요한 뼈대를 결정하고, 하위 클래스에서 객체 생성에 관한 구체적인 내용을 결정하는 패턴 => 즉, 객체를 생성하는 인터페이스는 미리 정의하지만 인스턴스를 만들 클래스의 결정은 서브 클래스 쪽에서 한다.
- 상위 클래스와 하위 클래스가 분리되기 때문에 느슨한 결합을 가진다. => 상위 클래스에서는 인스턴스 생성 방식에 대해 전혀 알 필요가 없기 때문에 더 많은 유연성을 갖게 된다.
- 객체 생성 로직이 따로 뗴어져 있기 때문에 코드를 리팩터링하더라도 한 곳만 고칠 수 있게 되면서 유지 보수성이 증가된다.
- ex) 라떼 레시피, 아메리카노 레시피, 우유 레시피라는 구체적인 내용이 들어가있는 하위 클래스가 컨베이어 벨트를 통해 전달되고, 상위 클래스인 바리스타 공장에서 이 레시피들을 토대로 우유 등을 생산!
JAVA 예) Computer: 상위 클래스 / PC extends Computer / ComputerFactory: 팩토리 클래스
//TestFactory
public class TestFactory {
public static void main(String[] args) {
Computer pc = ComputerFactory.getComputer("pc", "2GB", 500000, "2.4 GHz");
System.out.println("Factory PC Config::"+pc);
}
}
// Super Class 슈퍼클래스
abstract class Computer {
public abstract String getRAM();
public abstract int getPrice();
public abstract String getCPU();
@Override
public String toString() {
return "RAM = "+this.getRAM()+", Price = "+this.getPrice()
+", CPU = "+this.getCPU();
}
}
// Sub Class - 1 (PC)
class PC extends Computer {
private String ram;
private int price;
private String cpu;
public PC(String ram, int price, String cpu) {
this.ram = ram;
this.price = price;
this.cpu = cpu;
}
@Override
public String getRAM() {
return this.ram;
}
@Override
public int getPrice() {
return this.price;
}
@Override
public String getCPU() {
return this.cpu;
}
}
// Factory Class
class ComputerFactory {
public static Computer getComputer(String type, String ram, int price, String cpu) {
if("PC".equalsIgnoreCase(type)) {
return new PC(ram, price, cpu);
}
return null;
}
}
Factory PC Config::RAM = 2GB, Price = 500000, CPU = 2.4 GHz
이렇게 구현하게 되면 Computer 클래스에 더 많은 하위클래스가 추가된다고 해도 Factory class의 getComputer( )를 통해 인스턴스를 제공받던 Application의 코드는 수정할 필요가 없게 된다.
3. 전략 패턴(Strategey pattern)
전략 패턴: 정책 패턴이라고도 하며, 객체의 행위를 바꾸고 싶은 경우 '직접' 수정하지 않고 전략이라고 부르는 '캡슐화한 알고리즘'을 컨텍스트안에서 바꿔주면서 상호 교체가 가능하게 만드는 패턴
- 컨텍스트: 개발자가 어떠한 작업을 완료하는 데 필요한 모든 관련 정보
- 객체가 할 수 있는 행위에 대해 전략 클래스를 생성하고, 동적으로 행위의 수정이 필요한 경우 전략을 바꾸는 것만으로 행위의 수정이 가능하도록 만들었다.
JAVA 예)
// strategy class 커피를 내리는 전략 인터페이스
interface CoffeeStrategy {
String brew();
}
// CoffeeStrategy를 받으면 클라이언트 쪽에서 주입하는 구현체에 따라서(ex 라떼, 아메리카노)
// 전략이 결정된다.(구현체가 바뀌어도 CoffeeMachine 클래스는 수정할 필요가 없고,
// CoffeeStrategy의 brew()만 호출하면 된다.
// Context클래스
class CoffeeMachine {
public String brewing(CoffeeStrategy coffeeStrategy) {
return coffeeStrategy.brew();
}
}
// 전략클래스를 구현한 아메리카노 클래스(1)
class AmericanoStrategy implements CoffeeStrategy {
private static final String AMERICANO = "아메리카노";
@Override
public String brew() {
// 아메리카노를 내리는 기능
return AMERICANO;
}
}
// 전략클래스를 구현한 라떼 클래스(2)
class LatteStrategy implements CoffeeStrategy {
private static final String LATTE = "카페라떼";
@Override
public String brew() {
// 카페라떼를 내리는 기능
return LATTE;
}
}
public class TestStrategy {
public static void main(String[] args) {
CoffeeMachine coffeemachine = new CoffeeMachine();
// 아메리카노 버튼을 누르면 아메리카노 전략을 넣어 아메리카노를 추출한다.
String americano = coffeemachine.brewing(americanoButton());
System.out.println(americano);
// 라떼 버튼을 누르면 라떼 전략을 넣어 라떼를 추출한다.
String latte = coffeemachine.brewing(latteButton());
System.out.println(latte);
}
// 아메리카노 전략 버튼 메서드
public static CoffeeStrategy americanoButton() {
return new AmericanoStrategy();
}
// 라떼 전략 버튼 메서드
public static CoffeeStrategy latteButton() {
return new LatteStrategy();
}
}
4. 옵저버 패턴(Observer pattern)
옵저버 패턴: 주체가 어떤 객체의 상태 변화를 관찰하다가 상태 변화가 있을 때마다 메서드 등을 통해 옵저버 목록에 있는 옵저버들에게 변화를 알려주는 패턴 => 어떤 객체의 상태가 변할 때 그와 연관된 객체들에게 알림을 보냄!
- 주체: 객체의 상태 변화를 보고 있는 관찰자
- 옵저버: 이 객체의 상태 변화에 따라 전달되는 메서드 등을 기반으로 '추가 변화 사항'이 생기는 객체들
- 주체와 객체를 따로 두지 않고 상태가 변경되는 객체를 기반으로 구축하기도 함(객체와 주체가 합쳐진 옵저버 패턴)
- ex) 트위터 (새로운 트위터를 올리면 옵저버가 파악해서 팔로워들에게 알림이 간다.)
- 옵저버 패턴은 주로 이벤트 기반 시스템에 사용하고 MVC(Model-View-Controller) 패턴에도 사용된다.(MVC에서 모델과 뷰 사이를 느슨히 연결하게 하기 위해 사용)
Model에서 변경사항이 생겨 update( )메서드로 옵저버인 뷰에게 알려주고 이를 기반으로 컨트롤러(Controller) 등이 작동하는 것이다.
장점 | 단점 |
실시간으로 한 객체의 변경사항을 다른 객체에게 전달할 수 있다. | 너무 많이 사용하게 되면, 상태 관리가 힘들 수 있다. |
느슨한 결합으로 시스템이 유연하고 객체간의 의존성을 제거할 수 있다. | 데이터 배분에 문제가 생기면 문제로 이어질 수 있다. |
옵저버패턴의 구조
- Subject interface는 등록, 해제, 갱신을 위한 API를 제공한다.
- Subject interface를 상속받는 concrete Subject class는 등록, 해제, 갱신을 구현하고 기타 함수도 구현할 수 있다.
- Observer interface는 Subject에서 갱신할 때 호출되는 update API만 제공한다.
- Observer interface를 상속받는 A,B,C Class에 update를 구현한다.
import java.util.ArrayList;
import java.util.List;
// Observer 클래스
class Observer {
public String msg;
public void receive(String msg) {
System.out.println(this.msg+"에서 메세지를 받음 : " + msg);
}
}
// follower 1
class Follower1 extends Observer {
public Follower1(String msg) {
this.msg = msg;
}
}
// follower 2
class Follower2 extends Observer {
public Follower2(String msg) {
this.msg = msg;
}
}
// 옵저버에 공지사항을 받을 유저를 추가하고 삭제하고 공지사항 알림을 하는 Notice 클래스
class Notice {
private List<Observer> observers = new ArrayList<Observer>();
// 옵저버에 추가
public void attach(Observer observer) {
observers.add(observer);
}
// 옵저버에서 삭제
public void remove(Observer observer) {
observers.remove(observer);
}
// 옵저버들에게 알림
public void notifyObservers(String msg) {
for(Observer o:observers) {
o.receive(msg);
}
}
}
// Main 클래스
public class TestObserver {
public static void main(String[] args) {
Notice notice = new Notice();
Follower1 follower1 = new Follower1("팔로워1");
Follower2 follower2 = new Follower2("팔로워2");
notice.attach(follower1);
notice.attach(follower2);
String message = "공지사항입니다!";
notice.notifyObservers(message);
notice.remove(follower1);
message = "헤이헤이헤이";
notice.notifyObservers(message);
}
}
팔로워1에서 메세지를 받음 : 공지사항입니다!
팔로워2에서 메세지를 받음 : 공지사항입니다!
팔로워2에서 메세지를 받음 : 헤이헤이헤이
Q. 옵저버 패턴 구현 방법
- 여러 가지 방법이 있지만 프록시 객체를 써서 하곤 한다. 프록시 객체를 통해 객체의 속성이나 메서드 변화등을 감지하고 이를 미리 설정해 놓은 옵저버들에게 전달하는 방법으로 구현된다.
5. 프록시 패턴(Proxy pattern)
프록시 패턴: 대상 객체에 접근하기 전 그 접근에 대한 흐름을 가로채 대상 객체 앞단의 인터페이스 역할을 하는 디자인 패턴
- 이를 통해 객체의 속성, 변환 등을 보완하며, 보안, 데이터 검증, 캐싱, 로깅에 사용한다.
- 즉 어떤 객체를 사용하고자 할 때, 객체를 직접적으로 참조 하는 것이 아니라, 해당 객체를 대행하는 객체를 통해 대상객체에 접근하는 방식을 사용
장점 | 단점 |
사이즈가 큰 객체(ex 이미지)가 로딩되기 전에도 프록시를 통해 참조할 수 있다. | 객체를 생성할 때 한 단계를 거치게 되므로, 빈번한 객체 생성이 필요한 경우 성능이 저하될 수 있다. |
실제 객체의 public, protected 메서드들을 숨기고 인터페이스를 통해 노출시킬 수 있다. | 프록시 내부에서 객체 생성을 위해 스레드가 생성, 동기화가 구현되야 하는 경우 성능이 저하될 수 있다. |
로컬에 있지 않고 떨어져 있는 객체를 사용할 수 있다. | 로직이 난해해져 가독성이 떨어질 수 있다. |
원래 객체의 접근에 대해서 사전처리를 할 수 있다. |
5-1.프록시 서버(proxy server)
서버와 클라이언트 사이에서 클라이언트가 자신을 통해 다른 네트워크 서비스에 간접적으로 접속할 수 있게 해주는 컴퓨터 시스템이나 응용 프로그램
=> 서버 앞단에 프록시 서버를 둬서 캐싱, 로깅, 데이터 분석을 서버보다 먼저 하는 서버를 말한다.
- 포트 번호를 바꿔서 사용자가 실제 서버의 포트에 접근하지 못하게 할 수 있다.
- 공격자의 DDOS 공격을 차단하거나 CDN을 프록시 서버로 달아서 캐싱 처리를 용이하게 할 수 있다.
- 사용 사례
(1) nginx로 Node.js로 이루어진 서버의 앞단에 둬서 버퍼 오버플로우 해결
(2) CloudFlare를 둬서 캐싱, 로그 분석등
(1) nginx
- nginx는 비동기 이벤트 기반의 구조와 다수의 연결을 효과적으로 처리 가능한 웹 서버이다. 주로 Node.js 서버 앞단의 프록시 서버로 활용된다. => 이를 통해 익명 사용자의 직접적인 서버로의 접근을 차단하고 간접적으로 한 단계를 더 거치면서 보안성을 더욱 더 강화할 수 있다.
(2) CloudFlare
- CloudFlare는 전 세계적으로 분산된 서버가 있고 이를 통해 어떠한 시스템의 콘텐츠 전달을 빠르게 할 수 있는 CDN 서비스이다.
- CDN 말고도 DDOS 공격방어, HTTPS 구축이 있는데 모두 '프록시 서버'로 쓰기 때문에 가능하다.
- 사용자, 크롤러, 공격자가 자신의 웹 사이트에 접속할 때 CloudFlare를 통해 공격자로서 보호할 수 있다.
DDOS 공격 방어 | HTTPS 구축 |
짧은 기간 동안 네트워크에 많은 요청을 보내 네트워크를 마비시켜 웹 사이트의 가용성을 방해하는 사이버 공격 유형 | 서버에서 HTTPS를 구축할 때 인증서를 기반으로 구축할 수 있는데 CloudFlare를 사용하면 별도의 인증서 설치 없이 손쉽게 HTTPS를 구축할 수 있다. |
5-2. CORS(Cross-Origin Resource Sharing)와 프론트엔드의 프록시 서버
서버가 웹 브라우저에서 리소스를 로드할 때 다른 오리진을 통해 로드하지 못하게 하는 HTTP 헤더 기반 메커니즘
- 프론트엔드 개발 시 프론트엔드 서버를 만들어서 백엔드 서버와 통신할 때 주로 CORS 에러를 마주치는데 이를 해결하기 위해 프론트엔드에서 프록시 서버를 만들기도 한다.
- 오리진: 프로토콜과 호스트 이름, 포트의 조합 ex) https://kundol.com:12010/test에서 https://kundol.com:12010
- ex) 프론트엔드에서는 127.0.0.1:3000으로 테스팅을 하는데 백엔드 서버는 127.0.0.1:12010이라면 포트 번호가 달라서 CORS 에러가 나타난다. 이 때 프록시 서버를 둬서 프론트엔드 서버에서 요청되는 오리진을 127.0.0.1:12010으로 바꾼다. (localhost나 127.0.0.1은 본인 PC)
- 위의 그림처럼 프론트엔드 서버 앞단에 프록시 서버를 놓아 /api 요청은 users API, /api2 요청은 users API2에 요청할 수 있다.
- 자연스레 CORS 에러 해결은 물론이고 다양한 API 서버와의 통신도 가능해진다.
http://www.yes24.com/Product/Goods/108887922
https://thebook.io/080326/ch01/01/01-06/
https://thebook.io/080326/ch01/01/02/
https://readystory.tistory.com/117
https://jackjeong.tistory.com/108
https://hudi.blog/strategy-pattern/
https://luckygg.tistory.com/181
https://velog.io/@qkrtkdwns3410/%ED%94%84%EB%A1%9D%EC%8B%9C-%ED%8C%A8%ED%84%B4
https://coding-factory.tistory.com/711
'cs > 면접을 위한 CS 전공지식 노트' 카테고리의 다른 글
2-3. 네트워크 기기(스위치 등)/IP주소 (0) | 2022.09.27 |
---|---|
2-2. TCP/IP 4계층 모델 (0) | 2022.09.23 |
2-1. 네트워크의 기초(토폴로지&성능분석 명령어) (0) | 2022.09.22 |
1-2. 프로그래밍 패러다임(함수형,객체지향,절차적프로그래밍) (0) | 2022.09.20 |
1-1. 디자인 패턴(2) (0) | 2022.09.20 |