← go back

Database에 관한 필수 지식들


— 트랜잭션
트랜잭션에 대해
ACID중 Consistency(일관성)에 대해
트랜잭션 격리수준에 대해
— JPA (Java Persistence API)
JPA란 무엇인가
JPA와 Hibernate와의 관계
객체-관계 불일치에 대해
Spring Data JPA에 대해
JPA 1차 캐시 2차 캐시
JPA N+1 문제에 대해
JPA에서 Fetch Join은 무엇인가
영속성 컨텍스트란 무엇인가
JPA는 성능 최적화를 어떻게 하는가

트랜잭션에 대해


트랜잭션은 데이터베이스의 상태를 변환시키는 하나 이상의 연산들의 집합으로, 이들 연산들은 한꺼번에 수행되거나 전혀 수행되지 않아야 함을 보장하는 기능입니다. 이는 데이터베이스의 일관성을 보장하는 중요한 메커니즘입니다. 트랜잭션의 주요 특성은 ACID(Atomicity, Consistency, Isolation, Durability)로 요약됩니다.


Atomicity(원자성): 트랜잭션 내의 모든 연산은 한꺼번에 수행되거나 아예 수행되지 않아야 합니다. 즉, 트랜잭션의 연산은 부분적으로 실행되지 않음을 보장합니다.

Consistency(일관성): 일관성은 트랜잭션 실행 전과 후의 데이터 상태가 일관되게 유지된다는 것을 보장합니다. 즉, 트랜잭션이 일어난 이후의 데이터베이스는 데이터베이스의 제약이나 규칙을 만족해야 한다는 뜻입니다.

Isolation(독립성): 동시에 수행되는 여러 트랜잭션들이 서로에게 영향을 주지 않음을 보장합니다. 각 트랜잭션은 독립적인 수행을 완료하고, 다른 트랜잭션의 중간 결과를 볼 수 없습니다.

Durability(영속성): 성공적으로 완료된 트랜잭션의 결과는 영구적으로 반영되어야 합니다. 시스템 장애가 발생해도 이는 보장되어야 합니다.

이렇게 트랜잭션이 ACID 속성을 통해 데이터의 일관성을 보장하며, 이는 데이터베이스 시스템에서 매우 중요한 역할을 합니다.

ACID중 Consistency(일관성)에 대해


일관성이란 트랜잭션이 데이터 무결성에 대한 제약 조건을 만족되어야 함을 의미합니다. 즉, 데이터는 모순이 없어야 합니다. 트랜잭션이 제약 조건을 위반하는 경우 일관성을 통해 트랜잭션이 실행되지 않고 이전의 상태로 롤백됩니다.


일관성에 대해 더 자세히 이해하기 위해 예를 들어보겠습니다. 데이터베이스에 “모든 사용자는 이메일 주소를 가져야 한다”는 무결성 제약 조건이 있다고 가정해봅시다. 새 사용자를 생성하는 트랜잭션은 이메일 주소를 제공하지 않으면 실패해야 합니다. 트랜잭션이 성공적으로 완료된 후에는 모든 사용자가 여전히 이메일 주소를 가지고 있어야 하므로 데이터베이스는 일관된 상태를 유지합니다. 이와 같이 일관성은 데이터베이스의 무결성 제약 조건을 유지하면서 트랜잭션의 실행을 보장하는 속성입니다.

트랜잭션 격리수준에 대해


트랜잭션 격리 수준(Transaction Isolation Level)이란, 동시에 여러 트랜잭션이 처리될 때, 트랜잭션들이 서로에게 미치는 영향을 어느 정도 허용할지를 결정하는 것입니다. SQL 표준에서는 READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE의 4가지 수준을 정의하고 있습니다.


각 격리 수준은 다음과 같은 성질을 가집니다:

READ UNCOMMITTED (읽기 미완료): 가장 낮은 격리 수준으로, 한 트랜잭션에서 처리 중인 변경 내용이 Commit 여부와 상관없이 다른 트랜잭션에서 보일 수 있습니다. 이로 인해 Dirty Read, Non-Repeatable Read, Phantom Read와 같은 문제가 발생할 수 있습니다.

