Message-Driven vs. Event-Driven Architecture

Message-Driven vs. Event-Driven Architecture 메시지 기반 (Message‑Driven) 과 이벤트 기반 (Event‑Driven) 은 비동기 통신을 통한 분산 시스템 설계 방식이다. 메시지 기반 (Message‑Driven) 는 명령 (Command) 또는 Request-Response 워크플로우 중심이며 수신자 주소를 알고 직접 메시지를 주고 받는다. 반면 이벤트 기반 (Event‑Driven) 는 상태 변화 (State Change) 를 이벤트로 정의하고 Publish-Subscribe (pub/sub) 나 Event Bus 로 브로드캐스트 (Broadcast) 하며, 소비자 목록을 알 필요 없이 느슨하게 결합된 구조로 동작한다. 두 방식은 구현 목적, 응답 실시간성, 확장성, 복잡성 등에서 차이를 보이며, 실무에서는 상호 보완적으로 병용되기도 한다. ...

June 23, 2025 · 39 min · Me

Load Shifting vs. Load Balancing

Load Shifting vs. Load Balancing 네트워크와 시스템 관리에서 부하 관리는 시스템의 안정성과 효율성을 유지하는 핵심 요소이다. 특히 로드 시프팅과 로드 밸런싱은 자주 혼동되지만 실제로는 매우 다른 개념과 목적을 가지고 있다. 두 기술 모두 시스템 자원을 최적화하는 데 사용되지만, 접근 방식과 적용 시나리오가 다르다. 로드 밸런싱(Load Balancing) 로드 밸런싱은 네트워크 트래픽이나 작업 부하를 여러 서버나 리소스에 고르게 분산시키는 기술이다. 이는 주로 실시간으로 이루어지며, 시스템의 전체적인 성능과 가용성을 향상시키는 것이 목적이다. 주요 특징 목적: 시스템 성능 최적화, 가용성 향상, 응답 시간 개선 타이밍: 실시간 또는 거의 실시간으로 작동 분배 방식: 여러 리소스에 작업을 균등하게 분산 적용 사례: 웹 서버 클러스터, 데이터베이스 클러스터, 컴퓨팅 그리드 로드 밸런싱 알고리즘 라운드 로빈(Round Robin): 순차적으로 각 서버에 요청을 할당한다. ...

April 4, 2025 · 4 min · Me

Load shifting vs. autoscaling

Load Shifting vs. Autoscaling Load Shifting과 Autoscaling은 백엔드 시스템에서 리소스를 효율적으로 관리하기 위한 두 가지 중요한 전략이다. 이들은 서로 다른 목적과 작동 방식을 가지고 있으며, 특정 상황에 따라 적합하게 사용된다. Load Shifting 개념: 워크로드를 피크 시간대에서 비피크 시간대로 이동하여 리소스를 더 균형 있게 활용하는 전략. 주요 목적: 리소스 사용 최적화. 비용 절감 (예: 전력 소비가 적은 시간대 활용). 시스템 안정성 유지. 작동 방식: 작업 스케줄링을 통해 특정 시간대에 작업을 재배치. 지리적 부하 이동을 통해 다른 데이터 센터나 지역으로 워크로드를 분산. 적용 사례: 배치 작업을 야간에 실행하여 낮 시간대 사용자 요청 처리에 집중. 전력 비용이 낮은 시간대에 대규모 작업 수행. Autoscaling 개념: 실시간 수요 변화에 따라 시스템의 리소스(예: 서버 인스턴스)를 자동으로 확장하거나 축소하는 기술. 주요 목적: 실시간 트래픽 변화 대응. 과도한 리소스 프로비저닝 방지로 비용 최적화. 성능 및 사용자 경험 개선. 작동 방식: CPU, 메모리, 네트워크 트래픽 등 실시간 메트릭을 기반으로 인스턴스 수를 조정. 수요 증가 시 서버를 추가하고, 수요 감소 시 서버를 비활성화. 적용 사례: 전자상거래 사이트에서 트래픽 급증 시 서버 자동 확장. 클라우드 환경에서 이벤트 기반 애플리케이션의 동적 확장. 차이점 비교 특성 Load Shifting Autoscaling 목적 워크로드를 시간 또는 지역적으로 이동하여 리소스 최적화 실시간 수요 변화에 따라 리소스를 자동 조정 작동 방식 작업 스케줄링 및 지리적 부하 이동 실시간 메트릭 기반 서버 확장 및 축소 주요 적용 사례 배치 작업 재조정, 전력 비용 최적화 웹 애플리케이션의 트래픽 급증 대응 자동화 수준 사전 계획된 스케줄 기반 실시간 동적 조정 비용 절감 방식 비피크 시간대 활용 필요 시 인스턴스 추가 및 제거로 비용 최적화 사용 환경 장기적인 워크로드 관리 단기적인 수요 변화 대응 용어 정리 용어 설명 참고 및 출처

