본문 바로가기
Framework/Spring Boot

[Spring Boot] Redis 캐싱(Caching)을 이용하여 응답 속도 단축하기

스타트업이 처음 런칭했을 때는 유저가 적어 데이터베이스(RDB) 하나만으로도 충분히 서비스가 잘 돌아갑니다. 하지만 마케팅이 대박 나서 '초당 1만 건'의 요청이 쏟아지기 시작하면 상황은 달라집니다.
여러분이 앞선 에서 HikariCP 커넥션 풀을 아무리 완벽하게 튜닝해 놓았더라도, 물리적인 디스크(Disk)를 긁어서 데이터를 읽어오는 관계형 데이터베이스(MySQL, Oracle)는 태생적으로 속도의 한계가 있습니다.
특히 '쇼핑몰 메인 페이지의 배너 이미지 목록'이나 '오늘의 인기 검색어'처럼 수많은 유저가 똑같이 조회하지만, 데이터는 하루에 한두 번만 변하는 정보를 매번 느린 디스크 DB까지 내려가서 읽어오는 것은 엄청난 리소스 낭비입니다.

이를 해결하기 위해 백엔드 진영이 도입한 궁극의 마법, "디스크가 아니라 초고속 메모리(RAM)에 데이터를 띄워두고 바로바로 던져주자!"는 철학이 바로 캐싱(Caching) 아키텍처이며, 그 중심에 서 있는 1인자가 바로 Redis(레디스)입니다.

1. 메모리와 디스크의 압도적인 속도 차이

컴퓨터 공학의 진리 중 하나는 "메모리(RAM)는 디스크(SSD/HDD)보다 수만 배 빠르다"는 것입니다. 일반적인 MySQL 데이터베이스는 데이터를 영구적으로 보존하기 위해 하드디스크에 기록합니다. 반면 Redis(Remote Dictionary Server)는 Key-Value 구조의 데이터를 영구 보존이 아닌 '초고속 입출력'을 목적으로 메모리 위에 띄워놓는 In-Memory 데이터 스토어입니다. 속도가 워낙 빠르기 때문에 트래픽이 몰리는 병목 지점에 캐시 층(Layer)으로 끼워 넣으면 응답 속도가 문자 그대로 10배 이상 비약적으로 상승합니다.

 

2. Spring Boot의 우아한 캐시 추상화 (@Cacheable)

과거에는 캐시를 쓰려면 비즈니스 로직 사이에 if (redis.get("key") == null) { DB조회... } 같은 지저분한 코드를 덕지덕지 발라야 했습니다. 하지만 스프링 부트는 AOP(관점 지향 프로그래밍)를 활용하여, 비즈니스 로직을 전혀 건드리지 않고 어노테이션 단 하나로 캐싱을 적용하는 경이로운 추상화를 제공합니다.

[설정 1: 의존성 주입 및 활성화]

// build.gradle에 Redis 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
@EnableCaching //  Application 클래스나 설정 클래스에 이 한 줄을 달면 마법이 시작됩니다.
@SpringBootApplication
public class DemoApplication { ... }

[설정 2: 서비스 코드에 어노테이션 적용]

@Service
@RequiredArgsConstructor
public class NoticeService {

    private final NoticeRepository noticeRepository;

    //  이 메서드가 호출될 때, 스프링이 가로채서 Redis에 'noticeList'라는 키(Key)가 있는지 뒤집니다.
    // 캐시에 데이터가 있으면(Cache Hit) DB 조회를 건너뛰고 바로 반환합니다!
    // 캐시에 데이터가 없으면(Cache Miss) DB에서 꺼내와서 반환함과 동시에 Redis에 예쁘게 저장해 줍니다!
    @Cacheable(cacheNames = "noticeList")
    public List<NoticeResponse> getTopNotices() {
        // 이 코드는 캐시가 비어있을 때만 실행됩니다.
        return noticeRepository.findTop10ByOrderByCreatedAtDesc();
    }
    
    // 관리자가 공지사항을 수정하면 어떻게 할까요? 기존 캐시를 날려버려야 합니다(Cache Eviction).
    @CacheEvict(cacheNames = "noticeList", allEntries = true)
    public void updateNotice(Long id, NoticeRequest request) {
        noticeRepository.save(newNotice); // DB 수정 후 'noticeList' 캐시를 깔끔하게 날려버립니다.
    }
}

 

3. 캐싱 도입 시 주의할 실무 장애 포인트 (캐시 스탬피드)