READ COMMITTED (읽기 완료): Commit된 데이터만 읽을 수 있으므로 Dirty Read는 방지하지만, 트랜잭션 도중 다른 트랜잭션에서 변경한 데이터를 볼 수 있으므로 Non-Repeatable Read와 Phantom Read는 여전히 발생할 수 있습니다.

REPEATABLE READ (반복 가능한 읽기): 한 트랜잭션 내에서 동일한 결과를 보장하므로 Non-Repeatable Read는 방지하지만, 새로운 데이터가 추가된 경우에 대해선 이를 보장하지 못해 Phantom Read가 발생할 수 있습니다.

SERIALIZABLE (직렬화 가능): 가장 높은 격리 수준으로, 여러 트랜잭션이 동일한 레코드에 접근할 수 없습니다. 트랜잭션이 순차적으로 실행되도록 하여 모든 데이터 부정합 문제를 방지합니다. 즉, 순수한 SELECT 작업에서도 대상 레코드에 넥스트 키 락을 읽기 잠금(공유락, Shared Lock)으로 건다. 따라서 한 트랜잭션에서 넥스트 키 락이 걸린 레코드를 다른 트랜잭션에서는 절대 추가/수정/삭제할 수 없습니다. 하지만 이는 동시 처리 성능에 부정적인 영향을 미칠 수 있습니다.

이러한 격리 수준에 따른 문제를 해결하기 위해선 일반적으로 Locking, MVCC(Multi Version Concurrency Control) 등의 기법이 사용되며, DBMS마다 이를 다루는 방식에 차이가 있습니다. 따라서 개발자는 어플리케이션의 특성과 DBMS의 동작 방식을 고려하여 적절한 트랜잭션 격리 수준을 설정해야 합니다.

JPA란 무엇인가


JPA(Java Persistence API)는 개발자가 객체 지향적으로 데이터를 관리할 수 있게 돕는 ORM(Object-Relational Mapping) 프레임워크입니다.


JPA는 객체 지향 프로그래밍과 관계형 데이터베이스 간의 차이를 해소하는 역할을 합니다. 일반적으로 객체 지향 프로그래밍과 관계형 데이터베이스는 다른 특성과 방식을 가지고 있어, 두 세계를 연결하는 것은 어렵습니다. 이를 ‘객체-관계 불일치’라고 부르는데, JPA는 이러한 문제를 해결하기 위해 사용됩니다.

JPA는 다음과 같은 이점을 제공합니다:

  1. 생산성 향상: SQL을 직접 작성하는 대신, JPA는 데이터베이스 연산을 위한 고수준 API를 제공하므로, 개발자는 데이터베이스 관련 코드를 더 쉽고 빠르게 작성할 수 있습니다.

  2. 유지보수 용이: JPA를 사용하면 데이터베이스 스키마 변경에 대한 코드의 영향을 최소화할 수 있습니다. 이는 애플리케이션의 유지보수를 용이하게 합니다.

  3. 데이터베이스 독립성: JPA는 데이터베이스의 종류에 상관없이 일관된 방식으로 코드를 작성할 수 있도록 지원합니다. 이를 통해 애플리케이션이 다른 데이터베이스로 이전할 경우에도 코드의 변경을 최소화할 수 있습니다.

  4. 성능 최적화: JPA는 성능 최적화 기능(예: 캐싱, 지연 로딩 등)을 제공하여, 애플리케이션의 성능을 향상시킬 수 있습니다.

따라서 JPA는 개발자가 객체 지향적으로 데이터를 관리하고, 생산성을 향상시키며, 데이터베이스 독립적인 코드를 작성할 수 있도록 돕는 프레임워크로 인해 널리 사용되고 있습니다.

JPA와 Hibernate와의 관계


