Skip to content

유저의 과도한 데이터삽입은 어떻게 제한시키나요?

성유진 edited this page Mar 10, 2025 · 1 revision

서버의 제한된 스토리지

서버는 제한된 용량을 가지고 있기때문에 한 유저가 무한하게 데이터를 삽입하면 다른 유저에게 문제가 될 수 있습니다.

이러한 서버의 스토리지 관리를 위해 두가지의 처리방식이 있습니다.


Redis를 이용하여 유저 DB 관리하기

로그인이 없이 사용이 가능한 현재 서비스상 유저가 삽입한 데이터를 영원히 유지할 필요는 없습니다. 따라서 해당 유저가 일정기간동안 서비스를 이용하지 않으면 더미 데이터가 됩니다.

이러한 더미데이터를 처리하기위해 저희는 Redis의 Pub/Sub을 이용하여 여러 세션으로부터 생성되는 데이터베이스의 용량을 관리하였습니다.

pub/sub

만료된 세션 관리 Pub/Sub

세션을 Redis에 TTL을 이용해 저장하였으며, 만료 시 Redis에서 해당 키가 제거됩니다. 이때, 연결된 DB도 함께 제거해야 했습니다.

keyspace notification 설정

아래와 같이 두 가지 옵션을 활성화하여 만료 이벤트를 감지하도록 설정하였습니다:

  • E : Key 이벤트 활성화 옵션
  • x : 키의 만료 이벤트 활성화 옵션
this.defaultConnection.on('ready', () => {
  this.defaultConnection.config('SET', 'notify-keyspace-events', 'Ex');
});

PubSub

private subscribeToExpiredEvents() {
  this.pubsub.subscribe('__keyevent@0__:expired');

  this.pubsub.on('message', (event, session) => {
    this.queryDBAdapter.dropDatabase(session);
  });
}
  • subscribe(channel) : 채널(이벤트) 구독

    • __keyevent@0__:expired : Redis에서 특정 키가 만료될 때 발생하는 이벤트를 발생시키는 특수 채널

    사용자의 세션이 만료된 시점에 데이터베이스를 제거하는 로직을 실행시켜야 하였기 때문에, __keyevent@0__:expired 채널을 구독하였습니다.

  • on(channel, message)

    • __keyevent@0__:expired 채널에서는 만료된 키 값을 message로 반환합니다.

    key값을 세션ID로 설정하여 관리하고 있었으므로, message로 전달받은 만료된 키 값을 서비스 로직 함수로 전달하여 데이터베이스를 제거하는 작업을 수행하였습니다.


행 개수 제한을 통한 용량 제한

하지만 같은 세션에 대해서 무수히 많은 데이터 삽입을 시도할 수도 있습니다.

이를 막기 위해 삽입할 수 있는 데이터의 행 개수를 제한했습니다.

유저는 UI 상단바를 통해 현재 어느정도의 데이터를 삽입하였는지 확인할 수 있습니다.

스크린샷 2024-12-01 오후 4 41 15

유저가 쿼리를 실행하거나 랜덤 데이터 삽입을 통해 데이터를 넣을때 마다 이는 갱신됩니다.

만약 유저가 그 이상의 데이터 삽입을 시도하면 트랜잭션을 통해 롤백 시키고 에러를 반환합니다.

이는 다음과 같이 구현되어있습니다.

sequenceDiagram
    participant User
    participant Interceptor
    participant QueryService
    participant UsageService
    participant Redis

    User->>Interceptor: 데이터 삽입 요청 (쿼리 실행 또는 랜덤 데이터 삽입)
    Interceptor->>QueryService: DB Connection 생성 및 트랜잭션 시작
    QueryService->>UsageService: 해당 Connection으로 쿼리 실행
    UsageService-->>UsageService: 전체 테이블의 행 개수 조회
    alt 행 개수 제한 이내
        UsageService-->>Redis: 전체 테이블 행 개수 갱신
        UsageService-->>Interceptor: 성공 응답
        Interceptor-->>User: 트랜잭션 커밋 및 데이터 삽입 성공 알림
    else 행 개수 초과
        UsageService-->>Interceptor: 에러 반환
        Interceptor-->>User: 트랜잭션 롤백 및 데이터 삽입 실패 알림
    end
