728x90

여러분, 혹시 친구와 나눈 대화를 기억하지 못해서 당황했던 적 있으신가요?

 

"어? 네가 그 얘기 했었어? 난 기억이 안 나는데..."

 

이런 상황이 발생하면 뭔가 민망하지만, 상대는 이런 대화를 Stateless한 태도로 받아들일 수 있을지도 모릅니다.

 

오늘은 이런 "기억하지 않는 대화"와 비슷한 Stateless, 그리고 그와 연관된 Connectionless에 대해 이야기해볼까 합니다.


Stateless란 무엇인가요?

Stateless는 말 그대로 "상태를 기억하지 않는 것"입니다.
쉽게 말해, 여러분이 음식점에서 주문할 때를 떠올려봅시다.

  1. 여러분: "김치찌개 주세요!"
  2. 직원: "네"
  3. 직원: "누구 김치찌개 시켰나요?"
  4. 여러분: "저요!"

직원은 여러분의 상태를 기억하지 않습니다.
다시 말해, 여러분이 누구인지, 이전에 뭘 주문했는지에 대한 정보는 그때그때 잊혀지는 거죠.
HTTP도 딱 이와 같습니다.

HTTP는 요청과 응답 사이에 상태를 저장하지 않습니다.
즉, Stateless한 프로토콜입니다.


Connectionless란 무엇인가요?

Connectionless는 연결을 유지하지 않는다는 의미입니다.
마치 음식점에서 음식을 주문하고 나면, 직원이 테이블 옆에 서서 대기하지 않는 것과 비슷하죠.

  1. 여러분: "김치찌개 주세요!"
  2. 직원: "네" (메모한 뒤 떠남)

직원이 계속 옆에 서 있으면 더 빨리 요구를 들어줄 수도 있겠지만,
그렇게 하면 다른 손님들에게 서비스를 제공하지 못하게 되는 문제가 생깁니다.

HTTP의 Connectionless도 마찬가지입니다.

클라이언트(브라우저)와 서버는 요청을 보낸 뒤 연결을 끊고, 다음 요청 때 다시 연결을 시작합니다.


HTTP는 왜 Stateless를 채택했을까요?

"왜 기억을 안 하지?"라고 궁금할 수 있습니다.
그 이유는 간단합니다: 확장성과 효율성 때문이죠.

  • 확장성:
    상태를 기억하지 않으면 서버는 요청이 올 때마다 독립적으로 처리할 수 있습니다.
    많은 사용자가 동시에 접속해도, 서로의 상태를 신경 쓰지 않아도 되니 시스템이 더 단순해지고 확장성이 높아집니다.
  • 효율성:
    상태를 저장하려면 많은 메모리와 자원이 필요합니다.
    기억해야 할 정보가 많아질수록 서버는 더 느려지겠죠?
    하지만 Stateless구조라면 이런 부담을 줄일 수 있습니다.

여기서드는 의문!

 

"확장성과 효율성을 위해 Stateless하고, Connectionless하면 요청 할 때마다 연결을 끊고 다시 맺을텐데,

성능의 문제가 생길 수 있지 않을까요?"

 

이를 해결하기 위해 "Keep-Alive"가 등장하게 됩니다.


HTTP의 Keep-Alive: "계속 얘기하자!"

음식점 상황을 다시 생각해 봅시다.

  • 기본 HTTP 방식:
    직원이 주문을 받을 때마다 떠났다가 다시 돌아옵니다.
  • HTTP Keep-Alive 방식:
    "저 김치찌개 말고 밥도 추가요!"
    직원이 "네, 계속 말씀하세요!"라고 하며 테이블을 계속 오갑니다.

Keep-Alive는 여러 요청을 처리할 때 연결을 유지한 채 대화를 이어가도록 해줍니다.
그 덕분에 매번 연결을 새로 맺을 필요가 없어 성능이 개선됩니다.


