Skip to content

Latest commit

ย 

History

History
515 lines (434 loc) ยท 15.3 KB

File metadata and controls

515 lines (434 loc) ยท 15.3 KB

๋ฉ”์‹œ์ง• ์‹œ์Šคํ…œ ์„ค๊ณ„ (WhatsApp/Slack ์œ ์‚ฌ ์‹œ์Šคํ…œ)

๋ฉด์ ‘๊ด€: "์‹ค์‹œ๊ฐ„ ๋ฉ”์‹œ์ง• ์‹œ์Šคํ…œ์„ ์„ค๊ณ„ํ•ด์ฃผ์„ธ์š”. ๋‹ค์Œ ๊ธฐ๋Šฅ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค:

  1. 1:1 ์ฑ„ํŒ…
  2. ๊ทธ๋ฃน ์ฑ„ํŒ…
  3. ์˜จ๋ผ์ธ ์ƒํƒœ ํ‘œ์‹œ
  4. ๋ฉ”์‹œ์ง€ ์ €์žฅ ๋ฐ ๋™๊ธฐํ™”
  5. ํ‘ธ์‹œ ์•Œ๋ฆผ"

์ง€์›์ž: ๋จผ์ € ๋ช‡ ๊ฐ€์ง€ ์š”๊ตฌ์‚ฌํ•ญ์„ ํ™•์ธํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.

  1. ์˜ˆ์ƒ ์‚ฌ์šฉ์ž ์ˆ˜์™€ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ๋Ÿ‰์€ ์–ด๋Š ์ •๋„์ธ๊ฐ€์š”?
  2. ๋ฉ”์‹œ์ง€ ์ €์žฅ ๊ธฐ๊ฐ„๊ณผ ํฌ๊ธฐ ์ œํ•œ์€ ์žˆ๋‚˜์š”?
  3. ์–ด๋–ค ์ข…๋ฅ˜์˜ ๋ฉ”์‹œ์ง€๋ฅผ ์ง€์›ํ•ด์•ผ ํ•˜๋‚˜์š”? (ํ…์ŠคํŠธ, ์ด๋ฏธ์ง€, ํŒŒ์ผ ๋“ฑ)
  4. ๋ฐฐ๋‹ฌ ํ™•์ธ์ด๋‚˜ ์ฝ์Œ ํ™•์ธ ๊ธฐ๋Šฅ์ด ํ•„์š”ํ•œ๊ฐ€์š”?

๋ฉด์ ‘๊ด€:

  1. DAU 100๋งŒ ๋ช…, ์ดˆ๋‹น ํ‰๊ท  10,000๊ฑด์˜ ๋ฉ”์‹œ์ง€
  2. ๋ฉ”์‹œ์ง€๋Š” ์˜๊ตฌ ๋ณด๊ด€, ํŒŒ์ผ์€ 30์ผ, ํฌ๊ธฐ๋Š” ๋ฉ”์‹œ์ง€๋‹น ์ตœ๋Œ€ 100KB
  3. ํ…์ŠคํŠธ, ์ด๋ฏธ์ง€, ํŒŒ์ผ ๋ชจ๋‘ ์ง€์› ํ•„์š”
  4. ๋ฉ”์‹œ์ง€ ๋ฐฐ๋‹ฌ ํ™•์ธ๊ณผ ์ฝ์Œ ํ™•์ธ ๋ชจ๋‘ ํ•„์š”

์ง€์›์ž: ์•Œ๊ฒ ์Šต๋‹ˆ๋‹ค. ์ „์ฒด์ ์ธ ์•„ํ‚คํ…์ฒ˜๋ฅผ ์„ค๊ณ„ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

1. ์ „์ฒด ์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜

[ํด๋ผ์ด์–ธํŠธ]
    โ†“
[API Gateway / Load Balancer]
    โ†“
[Connection Manager (WebSocket)] -- [Presence Service]
    โ†“
[Chat Service] -- [Push Notification Service]
    โ†“
[Message Queue (Kafka)]
    โ†“
[Message Store (Cassandra)] -- [User Store (PostgreSQL)]
    โ†“
[File Store (S3/CDN)]

2. WebSocket ์—ฐ๊ฒฐ ๊ด€๋ฆฌ

@Service
public class ConnectionManager {
    private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
    
    @OnOpen
    public void onConnect(WebSocketSession session, @PathParam("userId") String userId) {
        sessions.put(userId, session);
        presenceService.updateStatus(userId, Status.ONLINE);
    }
    
    @OnMessage
    public void onMessage(String message, WebSocketSession session) {
        MessageDTO msg = parseMessage(message);
        chatService.processMessage(msg);
    }
    