April 2, 2025 · 2 min · Me

Rate Limiting vs. Throttling

Rate Limiting vs. Throttling Rate Limiting과 Throttling은 API 설계와 관리에서 핵심적인 요소로, 시스템의 안정성과 보안을 유지하는 데 중요한 역할을 한다. Rate Limiting과 Throttling은 모두 시스템 보호와 최적화를 위한 중요한 기술이지만, 그 목적과 구현 방식에는 명확한 차이가 있다. Rate Limiting은 특정 시간 내 허용되는 요청 수를 제한하여 남용을 방지하는 데 중점을 두는 반면, Throttling은 요청 처리 속도를 조절하여 시스템 리소스를 효율적으로 사용하는 데 중점을 둔다. 실제 애플리케이션에서는 두 기술을 함께 사용하여 더욱 견고하고 효율적인 시스템을 구축하는 것이 일반적입니다. Rate Limiting을 통해 과도한 요청을 차단하고, Throttling을 통해 허용된 요청을 적절한 속도로 처리함으로써 시스템의 안정성과 성능을 모두 확보할 수 있다. ...

February 25, 2025 · 7 min · Me

Replication

Replication 복제 (Replication) 는 시스템 디자인에서 데이터나 서비스의 동일한 복사본을 여러 위치에 유지하는 기술이다. 주요 목적은 장애 발생 시에도 서비스 가용성을 보장하고, 지리적으로 분산된 사용자에게 낮은 지연 시간을 제공하며, 읽기 성능을 향상시키는 것이다. 복제 방식은 동기식과 비동기식으로 나뉘며, 액티브 - 패시브 (마스터 - 슬레이브), 액티브 - 액티브 (다중 마스터) 등의 아키텍처를 통해 구현된다. 일관성, 가용성, 분할 내성 사이의 트레이드오프를 고려해 적절한 복제 전략을 선택하는 것이 중요하다. 핵심 개념 복제 (Replication) 는 시스템 디자인에서 데이터의 복사본을 여러 노드나 시스템에 분산시켜 저장하는 기술이다. 이를 통해 단일 장애점 (Single Point of Failure) 을 방지하고 시스템 가용성 (Availability) 을 높이는 것이 주요 목적이다. ...

February 18, 2025 · 24 min · Me

Rate Limiting

Rate Limiting Rate Limiting은 MSA(Microservices Architecture) 환경에서 시스템 보안과 안정성을 유지하기 위한 핵심 기술로, 과도한 트래픽으로 인한 서비스 장애 방지와 악성 공격 차단을 목표로 한다. 클라이언트/서비스 간 요청 처리량을 제어하는 메커니즘으로, 특히 API 기반 마이크로서비스 통신에서 중요하다. Rate Limiting은 단순 트래픽 제어를 넘어 마이크로서비스 생태계의 안전벨트 역할을 수행한다. 2025년 현재, 주요 클라우드 제공업체들은 AI 기반 예측 차단 기능을 표준으로 제공하며, 이는 시스템 보안 설계 시 필수 요소로 자리잡았다. 효과적 구현을 위해서는 서비스 특성에 맞는 알고리즘 선택과 지속적 모니터링 체계 수립이 관건이다. ...

November 18, 2024 · 4 min · Me

Caching

