Code Place에서 PostgreSQL 연결을 확인하던 중, 접속 자체는 되는데 쓰기 쿼리만 실패하는 상황을 겪었다. SELECT는 되는데 INSERT를 날리면 cannot execute INSERT in a read-only transaction 오류가 나왔다.
처음에는 애플리케이션 코드의 트랜잭션 설정을 의심했다. @Transactional(readOnly = true)가 잘못 적용됐는지, 드라이버 설정이 이상한지, 권한 문제인지부터 떠올렸다.
하지만 원인을 확인할수록 핵심은 애플리케이션 코드가 아니라 연결 대상이었다. 지금 애플리케이션이 실제로 쓰기 가능한 primary에 연결되어 있는지부터 확인해야 했다. 이 글은 CloudNativePG 환경에서 RW·RO 엔드포인트를 구분한 과정이다.
처음 보인 문제
처음 이 문제를 마주했을 때 가장 이상했던 것은 접속 자체는 된다는 점이었다. 아예 연결되지 않는다면 네트워크나 인증 문제를 의심하면 된다. 그런데 이번에는 연결은 됐고, 읽기 쿼리도 돌아갔다.
문제는 쓰기만 안 된다는 점이었다.
이때는 연결이 된다는 사실 때문에 판단이 더 어려웠다. DB 접속 자체는 성공했기 때문에 설정이 맞는 것처럼 보였지만, 실제로는 잘못된 대상에 연결되어 있을 수 있다.
원인을 확인한 과정
CloudNativePG를 쓰면 PostgreSQL 인스턴스를 단순히 Pod 몇 개로만 다루지 않는다. primary와 replica를 구분하고, 읽기/쓰기 트래픽을 어느 엔드포인트로 보낼지에 따라 서비스도 나눠준다.
운영하면서 특히 자주 확인한 것은 이런 서비스였다.
postgres-rw: primary에 대한 read-write 엔드포인트postgres-ro: replica를 대상으로 하는 read-only 엔드포인트postgres-r: 읽기 목적 전체 대상으로 보는 엔드포인트
처음에는 이름만 다르고 역할은 비슷해 보일 수 있다. 하지만 어떤 서비스 이름을 사용하느냐에 따라, 애플리케이션이 연결되는 대상과 쓰기 가능 여부가 달라진다.
해결 방안
replica는 기본적으로 primary의 상태를 따라가는 읽기 전용 복제본이다. 복제 구조를 유지해야 하므로 replica에 직접 쓰기를 허용하지 않는다. 그래서 replica에 연결된 상태에서 INSERT를 날리면 read-only transaction 오류가 나는 것이 자연스럽다.
처음에는 이 상황이 DB 장애처럼 보였다. 하지만 확인해보니 DB가 손상된 것이 아니라, 읽기 전용 대상에 쓰기 쿼리를 보낸 상황에 가까웠다.
적용 후 달라진 점
이후로는 데이터베이스 연결을 하나의 상태로만 판단하지 않는다.
예전에는 연결 성공이면 DB 정상, 연결 실패면 네트워크나 인증 문제 정도로 단순하게 판단했다. 하지만 실제 운영에서는 아래 항목을 분리해서 확인해야 했다.
- 연결은 되는가
- 읽기는 되는가
- 쓰기는 되는가
- 현재 연결 대상이 primary인가 replica인가
- 내가 사용한 서비스 엔드포인트가 원래 의도와 맞는가
즉, "연결된다"는 사실만으로는 모자랐다.
헷갈렸던 이유
이 문제를 혼동하기 쉬운 이유도 비교적 분명했다.
첫 번째는 접속 자체가 된다는 점이다. 인증도 되고 세션도 열리니, 처음에는 연결 구성이 맞는 것처럼 보였다. 하지만 실제로는 잘못된 서비스나 replica에 연결되어 있을 수 있다.
두 번째는 오류 문구가 트랜잭션 옵션 문제처럼 보인다는 점이다. read-only transaction이라는 표현 때문에, 애플리케이션 코드에서 @Transactional(readOnly = true) 같은 설정을 먼저 떠올리기 쉽다.
세 번째는 오퍼레이터가 엔드포인트를 여러 개 만든다는 점이다. 추상화는 편리하지만, 각 서비스의 역할을 모르면 오히려 판단이 어려워졌다. 특히 운영 초반에는 -rw, -ro, -r가 이름만 다른 서비스처럼 보일 수 있었다.
마무리
이 일을 겪은 뒤에는 쓰기 쿼리가 실패해도 곧바로 애플리케이션 코드부터 확인하지 않는다. 먼저 사용 중인 서비스 엔드포인트가 무엇인지, 그 엔드포인트가 primary를 가리키는지 replica를 가리키는지 확인한다.
이번 일은 큰 장애는 아니었지만, 데이터베이스 연결을 판단하는 기준은 분명히 바뀌었다. 연결 성공 여부만으로는 충분하지 않았고, primary/replica 구조에서는 연결 대상의 역할 확인이 우선이었다.