-
[SpringBoot] 8주차 외부 API 연동, Redis 연결, 그리고 TTL(Time-To-Live) 설정Study/SpringBoot 2026. 5. 28. 18:07
8주차에서는 7주차까지 진행한 내부 데이터베이스(MySQL) 작업에서 확장하여, 외부 시스템과 데이터를 주고받고 이를 메모리에 캐싱하여 조회 속도를 개선하는 과정을 다룬다.
이번 주차 공부 내용
- 외부 API 선택 및 호출 API 작성
- Redis 연결
- TTL 설정 후 캐시 테스트
기능 구현 Flow(코드 로직)
Step 1. 외부 API 연동: 서버 밖의 데이터 가져오기 (RestTemplate)
지금까지는 서버 내부의 DB(MySQL) 데이터만 다루었다면, 이제는 외부 시스템의 데이터를 우리 서버로 끌어와야 한다. 오픈 API(JSONPlaceholder)를 활용해 외부 데이터를 조회하는 로직을 구현했다.
- DTO Design: 외부 API의 JSON 응답 규격에 맞추어 데이터를 받아낼 ExternalTodoResponse 객체를 설계한다.
- Client Request: 클라이언트가 GET /api/external/todo/1 형태로 서버에 요청을 보낸다.
- External Call: 서비스 계층에서 스프링이 제공하는 RestTemplate을 활용해 외부 URL로 HTTP GET 요청을 날리고, 응답받은 JSON을 DTO로 즉시 맵핑하여 반환한다.
// ExternalApiService.java 중 일부 @Slf4j @Service @RequiredArgsConstructor public class ExternalApiService { private final RestTemplate restTemplate = new RestTemplate(); public ExternalTodoResponse getExternalTodo(Long id) { // 실제 메서드가 실행되는지 확인하기 위한 로그 log.info("외부 API를 호출합니다. ID: {}", id); String url = "https://jsonplaceholder.typicode.com/todos/" + id; return restTemplate.getForObject(url, ExternalTodoResponse.class); } }
JSONPlaceholder 란?실무에서는 네이버 로그인, 카카오페이 결제, 날씨 정보 등 외부 서버(다른 회사의 컴퓨터)에서 데이터를 가져와야 할 일이 생기므로 개발자들이 API 연동 테스트를 할 수 있도록, 누군가가 무료로 열어둔 Dummy API이다.사용법: 인터넷 브라우저 주소창에 https://jsonplaceholder.typicode.com/todos/1 이라고 치면, 진짜 서버가 응답하는 것처럼 아래와 같은 JSON 데이터를 뱉는다.{ "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false }
이제 서버는 외부의 유용한 데이터를 마음껏 가져와 클라이언트에게 제공할 수 있게 되었다. 하지만 외부 API 호출은 네트워크 환경에 따라 지연(Latency)이 발생할 수밖에 없다는 치명적인 단점이 존재한다.
이 Latency를 최소화하고 서버를 안전하게 지키는 방법에는 크게 3가지가 있다고 한다.
1. 커넥션 풀(Connection Pool) 유지하기 (가장 기본)
기본적으로 제공되는 new RestTemplate()은 외부 API에 요청할 때마다 매번 새로운 네트워크 연결(TCP 3-Way Handshake)을 맺고 끊는다. 이 과정에서 많은 지연 시간이 발생한다.
해결책: 미리 연결 통로(커넥션)를 여러 개 뚫어두고 재사용하는 커넥션 풀링(Connection Pooling)을 적용해야 한다. 스프링에서는 주로 Apache HttpClient를 주입하여 해결한다.
// 외부 라이브러리(Apache HttpClient) 의존성이 필요합니다. @Bean public RestTemplate restTemplate() { // 커넥션 풀 설정이 포함된 팩토리 사용 HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(); // 이 팩토리를 RestTemplate에 장착 return new RestTemplate(factory); }2. 타임아웃(Timeout) 설정 (서버 다운 방지)
만약 JSONPlaceholder 서버가 고장 나서 응답을 10초 동안 안 주면 내 서버도 그 10초 동안 스레드가 기다린다. 이런 요청들이 쌓이면 결국 내 서버까지 함께 터진다.
해결책: 제한된 시간 안에 대답을 안 하면 끊는 Fail-Fast(빠른 실패) 전략을 취해야 한다.
아래는 타임아웃 전략이다.
① Connect Timeout (연결 타임아웃)
- 의미: 내 서버가 외부 서버에 연결망 자체를 구축하는 데 기다려주는 시간이다.
- 적정 시간: 보통 1초 ~ 3초 내외로 아주 짧게 잡는다. 요즘 같은 통신 환경에서 3초 안에 연결조차 안 된다는 건 외부 서버가 완전히 죽었거나 네트워크망에 장애가 났다는 것이므로 기다려봤자 소용이 없으니 빨리 끊어버리는 게 낫다.
② Read Timeout (읽기 타임아웃)
- 의미: 연결은 성공했는데, 상대방 서버가 데이터를 처리하고 최종 결과물(JSON)을 돌려줄 때까지 기다려주는 시간이다.
- 적정 시간: 외부 API가 무슨 일을 하느냐에 따라 천차만별이다.
- 1~3초: JSONPlaceholder처럼 단순히 텍스트 하나 던져주는 가벼운 조회 API일 때.
- 5~10초 이상: 외부 서버에서 AI 이미지를 생성해서 주거나, 복잡한 결제 처리를 거쳐야 하는 무거운 API일 때. (이런 경우 3초로 잡으면 외부 서버는 정상인데 내 서버가 끊어버리는 에러가 발생한다.)
@Bean public RestTemplate restTemplate() { SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); // 1. 연결 자체를 맺는 데까지 기다려주는 시간 (예: 3초) factory.setConnectTimeout(3000); // 2. 연결 후, 데이터를 읽어오는 데까지 기다려주는 시간 (예: 3초) factory.setReadTimeout(3000); return new RestTemplate(factory); }3. RestTemplate 대신 WebClient 사용 (최신 트렌드)
RestTemplate은 동기/블로킹(Synchronous/Blocking) 방식이다. 즉, 외부 API에 요청을 보내면 응답이 올 때까지 내 서버의 스레드가 멈춰 있는 것이다.
해결책: 스프링에서 강력하게 밀고 있는 WebClient로 갈아타는 것이다. 이는 비동기/논블로킹(Asynchronous/Non-blocking) 방식이라, 외부 API에 요청을 던져놓고 스레드는 다른 일을 한다. 응답이 도착하면 그때 다시 와서 처리하므로 대규모 트래픽에서 압도적인 속도와 효율을 낸다.
// WebClient를 사용한 비동기 외부 API 호출 예시 @Service public class ExternalApiService { private final WebClient webClient = WebClient.create("https://jsonplaceholder.typicode.com"); public Mono<ExternalTodoResponse> getExternalTodo(Long id) { return webClient.get() .uri("/todos/{id}", id) .retrieve() .bodyToMono(ExternalTodoResponse.class); // 결과를 비동기로 받아옴 } }(참고: 스프링 공식 문서에서도 RestTemplate은 유지보수 모드에 들어갔으며, 새로운 프로젝트에는 WebClient 사용을 권장하고 있다.)
정리하자면, 외부 서버와의 지연 시간을 줄이려면 1) 통로를 열어두고(Connection Pool), 2) 오래 걸리면 가차 없이 끊어내며(Timeout), 궁극적으로는 3) 기다리지 않고 다른 일을 하는(WebClient) 방향으로 코드를 개선해 나가야 한다.
Step 2. Redis 캐시 인프라 구축: 메모리와 TTL 설정
외부 API를 매번 호출하는 것은 서버 속도를 깎아먹는 주범이다. 한 번 가져온 데이터는 속도가 미친 듯이 빠른 인메모리(RAM) 저장소인 Redis에 임시로 저장(Caching)해 두기로 했다.
- Dependency & Config: build.gradle과 application.yml에 Redis 연결 설정을 추가한다.
- Enable Caching: 설정 클래스 상단에 @EnableCaching을 붙여 스프링의 캐시 기능을 깨운다.
- Serialization & TTL: 데이터베이스와 달리 한정된 메모리를 사용하는 Redis가 터지는 것(OOM)을 막고 데이터의 최신화(정합성)를 유지하기 위해, 데이터의 수명인 TTL(Time-To-Live)을 1분으로 설정했다. 또한 데이터가 깨지지 않도록 JSON 형태로 포장(직렬화)하는 설정을 추가했다.
// RedisConfig.java 중 일부 @Configuration @EnableCaching // 스프링 캐시 기능 활성화 public class RedisConfig { @Bean public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() // 캐시 데이터의 수명(TTL)을 1분으로 제한 .entryTtl(Duration.ofMinutes(1)) // 데이터를 JSON 포맷으로 직렬화하여 저장 .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); return RedisCacheManager.builder(connectionFactory) .cacheDefaults(config) .build(); } }
Redis (초고속 임시 기억 장치)란?
Redis(레디스)는 실무 백엔드 인프라에서 절대 빠지지 않는 엄청나게 중요한 인메모리(In-Memory) 데이터베이스이다. 기존에 쓰던 MySQL과 비교하면 이해하기 쉽다.
- MySQL (하드디스크): 크고 무거운 지하 창고이다. 전원이 꺼져도 데이터가 안전하게 보관되지만, 매번 창고 문을 열고 물건을 찾아오는 데 시간이 제법 걸린다. (영구 저장용)
- Redis (메모리/RAM): 책상 위에 올려둔 포스트잇이나 자주 쓰는 서랍이다. 컴퓨터 메모리를 사용하기 때문에 읽고 쓰는 속도가 MySQL과는 비교도 안 되게 미친 듯이 빠르다. 단, 컴퓨터 전원이 꺼지면 포스트잇이 날아가듯 데이터가 사라진다. (임시 저장용)
Step 3. @Cacheable을 활용한 조회 성능 극대화 (Cache Hit & Miss)
인프라 구축이 끝났으니, 실제로 외부 API를 호출하는 메서드에 캐시를 적용하여 속도 개선을 이뤄낼 차례다.
- Cache Application: ExternalApiService의 메서드 상단에 @Cacheable 어노테이션을 부착한다. 캐시 저장소 이름은 todoCache로, 데이터를 찾을 열쇠(Key)는 파라미터로 넘어온 #id 값으로 지정했다.
- Cache Miss (최초 요청): 처음 1번 게시물을 요청하면 Redis에 데이터가 없으므로, 메서드 내부로 진입하여 외부 API를 호출하고 결과를 Redis에 저장한다. (콘솔에 로그가 찍힘)
- Cache Hit (연속 요청): 1분 안에 똑같은 1번 게시물을 요청하면, 스프링이 가로채서 메서드 안으로 들어가지 않고 Redis 메모리에서 데이터를 0.001초 만에 꺼내서 반환한다. (콘솔에 로그가 찍히지 않음)
// 외부 API 호출 메서드에 캐시 적용 @Cacheable(value = "todoCache", key = "#id") public ExternalTodoResponse getExternalTodo(Long id) { // 캐시에 데이터가 있으면 이 메서드는 아예 실행되지 않으므로 로그도 찍히지 않는다! log.info("외부 API를 호출합니다. ID: {}", id); String url = "https://jsonplaceholder.typicode.com/todos/" + id; return restTemplate.getForObject(url, ExternalTodoResponse.class); }프론트엔드 입장에서는 똑같은 응답이지만, 백엔드 내부에서는 무거운 네트워크 요청을 생략하고 메모리에서 데이터를 즉각 반환하는 고도의 최적화가 이루어지게 되었다.
코드에 사용된 문법(어노테이션) 및 객체 정리
Step 1. 외부 API 연동: 내 서버 밖의 데이터 가져오기
핵심 코드: ExternalApiService의 HTTP 통신 객체
private final RestTemplate restTemplate = new RestTemplate(); // 외부 API 호출 및 DTO 변환 return restTemplate.getForObject(url, ExternalTodoResponse.class);🔍 주요 어노테이션 및 객체 분석
- RestTemplate: 스프링에서 제공하는 가장 대표적인 동기식(Synchronous) HTTP 통신 클라이언트다. 복잡한 HTTP 연결 설정이나 스트림 처리 과정 없이, 직관적인 메서드 몇 개만으로 외부 서버와 데이터를 주고받을 수 있게 해준다.
- getForObject(String url, Class<T> responseType): 지정된 URL로 GET 요청을 보내고, 응답으로 돌아온 JSON 데이터를 우리가 지정한 자바 객체(DTO) 타입으로 즉시 변환(역직렬화)해 주는 핵심 메서드다. 직접 JSON 파싱을 할 필요가 없어 코드가 획기적으로 짧아진다.
Step 2. Redis 캐시 인프라 구축: 메모리의 마법과 TTL 설정
핵심 코드: RedisConfig의 캐시 매니저 설정
@EnableCaching public class RedisConfig { @Bean public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(1)) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); // ... 생략 } }🔍 주요 어노테이션 및 객체 분석
- @EnableCaching: 스프링 애플리케이션에 캐시 기능을 사용하겠다고 선언하는 마스터 스위치다. 이 어노테이션이 없으면 서비스 계층에 캐시 어노테이션을 아무리 붙여도 스프링이 이를 무시한다.
- entryTtl(Duration): Redis는 빠른 대신 용량이 적은 메모리(RAM)를 사용하므로, 데이터가 무한정 쌓이면 서버가 터지는 OOM(Out Of Memory)이 발생할 수 있다. 이를 방지하고 외부 데이터와의 최신화(정합성)를 맞추기 위해, 캐시 데이터의 수명인 TTL(Time-To-Live)을 제한하는 필수 설정이다.
- GenericJackson2JsonRedisSerializer: 스프링의 자바 객체를 Redis로 넘길 때(직렬화), 기본 형태인 바이트(Byte) 배열 대신 사람이 읽을 수 있는 JSON 포맷으로 변환하여 저장하도록 지시한다. 이를 통해 Redis CLI 등으로 직접 데이터를 조회할 때 글자가 깨지지 않고 구조화된 형태로 확인할 수 있다.
Step 3. @Cacheable을 활용한 조회 성능 극대화
핵심 코드: ExternalApiService의 메서드 캐시 적용
@Cacheable(value = "todoCache", key = "#id") public ExternalTodoResponse getExternalTodo(Long id) { ... }🔍 주요 어노테이션 및 객체 분석
- @Cacheable: 캐시 최적화의 꽃이다. 메서드 호출 시 스프링이 먼저 중간에 개입하여 Redis에 동일한 데이터가 있는지 확인한다. 데이터가 있다면(Cache Hit) 메서드 실행 자체를 건너뛰고 메모리에서 즉시 값을 반환하며, 없다면(Cache Miss) 메서드를 실행하고 그 결과를 Redis에 새로 저장한다.
- value (또는 cacheNames): Redis 내부에 데이터가 저장될 논리적인 그룹(폴더/파티션)의 이름이다. 여기서는 todoCache라는 영역을 할당했다.
- key: 그룹 내에서 특정 데이터를 식별하기 위한 고유 키값이다. #id와 같이 스프링 표현식(SpEL, Spring Expression Language)을 사용하면, 메서드의 파라미터로 넘어온 변수 값을 동적으로 키값에 매핑할 수 있다. (예: todoCache::1)
실행 결과
🟢 1. 최초 요청 (Cache Miss)
스웨거에서 로그인 이후 GET /api/external/todo/1을 호출했다.
결과: 외부 API(JSONPlaceholder)를 요청하여 데이터를 가져왔고, 응답 코드는 200
검증 포인트: 인텔리제이 콘솔 창에 ExternalApiService에 걸어둔 log.info 메시지("외부 API를 호출합니다. ID: 1")가 있다. 즉, 메서드가 정상적으로 끝까지 실행되었고 데이터가 방금 Redis 메모리에 캐싱(저장)되었다는 뜻이다.