JPA는 ORM(Object-Relational Mapping)을 위한 자바의 표준 스펙이며, Hibernate는 JPA의 구현체 중 하나입니다.


  1. JPA (Java Persistence API): JPA는 자바 애플리케이션에서 관계형 데이터베이스와의 상호 작용을 추상화하고 단순화하는 ORM 표준입니다. JPA는 인터페이스와 애노테이션의 집합으로 이루어져 있으며, 구체적인 기능은 구현체(예: Hibernate)에서 제공됩니다. JPA의 도입으로 인해 개발자는 데이터베이스와의 상호작용을 직접 다루지 않아도 되며, 객체 지향적인 방식으로 코드를 작성할 수 있게 됩니다.

  2. Hibernate: Hibernate는 JPA의 대표적인 구현체 중 하나로, JPA의 표준 스펙을 준수하면서도 추가적인 기능과 성능 향상을 제공합니다. Hibernate를 사용하면 개발자는 데이터베이스의 특정 세부사항을 걱정하지 않고도, JPA의 표준 인터페이스와 애노테이션을 사용하여 효율적인 데이터베이스 작업을 수행할 수 있습니다.

  3. 관계의 중요성: JPA와 Hibernate의 관계는 중요하게 다루어져야 합니다. JPA를 사용하면, 향후에 다른 JPA 구현체로 쉽게 교체할 수 있는 유연성이 제공됩니다. 반면 Hibernate와 같은 구현체는 표준 스펙 외에도 특정 기능을 제공하므로, 이를 활용하면 해당 구현체에 의존성이 생길 수 있습니다.

  4. 선택의 문제: 프로젝트의 요구 사항, 팀의 경험, 데이터베이스의 특정 요구 사항 등을 고려하여 JPA만을 사용할 것인지, 아니면 Hibernate와 같은 구현체의 추가 기능을 활용할 것인지 결정해야 합니다.

JPA와 Hibernate의 관계를 이해하는 것은 자바에서 데이터베이스 작업을 수행하는 방식을 이해하고 효과적으로 활용하는 데 중요합니다.

객체-관계 불일치에 대해


‘객체-관계 불일치’는 객체 지향 프로그래밍관계형 데이터베이스의 기본적인 차이 때문에 발생하는 문제를 말합니다. 이 두 가지는 각각 데이터를 다루는 방식이 다르기 때문에, 프로그래머는 데이터를 객체로 관리하면서 동시에 관계형 데이터베이스로 영구 저장해야 하는 복잡함에 직면하게 됩니다.


객체 지향 프로그래밍과 관계형 데이터베이스는 서로 매우 다른 원칙에 기반을 두고 있습니다. 이로 인해 여러 가지 문제가 발생하며, 이를 ‘객체-관계 불일치’라고 합니다. 이 문제는 다음과 같은 몇 가지 주요 문제를 포함합니다:

  1. 상속을 지원하지 않는다: 객체 지향 프로그래밍에서는 클래스간의 상속이 일반적인 패턴입니다. 하지만 대부분의 관계형 데이터베이스에서는 이런 개념을 직접 지원하지 않습니다.

  2. 동일하다는 판단의 조건이 다르다: 객체 지향 프로그래밍에서, 두 객체가 동일한지 여부는 그들의 속성이 아닌 그들이 같은 객체인지 여부에 의해 결정됩니다. 반면 관계형 데이터베이스에서 레코드의 동일성은 주로 키 값에 의해 결정됩니다.

  3. 참조 관계를 지원하지 않는다: 객체 지향 프로그래밍에서 객체간의 관계는 객체의 참조를 통해 정의됩니다. 반면에 관계형 데이터베이스에서는 외래 키를 통해 관계가 정의됩니다.

  4. 데이터 타입 지원의 한계: 객체 지향 프로그래밍은 다양한 데이터 타입(예: 사용자 정의 타입, 컬렉션 등)을 지원합니다. 하지만 관계형 데이터베이스에서는 일반적으로 보다 제한적인 데이터 타입을 지원합니다.

