๋ฉด์ ๊ด: "์ค์๊ฐ ๋ฉ์์ง ์์คํ ์ ์ค๊ณํด์ฃผ์ธ์. ๋ค์ ๊ธฐ๋ฅ์ด ํ์ํฉ๋๋ค:
- 1:1 ์ฑํ
- ๊ทธ๋ฃน ์ฑํ
- ์จ๋ผ์ธ ์ํ ํ์
- ๋ฉ์์ง ์ ์ฅ ๋ฐ ๋๊ธฐํ
- ํธ์ ์๋ฆผ"
์ง์์: ๋จผ์ ๋ช ๊ฐ์ง ์๊ตฌ์ฌํญ์ ํ์ธํ๊ณ ์ถ์ต๋๋ค.
- ์์ ์ฌ์ฉ์ ์์ ๋ฉ์์ง ์ฒ๋ฆฌ๋์ ์ด๋ ์ ๋์ธ๊ฐ์?
- ๋ฉ์์ง ์ ์ฅ ๊ธฐ๊ฐ๊ณผ ํฌ๊ธฐ ์ ํ์ ์๋์?
- ์ด๋ค ์ข ๋ฅ์ ๋ฉ์์ง๋ฅผ ์ง์ํด์ผ ํ๋์? (ํ ์คํธ, ์ด๋ฏธ์ง, ํ์ผ ๋ฑ)
- ๋ฐฐ๋ฌ ํ์ธ์ด๋ ์ฝ์ ํ์ธ ๊ธฐ๋ฅ์ด ํ์ํ๊ฐ์?
๋ฉด์ ๊ด:
- DAU 100๋ง ๋ช , ์ด๋น ํ๊ท 10,000๊ฑด์ ๋ฉ์์ง
- ๋ฉ์์ง๋ ์๊ตฌ ๋ณด๊ด, ํ์ผ์ 30์ผ, ํฌ๊ธฐ๋ ๋ฉ์์ง๋น ์ต๋ 100KB
- ํ ์คํธ, ์ด๋ฏธ์ง, ํ์ผ ๋ชจ๋ ์ง์ ํ์
- ๋ฉ์์ง ๋ฐฐ๋ฌ ํ์ธ๊ณผ ์ฝ์ ํ์ธ ๋ชจ๋ ํ์
์ง์์: ์๊ฒ ์ต๋๋ค. ์ ์ฒด์ ์ธ ์ํคํ ์ฒ๋ฅผ ์ค๊ณํด๋ณด๊ฒ ์ต๋๋ค.
[ํด๋ผ์ด์ธํธ]
โ
[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)]
@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);
}
}
@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()));
}
}
}
// 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);
}
}
@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));
}
}
@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");
}
}
@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);
}
}
์ด๋ฌํ ์ค๊ณ๋ฅผ ํตํด:
- ์ค์๊ฐ ๋ฉ์์ง ์ ์ก
- ๋ฉ์์ง ์์์ฑ
- ์คํ๋ผ์ธ ์ฌ์ฉ์ ์ง์
- ๊ทธ๋ฃน ์ฑํ ๊ธฐ๋ฅ
- ์ํ ๊ด๋ฆฌ
- ์๋ฆผ ์ฒ๋ฆฌ
๋ฑ์ ๊ธฐ๋ฅ์ ๊ตฌํํ ์ ์์ต๋๋ค.
๋ฉด์ ๊ด: ์์คํ ์ ํ์ฅ์ฑ ์ธก๋ฉด์์ ์ ์ฌ์ ์ธ ๋ณ๋ชฉ ์ง์ ๊ณผ ๊ทธ ํด๊ฒฐ ๋ฐฉ์์ ๋ฌด์์ธ๊ฐ์?
@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
);
}
}
@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);
}
}
}
@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();
}
}
}
@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);
});
}
}
@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);
}
}
}
์ด๋ฌํ ํ์ฅ์ฑ ์ ๋ต์ ํตํด:
- ์ฐ๊ฒฐ ์์ ์ ํ์ ํ์ฅ
- ๋ฉ์์ง ์ฒ๋ฆฌ๋ ํฅ์
- ๋ฐ์ดํฐ ์ ๊ทผ ์ต์ ํ
- ์๋ํ๋ ์ค์ผ์ผ๋ง
- ํจ๊ณผ์ ์ธ ์ฅ์ ์ฒ๋ฆฌ
๋ฅผ ๋ฌ์ฑํ ์ ์์ต๋๋ค. ํนํ ์ค์ํ ์ ์ ๊ฐ ์ปดํฌ๋ํธ๊ฐ ๋ ๋ฆฝ์ ์ผ๋ก ํ์ฅ ๊ฐ๋ฅํ๋๋ก ์ค๊ณํ๋ค๋ ๊ฒ์ ๋๋ค.