    @OnClose
    public void onDisconnect(WebSocketSession session, @PathParam("userId") String userId) {
        sessions.remove(userId);
        presenceService.updateStatus(userId, Status.OFFLINE);
    }
}

3. ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ

@Service
public class ChatService {
    private final KafkaTemplate<String, Message> kafkaTemplate;
    private final MessageRepository messageRepository;
    private final PushNotificationService pushService;
    
    public void processMessage(Message message) {
        // 1. ๋ฉ”์‹œ์ง€ ์œ ํšจ์„ฑ ๊ฒ€์ฆ
        validateMessage(message);
        
        // 2. ๋ฉ”์‹œ์ง€ ์ €์žฅ
        messageRepository.save(message);
        
        // 3. ๋ฉ”์‹œ์ง€ ์ „๋‹ฌ
        deliverMessage(message);
        
        // 4. ์˜คํ”„๋ผ์ธ ์‚ฌ์šฉ์ž์—๊ฒŒ ํ‘ธ์‹œ ์•Œ๋ฆผ
        if (!isUserOnline(message.getReceiverId())) {
            pushService.sendPush(message);
        }
    }
    
    private void deliverMessage(Message message) {
        // Kafka๋กœ ๋ฉ”์‹œ์ง€ ๋ฐœํ–‰
        kafkaTemplate.send("chat-messages", message);
        
        // ์˜จ๋ผ์ธ ์‚ฌ์šฉ์ž์—๊ฒŒ ์ฆ‰์‹œ ์ „๋‹ฌ
        WebSocketSession session = connectionManager.getSession(message.getReceiverId());
        if (session != null) {
            session.sendMessage(new TextMessage(message.toJson()));
        }
    }
}

4. ๋ฉ”์‹œ์ง€ ์ €์žฅ์†Œ ์„ค๊ณ„

// 1. ๋ฉ”์‹œ์ง€ ์Šคํ‚ค๋งˆ (Cassandra)
@Table("messages")
public class Message {
    @PrimaryKey
    private MessageId id;  // ๋ณตํ•ฉํ‚ค: chat_id + timestamp
    
    private String senderId;
    private String chatId;      // 1:1 ๋˜๋Š” ๊ทธ๋ฃน ์ฑ„ํŒ… ID
    private String content;
    private MessageType type;   // TEXT, IMAGE, FILE
    private long timestamp;
    private MessageStatus status; // SENT, DELIVERED, READ
    
    @Data
    public static class MessageId {
        private String chatId;
        private long timestamp;  // ์—ญ์ˆœ ์ •๋ ฌ์„ ์œ„ํ•ด ์‚ฌ์šฉ
    }
}

// 2. ๋ฉ”์‹œ์ง€ ์ €์žฅ์†Œ
@Repository
public class MessageRepository {
    private final CassandraTemplate cassandraTemplate;
    private final S3Client s3Client;
    
    public void saveMessage(Message message) {
        if (message.getType() != MessageType.TEXT) {
            // ๋ฏธ๋””์–ด ํŒŒ์ผ ์ €์žฅ
            String fileUrl = uploadToS3(message.getContent());
            message.setContent(fileUrl);
        }
        
        cassandraTemplate.insert(message);
    }
    
    public List<Message> getMessages(String chatId, long lastMessageTimestamp, int limit) {
        return cassandraTemplate.select(Query.builder()
            .partition("chat_id = ?", chatId)
            .range("timestamp < ?", lastMessageTimestamp)
            .limit(limit)
            .build(), Message.class);
    }
}

5. ์‹ค์‹œ๊ฐ„ ์ƒํƒœ ๊ด€๋ฆฌ

@Service
public class PresenceService {
    private final RedisTemplate<String, UserStatus> redisTemplate;
    private final UserStatusPublisher statusPublisher;
    
    public void updateStatus(String userId, Status status) {
        // Redis์— ์ƒํƒœ ์ €์žฅ (TTL ์„ค์ •)
        redisTemplate.opsForValue().set(
            "presence:" + userId, 
            new UserStatus(userId, status),
            30,
            TimeUnit.MINUTES
        );
        
        // ์ƒํƒœ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ ๋ฐœํ–‰
        statusPublisher.publish(new StatusChangeEvent(userId, status));
    }
    
    // ์‚ฌ์šฉ์ž ์ƒํƒœ ์กฐํšŒ (bulk)
    public Map<String, Status> getUserStatuses(List<String> userIds) {
        List<String> keys = userIds.stream()
            .map(id -> "presence:" + id)
            .collect(Collectors.toList());
            
        return redisTemplate.opsForValue()
            .multiGet(keys).stream()
            .collect(Collectors.toMap(
                UserStatus::getUserId,
                UserStatus::getStatus
            ));
    }
    