객체-관계 불일치 문제는 ORM(Object-Relational Mapping) 도구를 사용하여 해결할 수 있습니다. ORM 도구는 객체 모델과 관계형 모델 사이의 ‘다리’ 역할을 하여, 개발자가 두 세계의 차이에 대해 신경 쓸 필요 없이 데이터를 작업할 수 있게 돕습니다. ORM 도구의 예로는 Hibernate, JPA(Java Persistence API) 등이 있습니다.

Spring Data JPA에 대해


Spring Data JPA는 스프링 프레임워크의 일부로, JPA를 더욱 편리하게 사용할 수 있는 기능을 제공합니다.


Spring Data JPA는 JPA의 기능을 스프링 프레임워크와 통합하여 제공합니다. 기본 CRUD 연산 외에도, 메소드 이름을 기반으로 자동으로 쿼리를 생성하는 기능, 페이징 및 정렬, 트랜잭션 관리와 같은 고급 기능을 쉽게 사용할 수 있도록 해줍니다.

예를 들어, Spring Data JPA를 사용하면 Repository 인터페이스만 정의하면, 구현체 없이도 CRUD 작업을 수행할 수 있습니다. 이를 통해 개발 생산성을 향상시키며 유지보수도 용이해집니다.

JPA 1차 캐시 2차 캐시


JPA(Java Persistence API)의 캐시는 데이터베이스의 조회 성능을 향상시키기 위한 메커니즘이며, 주로 1차 캐시와 2차 캐시로 구분됩니다. 1차 캐시는 트랜잭션 범위 내에서 엔터티를 캐시하고, 2차 캐시는 트랜잭션을 넘어 전체 애플리케이션 범위에서 사용되는 공유 캐시입니다.


1차 캐시: 1차 캐시는 영속성 컨텍스트가 시작되는 순간부터 해당 트랜잭션 범위 내에서만 유효한 캐시입니다. 동일한 트랜잭션 내에서 동일한 엔터티를 조회할 때, 데이터베이스에 접근하지 않고 캐시된 값을 사용합니다. 이로 인해 트랜잭션 내에서의 조회 성능이 향상됩니다. 하지만 트랜잭션이 끝나면 이 캐시는 초기화됩니다.

2차 캐시: 2차 캐시는 여러 트랜잭션 간에 공유되는 애플리케이션 전체 범위의 캐시입니다. 동일한 엔터티에 대한 반복된 조회가 발생할 때, 이 캐시를 활용하여 데이터베이스 접근을 최소화하고 성능을 향상시킬 수 있습니다. 2차 캐시는 선택적으로 설정 가능하며, 적절한 캐시 전략을 선택하고 관리해야 하므로 복잡성이 증가할 수 있습니다.

활용과 주의점: 1차 캐시는 트랜잭션의 일관성을 유지하는 데 도움이 되지만, 규모가 큰 시스템에서는 영속성 컨텍스트가 과도하게 커질 위험이 있으므로 관리가 필요합니다. 2차 캐시는 성능 최적화에 유리하지만, 여러 스레드나 서버 환경에서의 동시성 제어, 캐시 유효성 관리 등의 복잡한 이슈를 고려해야 합니다.

결론적으로, JPA의 1차 캐시와 2차 캐시는 데이터베이스의 조회 성능을 향상시키는 메커니즘이며, 각각 트랜잭션 범위와 애플리케이션 전체 범위에서 작동합니다. 올바르게 활용하면 성능 최적화가 가능하나, 적절한 관리와 전략이 필요한 부분이 있으므로 주의 깊은 설계와 구현이 요구됩니다.

JPA N+1 문제에 대해


N+1 쿼리 문제는 ORM(Object-Relational Mapping)을 사용할 때 발생하는 성능 문제 중 하나로, 하나의 부모 레코드와 관련된 여러 자식 레코드를 조회할 때 쿼리가 과도하게 실행되는 현상을 말합니다. 여기서 ‘N’은 자식 레코드의 수이며, ‘+1’은 부모 레코드를 조회하는 쿼리를 의미합니다.