TCP의 Keep-Alive와 HTTP의 Keep-Alive의 차이

HTTP에서의 Keep-Alive와 TCP의 Keep-Alive는 어떤 차이가 있는지 알아보겠습니다.

 

이제 조금 더 깊이 들어가서, TCP Keep-AliveHTTP Keep-Alive의 차이를 알아봅시다.

  1. TCP Keep-Alive:
    TCP 레벨에서 네트워크 연결이 끊어졌는지 확인하는 작은 패킷을 주기적으로 보내는 기능입니다.
    즉, "연결이 살아 있나?"를 체크합니다.
  2. HTTP Keep-Alive:
    HTTP 요청과 응답을 처리할 때 연결을 유지하는 기능입니다.
    "새로운 연결을 맺지 말고 기존 연결을 재사용하자"는 뜻입니다.

비유하자면,

  • TCP Keep-Alive는 친구가 계속 살아있는지 확인하는 안부 전화이고,
  • HTTP Keep-Alive는 한 번에 많은 대화를 효율적으로 끝내는 방법입니다.

 

이러한 기능들은 우리가 사용하는 HTTP프로토콜에서 간단하게 확인해 볼 수 있는데요.

 

HTTP 1.1버전 기준으로 Connection속성keep-alive가 Default로 설정되어있고,

이에 대해 keep-Alive는 60초Timeout을 가지게 됩니다.


Reference

https://developer.mozilla.org/en-US/docs/Web/HTTP/Overview

https://en.wikipedia.org/wiki/HTTP_persistent_connection

https://blog.naver.com/whdgml1996/222153047879

 

728x90
728x90

1. Mysql 접속

기본적으로 데이터베이스 작업을 하거나 테이블 작업을 하려면 설치한 mysql에 접속해야합니다.

 

만약 처음 설치하였다면 기본적으로 root라는 계정을 할당해주고, 해당 계정은 초기 설치할 때 설정한 비밀번호로 설정이 되게 됩니다. 

mysql -u root -p

2. Database

2-1. 생성

CREATE DATABASE 데이터베이스명;

2-2. 목록 확인

SHOW DATABASES;

2-3. 데이터베이스 선택

USE 데이터베이스명;

2-4. 데이터베이스 삭제

DROP DATABASE 데이터베이스명;

3. 사용자 및 권한 설정

3-1. 사용자 생성

CREATE USER '사용자명'@'호스트' IDENTIFIED BY '비밀번호';

호스트: 접속할 수 있는 정보로 로컬만 허용할 경우 localhost, 모두 허용할 경우 %로 지정

3-2. 권한 부여

GRANT 권한종류 ON 데이터베이스명.* TO '사용자명'@'호스트';
FLUSH PRIVILEGES;

권한 종류: ALL PRIVILEGES, SELECT, INSERT, UPDATE, DELETE, CREATE, DROP

3 - 2. 권한 확인

SHOW GRANTS FOR '사용자명'@'호스트';

3-3. 사용자 삭제

DROP USER '사용자명'@'호스트';
728x90

'CS > Database' 카테고리의 다른 글

[Index] 구축한 서버 Query분석 및 성능 측정 후 Index 구축  (2) 2024.08.08
Redis는 어떤걸까?  (0) 2024.04.08
728x90

이전에 서버의 조회 성능을 향상하기 위해 Application에서 Caching 기술을 적용해 성능을 개선시켜 보았는데요, Caching은 DB에서 데이터를 처음 조회할 때 메모리에 임시 저장하여 이후 조회 시 빠르게 데이터를 제공하는 방식으로, DB 연결 횟수를 줄여 성능을 향상하는 데 큰 도움을 주었습니다.

 

그러나 Caching은 데이터가 자주 갱신되거나, 실시간성이 중요한 경우 Caching이 오히려 성능 저하를 초래할 수도 있습니다. 이럴 때 DB 자체의 조회 성능을 직접적으로 향상하는 방법을 고려할 필요가 있습니다.

 