캐싱 (Caching) 캐싱은 반복 접근이 많은 데이터를 빠른 저장소 (메모리, 디스크, 네트워크 등) 에 임시 저장하여 응답 속도를 높이고 시스템 부하를 줄이는 핵심 최적화 기술이다. 이는 CPU 와 메모리 사이의 하드웨어 캐시부터 애플리케이션 레벨의 In-Memory 캐시, Redis 같은 분산 캐시, CDN 기반 프록시 캐시까지 다양한 계층에서 적용된다. 캐싱은 시간·공간적 지역성 (locality) 을 활용하며, 구현 시 LRU/LFU 같은 교체 정책, TTL 기반 만료, Write-Through/Back 전략 등이 사용된다. 효과적인 캐시 설계는 일관성 (consistency), 무효화 (invalidation), 만료 (expiration) 정책과 함께 계층적 구조 (cache hierarchy) 를 고려해야 하며, 성능 향상, 비용 절감, 확장성 확보 등의 장점이 있다. 반면, 일관성 유지와 메모리 오버헤드는 주요 도전 과제로 남는다. ...

September 30, 2024 · 91 min · Me

Master-Slave

Master-Slave Pattern 마스터-슬레이브 패턴(Master-Slave Pattern)은 분산 시스템에서 널리 사용되는 소프트웨어 아키텍처 패턴. 이 패턴은 하나의 마스터 컴포넌트와 여러 슬레이브 컴포넌트로 구성되어 있으며, 작업을 효율적으로 분배하고 관리하는 데 사용된다. 주요 구성요소 마스터(Master): 작업 분배와 조정을 담당합니다 슬레이브들의 상태를 관리합니다 작업의 완료 여부를 추적합니다 결과를 취합하고 클라이언트에게 전달합니다 슬레이브(Slave): 마스터로부터 할당받은 작업을 처리합니다 독립적으로 동작합니다 처리 결과를 마스터에게 반환합니다 자신의 상태(사용 가능/처리 중)를 관리합니다 작업(Task): 처리해야 할 작업의 단위입니다 작업에 필요한 데이터와 결과를 포함합니다 고유한 식별자를 가집니다 작동 방식 마스터는 전체 작업을 여러 개의 하위 작업으로 분할합니다. 분할된 작업을 슬레이브들에게 분배합니다. 슬레이브들은 할당받은 작업을 독립적으로 수행합니다. 작업 완료 후, 슬레이브들은 결과를 마스터에게 보고합니다. 마스터는 모든 결과를 취합하여 최종 결과를 생성합니다. 장점 병렬 처리: 여러 슬레이브가 동시에 작업을 수행하여 전체 처리 속도를 향상시킵니다. 확장성: 슬레이브의 수를 늘리거나 줄여 시스템의 처리 능력을 조절할 수 있습니다. 부하 분산: 마스터가 작업을 효율적으로 분배하여 시스템 자원을 최적화할 수 있습니다. fault tolerance: 일부 슬레이브가 실패해도 마스터가 작업을 재분배하여 시스템이 계속 작동할 수 있습니다. 단점 단일 장애점: 마스터 노드가 실패하면 전체 시스템이 중단될 수 있습니다. 복잡성: 여러 노드 간의 통신과 동기화를 관리해야 하므로 시스템이 복잡해질 수 있습니다. 불균형한 작업 크기: 작업의 크기가 불균형할 경우 일부 슬레이브가 과부하될 수 있습니다. 적용 분야 데이터베이스 복제: 마스터 데이터베이스가 쓰기 작업을 처리하고, 슬레이브 데이터베이스들이 읽기 작업을 분산 처리합니다. 분산 컴퓨팅: 대규모 계산 작업을 여러 노드에 분산하여 처리합니다. 데이터 처리: 빅데이터 처리 시스템에서 마스터 노드가 작업을 관리하고 슬레이브 노드들이 실제 데이터 처리를 수행합니다. 임베디드 시스템: 여러 센서나 액추에이터를 제어하는 데 사용됩니다. 구현 예시 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 import threading from abc import ABC, abstractmethod from queue import Queue from typing import List import time import random # 작업을 정의하는 기본 클래스 class Task: def __init__(self, task_id: int, data: List[int]): self.task_id = task_id self.data = data self.result = None # 슬레이브의 추상 클래스 class Slave(ABC): def __init__(self, slave_id: int): self.slave_id = slave_id self.is_busy = False @abstractmethod def process_task(self, task: Task) -> None: pass # 구체적인 슬레이브 구현 - 숫자 배열의 합을 계산 class SumCalculatorSlave(Slave): def process_task(self, task: Task) -> None: print(f"Slave {self.slave_id} starting task {task.task_id}") # 실제 작업 처리를 시뮬레이션하기 위한 지연 time.sleep(random.uniform(0.5, 2.0)) task.result = sum(task.data) print(f"Slave {self.slave_id} completed task {task.task_id}, result: {task.result}") # 마스터 클래스 class Master: def __init__(self, num_slaves: int): # 슬레이브 풀 초기화 self.slaves = [SumCalculatorSlave(i) for i in range(num_slaves)] # 작업 큐 self.task_queue = Queue() # 완료된 작업 저장 self.completed_tasks = {} # 작업 분배를 위한 쓰레드 self.distribution_thread = threading.Thread(target=self._distribute_tasks) self.is_running = True def start(self): """마스터 시작""" print("Master starting…") self.distribution_thread.start() def stop(self): """마스터 종료""" print("Master stopping…") self.is_running = False self.distribution_thread.join() def submit_task(self, task: Task): """새로운 작업 제출""" print(f"Submitting task {task.task_id}") self.task_queue.put(task) def get_result(self, task_id: int) -> int: """작업 결과 조회""" while task_id not in self.completed_tasks: time.sleep(0.1) # 결과가 준비될 때까지 대기 return self.completed_tasks[task_id] def _distribute_tasks(self): """작업 분배 로직""" while self.is_running: try: # 대기 중인 작업이 있는지 확인 task = self.task_queue.get(timeout=1.0) # 사용 가능한 슬레이브 찾기 slave = self._get_available_slave() if slave: # 작업 처리를 위한 새 쓰레드 시작 threading.Thread( target=self._process_task_with_slave, args=(slave, task) ).start() except Queue.Empty: continue def _get_available_slave(self) -> Slave: """사용 가능한 슬레이브 찾기""" for slave in self.slaves: if not slave.is_busy: return slave return None def _process_task_with_slave(self, slave: Slave, task: Task): """슬레이브를 사용하여 작업 처리""" try: slave.is_busy = True slave.process_task(task) self.completed_tasks[task.task_id] = task.result finally: slave.is_busy = False # 사용 예시 def main(): # 3개의 슬레이브로 마스터 생성 master = Master(num_slaves=3) master.start() try: # 여러 작업 제출 tasks = [ Task(1, [1, 2, 3, 4, 5]), Task(2, [10, 20, 30, 40, 50]), Task(3, [100, 200, 300, 400, 500]), Task(4, [1000, 2000, 3000, 4000, 5000]) ] # 작업 제출 for task in tasks: master.submit_task(task) # 결과 수집 for task in tasks: result = master.get_result(task.task_id) print(f"Final result for task {task.task_id}: {result}") # 잠시 대기 후 종료 time.sleep(5) finally: master.stop() if __name__ == "__main__": main() 용어 정리 용어 설명 참고 및 출처

