
📚 목차
🧩 Http Session과 Session Clustering
HttpSession
- HTTP 요청은 상태를 저장하지 않아(STATELESS) 누가 몇 번째로 보낸 요청인지 서버에 요청할 때 마다 알려줘야 함
- 이렇게 사용자의 데이터를 저장하는 기술을 세션이라고 하고, 서버에서 작성해서 응답을 받은 브라우저가 저장하는 데이터를 쿠키라고 함
- 서버는 저장한 정보를 기반으로 브라우저가 누구인지 구분할 수 있음
SessionController
@RestController
public class SessionController {
@GetMapping("/set")
public String set(
@RequestParam("q")
String q,
HttpSession session
) {
session.setAttribute("q", q);
return "Saved: " + q;
}
@GetMapping("/get")
public String get(
HttpSession session
) {
return String.valueOf(session.getAttribute("q"));
}
}
http://localhost:8080/set?q=password

http://localhost:8080/get

- 여기서 JSESSIONID는 Spring Boot에 내장된 Tomcat 서버에서 생성한 것이다.
서버 Scale-Out 확장에 따른 문제점
- Spring Boot 서버를 여러 개의 포트로 바꿔서 올리면(Scale-Out) 세션 정보를 서버 내부에서 관리하기 어려워짐
- 예: 포트 8080에서
/set을 하고 포트 8081에서/get을 하면 찾을 수 없음

Sticky Session
- 특정 사람이 보낸 요청을 하나의 서버로 고정하는 방법
- 로드 밸런서를 통해 요청을 보낸 사용자를 기록하고 해당 사용자가 다시 요청을 할 경우 최초로 요청이 전달된 서버로 요청을 전달하는 방식
- 예: Alex가 처음에 서버 A에 세션을 만들면, 로드밸런서는 Alex의 요청을 전부 서버 A로 보냄
- 특정 서버로 보내진 사용자만 활발하게 활동한다면 요청이 균등하게 분산되지 않는다는 문제가 있음
- 예: A, B, C서버에 대해서 B서버의 사용자들만 활발히 서비스를 사용하다가 B서버가 다운된다면?
Session Clustering
- 여러 서버들이 하나의 저장소를 공유하고 해당 저장소에 세션에 대한 정보를 저장함으로서 요청이 어느 서버로 전달이 되든 세션 정보가 유지될 수 있도록 하는 방법
- 서버가 세션을 생성하되 이 세션에 연결된 정보를 외부 저장소에 저장함
- 서버 상태에 따라 세션이 사라질 위험은 없지만, 외부 저장소를 추가로 관리해야 하고 지연이 생길 수 있음
Spring Session
- Spring Boot와 Redis를 함께 쓰면 쉽게 Session Clustering을 사용가능함
- Spring Session Data Redis를 추가하면 내장 Tomcat의 세션 기능을 사용하지 않고 Redis에 별도로 세션을 저장하게 됨
implementation 'org.springframework.boot:spring-boot-starter-session-data-redis'
- 포트 8080에서 만든 세션을 포트 8081에서도 확인할 수 있음 (쿠키에는 JSESSIONID 대신 SESSION이 생성됨)

- Redis 저장소를 보면 세션이 저장된 모습을 볼 수 있음

RedisConfig:springSessionDefaultRedisSerializer메서드를 추가해 읽을 수 있는 형태로 저장
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return RedisSerializer.json();
}

🧩 리더보드와 Sorted Set
리더보드란?
- 실시간 랭킹을 보여주는 기능
- 예: 게임 순위, 실시간 검색 순위, 인기상품
- 관계형 DB로 구현하려면 복잡한 JOIN과 함께 SUM, COUNT 등의 집계함수도 필요함
SELECT i.id, SUM(o.count)
FROM item i
INNER JOIN orders o
ON i.id = o.item_id
GROUP BY i.id
ORDER BY SUM(o.count) DESC
LIMIT 10;
- 이를 피하기 위해 물품 테이블에 구매횟수 컬럼을 추가한다 해도 결국 해당 칼럼을 빈번하게 수정해야 하며 데이터를 조회하는 과정에서 정렬이 필요하기 때문에 성능 저하가 일어남
- 반면 Redis의 Sorted Set은 데이터를 추가하거나 조회하는 행위의 시간복잡도가 log에 비례하기 때문에 구현도 쉽고 편하게 진행할 수 있다.
RedisConfig
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, ItemDto> rankTemplate (
RedisConnectionFactory redisConnectionFactory
) {
RedisTemplate<String, ItemDto> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
template.setKeySerializer(RedisSerializer.string());
template.setValueSerializer(RedisSerializer.json());
return template;
}
}
ItemService
@Slf4j
@Service
@Slf4j
@Service
public class ItemService {
private final ItemRepository itemRepository;
private final OrderRepository orderRepository;
// RedisTemplate으로 Sorted Set을 사용한다면 ZSetOperations가 필요함
private final ZSetOperations<String, ItemDto> rankOps;
public ItemService(
ItemRepository itemRepository,
OrderRepository orderRepository,
RedisTemplate<String, ItemDto> rankTemplate
) {
this.itemRepository = itemRepository;
this.orderRepository = orderRepository;
this.rankOps = rankTemplate.opsForZSet();
}
public void purchase(Long id) {
Item item = itemRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
orderRepository.save(ItemOrder.builder()
.item(item)
.count(1)
.build());
rankOps.incrementScore(
"soldRanks",
ItemDto.fromEntity(item),
1
);
}
public List<ItemDto> getMostSold() {
Set<ItemDto> ranks = rankOps.reverseRange("soldRanks", 0, 9);
if (ranks == null) return Collections.emptyList();
return ranks.stream().toList();
}
}
http://localhost:8080/items/{id}/purchase로 여러 번 POST 요청을 보낸 후 Redis에 soldRank라는 Sorted Set이 생긴 것을 볼 수 있다.

