PostgreSQL와의 상호작용을 더 간결하게 만드는 방법

작성자: Manni Wood
작성일: 2025년 6월 30일

코딩 챌린지!

상황 설정:
당신은 psql을 통해 데이터베이스에 접속해 있습니다. 그리고 다음 질문에 답하는 SQL 쿼리를 막 작성해냈습니다:
“성(last_name)이 ‘R’로 시작하는 사용자들의 이름(first name)과 두 자리 국가 코드(country code)를 이름과 국가 코드 순으로 정렬해서 보여줘.”
SQL은 다음과 같습니다:

select u.first_name,
a.country_abbrev
from users as u
join addresses as a on u.address_id = a.id
where u.last_name like 'R%'
order by u.first_name,
a.country_abbrev;

챌린지:
이제 당신이 사용하는 프로그래밍 언어에서 이 쿼리 결과를 프론트엔드로 전달 가능한 형태로 직렬 해제(deserialize)하는 가장 간결한 방법을 찾는 것입니다. (프론트엔드는 HTML, JSON, Protobuf 등일 수 있으므로, 어떤 형식으로든 쉽게 변환할 수 있는 형태면 충분합니다.)

저는 Go 언어를 사용할 건데, 그게 가장 익숙하니까요.

먼저 위 SQL 쿼리를 여러 줄 문자열 리터럴에 담습니다.

query := `
select u.first_name,
a.country_abbrev
from users as u
join addresses as a on u.address_id = a.id
where u.last_name like 'R%'
order by u.first_name,
a.country_abbrev
`

이미 이 시점에서 논쟁의 여지가 있습니다. 지난 25년 동안, 개발자들은 ORM(Object-Relational Mapping)을 “베스트 프랙티스”로 받아들이라는 메시지를 수도 없이 들어왔습니다. 최소한 쿼리 빌더라도 써야 한다고요. “Raw SQL은 안 돼!”

하지만,

  1. 이번 과제는 최대한 간결한 방식으로 구현하는 것이 목표이고,
  2. SQL 쿼리는 이미 완성했고 마감일도 다가오고 있다면, 굳이 더 복잡하게 만들 필요가 있을까요?

이제 코드에서 PostgreSQL에 연결해야 합니다. 먼저 라이브러리가 필요하죠. Go 생태계에서는 pgx가 대표적입니다. (유일한 경쟁자였던 pqpgx 사용을 권장하고 있으니 선택은 명확합니다.)

PostgreSQL 연결하기

EDB Postgres AI의 Hybrid Control Plane에서는 psql 연결 시 URI를 사용하는데, pgx도 이 URI를 직접 사용할 수 있습니다. 제 노트북의 로컬 PostgreSQL 인스턴스에 연결하는 방법은 다음과 같습니다:

pool, err := pgxpool.New(
ctx, "postgresql://postgres:postgres@localhost:5432/postgres")
if err != nil {
log.Fatalf("Unable to establish connection: %v", err)
}

커넥션 풀(pool)을 생성하는 코드와 단일 연결을 생성하는 코드가 같기 때문에, 저는 풀을 선택했습니다.
실제 운영 환경이라면 URI를 하드코딩하지 않습니다. 보안적으로 안전한 방법으로 URI를 전달해야 하고,
연결 실패 시 단순히 에러 로그를 남기고 종료하지 않을 겁니다.
하지만, 이 예제에서는 설명 목적으로 충분합니다.

결과 직렬 해제용 구조체 정의

쿼리 결과는 이름과 두 자리 국가 코드로 구성되어 있으므로, 이에 맞는 struct를 정의해 봅시다:

type NameState struct {
FirstName string
State string
}

이 구조체들의 리스트를 이용해 쿼리 결과를 담을 수 있습니다.

다시 논쟁거리 등장!
이럴 때 ORM으로 사용자(User) 또는 주소(Address) 객체에 매핑해야 하지 않냐는 의견이 있을 수 있습니다.
하지만 이 쿼리는 usersaddresses를 조인한 결과이며, 각 테이블의 일부 필드만 사용하므로
ORM 객체에 매핑하려고 하면 애매해지죠.
ORM은 특정 상황에서는 유용하지만, 이처럼 단순한 시나리오에서는 struct 하나 만드는 게 훨씬 낫습니다.

PostgreSQL은 객체 저장소가 아니라 “답변 엔진(Answer Engine)”

Postgres는 객체 저장소가 아닙니다.
SQL 쿼리를 받아 결과 집합을 반환하는 서비스 API입니다.

실제로 저는 업무 중 psql을 자주 사용하는데요,
단순한 “단발성 질문”의 경우 SQL만 작성해서 바로 답을 얻고, 다음 업무로 넘어갑니다.

하지만 반복적으로 묻는 질문이나, 프론트엔드에서 활용해야 하는 경우에는
이런 질문들을 마이크로서비스의 엔드포인트로 구조화합니다.