    // ํ•˜ํŠธ๋น„ํŠธ ์ฒ˜๋ฆฌ
    @Scheduled(fixedRate = 30000)  // 30์ดˆ๋งˆ๋‹ค
    public void processHeartbeats() {
        // ๋งŒ๋ฃŒ๋œ ์ƒํƒœ ์ฒ˜๋ฆฌ
        Set<String> expiredUsers = findExpiredUsers();
        expiredUsers.forEach(userId -> 
            updateStatus(userId, Status.OFFLINE));
    }
}

6. ๊ทธ๋ฃน ์ฑ„ํŒ… ๊ด€๋ฆฌ

@Service
public class GroupChatService {
    private final RedisTemplate<String, Set<String>> redisTemplate;
    private final ChatRepository chatRepository;
    
    public void createGroup(String groupId, String name, List<String> members) {
        // 1. ๊ทธ๋ฃน ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ €์žฅ
        ChatGroup group = ChatGroup.builder()
            .id(groupId)
            .name(name)
            .createdAt(Instant.now())
            .build();
        chatRepository.saveGroup(group);
        
        // 2. ๋ฉค๋ฒ„ ์ •๋ณด ์บ์‹ฑ
        redisTemplate.opsForSet()
            .add("group:" + groupId, members.toArray(new String[0]));
    }
    
    public void sendGroupMessage(Message message) {
        Set<String> members = getGroupMembers(message.getChatId());
        
        // ๊ฐ ๋ฉค๋ฒ„์—๊ฒŒ ๋ฉ”์‹œ์ง€ ์ „๋‹ฌ
        CompletableFuture.allOf(
            members.stream()
                .map(memberId -> {
                    Message copy = message.toBuilder()
                        .receiverId(memberId)
                        .build();
                    return chatService.processMessage(copy);
                })
                .toArray(CompletableFuture[]::new)
        ).join();
    }
    
    // ๊ทธ๋ฃน ๋ฉค๋ฒ„ ๊ด€๋ฆฌ
    public void addMember(String groupId, String userId) {
        redisTemplate.opsForSet().add("group:" + groupId, userId);
        notifyMemberChange(groupId, userId, "ADDED");
    }
    
    public void removeMember(String groupId, String userId) {
        redisTemplate.opsForSet().remove("group:" + groupId, userId);
        notifyMemberChange(groupId, userId, "REMOVED");
    }
}

7. ํ‘ธ์‹œ ์•Œ๋ฆผ ์‹œ์Šคํ…œ

@Service
public class PushNotificationService {
    private final FirebaseMessaging firebaseMessaging;
    private final APNSClient apnsClient;
    private final DeviceRepository deviceRepository;
    
    public void sendPush(Message message) {
        List<Device> devices = deviceRepository
            .findByUserId(message.getReceiverId());
            
        devices.forEach(device -> {
            try {
                switch (device.getPlatform()) {
                    case ANDROID:
                        sendFirebaseNotification(device, message);
                        break;
                    case IOS:
                        sendAPNSNotification(device, message);
                        break;
                }
            } catch (Exception e) {
                log.error("Push notification failed", e);
                // ์‹คํŒจํ•œ ์•Œ๋ฆผ ์žฌ์‹œ๋„ ํ์— ์ถ”๊ฐ€
                retryQueue.add(new RetryNotification(device, message));
            }
        });
    }
    
    // ์•Œ๋ฆผ ํ…œํ”Œ๋ฆฟ ๊ด€๋ฆฌ
    private Notification createNotification(Message message) {
        NotificationTemplate template = switch (message.getType()) {
            case TEXT -> new TextMessageTemplate();
            case IMAGE -> new ImageMessageTemplate();
            case FILE -> new FileMessageTemplate();
        };
        
        return template.create(message);
    }
}

์ด๋Ÿฌํ•œ ์„ค๊ณ„๋ฅผ ํ†ตํ•ด:

  1. ์‹ค์‹œ๊ฐ„ ๋ฉ”์‹œ์ง€ ์ „์†ก
  2. ๋ฉ”์‹œ์ง€ ์˜์†์„ฑ
  3. ์˜คํ”„๋ผ์ธ ์‚ฌ์šฉ์ž ์ง€์›
  4. ๊ทธ๋ฃน ์ฑ„ํŒ… ๊ธฐ๋Šฅ
  5. ์ƒํƒœ ๊ด€๋ฆฌ
  6. ์•Œ๋ฆผ ์ฒ˜๋ฆฌ