예를 들어, 고객(User)과 주문(Order)과의 관계에서 고객 한 명을 조회하고 (1번 쿼리), 그 고객이 한 주문에 대한 상세 정보를 각각 조회한다고 가정할 때 (N번 쿼리), 총 N+1번의 쿼리가 발생합니다. 여기서 N은 고객이 한 주문의 수입니다. 이렇게 되면, 주문이 많은 경우에 데이터베이스에 부담을 주고 성능을 저하시키는 문제가 발생합니다.

해결 방법 1 - Fetch Join 사용: Fetch Join을 사용하면 부모 레코드와 관련된 자식 레코드를 한 번의 쿼리로 조회할 수 있습니다. 이 방법은 쿼리의 수를 크게 줄일 수 있지만, 데이터 중복이 발생할 수 있으므로 주의가 필요합니다.

해결 방법 2 - Fetch 전략 변경: ORM 프레임워크(예: Hibernate)에서는 Fetch 전략을 변경하여 N+1 문제를 해결할 수 있습니다. 예를 들어, EAGER 전략을 사용하면 관련된 자식 레코드를 미리 조회하여 N+1 쿼리 문제를 피할 수 있습니다. 반면 LAZY 전략은 필요한 시점에만 자식 레코드를 조회합니다.

해결 방법 3 - Batch Size 설정: 일부 ORM 프레임워크에서는 Batch Size를 설정하여 한 번에 여러 자식 레코드를 조회할 수 있게 함으로써 N+1 문제를 해결할 수 있습니다.

N+1 쿼리 문제의 해결 방법은 상황에 따라 다를 수 있으므로, 애플리케이션의 특성, 데이터베이스의 구조, 쿼리의 복잡성 등을 종합적으로 고려하여 최적의 해결책을 선택해야 합니다.

N+1 쿼리 문제는 성능 최적화의 중요한 관점이며, 이를 이해하고 적절하게 대응하는 능력은 데이터베이스와 상호 작용하는 애플리케이션 개발에서 필수적인 역량입니다.

JPA에서 Fetch Join은 무엇인가


Fetch Join은 JPA에서 성능 최적화를 위해 사용하는 기법 중 하나로, 엔티티 객체의 연관된 필드를 한 번의 쿼리로 가져오는 방식입니다.


JPA에서는 연관된 엔티티를 조회할 때, 기본적으로 ‘지연 로딩’ 방식을 사용합니다. 이는 실제로 해당 필드를 사용할 때까지 데이터 로딩을 미루는 것을 의미하며, 이로 인해 여러 번의 쿼리가 실행되는 문제(N+1 문제)가 발생할 수 있습니다.

이러한 문제를 해결하기 위해 JPA는 Fetch Join이라는 기법을 제공합니다. Fetch Join은 JPQL에서 제공하는 기능으로, 특정 엔티티를 조회할 때 연관된 엔티티를 함께 조회하도록 SQL을 생성합니다. 이를 통해 필요한 모든 데이터를 한 번의 쿼리로 조회할 수 있어, 성능 최적화에 크게 기여합니다.

예를 들어, 회원(Member)과 팀(Team)이 있을 때, 특정 회원과 그 회원이 속한 팀을 한 번의 쿼리로 조회하려면 다음과 같이 작성할 수 있습니다.

    String jpql = "select m from Member m join fetch m.team";

이렇게 하면 Member와 연관된 Team 엔티티를 한 번의 쿼리로 함께 조회하게 됩니다. 이렇게 Fetch Join을 이용하면 필요한 데이터를 효율적으로 조회할 수 있으며, 특히 연관된 엔티티를 자주 조회해야 하는 상황에서 성능 개선 효과를 볼 수 있습니다.

영속성 컨텍스트란 무엇인가


영속성 컨텍스트(Persistence Context)는 JPA에서 엔티티를 관리하는 환경으로, 엔티티의 생명주기를 관리하며 캐싱, 쓰기 지연, 변경 감지, 지연 로딩 등의 기능을 제공합니다.