September 27, 2024 · 4 min · Me

Incremental Model

증분 모델 (Incremental Model) 전체 시스템을 여러 개의 작은 부분(증분)으로 나누어 순차적으로 개발하고 제공하는 접근 방식. 각 증분은 완전한 기능을 갖춘 소프트웨어의 일부분으로, 사용자에게 점진적으로 제공 %%{init: {'theme': 'default', 'themeVariables': { 'fontSize': '14px'}, 'flowchart': {'width': 800, 'height': 600, 'diagramPadding': 15}}}%% graph TD %% 시작점 Start([프로젝트 시작]) --> Initial[초기 요구사항 분석] %% 증분 1: 핵심 기능 subgraph Inc1 [증분 1: 핵심 기능] R1[요구분석] --> D1[설계] D1 --> I1[구현] I1 --> T1[테스트] T1 --> V1[검증] end %% 증분 2: 확장 기능 subgraph Inc2 [증분 2: 확장 기능] R2[요구분석] --> D2[설계] D2 --> I2[구현] I2 --> T2[테스트] T2 --> V2[검증] end %% 증분 3: 최종 기능 subgraph Inc3 [증분 3: 최종 기능] R3[요구분석] --> D3[설계] D3 --> I3[구현] I3 --> T3[테스트] T3 --> V3[검증] end %% 증분 간 연결 Initial --> Inc1 V1 --> Inc2 V2 --> Inc3 V3 --> End([프로젝트 완료]) %% 산출물 연결 V1 -.제품 릴리즈 1.-> Rel1[동작하는 핵심 시스템] V2 -.제품 릴리즈 2.-> Rel2[확장된 시스템] V3 -.최종 릴리즈.-> Rel3[완성된 시스템] %% 피드백 루프 Rel1 -.피드백.-> R2 Rel2 -.피드백.-> R3 %% 스타일 정의 classDef default fill:#f9f9f9,stroke:#333,stroke-width:1px classDef phase fill:#e1f5fe,stroke:#01579b,stroke-width:1px classDef milestone fill:#e8f5e9,stroke:#2e7d32,stroke-width:1px classDef release fill:#fff3e0,stroke:#e65100,stroke-width:1px class Start,End,Initial milestone class R1,D1,I1,T1,V1,R2,D2,I2,T2,V2,R3,D3,I3,T3,V3 phase class Rel1,Rel2,Rel3 release style Inc1 fill:#f0f4f8,stroke:#666,stroke-width:1px style Inc2 fill:#e1f5fe,stroke:#666,stroke-width:1px style Inc3 fill:#e8f5e9,stroke:#666,stroke-width:1px 주요 단계 요구사항 분석: 현재 증분에 포함될 기능을 정의. 설계: 시스템 아키텍처와 상세 설계를 수행. 구현: 실제 코드를 작성. 테스트: 구현된 기능을 테스트하고 버그를 수정. 통합 및 배포: 새로운 증분을 기존 시스템과 통합하고 사용자에게 제공. 특징 단계적 개발: 전체 시스템을 여러 개의 증분으로 나누어 개발. 순차적 제공: 각 증분을 완성할 때마다 사용자에게 제공. 기능 우선순위: 중요도나 우선순위에 따라 증분을 계획. 반복적 프로세스: 각 증분마다 요구사항 분석부터 테스트까지의 과정을 반복. 점진적 기능 확장: 각 증분마다 새로운 기능이 추가되거나 기존 기능이 개선. 장점 조기 제품 출시: 첫 번째 증분부터 사용 가능한 제품을 제공할 수 있다. 유연한 변경 관리: 각 증분 사이에 요구사항 변경을 반영할 수 있다. 위험 감소: 중요한 기능을 먼저 개발하여 주요 위험을 조기에 해결할 수 있다. 사용자 피드백 활용: 각 증분 후 사용자 피드백을 받아 다음 증분에 반영할 수 있다. 병렬 개발 가능: 여러 팀이 동시에 다른 증분을 개발할 수 있다. 단점 전체 아키텍처 설계 필요: 초기에 전체 시스템의 아키텍처를 설계해야 한다. 인터페이스 관리 복잡성: 증분 간 인터페이스 관리가 복잡할 수 있다. 문서화 부담: 각 증분마다 문서화가 필요하여 작업량이 증가할 수 있다. 전체 비용 증가: 여러 번의 통합과 테스트로 인해 전체 비용이 증가할 수 있다. 적합한 프로젝트 유형 주요 요구사항은 명확하지만 세부사항은 변경될 수 있는 프로젝트 빠른 시장 출시가 필요한 프로젝트 새로운 기술이나 기능을 점진적으로 도입하고자 할 때 자금이나 인력 등의 자원이 제한적인 경우 참고 및 출처