๋“ฑ์˜ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฉด์ ‘๊ด€: ์‹œ์Šคํ…œ์˜ ํ™•์žฅ์„ฑ ์ธก๋ฉด์—์„œ ์ž ์žฌ์ ์ธ ๋ณ‘๋ชฉ ์ง€์ ๊ณผ ๊ทธ ํ•ด๊ฒฐ ๋ฐฉ์•ˆ์€ ๋ฌด์—‡์ธ๊ฐ€์š”?

8. ํ™•์žฅ์„ฑ ์ „๋žต

8.1 WebSocket ์—ฐ๊ฒฐ ํ™•์žฅ

@Configuration
public class WebSocketScalingConfig {
    
    // 1. ์—ฐ๊ฒฐ ๊ด€๋ฆฌ ํด๋Ÿฌ์Šคํ„ฐ
    @Bean
    public ConnectionCluster connectionCluster() {
        return ConnectionCluster.builder()
            .withNodes(List.of(
                new Node("ws-1", "10.0.1.1"),
                new Node("ws-2", "10.0.1.2")
            ))
            .withStickySessions(true)  // ๊ฐ™์€ ์‚ฌ์šฉ์ž๋Š” ๊ฐ™์€ ๋…ธ๋“œ๋กœ
            .build();
    }
    
    // 2. ์—ฐ๊ฒฐ ๋ถ€ํ•˜ ๋ถ„์‚ฐ
    @Bean
    public ConnectionLoadBalancer loadBalancer() {
        return new ConsistentHashLoadBalancer(
            metric -> metric.getActiveConnections(),
            threshold -> threshold < MAX_CONNECTIONS_PER_NODE
        );
    }
}

8.2 ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ํ™•์žฅ

@Service
public class MessageProcessingService {
    
    // 1. ์ƒค๋”ฉ ์ „๋žต
    private String determineShardKey(Message message) {
        if (message.isGroupMessage()) {
            return "group:" + message.getChatId();  // ๊ทธ๋ฃน๋ณ„ ์ƒค๋”ฉ
        }
        return "user:" + message.getSenderId();     // ์‚ฌ์šฉ์ž๋ณ„ ์ƒค๋”ฉ
    }
    
    // 2. ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ
    public void processMessages(List<Message> messages) {
        // ๋ฉ”์‹œ์ง€๋ฅผ ์ƒค๋“œ๋ณ„๋กœ ๊ทธ๋ฃนํ™”
        Map<String, List<Message>> shardedMessages = messages.stream()
            .collect(Collectors.groupingBy(this::determineShardKey));
            
        // ๊ฐ ์ƒค๋“œ๋ณ„๋กœ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ
        CompletableFuture.allOf(
            shardedMessages.entrySet().stream()
                .map(entry -> CompletableFuture.runAsync(() ->
                    processMessageBatch(entry.getValue()),
                    messageProcessorExecutor))
                .toArray(CompletableFuture[]::new)
        ).join();
    }
    
    // 3. ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ
    @Scheduled(fixedRate = 100)  // 100ms๋งˆ๋‹ค
    public void processBatch() {
        List<Message> batch = messageBatcher.getBatch();
        if (!batch.isEmpty()) {
            processMessages(batch);
        }
    }
}

8.3 ๋ฐ์ดํ„ฐ ์ €์žฅ์†Œ ํ™•์žฅ

@Configuration
public class StorageScalingConfig {
    
    // 1. ์ฝ๊ธฐ ๋ณต์ œ๋ณธ ๊ด€๋ฆฌ
    @Bean
    public ReadReplicaManager replicaManager() {
        return ReadReplicaManager.builder()
            .withReplicaNodes(getReplicaNodes())
            .withLoadBalancing(LoadBalancingStrategy.LEAST_CONNECTIONS)
            .withFailover(true)
            .build();
    }
    
    // 2. ์บ์‹œ ๊ณ„์ธตํ™”
    @Bean
    public CacheManager cacheManager() {
        return CacheManager.builder()
            .withL1Cache(new LocalCache(1000))    // ๋กœ์ปฌ ์บ์‹œ
            .withL2Cache(new RedisCache())        // ๋ถ„์‚ฐ ์บ์‹œ
            .withEvictionPolicy(EvictionPolicy.LRU)
            .build();
    }
    
