본문 바로가기
cs/면접을 위한 CS 전공지식 노트

1-1. 디자인 패턴(1)

by 이쟝 2022. 9. 19.
디자인 패턴
프로그램을 설계할 때 발생했던 문제점들을 객체 간의 상호 관계 등을 이용해 해결할 수 있도록 하나의 '규약' 형태로 만들어 놓은 것

디자인 패턴을 사용하면 상황에 맞는 올바른 설계를 더 빠르게 적용할 수 있고, 각 패턴의 장단점을 통해서 설계를 선택하는데 도움을 얻을 수 있다. 또한, 설계 패턴에 이름을 붙임으로써 시스템의 유지 보수에 도움을 얻을 수 있다.

싱글톤 패턴 팩토리 패턴  전략 패턴 옵저버 패턴 프록시 패턴
이터레이터 패턴 노출모듈 패턴 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) 등이 작동하는 것이다. 

장점 단점
실시간으로 한 객체의 변경사항을 다른 객체에게 전달할 수 있다. 너무 많이 사용하게 되면, 상태 관리가 힘들 수 있다. 
느슨한 결합으로 시스템이 유연하고 객체간의 의존성을 제거할 수 있다. 데이터 배분에 문제가 생기면 문제로 이어질 수 있다.

옵저버패턴의 구조

  1. Subject interface등록, 해제, 갱신을 위한 API를 제공한다.
  2. Subject interface를 상속받는 concrete Subject class등록, 해제, 갱신을 구현하고 기타 함수도 구현할 수 있다. 
  3. Observer interface는 Subject에서 갱신할 때 호출되는 update API만 제공한다. 
  4. Observer interface를 상속받는 A,B,C Classupdate를 구현한다. 

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

 

면접을 위한 CS 전공지식 노트 - YES24

디자인 패턴, 네트워크, 운영체제, 데이터베이스, 자료 구조, 개발자 면접과 포트폴리오까지!CS 전공지식 습득과 면접 대비, 이 책 한 권이면 충분하다!개발자 면접에서 큰 비중을 차지하는 CS(Comp

www.yes24.com

https://thebook.io/080326/ch01/01/01-06/

 

면접을 위한 CS 전공지식 노트: 1.1.1 싱글톤 패턴 - 6

 

thebook.io

https://thebook.io/080326/ch01/01/02/

 

면접을 위한 CS 전공지식 노트: 1.1.2 팩토리 패턴

 

thebook.io

https://readystory.tistory.com/117

 

[생성 패턴] 팩토리 패턴(Factory Pattern) 이해 및 예제

이번에 살펴볼 디자인 패턴은 가장 유명한 디자인 패턴 중 하나인 팩토리 패턴(Factory Pattern)입니다. 이 팩토리 패턴은 조금 더 구체적인 용어인 팩토리 메소드 패턴(Factory Method Pattern)으로도 널리

readystory.tistory.com

https://jackjeong.tistory.com/108

 

[Java] Strategy Pattern(전략패턴)[feat. Interface]

안녕하세요~ 잭코딩입니다! 이번에는 인터페이스의 활용에 대해 글을 써보려고 합니다! 요즘 글쓴이는 우아한테크캠프 Pro라는 교육과정을 듣고 있습니다 이번 미션에서 Strategy Pattern을 적용해

jackjeong.tistory.com

https://hudi.blog/strategy-pattern/

 

전략 패턴 (Strategy Pattern)

서론 우아한테크코스 레벨1 첫번째 미션인 자동차 경주를 구현하며, '랜덤값을 사용하는 메서드에 대한 테스트는 어떻게 하면 좋을까?' 에 대한 고민을 했다. 결론적으로 RandomGeneratable 인터페이

hudi.blog

https://flowarc.tistory.com/entry/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-%EC%98%B5%EC%A0%80%EB%B2%84-%ED%8C%A8%ED%84%B4Observer-Pattern

 

디자인패턴 - 옵저버 패턴(Observer Pattern)

정의 디자인 패턴 중 옵저버 패턴(Observer Pattern)을 알아보자. 객체지향 설계를 하다보면 객체들 사이에서 다양한 처리를 할 경우가 많다. 예를 들어 한 객체의 상태가 바뀔 경우 다른 객체들에게

flowarc.tistory.com

https://luckygg.tistory.com/181

 

[Design Pattern] 옵저버 패턴(Observer Pattern) 이야기 #1 (예제 포함)

옵저버 패턴(Observer Pattern) 옵저버 패턴은 관찰자 패턴이라고도 합니다. 일대다 관계를 이루고 있으며, 상태가 업데이트되면 모든 옵저버들에게 정보를 갱신할 수 있도록 하는 것을 의미합니다.

luckygg.tistory.com

https://velog.io/@qkrtkdwns3410/%ED%94%84%EB%A1%9D%EC%8B%9C-%ED%8C%A8%ED%84%B4

 

프록시 패턴

대상 객체(Subject)에 접근하기 전 그 접근에 대한 흐름을 가로채 대상 객체 앞단의 인터페이스 역할을 하는 디자인 패턴이다.이를 통해 객체의 속성, 변환 등을 보완하며 보안, 데이터 검증, 캐싱,

velog.io

https://coding-factory.tistory.com/711

 

[Design Pattern] 프록시 패턴(Proxy Pattern)에 대하여

프록시 패턴이란? 프록시는 대리인이라는 뜻으로, 무엇인가를 대신 처리하는 의미입니다. 일종의 비서라고 생각하시면 됩니다. 사장님한테 사소한 질문을 하기보다는 비서한테 먼저 물어보는

coding-factory.tistory.com