개요
지난번 부하 테스트 결과, 상당히 안좋은 결과가 나왔다.
이번 글에서는 MongoDB를 도입하여 성능을 개선하는 과정을 살펴보려 한다.
[10k-Chat] Spring Boot 실시간 채팅 서버 구현 (2) - WAS 이중화
개요이번 글에서는 부하 테스트 결과를 분석하고, 이를 바탕으로 WAS 이중화를 통해 성능을 개선하는 방안을 살펴보겠다.부하 테스트이미 JMeter, Locust, K6 등 좋은 도구들이 많지만 내가 원하는 기
woong99.tistory.com
MongoDB란?
MongoDB는 NoSQL 기반의 문서형 데이터베이스로, JSON 형태의 데이터를 저장하고 빠르게 조회할 수 있도록 설계된 DBMS이다.
MongoDB의 주요 특징
1. 스키마리스(Schema-less) 구조
- RDMS와 달리 고정된 테이블 구조가 필요 없음
- 데이터 형식이 유연하게 변경 가능
- JSON과 유사한 BSON(Binary JSON) 형식으로 저장됨
{
"_id": ObjectId("507f1f77bcf86cd799439011"),
"name": "Alice",
"age": 25,
"address": {
"city": "Seoul",
"zip": "12345"
}
}
2. 수직/수평 확장 용이 (Scale-Out 지원)
- 샤딩(Sharding) 기능을 통해 데이터를 여러 서버로 분산 저장
- 대용량 트래픽에도 성능 저하 없이 확장 가능
3. 빠른 읽기/쓰기 성능
- 인덱싱 및 메모리 기반 연산으로 대량 데이터 조회 속도가 빠름
NoSQL vs RDBMS
개념 비교
구분 | NoSQL | RDBMS |
종류 | MongoDB, Redis, Cassandra 등 | MySQL, PostgreSQL, Oracle 등 |
데이터 구조 | 스키마 없음 (Schema-less) | 고정된 테이블 구조 (Schema) |
확장 방식 | 수평 확장 (Scale-Out, 여러 서버에 분산) | 보통 수직 확장 (Scale-Up, 서버 성능 업그레이드) |
속도 | 빠름 (조인 없이 문서 형태 저장) | 느릴 수 있음 (조인으로 여러 테이블 조회) |
트랜잭션 | 제한적 (일부 NoSQL은 ACID 지원 X) | ACID 보장 |
데이터 저장 방식 | Key-Value, Document, Column 기반 | 테이블 기반 (Row & Column) |
읽기/쓰기 성능 | 대량 데이터에 강함 | 정규화된 데이터에 강함 |
적합한 사용 사례 | 로그 저장, 실시간 분석, 검색, 채팅 시스템 | 금융, ERD, 주문 처리 시스템 등 정확성이 중요한 경우 |
NoSQL이 RDBMS보다 더 빠른 이유
1. 스키마가 없어 데이터 저장이 빠름
- RDBMS는 데이터를 저장할 때 스키마(테이블 구조)를 맞춰야 함
- NoSQL은 JSON/BSON 문서 형태로 유연하게 저장되므로 쓰기 속도가 빠름
2. 조인(Join)이 없어서 조회 속도가 빠름
- RDBMS는 데이터 정규화를 위해 여러 테이블로 나누고, JOIN 연산을 사용함
- NoSQL은 하나의 문서(Document) 내부에 중첩된 데이터를 저장하므로, 조회할 때 JOIN이 필요 없음
MongoDB 설치
Docker-Compose를 이용해 MongoDB를 설치한다.
services:
mongodb:
image: mongo:latest
container_name: mongodb
restart: always
ports:
- "27017:27017"
volumes:
- /containers/data/mongodb:/data/db
networks:
- spring-10k-chat-network
environment:
TZ: Asia/Seoul
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: root!))$
networks:
spring-10k-chat-network:
external: true
코드 설명
MongoDB 적용 전 코드에서 추가된 부분에 대해서만 작성한다.
전체 코드를 보고 싶으면 Github를 참고해주세요.
build.gradle.kts
MongoDB 라이브러리를 추가한다.
dependencies {
// MongoDB
implementation("org.springframework.boot:spring-boot-starter-data-mongodb")
}
application.yaml
환경에 맞게 host를 설정해준다. 나는 WAS와 MongoDB 둘 다 Docker를 이용해 띄우므로 Docker Host로 작성했다.
spring:
data:
mongodb:
host: mongodb
port: 27017
database: spring_chat
username: root
password: root!))$
authentication-database: admin
MongoConfig
아래 설정은 필수는 아니다. 다만 설정하지 않으면 Document가 생성될 때 _class 라는 필드가 생성된다.
package potatowoong.springchat.global.config.db
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.mongodb.MongoDatabaseFactory
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver
import org.springframework.data.mongodb.core.convert.DefaultMongoTypeMapper
import org.springframework.data.mongodb.core.convert.MappingMongoConverter
import org.springframework.data.mongodb.core.mapping.MongoMappingContext
@Configuration
class MongoConfig {
/**
* _class가 생성되지 않도록 설정
*/
@Bean
fun mappingMongoConverter(
mongoDatabaseFactory: MongoDatabaseFactory,
mongoMappingContext: MongoMappingContext
): MappingMongoConverter {
val dbRefResolver = DefaultDbRefResolver(mongoDatabaseFactory)
val converter = MappingMongoConverter(dbRefResolver, mongoMappingContext)
converter.setTypeMapper(DefaultMongoTypeMapper(null))
return converter
}
}
ChatMessage
Member와 ChatRoom은 MariaDB(RDBMS)에 저장되어 있기 때문에 직접적으로 연관 관계를 맺을 수 없다. 그러므로 아래와 같이 FK로 사용되는 컬럼을 같이 저장해 조회 시 RDBMS의 결과와 함께 애플리케이션단에서 합친다.
Document는 RDBMS와 달리 @Entity를 사용하지 않는다.
@Document(collection = "chat_message")
class ChatMessage(
@Id
val id: String? = null,
@Column(nullable = false)
val content: String,
val memberId: Long,
val chatRoomId: String,
val sendAt: LocalDateTime = LocalDateTime.now()
) {
}
ChatMessageRepository
Repository는 MongoRepository를 구현한다.
interface ChatMessageRepository : MongoRepository<ChatMessage, String> {
}
ChatService
채팅을 저장할 때 기존 JPA를 사용할 때 처럼 save() 메소드를 사용하면 된다.
@Service
class ChatService(
private val memberRepository: MemberRepository,
private val chatRoomRepository: ChatRoomRepository,
private val chatRoomMemberRepository: ChatRoomMemberRepository,
private val chatMessageRepository: ChatMessageRepository,
) {
@Transactional
fun saveChat(
chatRoomId: String,
request: MessageDto.Request,
memberId: Long
) {
// 채팅방 정보 조회
val chatRoom = chatRoomRepository.findByIdOrNull(chatRoomId)
?: throw CustomException(ErrorCode.NOT_FOUND_CHAT_ROOM)
// 채팅 저장
chatMessageRepository.save(
ChatMessage(
content = request.message,
memberId = memberId,
chatRoomId = chatRoom.chatRoomId!!
)
)
// 마지막 접속 시간 갱신
val chatRoomMember = chatRoomMemberRepository.findByChatRoomChatRoomIdAndMemberId(chatRoomId, memberId)
?: throw CustomException(ErrorCode.NOT_FOUND_CHAT_ROOM)
chatRoomMember.updateLastJoinedAt()
}
}
부하 테스트 결과
인프라
CPU(Core) | RAM(GB) | |
WAS1 | 2 | 2 |
WAS2 | 2 | 2 |
MariaDB | 2 | 4 |
MongoDB | 2 | 2 |
테스트 결과
MongoDB 적용 후 테스트 결과, 1,000명 기준 성능이 50배 이상 향상되었다.
이는 MongoDB와 MariaDB 간의 부하 분산 효과도 있지만, NoSQL의 뛰어난 쓰기 성능이 큰 영향을 미친 것으로 보인다.
결론
MongoDB를 적용하면서 상당히 큰 성능 개선이 있었다. 그럼에도 불구하고 5,000명 이상의 트래픽을 감당하려면 더 많은 개선이 필요해 보인다 ㅠㅠ
다음 장에서는 RabbitMQ를 이용해 서버 이중화로 인한 부작용을 개선하는 글을 쓸 예정이다.
- STOMP 부하 테스트 코드 링크 : https://github.com/woong99/stomp-load-test-tool
- 백엔드 코드 : https://github.com/woong99/spring-chat
- 프론트엔드 코드 : https://github.com/woong99/spring-chat-front
'Programming > 10K-Chat' 카테고리의 다른 글
[10k-Chat] Spring Boot 실시간 채팅 서버 구현 (2) - WAS 이중화 (0) | 2025.02.08 |
---|---|
[10k-Chat] Spring Boot 실시간 채팅 서버 구현 (1) - Stomp (0) | 2025.01.19 |