Loading the catalog…
Loading the catalog…
“Java 객체를 바꾼 일이 어떻게 DB의 메모리와 파일 변경으로 이어지는가” 먼저 가장 중요한 기준부터 잡겠습니다. Java 객체 변경, SQL 실행, 트랜잭션 커밋, 데이터 파일 저장은 서로 다른 사건입니다. 이 네 가지가 구분되면 flush , MVCC, 로그, 잠금, ddl-auto , 무중단 변경을 하나의 흐름으로 이해할 수 있습니다. 아래는 MySQL 8.4·MariaDB 11.4의 InnoDB, PostgreSQL 17의 기본 heap 테이블 을 기준으로 설명합니다. 소스 함수 이름은 MySQL 8.4.0·MariaDB 11.4.2·PostgreSQL 17.0에서 확인한 이름입니다. 최신 버전이라는 뜻이 아니라, 구현을 비교하기 위한 기준입니다. 1. 전체 그림: 요청 하나가 어디까지 내려가는가? 다음과 같은 계좌 테이블을 예로 들겠습니다. 금액은 계산을 단순하게 하기 위해 정수 단위를 사용합니다. CREATE TABLE account ( id BIGINT PRIMARY KEY, owner_name VARCHAR(100) NOT NULL, balance BIGINT NOT NULL ); CREATE INDEX idx_account_owner ON account(owner_name); INSERT INTO account (id, owner_name, balance) VALUES (10, '민수', 10000); 이제 사용자가 이름을 바꾸거나 잔액을 조회하면 다음 경로를 거칩니다. [Spring 애플리케이션] HTTP 요청 → Controller → Service의 트랜잭션 프록시 → 실제 Service → JPA / Hibernate → JDBC 드라이버 → DB 연결을 통한 통신 ────────────────── 프로세스 경계 ────────────────── [데이터베이스 서버] 요청 수신 → SQL 문법 분석 → 테이블·컬럼·권한 확인 → 실행 방법 결정 → 필요한 잠금과 읽기 기준 확보 → 인덱스·데이터 페이지 접근 → 조회 또는 변경 → 결과 전송 / 트랜잭션 확정 [DB의 저장·유지 작업] 변경 로그 저장 ↔ 데이터 페이지 기록 ↔ 오래된 행 버전 정리 ↔ 체크포인트 ↔ 복제·백업 이것은 큰 책임의 흐름 입니다. 실제 코드에서는 잠금 획득, 메타데이터 조회, 읽기 기준 확보 등이 여러 단계에 걸쳐 나타나므로, 모든 SQL이 위 순서를 한 번씩만 통과한다고 해석하면 안 됩니다. PostgreSQL도 조회 처리와 DDL 처리를 서로 다른 실행 경로로 분기합니다. ( postgresql.org ) 2. DB는 Spring 안에 들어 있는 객체가 아니다 링크에서 JVM의 Heap과 Thread를 봤으니, 먼저 경계를 그어야 합니다. Spring JVM 프로세스 Java Heap Account 객체 Hibernate Session JDBC Connection 객체 │ SQL과 파라미터를 전송 ▼ DB 서버 프로세스 DB 전용 메모리 데이터 페이지 캐시 트랜잭션 상태 잠금 관리 정보 실행 계획 Java의 Account 객체와 DB에 저장된 account 행은 서로 다른 메모리 공간에 있는 별개의 표현 입니다. account.setBalance(...) 가 DB의 메모리 주소를 직접 변경하는 것은 아닙니다. PostgreSQL의 클라이언트·서버 구조도 이처럼 애플리케이션과 DB 서버를 분리합니다. ( postgresql.org ) DB 안에서는 누가 SQL을 실행하나? DB 일반적인 서버 실행 구조 MySQL 기본적으로 연결별 처리 스레드를 사용. 별도의 백그라운드 스레드도 존재 MariaDB 연결별 스레드 또는 설정된 스레드 풀 방식 PostgreSQL 일반적으로 연결마다 별도 backend 프로세스. 데이터 캐시 등은 공유 메모리 사용 따라서 Spring의 요청 처리 스레드와 DB에서 SQL을 실행하는 스레드·프로세스는 같은 것이 아닙니다. ( dev.mysql.com ) Spring 스레드는 SQL을 보내고 결과를 기다리는 동안 CPU를 거의 쓰지 않을 수 있습니다. 반대편 DB는 계산 중일 수도 있고, 디스크를 기다릴 수도 있고, 다른 트랜잭션의 잠금을 기다릴 수도 있습니다. 3. Connection은 무엇이고, HikariCP는 무엇을 재사용하나? Connection 은 단순히 “DB 주소가 담긴 객체”가 아닙니다. 이미 연결된 DB 세션을 Java에서 다루기 위한 인터페이스입니다. 세션에는 트랜잭션 상태, 자동 커밋 설정, 격리 수준 같은 상태가 연결됩니다. 커넥션 풀은 이런 물리적 연결을 매번 만들고 끊는 대신 빌려주고 돌려받습니다. ( github.com ) 요청 A → 연결 1번 빌림 → SQL 실행 → 트랜잭션 종료 → 연결 반환 요청 B → 같은 연결 1번을 나중에 빌림 → 자기 SQL 실행 여기서 재사용되는 것은 연결 이지, 이전 요청의 트랜잭션을 이어 쓰는 것이 아닙니다. 예를 들어: Tomcat 요청 처리 스레드: 200개 Hikari 최대 연결: 20개 라면, DB 연결이 필요한 요청 200개가 모두 동시에 연결을 가질 수는 없습니다. 연결을 얻지 못한 요청은 풀에서 기다립니다. HikariCP의 HikariPool.getConnection() 이 이 대여 경로를 담당합니다. ( raw.githubusercontent.com ) 연결 20개는 CPU 코어 20개를 뜻하지 않고, SQL 20개가 항상 동시에 CPU를 사용한다는 뜻도 아닙니다. 4. @Transactional 에서 Java 객체 변경까지 이미 account 테이블에 매핑된 엔티티가 있다고 하겠습니다. @Transactional public void rename(long id, String newName) { Account account = entityManager.find(Account.class, id); if (account == null) { throw new IllegalArgumentException("계좌가 없습니다."); } account.setOwnerName(newName); } 일반적인 명령형 Spring 트랜잭션에서는 다음 흐름입니다. Controller가 Service 호출 → 프록시가 호출을 가로챔 → 트랜잭션 시작 또는 기존 트랜잭션 참여 → 실제 rename() 실행 → 정상 종료하면 커밋 처리 → 실패하면 설정된 규칙에 따라 롤백 내부의 핵심은 TransactionInterceptor 와 트랜잭션 매니저입니다. 물리적 DB 연결은 설정에 따라 실제 SQL이 필요한 시점까지 늦게 획득할 수도 있으므로, 메서드 진입 순간 반드시 새 TCP 연결을 만드는 것은 아닙니다. ( docs.spring.io ) 또한 @Transactional 이 모든 예외를 무조건 롤백한다거나, 같은 객체 내부의 자기 호출에도 항상 적용되는 것은 아닙니다. 기본 프록시 방식에서는 프록시를 거쳤는지가 중요합니다. ( docs.spring.io ) 이름을 바꾼 순간에는 무엇이 달라지나? account.setOwnerName("영희"); 직후에는 우선 다음 상태일 수 있습니다. Java 객체: ownerName = "영희" DB 행: owner_name = "민수" Hibernate가 관리하는 엔티티의 변경을 감지해 SQL로 반영하기 전이기 때문입니다. DefaultFlushEntityEventListener 에는 엔티티 상태 확인, 변경 검사, 업데이트 예약 등의 처리가 있습니다. ( raw.githubusercontent.com ) 5. Hibernate의 flush 는 DB 커밋이 아니다 Hibernate의 flush는 메모리에서 관리하던 변경을 SQL 실행으로 동기화하는 과정 입니다. 관리 중인 엔티티 변경 → 변경된 속성 확인 → UPDATE 작업 구성 → JDBC로 SQL 실행 실제 UPDATE에 어떤 컬럼을 포함하는지는 Hibernate 매핑과 설정에 따라 다릅니다. Java에서 속성 하나만 바꿨다고 항상 그 컬럼 하나만 들어간 SQL이 생성되는 것은 아닙니다. ( raw.githubusercontent.com ) 다음 네 상태를 구분해야 합니다. 시점 Java 객체 DB에서 SQL 실행 트랜잭션 확정 setter 호출 직후 변경됨 아직 아닐 수 있음 안 됨 flush 완료 변경됨 실행됨 아직 안 됨 commit 완료 변경됨 실행됨 확정됨 rollback 완료 객체를 다시 읽어야 할 수 있음 실행됐던 변경 취소 처리 취소됨 flush는 커밋 전에 자동으로 일어날 수 있고, 쿼리 실행 전이나 명시적 flush() 호출로도 일어날 수 있습니다. INSERT는 ID 생성 전략 등에 따라 더 일찍 실행될 수도 있습니다. ( docs.jboss.org ) 그래서 다음은 서로 다릅니다. save() 호출했다 ≠ 반드시 지금 INSERT했다 SQL 로그에 UPDATE가 찍혔다 ≠ 트랜잭션이 커밋됐다 flush()가 끝났다 ≠ 데이터 파일까지 전부 저장됐다 6. JDBC는 SQL을 어떻게 DB로 보내나? JDBC 드라이버는 Java API 호출을 해당 DB가 이해하는 통신 메시지로 바꿉니다. PreparedStatement statement = connection.prepareStatement( "SELECT balance FROM account WHERE id = ?" ); statement.setLong(1, 10L); 여기서 SQL 구조와 값의 의미가 구분됩니다. 다만 JDBC의 PreparedStatement 를 사용했다는 사실과, 서버에 영구적으로 준비된 실행 계획이 존재한다는 사실은 동일하지 않습니다. 드라이버 설정과 실행 방식에 따라 경로가 달라집니다. DB 대표적인 메시지 처리 방식 MySQL·MariaDB 일반 쿼리 메시지와 준비 구문 실행 메시지를 구분 PostgreSQL Simple Query 또는 Parse·Bind·Execute 등의 Extended Query MySQL·MariaDB의 sql_parse.cc , PostgreSQL의 postgres.c 에서 이러한 메시지 분기를 확인할 수 있습니다. ( raw.githubusercontent.com ) 네트워크를 넘어가는 것은 Java 엔티티의 메모리 주소가 아니라, SQL·파라미터·프로토콜 메시지입니다. 7. DB가 SQL을 받으면 먼저 무엇을 알아내나? 다음 조회를 보겠습니다. SELECT balance FROM account WHERE id = 10; DB는 문자열을 받자마자 무작정 파일에서 10 을 검색하지 않습니다. 단계 DB가 알아내는 것 문법 분석 SELECT , FROM , WHERE 가 올바르게 작성되었는가? 의미 분석 account 가 어떤 테이블이고 balance 가 어떤 컬럼인가? 타입 확인 id 와 숫자 10 을 어떻게 비교하는가? 권한·객체 보호 이 객체를 사용할 권한이 있는가? 실행 중 정의가 바뀌지 않게 어떻게 보호하는가? 계획 수립 인덱스로 찾을까, 전체를 읽을까? PostgreSQL은 파싱한 쿼리를 분석·재작성하고, 계획을 만든 뒤 실행기에 전달합니다. 뷰를 실제 기반 테이블 접근으로 바꾸는 것도 이 처리 과정에 들어갑니다. ( postgresql.org ) 여기서 필요한 테이블 정보는 메타데이터 , 즉 “데이터의 구조를 설명하는 데이터”입니다. account 테이블 id: BIGINT owner_name: VARCHAR(100) balance: BIGINT 기본키: id 인덱스: owner_name 이 정의가 있어야 저장된 바이트를 숫자나 문자열로 해석할 수 있습니다. PostgreSQL도 실제 행의 속성을 해석할 때 pg_attribute 등의 정보를 사용합니다. ( postgresql.org ) DDL이 메타데이터를 바꿀 때 조회와 충돌하는 이유가 여기서 시작됩니다. 8. 옵티마이저는 무엇을 계산하나? 옵티마이저는 같은 정답을 얻는 여러 실행 방법 중 비용이 낮을 것으로 예상되는 방법을 선택 합니다. SELECT * FROM account WHERE owner_name = '민수'; 가능한 방법은 다음과 같습니다. 방법 A account 전체를 읽으면서 이름을 비교 방법 B owner_name 인덱스에서 '민수'를 찾고 해당하는 실제 행만 읽음 인덱스가 있더라도 조건에 맞는 행이 테이블 대부분이면 전체 스캔이 더 유리할 수 있습니다. 선택에는 데이터 분포, 예상 행 수, 페이지 접근 비용 등의 통계와 비용 모델이 사용됩니다. 옵티마이저는 모든 방법을 실제로 실행해 보고 고르는 것이 아닙니다. ( postgresql.org ) JOIN·정렬·집계도 실행 계획의 일부다 JOIN: 양쪽 데이터를 어떤 순서와 방법으로 연결할까? ORDER BY: 인덱스 순서를 활용할까, 별도로 정렬할까? GROUP BY: 어떤 방법으로 그룹을 만들고 집계할까? PostgreSQL의 실행 계획에는 스캔뿐 아니라 nested loop·hash·merge join, 정렬, 집계 등의 노드가 들어갑니다. 실행기는 이러한 계획을 따라 필요한 행을 받아 처리합니다. ( postgresql.org ) 따라서 “인덱스를 탔다”만으로 좋은 쿼리라고 판단할 수 없습니다. 얼마나 많은 행을 읽고, 반복하고, 정렬하고, 최종적으로 버리는지까지 봐야 합니다. 9. 실제 데이터는 행 단위 파일이 아니라 페이지에 저장된다 DB는 보통 다음처럼 저장하지 않습니다. account_10.txt account_11.txt account_12.txt 대신 테이블과 인덱스를 여러 페이지 로 나누어 관리합니다. 데이터 파일 ├─ 페이지 1 │ 여러 행 또는 인덱스 항목 ├─ 페이지 2 │ 여러 행 또는 인덱스 항목 └─ 페이지 3 ... DB 일반적인 기본 페이지 크기 MySQL InnoDB 16KiB MariaDB InnoDB 16KiB PostgreSQL 8KiB 설정·빌드에 따라 달라질 수 있습니다. 페이지 안에는 사용자 데이터 외에 헤더, 행 위치 정보, 여유 공간 등도 들어갑니다. ( dev.mysql.com ) 매번 디스크에서 읽지는 않는다 필요한 페이지를 찾음 ↓ DB 메모리에 있는가? ├─ 있음 → 메모리에서 읽음 └─ 없음 → 저장장치에서 가져와 메모리에 올림 MySQL·MariaDB InnoDB는 Buffer Pool , PostgreSQL은 shared buffers 를 사용합니다. PostgreSQL에서는 OS 파일 캐시도 중요한 역할을 합니다. ( dev.mysql.com ) 이 캐시는 Hibernate의 영속성 컨텍스트와 다릅니다. Hibernate: Account라는 Java 객체를 관리 DB: 테이블·인덱스의 데이터 페이지를 관리 10. 인덱스를 찾은 다음 실제 행까지 가는 길이 DB마다 다르다 10-1. MySQL·MariaDB InnoDB InnoDB의 일반적인 기본키 구조에서는 클러스터드 인덱스의 마지막 단계인 리프 페이지에 실제 행 데이터가 들어갑니다. 기본키 인덱스 루트 → 중간 페이지 → id=10이 있는 리프 페이지 id=10 owner_name='민수' balance=10000 반면 이름에 만든 보조 인덱스는 기본키 값도 포함합니다. owner_name 인덱스 '민수' → 기본키 10 ↓ 기본키 인덱스 탐색 ↓ 실제 행 그래서 보조 인덱스를 통한 조회는 실제 행을 얻기 위해 기본키 인덱스를 다시 찾아갈 수 있습니다. 필요한 컬럼이 보조 인덱스에 모두 있으면 일부 접근을 줄일 수 있습니다. ( dev.mysql.com ) 10-2. PostgreSQL PostgreSQL의 기본 heap 테이블에서는 기본키 인덱스도 실제 행과 별도로 저장 됩니다. 기본키 인덱스 id=10 → 행 위치 정보 ↓ heap 페이지 ↓ 실제 행 이 위치 정보는 페이지 번호와 페이지 안의 항목 위치를 조합한 것입니다. 여기서 PostgreSQL의 heap은 JVM Heap과 다른 말입니다. 정렬된 기본키 트리 자체에 전체 행을 담는 InnoDB와 대비되는 테이블 저장 구조입니다. ( postgresql.org ) PostgreSQL의 index-only scan도 무조건 테이블 접근이 0인 것은 아닙니다. 필요한 값이 인덱스에 있어도, 해당 행을 읽어도 되는지 확인하기 위해 heap에 접근해야 할 수 있습니다. 이를 줄이는 데 visibility map을 사용합니다. ( postgresql.org ) 그래서 B-tree 계열 인덱스는 무엇을 해 주나? 핵심은 전체를 한 줄씩 뒤지는 대신 값의 범위를 따라 필요한 페이지로 내려가는 것 입니다. 다만 인덱스의 논리적 정렬과 파일이 저장장치에서 물리적으로 연속 배치된다는 것은 다릅니다. 또한 결과 순서가 필요하면 SQL에 ORDER BY 를 명시해야 합니다. 11. SELECT 는 행을 찾았다고 바로 반환하지 않는다 이제 세 DB의 조회 경로를 연결할 수 있습니다. DB 일반적인 인덱스 조회의 주요 경로 MySQL SQL 분석 → 계획 → InnoDB 인덱스 탐색 → 행 버전 확인 → 결과 MariaDB SQL 분석 → 계획 → MariaDB InnoDB 인덱스 탐색 → 행 버전 확인 → 결과 PostgreSQL 분석·재작성 → 계획 → 인덱스/heap 접근 → 행 버전 확인 → 결과 MySQL과 MariaDB의 InnoDB에는 실제로 row_search_mvcc() 가 있고, PostgreSQL에는 HeapTupleSatisfiesMVCC() 가 있습니다. 행이 존재하는가와, 지금 이 조회에 보여도 되는가는 별개의 검사입니다. ( raw.githubusercontent.com ) 왜 이런 검사가 필요할까요? 트랜잭션 A: 잔액을 10000 → 9000으로 변경 아직 COMMIT 안 함 트랜잭션 B: 같은 계좌 잔액을 SELECT B가 A의 미완료 값을 그대로 읽으면, A가 나중에 롤백했을 때 문제가 됩니다. 그래서 DB는 여러 버전 중 이 조회에 보여 줄 버전을 선택 합니다. 이것이 MVCC입니다. ( postgresql.org ) 12. MVCC의 스냅샷은 DB 전체를 복사한 사진이 아니다 스냅샷이라는 말을 이렇게 이해하면 안 됩니다. 트랜잭션 시작 → DB 전체를 메모리에 복사 → 그 복사본 조회 실제 핵심은 다음과 같습니다. 조회 기준을 만들고 ↓ 각 행 버전의 생성·변경 트랜잭션을 확인하고 ↓ 그 기준에서 보여도 되는 버전을 선택 트랜잭션 번호, 실행 중인 트랜잭션 집합, 커밋·취소 상태 등의 정보가 사용됩니다. 단순히 “번호가 작으면 무조건 보인다”로 끝나는 판정도 아닙니다. PostgreSQL의 가시성 함수는 스냅샷과 현재 트랜잭션, 트랜잭션 상태 등을 함께 확인합니다. ( raw.githubusercontent.com ) 그리고 과거 버전을 보관하는 방식이 InnoDB와 PostgreSQL에서 크게 다릅니다. 13. MySQL·MariaDB의 UPDATE: 현재 행 + Undo 이력 다음 SQL을 실행한다고 하겠습니다. UPDATE account SET balance = balance - 1000 WHERE id = 10; InnoDB의 주요 처리를 의미 중심으로 풀면 다음과 같습니다. 수정할 행 탐색 → 필요한 행 잠금 확보 → 이전 상태를 복원할 Undo 정보 생성 → 현재 행 변경 → 관련 인덱스 변경 → 변경 페이지와 Redo 정보 관리 낮은 수준에서는 여러 페이지 변경과 로그 생성이 묶여 진행되므로, 이것을 모든 내부 함수의 엄격한 한 줄 호출 순서로 해석하면 안 됩니다. MySQL의 row0upd.cc 에서는 클러스터드 레코드 수정과 관련 보조 인덱스 수정 경로를 구분합니다. ( raw.githubusercontent.com ) 개념적으로는 다음 상태입니다. 현재 행: balance = 9000 변경 트랜잭션 정보 이전 버전으로 연결되는 정보 │ ▼ Undo: 이전 balance = 10000 실제로 Undo는 매번 행 전체 복사본일 필요는 없고, 이전 상태를 재구성하기 위한 정보를 담습니다. 이 정보는 롤백과 과거 버전 조회에 함께 사용 됩니다. ( dev.mysql.com ) 다른 조회가 9000을 볼 수 없는 기준이라면: 현재 버전 9000 → 이 조회에서는 아직 보이면 안 됨 → Undo를 따라 이전 상태 구성 → 10000 반환 이 때문에 일반적인 스냅샷 조회는 같은 행의 수정이 끝날 때까지 반드시 기다리는 대신 과거 버전을 읽을 수 있습니다. 하지만 이것은 행 버전 조회에 대한 이야기 입니다. 뒤에서 설명할 테이블 구조 잠금까지 무시하는 기능은 아닙니다. 14. PostgreSQL의 UPDATE: 새 행 버전을 만든
What RADAR observed and classified to build this opportunity. It is what the source published, not a verification that the offer is still active.
Java 객체를 바꾼 일이 어떻게 DB의 메모리와 파일 변경으로 이어지는가. “Java 객체를 바꾼 일이 어떻게 DB의 메모리와 파일 변경으로 이어지는가” 먼저 가장 중요한 기준부터 잡겠습니다. Java 객체 변경, SQL 실행, 트랜잭션 커밋, 데이터 파일 저장은 서로 다른 사건입니다. 이 네 가지가 구분되면 flush , MVCC, 로그, 잠금, ddl-auto , 무중단 변경을 하나의 흐름으로 이해할 수 있습니다. 아래는 MySQL 8.4·MariaDB 11.4의 InnoDB, PostgreSQL 17의 기본 heap 테이블 을 기준으로 설명합니다. 소스 함수 이름은 MySQL 8.4.0·MariaDB 11.4.2·PostgreSQL 17.0에서 확인한 이름입니다. 최신 버전이라는 뜻이 아니라,…
Open source