디자인 패턴
💡 라이브러리 VS 프레임워크
라이브러리: 공통으로 사용될 수 있는 특정한 기능을 모듈화 한 것. 코드의 제어권이 라이브러리에 있음.
프레임워크: 공통으로 사용될 수 있는 특정한 기능을 모듈화 한 것. 코드의 제어권이 개발자에 있음.
디자인 패턴
프로그램을 설계할 때 발생했던 문제점들을 객체간의 상호관계 등을 이용하여 해결할 수 있도록 하나의 규약 형태로 만들어 놓은것.
싱글톤 패턴(더 자세한 건 여기로)
하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴. 데이터 베이스 연결 모듈에 많이 사용.(datasource)
- 장점
- 하나의 인스턴스만 있을 때 다른 모듈들이 공유하며 사용하기 때문에 인스턴스 비용이 줄어듦.
- 단점
Java에서의 싱글톤 패턴
public class HelloSingleton {
static class Singleton {
private static class singleInstanceHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return singleInstanceHolder.INSTANCE;
}
}
public static void main(String[] args) {
Singleton a = Singleton.getInstance();
Singleton b = Singleton.getInstance();
System.out.println("a.hashCode() = " + a.hashCode());
System.out.println("b.hashCode( = " + b.hashCode());
System.out.println("a.equals(b) = " + a.equals(b));
}
}
싱글톤 패턴의 단점
- TDD(Test Driven Development)에서의 문제점 발생
- TDD할 때는 단위 테스트 위주.
- 단위 테스트는 테스트를 하는 클래스가 서로 독립적이여야함. => 싱글톤으로의 개발의 복잡성
의존성 주입
- 사용하기 쉽고 실용적이지만 모듈 간의 결합을 강하게 만들 수 있음.
- 의존성 주입(DI, Dependency Injection)을 통해 모듈 간의 결합을 느슨하게 만들 수 있음.
- 의존성 주입자를 통해 메인 모듈이 간접적으로 의존성을 주입하는 방법
- 장점
- 모듈들을 쉽게 교체할 수 있는 구조가 되어 테스팅하기 쉽고 마이그레이션 하기 수월 => 객체지향의 장점
- 구현할 때 추상화 레이어를 통해 구현체를 넣어주기 때문에 의존성 방향이 일관적이고 애플리케이션을 쉽게 추론할 수 있으며 모듈 간의 관계들이 명확
- 단점
- 원칙
- 상위 모듈은 하위 모듈에서 어떠한 것도 가져오면 안됨.
- 모두 추상화에 의존해야하며 추상화는 세부사항에 의존하면 안됨.
팩토리 패턴(더 자세한 건 여기로)
객체의 생성 부분을 제거하고 추상화한 패턴. 하위 클래스에서 객체 생성에 관한 구체적인 내용을 결정하는 패턴
- 클래스가 분리되기 때문에 느슨한 결합을 가지며 더 많은 유연성을 얻게 됨.
- 객체 생성 로직이 따로 있기 때문에 코드를 리팩토링 할 때 한 곳만 고치면 되기에 유지 보수성이 증가함.
- 전달받은 값에 따라 다른 객체를 생성하며 인스턴스의 타입 지정 가능.
enum CoffeeType {
LATTE(0),
ESPRESSO(1),
TEA(2);
private int type;
CoffeeType(int type) {
this.type = type;
}
public int getType() {
return type;
}
}
abstract class Coffee {
protected String name;
public String getName() {
return name;
}
}
class Latte extends Coffee {
public Latte() {
super.name = "Latte";
}
}
class Espresso extends Coffee {
public Espresso() {
super.name = "Espresso";
}
}
public class FactoryPattern {
static class CoffeeFactory {
public static Coffee createCoffee(CoffeeType type) {
switch (type) {
case LATTE -> {
return new Latte();
}
case ESPRESSO -> {
return new Espresso();
}
default -> {
throw new IllegalArgumentException("Invalid coffee type: " + type);
}
}
}
}
public static void main(String[] args) {
Coffee brianCoffee = CoffeeFactory.createCoffee(CoffeeType.LATTE);
Coffee jackCoffee = CoffeeFactory.createCoffee(CoffeeType.ESPRESSO);
System.out.println("brianCoffee = " + brianCoffee.getName());
System.out.println("jackCoffee = " + jackCoffee.getName());
}
}
- 의존성 주입이라고 할 수 도 있음. CoffeeFactory에서 LatteFacotry의 인스턴스를 생성하는 것이 아닌 주입하기 때문
- 정적 메소드(static으로 설정한 함수)의 경우 클래스를 기반으로 객체를 만들지 않고 호출이 가능하며, 해당 메소드에 대한 메모리 할당을 한번만 할 수 있다는 장점이 있음.
전략 패턴(더 자세한 건 여기로)
객체의 행위를 바꾸고 싶은 경우 코드의 수정없이 캡슐화된 알고리즘을 컨텍스트 안에서 바꿔주면서 상호 교체하는 패턴
import java.util.ArrayList;
import java.util.List;
interface PaymentStrategy {
public void pay(int amount);
}
class KAKAOCardStrategy implements PaymentStrategy {
private String name;
private String cardNumber;
private String cvv;
private String dateOfExpiry;
public KAKAOCardStrategy(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 KAKAOCard.");
}
}
class LUNACardStrategy implements PaymentStrategy {
private String emailId;
private String password;
public LUNACardStrategy(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 price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public int getPrice() {
return price;
}
}
class ShoppingCart {
List<Item> items;
public ShoppingCart() {
this.items = new ArrayList<>();
}
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 paymentStrategy) {
int amount = calculateTotal();
paymentStrategy.pay(amount);
}
}
public class StrategyPattern {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
Item A = new Item("hiroA", 100);
Item B = new Item("hiroB", 300);
cart.addItem(A);
cart.addItem(B);
cart.pay(new LUNACardStrategy("hiro@example.com", "TESt123!@#"));
cart.pay(new KAKAOCardStrategy("Ipjagum", "123123123", "123", "12/10"));
}
}
옵저버 패턴(더 자세한 건 여기로)
주체가 어떤 객체의 상태 변화를 관찰하다가 상태 변화가 있을 때마다 메소드 등을 통해 옵저버 목록에 있는 옵저버들에게 변화를 알려주는 디자인 패턴
- 주체
- 옵저버
- 객체의 상태 변화에 따라 전달되는 메소드 등을 기반으로 추가 변화 상항이 생기는 객체
- 이벤트 기반 시스템에서 주로 사용되며 MVC(Model-View-Controller) 패턴에도 사용가능
import java.util.ArrayList;
import java.util.List;
interface Observer {
public void update();
}
interface Subject {
public void register(Observer obj);
public void unregister(Observer obj);
public void notifyObservers();
public String getUpdate(Observer obj);
}
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) {
if (observers.contains(obj)) {
observers.remove(obj);
}
}
@Override
public void notifyObservers() {
this.observers.forEach(Observer::update);
}
@Override
public String getUpdate(Observer obj) {
return this.message;
}
public void postMessage(String msg) {
System.out.println("Message sent 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 = topic.getUpdate(this);
System.out.println(name + ":: got message >> " + msg);
}
}
public class ObserverPattern {
public static void main(String[] args) {
Topic topic = new Topic();
Observer o1 = new TopicSubscriber("A", topic);
Observer o2 = new TopicSubscriber("B", topic);
Observer o3 = new TopicSubscriber("C", topic);
topic.register(o1); topic.register(o2); topic.register(o3);
topic.postMessage("amumu is op champion!");
}
}
상속과 구현
- 상속
- 자식 클래스가 부모 클래스의 메소드 등을 상속받아 사용하여 자식 클래스에서 추가 및 확장을 할 수 있는 것.
- 재사용성, 중복의 최소화
- 일반 클래스, abstract 클래스를 기반
- 구현
- 부모 인터페이스를 자식 클래스에서 재정의하여 구현하는 것.
- 반드시 부모 클래스의 메소드를 재정의하여 구현
- 인터페이스 기반
프록시 패턴과 프록시 서버(더 자세한 건 여기로)
대상 객체(subject)에 접근하기 전 그 접근에 대한 흐름을 가로채 해당 접근을 필터링 하거나 수정하는 등의 역할을 하는 계층
- 객체의 속성, 변환 등을 보완. 데이터 검증, 캐싱 로깅에 사용.
프록시 서버
서버와 클라이언트 사이에서 클라이언트가 자신을 통해 다른 네트워크 서비스에 간접적으로 접속할 수 있게 해주는 컴퓨터 시스템이나 응용 프로그램을 가리킴
- 프록시 서버로 사용되는 nginx
- nginx는 비동기 이벤트 기반의 구조와 다수의 연결을 효과적으로 처리 가능한 웹서버이며, 주로 node.js 서버 앞단의 프록시 서버로 사용.
이터레이터 페턴(더 자세한 건 여기로)
이터레이터를 사용하여 컬렉션의 요소들에 접근하는 디자인 패턴 이를 통해 순회할 수 있는 여러가지 자료형의 구조와 상관없이 이터레이터라는 인터페이스를 통해 순회 가능.
- 자바에서는 Collection Framework를 상속하는 자료구조에서 사용 가능
MVC 패턴
모델(Model), 뷰(View), 컨트롤러(Controller)로 이루어진 디자인 패턴
애플리케이션의 구성 요소를 세가지로 나누어 개발 프로세스에서 각각의 구성요소에만 집중해서 개발할 수 있도록 만든 디자인 패턴.
- 재사용성과 확장성에 용이
- 모델과 뷰의 관계가 복잡해진다는 단점
- 모델
- 애플리케이션의 데이터인 데이터베이스, 상수, 변수를 의미
- 뷰
- 사용자 인터페이스 요소를 표현.
- 변경이 일어나면 컨트롤러에게 이를 전달해야함.
- 컨트롤러
- 하나 이상의 모델과 하나 이상의 뷰를 잇는 다리 역할.
- 이벤트 등 메인 로직 담당. 모델과 뷰의 생명주기도 관리하며 모델이나 뷰의 변경 통지를 받으면 이를 해석해서 각각의 구성요소에게 해당 내용을 전달.
MVP 패턴
MVC 패턴으로부터 파생. MVC에 해당하는 C가 Presenter로 교체된 패턴
뷰와 프레젠터는 1:1이라 더 강한 결합
MVVM 패턴
C가 View Model로 바뀐 패턴
- 뷰모델은 뷰를 더 추상화한 계층. MVVM 패턴은 MVC 패턴과 다르게 커맨드와 데이터 바인등을 가지는 것이 특징.
- 뷰와 뷰모델 사이의 양방향 데이터 바인딩을 지원하며 UI를 별도의 코드 수정없이 재사용할 수 있고 단위 테스팅 하기 쉽다는 장점.