핵심: 연관된 객체끼리 데이터를 공유하여 애플리케이션 메모리 최소화. 캐시 개념을 코드로 패턴화
이를 재사용한 객체 인스턴스를 공유시킴
- Flyweight : 경량 객체를 묶는 인터페이스
- ConcreteFlyweight : 공유 가능하여 재사용되는 객체 (intrinsic state), flIweight의 하위클래스
- UnsharedConcreteFlyweight : 공유 불가능한 객체 (extrinsic state), flIweight의 하위클래스
- FlyweightFactory : Flyweight 객체 관리 클래스. 경량 객체를 만드는 공장 역할 &캐시 역할-
- GetFlyweight() 메서드는 팩토리 메서드 역할
- 만일 객체가 메모리에 존재하면 그대로 가져와 반환하고, 없다면 새로 생성해 반환한다
- Client : 클라이언트는 FlyweightFactory를 통해 Flyweight 타입의 객체를 얻어 사용
intrinsicState 내부 상태, 공유 가능한 정보 (예: 타입, 색상)
extrinsicState 외부 상태, 개별 객체만의 정보 (예: 위치, 방향)
- 자주 변하는 속성(= 외적인 속성, extrinsit)과 변하지 않는 속성(= 내적인 속성, intrinsit)을 분리하고 재사용하여 메모리 사용을 줄임
- 다시 말해, 메모리 사용 최적화를 위해 공통 데이터를 공유하고, 변하는 데이터는 외부에서 주입
pool: 공유 가능한 객체들을 모아놓은 저장소(캐시)
흐름:
- client가 flightwighFactory에 요청(객체를 직접 만들지 x, FlyweightFactory.getFlyweight(key)호출)
- Facroty는 해당 key로 된 flyweight가 존재하면 이를 재사용, 없다면 새로 만든 후 pool에 저장(캐시 구조)
- Client는 받은 Flyweight을 가지고 사용. 이 때 외부상태는 직접 넘겨줌.
*공통된 정보는 공유된 객체(Flyweight)**가 가지고 있고
각자의 다른 정보(ex: 위치 등)는 Client가 직접.
- Flyweight의 사용처
- 공통적인 인스턴스를 많이 사용되는 로직, 자주 변하지 않는 속성을 재사용
- 불변 객체가 아니라면 어떤 코드가 플라이웨이트 객체를 임의로 수정했을 때 그 객체를 공유하고 있는 다른 코드에 영향을 미치기 때문 → 이는 싱글톤과도 비슷
- 시스템에 많은 수의 동일한 불변 객체가 있다면 이 패턴을 사용해서 객체를 플라이웨이트로 설계하고 메모리에 하나의 인스턴스만 보관하여 메모리를 절약할 수 있음
- 꼭 동일한 객체가 아니더라도 유사도가 높은 객체에 대해 동일한 필드를 추출해서 플라이웨이트로 설계 가능 → 이 때 변하지 않는 불변 속성은 플라이웨이트 객체로 설계하고, 변하는 속성은 이를 사용하는 클라이언트가 정의하도록 함
다음과 같은 상황을 가정하자.
캐릭터 수천명을 만들어야 하는 게임
공통 속성(공유 가능): 캐릭터 타입, 무기, 스킨
개별 속성: 좌표(x,y)
타입(Type, types)이라는 공유 객체로 메모리 절약
// 1. Flyweight: 공통 속성 객체 - 쉽게 말해 Orc + Axe + Green” 조합을
// 가진 캐릭터가 1,000명 있어도 CharacterType 인스턴스는 1개만 생성
class CharacterType {
constructor(type, weapon, skin) {
this.type = type;
this.weapon = weapon;
this.skin = skin;
}
describe() {
console.log(`Type: ${this.type}, Weapon: ${this.weapon}, Skin: ${this.skin}`);
}
}
// 2. Flyweight Factory: 이미 존재하는 타입은 재사용
class CharacterTypeFactory {
constructor() {
this.characterTypePool = {}; //flyweight Pool, 이미 만든 인스턴스를 key 기반 저장
}
get(type, weapon, skin) {
const key = `${type}_${weapon}_${skin}`; //고유키조합
if (!this.characterTypePool[key]) { //없을 때 새로 생성
this.characterTypePool[key] = new CharacterType(type, weapon, skin);
}
return this.characterTypePool[key]; //있으면 기존 객체 재사용
}
}
// 3. 개별 캐릭터 - 개별 속성 포함
class GameCharacter {
constructor(x, y, characterType) {
this.x = x;
this.y = y; //공유 불가
this.characterType = characterType; // 공통 속성
}
draw() {
console.log(`Draw ${this.characterType.type} at (${this.x}, ${this.y})`);
}
}
// 사용 예시
const factory = new CharacterTypeFactory();
const characters = [];
characters.push(new GameCharacter(10, 20, factory.get("Orc", "Axe", "Green")));
characters.push(new GameCharacter(15, 30, factory.get("Orc", "Axe", "Green"))); // 재사용됨
characters.push(new GameCharacter(50, 60, factory.get("Elf", "Bow", "Blue")));
characters.forEach(c => {
c.draw();
c.characterType.describe();
});
CharacterType: 공유 객체 (Flyweight)
CharacterTypeFactory: Flyweight를 관리하고 재사용
GameCharacter: 개별 위치 정보 등은 따로 가짐
구조도
CharaterTypeFactory(FlyweightFactory) —(생성 재사용)→
GameCharacter(Context, 좌표 포함) ——> CharacterType (Flyweight Object)
GameCharacter(Context, 좌표 포함) —(참조공유)——>
CharacterType: 공유 객체 (같은 타입, 무기, 스킨이면 재사용)
GameCharacter: 개별 객체 (x, y 위치 등 개별 정보 포함)
CharacterTypeFactory: 공유 객체를 만들고 관리하는 팩토리
활용
const NUM_ENEMIES = 10000;
const enemyTypes = [
["Orc", "Axe", "Green"],
["Elf", "Bow", "Blue"],
["Troll", "Club", "Gray"]
];
const factory = new CharacterTypeFactory();
const enemies = [];
for (let i = 0; i < NUM_ENEMIES; i++) {
// 무작위 캐릭터 타입 선택
const [type, weapon, skin] = enemyTypes[Math.floor(Math.random() * enemyTypes.length)];
const characterType = factory.get(type, weapon, skin);
// 무작위 좌표
const x = Math.floor(Math.random() * 500);
const y = Math.floor(Math.random() * 500);
enemies.push(new GameCharacter(x, y, characterType));
}
// 출력해
for (let i = 0; i < 5; i++) {
enemies[i].draw();
enemies[i].type.describe();
}
console.log(`총 캐릭터 수: ${enemies.length}`);
console.log(`실제 생성된 CharacterType 수: ${Object.keys(factory.types).length}`);
결과
Draw Orc at (123, 452)
Type: Orc, Weapon: Axe, Skin: Green
Draw Troll at (22, 331)
Type: Troll, Weapon: Club, Skin: Gray
...
총 캐릭터 수: 10000
실제 생성된 CharacterType 수: 3
- 메모리 비교
// 일반
class User {
constructor(fullName) {
this.fullName = fullName;
}
}
// Flyweight 패턴
class User2 {
constructor(fullName) {
const getOrAdd = function (s) {
let idx = User2.strings.indexOf(s);
if (idx !== -1) return idx;
User2.strings.push(s);
return User2.strings.length - 1;
};
this.names = fullName.split(' ').map(getOrAdd);
}
}
User2.strings = [];
// 랜덤 문자열 생성
function getRandomInt(max) {
return Math.floor(Math.random() * Math.floor(max));
}
let randomString = function () {
let result = [];
for (let i = 0; i < 10; ++i) {
result.push(String.fromCharCode(65 + getRandomInt(26))); // A~Z
}
return result.join('');
};
// 데이터 준비
let users = [],
users2 = [],
firstNames = [],
lastNames = [];
for (let i = 0; i < 100; ++i) {
firstNames.push(randomString()); // 100개의 first name
lastNames.push(randomString()); // 100개의 last name
}
// 10000명 생성 (100 x 100)
for (let first of firstNames) {
for (let last of lastNames) {
users.push(new User(`${first} ${last}`));
users2.push(new User2(`${first} ${last}`));
}
}
// 메모리 비교
console.log(`10k users take up approx ${JSON.stringify(users).length} chars`);
let users2length = [users2, User2.strings]
.map(x => JSON.stringify(x).length)
.reduce((x, y) => x + y);
console.log(`10k flyweight users take ~${users2length} chars`);
발행 - 구독 패턴
비동기식 메세징 패턴이다.
발행자: 이번트 발행
구독자: 특정 이벤트 구독중. 이벤트 발생시 알림을 받아 자율적으로 처리
구조
Publisher → [ EventChannel ] → Subscribers
- Publisher: 이벤트를 보냄 (ex: 뉴스 발행)
- EventChannel(EventBus)(Broker or Topic or Mediator): 중간 매개체. 발행자와 구독자 사이를 연결
- Subscriber: 관심 있는 이벤트를 구독하고 있다가 response
특징
- 발행자과 구독자는 직접적인 연관x(직접 연결되지 않은 상태로 서로의 존재를 모름). 떄문에 각각 어디에 있어도 상관 없으며, 구독설정만 하면 어디에서든 발행되는 데이터를 받을 수 있음 →확장에 용이
- 중간에 매개체
- 출판사와 구독자 사이를 연결(Tunneling). 때문에 직접 참조가 아닌 간접참조로 다른 객체간 이벤트 연결
const EventBus = {
events: {},
// 특정 이벤트에 콜백 등록
subscribe(eventName, callback) {
if (!this.events[eventName]) //이벤트 없으면 초기화
this.events[eventName] = [];
}
// 해당 이벤트의 콜백 리스트에 새 콜백 추가
this.events[eventName].push(callback);
},
// 이벤트 발생 시 구독된 콜백들 실행
publish(eventName, data) {
if (this.events[eventName]) {//해당 이벤트에 등록된 콜백o=>실행
this.events[eventName].forEach(cb => cb(data));
}
}
};
// 구독자
EventBus.subscribe('news', (data) => {
console.log('뉴스 받음:', data);
});
// 발행자
EventBus.publish('news', '속보! AI가 세상을 바꿈!');// 등록된 이벤트 모두 실행
옵저버 패턴과의 공통점
시스템간의 구성요소 간 결합도를 낮추는 패턴
디버깅의 어려움. 연결이 분리된 특성
어떤 구독자가 어떤 발행자에 연결되는지 알기 힘듦
옵저버 패턴과의 차이
옵저버 — 발행구독패턴
연결구조 | Subject가 Observer 직접 찹조 | Publisher과 Subscriber 분리 |
중재자 | x | ㅇ |
의존성 | 약한 결합 | 완전 분리 |
ex | 모델이 변할 시 뷰를 업데이트 | 이벤트 시스템, 채팅, 뉴스 |
실사용 | react 상태관리 라이브러리 jotai | Node.js EventEmitter, Redux, Kafka |
구분 옵저버 발행구독
?왜 완전 분리이죠?→서로가 서로의 존재를 모르는 상태
?왜 약한 결합이죠? → 주체만이 관찰자의 존재를 아는 상태
코드상 참조=알고있다
쉽게 말하자면
[옵저버 패턴] Subject ------> Observer1 ------> Observer2 ------> Observer3
[발행-구독 패턴] Publisher1 ---> MessageBroker ---> Subscriber1 Publisher2 ---> ↓ ---> Subscriber2 Publisher3 ---> ↓ ---> Subscriber3
1. 옵저버 패턴
- 관찰자(Observer)와 주체(Subject)가 서로를 알고 있음
- 1:N 관계 (한 주체에 여러 관찰자)
- 직접적인 통신
실생활 예시
- 유튜버와 구독자의 직접 소통
- 선생님이 학생들에게 직접 공지
2. 발행-구독 패턴
- 발행자(Publisher)와 구독자(Subscriber)가 서로를 모름
- N:M 관계 (여러 발행자:여러 구독자)
- 중간에 메시지 브로커/이벤트 채널을 통한 통신
실생활 예시
- 카카오톡 채팅방 (참여자들은 채팅방을 통해 소통)
- 우체국 (보내는 사람과 받는 사람은 우체국을 통해 소통)
- 옵저버 패턴: “너 내 친구니까 내가 뭐 바뀌면 바로 알려줄게” (직접 알림)
- 발행-구독 패턴: “뉴스 채널에 구독 걸면, 뉴스 뜰 때 자동으로 알려줘” (중개자 있음)
옵저버 패턴의 예시
jotai(react 라이브러리) 구독. 옵저버 패턴 기반 상태관리 라이브러리
- atom = Subject (관찰 대상)
- 컴포넌트 = Observer (관찰자)
- useAtom()을 쓰는 순간 → 컴포넌트가 atom을 “구독”함
- atom 값이 바뀌면 → 구독 중인 컴포넌트에 알림이 가서 리렌더링
- 상태가 바뀌면 jotai 내부 시스템이 컴포넌트에게 알림 →리렌더링
import { atom, useAtom } from 'jotai';
// subject
const countAtom = atom(0);
// observer
function Counter() {
const [count, setCount] = useAtom(countAtom); // 이 순간 구독함
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>+</button>
</div>
);
}
- Jotai는 React의 렌더링 흐름에 최적화된 세분화된 반응형(fine-grained reactivity)을 제공하는 구조로, atom 단위로 상태를 구독하므로(어떤 atom이 바뀌면 그 atom을 사용하는 컴포넌트만 리렌더링됨) 성능 면에서 이점이 큼
- Redux는 Flux 아키텍처에 기반을 두나 store.subscribe()를 통한 구독 메커니즘이 Pub-Sub 구조라고 할 수 있음Publisher (발행자) dispatch(action) 하는 코드 (컴포넌트, 미들웨어 등)
Message/Event action 객체 ({ type: "ADD_TODO" }) 중개자 store 자체 Subscriber (구독자) store.subscribe(listener) 또는 useSelector()로 store를 구독하는 컴포넌트
// store 생성
const store = createStore(reducer);
// 구독자 등록 (Subscriber)
store.subscribe(() => {
console.log("상태가 변경되었습니다", store.getState());
});
// 발행자 (Publisher)
store.dispatch({ type: 'INCREMENT' });
class PubSub {
constructor() {
this.subscribers = {};
}
subscribe(topic, callback) {
if (!this.subscribers[topic]) this.subscribers[topic] = [];
this.subscribers[topic].push(callback);
}
publish(topic, data) {
if (this.subscribers[topic]) {
this.subscribers[topic].forEach(cb => cb(data));
}
}
}
// 사용 예시
const pubsub = new PubSub();
pubsub.subscribe("news", msg => console.log("뉴스 받음:", msg));
pubsub.publish("news", "오늘 날씨는 맑음!");
- dispatch()는 action을 "발행"
- store는 리듀서를 거쳐 상태를 변경하고
- subscribe()에 등록된 모든 구독자에게 알림
MV* 주요 패턴
Model: 앱 데이터, 상태, 비즈니스 로직
View:
*: Model과 View사이 중재
프론트엔드에서 이 구조는 완벽하게 들어맞지 않는다. React는 MVVM에 가깝지만, 이 역시 완전히 일치하지 않으며
컴포넌트 기반 아키텍쳐이기 때문에 역할이 자연스럽게 섞인다.
리액트의 Unopinionated about architecture라는 설계 철학(라이브러리)에 따라 패턴이 고정되어있지 않음.
- MVC(Model View Controller)
프론트엔드에서 거의 직접적으로 쓰이지 않고, 전통적인 백엔드 패턴
Model: 서버나 Api에서 오는 데이터 (ex: json)
View: DOM, 템플릿
Controller: 이벤트 핸들러, 라우터 등
- Model: API로부터 받아온 데이터 (useEffect, fetch)
- View: JSX 컴포넌트
- Controller: 모델과 뷰 사이의 중재자: 상태 변경 로직, 핸들러 함수들
// React 예시
// Controller + View
function UserProfile() {
const [user, setUser] = useState(null);
useEffect(() => {//useEffect, setUser이 Controller
fetchUser().then(setUser); // Model
}, []);
return <div>{user ? user.name : 'Loading...'}</div>; // View
}
장점: 모델과 뷰의 분리로 단위(Unit)테스트 작성 편리
- MVP(Model View Presenter)
React에서는 container/presenter 패턴으로 구현하는 경우가 많음
- Model: 비즈니스 로직 / API
- View: Dumb Component (props만 받음)
- Presenter: 데이터를 가공해서 View에 전달 (Container)
// Dumb View Component (View)
const UserProfileView = ({ user }) => (
<div>{user.name}</div>
);
// Container (Presenter)
const UserProfileContainer = () => {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser().then(setUser); // Model, 외부 데이터 불러옴
}, []);
return user ? <UserProfileView user={user} /> : <div>Loading...</div>;
};
프론트엔드에서 MVP와 MVC는 명확하게 구분되지 않으며, 혼합해서 쓰고 있다.
- MVVM(Model View ViewModel)
React의 상태 기반 UI + 양방향 바인딩 느낌이 MVVM과 매우 유사
- Model: 상태 or API 데이터
- ViewModel: 데이터를 준비, 가공, View에 넘겨주는 중간단계 useState, useMemo, useCallback, zustand, Recoil 등으로 만든 중간 계층
- View: JSX로 렌더링하는 UI
// ViewModel 역할: 데이터를 불러오고, 가공
function useUserViewModel() {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser().then(setUser);//Model
}, []);
return {
userName: user?.name ?? 'Loading...',
};
}
// View
function UserProfile() {
const { userName } = useUserViewModel();
return <div>{userName}</div>;
}
주요 패턴 | MVC | MVP | MVVM |
주 역할 | 사용자 입력 처리 흐름 제어 | Model →View데이터 포맷 | 상태관리 + VIew와 데이터 바인딩 |
view와의 관계 | View호출/명령 | View에 props 전달 | View가 데이터를 구독 |
상태 | x | 간단한 처리 | 직접 관리 |
React에서의 활용 | 이벤트 핸들러, useEffect 내부 | Container 컴포넌트 | 커스텀 훅 |
항목 Controller Presenter ViewModel
FLUX 패턴
기존mvc
MVC 패턴은 양방향 데이터 흐름
Model이 변경되면 View가 변경되며, 유저에 의해 View에서 의도치 않게 Action이 일어난다면 데이터를 관리하고 변환, 처리하는 역할을 가진 Model은 View로부터의 데이터 또한 처리해야함
→ MVC 패턴에서 애플리케이션의 규모가 커지면 커질수록 애플리케이션의 구조가 아주 복잡해짐
단방향 데이터 흐름의 디자인 패턴인 Flux: 데이터의 흐름을 단방향(한 방향)으로 제한함으로써,애플리케이션의 구조와 상태를 심플하게 파악
- Store: 애플리케이션의 state(상태)를 저장하는 object(객체). Dispatcher로부터 state(상태) 갱신을 위한 명령을 받아 state(상태)를 갱신하는 기능도 가지고 있음.
- Action: 유저의 움직임로부터 발생된 이벤트(버튼 클릭, input에 데이터 입력 등)와 API로부터의 데이터 수신 등 state(상태)를 갱신하기 위한 정보를 담은 object(객체)
- Dispatcher: 애플리케이션 내의 모든 Action을 받음. Store에게 Action내용에 따른 state(상태) 갱신(변경)을 명령하는 함수
- View: 애플리케이션의 유저 인터페이스(UI)
- 유저가 View를 조작함으로써 Action이 발생된다.
- state 갱신 내용인 Action가 Dispatcher에 전달된다.
- Dispatcher가 Store에 Action을 전달하면서, Store에게 state 갱신을 명령한다.
- Store가 state를 갱신한다.
- 갱신된 state를 View에게 전달한다.
- 새로운 state가 브라우저(View)에 렌더링(표시)된다.
이러한 Flux의 단방향 데이터 흐름은 기존의 MVC 패턴에 있던 state의 전이를 없애주고, 데이터의 흐름을 단순화시켜 예측 가능하게 해줌으로써 state 관리를 용이하게 해줌
Redux = Reducer + Flux
차이점 | Dispatcher가 애플리케이션 내의 모든 Action을 받으며, 각 Action에 따라 Store에게 state 갱신을 명령 | • Reducer 함수가 Store에 보내져 온 Action과 기존의 state를 토대로 새로운 state를 생성하여 return |
• Store는 반드시 1개만 존재 | ||
• state의 변경, 갱신하려면 Action을 Store에게 dispatch(송신) | ||
공통점 | 단방향 데이터 흐름 | 단방향 데이터 흐름 |
FLUX REDUX
- Store: 애플리케이션의 state(상태)를 저장하는 곳. Store 내에 Reducer 함수가 존재
- Dispatch: Reducer 함수에게 Action을 송신하는 역할
- Action: 유저의 움직임로부터 발생된 이벤트(버튼 클릭, input에 데이터 입력 등)와 API로부터의 데이터 수신 등 state(상태)를 갱신하기 위한 정보를 담은 object(객체). Dispatch 함수에 의해 Action을 Reducer에게 보낼 수 있음
- Reducer: 함수. Dispatch통해 받은 Action을 토대로, state를 modify, 갱신, 변경
- View: 애플리케이션의 유저 인터페이스(UI)