http://localhost:8080/items/ranks로 GET 요청을 보내면 리더보드를 List 형태로 볼 수 있다.

🧩 캐싱 개념과 캐싱 전략
캐시란?
- 캐시: 빈번히 접근하게 되는 데이터를 저장해두는 CPU 내부의 임시 기억 장치
- 영속성을 위해 파일시스템(SSD)에 저장
- 빠른 활용을 위해 메모리(RAM)에 저장
- 빈번하게 사용되는 휘발성 데이터를 캐시에 저장
- 캐싱: 빈번하게 접근하게 되는 DB의 데이터를 Redis 등의 In-Memory DB에 저장함으로서 데이터를 조회하는데 걸리는 시간과 자원을 감소시키는 기술
- 예: 자주 바뀌지 않는 이미지 등을 브라우저 캐시에 저장해 페이지 로드를 줄이는 것
- RESTful 설계 원칙 중 응답이 캐싱이 가능한지 명시해야 한다는 제약사항이 존재함
캐싱 전략
- 캐시에 저장한 데이터는 언제든 사라질 수 있기 때문에 너무 크지 않게 관리되어야 함
- 캐시를 확인했을 때 필요한 데이터가 있을수도 있고 없을 수도 있음
- 캐시 적중(Cache Hit): 캐시에 접근했을 때 찾고 있는 데이터가 있는 경우
- 캐시 누락(Cache Miss): 캐시에 접근했을 때 찾고 있는 데이터가 없는 경우
- 삭제 정책: 캐시에 공간이 부족할때 어떻게 공간을 확보하는지에 대한 정책
- 캐시에 찾는 데이터가 있을지 없을지는 캐시에 접근하기 전까지는 알기 어렵기 때문에, 어떤 데이터를 얼마나 오래 캐시에 보관할지에 대한 전략을 잘 세워 적중률을 높이고 누락을 최소화해야 함
Cache-Aside (Lazy Loading)
- 데이터를 조회할 때 항상 캐시를 먼저 확인하는 전략
- 캐시에 데이터가 있으면 캐시에서 데이터를 가져옴
- 캐시에 데이터가 없으면 원본에서 데이터를 가져온 뒤 캐시에 저장함
- 애플리케이션이 캐시와 DB를 모두 직접 제어하며 읽기 성능을 극대화하는 방식
- 필요한 데이터만 캐시에 보관됨
- 최초로 조회할 때 캐시를 확인하기 때문에 최초의 요청은 상대적으로 오래 걸림
- 원본 데이터를 확인하지 않아 데이터가 최신이라는 보장이 없음
Write-Through
- 데이터를 작성할때 항상 캐시에 작성하고 원본에도 작성하는 전략
- 캐시에 저장되는 데이터가 최신이 보장됨
- 자주 사용하지 않는 데이터도 캐시에 중복해서 작성하기 때문에 시간이 오래 걸림
Write-Behind
- 캐시에만 데이터를 작성하고, 일정 주기로 원본을 갱신하는 방식
- 쓰기가 잦은 상황에 DB의 부하를 줄일 수 있음
- 캐시 서버가 다운되면 아직 DB에 동기화되지 않은 최신 데이터가 영구적으로 유실될 위험이 있음
| 전략 | 실무 적용 사례 | 장점 | 단점 |
|---|---|---|---|
| Cache-Aside | 일반적인 웹/앱 API, 상품 정보, 사용자 프로필 | 유연한 구조, 캐시 장애 시 방어 가능 | 초기 조회 속도 저하, 정합성 관리 필요 |
| Write-Through | 결제 정보, 재고 상태 등 정합성이 필수인 도메인 | 완벽한 데이터 일관성 | 쓰기 성능 저하 |
| Write-Behind | 조회수/좋아요 카운트, 대규모 로그 및 통계 | 쓰기 성능 극대화, DB 병목 해소 | 데이터 유실 위험, 복잡한 구현 |
'내일배움캠프' 카테고리의 다른 글
| [내일배움캠프] Redis 실습 (0) | 2026.05.11 |
|---|---|
| [내일배움캠프] Spring Boot에 캐싱 적용하기 (0) | 2026.05.10 |
| [내일배움캠프] Redis (1) | 2026.05.07 |
| [내일배움캠프] Github Actions와 CI/CD (0) | 2026.05.06 |
| [내일배움캠프] CI/CD와 AWS ECS (0) | 2026.05.05 |