이전 글에 이어서 이번에는 스프링부트에서 어떻게 NoSQL을 캐싱 DB로 사용할 수 있는지 알아보려고 합니다.
2024.10.23 - [방구석 컴퓨터/방구석 DB] - RDB와 NoSQL
그전에 NoSQL이 어떤 식으로 활용되는지 좀 더 알아보려고 하는데요.
크게 2가지 상황을 이야기할 수 있을 거 같습니다.
1. 서로 다른 데이터를 저장하는 경우
RDB는 중요하고 복잡한 관계를 필요로 하는 데이터를 저장하는 데 사용되고, NoSQL은 실시간 처리나 대규모 비정형 데이터에 적합한 데이터를 저장하는데 사용됩니다.
이 경우, 서로 다른 역할을 맡고 있기 때문에, RDB와 NoSQL에 저장하는 데이터는 완전히 다릅니다.
온라인 게임 홈페이지를 예를 들면
- RDB에는 중요한 사용자 정보나 결제 정보, 게임에서의 진척도(레벨, 아이템 소유 내역 등) 같은 관계형 데이터를 저장합니다.
- NoSQL에는 실시간으로 변화하고 빠르게 접근해야 하는 데이터나 비정형 데이터를 저장합니다. ex) 실시간 게임 랭킹, 사용자 채팅 기록, 일일 접속 로그, 실시간 이벤트 통계 등
이러한 경우는 RDB와 NoSQL이 서로 다른 데이터를 관리하고 있습니다. NoSQL은 실시간 데이터를 관리하며 빠른 응답을 제공하고, RDB는 중요한 트랜잭션 데이터를 안정적으로 처리하는 구조입니다.
2. 같은 데이터를 캐싱하는 경우
RDB에 기본 데이터를 저장해 두고, NoSQL을 캐싱 레이어로 활용해 자주 사용되는 데이터를 빠르게 제공하는 방식입니다. 이 경우, 두 시스템 간에 같은 데이터가 존재할 수 있게 되고, NoSQL은 일시적으로 데이터를 저장하고 빠른 접근을 위한 캐시로 작동합니다.
이 방식도 온라인 게임 홈페이지로 예를 들면
- RDB에 저장된 사용자의 정보(프로필, 레벨, 아이템 소유 내역 등)를 기반으로, 게임 접속 시 사용자 정보가 자주 필요할 경우 NoSQL로 데이터를 가져와서 캐싱해 둡니다. 이렇게 하면 매번 RDB에서 데이터를 불러오지 않고 NoSQL에서 빠르게 조회가 가능합니다.
- 배치 프로그램을 통해 주기적으로 RDB의 중요한 데이터들을 NoSQL로 동기화하거나, 특정 이벤트 발생 시 NoSQL에 데이터가 캐싱되도록 설정할 수 있습니다. 하루에 한 번 혹은 사용자가 처음 로그인할 때, RDB에서 사용자 데이터를 NoSQL로 캐싱해둡니다.
위와 같이 2가지 방식이 있는데, 많은 경우 이 2가지 방식을 혼용해서 사용하는 경우가 많습니다.
즉, RDB에는 중요한 트랜잭션 데이터를 저장해 두고, NoSQL에는 일부 데이터를 캐시로 저장하거나, 빠른 응답이 필요한 데이터를 저장해 둔다고 생각하면 됩니다.
좀 더 자세한 예시가 궁금하면 이전 글을 참고해 주세요
그럼 이번에는 Spring Boot에서 NoSQL을 RDB의 데이터를 캐싱하게 만들어 보려고 합니다.
Spring Boot는 캐싱을 쉽게 구현할 수 있도록 지원하기 때문에 생각보다 상당히 간단하게 구현할 수 있습니다.
이번 예시에서 NoSQL은 Redis를 사용하려고 하는데요.
다양한 NoSQL에 대한 설명은 다음 글에서 정리하도록 하겠습니다
참고로 밑에서 설명될 `@Cacheable`, `@CacheEvict`는 추상화를 통해 따로 쿼리문 작성 없이 실제 저장소가 Redis이든 Couchbase이든 다른 NoSQL이든 관계없이 동일한 코드로 캐싱을 처리할 수 있습니다.
1. Spring Cache 설정
먼저 Spring Boot의 캐시 기능을 활성화해야 하는데요.
`@EnableCaching`을 사용하여 애플리케이션에 캐싱을 적용해야 합니다.
@SpringBootApplication
@EnableCaching
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
이 설정을 통해 애플리케이션에서 캐시 설정을 활성화합니다. (아래에서 추가하는 의존성 필요)
2. Redis 설정
Redis를 캐시로 사용하기 위한 의존성을 추가해야 하는데요.
캐시로 사용하기 위한 `spring-boot-starter-cache`와 Redis를 사용하기 위한 `spring-boot-starter-data-redis`가 필요합니다.
// Gradle (build.gradle)
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-cache'
// Maven (pom.xml)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
3. Redis 서버 설정
Redis 서버가 필요하기 때문에, 직접 설치하거나 Docker를 사용해 빠르게 설정할 수 있습니다.
Docker를 사용한다면 아래의 명령어를 사용할 수 있습니다.
docker run --name redis -p 6379:6379 -d redis
4. apllication.proeprties 설정
스프링부트와 Redis를 연결하기 위해 설정을 추가합니다.
// application.properties
spring.cache.type=redis
spring.redis.host=localhost
spring.redis.port=6379
5. 캐시 적용
캐싱하고 싶은 서비스 메서드에 `@Cacheable` 어노테이션을 적용하면, 해당 메서드의 결과가 Redis로 캐싱됩니다. 이후 동일한 입력으로 메서드를 호출하면 캐시 된 결과가 반환돼서 성능이 개선되게 됩니다.
@Service
public class GameService {
@Cacheable(value = "gameDataCache", key = "#userId")
public GameData getGameData(String userId) {
// RDBMS에서 게임 데이터를 가져오는 로직
return gameRepository.findGameDataByUserId(userId);
}
}
`@Cacheable`을 붙인 메서드가 호출될 때, 스프링은 먼저 캐시에 해당 데이터가 있는지 확인합니다. 그리고 캐시에 데이터가 있으면 그 데이터를 반환하고, 없으면 RDBMS에서 데이터를 조회한 후 캐시에 저장한 다음 데이터를 반환해 줍니다.
따라서 해당 API를 호출하는 프론트엔드단에서는 신경 쓰지 않고 API를 호출하기만 하면 되고, 캐시 된 데이터를 사용할지, RDB에서 데이터를 가져올지는 백엔드에서 자동으로 결정합니다.
Value에는 캐시의 이름을 넣고, Key는 어떤 기준으로 캐시 할지 지정하는 키입니다.
즉, Redis 내부에서 gameDataCache라는 네임스페이스 아래에, userId가 키로 저장되어 있는 값을 조회하는 것입니다.
Redis의 구체적인 동작 방식에 대해서 좀 더 얘기하려 합니다.
Redis는 전통적인 RDB처럼 테이블이라는 개념이 없이 일종의 태그나 분류를 위한 '네임스페이스'라는 것이 존재합니다. Key-Value 저장소이기 때문에 여러 종류의 데이터를 분류하고 구분하기 위해 접두어를 사용하는 방식입니다.
그리고 네임스페이스 아래에 키를 통해 값이 저장되게 됩니다.
위의 코드를 예시로 들자면
Key는 gameDataCache::userId로 지정되고, Value는 getGameData 메서드의 반환값이 들어가게 됩니다.
예를 들어, userId = "1234"이고, getGameData("1234")가 GameData(name="Player1", level=10)를 반환한다고 가정하면, Redis에는 아래와 같이 저장됩니다.
Key: gameDataCache::1234 Value: {"name": "Player1", "level": 10}
이 데이터는 Redis 내부에서 String 형태로 저장되어 있지만, 실제로는 직렬화된 객체이므로, 스프링에서 userId를 이용해 데이터를 가져올 때 다시 역직렬화해서 Java 객체로 반환해 줍니다.
6. 캐시 무효화 및 갱신
캐싱된 데이터가 변경되거나 유효하지 않을 때는 `@CacheEvict`를 사용해 해당 캐시를 삭제할 수 있습니다.
@CacheEvict(value = "gameDataCache", key = "#userId")
public void updateGameData(String userId, GameData newData) {
// RDBMS에 새로운 데이터 저장
gameRepository.save(newData);
}
`@CacheEvict`는 캐시 된 데이터를 무효화하기 때문에, 다음번에 해당 데이터를 불러올 때는 RDB에서 데이터를 가져와서 캐시를 갱신하게 됩니다.
예를 들어, 사용자가 프로필을 수정하거나 게임 데이터가 업데이트되면 기존의 캐시를 삭제하거나 갱신해주어야 하는데, 이때 `@CacheEvict`를 사용해주어야 합니다.
7. TTL (Time to Live) 설정
캐시 데이터에 TTL (Time to Live) 설정을 통해 일정 시간 후 자동으로 갱신하거나 삭제되도록 할 수 있습니다.
// application.properties
spring.cache.redis.time-to-live=600s # 10분 동안 캐시 유지
8. 캐시 미스 처리
캐시에서 데이터를 찾지 못할 때는 `@Cacheable` 메서드에서 RDB에 데이터를 다시 조회하고 캐시 해주는 로직이 이미 적용되어 있습니다.
또다시 게임 웹페이지에 적용을 해보면
- 게임 프로필 캐싱: 게임 사용자의 프로필 정보를 RDB에서 조회한 후, 자주 변경되지 않는 정보는 Redis에 캐싱해 두고, 빠르게 응답할 수 있도록 설정합니다.
- 실시간 게임 랭킹 캐싱: 실시간 랭킹 데이터는 빠르게 변할 수 있지만, 캐싱을 통해 실시간 랭킹 업데이트 빈도에 맞춰 갱신하면서 성능을 높일 수 있습니다. 예를 들어, 캐시 TTL을 설정해 5분마다 새로운 랭킹 데이터를 RDB에서 가져오도록 설정하는 방식이 있을 수 있습니다.
캐싱에서 중요한 점은 해당 캐시를 얼마나 오래 유지할지, 그리고 언제 RDB에서 데이터를 가져와서 갱신할지 결정하는 것입니다.
이러한 캐싱된 데이터는 위에서 설명했던 TTL 설정이나 캐시 무효화 전략을 통해 제어할 수 있습니다.
1. TTL(Time To Live) 설정 방법
위에서 설명했지만 한번 더 설명하자면,
TTL은 캐시 데이터의 유효 기간을 설정하는 것으로, 설정 시간이 지나면 캐시 된 데이터를 자동으로 무효화해서 RDB에서 다시 최신 데이터를 가져오도록 하는 방식입니다. (설정 시간과 관련된 설정 방법은 위에 적어놓았습니다)
이 방식은 데이터의 변경 빈도에 따라 유연하게 조정할 수 있다는 장점이 있습니다. 자주 바뀌는 데이터는 TTL을 짧게 설정하고, 잘 변하지 않는 데이터는 긴 TTL을 설정해서 최적화할 수 있습니다.
2. 캐시 무효화
캐시 무효화는 특정 이벤트나 조건이 발생할 때 수동으로 캐시를 삭제하거나 갱신하는 방식입니다.
예를 들어, 사용자가 프로필을 수정하거나 게임 데이터를 업데이트하면 해당 데이터를 캐시에서 삭제하거나 갱신해야 하는데, 이렇게 하면 데이터가 변경될 때마다 최신 데이터를 유지할 수 있습니다.
위에서 설명했던 `@CacheEvict` 어노테이션을 사용해서 캐시를 무효화할 수 있습니다.
@Service
public class GameService {
@Cacheable(value = "gameDataCache", key = "#userId")
public GameData getGameData(String userId) {
return gameRepository.findGameDataByUserId(userId);
}
@CacheEvict(value = "gameDataCache", key = "#userId")
public void updateGameData(String userId, GameData newData) {
// RDBMS에 새로운 데이터 저장
gameRepository.save(newData);
}
}
위에 코드에서처럼 `getGameData`처럼 DB에서 가져오는 메서드에서는 `@Cacheable`을 이용한 캐시 설정을 하고, `updateGameData`처럼 데이터를 새롭게 저장하는 메소드에서는 `@CacheEvict`을 통해 현재의 캐시를 무효화합니다.
3. 혼합 방식
TTL과 캐시 무효화 전략을 함께 사용할 수도 있습니다.
예를 들어, 사용자가 프로필을 업데이트할 때는 즉시 캐시를 무효화하고, 그 외에는 TTL을 설정해 일정 시간 후에 자동으로 최신 데이터를 가져오게 하는 방법입니다.
이렇게 하면 데이터의 갱신 빈도와 성능 최적화를 적절히 조화시킬 수 있습니다.
지금까지의 내용을 종합해서 NoSQL을 활용한 캐시의 스프링 부트에서의 흐름은
- 캐시 저장소 설정: Redis, Couchbase 등 캐시 저장소를 설정해 두면, 스프링 부트는 이를 통해 자동으로 캐시 처리를 진행합니다.
- 메서드 호출 시 캐시 확인: `@Cacheable`이 붙은 메서드가 호출되면, 먼저 캐시에 해당 데이터가 있는지 확인하고, 캐시에 데이터가 없으면 원래 메소드 로직을 실행해서 RDB에서 데이터를 조회해서 그 결과를 캐시에 저장합니다.
- 캐시 무효화: `@CacheEvict`가 적용된 메서드를 호출하면, 자동으로 캐시에서 데이터를 삭제하고, 이후 `@Cacheable`이 붙은 메서드가 호출되면 새로운 데이터를 다시 캐싱합니다.
'방구석 컴퓨터 > 방구석 DB' 카테고리의 다른 글
PK(Primary Key)와 UK(Unique Key) (0) | 2024.11.27 |
---|---|
데이터베이스 인덱싱 (1) | 2024.11.20 |
JPA와 프로시저 (1) | 2024.11.13 |
RDB와 NoSQL (0) | 2024.10.23 |