diff --git "a/\354\261\225\355\204\260_7/\354\235\264\354\203\201\354\241\260.md" "b/\354\261\225\355\204\260_7/\354\235\264\354\203\201\354\241\260.md" index 14f29c3..3b45cdf 100644 --- "a/\354\261\225\355\204\260_7/\354\235\264\354\203\201\354\241\260.md" +++ "b/\354\261\225\355\204\260_7/\354\235\264\354\203\201\354\241\260.md" @@ -122,3 +122,469 @@ ensureImplements 메서드를 사용해서 인터페이스에 정의된 actions 비슷한 개념인 것 같음. 일단 데코레이터 패턴의 예시처럼 기본 객체에 계속 데코레이터들이 뭘 추가해주는 형태로 진행되는데, 여기도 결국 ensureImplements를 사용해서 반드시 메서드를 구현하도록 한다고 나와있다. 데코레이터는 아닌데, 우리 회사에서는 추상 클래스로 추상 메서드를 반드시 구현하게 하는 그런 코드가 좀 남아있다... 물론 지금은 사용하지 않는 코드 + + +## 플라이웨이트 패턴 + +같은 것은 한 번만 만들고 공유해서 메모리를 절약하는 방식. +책 설명이 이해가 안가서 이것저것 찾아보고 나름대로 정리; + +```js +// 플라이웨이트 클래스 (공유될 데이터를 담고 있음. 여기에 내재적 상태가 담김) +class 휴대폰Type { + constructor(제조사, 모델명) { + this.제조사 = 제조사; + this.모델명 = 모델명; + } +} + +// 플라이웨이트 팩토리 클래스 (공유 객체를 관리) +class 휴대폰Factory { + constructor() { + this.휴대폰Types = {}; + } + + get휴대폰Type(제조사, 모델명) { + // 이미 해당 타입이 있다면 그것을 반환 + const key = 모델명; + if (!this.휴대폰Types[key]) { + // 없다면 새로 만들어서 저장 + this.휴대폰Types[key] = new 휴대폰Type(제조사, 모델명); + } + + return this.휴대폰Types[key]; + } +} + +// 실제 휴대폰 인스턴스 (고유한 데이터를 가짐. 여기에 외재적 상태가 담김) +class 휴대폰 { + constructor(벨소리, 휴대폰Type) { + this.벨소리 = 벨소리; + this.휴대폰Type = 휴대폰Type; // 공유되는 데이터 + } + + 전화오기() { + console.log(this.휴대폰Type.벨소리); + } +} + +// 사용 예시 +const factory = new 휴대폰Factory(); + +// 휴대폰 타입 생성 +const 갤럭시24Type = factory.get휴대폰Type('삼성', '갤럭시24'); +const 아이폰16Type = factory.get휴대폰Type('애플', '아이폰16'); + +// 여러 휴대폰 인스턴스 생성 +const 엄마_갤럭시 = new 휴대폰('따르릉', 갤럭시24Type); +const 아빠_갤럭시 = new 휴대폰('진동', 갤럭시24Type); +const 내_아이폰 = new 휴대폰('무음', 아이폰16Type); + +엄마_갤럭시.전화오기(); // "따르릉" +아빠_갤럭시.전화오기(); // "진동" +내_아이폰.전화오기(); // "무음" +``` + +이 예시에서: +1. `휴대폰Type`은 여러 휴대폰이 공유할 수 있는 데이터를 가짐 +2. `휴대폰Factory`는 이미 만들어진 휴대폰 타입을 재사용할 수 있도록 관리 +3. `휴대폰` 클래스는 각 휴대폰의 고유한 데이터(벨소리)와 공유 데이터(휴대폰Type: 기종과 제조사)를 함께 사용 + +이제 100대의 삼성폰을 만들더라도 제조사와 기종은 한 번만 메모리에 저장, 각 삼성폰은 본인의 벨소리만 가지면 되므로 메모리를 절약 +뭔가 복잡하다... + +### 중앙 집중식 이벤트 핸들링 + +이건 흔히 제어의 역전 예시로 드는 이벤트 위임, 리액트에서 구현한 중앙 집중식 이벤트 처리를 생각하면 될 것 같다? + +## 행위 패턴 +- 관찰자 패턴 +- 중재자 패턴 +- 커맨드 패턴 + +## 관찰자 패턴 + +이것도 예시가 너무 복잡해서 읽다가 짜증났음 +뭔가를 구독하고 구독한 것들에게 뭘 알려주고 이런 것으로 이해 +유튜브로 이해하면 편할 듯 + +```js +// 주체(Subject) 클래스 +class YoutubeChannel { + constructor() { + this.subscribers = []; // 구독자 목록 + this.latestVideo = null; // 최신 영상 + } + + // 구독자 추가 + subscribe(observer) { + if (!this.subscribers.includes(observer)) { + this.subscribers.push(observer); + console.log('새 구독자 추가'); + } + } + + // 구독 취소 + unsubscribe(observer) { + this.subscribers = this.subscribers.filter(sub => sub !== observer); + console.log('구독 취소'); + } + + // 모든 구독자에게 알림 + notifySubscribers() { + this.subscribers.forEach(subscriber => { + subscriber.update(this.latestVideo); + }); + } + + // 새 영상 업로드 + uploadVideo(title) { + this.latestVideo = title; + console.log(`새 영상: ${title}`); + this.notifySubscribers(); // 모든 구독자에게 알림 보내기 + } +} + +// 관찰자(Observer) 클래스 +class Subscriber { + constructor(name) { + this.name = name; + } + + update(videoTitle) { + console.log(`${this.name}, 구독한 채널의 새 영상이 올라옴: ${videoTitle}`); + } +} + +// 사용 예시 +const 디지몬Channel = new YoutubeChannel(); + +const subscriber1 = new Subscriber('피카츄'); +const subscriber2 = new Subscriber('파이리'); +const subscriber3 = new Subscriber('꼬부기'); + +// 구독하기 +디지몬Channel.subscribe(subscriber1); +디지몬Channel.subscribe(subscriber2); +디지몬Channel.subscribe(subscriber3); + +// 새 영상 업로드 +디지몬Channel.uploadVideo('디지몬 어드밴쳐 극장판'); + +// 구독 취소 +디지몬Channel.unsubscribe(subscriber2); + +// 다른 영상 업로드 +디지몬Channel.uploadVideo('디지몬 테이머즈'); +``` + +리액트에서는 어떻게 쓰이는지 궁금해서 커서한테 물어봤더니 만들어줬음 + +```js +// 커스텀 이벤트 관리자 +class EventManager { + constructor() { + this.events = {}; + } + + // 이벤트 구독 + subscribe(eventName, callback) { + if (!this.events[eventName]) { + this.events[eventName] = []; + } + this.events[eventName].push(callback); + + // 구독 취소 함수 반환 + return () => { + this.events[eventName] = this.events[eventName].filter(cb => cb !== callback); + }; + } + + // 이벤트 발행 + publish(eventName, data) { + if (this.events[eventName]) { + this.events[eventName].forEach(callback => callback(data)); + } + } +} + +// 전역 이벤트 관리자 인스턴스 +const eventManager = new EventManager(); + +// 알림 컴포넌트 +function NotificationComponent() { + const [notifications, setNotifications] = useState([]); + + useEffect(() => { + // 새로운 알림 구독 + const unsubscribe = eventManager.subscribe('NEW_NOTIFICATION', (message) => { + setNotifications(prev => [...prev, message]); + }); + + // 컴포넌트 언마운트 시 구독 취소 + return () => unsubscribe(); + }, []); + + return ( +
+ {notifications.map((notification, index) => ( +
+ {notification} +
+ ))} +
+ ); +} + +// 알림 발생 버튼 컴포넌트 +function NotificationButton() { + const handleClick = () => { + eventManager.publish('NEW_NOTIFICATION', `새로운 알림 - ${Date.now()}`); + }; + + return ( + + ); +} + +// 메인 앱 컴포넌트 +function App() { + return ( +
+ + +
+ ); +} +``` + +### 관찰자 패턴 vs 발행/구독 패턴 + +관찰자 패턴은 발행자와 구독자가 서로를 알고 있어야 하지만 발행/구독 패턴은 그것을 중계해주는 채널이 있어서 발행자와 구독자는 서로를 몰라도 된다고 함 +유튜브로 예를 들면 위의 관찰자 예시는 유튜버가 구독자가 누군지 알고 있는 형태 +발행/구독 패턴의 경우 유튜버는 유튜브 플랫폼에다 영상 열심히 올리기만 하고, 유튜브 플랫폼이 새로운 영상 받으면 알아서 구독자에게 알려주는 역할 + + +## 중재자 패턴 + +갑자기 여기서 플라이웨이트 패턴때 설명했던 이벤트 위임을 얘기하는데, 그래서 이벤트 위임은 어디에 해당한다는 건지...? +중재자 패턴과 발행/구독 패턴이 비슷하게 느껴짐. 둘 다 소통하는 객체 사이에 통신을 중개하는 레이어를 두는 것이 특징인 것 같다. +어떻게 다른지 구체적으로 알고싶어서 클로드한테 물어봄 + +발행/구독 패턴: +1. 유튜버는 그냥 영상을 올립니다. 누가 볼지 신경 쓰지 않죠. +2. 구독자들은 그냥 채널을 구독합니다. 다른 구독자가 누군지 모릅니다. +3. 유튜브 플랫폼은 단순히 "이 채널에 새 영상이 올라왔어요"라고 알려주기만 합니다. + +```javascript +// 발행/구독 예시 +class YouTube { + constructor() { + this.channels = new Map(); // { 채널명: [구독자들] } + } + + subscribe(channelName, subscriber) { + if (!this.channels.has(channelName)) { + this.channels.set(channelName, []); + } + this.channels.get(channelName).push(subscriber); + } + + // 단순히 알림만 전달 + notify(channelName, video) { + const subscribers = this.channels.get(channelName) || []; + subscribers.forEach(subscriber => { + subscriber.update(`${channelName}에 새 영상이 업로드됨: ${video}`); + }); + } +} +``` + +중재자 패턴: +1. 메시지를 보낼 때 특정 사람을 지정할 수 있습니다. +2. 귓속말도 가능합니다. +3. 카카오톡 서버는 "누가 누구에게" 보내는지 알고 있고, 그에 따라 다르게 처리합니다. +4. 방장 권한, 강퇴, 초대 등 복잡한 규칙이 있습니다. + +```javascript +// 중재자 예시 +class KakaoTalkRoom { + constructor() { + this.users = new Map(); + this.admin = null; + } + + // 복잡한 규칙들을 관리 + sendMessage(from, message, to = null) { + if (this.isBanned(from)) { + return this.notify(from, "당신은 차단된 사용자입니다."); + } + + if (to) { + // 귓속말 + this.sendPrivateMessage(from, to, message); + } else { + // 전체 메시지 + this.broadcastMessage(from, message); + } + } + + kickUser(admin, userToKick) { + if (this.admin !== admin) { + return this.notify(admin, "방장만 강퇴할 수 있습니다."); + } + // 강퇴 로직... + } +} +``` + +둘의 차이 +1. **복잡도** + - 발행/구독: 단순한 알림 전달만 함 (유튜브 알림처럼) + - 중재자: 복잡한 규칙과 로직이 있음 (카카오톡 방처럼) + +2. **방향성** + - 발행/구독: 일방적인 알림 (유튜버 → 구독자) + - 중재자: 양방향 소통 (카톡방 사람들끼리) + +3. **규칙** + - 발행/구독: 거의 없음 ("구독하면 알림 받음" 정도) + - 중재자: 많음 (방장 권한, 귓속말, 강퇴 등) + +그렇다고 한다. +책보다 클로드한테 물어보는게 더 이해가 잘되는 듯. + +책의 설명을 조금 곁들이자면, +이벤트 집합 패턴(발행/구독): 실행되어야 하는 모든 비즈니스 로직은 이벤트 발생 객체와 처리 객체에 직접 구현된다. +중재자 패턴: 모든 비즈니스 로직이 중재자 내부에 집중된다. + +## 커맨드 패턴 + +이것도 책 예시가 조금 불친절한 것 같아서 자세히 알아보기 위해 클로드를 활용 +책 설명을 보니까 abstract 클래스를 쓰면 편할 것 같아 타입스크립트로 만들어보라고 함 + +```typescript +// 추상 커맨드 클래스 +abstract class Command { + abstract execute(): void; + abstract undo(): void; +} + +// Receiver: TV +class TV { + private isOn: boolean = false; + + turnOn(): void { + this.isOn = true; + console.log('TV가 켜졌습니다.'); + } + + turnOff(): void { + this.isOn = false; + console.log('TV가 꺼졌습니다.'); + } + + getStatus(): boolean { + return this.isOn; + } +} + +// Concrete Command: TV 전원 커맨드 +class PowerCommand extends Command { + constructor(private tv: TV) { + super(); + } + + execute(): void { + if (this.tv.getStatus()) { + this.tv.turnOff(); + } else { + this.tv.turnOn(); + } + } + + undo(): void { + this.execute(); // 전원은 토글이므로 같은 동작 + } +} + +// Invoker: 리모컨 +class RemoteControl { + private history: Command[] = []; + + pressButton(command: Command): void { + command.execute(); + this.history.push(command); + } + + undo(): void { + const command = this.history.pop(); + if (command) { + command.undo(); + } + } +} + +// 사용 예시 +const tv = new TV(); +const powerCommand = new PowerCommand(tv); +const remote = new RemoteControl(); + +remote.pressButton(powerCommand); // TV 켜기 +remote.pressButton(powerCommand); // TV 끄기 +remote.undo(); // 마지막 동작 취소 +``` + + +실생활에서 TV 리모컨을 사용할 때를 생각해보세요. 리모컨의 버튼을 누르면 TV가 그에 맞는 동작을 수행합니다. 이때 리모컨은 TV의 내부 동작 방식을 전혀 알 필요가 없습니다. 단지 "이 버튼을 누르면 이런 일이 일어난다"는 것만 알면 됩니다. +커맨드 패턴의 구성 요소를 리모컨의 관점에서 보면: + +1. **추상 Command 클래스** + - 모든 리모컨 버튼의 기본 형태를 정의합니다. + - execute()와 undo() 라는 두 가지 추상 메서드를 가집니다. + - 마치 리모컨 버튼의 설계도와 같습니다. + +2. **TV (Receiver)** + - 실제로 작동하는 기기입니다. + - 자신이 할 수 있는 기능들을 가지고 있습니다 (전원 켜기/끄기). + - 누가 자신을 제어하는지는 알 필요가 없습니다. + +3. **PowerCommand (Concrete Command)** + - 실제 리모컨의 전원 버튼과 같습니다. + - TV를 알고 있어서 TV에게 적절한 명령을 내릴 수 있습니다. + - execute()는 TV의 현재 상태에 따라 켜거나 끄는 동작을 수행합니다. + - undo()는 이전 상태로 되돌리는 동작을 수행합니다. + +4. **RemoteControl (Invoker)** + - 실제 리모컨 본체입니다. + - 버튼들의 명령을 실행하고 기록을 관리합니다. + - 어떤 버튼이 어떤 동작을 하는지만 알면 됩니다. + +이 패턴의 가장 큰 장점은 각 부분이 서로 독립적이라는 것입니다: + +1. **분리된 책임** + - TV는 자신의 기능만 구현하면 됩니다. + - 리모컨은 명령을 전달하기만 하면 됩니다. + - 각 명령은 자신의 동작만 알면 됩니다. + +2. **유연한 확장** + - 새로운 기능을 추가하려면 새로운 Command 클래스만 만들면 됩니다. + - 기존 코드를 수정할 필요가 없습니다. + +3. **실행 취소 기능** + - 모든 명령이 undo() 메서드를 구현하므로 쉽게 실행 취소가 가능합니다. + - 명령의 기록을 저장해두면 여러 단계의 실행 취소도 가능합니다. + +타입스크립트의 abstract 클래스를 사용하면: +1. 모든 커맨드가 반드시 execute()와 undo()를 구현하도록 강제할 수 있습니다. +2. 컴파일 시점에 타입 체크가 가능합니다. +3. 코드의 안정성이 높아집니다. + +이 패턴은 특히: +- 실행 취소/다시 실행이 필요할 때 +- 명령을 큐에 저장해야 할 때 +- 명령의 실행을 지연시키고 싶을 때 +매우 유용하게 사용됩니다. +