Chat Conversation

참여자: User, QueryPie

Message 1 from User

프로그래밍에서 사용부에 context 객체를 제공하는 패턴에 대해서 자세히 알려줘

Message 2 from QueryPie

Context 패턴은 프로그래밍에서 애플리케이션의 상태, 설정, 의존성 등을 하나의 객체로 묶어서 전달하는 설계 패턴입니다. 이 패턴은 특히 복잡한 애플리케이션에서 공통적으로 필요한 데이터나 서비스를 효율적으로 관리하고 전파하는 데 사용됩니다.

Context 패턴의 핵심 개념

Context 객체는 애플리케이션의 실행 환경에서 필요한 정보들을 캡슐화한 컨테이너 역할을 합니다. 이는 전역 상태를 피하면서도 필요한 데이터를 계층 구조를 통해 전달할 수 있게 해줍니다.

주요 특징:

  • 상태와 동작의 캡슐화
  • 의존성 주입의 역할
  • 계층적 데이터 전파
  • 테스트 가능성 향상

다양한 구현 방식

React Context API

React에서는 컴포넌트 트리 전체에 데이터를 전달하기 위해 Context를 사용합니다:

// Context 생성
const ThemeContext = React.createContext('light');
const UserContext = React.createContext(null);

// Provider로 값 제공
function App() {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('dark');

  return (
    <UserContext.Provider value={{user, setUser}}>
      <ThemeContext.Provider value={theme}>
        <Dashboard />
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

// Consumer에서 사용
function Dashboard() {
  const user = useContext(UserContext);
  const theme = useContext(ThemeContext);

  return (
    <div className={`dashboard ${theme}`}>
      Welcome, {user?.name}
    </div>
  );
}

Go의 context 패키지

Go에서는 요청 범위의 값, 취소 신호, 데드라인을 전달하기 위해 context를 사용합니다:

func processRequest(ctx context.Context, userID string) error {
    // 타임아웃 설정
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()
    
    // Context에 값 추가
    ctx = context.WithValue(ctx, "userID", userID)
    
    return fetchUserData(ctx)
}

func fetchUserData(ctx context.Context) error {
    select {
    case <-ctx.Done():
        return ctx.Err() // 취소 또는 타임아웃
    default:
        userID := ctx.Value("userID").(string)
        // 데이터 처리 로직
        return nil
    }
}

Java/Spring의 ApplicationContext

Spring에서는 Bean 컨테이너와 설정 정보를 관리하기 위해 Context를 사용합니다:

@Component
public class UserService {
    @Autowired
    private ApplicationContext context;
    
    public void processUser(String userId) {
        // Context에서 Bean 조회
        EmailService emailService = context.getBean(EmailService.class);
        
        // 환경 설정 접근
        Environment env = context.getEnvironment();
        String apiUrl = env.getProperty("api.url");
        
        emailService.sendNotification(userId, apiUrl);
    }
}

실용적 구현 패턴

1. Request Context 패턴

웹 애플리케이션에서 요청별 데이터를 관리하는 패턴:

interface RequestContext {
  userId: string;
  requestId: string;
  timestamp: Date;
  permissions: string[];
}

class RequestHandler {
  constructor(private context: RequestContext) {}
  
  async handleUserUpdate(userData: any) {
    if (!this.context.permissions.includes('user:update')) {
      throw new Error('Insufficient permissions');
    }
    
    const auditLog = {
      userId: this.context.userId,
      requestId: this.context.requestId,
      action: 'user_update',
      timestamp: this.context.timestamp
    };
    
    // 업데이트 로직 및 감사 로그
    await this.updateUser(userData, auditLog);
  }
}

2. Configuration Context 패턴

애플리케이션 설정을 관리하는 패턴:

class ConfigContext:
    def __init__(self):
        self.database_url = os.getenv('DATABASE_URL')
        self.api_key = os.getenv('API_KEY')
        self.debug_mode = os.getenv('DEBUG', 'false').lower() == 'true'
        self.feature_flags = self._load_feature_flags()
    
    def _load_feature_flags(self):
        return {
            'new_ui': True,
            'beta_features': False
        }

class UserController:
    def __init__(self, context: ConfigContext):
        self.context = context
    
    def get_user_profile(self, user_id):
        if self.context.feature_flags['new_ui']:
            return self.get_enhanced_profile(user_id)
        return self.get_basic_profile(user_id)

장점과 활용 시나리오

주요 장점:

  • Prop Drilling 방지: 깊은 컴포넌트 계층에서 데이터 전달이 용이
  • 전역 상태 관리: 애플리케이션 전반에서 공유되는 데이터 관리
  • 의존성 역전: 구체적인 구현보다 추상화된 인터페이스에 의존
  • 테스트 용이성: Mock 객체를 통한 단위 테스트 작성이 쉬움
  • 관심사 분리: 비즈니스 로직과 환경 설정의 분리

적합한 시나리오:

  • 사용자 인증 정보 관리
  • 테마/언어 설정 전파
  • 로깅 및 모니터링 컨텍스트
  • 데이터베이스 연결 관리
  • 요청별 트랜잭션 관리

주의사항과 모범 사례

성능 고려사항:

  • Context 값이 변경될 때마다 모든 consumer가 리렌더링될 수 있음
  • 너무 많은 데이터를 하나의 Context에 담으면 불필요한 렌더링 발생
  • Context를 적절히 분리하여 사용하는 것이 중요

설계 원칙:

  • Context는 진정으로 "전역적"인 데이터에만 사용
  • 컴포넌트 간의 간단한 데이터 전달에는 props 사용 권장
  • Context의 값은 불변성을 유지하도록 설계
  • Provider와 Consumer의 명확한 책임 분리

Context 패턴은 현대 프로그래밍에서 복잡한 애플리케이션의 상태와 의존성을 효율적으로 관리하는 핵심적인 패턴입니다. 적절히 사용하면 코드의 가독성과 유지보수성을 크게 향상시킬 수 있습니다.

Message 3 from User

확장 api를 제공하는 모듈을 만드는데 어떻게 활용되는지 알려줘

Message 4 from QueryPie

확장 API를 제공하는 모듈에서 Context 패턴은 플러그인이나 확장 기능이 안전하고 일관된 방식으로 호스트 애플리케이션과 상호작용할 수 있도록 하는 핵심 메커니즘입니다. Context 객체는 확장 모듈에게 필요한 권한, 리소스, 서비스에 대한 제어된 접근을 제공합니다.

확장 API에서의 Context 역할

Context는 확장 모듈과 호스트 시스템 간의 통신 브리지 역할을 하며, 다음과 같은 핵심 기능을 제공합니다:

보안 경계: 확장이 접근할 수 있는 기능을 제한 리소스 관리: 메모리, 파일, 네트워크 등의 리소스 접근 제어 생명주기 관리: 확장의 초기화, 실행, 종료 관리 상태 격리: 확장 간 상태 간섭 방지

실제 구현 예시

1. 브라우저 확장 시스템

Chrome Extension과 유사한 구조에서 Context 활용:

interface ExtensionContext {
  // 권한 관리
  permissions: Permission[];
  
  // 호스트 API 접근
  tabs: TabsAPI;
  storage: StorageAPI;
  notifications: NotificationsAPI;
  
  // 확장 메타데이터
  extensionId: string;
  version: string;
  
  // 생명주기 관리
  onInstalled: Event;
  onUninstalled: Event;
}

class ExtensionManager {
  private contexts = new Map<string, ExtensionContext>();
  
  loadExtension(extensionId: string, manifest: ExtensionManifest) {
    const context = this.createContext(extensionId, manifest);
    this.contexts.set(extensionId, context);
    
    // 확장에 Context 제공
    const extension = new Extension(context);
    extension.initialize();
  }
  
  private createContext(id: string, manifest: ExtensionManifest): ExtensionContext {
    return {
      extensionId: id,
      version: manifest.version,
      permissions: this.validatePermissions(manifest.permissions),
      tabs: new RestrictedTabsAPI(manifest.permissions),
      storage: new ScopedStorageAPI(id),
      notifications: new NotificationsAPI(),
      onInstalled: new Event(),
      onUninstalled: new Event()
    };
  }
}

// 확장 개발자가 사용하는 방식
class MyExtension {
  constructor(private context: ExtensionContext) {}
  
  async initialize() {
    // Context를 통해 호스트 API 사용
    const tabs = await this.context.tabs.query({active: true});
    
    this.context.notifications.create({
      title: 'Extension Loaded',
      message: `Version ${this.context.version} is ready`
    });
  }
}

2. 플러그인 시스템

IDE나 텍스트 에디터의 플러그인 시스템:

interface PluginContext {
  // 에디터 API
  editor: EditorAPI;
  workspace: WorkspaceAPI;
  ui: UIAPI;
  
  // 플러그인 정보
  pluginPath: string;
  config: PluginConfig;
  
  // 이벤트 시스템
  events: EventEmitter;
  
  // 리소스 관리
  subscriptions: Disposable[];
}

class PluginHost {
  async loadPlugin(pluginPath: string): Promise<void> {
    const context = await this.createPluginContext(pluginPath);
    
    // 플러그인 모듈 로드
    const pluginModule = await import(pluginPath);
    const plugin = new pluginModule.default();
    
    // Context 주입으로 플러그인 활성화
    await plugin.activate(context);
  }
  
  private async createPluginContext(pluginPath: string): Promise<PluginContext> {
    const config = await this.loadPluginConfig(pluginPath);
    
    return {
      pluginPath,
      config,
      editor: new RestrictedEditorAPI(config.permissions),
      workspace: new WorkspaceAPI(),
      ui: new UIAPI(),
      events: new EventEmitter(),
      subscriptions: []
    };
  }
}

// 플러그인 개발자 코드
export class MyPlugin {
  async activate(context: PluginContext) {
    // 명령어 등록
    const disposable = context.editor.registerCommand('myPlugin.doSomething', () => {
      context.ui.showMessage('Hello from plugin!');
    });
    
    // 정리를 위한 구독 등록
    context.subscriptions.push(disposable);
    
    // 설정 변경 감지
    context.events.on('configChanged', (newConfig) => {
      this.updateSettings(newConfig);
    });
  }
}

3. 마이크로서비스 확장 시스템

서버 사이드 확장 모듈을 위한 Context:

type ExtensionContext struct {
    Logger     logger.Interface
    Database   db.Interface
    Cache      cache.Interface
    HTTP       http.Client
    Config     map[string]interface{}
    ExtensionID string
    Metrics    metrics.Interface
}

type Extension interface {
    Initialize(ctx ExtensionContext) error
    HandleRequest(ctx context.Context, req *Request) (*Response, error)
    Shutdown() error
}

type ExtensionManager struct {
    extensions map[string]Extension
    contexts   map[string]ExtensionContext
}

func (em *ExtensionManager) LoadExtension(id string, extension Extension) error {
    // 격리된 Context 생성
    extCtx := ExtensionContext{
        ExtensionID: id,
        Logger:      em.createScopedLogger(id),
        Database:    em.createRestrictedDB(id),
        Cache:       em.createScopedCache(id),
        HTTP:        em.createRateLimitedHTTP(id),
        Config:      em.loadExtensionConfig(id),
        Metrics:     em.createScopedMetrics(id),
    }
    
    // 확장 초기화
    if err := extension.Initialize(extCtx); err != nil {
        return fmt.Errorf("failed to initialize extension %s: %w", id, err)
    }
    
    em.extensions[id] = extension
    em.contexts[id] = extCtx
    
    return nil
}

// 확장 개발자가 구현하는 코드
type MyExtension struct {
    ctx ExtensionContext
}

func (e *MyExtension) Initialize(ctx ExtensionContext) error {
    e.ctx = ctx
    
    ctx.Logger.Info("Extension initialized", "version", ctx.Config["version"])
    
    // 데이터베이스 스키마 마이그레이션
    return e.ctx.Database.Migrate("my_extension_tables")
}

func (e *MyExtension) HandleRequest(ctx context.Context, req *Request) (*Response, error) {
    // Context를 통한 안전한 리소스 접근
    start := time.Now()
    defer func() {
        e.ctx.Metrics.RecordDuration("request_duration", time.Since(start))
    }()
    
    // 캐시 확인
    if cached := e.ctx.Cache.Get(req.Key); cached != nil {
        return cached.(*Response), nil
    }
    
    // 처리 로직
    response := e.processRequest(req)
    
    // 결과 캐싱
    e.ctx.Cache.Set(req.Key, response, 5*time.Minute)
    
    return response, nil
}

고급 Context 패턴

1. 계층적 권한 관리

interface Permission {
  resource: string;
  actions: string[];
  conditions?: Record<string, any>;
}

class PermissionManager {
  checkPermission(context: ExtensionContext, resource: string, action: string): boolean {
    return context.permissions.some(p => 
      p.resource === resource && 
      p.actions.includes(action) &&
      this.evaluateConditions(p.conditions, context)
    );
  }
  
  private evaluateConditions(conditions: Record<string, any>, context: ExtensionContext): boolean {
    // 동적 권한 평가 로직
    return true;
  }
}

// 권한 검사를 포함한 API 래퍼
class SecureAPI {
  constructor(
    private context: ExtensionContext,
    private permissionManager: PermissionManager
  ) {}
  
  async readFile(path: string): Promise<string> {
    if (!this.permissionManager.checkPermission(this.context, 'filesystem', 'read')) {
      throw new Error('Permission denied: filesystem read');
    }
    
    return fs.readFileSync(path, 'utf8');
  }
}

2. 리소스 추적 및 정리

class ResourceTracker {
  private resources = new Map<string, Set<Disposable>>();
  
  track(extensionId: string, resource: Disposable): void {
    if (!this.resources.has(extensionId)) {
      this.resources.set(extensionId, new Set());
    }
    this.resources.get(extensionId)!.add(resource);
  }
  
  async cleanup(extensionId: string): Promise<void> {
    const extensionResources = this.resources.get(extensionId);
    if (!extensionResources) return;
    
    for (const resource of extensionResources) {
      try {
        await resource.dispose();
      } catch (error) {
        console.error(`Failed to dispose resource for ${extensionId}:`, error);
      }
    }
    
    this.resources.delete(extensionId);
  }
}

3. 이벤트 기반 통신

interface ExtensionEvent {
  type: string;
  data: any;
  source: string;
  timestamp: number;
}

class ExtensionEventBus {
  private subscribers = new Map<string, Map<string, EventHandler[]>>();
  
  subscribe(extensionId: string, eventType: string, handler: EventHandler): void {
    if (!this.subscribers.has(extensionId)) {
      this.subscribers.set(extensionId, new Map());
    }
    
    const extSubscriptions = this.subscribers.get(extensionId)!;
    if (!extSubscriptions.has(eventType)) {
      extSubscriptions.set(eventType, []);
    }
    
    extSubscriptions.get(eventType)!.push(handler);
  }
  
  publish(event: ExtensionEvent): void {
    for (const [extensionId, subscriptions] of this.subscribers) {
      const handlers = subscriptions.get(event.type);
      if (handlers) {
        handlers.forEach(handler => {
          try {
            handler(event);
          } catch (error) {
            console.error(`Event handler error in ${extensionId}:`, error);
          }
        });
      }
    }
  }
}

모범 사례

보안 설계:

  • 최소 권한 원칙 적용
  • 권한 검사를 API 레이어에서 수행
  • 확장 간 데이터 격리 보장

성능 최적화:

  • 지연 로딩을 통한 리소스 절약
  • 리소스 풀링으로 재사용성 증대
  • 메모리 누수 방지를 위한 자동 정리

개발자 경험:

  • 명확하고 일관된 API 제공
  • 풍부한 문서화와 예제
  • 디버깅 도구와 로깅 지원

확장 API에서 Context 패턴은 안전하고 확장 가능한 플러그인 생태계를 구축하는 데 필수적인 도구입니다. 적절히 설계된 Context는 확장 개발자에게 강력한 기능을 제공하면서도 호스트 시스템의 안정성과 보안을 보장합니다.

Message 5 from User

일반적인 객체지향 원칙에 따라 class를 설계한다고 할때도 유용할까

Message 6 from QueryPie

네, 일반적인 객체지향 설계에서도 Context 패턴은 매우 유용합니다. 사실 많은 객체지향 원칙들과 자연스럽게 어우러지며, 코드의 품질을 크게 향상시킬 수 있습니다.

객체지향 원칙과의 조화

단일 책임 원칙 (SRP) 지원

Context를 사용하면 클래스가 비즈니스 로직에만 집중할 수 있고, 의존성 관리나 환경 설정은 Context가 담당하게 됩니다:

// Context 없이 - SRP 위반
class OrderService {
    private DatabaseConnection db;
    private EmailService emailService;
    private Logger logger;
    private ConfigurationManager config;
    
    public OrderService() {
        // 의존성 초기화 코드들...
        this.db = DatabaseConnectionFactory.create();
        this.emailService = new EmailService(config.getSmtpSettings());
        this.logger = LoggerFactory.getLogger(OrderService.class);
        this.config = ConfigurationManager.getInstance();
    }
    
    public void processOrder(Order order) {
        // 비즈니스 로직과 인프라 관심사가 섞임
        logger.info("Processing order: " + order.getId());
        
        if (!config.isEmailEnabled()) return;
        
        // 실제 비즈니스 로직
        db.save(order);
        emailService.sendConfirmation(order.getCustomerEmail());
    }
}

// Context 사용 - SRP 준수
class ApplicationContext {
    private final DatabaseConnection database;
    private final EmailService emailService;
    private final Logger logger;
    private final Configuration config;
    
    // 의존성 관리 책임
    public ApplicationContext(Configuration config) {
        this.config = config;
        this.database = new DatabaseConnection(config.getDatabaseUrl());
        this.emailService = new EmailService(config.getEmailConfig());
        this.logger = LoggerFactory.getLogger("application");
    }
    
    // Getters...
}

class OrderService {
    private final ApplicationContext context;
    
    public OrderService(ApplicationContext context) {
        this.context = context;
    }
    
    public void processOrder(Order order) {
        // 순수한 비즈니스 로직에만 집중
        context.getLogger().info("Processing order: " + order.getId());
        
        context.getDatabase().save(order);
        
        if (context.getConfig().isEmailEnabled()) {
            context.getEmailService().sendConfirmation(order.getCustomerEmail());
        }
    }
}

의존성 역전 원칙 (DIP) 강화

Context를 통해 구체적인 구현이 아닌 추상화에 의존하게 만들 수 있습니다:

// 추상화된 Context 인터페이스
interface ServiceContext {
  getRepository<T>(type: new() => T): Repository<T>;
  getLogger(): Logger;
  getEventBus(): EventBus;
}

// 구체적인 Context 구현
class ProductionContext implements ServiceContext {
  private repositories = new Map();
  
  getRepository<T>(type: new() => T): Repository<T> {
    if (!this.repositories.has(type)) {
      this.repositories.set(type, new DatabaseRepository<T>());
    }
    return this.repositories.get(type);
  }
  
  getLogger(): Logger {
    return new ProductionLogger();
  }
  
  getEventBus(): EventBus {
    return new RedisEventBus();
  }
}

class TestContext implements ServiceContext {
  getRepository<T>(type: new() => T): Repository<T> {
    return new InMemoryRepository<T>();
  }
  
  getLogger(): Logger {
    return new MockLogger();
  }
  
  getEventBus(): EventBus {
    return new InMemoryEventBus();
  }
}

// 비즈니스 로직은 추상화에만 의존
class UserService {
  constructor(private context: ServiceContext) {}
  
  async createUser(userData: UserData): Promise<User> {
    const userRepo = this.context.getRepository(User);
    const logger = this.context.getLogger();
    const eventBus = this.context.getEventBus();
    
    const user = new User(userData);
    await userRepo.save(user);
    
    logger.info(`User created: ${user.id}`);
    eventBus.publish('user.created', user);
    
    return user;
  }
}

개방-폐쇄 원칙 (OCP) 지원

Context를 통해 기능 확장이 쉬워집니다:

class ProcessingContext:
    def __init__(self):
        self.validators = []
        self.processors = []
        self.notifiers = []
    
    def add_validator(self, validator):
        self.validators.append(validator)
        return self
    
    def add_processor(self, processor):
        self.processors.append(processor)
        return self
    
    def add_notifier(self, notifier):
        self.notifiers.append(notifier)
        return self

class DataProcessor:
    def __init__(self, context: ProcessingContext):
        self.context = context
    
    def process(self, data):
        # 검증 단계
        for validator in self.context.validators:
            if not validator.validate(data):
                raise ValueError(f"Validation failed: {validator.__class__.__name__}")
        
        # 처리 단계
        result = data
        for processor in self.context.processors:
            result = processor.process(result)
        
        # 알림 단계
        for notifier in self.context.notifiers:
            notifier.notify(result)
        
        return result

# 사용 - 새로운 기능을 추가할 때 기존 코드 변경 없음
context = (ProcessingContext()
    .add_validator(EmailValidator())
    .add_validator(LengthValidator())
    .add_processor(DataCleaner())
    .add_processor(DataEnricher())
    .add_notifier(EmailNotifier())
    .add_notifier(SlackNotifier()))

processor = DataProcessor(context)

실용적인 활용 패턴

1. 계층별 Context 분리

// 도메인 레이어 Context
public interface IDomainContext
{
    IEventPublisher EventPublisher { get; }
    IDateTimeProvider DateTimeProvider { get; }
    IUserProvider UserProvider { get; }
}

// 애플리케이션 레이어 Context
public interface IApplicationContext : IDomainContext
{
    IUnitOfWork UnitOfWork { get; }
    IValidator Validator { get; }
    IMapper Mapper { get; }
}

// 인프라 레이어 Context
public interface IInfrastructureContext : IApplicationContext
{
    IDatabase Database { get; }
    IFileSystem FileSystem { get; }
    IHttpClient HttpClient { get; }
}

public class OrderDomainService
{
    private readonly IDomainContext context;
    
    public OrderDomainService(IDomainContext context)
    {
        this.context = context;
    }
    
    public Order CreateOrder(CreateOrderRequest request)
    {
        var order = new Order
        {
            Id = Guid.NewGuid(),
            CreatedAt = context.DateTimeProvider.UtcNow,
            CreatedBy = context.UserProvider.CurrentUserId,
            Items = request.Items
        };
        
        // 도메인 이벤트 발행
        context.EventPublisher.Publish(new OrderCreatedEvent(order));
        
        return order;
    }
}

2. Builder 패턴과 결합

public class ServiceContextBuilder {
    private Logger logger;
    private Database database;
    private CacheManager cache;
    private ConfigurationProvider config;
    
    public ServiceContextBuilder withLogger(Logger logger) {
        this.logger = logger;
        return this;
    }
    
    public ServiceContextBuilder withDatabase(String connectionString) {
        this.database = new Database(connectionString);
        return this;
    }
    
    public ServiceContextBuilder withCache(CacheConfig config) {
        this.cache = new CacheManager(config);
        return this;
    }
    
    public ServiceContextBuilder withConfiguration(ConfigurationProvider config) {
        this.config = config;
        return this;
    }
    
    public ServiceContext build() {
        // 기본값 설정
        if (logger == null) logger = new ConsoleLogger();
        if (database == null) throw new IllegalStateException("Database is required");
        
        return new ServiceContext(logger, database, cache, config);
    }
}

// 사용법
ServiceContext context = new ServiceContextBuilder()
    .withLogger(new FileLogger("/var/log/app.log"))
    .withDatabase("jdbc:postgresql://localhost/mydb")
    .withCache(new CacheConfig().withTtl(Duration.ofMinutes(30)))
    .withConfiguration(new PropertyFileProvider("app.properties"))
    .build();

3. 상태 기반 Context

enum ProcessingState {
  INITIALIZING = 'initializing',
  PROCESSING = 'processing',
  COMPLETING = 'completing',
  COMPLETED = 'completed',
  ERROR = 'error'
}

class StatefulContext {
  private state: ProcessingState = ProcessingState.INITIALIZING;
  private readonly stateHandlers = new Map<ProcessingState, StateHandler>();
  
  constructor(
    private logger: Logger,
    private metrics: MetricsCollector
  ) {
    this.setupStateHandlers();
  }
  
  private setupStateHandlers() {
    this.stateHandlers.set(ProcessingState.INITIALIZING, new InitializingHandler());
    this.stateHandlers.set(ProcessingState.PROCESSING, new ProcessingHandler());
    this.stateHandlers.set(ProcessingState.COMPLETING, new CompletingHandler());
    // ... 다른 핸들러들
  }
  
  transition(newState: ProcessingState): void {
    const oldState = this.state;
    this.state = newState;
    
    this.logger.info(`State transition: ${oldState} -> ${newState}`);
    this.metrics.incrementCounter('state_transitions', { from: oldState, to: newState });
    
    // 상태별 핸들러 실행
    const handler = this.stateHandlers.get(newState);
    if (handler) {
      handler.onEnter(this);
    }
  }
  
  getCurrentState(): ProcessingState {
    return this.state;
  }
  
  canExecute(operation: string): boolean {
    const handler = this.stateHandlers.get(this.state);
    return handler ? handler.canExecute(operation) : false;
  }
}

class TaskProcessor {
  constructor(private context: StatefulContext) {}
  
  async processTask(task: Task): Promise<void> {
    if (!this.context.canExecute('process')) {
      throw new Error(`Cannot process task in state: ${this.context.getCurrentState()}`);
    }
    
    this.context.transition(ProcessingState.PROCESSING);
    
    try {
      // 작업 처리 로직
      await this.doProcessTask(task);
      
      this.context.transition(ProcessingState.COMPLETING);
      // 완료 처리 로직
      
      this.context.transition(ProcessingState.COMPLETED);
    } catch (error) {
      this.context.transition(ProcessingState.ERROR);
      throw error;
    }
  }
}

테스트 용이성 향상

Context 패턴은 단위 테스트를 매우 쉽게 만들어줍니다:

public class UserServiceTest {
    private TestContext context;
    private UserService userService;
    
    @BeforeEach
    void setUp() {
        context = new TestContext();
        userService = new UserService(context);
    }
    
    @Test
    void shouldCreateUserSuccessfully() {
        // Given
        UserData userData = new UserData("john@example.com", "John Doe");
        
        // When
        User result = userService.createUser(userData);
        
        // Then
        assertThat(result.getEmail()).isEqualTo("john@example.com");
        assertThat(context.getRepository(User.class).findAll()).hasSize(1);
        assertThat(context.getEventBus().getPublishedEvents()).hasSize(1);
        assertThat(context.getLogger().getLogEntries())
            .anyMatch(entry -> entry.contains("User created"));
    }
    
    @Test
    void shouldHandleValidationError() {
        // Given
        context.addValidator(new EmailValidator() {
            @Override
            public boolean validate(Object data) {
                return false; // 의도적으로 실패
            }
        });
        
        UserData invalidData = new UserData("invalid-email", "John");
        
        // When & Then
        assertThatThrownBy(() -> userService.createUser(invalidData))
            .isInstanceOf(ValidationException.class);
        
        assertThat(context.getRepository(User.class).findAll()).isEmpty();
    }
}

주의사항과 모범 사례

설계 원칙:

  • Context는 상태가 아닌 서비스와 설정을 담아야 함
  • 너무 많은 책임을 한 Context에 몰아주지 말 것
  • 인터페이스를 통한 추상화로 테스트 가능성 확보

성능 고려사항:

  • Context 생성 비용을 고려하여 적절한 생명주기 관리
  • 싱글톤 패턴과 결합할 때는 스레드 안전성 보장
  • 무거운 리소스는 지연 초기화 활용

유지보수성:

  • Context의 의존성 그래프가 복잡해지지 않도록 주의
  • 명시적인 의존성 주입으로 숨겨진 의존성 방지
  • 적절한 문서화와 예제 제공

Context 패턴은 객체지향 설계에서 의존성 관리, 테스트 용이성, 코드 재사용성을 크게 향상시키는 강력한 도구입니다. 특히 복잡한 비즈니스 로직을 가진 애플리케이션에서 그 진가를 발휘합니다.

Message 7 from User

context에 상태를 관리할때의 이점도 있지않아?

Message 8 from QueryPie

맞습니다! Context에서 상태를 관리하는 것은 많은 중요한 이점들이 있습니다. 실제로 현대 프로그래밍에서 Context 패턴이 널리 사용되는 핵심 이유 중 하나입니다.

Context 상태 관리의 주요 이점

1. 전역 상태의 안전한 대안

전역 변수의 문제점을 해결하면서도 필요한 상태 공유를 가능하게 합니다:

// 전역 변수 사용 - 문제가 많음
let currentUser: User | null = null;
let theme: 'light' | 'dark' = 'light';
let language: string = 'en';

// Context를 통한 상태 관리 - 안전하고 제어 가능
interface AppState {
  currentUser: User | null;
  theme: 'light' | 'dark';
  language: string;
  notifications: Notification[];
}

class AppContext {
  private state: AppState = {
    currentUser: null,
    theme: 'light',
    language: 'en',
    notifications: []
  };
  
  private listeners = new Set<StateListener>();
  
  // 상태 조회 (읽기 전용)
  getState(): Readonly<AppState> {
    return { ...this.state };
  }
  
  // 상태 변경 (제어된 방식)
  updateState(updater: (state: AppState) => Partial<AppState>): void {
    const newState = { ...this.state, ...updater(this.state) };
    const oldState = this.state;
    this.state = newState;
    
    // 변경 알림
    this.notifyListeners(oldState, newState);
  }
  
  // 상태 변경 구독
  subscribe(listener: StateListener): () => void {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  }
}

2. 계층적 상태 전파

React의 Context처럼 컴포넌트 트리나 모듈 계층에서 상태를 효율적으로 전파할 수 있습니다:

interface TransactionContext {
  transactionId: string;
  startTime: Date;
  userId: string;
  metadata: Record<string, any>;
  readonly isActive: boolean;
}

class TransactionManager {
  private activeTransactions = new Map<string, TransactionContext>();
  
  beginTransaction(userId: string, metadata: Record<string, any> = {}): TransactionContext {
    const context: TransactionContext = {
      transactionId: this.generateId(),
      startTime: new Date(),
      userId,
      metadata,
      isActive: true
    };
    
    this.activeTransactions.set(context.transactionId, context);
    return context;
  }
  
  endTransaction(transactionId: string): void {
    const context = this.activeTransactions.get(transactionId);
    if (context) {
      // 불변성을 유지하면서 상태 업데이트
      Object.defineProperty(context, 'isActive', { value: false });
      this.activeTransactions.delete(transactionId);
    }
  }
}

// 하위 서비스에서 트랜잭션 정보 활용
class OrderService {
  constructor(private context: TransactionContext) {}
  
  createOrder(orderData: OrderData): Order {
    if (!this.context.isActive) {
      throw new Error('Transaction is not active');
    }
    
    const order = new Order({
      ...orderData,
      transactionId: this.context.transactionId,
      createdBy: this.context.userId,
      createdAt: this.context.startTime
    });
    
    // 트랜잭션 메타데이터에 주문 정보 추가
    this.context.metadata.orderCount = (this.context.metadata.orderCount || 0) + 1;
    
    return order;
  }
}

3. 요청 범위 상태 관리

웹 애플리케이션에서 요청별 상태를 안전하게 관리할 수 있습니다:

from contextvars import ContextVar
from typing import Optional, Dict, Any
import uuid
from datetime import datetime

# Python의 contextvars를 활용한 요청 컨텍스트
class RequestContext:
    def __init__(self):
        self.request_id: str = str(uuid.uuid4())
        self.start_time: datetime = datetime.utcnow()
        self.user_id: Optional[str] = None
        self.session_data: Dict[str, Any] = {}
        self.request_data: Dict[str, Any] = {}
        self.response_data: Dict[str, Any] = {}
        self._trace_data: Dict[str, Any] = {}
    
    def add_trace(self, key: str, value: Any) -> None:
        self._trace_data[key] = value
    
    def get_trace(self, key: str) -> Any:
        return self._trace_data.get(key)
    
    def get_execution_time(self) -> float:
        return (datetime.utcnow() - self.start_time).total_seconds()

# 전역 컨텍스트 변수
current_request_context: ContextVar[RequestContext] = ContextVar('current_request_context')

class RequestContextMiddleware:
    def __init__(self, app):
        self.app = app
    
    async def __call__(self, scope, receive, send):
        # 요청마다 새로운 컨텍스트 생성
        context = RequestContext()
        
        # 사용자 정보 추출 및 설정
        if 'user' in scope:
            context.user_id = scope['user'].get('id')
        
        # 컨텍스트 설정
        token = current_request_context.set(context)
        
        try:
            await self.app(scope, receive, send)
        finally:
            # 정리
            current_request_context.reset(token)

# 어플리케이션 코드에서 컨텍스트 사용
class UserService:
    def get_user_profile(self, user_id: str):
        context = current_request_context.get()
        
        # 추적 정보 추가
        context.add_trace('service_called', 'UserService.get_user_profile')
        context.add_trace('target_user_id', user_id)
        
        # 권한 확인
        if context.user_id != user_id and not self.is_admin(context.user_id):
            raise PermissionError("Access denied")
        
        # 비즈니스 로직...
        profile = self.fetch_profile(user_id)
        
        # 응답 데이터에 추가
        context.response_data['profile_fetched'] = True
        
        return profile

4. 상태 기반 워크플로우 관리

복잡한 비즈니스 프로세스에서 상태를 체계적으로 관리할 수 있습니다:

public enum OrderState {
    CREATED, VALIDATED, PAYMENT_PENDING, PAID, SHIPPED, DELIVERED, CANCELLED
}

public class OrderContext {
    private OrderState currentState;
    private final Order order;
    private final Map<String, Object> stateData;
    private final List<StateTransition> history;
    private final StateValidator validator;
    
    public OrderContext(Order order) {
        this.order = order;
        this.currentState = OrderState.CREATED;
        this.stateData = new HashMap<>();
        this.history = new ArrayList<>();
        this.validator = new StateValidator();
    }
    
    public void transitionTo(OrderState newState, Map<String, Object> transitionData) {
        // 상태 전환 검증
        if (!validator.canTransition(currentState, newState)) {
            throw new IllegalStateException(
                String.format("Cannot transition from %s to %s", currentState, newState)
            );
        }
        
        OrderState previousState = currentState;
        
        // 상태 전환 실행
        executeStateTransition(previousState, newState, transitionData);
        
        // 히스토리 기록
        history.add(new StateTransition(
            previousState, newState, Instant.now(), transitionData
        ));
        
        // 상태 업데이트
        currentState = newState;
        
        // 상태별 후처리
        onStateEntered(newState, transitionData);
    }
    
    private void executeStateTransition(
        OrderState from, 
        OrderState to, 
        Map<String, Object> data
    ) {
        switch (to) {
            case VALIDATED:
                validateOrder(data);
                break;
            case PAYMENT_PENDING:
                initializePayment(data);
                break;
            case PAID:
                confirmPayment(data);
                break;
            case SHIPPED:
                createShipment(data);
                break;
            // ... 다른 상태들
        }
    }
    
    // 상태별 데이터 관리
    public void setStateData(String key, Object value) {
        stateData.put(currentState + "." + key, value);
    }
    
    public <T> T getStateData(String key, Class<T> type) {
        Object value = stateData.get(currentState + "." + key);
        return type.cast(value);
    }
    
    // 상태 기반 행동 제어
    public boolean canPerformAction(String action) {
        return validator.isActionAllowed(currentState, action);
    }
}

// 사용 예시
public class OrderService {
    public void processOrder(Long orderId, PaymentInfo paymentInfo) {
        Order order = orderRepository.findById(orderId);
        OrderContext context = new OrderContext(order);
        
        try {
            // 검증 단계
            context.transitionTo(OrderState.VALIDATED, Map.of("validator", "system"));
            
            // 결제 대기
            context.transitionTo(OrderState.PAYMENT_PENDING, Map.of(
                "paymentMethod", paymentInfo.getMethod(),
                "amount", order.getTotalAmount()
            ));
            
            // 결제 처리
            PaymentResult result = paymentService.processPayment(paymentInfo);
            context.transitionTo(OrderState.PAID, Map.of(
                "paymentId", result.getPaymentId(),
                "transactionId", result.getTransactionId()
            ));
            
        } catch (StateTransitionException e) {
            // 상태 전환 실패 시 보상 트랜잭션
            context.transitionTo(OrderState.CANCELLED, Map.of(
                "reason", e.getMessage(),
                "cancelledAt", Instant.now()
            ));
            throw new OrderProcessingException("Order processing failed", e);
        }
    }
}

5. 이벤트 기반 상태 동기화

여러 컴포넌트 간 상태 동기화를 이벤트를 통해 자동으로 처리할 수 있습니다:

interface GameState {
  players: Player[];
  currentTurn: number;
  gamePhase: 'waiting' | 'playing' | 'ended';
  score: Record<string, number>;
  gameData: any;
}

class GameContext extends EventEmitter {
  private state: GameState;
  
  constructor(initialState: GameState) {
    super();
    this.state = initialState;
  }
  
  updateState(updater: (state: GameState) => Partial<GameState>): void {
    const oldState = { ...this.state };
    const updates = updater(oldState);
    const newState = { ...oldState, ...updates };
    
    this.state = newState;
    
    // 변경된 필드별로 이벤트 발송
    for (const [key, value] of Object.entries(updates)) {
      this.emit(`state:${key}:changed`, value, oldState[key as keyof GameState]);
    }
    
    // 전체 상태 변경 이벤트
    this.emit('state:changed', newState, oldState);
  }
  
  getState(): Readonly<GameState> {
    return { ...this.state };
  }
  
  // 특정 상태 조건 대기
  waitForCondition(condition: (state: GameState) => boolean): Promise<GameState> {
    return new Promise((resolve) => {
      const checkCondition = (state: GameState) => {
        if (condition(state)) {
          this.off('state:changed', checkCondition);
          resolve(state);
        }
      };
      
      // 현재 상태가 이미 조건을 만족하는지 확인
      if (condition(this.state)) {
        resolve(this.state);
      } else {
        this.on('state:changed', checkCondition);
      }
    });
  }
}

// 게임 컴포넌트들이 상태 변화에 반응
class GameUI {
  constructor(private context: GameContext) {
    this.setupEventListeners();
  }
  
  private setupEventListeners(): void {
    this.context.on('state:currentTurn:changed', (newTurn, oldTurn) => {
      this.highlightCurrentPlayer(newTurn);
      this.showTurnTransition(oldTurn, newTurn);
    });
    
    this.context.on('state:gamePhase:changed', (newPhase) => {
      switch (newPhase) {
        case 'waiting':
          this.showWaitingScreen();
          break;
        case 'playing':
          this.showGameBoard();
          break;
        case 'ended':
          this.showGameResults();
          break;
      }
    });
    
    this.context.on('state:score:changed', (newScores) => {
      this.updateScoreboard(newScores);
    });
  }
}

class GameLogic {
  constructor(private context: GameContext) {
    this.setupGameRules();
  }
  
  private setupGameRules(): void {
    // 게임 종료 조건 감시
    this.context.on('state:score:changed', (scores) => {
      const maxScore = Math.max(...Object.values(scores));
      if (maxScore >= 100) {
        this.context.updateState(state => ({
          gamePhase: 'ended'
        }));
      }
    });
    
    // 턴 자동 진행
    this.context.on('state:currentTurn:changed', async (newTurn) => {
      await this.delay(1000); // 턴 간 딜레이
      
      const state = this.context.getState();
      if (state.gamePhase === 'playing') {
        const nextTurn = (newTurn + 1) % state.players.length;
        this.context.updateState(s => ({ currentTurn: nextTurn }));
      }
    });
  }
}

상태 관리의 고급 패턴

타임 트래블과 상태 히스토리

class StatefulContext<T> {
  private states: T[] = [];
  private currentIndex: number = -1;
  private maxHistorySize: number = 100;
  
  constructor(initialState: T) {
    this.pushState(initialState);
  }
  
  private pushState(state: T): void {
    // 현재 위치 이후의 상태들 제거 (새로운 분기 생성)
    this.states = this.states.slice(0, this.currentIndex + 1);
    
    this.states.push(state);
    this.currentIndex++;
    
    // 히스토리 크기 제한
    if (this.states.length > this.maxHistorySize) {
      this.states.shift();
      this.currentIndex--;
    }
  }
  
  updateState(updater: (state: T) => T): void {
    const currentState = this.getCurrentState();
    const newState = updater(currentState);
    this.pushState(newState);
  }
  
  undo(): T | null {
    if (this.currentIndex > 0) {
      this.currentIndex--;
      return this.getCurrentState();
    }
    return null;
  }
  
  redo(): T | null {
    if (this.currentIndex < this.states.length - 1) {
      this.currentIndex++;
      return this.getCurrentState();
    }
    return null;
  }
  
  getCurrentState(): T {
    return this.states[this.currentIndex];
  }
  
  canUndo(): boolean {
    return this.currentIndex > 0;
  }
  
  canRedo(): boolean {
    return this.currentIndex < this.states.length - 1;
  }
}

Context에서의 상태 관리는 단순한 데이터 보관을 넘어서서 애플리케이션의 복잡한 상태 흐름을 체계적으로 제어할 수 있게 해주는 강력한 패턴입니다. 특히 상태 변화가 복잡하고 여러 컴포넌트에 영향을 미치는 현대적인 애플리케이션에서는 필수적인 도구라고 할 수 있습니다.