gRPC 마이크로서비스를 작성하는 사람이라면,
Postgres의 Q&A 서비스 패러다임은 gRPC의 request-response 메시지 구조와도 아주 잘 어울립니다.

그래서 저는 이 결과에 딱 맞는 NameState 구조체를 사용하는 거고,
ORM 객체를 사용하는 대신 질문에 맞는 목적 구조체로 바로 매핑하는 겁니다.

(참고로 실제 마이크로서비스에서는 중간 struct도 없이 바로 protobuf 메시지에 직렬 해제하기도 합니다.
하지만 여기선 단순화를 위해 NameState struct를 사용했습니다.)

SQL 실행 및 결과 처리

rowFetcher, err := pool.Query(ctx, query)
if err != nil {
log.Fatalf("Unable to run SQL query: %v", err)
}

nameStates, err := pgx.CollectRows(rowFetcher, pgx.RowToStructByPos[NameState])
if err != nil {
log.Fatalf("Unable to collect rows: %v", err)
}

주의할 점은, 별도로 커넥션 풀에서 커넥션을 빌려오거나 반환하는 코드가 없다는 것입니다.
pgx는 이러한 관리 작업을 모두 내부적으로 처리해줍니다.
정상 경로든 오류 경로든 코드가 깔끔해지죠.

또한 직렬 해제 코드도 수작업이 없습니다.
pgx.RowToStructByPos는 쿼리 결과의 컬럼 순서와 struct 필드 순서를 기반으로 자동 매핑합니다.
(필드명을 기준으로 매핑하는 기능도 있지만, 여기선 단순화 차원에서 순서 기반 매핑 사용.)
불필요한 라이브러리 없이도 직렬 해제가 가능해졌습니다.


전체 코드

package main
import (
"context"
"fmt"
"log"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type NameState struct {
FirstName string
State string
}
func main() {
query := `
select u.first_name,
a.state
from users as u
join addresses as a on u.address_id = a.id
where u.last_name like 'R%'
order by u.first_name,
a.state;
`
ctx := context.TODO()
pool, err := pgxpool.New(
ctx, "postgresql://postgres:postgres@localhost:5432/postgres")
if err != nil {
log.Fatalf("Unable to establish connection: %v", err)
}
rowFetcher, err := pool.Query(ctx, query)
if err != nil {
log.Fatalf("Unable to run SQL query: %v", err)
}
nameStates, err := pgx.CollectRows(
rowFetcher, pgx.RowToStructByPos[NameState])
if err != nil {
log.Fatalf("Unable to collect rows: %v", err)
}
for _, nameState := range nameStates {
fmt.Printf("Name: %s, State: %s\n",
nameState.FirstName, nameState.State)
}
}


스크립트 언어는 더 쉬웠다?

Python이나 Ruby 같은 스크립팅 언어를 쓰는 분들은
“이런 건 원래부터 쉽게 할 수 있었지! 이 정도 코딩 과제는 식은 죽 먹기!”라고 생각할 수 있습니다.

하지만 제가 Ruby 대규모 프로젝트를 다룰 때조차도,
이렇게 간결하게 SQL을 직접 쓰지 않고 ActiveRecord 같은 ORM-ish 라이브러리를 사용하고 있었습니다.
할 수 있다고 해서 꼭 그렇게 하진 않는다는 게 흥미로운 점이죠.

Java의 초창기 복잡한 DB 처리 방식이, 오히려 ORM과 쿼리 빌더의 범람을 초래했고,
그 영향은 다른 언어 생태계까지 퍼진 것 같다는 생각도 듭니다.


Go의 영향: “SQL을 그냥 쓰자”

Go의 부상과 함께 좋은 변화 중 하나는 바로
“그냥 SQL 쓰자”는 실용주의 조언이 많아졌다는 점입니다.
Go 같은 언어가 주류가 되면서,
개발자들이 “SQL 직접 써도 된다”는 심리적 자유를 얻은 것이죠.


간결한 코드를 가능하게 한 요소들

성능 중심 언어들이 스크립트 언어의 좋은 점을 받아들이면서 다음 기능들이 보편화되었습니다:

  • 멀티라인 문자열 리터럴: SQL을 코드에 붙여넣기 쉽게 해줍니다. (Java도 2019년 13버전부터 지원!)
  • 자동 직렬 해제 지원: pgx v5에서는 struct와 매핑 함수만 넘기면 자동 직렬 해제가 됩니다.
  • 커넥션 풀과 에러 핸들링의 사용자 친화적 처리: Go만의 특성이 아니라, 모든 언어 생태계가 오랜 시간 다듬어 온 RDBMS 연동 경험의 결과입니다.

마무리

가끔은 기본적인 작업 방식도 다시 돌아보는 게 좋습니다.
PostgreSQL과 상호작용하는 방식, 혹시 너무 복잡하게 하고 있진 않으신가요?
더 간결하게, 더 효율적으로 할 수는 없을까요?


문의 메일: salesinquiry@enterprisedb.com