GET /api/external/todo/1 🟢 2. 1분 이내 연속 요청 (Cache Hit)
방금 요청했던 똑같은 1번 ID 요청을 1분(설정한 TTL)이 지나기 전에 여러 번 클릭해 보았다.
결과: 스웨거에는 첫 번째와 똑같은 JSON 응답이 0.001초 만에 즉각적으로 나타났다.
검증 포인트: 인텔리제이 콘솔 창에 더 이상 로그가 찍히지 않는다. 스프링의 @Cacheable이 개입하여 외부 API를 호출하는 메서드 자체를 건너뛰고, 메모리(Redis)에서 데이터를 바로 꺼내서 반환했다는 것이다.
🟢 3. TTL 만료 후 재요청
1분이 지난 뒤 다시 똑같은 1번 ID를 요청했다.
결과: 인텔리제이 콘솔 창에 다시 "외부 API를 호출합니다. ID: 1" 로그가 찍혔다. 캐시의 수명(TTL)이 다해서 자동으로 꺼졌기 때문에, 서버가 다시 외부로 나가 최신 데이터를 가져와 캐시를 갱신한 것이다. 데이터의 정합성과 메모리 관리가 완벽하게 통제되고 있었다.

외부 API를 호출한 모습
트러블 슈팅 (Troubleshooting)
Issue 1. 인텔리제이의 취소선과 Serialization의 늪
[현상] RedisConfig 클래스를 작성하며 데이터를 JSON 포맷으로 바꾸기 위해 GenericJackson2JsonRedisSerializer를 적용했다. 그런데 인텔리제이가 해당 클래스에 노란색 경고(Warning)를 띄웠다.
[원인 분석] 에러 메시지에서 이 클래스는 "Spring Boot 4.0 이상에서 지원 중단되며 제거될 예정"이라는 사전 경고(Deprecation)였다. 이는 스프링 부트가 Jackson 3 기반의 새로운 클래스를 준비 중이므로 미리 알려준 것일 뿐, 현재 실무와 학습 환경인 Spring Boot 3.x 버전에서는 기능상 정상 작동하는 코드였다.
[해결 방안 및 배운 점] 기능에 전혀 문제가 없다는 것을 깨달았다. 다만 취소선을 방치하기 싫어서, cacheManager 메서드 상단에 @SuppressWarnings("deprecation") 어노테이션을 달아 프레임워크의 경고를 숨김 처리했다. 만약 기본값(JdkSerializationRedisSerializer)을 썼다면 Redis CLI로 데이터를 까봤을 때 \xac\xed\x00... 같은 설정을 봤을 것이고, JSON 직렬화 설정 덕분에 메모리에 깔끔한 데이터가 저장되는 것을 확인했다.
Issue 2. Connection Refused
[현상] 의존성과 캐시 설정을 마치고 스프링 서버를 Run 했는데, 톰캣이 켜지자마자 빨간색 에러 로그가 터지며 서버가 강제 종료되었다. 에러의 핵심 문구는 RedisConnectionFailureException: Unable to connect to Redis였다.
[원인 분석] 기초적인 이유로 오류가 발생한 거였다. application.yml에 스프링더러 localhost:6379로 가서 Redis와 연결하라고 명령만 해두고, 정작 로컬 환경에서 Redis 서버 자체를 켜두지 않은 것이다.
[해결 방안 및 배운 점] 터미널에 docker run -p 6379:6379 -d redis 명령어를 통해 로컬 환경에서 6379 포트를 물고 있는 Redis 서버 컨테이너를 띄웠다. 이후 스프링 서버를 다시 실행하니 에러 없이 서버가 부팅되었다. 외부 인프라(DB, Redis 등)와 연동하는 백엔드 서버를 띄울 때는, 반드시 그 인프라가 켜져 있는지 확인하는 습관을 들여야 함을 느꼈다.
학습 회고
과거 프론트엔드에서 API 속도를 높이려면 번들 크기를 줄이거나 렌더링 횟수를 최적화하는 데 집중했다. 하지만 이번 8주차 미션을 통해, 백엔드에서 네트워크 통신을 Cache Hit 하는 것이 얼마나 큰 속도 향상을 가져오는지 체감했다.
또한 무한정 캐싱하지 않고 적절한 TTL을 부여해 '데이터의 신선도'와 '서버 메모리 용량' 과정은 꽤나 매력적이었다. 외부 서버가 다운되어도 내 서버의 커넥션 풀과 타임아웃을 통해 장애를 방어하고, 1분 동안은 캐시로 꿋꿋이 응답을 내려줄 수 있는 튼튼한 시스템. 이것이 진짜 백엔드 아키텍처의 시작이 아닐까 싶다.
'Study > SpringBoot' 카테고리의 다른 글
[SpringBoot] 10주차 AWS 클라우드 배포, CI/CD 자동화 및 HTTPS 무중단 보안 적용 (0) 2026.06.10 [SpringBoot] 9주차 테스트 코드(JUnit/Mockito) 자동화 및 Docker 가상화 인프라 배포 (0) 2026.06.06 [SpringBoot] 7주차 게시글 + 댓글 동시 저장 서비스 작성, 예외 케이스 적용, 인덱스 생성 (0) 2026.05.20 [SpringBoot] 6주차 페이지네이션(Pagination), N+1 문제 해결 (0) 2026.05.07 [SpringBoot] 5주차 JWT 인증/인가(회원가입 및 로그인) API 구현 (0) 2026.04.30