이번 글에서는 DB에서 조회 성능을 향상하는 대표적인 방법 중 하나인 Indexing에 대해 알아보고, 이를 현재 제작 중인 콘서트 서비스에 어떻게 적용할 수 있을지 분석 및 적용하여 성능 측정을 진행해 보도록 하겠습니다.

Index란?

데이터 베이스에서 데이터를 특정 컬럼들로 조회를 할 경우 빠르게 조회를 하기 위해 B-Tree구조로 컬럼들을 정의해 두고, 이를 통해 정렬되어 있는 클러스터들을 통해 해당 데이터를 빠르게  찾을 수 있도록 해주는 기능입니다.

 

MySQL에서는 기본 키(Primary Key, PK)를 정의하면, 해당 키에 대해 자동으로 클러스터드 인덱스(Clustered Index)가 생성됩니다. 클러스터드 인덱스는 데이터의 물리적 순서와 인덱스의 순서가 일치하도록 보장하여, 기본 키를 사용한 조회 성능을 최적화합니다.

 

인덱스의 종류
인덱스는 크게 단일 인덱스(Single Index)와 복합 인덱스(Composite Index)로 나눌 수 있습니다. 복합 인덱스는 여러 컬럼을 조합하여 생성한 인덱스로, 커버링 인덱스(Covering Index)도 그 중 하나입니다. 커버링 인덱스는 인덱스에 포함된 모든 컬럼을 통해 필요한 데이터를 직접 조회할 수 있어, 실제 데이터를 추가로 조회하지 않아도 된다는 장점이 있습니다.

 

인덱스의 단점
인덱스는 조회 성능을 크게 향상시킬 수 있지만, 단점도 있습니다. 인덱스가 설정된 컬럼의 데이터가 추가, 변경, 삭제될 때마다 인덱스가 재정의(재정렬)되는데, 이 과정은 데이터베이스의 성능 저하를 일으킬 수 있습니다.

 

따라서 무조건 Index를 거는 것보다는 Query를 분석하여 적절한 상황에 적용하는 것을 권장드립니다.

Concert에서 발생하는 Query분석

각각의 도메인에서 발생하는 Query들을 알아보고 Index를 적용하기 적절한지 또한 굳이 생성하지 않아도 괜찮은지에 대해 알아보도록 하겠습니다.

1. Concert

콘서트는 메인 도메인인 Concert와 2개의 서브도메인인 ConcertSeries와 ConcertSeat로 이루어져 있습니다.

이에 서비스에서 각각 발생하는 쿼리들을 분석해 보도록 하겠습니다.

 

Concert
콘서트 조회는 현재 전체 리스트 조회와 PK로 조회하는 Query만을 사용하고 있어 추가적인 Index생성은 불필요하다 판단하여 Index작업을 수행하지 않았습니다.

@Repository
@RequiredArgsConstructor
public class ConcertRepositoryImpl implements ConcertRepository {
    //
    private final ConcertJpaRepository concertJpaRepository;

    @Override
    public void save(Concert concert){
        //
        this.concertJpaRepository.save(new ConcertJpo(concert));
    }

    @Override
    public Concert findById(String concertId) {
        Optional<ConcertJpo> jpo = this.concertJpaRepository.findById(concertId);
        return jpo.map(ConcertJpo::toDomain).orElse(null);
    }

    @Override
    public List<Concert> findAll() {
        //
        List<ConcertJpo> jpos =  this.concertJpaRepository.findAll();
        return jpos.stream().map(ConcertJpo::toDomain).toList();
    }
}

 

ConcertSeries

콘서트 시리즈의 경우 아래와 같이 예매 가능한 콘서트 시리즈를 조회하는 Query를 날리는 상황이 존재합니다.