Loading

고려했던 방안 - 데이터베이스의 실제 크기 확인

행 개수가 아닌 데이터베이스의 실제 크기를 확인하는 방안을 고려하였지만, 데이터베이스의 실제 크기를 구하는 방안을 구현하는 두 가지 방식 모두 구현상의 문제점이 있었습니다.
또한, 사용자가 서비스를 사용하는 관점에서 데이터의 용량 크기보다는 행의 개수로 제한하는 것이 직관적이라고 판단하여 행 수로 용량을 제한하는 것으로 결정하였습니다.

고려했던 방안과 해당 방식들을 선택하지 않은 구체적인 이유는 다음과 같습니다.

1. MySQL에서 제공하는 information_schema로부터 정보 수집하기

information_schema의 tables에서 저장공간과 관련된 정보를 수집할 수 있습니다.

엔진이 InnoDB이기 때문에 DATA_LENGTHINDEX_LENGTH의 값을 더하여 실제 데이터베이스의 크기를 구할 수 있습니다.

그에 따라서 특정 데이터베이스의 테이블 크기를 구하는 쿼리와 그에 따른 실행결과는 다음과 같습니다.

하지만 데이터를 많이 넣어 확인을 해보아도 용량 크기가 변하지 않았습니다. 다음날에 확인하니 반영된 것을 확인할 수 있었습니다.


채택하지 않은 이유

MySQL 자체에서 이런 메타 정보를 실시간으로 업데이트하지 않는 것을 확인할 수 있었습니다.
하지만, 데이터 용량 제한을 위해서는 즉각적인 확인이 필요했기 때문에 해당 방법을 채택하지 않았습니다.


2. 실제로 데이터베이스가 저장되는 디렉토리의 크기를 리눅스 명령어로 확인하기

DB 생성시 만들어지는 데이터 디렉토리 용량을 제한 drop database db7b13d7e7cc4141ecafee3cb3320331cf(데이터베이스명); 를 실행한 경우에 발생하는 변화

수정 일자 변동은 제외하고 파일의 크기만 비교하면 아래 두 가지의 변화만 발생한 것을 알 수 있습니다.

  • 해당 데이터베이스의 디렉토리 삭제
  • binlog.000004 의 데이터 크기 증가

binlog.000004는 로그파일이기 때문에, 데이터베이스에 대한 정보는 해당 데이터베이스명의 디렉토리 내부에만 존재한다는 것을 확인할 수 있었습니다.

또한, 데이터를 삽입하면 해당 데이터베이스 디렉터리의 내부 크기가 증가하는 것을 확인할 수 있었습니다.

따라서 information_schema를 활용하는 방식과 달리 즉각적으로 데이터베이스의 용량을 반영할 수 있는 방식이었습니다.


채택하지 않은 이유

로컬에서 진행한 테스트는 docker로 띄운 MySQL 데이터 파일이 저장되는 디렉토리를 마운트할 수 있는 상황에서 진행하였습니다.
하지만 실제 개발 환경은 서비스를 제공하는 서버와 데이터베이스의 서버를 분리하였기 떄문에 해당 방식을 구현하는데에 어려움이 있었습니다.
또한 리눅스 명령어인 du 명령어를 사용하여 확인을 하였는데, 운영체제의 명령어에 의존하고 있는 방식이었습니다.

즉, 개발 환경 구성에 많이 의존할 수 밖에 없는 방식이었기 때문에 해당 방법을 채택하지 않았습니다.

로그인이 없는 서비스이기 때문에 세션으로 사용자를 관리하였습니다.

Redis를 저장소로 활용하여 세션을 관리하도록 하였고, ioredis를 통해 NestJS에서 Redis를 사용하였습니다.


Clone this wiki locally