Post

MCP/ToolSet 트랜잭션 기반 데이터 일관성 보장

Revoke, Connect/Disconnect, Cascade 작업에 MongoDB 트랜잭션을 적용하고, 정기적 동기화 배치 작업으로 데이터 불일치를 자동 복구하는 설계

MCP/ToolSet 트랜잭션 기반 데이터 일관성 보장

문제의 발견

MCP/ToolSet 관련 버그 리포트가 여러 건 들어왔다.

“MCP 권한을 취소했는데 Agent에서는 여전히 사용할 수 있어요.” “Agent 설정과 Session 설정이 다르게 보여요.”

로그를 분석해보니 부분 실패로 인한 데이터 불일치였다. 여러 컬렉션에 걸친 작업 중 일부만 성공하고 나머지가 실패하면서 데이터가 일관되지 않게 남아있었다.

문제 상황

영향받는 작업들

작업관련 컬렉션문제점
Revokepermissions, user_configurations, agent_configurations, session_configurations 등 8개일부 삭제 후 실패 시 불일치
Connect/Disconnectagent_configurations, agents, session_configurationsAgent와 Configuration 불일치
Cascadeagent_configurations, session_configurations, sub_agent_configurations부분 전파 실패

데이터 불일치 시나리오

sequenceDiagram
    participant Admin
    participant Service
    participant Collection1 as Permissions
    participant Collection2 as UserConfig
    participant Collection3 as AgentConfig

    Admin->>Service: RevokeMcpAccess(userID, mcpID)
    Service->>Collection1: Delete permission ✅
    Service->>Collection2: Remove from user config ✅
    Service->>Collection3: Remove from agent configs ❌ (실패!)

    Note over Admin,Collection3: 결과: permission은 삭제됨, user config도 정리됨<br/>하지만 agent config에는 MCP가 남아있음

해결책: 트랜잭션 + 배치 동기화

1. Revoke 작업 트랜잭션

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
func (s *permissionService) RevokeMcpAccess(ctx context.Context, userID, mcpID string) error {
    session, err := s.mongoDB.StartSession()
    if err != nil {
        return err
    }
    defer session.EndSession(ctx)

    _, err = session.WithTransaction(ctx, func(mongoCtx mongo.SessionContext) (any, error) {
        // 1. permissions 컬렉션에서 삭제
        if err := s.deletePermission(mongoCtx, userID, mcpID); err != nil {
            return nil, err
        }

        // 2. permission_requests 컬렉션에서 삭제
        if err := s.deletePermissionRequests(mongoCtx, userID, mcpID); err != nil {
            return nil, err
        }

        // 3. user_mcp_info 컬렉션에서 삭제
        if err := s.deleteUserMcpInfo(mongoCtx, userID, mcpID); err != nil {
            return nil, err
        }

        // 4. user_configurations 컬렉션에서 삭제
        if err := s.removeFromUserConfig(mongoCtx, userID, mcpID); err != nil {
            return nil, err
        }

        // 5. agents 컬렉션에서 삭제
        if err := s.removeFromAgents(mongoCtx, userID, mcpID); err != nil {
            return nil, err
        }

        // 6. agent_configurations 컬렉션에서 삭제
        if err := s.removeFromAgentConfigs(mongoCtx, userID, mcpID); err != nil {
            return nil, err
        }

        // 7. session_configurations 컬렉션에서 삭제
        if err := s.removeFromSessionConfigs(mongoCtx, userID, mcpID); err != nil {
            return nil, err
        }

        // 8. sub_agent_configurations 컬렉션에서 삭제
        if err := s.removeFromSubAgentConfigs(mongoCtx, userID, mcpID); err != nil {
            return nil, err
        }

        return nil, nil
    })

    return err
}

핵심: 8개 컬렉션에 걸친 작업을 하나의 트랜잭션으로 처리. 하나라도 실패하면 전체 롤백.