September 21, 2024 · 2 min · Me

Prototyping Model

프로토타이핑(Prototyping) 모델 최종 제품의 초기 버전 또는 모형을 만들어 사용자의 피드백을 받고 요구사항을 명확히 하는 방법. 이 모델은 특히 사용자 인터페이스나 시스템의 기능이 명확하지 않을 때 유용 %%{init: {'theme': 'default', 'themeVariables': { 'fontSize': '14px'}, 'flowchart': {'width': 800, 'height': 600, 'diagramPadding': 15}}}%% graph TD Start([프로젝트 시작]) --> Init[요구사항 수집] subgraph PrototypeCycle [프로토타입 개발 사이클] subgraph Requirements [1. 요구분석] R1[요구사항 정의] --> R2[범위 설정] end subgraph Design [2. 설계] D1[기본 설계] --> D2[UI/UX 설계] end subgraph Build [3. 구현] B1[프로토타입 개발] --> B2[기능 구현] end subgraph Evaluate [4. 평가] E1[사용자 테스트] --> E2[피드백 수집] end end subgraph Final [최종 단계] F1[프로토타입 개선] --> F2[최종 개발] end %% 메인 프로세스 흐름 Init --> Requirements Requirements --> Design Design --> Build Build --> Evaluate Evaluate --> Decision{요구사항 충족?} Decision -->|No| F1 F1 --> Requirements Decision -->|Yes| F2 F2 --> End([프로젝트 완료]) %% 주요 특성 subgraph Features [핵심 특성] C1[빠른 개발] C2[사용자 참여] end %% 스타일 정의 classDef default fill:#f9f9f9,stroke:#333,stroke-width:1px classDef phase fill:#e1f5fe,stroke:#01579b,stroke-width:1px classDef decision fill:#fff3e0,stroke:#e65100,stroke-width:1px classDef milestone fill:#e8f5e9,stroke:#2e7d32,stroke-width:1px class Start,End,Init milestone class R1,R2,D1,D2,B1,B2,E1,E2,F1,F2 phase class Decision decision class C1,C2 phase style PrototypeCycle fill:#fafafa,stroke:#666,stroke-width:1px style Final fill:#e1f5fe,stroke:#666,stroke-width:1px style Features fill:#f5f5f5,stroke:#666,stroke-width:1px 주요 단계 요구사항 수집: 기본적인 요구사항을 수집. 빠른 설계: 프로토타입의 초기 설계를 수행. 프로토타입 구축: 작동하는 프로토타입을 개발. 사용자 평가: 사용자가 프로토타입을 사용해보고 피드백을 제공. 프로토타입 개선: 사용자 피드백을 바탕으로 프로토타입을 수정. 최종 제품 개발: 완성된 프로토타입을 바탕으로 최종 제품을 개발. 특징 빠른 개발: 초기 버전을 신속하게 만들어 사용자에게 제시한다. 반복적 개선: 사용자 피드백을 바탕으로 프로토타입을 지속적으로 개선한다. 시각화: 추상적인 아이디어를 구체적인 형태로 시각화한다. 요구사항 명확화: 사용자와의 상호작용을 통해 요구사항을 더 정확히 파악한다. 위험 감소: 초기 단계에서 설계 문제를 발견하고 수정할 수 있다. 유형 일회용 프로토타이핑: 프로토타입을 버리고 최종 제품을 새로 개발. 진화형 프로토타이핑: 프로토타입을 계속 개선하여 최종 제품으로 발전. 증분형 프로토타이핑: 시스템을 작은 단위로 나누어 각각 프로토타입을 만들고 통합. 장점 사용자 참여 증가: 사용자가 개발 과정에 적극적으로 참여하게 된다. 요구사항 명확화: 사용자의 실제 요구사항을 더 정확히 파악할 수 있다. 조기 피드백: 개발 초기 단계에서 문제점을 발견하고 수정할 수 있다. 사용성 향상: 사용자 인터페이스와 사용자 경험을 개선할 수 있다. 위험 감소: 잘못된 설계나 기능을 초기에 식별하고 수정할 수 있다. 단점 시간과 비용 증가: 여러 번의 프로토타입 개발로 인해 초기 비용이 증가할 수 있다. 불완전한 문서화: 빠른 개발로 인해 문서화가 부족할 수 있다. 과도한 사용자 기대: 사용자가 프로토타입을 완성된 제품으로 오해할 수 있다. 부적절한 프로토타입 사용: 임시로 만든 프로토타입이 최종 제품의 기반이 될 수 있다. 적합한 프로젝트 유형 사용자 인터페이스가 중요한 프로젝트 요구사항이 불명확하거나 복잡한 경우 새로운 기술이나 혁신적인 제품을 개발할 때 사용자와의 지속적인 상호작용이 필요한 프로젝트 참고 및 출처

September 21, 2024 · 2 min · Me