select csj1_0.series_id,csj1_0.concert_id,csj1_0.create_at,csj1_0.end_at,csj1_0.reserve_end_at,csj1_0.reserve_start_at,csj1_0.start_at from concert_series csj1_0 where csj1_0.concert_id=? and csj1_0.reserve_start_at<=? and csj1_0.reserve_end_at>=?

이전 Index를 걸기 전 10만 건 중 콘서트 아이디와 현재 예약가능한 콘서트를 조회한 결과입니다. 총 65ms의 속도를 자랑하는 쿼리 성능입니다.

 

Index를 적용한 Query성능입니다. 솔직히 65ms의 속도가 나와 성능이 향상될까 싶었지만 11ms의 성능이 향상되었습니다.

 

왜 그럴까?

우선 Index를 생성할 때 사용한 코드입니다.

CREATE INDEX concert_series_active_index ON concert_series(reserve_start_at,concert_id, reserve_end_at);

추후 조회 할 때 sort를 reserve_start_at으로 걸 거 같아 reserve_start_at, concert_id, reserve_end_at순으로 걸었습니다.

 

성능은 향상되었지만 현재 가져오는 속도가 너무 빨라 왜 그런지 확인을 해보았습니다.

ConcertSeries에 걸려있는 Index들입니다. 현재 저는 concert_series_active_index를 사용을 하였는데요.

reserve_start_at의 Cardinality를 보시면 1인 것을 확인하실 수 있습니다.

 

아뿔싸 제가 데이터를 적재할 때 reserve_start_at와 reserve_end_at의 값을 전부 동일하게 준 것을 기억하였습니다.

실제 오픈한 서비스에 해당 데이터들이 있다면 성능 이슈 없는 좋은 상황이겠지만, 테스트를 해야 하는 저에게는 좋지 않은 상황이었습니다.

 

다만 Index에 대한 성능 향상을 확인하였고, 동일한 데이터로 인한 성능 향상은 결국 데이터 퀄리티에 의해 영향을 받는다라는 점을 알 수 있어 좋았습니다.

 

쿼리 실행계획을 확인해 보면 Index에 걸린 row수가 꽤 된다는 것을 확인해 볼 수 있습니다.

 

ConcertSeat

가장 성능이슈가 생길 것으로 예상한 데이터입니다.

총 1000만 건으로 테스트를 진행해 보도록 하겠습니다.

3초 정도 걸리는 쿼리인 것을 확인할 수 있습니다.

쿼리 실행 계획의 경우

위와 같이 아무런 Index를 타고 있지 않아 풀스캔한 것을 확인하실 수 있습니다.

 

3초나 걸려 좌석 시트를 조회하게 된다면 그 사이 좌석이 다 예약이 될 수도 있겠죠 그래서 성능 향상은 필수인 거 같습니다.

series_id로 Index를 생성하게 될 경우 동일한 쿼리의 속도가 36ms로 주는 것을 확인해 볼 수 있습니다.

concertSeat의 경우 seat점유 상태의 값이 자주 바뀌니 해당 상태로 Index를 거는 것이 아니라면 좋은 성능을 낼 수 있다 생각합니다.


2. Payment

payment는 결제내역이므로 단건 조회의 경우 PK를 사용하여 기본 Index를 사용합니다.

목록의 경우 userId로 조회를 하기 때문에 Index가 필요한 상황입니다. 

 

아래는 100만 건으로 테스트하였습니다.

인덱스를 건 후 69ms -> 19ms로 조회 시간이 단축되는 것을 확인할 수 있습니다.

 

위 Index를 통해 충분히 성능 향상을 했다 생각합니다.


3. Point

point는 pk를  userId로 가져가고 조회 또한 userId로 조회하기 때문에 기본적으로 제공해 주는 Index를 타게 됩니다.

 

문제는 pointHistory인데, 이는  userId로 조회하기 때문에 따로 Index를 추가해주어야 합니다.

 

