Databricks와 PostgreSQL로 AI 데이터베이스 디버깅

이 글에는 짧은 영상이 있습니다:TikTok

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는 제공하는 데이터의 품질만큼만 좋습니다. 항상 제안을 검증하세요. 특히 권한 부여나 인프라 변경은 실제 시스템에 적용하기 전에 검증해야 합니다.

databrickspostgresqlaidebuggingspring-boot