2. Connect/Disconnect 트랜잭션

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func (s *agentConfigService) ConnectMCPToAgent(ctx context.Context, agentID, mcpID string) error {
    session, err := s.mongoDB.StartSession()
    if err != nil {
        return err
    }
    defer session.EndSession(ctx)

    _, err = session.WithTransaction(ctx, func(mongoCtx mongo.SessionContext) (any, error) {
        // 1. Agent Configuration 업데이트
        if err := s.addMcpToAgentConfig(mongoCtx, agentID, mcpID); err != nil {
            return nil, err
        }

        // 2. Agent 모델의 mcp_server_ids 업데이트
        if err := s.addMcpToAgent(mongoCtx, agentID, mcpID); err != nil {
            return nil, err
        }

        return nil, nil
    })

    return err
}

3. Cascade 작업 트랜잭션

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
func (s *agentConfigService) CascadeAgentMcpConnectToSessions(ctx context.Context, agentID, mcpID string) error {
    session, err := s.mongoDB.StartSession()
    if err != nil {
        return err
    }
    defer session.EndSession(ctx)

    _, err = session.WithTransaction(ctx, func(mongoCtx mongo.SessionContext) (any, error) {
        // Agent의 모든 Session Configuration을 한 번에 업데이트
        sessions, err := s.getSessionsByAgentID(mongoCtx, agentID)
        if err != nil {
            return nil, err
        }

        for _, sess := range sessions {
            if err := s.addMcpToSessionConfig(mongoCtx, sess.ID, mcpID); err != nil {
                return nil, err  // 하나라도 실패하면 전체 롤백
            }
        }

        return nil, nil
    })

    return err
}

4. 정기적 동기화 배치 작업

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
type ConfigurationSyncService struct {
    mongoDB     *mongo.Client
    logger      *zap.Logger
    ticker      *time.Ticker
    stopChan    chan struct{}
}

func (s *ConfigurationSyncService) Start() {
    s.ticker = time.NewTicker(1 * time.Hour)
    go func() {
        for {
            select {
            case <-s.ticker.C:
                s.syncAllConfigurations()
            case <-s.stopChan:
                return
            }
        }
    }()
}

func (s *ConfigurationSyncService) syncAllConfigurations() {
    ctx := context.Background()

    // 1. Agent Configuration과 Session Configuration 동기화
    s.syncAgentSessionConfigs(ctx)

    // 2. Agent 모델과 Agent Configuration 동기화
    s.syncAgentModels(ctx)

    // 3. SubAgent Configuration과 Parent Agent Configuration 동기화
    s.syncSubAgentConfigs(ctx)
}

func (s *ConfigurationSyncService) syncAgentSessionConfigs(ctx context.Context) {
    // Agent Configuration의 MCP/ToolSet 목록과
    // Session Configuration의 MCP/ToolSet 목록 비교
    agentConfigs, _ := s.getAgentConfigurations(ctx)

    for _, agentConfig := range agentConfigs {
        sessions, _ := s.getSessionsByAgentID(ctx, agentConfig.AgentID)

        for _, session := range sessions {
            sessionConfig, _ := s.getSessionConfiguration(ctx, session.ID)

            // 불일치 감지 및 수정
            if !equalMcpLists(agentConfig.McpServers, sessionConfig.McpServers) {
                s.logger.Warnf("MCP mismatch detected: agent=%s, session=%s", agentConfig.AgentID, session.ID)
                s.fixSessionConfiguration(ctx, session.ID, agentConfig.McpServers)
            }
        }
    }
}

시퀀스 다이어그램

트랜잭션 기반 Revoke

sequenceDiagram
    participant Admin
    participant Service
    participant MongoDB
    participant Collections as 8개 컬렉션

    Admin->>Service: RevokeMcpAccess(userID, mcpID)
    Service->>MongoDB: StartSession()
    Service->>MongoDB: WithTransaction()

    loop 8개 컬렉션
        Service->>Collections: Delete/Update
        alt 실패
            Collections-->>Service: Error
            Service->>MongoDB: Rollback
            MongoDB-->>Service: All changes reverted
            Service-->>Admin: Error
        end
    end

    Service->>MongoDB: Commit
    MongoDB-->>Service: Success
    Service-->>Admin: Success

배치 동기화

sequenceDiagram
    participant Scheduler
    participant SyncService
    participant MongoDB

    loop 매 1시간
        Scheduler->>SyncService: syncAllConfigurations()

        SyncService->>MongoDB: Get all AgentConfigurations
        SyncService->>MongoDB: Get all SessionConfigurations

        loop 각 Agent
            SyncService->>SyncService: Compare MCP/ToolSet lists
            alt 불일치 감지
                SyncService->>MongoDB: Fix SessionConfiguration (트랜잭션)
                Note over SyncService: 로그 기록
            end
        end
    end