인덱스가 걸려있지 않은 상태로 조회하였을 때의 성능입니다. 1건의 데이터를 가져오는데 359ms의 시간이 소요되는 것을 확인할 수 있습니다.

 

userId에 Index를 걸게 될 경우 359ms -> 20ms로 확연하게 주는 것을 확인하실 수 있습니다.

쿼리 실행 계획을 확인해 보면 extra에 성능에 이슈가 될만한 use filesort 같은 것들이 명시되어있지 않고 rows가 1개이므로 성능향상에 도움이 많이 된 것 같습니다.

4. TemporaryReservation - Index생성에는 부적합

TemporaryReservation은 임시 예약 테이블로써 주기적으로 삭제가 이뤄지고 잦은 수정이 이뤄지는 곳입니다.

그로써 주기적인 수정은 Index 재정의를 유발할 수 있으므로 적합하지 않다 판단하였습니다.


5. Reservation

Reservation은 예약 테이블입니다. 예약이 완료된 건에 대하여 생성이 되며, 한번 생성되면 환불하지 않는 한 삭제는 되지 않습니다.

따라서 Index를 걸기 좋은 상황으로 판단하여 Index를 적용하였습니다.

 

Index를 userId로 걸어 생성할 경우 466ms -> 37ms로 성능이 향상되었습니다.

쿼리 플랜을 보았을 때 100건에 대한 데이터들이 존재하며 좋지 않은 상황들은 쿼리 플랜에 명시되어있지 않은 것으로 보아 유효한 Indexing이었던 것 같습니다.

 

이상으로 Concert서비스에서 사용하는 여러개의 Query들에 대해 분석하고 Indexing을 통해 성능 향상을 해보았습니다.

Indexing은 데이터베이스에 탐색을 위한 자료형을 사용하는 만큼 데이터베이스 성능에 영향을 끼친다고 볼 수 있습니다.

 

따라서  Query의 분석을 통해 정말 걸어야 하는 Query인지, Indexing 하는 컬럼들의 값들이 자주 변경되지 않는지 등을 염두하여 적재적소에 잘 활용하는 것이 중요하다 생각합니다.

728x90

'CS > Database' 카테고리의 다른 글

[ Mysql ] Mysql 기본 작업  (0) 2024.11.13
Redis는 어떤걸까?  (0) 2024.04.08
728x90

우리가 개발을 하다보면 Redis라는 용어를 한번쯤 들어보았을 것이고, RedisTemplate라는 것을 한번은 코드상에서 봤을 수 있습니다.

개발을 하면서 데이터를 저장할 때 쓴다고 한 Mysql, Mongodb등 기존 데이터 베이스를 두고 왜 Redis라는 Database를 또 사용하는지, 그리고 어떻게 사용하는지에 대해 다뤄보겠습니다.

Redis란?

Redis는 REmote Dictionary Server의 약자이며, Mysql과 같이 서버와는 별개로 저장이 되지 않고 메모리상에 저장되는 In memory구조로 구성되어있습니다.

데이터는 Key Value로 관리가 되고 있으며, 다양한 구조 집합을 제공함으로 다양한 구조를 Value로 저장할 수 있으며 최대 512MB의 데이터가 저장가능합니다.

Key로 데이터를 관리하기때문에 조회에 있어 가장 빠른 성능을 자랑하며, 빠른 조회 성능을 요구하는 캐싱, 세션 관리 등에 사용을 하고 있습니다.

Redis는 In memory 구조로 key value형태로 데이터를 저장 하고 있으며, key로 데이터를 관리하기때문에 조회에 있어 빠른 성능을 자랑하며, 현재 가장 인기 있는 key value store입니다.

Key Value로 처리하면서 Redis는 Row로 저장되는 것 보다는 Column을 저장한다는 것으로 보이고, 이때문에 많은 양의 데이터를 저장하고 지속적으로 관리하는 기존 데이터베이스와 사용용도는 다르다고 필자는 생각합니다.

