봉황대 in CS

[Spring] LazyConnectionDataSourceProxy로 DB connection 요청 줄이기 본문

Server/Spring

[Spring] LazyConnectionDataSourceProxy로 DB connection 요청 줄이기

등 긁는 봉황대 2025. 1. 31. 23:15

문제 상황


이벤트 목록을 조회하는 API를 구현하는 중이었다. 이벤트 목록은 자주 조회되는 데이터라, 매 순간마다 DB에 요청을 보내는 것은 비합리적이다. 한번에 얻을 수 있는 DB connection의 개수는 한정적이기 때문에 DB에게 부하를 많이 주게 되면 connection을 얻기 위해 기다리는 시간이 늘어날 수 밖에 없고, 이벤트 목록 조회 뿐만 아니라 다른 요청들도 DB에게 가게 된다면 그 파급 효과가 더욱 커지게 된다. 따라서 Redis에 캐싱해두는 전략을 사용하여 DB connection 요청을 줄이고자 하였다. (+ 이벤트 목록은 모든 사용자에게 동등하게 보이는 Global 데이터이기 때문에 Redis cache에 올려두어도 문제가 없다.)

 

조회 로직은 다음과 같다.

1. Redis에 캐싱된 데이터가 없는 경우,
    1) DB에 조회 요청을 보내어 데이터를 받아온다.
       이때 분산 lock을 통해서 한 thread 만이 데이터를 받아오도록 한다.
    2) 데이터를 Redis에 캐싱한다.
    3) 데이터를 반환한다.

2. Redis에 캐싱된 데이터가 존재하는 경우, 그 데이터를 바로 반환한다.

 

만약 이벤트 목록에 갱신이 발생했거나 TTL이 만료돼서 캐시가 invalid 되는 상황이 발생하지 않는다면, DB connection은 Redis에 데이터를 올리는 시점 단 한번만 요청될 것이다. 초기 상태에는 Redis에 아무 데이터도 올라가 있지 않기 때문이다.

 

 

그러나 .. Thread 100개가 이벤트 목록을 조회하도록 부하 테스트를 걸어놓고 모니터링을 해보니 위의 예상과는 다르게 DB connection을 모든 thread가 요청하고 있는 것을 볼 수 있었다. Redis cache 도입 목적을 달성하지 못하고 있는 것이다.

 

 

문제 발생 원인 : Transaction 기본 동작 방식


아래 코드에서 문제의 원인을 발견할 수 있다.

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class EventService {

    private final EventsCacheService eventsCacheService;

    private final EventRepository eventRepository;

    public GetEventsResponse getActiveEventsByRedisCache(LocalDateTime now) {
        List<Event> activeEvents = eventsCacheService.getActiveEvents(now);
        return GetEventsResponse.of(activeEvents);
    }
}

 

바로바로 @Transactional이 걸려져 있기 때문이다.

 

Spring에서는 기본적으로 Transaction에 진입하는 순간, 설정된 DataSource의 connection을 바로 가져오게 된다. (참고로 DataSource는 DB connection을 획득하는 방법을 추상화한 인터페이스로, SpringBoot가 기본적으로 사용하는 구현체는 HikariCP(https://www.baeldung.com/hikaricp)이다.) 따라서 실제로 DB connection을 사용하고 있지 않는데도 계속해서 점유하고 있는 문제가 발생하게 되고, 지금 상황이 바로 이 문제 상황인 것이다.

 

해결법 : LazyConnectionDataSource 사용하기


DataSource 구현체를 기본 HikariCP 말고 LazyConnectionDataSource로 사용하면 된다. LazyConnectionDataSource는 실제로 Connection이 필요한 시점에서야 DB connection을 획득하도록 요청한다.

 

[참고] https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/datasource/LazyConnectionDataSourceProxy.html

- Proxy for a target DataSource, fetching actual JDBC Connections lazily, i.e. not until first creation of a Statement.

- This DataSource proxy allows to avoid fetching JDBC Connections from a pool unless actually necessary.

 

@Configuration
public class DataSourceConfig {

    @Value("${spring.datasource.url}")
    private String url;

    @Value("${spring.datasource.username}")
    private String username;

    @Value("${spring.datasource.password}")
    private String password;

    @Value("${spring.datasource.driver-class-name}")
    private String driverClassName;

    @Bean
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(url);
        config.setUsername(username);
        config.setPassword(password);
        config.setDriverClassName(driverClassName);
        return new LazyConnectionDataSourceProxy(new HikariDataSource(config));
    }
}

 

DataSource 변경 후 다시 테스트를 해보니, DB connection을 획득하여 요청을 보내는 횟수가 의도한대로 딱 한 번으로 찍혔다. ^0^

 

 

 

반응형
Comments