    // 3. ํ•ซ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ
    @Service
    public class HotDataManager {
        public void manageHotData() {
            // ํ™œ์„ฑ ์ฑ„ํŒ…๋ฐฉ ์‹๋ณ„
            Set<String> activeChats = identifyActiveChats();
            
            // ํ•ซ ๋ฐ์ดํ„ฐ ํ”„๋ฆฌํŽ˜์น˜
            activeChats.forEach(chatId -> 
                prefetchRecentMessages(chatId));
                
            // ์ฝœ๋“œ ๋ฐ์ดํ„ฐ ์•„์นด์ด๋น™
            archiveColdData();
        }
    }
}

8.4 ๋ถ€ํ•˜ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ์ž๋™ ์Šค์ผ€์ผ๋ง

@Service
public class ScalingMonitor {
    
    private final MetricRegistry metrics;
    private final AutoScaler autoScaler;
    
    // 1. ์‹œ์Šคํ…œ ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘
    @Scheduled(fixedRate = 5000)  // 5์ดˆ๋งˆ๋‹ค
    public void collectMetrics() {
        metrics.gauge("websocket.connections", () -> 
            connectionManager.getActiveConnections());
            
        metrics.gauge("message.processing.rate", () ->
            messageProcessor.getProcessingRate());
            
        metrics.gauge("storage.usage", () ->
            storageManager.getUsagePercentage());
    }
    
    // 2. ์ž๋™ ์Šค์ผ€์ผ๋ง ๊ฒฐ์ •
    @Scheduled(fixedRate = 30000)  // 30์ดˆ๋งˆ๋‹ค
    public void checkScaling() {
        ScalingDecision decision = ScalingAnalyzer.analyze(
            metrics.getMetrics(),
            ScalingPolicy.builder()
                .withCpuThreshold(70)
                .withMemoryThreshold(80)
                .withConnectionThreshold(5000)
                .build()
        );
        
        if (decision.requiresScaling()) {
            autoScaler.scale(decision);
        }
    }
    
    // 3. ์„ฑ๋Šฅ ๋ณ‘๋ชฉ ๊ฐ์ง€
    @Scheduled(fixedRate = 10000)  // 10์ดˆ๋งˆ๋‹ค
    public void detectBottlenecks() {
        List<BottleneckReport> reports = BottleneckDetector
            .analyze(metrics.getMetrics());
            
        reports.forEach(report -> {
            log.warn("Bottleneck detected: {}", report);
            alertOperations(report);
            applyMitigation(report);
        });
    }
}

8.5 ์žฅ์•  ๋Œ€์‘ ์ „๋žต

@Service
public class FailureHandlingService {
    
    // 1. ์„œํ‚ท๋ธŒ๋ ˆ์ด์ปค ๊ตฌ์„ฑ
    @Bean
    public CircuitBreakerFactory circuitBreakerFactory() {
        return new CircuitBreakerFactory()
            .withFailureThreshold(5)
            .withTimeout(Duration.ofSeconds(1))
            .withResetTimeout(Duration.ofSeconds(30));
    }
    
    // 2. ์žฅ์•  ๊ฒฉ๋ฆฌ
    @Service
    public class FailureIsolator {
        public void isolateFailure(Failure failure) {
            // ์‹คํŒจํ•œ ๋…ธ๋“œ ๊ฒฉ๋ฆฌ
            nodeManager.isolateNode(failure.getNodeId());
            
            // ํŠธ๋ž˜ํ”ฝ ์žฌ๋ถ„๋ฐฐ
            trafficManager.redistributeTraffic(
                failure.getNodeId(), 
                getHealthyNodes()
            );
            
            // ๋ณต๊ตฌ ํ”„๋กœ์„ธ์Šค ์‹œ์ž‘
            recoveryManager.startRecovery(failure);
        }
    }
}

์ด๋Ÿฌํ•œ ํ™•์žฅ์„ฑ ์ „๋žต์„ ํ†ตํ•ด:

  1. ์—ฐ๊ฒฐ ์ˆ˜์˜ ์„ ํ˜•์  ํ™•์žฅ
  2. ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ๋Ÿ‰ ํ–ฅ์ƒ
  3. ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ์ตœ์ ํ™”
  4. ์ž๋™ํ™”๋œ ์Šค์ผ€์ผ๋ง
  5. ํšจ๊ณผ์ ์ธ ์žฅ์•  ์ฒ˜๋ฆฌ

๋ฅผ ๋‹ฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํŠนํžˆ ์ค‘์š”ํ•œ ์ ์€ ๊ฐ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋…๋ฆฝ์ ์œผ๋กœ ํ™•์žฅ ๊ฐ€๋Šฅํ•˜๋„๋ก ์„ค๊ณ„ํ–ˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.