영속성 컨텍스트는 엔티티의 영구 저장을 위한 내부 캐시와 같은 역할을 합니다. 여기서 ‘영속성’이란 데이터를 생성한 프로그램의 생명주기와 관계없이 데이터가 계속 보존되는 특성을 말합니다.

  1. 1차 캐시: 영속성 컨텍스트는 트랜잭션이 끝날 때까지 엔티티의 1차 캐시를 유지합니다. 이를 통해 동일한 엔티티에 대한 반복적인 데이터베이스 접근을 줄일 수 있습니다.

  2. 쓰기 지연: 영속성 컨텍스트는 트랜잭션이 끝날 때까지 데이터베이스에 쓰는 작업을 지연시킵니다. 이를 통해 네트워크를 통한 비용이나, 리소스 사용량을 최소화할 수 있습니다.

  3. 변경 감지: 영속성 컨텍스트는 트랜잭션 커밋 시점에 엔티티의 변화를 감지하여 변경된 엔티티만 데이터베이스에 반영합니다. 이를 통해 불필요한 업데이트 쿼리 실행을 줄일 수 있습니다.

  4. 지연 로딩: 영속성 컨텍스트는 지연 로딩(Lazy Loading)을 지원합니다. 이는 실제로 데이터가 필요한 시점까지 데이터 로딩을 지연시키는 것으로, 성능 향상에 크게 기여합니다.

이처럼, 영속성 컨텍스트는 JPA가 데이터베이스에서 데이터를 안정적이고 효율적으로 처리하는 데 큰 역할을 합니다. 이를 통해 개발자는 데이터 접근 및 관리에 대한 부담을 크게 줄일 수 있으며, 그 결과로 애플리케이션 로직에 더 집중할 수 있게 됩니다.

JPA는 성능 최적화를 어떻게 하는가


JPA(Java Persistence API) 성능 최적화는 주로 쿼리 최적화, 지연 로딩 활용, 캐싱 등의 방법을 통해 이루어집니다. 이 방법들은 데이터베이스에 대한 불필요한 접근을 줄이고, 네트워크 트래픽을 감소시키며, CPU 사용률을 최적화하는데 도움을 줍니다.


  1. 쿼리 최적화: JPA를 사용하면 JPQL(Java Persistence Query Language) 또는 Criteria API를 사용하여 데이터를 조회할 수 있습니다. 이때, 필요한 데이터만 조회하도록 쿼리를 작성하고, 불필요한 조인을 최소화함으로써 성능을 향상시킬 수 있습니다. 또한, Batch insert, Batch update 등의 기법을 사용하여 여러 건의 데이터를 한 번의 쿼리로 처리할 수 있습니다.

  2. 지연 로딩(Lazy Loading) 활용: Eager Loading은 연관된 엔티티를 항상 함께 로드하는 반면, Lazy Loading은 필요할 때만 연관된 엔티티를 로드합니다. 따라서 불필요한 데이터 로드를 줄여 성능을 향상시킬 수 있습니다. JPA에서는 @OneToMany, @ManyToMany 관계의 기본 설정이 지연 로딩입니다.

  3. 캐싱 활용: JPA는 일차 캐시와 이차 캐시를 제공합니다. 일차 캐시는 트랜잭션 범위의 캐시이며, 이차 캐시는 애플리케이션 범위의 캐시입니다. 이들 캐시를 적절히 활용하면, 데이터베이스에 대한 접근을 줄이고, 성능을 향상시킬 수 있습니다.

  4. Fetch Join: 필요한 엔티티를 한 번의 쿼리로 함께 조회하는 기법입니다. N+1 문제를 해결하는 데 효과적입니다.

  5. 엔티티 최적화: 엔티티에 변화가 없다면, 영속성 컨텍스트에서 분리(Detach)하는 것이 좋습니다. 이는 불필요한 체크와 스냅샷 생성을 피할 수 있습니다.

이외에도, 페이징 처리, 커넥션 풀 관리 등을 통해 JPA의 성능을 최적화할 수 있습니다. 중요한 것은 성능 최적화를 위해 적절한 전략을 선택하고, 해당 전략이 성능 향상에 실질적으로 기여하도록 하기 위해 성능을 지속적으로 모니터링하고 튜닝해야 한다는 것입니다.