또한 InMemory구조로인한 데이터를 지속적으로 백업해주어야하는 문제가 있으며, 이를 해결하여 사용하기보다는 기존의 다른 데이터베이스와 함께 사용하면서 용도에 맞춰 사용하는 것이 가장 현명한거 같다 생각합니다.

Redis는 요청에 의했을 때는 Single Thread 개념을 가져오지만, background에서는 Multi Thread를 지원하는 만큼 데이터를 조회하는데에 있어 빠른 성능을 강점으로 가져가는 것 같습니다.

이처럼 Redis는 java의 Map과 같이 Key Value로 데이터를 저장하며, inmemory구조지만 Redis를 사용하는 상황은 분산 시스템에 있습니다. 분산시스템에서 동일한 key value의 저장소를 공유해야만 하는 상황이라면 Redis를 사용하여 해당 Issue를 해결하는 방법도 좋은 방법이 될 수 있습니다.


사용법

설치

우선 필자는 Database관리를 Docker로 하고 있기 때문에 Container로 만들어 관리를 하도록 하겠습니다.

1. Redis Image Pull

docker pull redis

Docker에서 지원해주는 Redis Image를 다운받아줍니다.

2. Redis Container run

docker run --name test-redis -p 6379:6379 redis

redis이미지를 토대로 container를 생성하여 실행하여줍니다.
port는 redis기본 port인 6379로 설정하였습니다.

3. redis접속

docker exec -it test-redis /bin/bash

위 명령어를 통해 실행중인 container에 접속해주도록 합니다.

redis-cli

위 명령어 입력을 완료하면 container로 실행중인 redis안에 접속할 수 있습니다.

4. CRD

set {key} {value}
get {key}
del {key}

위 명령어와 같이 set을 통해 데이터를 등록할 수 있습니다.
key는 식별자 역할을 함으로써 수정할 때에도 동일하게 set명령어를 사용할 경우 해당 keyvalue를 변경할 수 있습니다.

이처럼 Redis자체를 사용하는 것은 쉽게 사용할 수 있습니다.
그럼 이제 서버에서 어떻게 사용하는지를 알아보겠습니다.

JAVA에서 Redis사용법

spring:
  data:
    redis:
      host: localhost
      port: 6379

Application.yml설정입니다.
Redis기본 port가 6379이기 때문에 따로 port가 겹치지 않아 6379Port로 설정하여 사용하였습니다.

@Configuration
public class RedisConfig {
    @Value("${spring.data.redis.host}")
    private String redisHost;
    @Value("${spring.data.redis.port}")
    private int redisPort;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(redisHost, redisPort);
    }

    @Bean
    public RedisTemplate<String, String> redisTemplate() {
        RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}

RedisConfig설정입니다.
연결을 위해 redisHost와 redisPort값을 받아 Redis저장소에 연결 즉 connect를 하고, 이후 RedisTemplate를 정의함으로써 보다 편하게 사용할 수 있게 정의하였습니다.

@Repository
@Transactional
@RequiredArgsConstructor
public class TestStore {
    //
    private final RedisTemplate<String, String> redisTemplate;

    public void create(String key, String data) {
        ValueOperations<String, String> opreation = redisTemplate.opsForValue();
        opreation.set(key, data);
    }

    public void update(String key, String data) {
        ValueOperations<String, String> opreation = redisTemplate.opsForValue();
        opreation.set(key, data);
    }

    public String load(String key) {
        ValueOperations<String, String> opreation = redisTemplate.opsForValue();
        return opreation.get(key);
    }

    public void remove(String key) {
        redisTemplate.delete(key);
    }
}

TestStore입니다.
RedisTemplate를 통해 데이터를 CRUD를 하는 작업을 담당하게 구현하였습니다.


참고자료: https://redis.io/docs/

728x90

+ Recent posts