캐시를 붙였다고 무조건 안심하면 안 됩니다. 실무에서 가장 자주 터지는 대형 장애가 바로 Cache Stampede(캐시 스탬피드, 캐시 붕괴) 현상입니다.

새벽 3시에 Redis에 저장해 둔 '메인 배너 캐시'의 수명(TTL)이 다해서 펑 하고 만료(Expired)되었다고 칩시다. 하필 그 순간 10,000명의 유저가 동시에 메인 페이지를 새로고침했습니다. 10,000개의 쓰레드가 Redis를 찔렀는데 데이터가 없으니까(Cache Miss), 10,000명이 동시에 똑같은 쿼리를 들고 DB로 달려가버립니다. 평소 캐시가 막아주던 부하가 한순간에 DB로 쏟아지며 서버가 완전히 뻗어버리는 대참사입니다.

해결책: 스프링 캐시 추상화의 @Cacheable(sync = true) 옵션을 주면 됩니다. 10,000명이 몰려와도 단 1개의 쓰레드만 락(Lock)을 쥐고 DB에 다녀오게 만들고, 나머지 9,999명은 얌전히 기다렸다가 갱신된 캐시를 받아 가도록 통제하여 DB 폭파를 완벽히 막아낼 수 있습니다.

 

4. 마무리 및 주요 발전 방향

지금까지 느린 디스크(DB)의 병목을 초고속 메모리 스토어로 대체하여 응답 속도를 비약적으로 상승시키고, 어노테이션 기반의 우아한 AOP 캐싱 적용법과 끔찍한 캐시 붕괴(Stampede)를 막는 sync=true 실무 노하우까지 다루어 보았습니다. 이제 여러분의 서버는 어떠한 트래픽 폭풍이 몰아쳐도 끄떡없을 것입니다.

서버 성능 튜닝의 양대 산맥(커넥션 풀, 캐싱)을 모두 정복했습니다. 이제 기능도 완벽하고 성능도 훌륭합니다. 그런데, 누군가 아무나 회원 정보를 삭제하는 API를 마구 호출한다면 어떻게 될까요? 지금까지 우리는 문을 활짝 열어놓고 장사를 하고 있었습니다. 이제 애플리케이션의 '보안(Security)'이라는 거대하고 무거운 철문을 달아줄 시간입니다. 본 설명에서는 "전 세계에서 가장 강력하지만 악명 높게 복잡한 보안 프레임워크!" Spring Security의 아키텍처와 핵심 원리, 그리고 폼 로그인(Form Login) 기반의 인증 체계에 대해 아주 뼈 때리게 파헤쳐 보겠습니다!


웹 서비스를 운영하다 보면 "로그인한 회원만 글을 쓸 수 있게 해라", "관리자(Admin) 권한이 있는 사람만 이 페이지에 접근하게 해라"와 같은 요구사항이 쏟아집니다. 초보 개발자들은 컨트롤러(Controller)의 모든 메서드 첫 줄에 if (session.getAttribute("user") == null) throw new Error(); 같은 코드를 복붙하며 지옥을 맛보곤 합니다. 이처럼 중복되는 보안(Security) 검사 로직을 애플리케이션의 핵심 비즈니스 로직과 완벽하게 분리하여, 개발자가 비즈니스에만 집중할 수 있게 해주는 전 세계에서 가장 강력하고 튼튼한 방패가 바로 Spring Security(스프링 시큐리티)입니다. 하지만 그 강력함만큼이나 초기 진입 장벽이 악명 높게 높은 것으로도 유명합니다.

.

1. 필터 체인(Filter Chain): 요청을 검열하는 만리장성

스프링 시큐리티의 동작 원리를 관통하는 가장 핵심적인 키워드는 서블릿 필터(Servlet Filter)입니다. 클라이언트(브라우저)가 /api/boards라는 주소로 요청을 쏘면, 이 요청은 곧바로 스프링의 @Controller로 가지 않습니다. 컨트롤러 앞에 쳐져 있는 거대한 그물망, 즉 10~15개 정도의 보안 필터들이 한 줄로 이어져 있는 '필터 체인'을 순서대로 거치게 됩니다.

대표적으로 UsernamePasswordAuthenticationFilter는 폼 로그인을 가로채서 ID/PW가 맞는지 검사하고, FilterSecurityInterceptor는 "이 녀석이 이 페이지에 들어갈 권한(Role)이 있는가?"를 마지막으로 검사합니다. 통과하지 못하면 401(미인증) 또는 403(권한없음) 에러를 뱉어내며 요청을 쫓아내 버립니다. 이 덕분에 컨트롤러에는 보안 코드가 단 한 줄도 들어갈 필요가 없어집니다!


