🟣 연결 하나 여는 데 드는 비용
앞 장까지 서버는 요청을 받아 DB에 읽고 쓰는 형태를 갖췄다. 그런데 "DB에 접근한다"는 한 줄 뒤에는 생각보다 무거운 과정이 있다. 애플리케이션이 DB와 통신하려면 먼저 연결(connection)을 하나 열어야 하는데, 이 열기가 공짜가 아니다. TCP 연결을 맺고, 사용자 인증을 거치고, DB 쪽에 세션을 위한 자원을 잡는 절차가 매번 필요하다.
요청마다 이 연결을 새로 열고 닫으면, 실제 쿼리 시간보다 연결을 준비하는 시간이 더 걸리는 상황이 생긴다. 초당 수백 건이 들어오는 서버라면 이 낭비가 그대로 응답 지연으로 쌓인다. 앱에서도 요청마다 새로 소켓을 여는 대신 연결을 재사용하는 편이 빠른데, 서버와 DB 사이에도 연결을 매번 새로 만들지 말고 미리 열어 두고 돌려쓰자는 같은 발상이 필요하다.
🟣 커넥션 풀: 연결을 미리 열어 돌려쓴다
커넥션 풀은 애플리케이션이 뜰 때 DB 연결을 여러 개 미리 열어 한곳에 담아 두는 장치다. 요청이 DB가 필요하면 풀에서 연결 하나를 빌려 쓰고, 끝나면 닫지 않고 풀에 반납한다. 반납된 연결은 다음 요청이 다시 빌려 간다. 연결을 여는 비용은 시작할 때 한 번만 치르고, 이후로는 빌리고 반납하는 값싼 동작만 반복한다.
스프링부트는 기본 커넥션 풀로 HikariCP를 쓴다. 별도 설정 없이도 켜지고, 풀 크기 같은 값만 조정한다.
🚀 풀 크기는 클수록 좋은 게 아니다
풀 크기를 정할 때 처음엔 크게 잡을수록 많은 요청을 동시에 처리할 것 같았다. 실제로는 반대다. DB가 동시에 효율적으로 처리할 수 있는 연결 수에는 한계가 있고, 그보다 많은 연결을 열면 DB 안에서 자원 경쟁과 컨텍스트 전환이 늘어 오히려 전체 처리량이 떨어진다.
그래서 풀 크기는 DB가 감당할 수 있는 선에서, 대개 코어 수를 기준으로 잡은 크지 않은 값으로 둔다. 연결을 늘리는 것과 DB가 실제로 병렬 처리할 수 있는 양은 다르다는 걸 구분하는 게 핵심이다. 병목이 애플리케이션이 아니라 DB에 있을 때, 연결만 늘리면 문제를 DB 쪽으로 밀어 넣을 뿐이다.
🟣 트랜잭션이 연결을 붙잡는 시간
커넥션 풀에서 자주 겪는 문제는 연결 고갈이다. 풀에 연결이 열 개인데 요청이 그보다 많이 몰려 전부 빌려 간 상태가 되면, 다음 요청은 연결이 반납될 때까지 기다린다. 대기가 정해진 시간을 넘기면 예외가 나면서 요청이 실패한다.
여기서 앞 장의 트랜잭션이 다시 등장한다. @Transactional 메서드는 시작할 때 풀에서 연결을 하나 빌려, 메서드가 끝날 때까지 그 연결을 붙잡고 있는다. 그래서 트랜잭션 안에서 외부 API를 호출하거나 무거운 계산을 하면, 정작 DB 작업은 짧은데 연결은 그 긴 시간 내내 묶여 있다. 이런 메서드가 몇 개만 겹쳐도 풀이 금세 마른다.
그래서 트랜잭션은 꼭 필요한 DB 작업만 감싸 짧게 유지하고, 외부 호출처럼 오래 걸리는 일은 트랜잭션 밖으로 빼는 게 원칙이다. 트랜잭션 범위를 좁게 잡는 판단이 곧 연결을 아끼는 판단으로 이어진다.
🟣 DB 세션이라는 반대쪽 끝
연결의 애플리케이션 쪽 끝이 커넥션 풀이라면, DB 쪽 끝에는 세션이 있다. 클라이언트가 연결을 하나 열면 DB는 그에 대응하는 세션을 만들어, 그 연결 위에서 오가는 작업의 상태를 관리한다. 진행 중인 트랜잭션, 잡고 있는 잠금, 임시 설정 같은 것이 세션에 묶인다.
이 세션 개념은 낯선 게 아니다. 학부에서 리눅스를 공부할 때 프로세스가 연결마다 상태를 들고 자원을 붙잡던 그 구조와 닮았다. DB도 연결 하나하나에 자원을 할당하기 때문에, 연결을 무한정 열 수 없고 그래서 애플리케이션 쪽에서 풀로 수를 통제한다. 양쪽 끝이 서로의 자원 한계를 맞춰 주는 관계인 셈이다.
세션이 붙잡은 자원은 트랜잭션이 끝나거나 연결이 반납될 때 정리된다. 만약 트랜잭션을 커밋도 롤백도 하지 않고 연결을 오래 쥐고 있으면, DB 세션은 잠금을 계속 붙든 채 남아 다른 세션을 막는다. 커넥션 풀에서 연결을 짧게 쓰라는 원칙이 DB 세션 쪽에서도 같은 이유로 중요해지는 지점이다.
🟣 정리
- DB 연결을 여는 데는 TCP·인증·세션 할당 비용이 든다. 요청마다 새로 열면 그 비용이 응답 지연으로 쌓인다.
- 커넥션 풀은 연결을 미리 열어 두고 빌려주고 반납받는다. 여는 비용은 시작할 때 한 번만 치른다. 스프링부트는 HikariCP를 기본으로 쓴다.
- 풀 크기는 클수록 좋은 게 아니다. DB의 병렬 처리 한계를 넘으면 오히려 처리량이 준다. 연결 수와 DB의 처리 능력은 다르다.
- 트랜잭션은 시작부터 끝까지 연결을 붙잡는다. 트랜잭션을 짧게 유지하고 외부 호출을 밖으로 빼는 것이 연결 고갈을 막는 기본이다.
- DB 세션은 연결의 반대쪽 끝에서 자원을 잡는다. 연결을 짧게 쓰라는 원칙은 풀과 세션 양쪽에서 같은 이유로 성립한다.
여기까지가 스프링부트로 서버를 이해하려고 밟은 길이었다. 객체를 컨테이너가 조립하고, JPA가 객체와 DB를 잇고, 인증이 요청의 신원을 가리고, 커넥션 풀이 그 많은 요청을 연결 몇 개로 버티게 한다. 앱에서 API를 호출하고 토큰을 실어 보내던 동작 하나하나가 서버의 어느 지점과 만나는지를 양쪽에서 그려 볼 수 있게 된 것이, 이 공부로 얻은 가장 큰 소득이었다.