설계 결정

1. All-or-Nothing vs Best Effort

방식장점단점
All-or-Nothing (트랜잭션)데이터 일관성 보장일부 실패 시 전체 실패
Best Effort (기존)부분 성공 가능데이터 불일치 발생

결정: All-or-Nothing. 부분 실패로 인한 불일치가 더 큰 문제.

2. 배치 동기화 주기

1
2
3
configuration_sync:
  interval: 1h  # 1시간마다 실행
  enabled: true

이유:

  • 실시간 동기화는 성능 부담
  • 1시간 주기는 불일치 허용 시간과 성능 사이의 균형점
  • 트랜잭션 적용 후에는 새로운 불일치 발생 최소화

3. 트랜잭션 범위

1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌─────────────────────────────────────────────────────────────────┐
│                     트랜잭션 범위                                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ✅ 트랜잭션 적용                                               │
│  - Revoke 작업 (8개 컬렉션)                                     │
│  - Connect/Disconnect 작업 (2개 컬렉션)                         │
│  - Cascade 작업 (N개 Session Configuration)                     │
│                                                                 │
│  ❌ 트랜잭션 미적용                                             │
│  - 단일 컬렉션 읽기 작업                                        │
│  - 배치 동기화 (개별 수정은 트랜잭션)                           │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

결과

데이터 일관성 개선

지표BeforeAfter
Revoke 후 불일치발생없음
Connect/Disconnect 불일치발생없음
Cascade 부분 실패발생없음

로그 변화

수정 전:

1
2
3
4
[15:23:45] RevokeMcpAccess: permission deleted
[15:23:45] RevokeMcpAccess: user config updated
[15:23:46] RevokeMcpAccess: ERROR agent config update failed
(불일치 상태로 남음)

수정 후:

1
2
3
4
[15:23:45] RevokeMcpAccess: starting transaction
[15:23:46] RevokeMcpAccess: ERROR agent config update failed
[15:23:46] RevokeMcpAccess: transaction rolled back
(모든 변경사항 롤백됨)

배운 점

1. 여러 컬렉션에 걸친 작업은 트랜잭션 필수

1
2
3
4
5
6
7
8
9
10
11
12
// 안티패턴: 개별 업데이트
deleteFromCollection1(id)  // 성공
deleteFromCollection2(id)  // 실패! → 불일치
deleteFromCollection3(id)  // 실행 안 됨

// 올바른 패턴: 트랜잭션
session.WithTransaction(ctx, func(mongoCtx) {
    deleteFromCollection1(id)
    deleteFromCollection2(id)
    deleteFromCollection3(id)
    // 하나라도 실패하면 전체 롤백
})

2. 배치 동기화는 보험이다

트랜잭션을 적용해도 과거 데이터의 불일치는 해결되지 않는다. 배치 동기화로 기존 불일치를 자동 복구한다.

1
2
3
4
// 1시간마다 실행
func (s *ConfigurationSyncService) syncAllConfigurations() {
    // 불일치 감지 → 로그 기록 → 자동 수정
}

3. Warn 로그만 남기고 계속 진행하지 마라

1
2
3
4
5
6
7
8
9
10
// 안티패턴: 경고만 남기고 계속
if err := updateAgentConfig(ctx, agentID); err != nil {
    logger.Warnf("failed to update agent config: %v", err)
    // 계속 진행... 불일치 발생!
}

// 올바른 패턴: 트랜잭션 실패 시 롤백
if err := updateAgentConfig(mongoCtx, agentID); err != nil {
    return nil, err  // 트랜잭션 롤백
}

결론

MCP/ToolSet 데이터 일관성 보장의 핵심:

  1. 트랜잭션 적용: 여러 컬렉션에 걸친 작업은 All-or-Nothing
  2. 배치 동기화: 1시간마다 불일치 감지 및 자동 복구
  3. 명시적 에러 처리: Warn 로그만 남기는 대신 트랜잭션 롤백

데이터 일관성은 “일부 성공”보다 “전체 실패”가 낫다.

This post is licensed under CC BY 4.0 by the author.