2. 인증(Authentication)과 인가(Authorization)의 명확한 차이

시큐리티를 배울 때 가장 헷갈리는 두 단어를 확실히 정리해야 합니다.

  • 인증 (Authentication - "너 누구냐?"): 신원을 확인하는 과정입니다. 아이디와 비밀번호를 입력하고 "나 홍길동이야!"라고 증명하는 로그인 과정이 바로 인증입니다.
  • 인가 (Authorization - "너 여기 들어갈 자격 있어?"): 인증이 완료된 후의 단계입니다. 홍길동이 로그인은 했지만, VVIP 회원 전용 페이지에 들어가려고 할 때 "너 일반 회원(USER)이잖아? 여긴 관리자(ADMIN)만 들어와!"라고 막아버리는 권한 검사 과정입니다.

3. Spring Boot 3.x Security 실무 설정 코드 (SecurityFilterChain)

과거 Spring Boot 2.x 시절에는 WebSecurityConfigurerAdapter를 상속받아 세팅했지만, 3.x부터는 이 클래스가 완전히 폐기(Deprecated)되고 컴포넌트 기반의 빈(Bean) 등록 방식으로 전면 개편되었습니다. 아주 깔끔해진 현대의 설정 코드를 봅시다.

@Configuration
@EnableWebSecurity // 스프링 시큐리티 필터 체인을 활성화하는 핵심 마법
public class SecurityConfig {

    // 1. 단방향 비밀번호 암호화 인코더 빈 등록 (회원가입 시 무조건 이걸로 암호화해서 DB에 넣어야 함!)
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 2. 가장 중요한 보안 룰(Rule) 세팅
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // 최신 람다식(Lambda) 문법 사용 강제 (Boot 3.x 특징)
            .csrf(csrf -> csrf.disable()) // 보통 REST API 서버를 만들 땐 CSRF 방어를 끄는 것이 편함
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**", "/login", "/signup").permitAll() // 이 주소들은 아무나 통과!
                .requestMatchers("/api/admin/**").hasRole("ADMIN") // 여기는 관리자 권한 필수!
                .anyRequest().authenticated() // 그 외의 모든 쩌리(?) 요청들은 일단 로그인은 하고 와라!
            )
            .formLogin(form -> form
                .loginPage("/login") // 우리가 만든 예쁜 커스텀 로그인 HTML 페이지 주소 지정
                .loginProcessingUrl("/perform_login") // 프론트가 form action="/perform_login"으로 POST 날리면 시큐리티가 가로챔!
                .defaultSuccessUrl("/home") // 로그인 성공하면 갈 곳
            )
            .logout(logout -> logout
                .logoutUrl("/logout") // 로그아웃 요청 주소
                .logoutSuccessUrl("/login?logout") // 로그아웃 성공 시 갈 곳
                .invalidateHttpSession(true) // 로그아웃하면 세션 메모리 싹 날려라!
            );
            
        return http.build();
    }
}

4. 마무리 및 주요 발전 방향

지금까지 지저분한 if문 도배를 끝내고, 컨트롤러에 닿기 전에 거대한 필터 체인(Filter Chain) 방어막을 쳐서 완벽하게 '인증(로그인)'과 '인가(권한 검사)'를 수행해 내는 Spring Boot 3.x 기반의 Spring Security 폼 로그인(Form Login) 아키텍처에 대해 다루어 보았습니다. 이제 웹 페이지(HTML)를 그리는 전통적인 방식의 보안은 끄떡없습니다.

하지만 치명적인 문제가 있습니다. 현대의 서비스는 서버에서 HTML을 그리지 않고, React, Vue 프론트엔드나 iOS, Android 모바일 앱과 통신하는 'REST API' 서버로 동작합니다. 모바일 앱 환경에서는 폼 로그인의 핵심인 '쿠키(Cookie)'와 '세션(Session)'을 유지하기가 극도로 까다로우며, 서버를 여러 대 두었을 때 세션 정보가 찢어지는 현상도 발생합니다. 이 무거운 상태(Stateful) 기반의 세션을 던져버리고, 가벼운 토큰(Token) 하나로 만사를 해결하는 완벽한 무상태(Stateless) 보안 아키텍처로 넘어가야 할 때입니다.