Databricks와 PostgreSQL로 AI 데이터베이스 디버깅
1. 개요
애플리케이션이 Databricks의 PostgreSQL에 연결할 때(Lakebase 또는 Lakehouse Federation을 통해) 오류가 발생하면, 스택 트레이스, HikariCP 로그, EXPLAIN ANALYZE를 수동으로 읽는 데 많은 시간이 소요됩니다. Databricks의 통합 AI Assistant는 로그, SQL 쿼리, 연결 풀 구성을 읽어 원인을 제안하고 직접적인 해결 방법을 제시할 수 있습니다.
이 기사는 AI 데이터베이스 디버깅 메커니즘의 작동 방식을 설명한 다음, 일반적인 PostgreSQL 오류 상황 3가지(연결 풀 고갈, N+1 쿼리, 권한 부족)를 Spring Boot 코드와 함께 다루어 재현하고 AI의 제안을 검증할 수 있게 합니다. 읽은 후에는 AI(또는 직접) 진단을 더 빠르고 정확하게 할 수 있도록 로그와 쿼리를 준비하는 방법을 알게 될 것입니다.
2. Databricks Postgres(Lakebase)란 무엇인가
Lakebase는 Databricks 워크스페이스 내에서 실행되는 관리형 PostgreSQL이며, OLAP용 Delta Lake 외에 OLTP(트랜잭션) 워크로드에 사용됩니다. 백엔드 애플리케이션은 Lakebase에 일반적인 PostgreSQL처럼 연결됩니다. JDBC를 통해 Databricks에서 제공하는 호스트/포트/데이터베이스로 연결합니다.
2.1. Spring Boot에서 연결
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
spring.datasource.url=jdbc:postgresql://<workspace>.lakebase.databricks.com:5432/orders_db
spring.datasource.username=app_user
spring.datasource.password=${DB_PASSWORD}
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.connection-timeout=3000
이것이 최소 구성입니다. maximum-pool-size와 connection-timeout은 부분 4의 디버깅 사례에서 계속 나타날 두 파라미터입니다. AI는 보통 여기서부터 분석을 시작합니다.
3. Databricks의 AI 데이터베이스 디버깅은 어떻게 작동하나
3.1. AI가 읽는 데이터 소스
Databricks Assistant는 무작위로 ’추측’하지 않습니다. 세 가지 소스를 읽습니다:
- Stack trace / exception log - 작업 또는 노트북에서
- Query plan (
EXPLAIN ANALYZE) - 느린 쿼리에 대해 물을 때 - Schema와 metadata (테이블, 인덱스, 제약 조건) - SQL의 유효성을 확인하기 위해
다시 말해, AI는 로그가 충분히 상세할 때만 좋습니다. "Connection is not available"만 출력하는 예외는 현재 풀 크기와 활성 연결 수를 포함하는 로그보다 훨씬 더 모호한 제안을 제공합니다.
3.2. 일반적인 디버깅 흐름
1. Databricks Assistant에 스택 트레이스 또는 오류 SQL을 붙여넣습니다.
2. AI는 오류 유형을 식별합니다: 연결 풀, 구문, 권한, 잠금, 시간 초과...
3. AI는 워크스페이스에 있는 현재 스키마/메타데이터와 비교합니다.
4. AI는 fix를 제안합니다: 설정 변경, SQL 수정, 인덱스 추가, GRANT 실행...
5. fix를 적용하고 다시 실행하면 AI가 깨끗한 로그를 확인합니다.
다음 섹션은 이 흐름을 세 가지 구체적인 사례로 재현합니다.
4. 실습: 오류 재현 및 디버깅 제안 읽기
4.1. 데이터 준비
CREATE TABLE customers (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL
);
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
customer_id BIGINT REFERENCES customers(id),
total NUMERIC(12,2) NOT NULL,
created_at TIMESTAMP DEFAULT now()
);
그리고 해당 엔티티:
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;
private BigDecimal total;
}
4.2. Case 1 — 연결 풀 고갈
다음 서비스는 EntityManager를 수동으로 열었지만 닫지 않아 연결 누수가 발생합니다:
@Service
public class OrderReportService {
@PersistenceContext
private EntityManager entityManager;
public List<Order> findAllOrdersUnsafe() {
EntityManager em = entityManager.getEntityManagerFactory()
.createEntityManager(); // 절대 close()하지 않음
return em.createQuery("SELECT o FROM Order o", Order.class)
.getResultList();
}
}
이 API를 계속 호출하면 다음 로그가 생성됩니다:
java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available,
request timed out after 3000ms.
이 스택 트레이스를 application.properties 파일과 함께 AI에 제공하면, 반환되는 제안은 보통:
- 풀 크기(10)가 주요 원인이 아닙니다. 문제는 연결이 풀로 반환되지 않는다는 것입니다.
- createEntityManager()/getConnection()을 수동으로 호출하는 모든 위치를 검토하십시오.
- try-with-resources를 사용하거나 Spring이 트랜잭션을 관리하도록 하십시오.
올바른 수정은 EntityManager를 수동으로 만드는 것을 완전히 제거하고 표준 repository를 사용하는 것입니다:
public interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findAll();
}
@Service
public class OrderReportService {
private final OrderRepository orderRepository;
public OrderReportService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public List<Order> findAllOrdersSafe() {
return orderRepository.findAll(); // Spring이 연결을 자동으로 관리합니다
}
}
주의: 이 오류가 발생할 때
maximum-pool-size를 늘리는 것은 잘못된 방향입니다. 실제 연결 누수를 수정하는 대신 문제를 지연시킬 뿐입니다.
4.3. Case 2 — N+1 쿼리로 인한 성능 저하
public List<OrderSummary> summarize(List<Order> orders) {
return orders.stream()
.map(o -> new OrderSummary(o.getId(), o.getCustomer().getName())) // 지연 로드
.toList();
}
customer가 FetchType.LAZY이므로 각 요소는 customers에 대한 별도의 쿼리를 생성합니다. Databricks 로그에서 EXPLAIN ANALYZE를 실행하면 수백 번 보일 것입니다:
SELECT * FROM customers WHERE id = $1;
AI가 이 쿼리 계획을 읽을 때 보통 다음을 지적합니다:
- 하나의 요청 내에서 200개의 쿼리가 동일한 SELECT ... WHERE id = $1 패턴을 반복합니다.
- lazy loading에 JOIN FETCH가 없어서 N+1 의심.
- 제안: JOIN FETCH 또는 @EntityGraph를 사용하여 1개의 쿼리로 병합하세요.
JPQL JOIN FETCH로 수정:
@Query("SELECT o FROM Order o JOIN FETCH o.customer")
List<Order> findAllWithCustomer();
결과: N+1개의 개별 쿼리 대신 하나의 SELECT ... JOIN customers ... 문만 남습니다.
4.4. Case 3 — 권한 부족(permission denied)
SELECT * FROM orders WHERE total > 1000;
ERROR: permission denied for table orders
AI는 Databricks의 역할 메타데이터를 확인하고 적절한 GRANT 제안을 반환합니다:
GRANT SELECT ON orders TO app_user;
주의: 프로덕션에서 실행하기 전에 항상 GRANT 제안을 확인하십시오. AI는 내부 보안 정책을 알 수 없으며 쿼리 실행에 필요한 구문만 압니다. 필요한 최소 수준을 초과하여 권한을 부여하지 마세요(최소 권한 원칙).
5. fix를 확인하는 테스트 작성
case 4.3에서 N+1을 수정한 후, 회귀가 없는지 확인하기 위해 테스트가 필요합니다:
@DataJpaTest
class OrderRepositoryTest {
@Autowired
private OrderRepository orderRepository;
@Autowired
private TestEntityManager entityManager;
@Test
void shouldFetchOrdersWithCustomerInOneQuery() {
Customer customer = new Customer();
customer.setName("Nón Lá Dev");
entityManager.persist(customer);
Order order = new Order();
order.setCustomer(customer);
order.setTotal(new BigDecimal("150.00"));
entityManager.persist(order);
entityManager.flush();
List<Order> orders = orderRepository.findAllWithCustomer();
assertThat(orders)
.hasSize(1)
.first()
.extracting(o -> o.getCustomer().getName())
.isEqualTo("Nón Lá Dev");
}
}
이 테스트는 직접적으로 ‘쿼리 수를 세지’ 않지만, Hibernate 통계(spring.jpa.properties.hibernate.generate_statistics=true)와 결합하면 실행된 쿼리 수를 기록하고 1과 같은지 확인할 수 있습니다. 이것이 AI가 fix가 효과적인지 확인하는 데 사용하는 로그 유형입니다.
6. AI 디버깅을 더 효과적으로 하기 위한 모니터링
AI(또는 나중에 로그를 읽을 동료)가 빠르게 진단하도록 하려면 단순히 e.printStackTrace() 대신 구조화된 로그를 작성하세요:
@RestControllerAdvice
public class DatabaseExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(DatabaseExceptionHandler.class);
@ExceptionHandler(DataAccessException.class)
public ResponseEntity<String> handle(DataAccessException ex) {
log.error("db_error_type={}, sql_state={}, message={}",
ex.getClass().getSimpleName(),
extractSqlState(ex),
ex.getMostSpecificCause().getMessage());
return ResponseEntity.status(503).body("Database error, retry later");
}
private String extractSqlState(DataAccessException ex) {
if (ex.getCause() instanceof SQLException sqlEx) {
return sqlEx.getSQLState();
}
return "unknown";
}
}
키-값 형식의 로그(db_error_type=, sql_state=)는 순수 텍스트 스택 트레이스보다 파싱하기 훨씬 쉬우며, Databricks Assistant에 붙여넣어 ’이 오류는 어디서 나왔나?’라고 물을 때 이상적인 입력입니다.
7. AI 디버깅을 사용하면 안 되는 경우
- 민감한 데이터와 관련된 오류: 실제 고객 데이터가 포함된 로그를 AI 어시스턴트에 붙여넣지 마세요. 내부 데이터도 마찬가지입니다. 먼저 마스킹하세요.
- 인프라 수준의 성능 문제 (디스크 I/O, 리전 간 네트워크 지연): AI는 쿼리 계획을 읽을 수 있지만 물리적 인프라 계층을 볼 수 없어서 잘못된 제안을 할 수 있습니다(예: 원인이 네트워크인데 ’인덱스 추가’라고 제안).
- 일시적 단일 오류: 예외가 네트워크 중단으로 인해 한 번만 발생하면, AI로 디버깅하는 것보다 재시도하고 계속 모니터링하는 것이 더 시간 효율적입니다.
- 코드 리뷰 대체: AI의 GRANT 또는 풀 크기 변경 제안은 인프라 권한이 있는 사람이 다시 확인해야 하며, 자동으로 프로덕션에 적용해서는 안 됩니다.
8. 결론
Databricks의 AI 데이터베이스 디버깅은 스택 트레이스, 쿼리 계획, 메타데이터를 빠르게 읽어 원인을 파악하는 데 강점이 있습니다. 연결 누수, N+1 쿼리, 권한 부족이 세 가지 매우 일반적인 경우이며, 이 기사에서 Spring Boot와 PostgreSQL로 재현했습니다. 실제 가치는 구조화된 로그를 작성하고 fix를 확인하는 테스트를 작성하는 데 있습니다. AI는 제공하는 데이터의 품질만큼만 좋습니다. 항상 제안을 검증하세요. 특히 권한 부여나 인프라 변경은 실제 시스템에 적용하기 전에 검증해야 합니다.