RDBMS (Relational Database Management System)
📌 데이터를 테이블 형식으로 저장, 관리, 검색하는 시스템을 의미합니다.
- 관계형 데이터베이스(RDB)는 데이터가 **행(Row)과 열(Column)**로 구성된 테이블(Table) 형태로 저장되며, 데이터 간 관계를 정의할 수 있습니다.
RDBMS의 주요 특징
- 테이블 기반 구조
- 데이터는 테이블(Table) 형태로 저장되며, 각 테이블은 행(Row)과 열(Column)로 구성.
- 열: 데이터의 속성(예: 이름, 나이).
- 행: 데이터의 실제 값(레코드).
- 관계(Relationship)
- 테이블 간 관계를 설정하여 데이터를 연결.
- 예: 외래 키(Foreign Key)를 사용해 두 테이블 간 참조 관계를 설정.
- SQL 지원
- 데이터를 정의, 조작, 검색하기 위해 **SQL(Structured Query Language)**을 사용.
- 예: SELECT, INSERT, UPDATE, DELETE.
- 정규화(Normalization)
- 데이터 중복을 줄이고, 데이터 무결성을 유지하기 위해 테이블을 나누는 프로세스.
- 데이터 무결성 및 일관성
- 트랜잭션(ACID 특성)을 통해 데이터의 무결성과 일관성을 보장.
- ACID:
- Atomicity(원자성): 트랜잭션은 완전히 수행되거나 전혀 수행되지 않음.
- Consistency(일관성): 트랜잭션 수행 후 데이터는 항상 일관된 상태를 유지.
- Isolation(격리성): 트랜잭션은 독립적으로 수행되어야 함.
- Durability(지속성): 트랜잭션이 완료되면 데이터는 영구적으로 저장.
- 데이터 검색 및 조작
- 복잡한 조건과 다양한 방식으로 데이터를 검색(Join, Subquery 등).
- 동시성 제어
- 다수의 사용자와 애플리케이션이 동시에 데이터베이스에 접근할 수 있도록 지원.
RDBMS의 주요 구성 요소
구성 요소 | 설명 |
테이블(Table) | 데이터를 저장하는 기본 단위. 열(Column)과 행(Row)으로 구성. |
스키마(Schema) | 데이터베이스의 구조를 정의. 테이블, 열, 데이터 타입 등 메타데이터 포함. |
Primary Key | 각 행을 고유하게 식별하는 열. |
Foreign Key | 다른 테이블의 Primary Key를 참조하여 관계를 정의. |
View | 하나 이상의 테이블로부터 데이터를 가져와 보여주는 가상 테이블. |
Index | 데이터 검색 속도를 높이기 위해 특정 열에 대한 색인. |
RDBMS의 주요 사용 사례
- ERP/CRM 시스템
- 기업 내 리소스 관리 및 고객 관리 시스템의 데이터 저장소.
- 전자상거래
- 상품, 사용자, 주문 데이터를 관리.
- 금융 시스템
- 계좌, 거래 내역, 결제 데이터 관리.
- 소셜 네트워크
- 사용자 정보, 친구 관계, 게시물 데이터 관리.
대표적인 RDBMS 소프트웨어
RDBMS | 특징 |
MySQL | 오픈 소스 데이터베이스, 많은 웹 애플리케이션에서 사용. |
PostgreSQL | 오픈 소스, 고급 기능 지원(확장성, 복잡한 쿼리 처리). |
Oracle DB | 상용 데이터베이스, 대규모 기업 애플리케이션에서 사용. |
Microsoft SQL Server | MS에서 제공하는 데이터베이스, 윈도우 환경에 최적화. |
MariaDB | MySQL에서 분리된 오픈 소스 데이터베이스. |
SQLite | 경량 데이터베이스, 모바일 및 임베디드 시스템에서 자주 사용. |
RDBMS의 장단점
장점 | 단점 |
데이터 무결성과 일관성을 보장 | 대규모 데이터와 복잡한 관계 처리 시 성능 저하 발생 가능 |
SQL로 데이터를 쉽게 검색, 조작 가능 | 분산 시스템에서 확장성이 제한될 수 있음 |
테이블 간의 관계를 명확하게 정의 가능 | 비정형 데이터(이미지, JSON 등) 처리에는 부적합 |
ACID 특성을 통해 안정적인 트랜잭션 처리 | NoSQL에 비해 구조적 제약이 많아 유연성이 떨어질 수 있음 |
RDBMS와 NoSQL의 비교
RDBMS | NoSQL | |
데이터 구조 | 정형 데이터 (테이블, 행, 열) | 비정형 데이터 (문서, 키-값, 그래프 등) |
스키마 | 고정된 스키마 필요 | 스키마 유연 |
확장성 | 수직적 확장(서버 성능 업그레이드) | 수평적 확장(서버 추가) |
적용 사례 | 트랜잭션 중심 시스템(은행, ERP 등) | 비정형 데이터 처리(소셜 미디어, 로그 데이터 등) |
쉽게 이해하기
- RDBMS는 데이터를 "엑셀 표처럼" 테이블에 저장하고, 테이블 간 관계를 설정하여 데이터를 체계적으로 관리하는 시스템입니다.
- 예를 들어:
- 사용자 테이블: 사용자 이름, 이메일, ID.
- 주문 테이블: 주문 ID, 사용자 ID, 상품.
- 사용자 ID를 통해 두 테이블 간 관계를 설정.
추가로 알아야 할 점
- JPA와 RDBMS: JPA는 RDBMS의 데이터를 객체지향적으로 다루기 위한 기술입니다. RDBMS를 잘 이해하면 JPA 사용이 훨씬 수월해집니다.
- 트랜잭션 이해: RDBMS의 핵심은 트랜잭션과 무결성입니다. 이를 이해하면 애플리케이션 설계에 큰 도움이 됩니다.
[심층적인 구조]
데이터베이스 시스템(DBMS)의 구조 설명
위 이미지는 데이터베이스 시스템(DBMS)의 전체적인 구조를 나타낸 것입니다. 데이터베이스 시스템은 사용자 인터페이스, DBMS(데이터베이스 관리 시스템), 데이터베이스라는 3개의 주요 계층으로 구성됩니다. 각각의 구성 요소와 동작 방식은 다음과 같습니다.
1. 사용자 계층 (Interface Layer)
- 사용자: 데이터베이스를 직접 사용하거나, 애플리케이션을 통해 간접적으로 접근하는 주체입니다.
- 응용 프로그램 화면: 일반 사용자들이 데이터베이스와 상호작용하기 위해 사용하는 UI.
- 응용 프로그램 개발: 응용 프로그램 개발자가 SQL이나 프로그래밍 언어를 사용해 데이터베이스와 연동하는 로직을 작성.
- SQL 사용자: SQL을 통해 데이터를 직접 질의하거나 수정하는 사용자.
- DBA (Database Administrator): 데이터베이스 관리자로, 데이터베이스의 설계, 최적화, 권한 부여 등을 담당.
2. DBMS 계층 (Database Management System Layer)
이 계층은 데이터베이스의 데이터를 저장, 질의, 관리, 최적화하는 모든 기능을 담당합니다. 주로 아래의 요소들로 구성됩니다:
1) 컴파일러
- Embedded DML: 응용 프로그램 코드에 포함된 SQL 명령을 처리합니다.
- DML 컴파일러: 데이터 조작 언어(Data Manipulation Language)를 컴파일하여 실행.
- 예: SELECT, INSERT, UPDATE, DELETE 명령어.
- DDL 컴파일러: 데이터 정의 언어(Data Definition Language)를 해석하여 데이터베이스 구조를 생성 및 변경.
- 예: CREATE TABLE, ALTER TABLE.
2) 질의 처리기 (Query Processor)
- SQL 질의를 실행하기 위한 주요 컴포넌트:
- SQL Interface: SQL 명령어를 입력받는 인터페이스.
- Parser: SQL 명령어를 해석하여 쿼리 트리(Query Tree)로 변환.
- Optimizer: 최적의 실행 계획을 수립하여 쿼리를 효율적으로 처리.
- Caches & Buffers: 자주 사용하는 데이터를 캐시와 버퍼에 저장하여 성능 향상.
3) 트랜잭션 관리자 (Transaction Manager)
- 데이터의 무결성과 일관성을 보장하기 위해 ACID 특성을 관리:
- 원자성, 일관성, 고립성, 지속성.
4) 파일 관리자 및 버퍼 관리자
- 파일 관리자: 데이터 파일과 데이터베이스 파일 시스템 간의 인터페이스.
- 버퍼 관리자: 디스크 I/O를 최소화하고 메모리를 효율적으로 사용.
3. 데이터 계층 (Database Layer)
- 데이터베이스 계층은 데이터를 물리적으로 저장하고 관리하는 부분입니다.
- 데이터 파일: 실제 데이터를 저장하는 파일.
- 인덱스: 데이터를 빠르게 검색할 수 있도록 도와주는 구조.
- 데이터 통계: 데이터 분포에 대한 정보를 저장, 쿼리 최적화에 사용.
- 데이터 사전: 데이터베이스 메타데이터를 저장. 테이블, 열, 데이터 유형 등에 대한 정보를 포함.
전체적인 동작 흐름
- 사용자 입력
- 사용자가 SQL 질의를 실행하거나, 응용 프로그램을 통해 데이터베이스와 상호작용합니다.
- 질의 처리
- SQL 질의는 SQL Interface를 통해 입력되고, Parser와 Optimizer를 거쳐 실행 계획이 생성됩니다.
- 실행
- 실행 계획에 따라 트랜잭션 관리자와 파일 관리자가 협력하여 데이터를 검색, 수정, 저장합니다.
- 데이터 입출력
- 요청된 데이터는 데이터베이스 계층에서 검색되어 캐시/버퍼를 통해 사용자에게 반환됩니다.
특징 및 장점
- 모듈화
- 각 계층은 독립적으로 동작하면서도 서로 협력하여 효율적인 데이터 관리 가능.
- 효율적인 데이터 처리
- 쿼리 최적화, 캐싱, 트랜잭션 관리 등을 통해 성능을 극대화.
- 데이터 무결성 보장
- 트랜잭션 관리와 데이터 사전을 통해 데이터의 무결성과 일관성 유지.
- 확장성
- 다양한 사용자 요구를 충족하며, 대규모 데이터와 동시 사용자 환경에서도 안정적인 성능 제공.
쉽게 이해하기
이 구조는 사용자가 간단한 SQL 명령을 입력하면,
DBMS 내부에서 이를 해석, 최적화, 실행하고, 실제 데이터베이스에서 결과를 반환하는 일련의 과정을 보여주는 설계입니다.
궁금한 점이 있거나 더 설명이 필요한 부분이 있다면 말씀해주세요! 😊
객체 지향 프로그래밍(OOP)과 관계형 데이터베이스(RDB)의 패러다임 불일치
JPA의 등장 이유
- 객체 지향 프로그래밍(OOP)과 관계형 데이터베이스(RDB)는 서로 다른 방식으로 데이터를 표현합니다. 이로 인해 개발자는 객체와 데이터베이스 간의 불일치를 해결하기 위해 많은 작업을 해야 하며, 이 문제를 해결하기 위해 JPA(Java Persistence API)가 등장했습니다. 다음은 주요 내용을 정리한 것입니다.
1. 객체와 관계형 데이터베이스의 구조적 차이
- 객체 지향 모델:
클래스(설계도)를 기반으로 객체를 생성하며, 속성(필드)과 동작(메서드)을 포함. - 관계형 모델:
데이터를 테이블 형식으로 저장하며, 열(컬럼)과 행(로우)으로 구성.
이 두 모델의 표현 방식 차이로 인해, 데이터를 저장하거나 조회할 때 매핑 작업이 필수적입니다.
2. 반복되는 CRUD 작업
- 객체를 데이터베이스에 저장(Create), 조회(Read), 수정(Update), 삭제(Delete)하기 위해 개발자는 항상 SQL 쿼리를 직접 작성해야 합니다.
- 이로 인해 코드 중복이 발생하고, 유지보수가 어려워집니다.
3. 상속과 연관관계 매핑의 어려움
- 상속:
객체 지향 언어는 상속을 지원하지만, 관계형 데이터베이스는 이를 직접적으로 지원하지 않습니다. 따라서 별도의 매핑 작업이 필요합니다. - 연관관계:
객체는 참조를 통해 관계를 표현하지만, 데이터베이스는 **외래 키(Foreign Key)**와 **조인(JOIN)**을 통해 관계를 나타냅니다.
이 차이로 인해 객체 간의 관계를 데이터베이스에 저장하거나 조회하는 과정이 복잡해질 수 있습니다.
4. 객체 비교의 문제
- 동일한 데이터를 조회하더라도 다른 객체 인스턴스로 다뤄질 수 있습니다.
- 이로 인해 객체를 비교할 때 의도한 결과를 얻지 못할 가능성이 있습니다.
JPA의 등장
이러한 패러다임 불일치 문제를 해결하기 위해 JPA가 도입되었습니다.
JPA는 다음과 같은 방식으로 문제를 해소합니다:
- 객체를 자바 컬렉션(LIST, MAP등)처럼 다룰 수 있게 해줌.
- 객체와 데이터베이스 간의 매핑 작업을 자동화하여 개발자의 부담을 줄임.
JPA의 장점:
- 개발 생산성 향상
- 코드 중복 감소
- 유지보수성 강화
JDBC(Java Database Connectivity)
📌 JDBC(Java Database Connectivity*는 Java에서 데이터베이스와 상호작용하기 위한 표준 API입니다. 이를 통해 SQL 명령을 실행하고 데이터베이스와 연결하여 데이터를 조회하거나 변경할 수 있습니다.
- JDBC는 다양한 DBMS(MySQL, PostgreSQL, Oracle 등)와 연동하기 위한 기반 기술입니다.
JDBC의 주요 특징
- 표준화된 데이터베이스 액세스 API:
- Java와 데이터베이스 간의 통신을 위한 표준 인터페이스를 제공합니다.
- DBMS에 의존적이지 않으며, 다양한 데이터베이스 드라이버를 통해 활용 가능합니다.
- SQL 실행:
- 데이터베이스에서 CRUD 작업(Create, Read, Update, Delete)을 수행할 수 있습니다.
- DB 드라이버 기반 동작:
- 각 DBMS에 맞는 JDBC 드라이버를 통해 데이터베이스와 상호작용합니다.
- 저수준 API:
- 데이터베이스 연결, SQL 실행, 자원 관리 등 모든 과정을 개발자가 직접 제어해야 합니다.
- 광범위한 지원:
- 모든 주요 데이터베이스(MySQL, Oracle, PostgreSQL 등)와 연동 가능합니다.
JDBC의 주요 구성 요소
구성 요소 | 설명 |
DriverManager | 데이터베이스 연결을 관리하고, 적절한 드라이버를 선택해주는 역할을 합니다. |
Connection | 데이터베이스와의 연결을 나타내는 객체로, SQL 실행을 위한 Statement 객체를 생성합니다. |
Statement | SQL 쿼리를 실행하는 객체로, 정적 SQL 실행을 처리합니다. |
PreparedStatement | 파라미터화된 SQL 쿼리를 실행하는 객체로, 성능 향상과 보안(SQL Injection 방지)에 유리합니다. |
CallableStatement | Stored Procedure를 호출하는 데 사용되는 객체입니다. |
ResultSet | SQL 쿼리 실행 결과를 테이블 형식으로 나타내는 객체입니다. |
SQLException | 데이터베이스 작업 중 발생하는 예외를 처리하는 데 사용됩니다. |
[Statement]
Statement
Statement는 **JDBC(Java Database Connectivity)**에서 제공하는 인터페이스로, SQL 쿼리를 실행하고 결과를 처리하는 데 사용됩니다. 데이터베이스와 상호작용하기 위해 SQL을 실행하는 기본 도구이며, JDBC API의 핵심 구성 요소 중 하나입니다.
Statement의 특징
- SQL 실행:
- 정적 SQL 쿼리를 실행하는 데 사용됩니다.
- SQL 문자열을 전달하면 데이터베이스에서 실행하고 결과를 반환받습니다.
- 직접적인 SQL 실행:
- SQL 쿼리를 문자열로 작성하여 Statement 객체를 통해 실행합니다.
- SQL 쿼리에 파라미터를 포함하지 않고, 동적 데이터 삽입 시 문자열 연결을 사용합니다.
- 자원 관리 필요:
- Connection 객체를 통해 생성되고, 실행 후 반드시 닫아야 합니다.
Statement 사용 방법
1. Statement 객체 생성
Connection 객체를 사용하여 Statement 객체를 생성합니다.
Statement stmt = connection.createStatement();
2. SQL 실행
- 데이터 조회 (SELECT):
- executeQuery() 메서드로 쿼리를 실행하고, 결과를 ResultSet 객체로 반환받습니다.
String sql = "SELECT * FROM users"; ResultSet rs = stmt.executeQuery(sql); while (rs.next()) { int id = rs.getInt("id"); String name = rs.getString("name"); System.out.println("ID: " + id + ", Name: " + name); }
- 데이터 변경 (INSERT, UPDATE, DELETE):
- executeUpdate() 메서드로 쿼리를 실행하고, 영향을 받은 행(row) 수를 반환합니다.
String sql = "INSERT INTO users (name, email) VALUES ('John', 'john@example.com')"; int rowsInserted = stmt.executeUpdate(sql); System.out.println(rowsInserted + " row(s) inserted.");
- 기타 쿼리 실행:
- execute() 메서드로 쿼리를 실행하며, 실행 결과가 여러 종류(SELECT, UPDATE 등)일 수 있을 때 사용됩니다.
boolean isResultSet = stmt.execute("SOME SQL QUERY");
3. 자원 해제
- 실행 후 반드시 Statement와 ResultSet 객체를 닫아야 합니다.
rs.close(); stmt.close();
Statement의 한계와 문제점
- SQL Injection 위험:
- 문자열 연결을 통해 동적 SQL을 처리하므로, SQL 주입 공격(SQL Injection)에 취약합니다.
- 예:
사용자가 ' OR '1'='1 같은 값을 입력하면 보안 취약점이 발생합니다.String sql = "SELECT * FROM users WHERE username = '" + username + "'";
- 자원 관리 복잡성:
- Connection, Statement, ResultSet 등을 명시적으로 닫아야 하므로 코드가 복잡해질 수 있습니다.
- 반복 코드 증가:
- SQL 쿼리 실행 및 결과 처리 코드가 반복적으로 나타나 유지보수가 어려워질 수 있습니다.
- 비효율성:
- 같은 SQL 쿼리를 여러 번 실행할 때도 매번 컴파일되므로 성능이 떨어질 수 있습니다.
Statement의 개선된 대안
- PreparedStatement:
- 파라미터화된 쿼리를 지원하여 SQL Injection을 방지하고, 성능을 개선합니다.
- 예:
String sql = "SELECT * FROM users WHERE username = ?"; PreparedStatement pstmt = connection.prepareStatement(sql); pstmt.setString(1, "John"); ResultSet rs = pstmt.executeQuery();
- CallableStatement:
- Stored Procedure를 호출하는 데 사용됩니다.
- 예:
CallableStatement cstmt = connection.prepareCall("{call procedure_name(?)}"); cstmt.setInt(1, 123); ResultSet rs = cstmt.executeQuery();
- JDBC Template:
- Spring에서 제공하는 도구로, 자원 관리 및 SQL 실행을 단순화합니다.
- SQL Mapper (MyBatis):
- SQL 작성과 객체 매핑을 단순화하는 프레임워크.
- ORM (JPA/Hibernate):
- SQL 작성 없이 객체와 테이블을 매핑하여 데이터베이스 작업을 자동화.
Statement vs. PreparedStatement vs. CallableStatement 비교
Statement | PreparedStatement | CallableStatement | |
주요 목적 | 정적 SQL 실행 | 동적 SQL 실행 (파라미터화된 쿼리) | Stored Procedure 호출 |
SQL Injection 방지 | 불가능 | 가능 | 가능 |
성능 | 낮음 (쿼리 매번 컴파일) | 높음 (쿼리 미리 컴파일, 재사용 가능) | 높음 (Stored Procedure 최적화 가능) |
사용 방식 | 정적 SQL 문자열 작성 | ?로 파라미터 지정, 값 바인딩 | 프로시저 이름과 파라미터 지정 |
유연성 | 낮음 (복잡한 작업에 부적합) | 높음 (복잡한 SQL도 유연하게 처리 가능) | 낮음 (Stored Procedure 의존) |
결론
- Statement는 기본적인 SQL 실행 도구로, 단순한 SQL 작업에는 적합하지만, 보안이나 성능 면에서 한계가 있습니다.
- PreparedStatement는 SQL Injection을 방지하고 성능을 개선할 수 있어 권장되는 대안입니다.
- CallableStatement는 Stored Procedure를 사용하는 경우에만 사용됩니다.
- 실제 프로젝트에서는 Statement보다는 PreparedStatement 또는 상위 추상화 도구(예: JDBC Template, MyBatis, JPA)가 더 많이 사용됩니다. 😊
JDBC의 동작 순서
- JDBC 드라이버 로드:
- Java에서 데이터베이스에 접근하기 위해 해당 DBMS의 JDBC 드라이버를 로드합니다.
Class.forName("com.mysql.cj.jdbc.Driver");
- 데이터베이스 연결:
- DriverManager.getConnection() 메서드를 통해 데이터베이스와 연결합니다.
Connection connection = DriverManager.getConnection( "jdbc:mysql://localhost:3306/testdb", "username", "password");
- SQL 실행:
- Statement, PreparedStatement, 또는 CallableStatement 객체를 생성하여 SQL을 실행합니다.
Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM users");
- 결과 처리:
- ResultSet을 통해 SQL 실행 결과를 처리합니다.
while (rs.next()) { System.out.println("ID: " + rs.getInt("id")); System.out.println("Name: " + rs.getString("name")); }
- 자원 해제:
- 데이터베이스 연결 및 모든 자원을 닫아야 합니다.
rs.close(); stmt.close(); connection.close();
JDBC 주요 메서드
메서드 | 설명 |
executeQuery() | SELECT 쿼리를 실행하고, 결과를 ResultSet 객체로 반환합니다. |
executeUpdate() | INSERT, UPDATE, DELETE와 같은 데이터 변경 작업을 실행하며, 영향을 받은 행(row) 수를 반환합니다. |
execute() | SELECT, INSERT 등 모든 유형의 SQL을 실행하며, 결과가 ResultSet인지 아닌지를 알 수 없을 때 사용합니다. |
prepareStatement() | 파라미터화된 SQL 쿼리를 실행하는 PreparedStatement 객체를 생성합니다. |
prepareCall() | Stored Procedure를 호출하는 CallableStatement 객체를 생성합니다. |
close() | 데이터베이스 연결, Statement, ResultSet 등의 자원을 닫습니다. |
JDBC의 장점과 단점
장점
- 데이터베이스 독립성:
- Java 표준 인터페이스를 사용하므로 다양한 DBMS와 연동 가능.
- 유연성:
- SQL을 자유롭게 작성하고, DBMS 고유의 기능도 사용할 수 있음.
- 직접 제어:
- SQL과 데이터베이스 작업을 세부적으로 제어할 수 있음.
단점
- 반복 코드:
- 연결, 자원 관리, 예외 처리 등 반복적인 코드가 많음.
- SQL Injection 위험:
- Statement 사용 시 동적 SQL에서 보안 취약점이 발생할 수 있음.
- 복잡한 자원 관리:
- Connection, Statement, ResultSet 등을 명시적으로 닫아야 함.
- 생산성 부족:
- CRUD 같은 간단한 작업도 많은 코드가 필요함.
결론
- JDBC는 Java와 데이터베이스 간의 기본적인 통신을 가능하게 하는 표준 API입니다.
- 저수준 API로 유연하지만, 반복적인 코드와 자원 관리의 복잡성 때문에 실제 프로젝트에서는 JDBC Template, MyBatis, JPA/Hibernate 같은 고수준 프레임워크가 선호됩니다.
- 하지만 고수준 프레임워크의 대표격인 JPA도 JDBC를 바탕으로 움직이며, JPA가 해결 못하는 경우 JDBC가 사용되기도 합니다.
- 하지만, SQL 직접 제어가 필요한 경우나 간단한 데이터베이스 작업에서는 여전히 JDBC가 유용하게 사용됩니다
JPA(Java Persistence API)
📌 자바 애플리케이션에서 관계형 데이터베이스(RDB)를 다루기 위한 표준 인터페이스입니다.
- JPA는 자바 객체와 관계형 데이터베이스 간의 **매핑(ORM: Object-Relational Mapping)**을 제공하여, 개발자가 SQL 중심이 아닌 객체 중심으로 데이터를 처리할 수 있도록 돕습니다.
JPA의 주요 특징
- ORM (Object-Relational Mapping) 기술
- 자바 객체와 데이터베이스 테이블을 매핑.
- 객체 필드는 테이블 컬럼에, 객체 간의 관계는 외래 키(FK)에 매핑.
- SQL 추상화
- 데이터베이스와의 상호작용을 위한 SQL을 직접 작성하지 않아도 됨.
- 개발자는 자바 코드로 데이터를 조작하며, JPA가 필요한 SQL을 자동 생성.
- POJO 기반 개발
- JPA 엔티티 클래스는 Plain Old Java Object(POJO)로 작성.
- 특정 라이브러리에 종속되지 않으며, 일반적인 자바 클래스를 사용할 수 있음.
-
더보기
POJO란?
특별한 규칙이나 라이브러리 의존 없이 작성된 순수한 자바 객체를 의미합니다.
즉, 자바의 기본 문법으로 작성된 간단한 객체로, 특정 프레임워크나 기술에 종속되지 않습니다.
POJO의 특징
- 순수한 자바 객체
- 자바 문법으로 작성된 일반적인 클래스.
- 상속받아야 하거나 특정 인터페이스를 구현해야 하는 제약이 없음.
- 독립적
- 특정 프레임워크나 라이브러리에 의존하지 않음.
- 예를 들어, EJB(Enterprise JavaBeans)와 같은 복잡한 환경과 반대되는 개념.
- 간결하고 직관적
- 클래스의 속성(필드)과 이를 조작하기 위한 메서드(getter, setter 등)만 포함.
POJO의 예시
POJO 클래스
public class User { private String name; private int age; // 기본 생성자 public User() {} // 매개변수 있는 생성자 public User(String name, int age) { this.name = name; this.age = age; } // Getter와 Setter public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
사용
public class Main { public static void main(String[] args) { User user = new User("Alice", 25); System.out.println(user.getName()); // Alice user.setAge(26); System.out.println(user.getAge()); // 26 } }
POJO가 중요한 이유
- 단순성
- 복잡한 규약 없이도 자바 기본 문법으로 개발 가능.
- 테스트 용이성
- 외부 의존성이 없으므로 단위 테스트(Unit Test)를 쉽게 작성할 수 있음.
- 프레임워크 독립성
- 특정 프레임워크에 종속되지 않기 때문에 코드가 재사용 가능하고 이식성이 높음.
- JPA와의 연관성
- JPA의 엔티티(Entity)는 POJO로 작성되므로, 객체 지향적인 코드를 유지할 수 있음.
- 예: JPA의 @Entity 클래스는 POJO의 형태를 따름.
POJO와 다른 개념의 비교
POJO JavaBean 단순한 자바 객체 POJO의 일종으로, 특정 규약을 따름 규칙 없음 기본 생성자, Getter/Setter 필수 특정 프레임워크 의존 없음 JavaBean 표준 스펙 준수
결론
POJO는 자바 프로그래밍의 기본적인 단순 객체로, 복잡한 제약 없이 작성되어 코드의 가독성과 유지보수성을 높여줍니다. JPA, 스프링과 같은 프레임워크에서 핵심적으로 사용되는 객체 형태입니다.
- 순수한 자바 객체
- DBMS 독립성
- JPA는 DBMS에 종속되지 않으며, 설정에 따라 MySQL, PostgreSQL, Oracle 등 다양한 데이터베이스에서 작동.
- 캐싱 지원
- 1차 캐시(영속성 컨텍스트)와 2차 캐시를 통해 성능을 최적화.
JPA의 동작 원리
- 엔티티(Entity)
- 데이터베이스 테이블에 매핑되는 자바 클래스.
- 예: @Entity 어노테이션을 사용하여 매핑.
- 영속성 컨텍스트(Persistence Context)
- 엔티티 객체를 관리하는 JPA의 내부 메커니즘.
- 데이터베이스와의 연결을 관리하며, 엔티티를 캐싱해 성능을 최적화.
- EntityManager
- 엔티티의 생성, 조회, 수정, 삭제 작업을 담당하는 JPA의 핵심 인터페이스.
- JPA를 통해 데이터베이스 작업을 처리하는 주요 도구.
JPA의 주요 동작
상태 | 설명 |
비영속(New) | 엔티티가 영속성 컨텍스트에 저장되지 않은 상태. |
영속(Managed) | 엔티티가 영속성 컨텍스트에 저장되어 데이터베이스와 동기화되는 상태. |
준영속(Detached) | 영속성 컨텍스트에서 분리된 상태로, 데이터베이스와 동기화되지 않음. |
삭제(Removed) | 엔티티가 영속성 컨텍스트에서 제거되고 데이터베이스에서 삭제 대기 상태. |
JPA를 사용하는 이유
- 객체 중심 개발
- SQL 중심 개발에서 객체 중심 개발로 전환 가능.
- 데이터베이스 작업 시 객체 지향 패러다임을 유지.
- 생산성 향상
- CRUD 작업에 필요한 SQL을 직접 작성하지 않아도 됨.
- 코드의 중복 제거, 유지보수 용이.
- DBMS 독립성
- 데이터베이스 종류에 관계없이 동일한 코드로 동작 가능.
- 성능 최적화
- 1차 캐시, 지연 로딩(Lazy Loading), 벌크 작업 최적화 등 다양한 성능 향상 기법 제공.
- 트랜잭션 관리
- JPA는 트랜잭션 경계 내에서 데이터 일관성과 무결성을 보장.
JPA 주요 어노테이션
어노테이션 | 설명 |
@Entity | 클래스가 데이터베이스 테이블과 매핑됨을 나타냄. |
@Table | 엔티티가 매핑될 테이블 이름을 지정. |
@Id | 기본 키를 지정. |
@GeneratedValue | 기본 키의 자동 생성 전략을 지정. |
@Column | 엔티티의 필드를 테이블의 컬럼과 매핑. |
@ManyToOne | 다대일 관계를 매핑. |
@OneToMany | 일대다 관계를 매핑. |
@JoinColumn | 외래 키를 지정. |
JPA vs Hibernate
- JPA: 자바에서 ORM을 위한 표준 인터페이스.
- Hibernate: JPA의 구현체 중 하나로, 추가적인 기능을 제공.
JPA 사용 예시
1. 엔티티 클래스
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
private int age;
// Getters and Setters
}
2. 데이터 저장
EntityManager em = entityManagerFactory.createEntityManager();
em.getTransaction().begin();
User user = new User();
user.setName("John");
user.setAge(30);
em.persist(user); // 데이터 저장
em.getTransaction().commit();
3. 데이터 조회
User user = em.find(User.class, 1L); // ID가 1인 사용자 조회
System.out.println(user.getName());
JPA의 장단점
장점
- 객체 지향 패러다임과 데이터베이스 간의 불일치 해결.
- SQL 작성 없이 객체 중심으로 데이터 처리.
- 데이터베이스 변경 시 코드 변경 최소화.
- 트랜잭션, 캐싱, 성능 최적화 기능 내장.
단점
- 학습 곡선이 있음 (초기 학습 난이도).
- 복잡한 쿼리는 JPQL로 처리해야 하며, SQL보다 직관성이 떨어질 수 있음.
- 대규모 프로젝트에서는 잘못된 설정으로 성능 저하 가능.
결론
JPA는 자바 애플리케이션에서 관계형 데이터베이스를 효율적으로 다루기 위한 표준화된 솔루션입니다. 객체 중심 개발을 가능하게 하며, 생산성과 유지보수성을 크게 향상시킬 수 있습니다.
트랜잭션
🐳 데이터베이스에서 작업의 논리적인 단위를 의미합니다. 트랜잭션은 하나의 작업 단위를 구성하며, 이 단위가 완전히 성공하거나 실패해야 데이터의 무결성을 보장합니다.
트랜잭션의 동작 예시
1) 은행 송금 시스템
- 계좌 A에서 100원을 인출하고, 계좌 B에 100원을 입금하는 작업:
BEGIN TRANSACTION
1. 계좌 A에서 100원 출금
2. 계좌 B에 100원 입금
IF 모든 작업 성공:
COMMIT
ELSE:
ROLLBACK
- 두 작업(출금과 입금)은 하나의 트랜잭션으로 처리되며, 둘 중 하나라도 실패하면 롤백되어 데이터 무결성을 유지합니다.
트랜잭션의 특징 (ACID 속성)
- 원자성 (Atomicity):
- 트랜잭션 내의 모든 작업이 모두 성공하거나 모두 실패해야 합니다.
- 일부 작업만 실행되고 나머지가 실패하면, 전체 작업을 취소(롤백)하여 데이터 일관성을 유지합니다.
- 일관성 (Consistency):
- 트랜잭션이 성공적으로 완료되면, 데이터베이스가 항상 일관성 있는 상태로 유지됩니다.
- 예: 은행 송금 시, 한 계좌에서 돈을 빼면 다른 계좌에 같은 금액이 추가되어야 함.
- 고립성 (Isolation):
- 여러 트랜잭션이 동시에 실행될 경우, 각 트랜잭션은 서로 독립적으로 실행되어야 합니다.
- 한 트랜잭션의 중간 결과가 다른 트랜잭션에 노출되지 않음.
- 지속성 (Durability):
- 트랜잭션이 커밋된 후에는 영구적으로 데이터베이스에 반영되어야 합니다.
- 서버가 중단되거나 시스템 장애가 발생해도 데이터는 손실되지 않습니다.
트랜잭션의 상태
- 활성 (Active):
- 트랜잭션이 시작되고 작업이 진행 중인 상태.
- 부분 완료 (Partially Committed):
- 트랜잭션의 마지막 명령이 실행되었지만, 아직 커밋되지 않은 상태.
- 완료 (Committed):
- 트랜잭션이 성공적으로 완료되어 데이터베이스에 반영된 상태.
- 실패 (Failed):
- 트랜잭션이 오류로 인해 중단된 상태.
- 철회 (Aborted):
- 트랜잭션이 실패하거나 취소되어 롤백된 상태.
트랜잭션의 처리 과정
- 트랜잭션 시작:
- 트랜잭션을 시작하여 작업 단위를 정의.
- 작업 실행:
- 트랜잭션 내에서 여러 데이터베이스 작업(쿼리, 삽입, 업데이트, 삭제 등)을 실행.
- 커밋 또는 롤백:
- 모든 작업이 성공하면 커밋하여 데이터베이스에 변경 사항을 반영.
- 작업 중 오류가 발생하면 롤백하여 변경 사항을 취소.
Spring에서의 트랜잭션 관리
Spring은 프록시 기반 AOP를 사용하여 트랜잭션 관리를 제공합니다. Spring의 트랜잭션 관리 기능은 선언적 방식과 프로그래밍 방식으로 사용 가능합니다.
1. 선언적 트랜잭션 관리
Spring에서는 @Transactional 어노테이션을 사용하여 선언적으로 트랜잭션을 관리할 수 있습니다.
@Service
public class UserService {
@Transactional
public void createUser(User user) {
userRepository.save(user);
emailService.sendWelcomeEmail(user); // 예외 발생 시 전체 롤백
}
}
- @Transactional이 붙은 메서드는 트랜잭션이 시작되고, 예외 발생 시 롤백됩니다.
- 커밋은 메서드 실행이 성공적으로 종료되면 수행됩니다.
2. 프로그래밍 방식 트랜잭션 관리
프로그래밍 방식으로 PlatformTransactionManager를 사용하여 트랜잭션을 직접 관리할 수 있습니다.
@Service
public class UserService {
@Autowired
private PlatformTransactionManager transactionManager;
public void createUser(User user) {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
userRepository.save(user);
emailService.sendWelcomeEmail(user);
transactionManager.commit(status); // 성공 시 커밋
} catch (Exception e) {
transactionManager.rollback(status); // 실패 시 롤백
}
}
}
트랜잭션 전파 수준 (Propagation)
트랜잭션 전파(Propagation)는 트랜잭션이 다른 메서드 호출 시 어떻게 동작할지 정의합니다.
전파 속성 | 설명 |
REQUIRED | 기본값. 기존 트랜잭션이 있으면 참여하고, 없으면 새 트랜잭션 생성. |
REQUIRES_NEW | 항상 새 트랜잭션을 생성. 기존 트랜잭션은 일시 중단. |
NESTED | 중첩 트랜잭션을 생성. 롤백은 부모 트랜잭션과 독립적. |
MANDATORY | 기존 트랜잭션이 없으면 예외 발생. |
SUPPORTS | 트랜잭션이 있으면 참여, 없으면 트랜잭션 없이 실행. |
NOT_SUPPORTED | 항상 트랜잭션 없이 실행. 기존 트랜잭션은 일시 중단. |
NEVER | 트랜잭션 없이 실행하며, 기존 트랜잭션이 있으면 예외 발생. |
트랜잭션 격리 수준 (Isolation)
트랜잭션 격리 수준은 동시에 실행되는 트랜잭션 간의 상호작용을 제어합니다.
격리 수준 | 설명 |
DEFAULT | 데이터베이스의 기본 격리 수준을 따름. |
READ_UNCOMMITTED | 다른 트랜잭션이 커밋하지 않은 데이터도 읽을 수 있음. (Dirty Read 가능) |
READ_COMMITTED | 다른 트랜잭션이 커밋한 데이터만 읽을 수 있음. |
REPEATABLE_READ | 같은 트랜잭션 내에서 동일 데이터를 반복적으로 읽어도 동일한 결과를 보장. |
SERIALIZABLE | 가장 높은 격리 수준. 트랜잭션을 순차적으로 실행하여 충돌 방지. |
트랜잭션 롤백 조건
Spring의 @Transactional은 기본적으로 런타임 예외가 발생할 때 롤백합니다.
롤백 예외 설정
@Transactional(rollbackFor = Exception.class)
public void process() {
// Exception 발생 시 롤백
}
롤백 제외 설정
@Transactional(noRollbackFor = CustomException.class)
public void process() {
// CustomException 발생 시 롤백하지 않음
}
트랜잭션의 장점
- 데이터 무결성 보장:
- 작업 도중 오류가 발생해도 데이터 손상 방지.
- 동시성 제어:
- 여러 사용자가 동시에 데이터베이스에 접근해도 데이터 충돌 방지.
- 복잡한 작업 관리:
- 여러 작업 단위를 하나의 트랜잭션으로 묶어 관리 가능.
- 자동화된 관리:
- Spring의 트랜잭션 관리를 통해 선언적으로 간단하게 설정 가능.
정리
**트랜잭션(Transaction)**은 하나의 논리적인 작업 단위를 정의하며, 데이터베이스의 무결성과 일관성을 보장합니다. Spring은 선언적 트랜잭션(@Transactional)을 제공하여 개발자가 쉽게 트랜잭션을 관리할 수 있도록 돕습니다.
핵심 개념:
- ACID 속성: 원자성, 일관성, 고립성, 지속성.
- Spring의 지원: 선언적(@Transactional), 프로그래밍 방식.
- 전파와 격리 수준: 트랜잭션의 실행 방식을 제어.
트랜잭션 전파
🐳 하나의 트랜잭션이 다른 트랜잭션 내에서 어떻게 동작할지를 결정하는 규칙으로 여러 개의 트랜잭션이 포함된 시스템에서 특정 작업이 다른 작업에 어떻게 영향을 미칠지를 정의한다.
- 현재 클래스의 트랜잭션과 다른 클래스의 트랜잭션을 교통정리 한다.
- 트랜잭션이 여러 계층 또는 메서드에서 어떻게 처리될지 정의한다.(@Transactional)
- propagation 속성을 통해 트랜잭션의 동작 방식을 제어할 수 있다.
- 다양한 비즈니스 요구 사항에 맞춰 복잡한 트랜잭션 흐름을 유연하게 설계할 수 있도록 돕는다.
- 데이터 무결성과 비지니스 로직의 안정성을 보장할 수 있다.
- 코드 예시
- REQUIRED(Default) 사용
@Service
@RequiredArgsConstructor
public class MemberService {
private final PointPolicy pointPolicy;
@Transactional
public void signUp(Member member) {
// 회원 등록
memberRepository.save(member);
// 포인트 지급
pointPolicy.addPoints(member.getId(), 100);
}
}
@Component
public class PointPolicy {
public void addPoints(Long memberId, int points) {
// 포인트 지급 로직
pointRepository.save(new Point(memberId, points));
}
}
- signUp() 메서드에 @Transactional 을 통해 트랜잭션 설정
- 하위 addPoints() 메서드에 트랜잭션이 전파된다.
- 하위 메서드가 실패하면 롤백된다.
- 포인트 지급 로직에서 문제가 발생해도 회원 등록은 롤백된다.
- 트랜잭션 동작
- 트랜잭션 전파 종류
- propagation 속성
- REQUIRED(Default)
- 기존 트랜잭션이 있다면 기존 트랜잭션을 사용한다.
- 기존 트랜잭션이 없다면 트랜잭션을 새로 생성한다.
- REQUIRES_NEW
- 항상 새로운 트랜잭션을 시작하고, 기존의 트랜잭션은 보류한다.
- 두 트랜잭션은 독립적으로 동작한다.
- SUPPORTS
- 기존 트랜잭션이 있으면 해당 트랜잭션을 사용한다.
- 기존 트랜잭션이 없으면 트랜잭션 없이 실행한다.
- NOT_SUPPORTED
- 기존 트랜잭션이 있어도 트랜잭션을 중단하고 트랜잭션 없이 실행된다.
- MANDATORY
- 기존 트랜잭션이 반드시 있어야한다.
- 트랜잭션이 없으면 실행하지 않고 예외를 발생시킨다.
- NEVER
- 트랜잭션 없이 실행되어야 한다.
- 트랜잭션이 있으면 예외를 발생시킨다.
- NESTED
- 현재 트랜잭션 내에서 중첩 트랜잭션을 생성한다.
- 중첩 트랜잭션은 독립적으로 롤백할 수 있다.
- 기존 트랜잭션이 Commit되면 중첩 트랜잭션도 Commit 된다.
- REQUIRED(Default)
- propagation 속성
- REQUIRES_NEW
- 회원 가입과 동시에 회원에게 포인트를 지급해야 하는 경우
@Service
@RequiredArgsConstructor
public class MemberService {
private final PointPolicy pointPolicy;
@Transactional
public void signUp(Member member) {
// 회원 등록
memberRepository.save(member);
// 포인트 지급
pointPolicy.addPoints(member.getId(), 100);
}
}
@Component
public class PointPolicy {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addPoints(Long memberId, int points) {
// 포인트 지급 로직
pointRepository.save(new Point(memberId, points));
}
}
- 요구사항 : 포인트 지급에 **실패**해도 회원가입은 완료되어야 한다.
- REQUIRES_NEW 설정(독립적인 트랜잭션 유지)
- 포인트 지급 로직에서 문제가 발생해도 회원 등록은 롤백되지 않는다.
- 트랜잭션 동작
DB Lock
https://kyunghun0515.tistory.com/161
Spring Boot와 JPA
https://kyunghun0515.tistory.com/107
[아래의 내용은 Spring과 DB의 상호작용에 대한 내용입니다.]
매핑(Mappin)
📌 객체(Object)와 데이터베이스 테이블(Table)을 연결하는 과정을 의미합니다. 매핑은 객체지향 프로그래밍 언어와 관계형 데이터베이스 간의 불일치를 해결하고, 데이터를 저장하거나 조회할 때 이를 자동화하는 데 도움을 줍니다.
주요 매핑 유형
1. 엔티티와 테이블 매핑
- 객체를 데이터베이스 테이블과 매핑합니다.
- @Entity와 @Table 애너테이션 사용.
- 예시:
@Entity @Table(name = "users") public class User { @Id private Long id; private String name; }
2. 필드와 컬럼 매핑
- 객체의 필드를 데이터베이스의 컬럼과 매핑합니다.
- @Column 애너테이션 사용.
- 예시:
@Column(name = "user_name", nullable = false, length = 100) private String name;
3. 기본 키 매핑
- 엔티티의 식별자를 데이터베이스의 기본 키와 매핑합니다.
- @Id와 @GeneratedValue 사용.
- 예시:
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
4. 연관관계 매핑
- 객체 간의 관계를 데이터베이스의 외래 키로 매핑합니다.
- @OneToOne, @OneToMany, @ManyToOne, @ManyToMany 애너테이션 사용.
- 예시:
@OneToMany(mappedBy = "user") private List<Order> orders;
5. 임베디드 매핑
- 재사용 가능한 값 타입 객체를 다른 엔티티에 매핑.
- @Embeddable과 @Embedded 사용.
- 예시:
@Embeddable public class Address { private String city; private String street; } @Entity public class User { @Embedded private Address address; }
매핑의 주요 목적
- 객체-관계 변환: 객체의 필드와 테이블의 컬럼 간 변환 자동화.
- 재사용성: 임베디드 객체를 통해 코드 재사용 가능.
- 유지보수성: 객체 중심으로 설계하면 유지보수가 쉬워짐.
- 생산성 향상: CRUD 작업 자동화.
매핑 사용 시 주의점
- 필드와 컬럼 간 데이터 타입 일치 확인.
- 복잡한 연관관계 설계 시 성능 고려.
- 기본 키 전략 선택의 중요성.
- 임베디드 타입 사용 시 중복 컬럼 방지.
연관관계 매핑
🐳 객체 간의 관계를 데이터베이스의 테이블 간의 외래 키 관계로 변환하는 과정을 의미합니다. 객체지향 언어에서는 객체 간 참조로 관계를 표현하지만, 관계형 데이터베이스에서는 외래 키를 사용하여 테이블 간 관계를 표현합니다.
1. 연관관계 매핑 종류
관계 | 데이터베이스 관계 설명 | |
1:1 | 1:1 | 한 객체가 다른 객체와 1:1로 매핑됩니다. |
N:1 | N:1 | 여러 객체가 한 객체와 연결됩니다. (다수 엔티티가 하나의 엔티티를 참조) |
1:N | 1:N | 한 객체가 여러 객체를 참조합니다. (하나의 엔티티가 다수 엔티티를 관리) |
N:M | N:M | 다수의 객체가 다수의 객체와 연결됩니다. (테이블에서는 중간 테이블을 사용하여 구현) |
2. 연관관계 매핑 주요 어노테이션
어노테이션 | 설명 |
@OneToOne | 1:1 관계를 매핑. |
@OneToMany | 1:N 관계를 매핑. |
@ManyToOne | N:1 관계를 매핑. |
@ManyToMany | N:M 관계를 매핑. |
@JoinColumn | 외래 키 컬럼을 명시적으로 매핑. |
@JoinTable | N:M 관계에서 중간 테이블을 명시적으로 매핑. |
mappedBy | 연관 관계의 주인을 지정. 주인은 데이터베이스의 외래 키를 관리. |
cascade | 영속성 전이 전략 설정. |
fetch | 연관 엔티티 로딩 방식 설정 (즉시 로딩 EAGER vs 지연 로딩 LAZY). |
3. 연관관계 매핑 상세
(1) @OneToOne (1:1 관계)
- 한 객체가 다른 객체와 1:1로 매핑.
- 외래 키를 어느 쪽에 설정할지 결정해야 함.
- @JoinColumn으로 외래 키를 명시적으로 지정.
예제
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToOne
@JoinColumn(name = "profile_id") // 외래 키 설정
private Profile profile;
}
@Entity
public class Profile {
@Id
@GeneratedValue
private Long id;
private String bio;
}
(2) @ManyToOne (N:1 관계)
- 여러 객체가 하나의 객체를 참조.
- 관계형 데이터베이스에서 외래 키가 있는 테이블은 다(N) 쪽.
예제
@Entity
public class Order {
@Id
@GeneratedValue
private Long id;
private String product;
@ManyToOne
@JoinColumn(name = "user_id") // 외래 키 설정
private User user;
}
(3) @OneToMany (1:N 관계)
- 한 객체가 여러 객체를 관리.
- 관계형 데이터베이스에서는 다(N) 쪽이 외래 키를 관리하므로, mappedBy로 외래 키 주인을 설정해야 함.
예제
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "user") // 외래 키 관리 주인 설정
private List<Order> orders = new ArrayList<>();
}
@Entity
public class Order {
@Id
@GeneratedValue
private Long id;
private String product;
@ManyToOne
@JoinColumn(name = "user_id") // 외래 키 설정
private User user;
}
(4) @ManyToMany (N:M 관계)
- 다수의 객체가 다수의 객체와 연결.
- 관계형 데이터베이스에서는 중간 테이블을 통해 구현.
- JPA에서는 @JoinTable을 사용해 중간 테이블을 정의.
예제
@Entity
public class Student {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToMany
@JoinTable(
name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private List<Course> courses = new ArrayList<>();
}
@Entity
public class Course {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToMany(mappedBy = "courses") // 관계의 주인이 아님
private List<Student> students = new ArrayList<>();
}
4. mappedBy
- mappedBy는 연관 관계의 주인을 설정하는 데 사용.
- 주인: 외래 키를 관리하는 쪽.
- mappedBy 속성: 주인이 아닌 엔티티에서 주인을 지정.
예제
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "user") // Order 엔티티의 user 필드가 주인임을 지정
private List<Order> orders = new ArrayList<>();
}
@Entity
public class Order {
@Id
@GeneratedValue
private Long id;
private String product;
@ManyToOne
@JoinColumn(name = "user_id") // 외래 키 주인
private User user;
}
5. Fetch 전략
- 연관 관계 매핑에서 데이터 조회 시점을 설정.
- FetchType.LAZY (지연 로딩):
- 연관된 엔티티를 실제로 사용할 때 데이터베이스에서 로드.
- 기본값: @OneToMany, @ManyToMany.
- FetchType.EAGER (즉시 로딩):
- 연관된 엔티티를 즉시 로드 (JOIN 사용).
- 기본값: @OneToOne, @ManyToOne.
6. 연관관계 매핑 시 주의사항
- 단방향과 양방향 관계
- 단방향 관계: 한 방향으로만 참조.
- 양방향 관계: 양쪽 엔티티에서 서로 참조.
- mappedBy 설정
- 양방향 관계에서는 반드시 mappedBy를 통해 주인을 설정.
- N+1 문제
- 지연 로딩 사용 시, 연관된 데이터가 많아지면 불필요한 쿼리가 다수 발생(N+1 문제).
- 해결 방법:
- FETCH JOIN 사용.
- 배치 페치 크기 조정 (hibernate.default_batch_fetch_size).
- Cascade 설정
- 부모 엔티티와 연관된 자식 엔티티를 함께 저장, 삭제 등 자동 관리.
7. 연관관계 매핑 요약
연관관계 | 설명 예제 | 어노테이션 |
1:1 | 한 객체가 다른 객체와 1:1로 매핑. | @OneToOne |
N:1 | 여러 객체가 한 객체를 참조. | @ManyToOne |
1:N | 한 객체가 여러 객체를 관리. | @OneToMany(mappedBy = "parent") |
N:M | 다수의 객체가 다수의 객체와 연결. | @ManyToMany, @JoinTable |
결론
JPA의 연관관계 매핑은 객체 지향적인 설계를 데이터베이스 관계형 모델에 반영할 수 있게 해줍니다. 매핑을 설정할 때는 **연관 관계의 방향성, 외래 키의 관리 주체, 성능 최적화(Fetch 전략)**를 고려하여 설계하는 것이 중요합니다.
추가적으로 궁금한 점이나 더 깊은 설명이 필요하면 언제든 말씀해주세요! 😊
상속관계 매핑
🐳 객체 지향 프로그래밍의 상속 구조를 관계형 데이터베이스 테이블에 매핑하는 방법입니다. 상속 구조를 데이터베이스에서 효율적으로 표현하기 위해 JPA는 3가지 상속 전략을 제공합니다.
1. 상속관계 매핑 전략
전략 | 설명 |
단일 테이블 전략 (SINGLE_TABLE) | 부모 클래스와 자식 클래스를 하나의 테이블에 매핑. |
조인 전략 (JOINED) | 부모 클래스와 자식 클래스를 각각 테이블로 매핑하고 JOIN으로 조회. |
테이블별 클래스 전략 (TABLE_PER_CLASS) | 부모 클래스는 테이블 없이, 자식 클래스마다 개별 테이블 생성. |
2. 주요 어노테이션
어노테이션 | 설명 |
@Inheritance | 상속 전략을 정의. 사용 가능한 값: SINGLE_TABLE, JOINED, TABLE_PER_CLASS. |
@DiscriminatorColumn | 단일 테이블 전략 또는 조인 전략에서 엔티티 구분 컬럼을 정의. |
@DiscriminatorValue | 특정 엔티티가 구분 컬럼에서 가질 값을 정의. |
dtype (Discriminator Column)
📌 JPA에서 상속관계 매핑 시 구분 컬럼(Discriminator Column)을 나타냅니다. 이는 부모 테이블이나 단일 테이블 전략에서 데이터가 어느 자식 엔티티에 해당하는지 구분하기 위해 사용됩니다. **단일 테이블 전략(SINGLE_TABLE)**과 **조인 전략(JOINED)**에서 사용됩니다.
1. dtype의 기본 동작
- Discriminator Column (dtype)
- 부모 테이블에 추가되는 컬럼으로, 해당 데이터가 어느 자식 클래스에 해당하는지를 나타냄.
- 기본 이름은 **dtype**이며, 값을 통해 엔티티를 구분.
- 기본값:
- 컬럼 이름: dtype
- 컬럼 타입: VARCHAR
- 자식 클래스 이름이 값으로 설정됨.
- Discriminator Value (@DiscriminatorValue)
- 특정 자식 엔티티가 구분 컬럼에서 가질 값을 지정.
- 기본값은 자식 클래스의 이름.
2. dtype 기본 설정
(1) 기본 설정 예제
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) // 단일 테이블 전략
@DiscriminatorColumn(name = "dtype") // 구분 컬럼 이름 설정
public abstract class Item {
@Id
@GeneratedValue
private Long id;
private String name;
}
@Entity
@DiscriminatorValue("B") // dtype 값 설정
public class Book extends Item {
private String author;
}
@Entity
@DiscriminatorValue("M") // dtype 값 설정
public class Movie extends Item {
private String director;
}
(2) 생성된 테이블 구조
- Item (부모) 테이블ID NAME AUTHOR DIRECTOR DTYPE
1 Book 1 Author A NULL B 2 Movie 1 NULL Director A M
3. @DiscriminatorColumn
- 구분 컬럼 정의 어노테이션으로 부모 클래스에 설정.
- 속성:
- name: 구분 컬럼의 이름을 지정 (기본값: dtype).
- length: 구분 컬럼의 길이 (기본값: 31).
- discriminatorType: 구분 컬럼의 데이터 타입을 설정 (기본값: DiscriminatorType.STRING).
- DiscriminatorType.STRING: 문자열 값 (기본값).
- DiscriminatorType.CHAR: 단일 문자 값.
- DiscriminatorType.INTEGER: 정수 값.
사용 예시
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "item_type", discriminatorType = DiscriminatorType.STRING, length = 10)
public abstract class Item {
@Id
@GeneratedValue
private Long id;
private String name;
}
4. @DiscriminatorValue
- 자식 클래스에서 구분 컬럼의 값을 명시적으로 설정.
- 설정하지 않으면 자식 클래스의 이름이 기본값으로 사용됨.
사용 예시
@Entity
@DiscriminatorValue("BookType") // dtype 값 설정
public class Book extends Item {
private String author;
}
@Entity
@DiscriminatorValue("MovieType") // dtype 값 설정
public class Movie extends Item {
private String director;
}
생성된 테이블
ID NAME AUTHOR DIRECTOR ITEM_TYPE1 | Book 1 | Author A | NULL | BookType |
2 | Movie 1 | NULL | Director A | MovieType |
5. 기본값과 사용자 지정
기본값 | 사용자 지정 | |
컬럼 이름 | dtype | @DiscriminatorColumn(name = "column_name") |
컬럼 타입 | DiscriminatorType.STRING | DiscriminatorType.INTEGER 또는 DiscriminatorType.CHAR |
구분 값 | 클래스 이름 | @DiscriminatorValue("value") |
6. Discriminator Column 전략별 동작
전략 | 구분 컬럼 사용 여부 | 구분 컬럼 위치 | 설명 |
SINGLE_TABLE (단일 테이블) | 사용 | 부모 테이블 | 단일 테이블에서 dtype 컬럼으로 엔티티를 구분. |
JOINED (조인 전략) | 사용 | 부모 테이블 | 부모 테이블에 dtype 컬럼이 추가되어, 데이터가 어느 자식 테이블에 속하는지 구분. |
TABLE_PER_CLASS | 사용 안 함 | 없음 | 테이블별로 클래스가 나뉘므로 구분 컬럼이 필요 없음. |
7. Discriminator 관련 주요 SQL
(1) 조회 쿼리
SELECT * FROM Item WHERE dtype = 'B';
(2) 데이터 삽입
- JPA가 삽입 시 dtype 컬럼을 자동으로 설정.
8. 주의사항
- 구분 컬럼은 상속 전략에 따라 필수적일 수 있음:
- 단일 테이블(SINGLE_TABLE)과 조인 전략(JOINED)에서 필수.
- 데이터 타입 확인:
- 구분 컬럼의 데이터 타입을 명확히 지정해야 타입 불일치를 방지할 수 있음.
- 명시적 설정 권장:
- @DiscriminatorValue를 통해 명시적으로 값을 설정하면 유지보수가 쉬워짐.
- 테이블별 클래스 전략에는 사용되지 않음:
- 각 클래스가 독립된 테이블로 매핑되므로 dtype 컬럼이 필요하지 않음.
9. 결론
dtype은 JPA 상속 매핑에서 부모와 자식 엔티티를 구분하기 위해 매우 중요한 역할을 합니다. 특히 단일 테이블(SINGLE_TABLE)이나 조인 전략(JOINED)에서 엔티티를 명확히 구분해야 할 때 필수적입니다. 구분 컬럼과 값의 설정은 프로젝트 요구 사항에 따라 적절히 정의하는 것이 중요합니다.
추가적으로 궁금한 사항이나 더 깊은 설명이 필요하면 말씀해주세요! 😊
3. 상속 매핑 전략 상세
(1) 단일 테이블 전략 (SINGLE_TABLE)
- 특징:
- 부모 클래스와 자식 클래스의 데이터를 하나의 테이블에 저장.
- 상속 구조에 대한 테이블이 하나뿐이므로 성능이 좋음.
- 테이블 구조가 단순하지만, 많은 컬럼이 생길 수 있음(불필요한 NULL 컬럼 발생 가능).
- 구현 예제:
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "dtype") // 구분 컬럼
public abstract class Item {
@Id
@GeneratedValue
private Long id;
private String name;
}
@Entity
@DiscriminatorValue("Book") // dtype 값
public class Book extends Item {
private String author;
}
@Entity
@DiscriminatorValue("Movie") // dtype 값
public class Movie extends Item {
private String director;
}
- 생성된 테이블 구조:
ID | NAME | AUTHOR | DIRECTOR | DTYPE |
1 | Book 1 | Author A | NULL | Book |
2 | Movie 1 | NULL | Director A | Movie |
(2) 조인 전략 (JOINED)
- 특징:
- 부모 클래스와 자식 클래스 각각 별도의 테이블에 저장.
- 조회 시 부모 테이블과 자식 테이블을 JOIN하여 데이터를 조회.
- 데이터 정규화로 중복을 줄이고 구조가 깔끔, 하지만 조회 시 성능이 비교적 느릴 수 있음.
- 구현 예제:
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Item {
@Id
@GeneratedValue
private Long id;
private String name;
}
@Entity
public class Book extends Item {
private String author;
}
@Entity
public class Movie extends Item {
private String director;
}
- 생성된 테이블 구조:
- 부모 테이블 (Item):ID NAME
1 Book 1 2 Movie 1 - 자식 테이블 (Book):ID AUTHOR
1 Author A - 자식 테이블 (Movie):ID DIRECTOR
2 Director A
- 부모 테이블 (Item):ID NAME
- 조회 SQL:
SELECT i.*, b.*
FROM Item i
LEFT JOIN Book b ON i.id = b.id
WHERE i.id = 1;
(3) 테이블별 클래스 전략 (TABLE_PER_CLASS)
- 특징:
- 부모 클래스는 테이블을 생성하지 않고, 자식 클래스마다 독립적인 테이블을 생성.
- 자식 클래스별로 테이블이 독립적이므로 JOIN이 필요 없음.
- 중복된 컬럼이 많아질 수 있으며, 쿼리 성능이 떨어질 가능성이 있음.
- 구현 예제:
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {
@Id
@GeneratedValue
private Long id;
private String name;
}
@Entity
public class Book extends Item {
private String author;
}
@Entity
public class Movie extends Item {
private String director;
}
- 생성된 테이블 구조:
- 자식 테이블 (Book):ID NAME AUTHOR
1 Book 1 Author A - 자식 테이블 (Movie):ID NAME DIRECTOR
2 Movie 1 Director A
- 자식 테이블 (Book):ID NAME AUTHOR
4. 상속 매핑 전략 비교
장점 | 단점 | 적용 사례 | |
단일 테이블 (SINGLE_TABLE) | 성능이 좋음 (한 번의 SELECT로 모든 데이터를 조회). | NULL 컬럼이 많아질 수 있음. | 테이블 수를 줄이고 싶을 때. |
조인 전략 (JOINED) | 데이터 정규화. 중복 데이터가 없음. | 조회 시 JOIN으로 인해 성능 저하 가능. | 데이터 정규화가 필요할 때. |
테이블별 클래스 (TABLE_PER_CLASS) | JOIN 없이 독립적으로 쿼리 가능. | 중복 데이터 증가, UNION을 사용하는 조회 쿼리로 인해 성능 저하 가능. | 자식 클래스별 독립적 구조가 필요할 때. |
5. 상속 매핑 선택 기준
- 단일 테이블 전략:
- 테이블 수를 줄이고 싶을 때.
- 엔티티의 종류가 많지 않거나, NULL 컬럼이 크게 문제가 되지 않을 때.
- 조인 전략:
- 정규화된 데이터 모델을 선호하거나, 데이터 중복을 최소화해야 할 때.
- 데이터베이스 성능이 JOIN 쿼리를 잘 처리할 수 있을 때.
- 테이블별 클래스 전략:
- 자식 엔티티 간에 완전히 독립적인 테이블이 필요할 때.
- 조회 쿼리 성능이 중요한 경우 피하는 것이 좋음.
6. 상속 매핑 주의사항
- 추상 클래스 사용:
- 상위 클래스는 보통 추상 클래스로 선언하여 직접 사용하지 않도록 설계.
@Entity public abstract class Item { }
- Discriminator 컬럼:
- 단일 테이블 전략과 조인 전략에서는 구분 컬럼을 통해 엔티티 구분.
- 복잡한 상속 구조 피하기:
- 지나치게 깊은 상속 구조는 설계와 유지보수를 어렵게 만듦.
7. 결론
JPA의 상속 매핑은 객체지향 설계와 관계형 데이터베이스 간의 불일치를 해결하는 데 매우 유용합니다. 프로젝트의 요구사항(성능, 데이터 중복, 정규화 등)에 따라 적절한 상속 전략을 선택해야 하며, 각 전략의 장단점을 고려하여 설계하는 것이 중요합니다.
궁금한 점이나 추가적인 설명이 필요하면 언제든 말씀해주세요! 😊
JPA에서 테이블과 객체 매핑
📌 **데이터베이스의 테이블(Table)**과 **Java 객체(Entity)**를 매핑하여 객체지향적으로 데이터베이스를 사용할 수 있도록 합니다. 이를 통해 SQL을 직접 작성하지 않아도, 엔티티 객체의 필드와 테이블 컬럼 간의 변환을 자동으로 처리할 수 있습니다.
1. 테이블과 객체 매핑 기본 개념
구성 요소 | 설명 |
Entity 클래스 | 데이터베이스 테이블과 매핑되는 Java 클래스. |
Table 어노테이션 | 매핑할 데이터베이스 테이블의 이름과 속성을 설정. |
Column 어노테이션 | 엔티티의 필드를 데이터베이스의 특정 컬럼에 매핑. |
Primary Key | @Id를 사용하여 기본 키(Primary Key)를 설정. |
연관 관계 | 테이블 간의 관계(외래 키)는 @OneToOne, @OneToMany 등으로 객체 간 관계로 표현. |
2. 기본 매핑 어노테이션
(1) @Entity
- 클래스가 JPA 엔티티임을 선언.
- 데이터베이스 테이블과 매핑되는 클래스.
- 필수 조건:
- 기본 생성자 필요.
- @Id로 기본 키를 설정.
import jakarta.persistence.*;
@Entity // 엔티티 선언
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // 기본 키 자동 생성
private Long id;
private String name; // 매핑 컬럼
private String email; // 매핑 컬럼
}
(2) @Table
- 엔티티와 매핑되는 데이터베이스 테이블의 이름을 지정.
- 속성:
- name: 매핑할 테이블 이름 (기본값은 클래스 이름).
- schema: 테이블이 속한 스키마 이름.
- catalog: 테이블이 속한 카탈로그 이름.
- uniqueConstraints: 유니크 제약 조건 설정.
@Entity
@Table(name = "users", schema = "public")
public class User {
@Id
@GeneratedValue
private Long id;
@Column(nullable = false)
private String name;
}
(3) @Column
- 엔티티 필드와 테이블 컬럼을 매핑.
- 속성:
- name: 컬럼 이름 지정.
- nullable: NULL 허용 여부.
- unique: 유니크 제약 조건 설정.
- length: 문자열 컬럼 길이 지정.
- precision 및 scale: 숫자 타입의 정밀도와 소수점 자리수 지정.
@Column(name = "user_name", nullable = false, length = 100)
private String name;
@Column(name = "user_email", unique = true)
private String email;
(4) @Id
- 기본 키(Primary Key)로 사용할 필드를 설정.
- 반드시 한 개의 필드에 선언해야 함.
(5) @GeneratedValue
- 기본 키 값을 자동으로 생성.
- 전략(strategy):
- GenerationType.IDENTITY: 데이터베이스의 자동 증가(AUTO_INCREMENT) 사용.
- GenerationType.SEQUENCE: 데이터베이스 시퀀스를 사용해 기본 키 생성.
- GenerationType.TABLE: 키 생성용 별도 테이블을 사용.
- GenerationType.AUTO: 데이터베이스에 맞게 자동 선택.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
3. 테이블과 객체 매핑 예시
(1) 단순 매핑
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_name", nullable = false, length = 50)
private String name;
@Column(name = "user_email", unique = true)
private String email;
// 기본 생성자
public User() {}
// Getters and Setters
}
(2) 연관 관계 매핑
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne // N:1 관계
@JoinColumn(name = "user_id") // 외래 키 매핑
private User user;
private String product;
private int quantity;
}
4. @Table과 @Column 속성 정리
어노테이션 | 속성 | 설명 | 예시 |
@Table | name | 매핑할 테이블 이름 설정. | @Table(name = "users") |
schema | 테이블이 속한 스키마 지정. | @Table(schema = "public") | |
catalog | 테이블이 속한 카탈로그 지정. | @Table(catalog = "catalog_name") | |
uniqueConstraints | 유니크 제약 조건 설정. | @Table(uniqueConstraints = @UniqueConstraint(columnNames = {"name"})) | |
@Column | name | 매핑할 컬럼 이름 설정. | @Column(name = "user_name") |
nullable | 컬럼의 NULL 허용 여부 설정. | @Column(nullable = false) | |
unique | 유니크 제약 조건 설정. | @Column(unique = true) | |
length | 문자열 컬럼 길이 설정. | @Column(length = 100) | |
precision & scale | 숫자 타입의 정밀도와 소수점 자리수 설정 (예: BigDecimal). | @Column(precision = 10, scale = 2) |
5. 자동 테이블 생성
JPA는 애플리케이션 실행 시 엔티티 클래스 기반으로 테이블을 자동 생성할 수 있습니다.
이를 위해 hibernate.hbm2ddl.auto 속성을 설정합니다.
설정 값 | 설명 |
create | 애플리케이션 시작 시 기존 테이블을 삭제하고 새로 생성. |
create-drop | create와 동일하나, 애플리케이션 종료 시 테이블 삭제. |
update | 기존 테이블을 유지하면서 변경된 필드에 따라 업데이트. |
validate | 테이블이 엔티티 매핑과 일치하는지 확인 (변경 사항 반영 안 함). |
none | 테이블 자동 생성 기능 비활성화. |
설정 예시 (persistence.xml 또는 application.properties)
spring.jpa.hibernate.ddl-auto=update
6. 테이블 객체 매핑 장점
- SQL 자동화
- CRUD 작업을 자동으로 처리.
- 객체 지향적 설계
- 테이블 설계를 객체 모델로 표현 가능.
- 데이터베이스 독립성
- JPA 설정으로 다양한 데이터베이스를 지원.
- 생산성 향상
- 매핑 설정만으로 데이터 처리 간소화.
테이블 객체 다루는법
Cascade (영속성 전이)
Cascade(영속성 전이)는 JPA에서 엔티티 간의 연관관계가 있을 때, 한 엔티티의 작업(저장, 삭제 등)을 관련된 다른 엔티티에도 자동으로 전파하는 기능입니다. 이를 통해 개발자는 연관된 엔티티를 명시적으로 관리하지 않아도 되어 코드의 간결성과 유지보수성을 높일 수 있습니다.
1. Cascade의 필요성
- 연관된 엔티티 처리: 부모 엔티티와 연관된 자식 엔티티를 함께 저장하거나 삭제할 때, 명시적으로 각 엔티티에 대해 저장 또는 삭제를 호출하지 않고, Cascade 설정을 통해 자동으로 처리.
- 코드 간소화: 연관된 엔티티가 많을 경우 Cascade를 사용하면 작업을 자동화하여 코드의 복잡성을 줄임.
2. Cascade의 주요 유형
Cascade Type | 설명 |
ALL | 모든 영속성 전이 동작을 적용 (PERSIST, MERGE, REMOVE, REFRESH, DETACH). |
PERSIST | 엔티티 저장 시 연관된 엔티티도 함께 저장. |
MERGE | 엔티티 병합(수정) 시 연관된 엔티티도 함께 병합. |
REMOVE | 엔티티 삭제 시 연관된 엔티티도 함께 삭제. |
REFRESH | 엔티티 갱신 시 연관된 엔티티도 갱신. |
DETACH | 엔티티를 준영속(detach) 상태로 전환할 때 연관된 엔티티도 함께 준영속 상태로 전환. |
3. Cascade 사용 방법
(1) Cascade 설정
Cascade는 엔티티 간 연관 관계를 매핑할 때 설정합니다.
import jakarta.persistence.*;
@Entity
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL) // Cascade 설정
private List<Child> children = new ArrayList<>();
// Getter, Setter
}
(2) Cascade 적용된 연관 엔티티 저장
Parent parent = new Parent();
parent.setName("Parent");
Child child1 = new Child();
child1.setName("Child 1");
Child child2 = new Child();
child2.setName("Child 2");
// 부모와 자식 관계 설정
parent.getChildren().add(child1);
parent.getChildren().add(child2);
child1.setParent(parent);
child2.setParent(parent);
// 부모를 저장하면 자식 엔티티도 자동으로 저장
entityManager.persist(parent);
- CascadeType.PERSIST로 인해 parent를 저장하면 child1과 child2도 자동으로 저장됩니다.
4. Cascade 유형별 동작 예시
(1) PERSIST
- 부모 엔티티 저장 시 자식 엔티티도 함께 저장.
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
private List<Child> children = new ArrayList<>();
(2) MERGE
- 부모 엔티티 병합 시 자식 엔티티도 병합.
@OneToMany(mappedBy = "parent", cascade = CascadeType.MERGE)
private List<Child> children = new ArrayList<>();
(3) REMOVE
- 부모 엔티티 삭제 시 자식 엔티티도 삭제.
@OneToMany(mappedBy = "parent", cascade = CascadeType.REMOVE)
private List<Child> children = new ArrayList<>();
(4) ALL
- 모든 Cascade 동작을 포함 (PERSIST, MERGE, REMOVE, REFRESH, DETACH).
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> children = new ArrayList<>();
5. Cascade의 장점
- 코드 간소화
- 연관된 엔티티에 대해 명시적으로 작업을 호출하지 않아도 자동으로 처리 가능.
- 연관 데이터 일관성 유지
- 부모 엔티티와 연관된 자식 엔티티 간의 데이터 동기화 보장.
- 효율적인 관리
- 대량의 연관 데이터를 효율적으로 처리 가능.
6. Cascade 사용 시 주의사항
- CascadeType.REMOVE 주의
- 부모 엔티티 삭제 시 자식 엔티티도 함께 삭제되므로, 실제 데이터 손실이 발생할 수 있음.
- 예: 부모-자식 관계가 아닌 연관된 다른 테이블에서 참조되는 데이터가 있다면 데이터 무결성 문제가 생길 수 있음.
- CascadeType.ALL 사용
- 모든 영속성 전이를 포함하므로, 반드시 필요한 경우에만 사용.
- 잘못 사용하면 의도하지 않은 상태 변화가 발생할 수 있음.
- Lazy Loading과의 충돌
- 연관 관계가 Lazy 로딩일 경우, Cascade 작업 시 필요 이상의 데이터가 로드될 수 있음.
7. Cascade 적용 여부 판단 기준
적용 여부 | 권장 상황 |
CascadeType.PERSIST | 부모 엔티티 저장 시 자식 엔티티도 항상 저장되어야 하는 경우. |
CascadeType.REMOVE | 부모 엔티티 삭제 시 자식 엔티티도 항상 함께 삭제되어야 하는 경우. |
CascadeType.ALL | 부모와 자식 간의 모든 작업(PERSIST, REMOVE, MERGE 등)이 항상 연동되어야 하는 경우. |
CascadeType.MERGE | 병합 작업에서 부모와 자식 간 변경 사항이 항상 함께 반영되어야 하는 경우. |
CascadeType.REFRESH | 부모 갱신 시 자식 엔티티의 상태도 항상 동기화가 필요할 경우. |
CascadeType.DETACH | 부모와 자식을 동시에 준영속 상태로 전환할 필요가 있는 경우. |
8. Cascade 설정의 실용적 예제
부모-자식 관계 (부모와 자식 삭제 동작)
@Entity
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> children = new ArrayList<>();
}
@Entity
public class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
}
Parent parent = entityManager.find(Parent.class, 1L);
entityManager.remove(parent); // 자식 엔티티도 함께 삭제
9. 결론
JPA의 Cascade는 엔티티 간의 연관 관계에서 작업을 전이시켜 개발 생산성을 높이고, 일관된 데이터 관리를 가능하게 합니다. 그러나 모든 작업에서 Cascade를 사용하는 것은 권장되지 않으며, 특정 상황에 적합한 Cascade 타입을 선택해 사용하는 것이 중요합니다. 😊
orphanRemoval (고아 객체 제거)
부모-자식 관계에서 **부모 엔티티와의 연관 관계가 제거된 자식 엔티티(고아 객체)**를 데이터베이스에서 자동으로 삭제해주는 설정입니다. 이를 통해 연관 관계가 없는 자식 데이터를 자동으로 정리할 수 있어 데이터 무결성을 유지하고 개발자의 부담을 줄여줍니다.
1. orphanRemoval의 기본 개념
- 고아 객체: 부모 엔티티와의 연관 관계가 끊어진 자식 엔티티.
- 기능: 부모 엔티티에서 자식 엔티티를 컬렉션에서 제거하거나 관계를 끊으면, JPA가 해당 자식 엔티티를 데이터베이스에서 삭제.
- 작동 방식: 부모-자식 관계의 컬렉션에서 자식 엔티티를 제거하거나, null로 설정했을 때 자동으로 DELETE SQL 실행.
2. 사용 방법
- @OneToMany 또는 @OneToOne 관계에서 사용 가능.
- orphanRemoval = true 속성을 지정하면 고아 객체를 자동으로 제거.
예제: 부모-자식 관계 설정
@Entity
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> children = new ArrayList<>();
// Helper method to manage bidirectional relationship
public void addChild(Child child) {
children.add(child);
child.setParent(this);
}
public void removeChild(Child child) {
children.remove(child);
child.setParent(null);
}
}
@Entity
public class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
}
orphanRemoval 동작 예시
// 부모 엔티티 생성
Parent parent = new Parent();
parent.setName("Parent");
// 자식 엔티티 생성 및 관계 설정
Child child1 = new Child();
child1.setName("Child 1");
Child child2 = new Child();
child2.setName("Child 2");
parent.addChild(child1);
parent.addChild(child2);
entityManager.persist(parent); // Parent와 Child가 모두 저장
// 부모 엔티티에서 자식 엔티티 제거
parent.removeChild(child1);
entityManager.flush(); // flush() 시점에 child1에 대한 DELETE SQL 실행
3. orphanRemoval 동작 방식
상황 동작부모 컬렉션에서 자식 제거 | 자식 엔티티를 부모 컬렉션에서 삭제하면, 해당 자식은 데이터베이스에서도 삭제. |
자식의 부모 관계 해제 (null) | 자식 엔티티의 부모 관계를 null로 설정하면, 자식은 고아 객체로 간주되어 삭제. |
예시: 관계 해제 후 삭제
child1.setParent(null); // 부모 관계 해제
entityManager.flush(); // flush 시점에 DELETE SQL 실행
4. orphanRemoval vs. CascadeType.REMOVE
orphanRemoval | CascadeType.REMOVE | |
목적 | 부모와의 관계가 끊어진 고아 객체를 제거. | 부모 엔티티 삭제 시, 연관된 자식 엔티티도 삭제. |
작동 조건 | 부모-자식 관계가 끊긴 경우. | 부모 엔티티가 삭제될 때 전파. |
동작 트리거 | 부모-자식 관계를 null로 설정하거나, 컬렉션에서 제거. | 부모 엔티티의 remove() 호출. |
사용 범위 | @OneToOne, @OneToMany에서 사용 가능. | @OneToOne, @OneToMany, @ManyToOne 등 사용 가능. |
비교 예제
- orphanRemoval
- parent.getChildren().remove(child); // 고아 객체로 간주되어 삭제됨
- CascadeType.REMOVE
- entityManager.remove(parent); // 부모 삭제 시 자식도 함께 삭제됨
5. 장점
- 자동 데이터 정리
- 부모-자식 관계가 끊긴 경우 데이터베이스에서 자동으로 자식 데이터를 삭제.
- 데이터 무결성 유지
- 고아 객체가 데이터베이스에 남아 있지 않아 데이터 무결성을 보장.
- 코드 간소화
- 자식 엔티티를 명시적으로 삭제하지 않아도 되므로 코드가 간결해짐.
6. 주의사항
- Cascade와의 조합
- 일반적으로 CascadeType.ALL 또는 CascadeType.REMOVE와 함께 사용되며, 올바르게 조합하지 않으면 예상치 못한 동작이 발생할 수 있음.
- 컬렉션 관리
- 자식 엔티티를 컬렉션에서 제거하거나 관계를 null로 설정하지 않으면, orphanRemoval이 동작하지 않음.
- 퍼포먼스 고려
- 부모-자식 관계에서 자식 엔티티가 많으면, 삭제 작업으로 인해 성능에 영향을 줄 수 있음.
- N:1 관계에서는 사용 불가
- orphanRemoval은 @OneToMany 또는 @OneToOne에서만 사용할 수 있으며, @ManyToOne에서는 지원하지 않음.
7. 결론
**orphanRemoval**은 JPA에서 부모-자식 관계를 다룰 때, 부모와의 연관이 끊어진 고아 객체를 자동으로 삭제해주는 강력한 기능입니다. 데이터 무결성을 유지하고 개발자의 작업을 줄이는 데 유용하지만, 관계와 삭제 조건을 명확히 이해하고 사용해야 예상치 못한 데이터 삭제를 방지할 수 있습니다. 😊
추가로 궁금한 점이 있다면 알려주세요!
Fetch (조회 시점)
엔티티 간의 연관 관계에서 데이터를 언제 조회할지를 결정하는 전략을 의미합니다. JPA는 @OneToOne, @OneToMany, @ManyToOne, @ManyToMany와 같은 연관 관계를 매핑할 때 **지연 로딩(Lazy Loading)**과 즉시 로딩(Eager Loading) 방식을 제공합니다.
Fetch 전략의 종류
전략 설명 | |
즉시 로딩 | 연관된 엔티티를 즉시 조회. 부모 엔티티를 조회할 때 연관된 자식 엔티티를 함께 가져옴. |
지연 로딩 | 연관된 엔티티를 필요할 때 조회. 부모 엔티티만 먼저 조회하고, 자식 엔티티는 실제로 접근할 때 로딩. |
1. Fetch 전략 설정
Fetch 전략은 연관 관계 매핑 애너테이션에서 fetch 속성을 사용해 설정합니다.
1) 즉시 로딩 (EAGER)
- 연관된 엔티티를 즉시 조회.
- 부모 엔티티를 조회할 때, 연관된 자식 엔티티를 JOIN으로 한 번에 가져옴.
- 기본값:
- @OneToOne, @ManyToOne 관계에서 즉시 로딩이 기본값.
설정 예시
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "user_id")
private User user;
2) 지연 로딩 (LAZY)
- 연관된 엔티티를 필요할 때 조회.
- 부모 엔티티만 먼저 로드하고, 자식 엔티티는 실제로 접근하는 시점에 별도의 쿼리로 가져옴.
- 기본값:
- @OneToMany, @ManyToMany 관계에서 지연 로딩이 기본값.
설정 예시
@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
private List<Child> children;
2. Fetch 전략 비교
즉시 로딩 (EAGER) | 지연 로딩 (LAZY) | |
로딩 시점 | 부모 엔티티를 조회할 때 연관된 자식 엔티티를 함께 로드. | 연관된 자식 엔티티를 실제 접근할 때 로드. |
쿼리 성능 | 한 번의 JOIN 쿼리로 가져오기 때문에 처음에는 효율적일 수 있음. | 부모 엔티티만 조회하고, 자식 엔티티는 필요한 시점에 별도 쿼리 실행. |
사용 상황 | 연관된 엔티티를 항상 함께 사용해야 하는 경우. | 연관된 엔티티를 사용할 가능성이 낮거나, 데이터가 많아 성능이 우려되는 경우. |
장점 | 간단한 데이터 조회에서 쿼리 성능이 좋음. | 메모리와 성능 효율이 높음. |
단점 | 필요 없는 데이터까지 로드할 수 있음. | 지연 로딩이 많아지면 N+1 문제 발생 가능. |
3. Fetch 전략 동작 예시
(1) 즉시 로딩
@Entity
public class Order {
@Id
@GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.EAGER) // 즉시 로딩 설정
@JoinColumn(name = "user_id")
private User user;
}
- em.find(Order.class, 1L) 실행 시, Order와 연관된 User를 한 번의 JOIN 쿼리로 가져옵니다.
실행 쿼리 (즉시 로딩)
SELECT o.*, u.*
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE o.id = 1;
(2) 지연 로딩
@Entity
public class Order {
@Id
@GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY) // 지연 로딩 설정
@JoinColumn(name = "user_id")
private User user;
}
- em.find(Order.class, 1L) 실행 시, Order만 조회되고, User는 로드되지 않습니다.
- order.getUser() 호출 시점에 User를 조회하는 쿼리가 실행됩니다.
실행 쿼리 (지연 로딩)
- 부모 엔티티 조회:
SELECT * FROM orders WHERE id = 1;
- 연관된 자식 엔티티 조회:
SELECT * FROM users WHERE id = ?;
4. N+1 문제
- 지연 로딩을 사용할 때, 부모 엔티티를 조회하고 각 자식 엔티티를 조회하는 쿼리가 추가로 발생하여 성능 저하.
- 예: 부모 엔티티 1개와 자식 엔티티 N개를 조회하는 경우 N+1개의 쿼리가 실행.
예시
List<Order> orders = em.createQuery("SELECT o FROM Order o", Order.class).getResultList();
for (Order order : orders) {
System.out.println(order.getUser().getName()); // 각 User를 조회할 때마다 쿼리 실행
}
해결 방법
- JPQL의 FETCH JOIN 사용
- 필요한 데이터를 한 번의 쿼리로 가져옴.
List<Order> orders = em.createQuery( "SELECT o FROM Order o JOIN FETCH o.user", Order.class).getResultList();
- Batch Fetch 크기 조정
- Hibernate 설정에서 Batch Fetch를 사용해 N+1 문제를 완화.
hibernate.default_batch_fetch_size=10
5. Fetch 전략 선택 기준
상황 추천 | Fetch 전략 |
연관된 엔티티를 항상 함께 사용해야 할 때 | 즉시 로딩 (EAGER) |
연관된 엔티티를 사용할 가능성이 적을 때 | 지연 로딩 (LAZY) |
데이터가 많고, 관계를 자주 조회해야 할 때 | 지연 로딩 (LAZY) + FETCH JOIN |
6. 즉시 로딩과 지연 로딩의 기본값
연관 관계 기본 | Fetch 전략 |
@OneToOne | 즉시 로딩 (EAGER) |
@ManyToOne | 즉시 로딩 (EAGER) |
@OneToMany | 지연 로딩 (LAZY) |
@ManyToMany | 지연 로딩 (LAZY) |
7. 결론
- 즉시 로딩 (EAGER): 항상 연관 데이터를 함께 사용해야 하거나, 관계가 단순한 경우 적합.
- 지연 로딩 (LAZY): 성능과 메모리 효율을 위해 기본적으로 추천되며, 필요 시 FETCH JOIN으로 최적화.
Fetch 전략은 애플리케이션의 데이터 접근 패턴과 성능 요구 사항에 따라 신중하게 설정해야 합니다. N+1 문제와 Lazy Loading 설계에 주의하며, 적절한 시점에 데이터를 로딩하는 방식으로 성능 최적화를 도모할 수 있습니다. 😊
Entity
[아래는 위 내용의 요약본입니다.]
Entity 표 요약
구분 | 설명 |
Entity 정의 | 데이터베이스 테이블과 매핑되는 Java 클래스 |
주요 특징 | - 테이블과 1:1 매핑- Java 객체 필드와 DB 컬럼 매핑- @Entity 및 @Id 어노테이션 필수- 영속성 관리 |
주요 어노테이션 | - @Entity: 클래스를 엔티티로 지정- @Id: 기본 키 지정- @GeneratedValue: 키 자동 생성 전략 지정- @Column: 컬럼 매핑 속성 설정- @Table: 테이블 매핑 설정- @Transient: 매핑 제외 |
Entity 활용 방법 | - 데이터 저장: em.persist()- 데이터 조회: em.find()- 데이터 수정: Dirty Checking- 데이터 삭제: em.remove() |
관계 매핑 | - @OneToOne- @OneToMany- @ManyToOne- @ManyToMany |
장점 | - 객체 지향 데이터 모델링 가능- SQL 자동 생성- 데이터베이스 독립성- 생산성 향상 |
주의사항 | - 기본 생성자 필수- Serializable 구현 권장- equals()와 hashCode() 재정의 |
주요 어노테이션 요약
어노테이션 | 설명 | 예시 |
@Entity | 클래스를 JPA Entity로 선언. 필수 어노테이션. | @Entity public class Product { } |
@Table | 매핑할 테이블 이름 지정. 생략 시 클래스 이름이 테이블 이름으로 사용됨. | @Table(name = "products") |
@Id | 기본 키로 사용할 필드 지정. | @Id private Long id; |
@GeneratedValue | 기본 키 자동 생성 전략 지정. | @GeneratedValue(strategy = GenerationType.IDENTITY) |
@Column | 컬럼 매핑 설정. 속성: name, nullable, unique, length 등. | @Column(name = "user_name", nullable = false, length = 50) |
@Transient | 특정 필드를 데이터베이스에 매핑하지 않음. | @Transient private String temporaryData; |
주요 관계 매핑 요약
관계 | 설명 | 예시 |
@OneToOne | 1:1 관계 | @OneToOne private Profile profile; |
@OneToMany | 1:N 관계 | @OneToMany(mappedBy = "user") private List<Order> orders; |
@ManyToOne | N:1 관계 | @ManyToOne @JoinColumn(name = "user_id") private User user; |
@ManyToMany | N:M 관계 | @ManyToMany @JoinTable(name = "user_roles") private List<Role> roles; |
Entity 생명주기
엔티티 생명주기 표 요약
상태 | 설명 | 비유 | 특징 | 전환 메서드 |
비영속 (New) | 영속성 컨텍스트가 모르는 새로운 상태. 데이터베이스와 연관 없음. | 주민등록되지 않은 아기 | - 메모리에만 존재. - 데이터베이스에 저장되지 않음. |
new Entity() |
영속 (Managed) | 영속성 컨텍스트에 저장되고 관리되는 상태. 데이터베이스와 동기화됨. | 주민등록 완료된 상태 | - 영속성 컨텍스트가 관리. - 변경 감지(Dirty Checking)로 변경 사항 자동 반영. |
em.persist(entity) |
준영속 (Detached) | 한때 영속 상태였지만 영속성 컨텍스트에서 분리된 상태. 데이터베이스와 동기화되지 않음. | 주민등록 말소 상태 | - 영속성 컨텍스트와 연결 끊김. - 변경 사항이 데이터베이스에 반영되지 않음. |
em.detach(entity), em.clear(), em.close() |
삭제 (Removed) | 영속성 컨텍스트에 의해 삭제로 표시된 상태. 트랜잭션 완료 시 데이터베이스에서 제거됨. | 사망신고 상태 | - 영속성 컨텍스트에 의해 삭제로 표시. - 트랜잭션 완료 시 데이터베이스에서 완전히 삭제. |
em.remove(entity) |
상태 전환 흐름 예시
상태 전환 | 설명 |
비영속 → 영속 | em.persist(entity) 호출 시. |
영속 → 준영속 | em.detach(entity) 또는 em.clear() 호출 시. |
영속 → 삭제 | em.remove(entity) 호출 후 트랜잭션 완료 시. |
상태 전환 주요 코드 예시
- 비영속 상태
- User user = new User(); // 비영속 user.setName("John");
- 영속 상태로 전환
- em.persist(user); // 영속 상태
- 준영속 상태로 전환
- em.detach(user); // 준영속 상태
- 삭제 상태로 전환
- em.remove(user); // 삭제 상태 em.getTransaction().commit(); // 데이터베이스에서 제거
영속성 컨텍스트 (Persistence Context)
📌 JPA에서 엔티티(Entity)를 관리하는 일종의 메모리 공간으로, 애플리케이션과 데이터베이스 사이의 중간 계층 역할을 합니다. 엔티티 객체의 상태를 관리하고, 데이터베이스와 동기화하며, 데이터 변경 사항을 추적합니다.
1. 영속성 컨텍스트의 주요 개념
항목 | 설명 |
정의 | 엔티티를 저장하고 관리하며, 엔티티 객체와 데이터베이스 간의 중개자 역할을 하는 메모리 공간. |
EntityManager와 관계 | EntityManager를 통해 영속성 컨텍스트에 접근하고 관리. |
엔티티 생명주기 | 엔티티 객체의 상태(비영속, 영속, 준영속, 삭제)를 관리. |
1차 캐시 | 영속성 컨텍스트 내부에 저장된 엔티티 객체를 캐싱하여 동일 엔티티를 중복 조회하지 않음. |
변경 감지 (Dirty Checking) | 영속성 컨텍스트에 의해 관리되는 엔티티의 변경 사항을 감지하여 자동으로 데이터베이스에 반영. |
2. 영속성 컨텍스트의 주요 기능
(1) 1차 캐시
- 영속성 컨텍스트 내부에 엔티티 객체를 캐싱하여 관리.
- 동일한 식별자를 가진 엔티티를 여러 번 조회할 경우, 데이터베이스를 재조회하지 않고 1차 캐시에서 반환.
// 첫 번째 조회 -> 데이터베이스에서 조회 후 1차 캐시에 저장
User user1 = em.find(User.class, 1L);
// 두 번째 조회 -> 1차 캐시에서 반환 (SQL 실행되지 않음)
User user2 = em.find(User.class, 1L);
System.out.println(user1 == user2); // true
(2) 엔티티 동일성 보장
- 동일한 트랜잭션 내에서는 같은 식별자를 가진 엔티티 객체는 **동일성 (==)**을 보장.
User user1 = em.find(User.class, 1L);
User user2 = em.find(User.class, 1L);
System.out.println(user1 == user2); // true (동일 객체)
(3) 변경 감지 (Dirty Checking)
- 영속성 컨텍스트는 관리 중인 엔티티의 변경 사항을 감지하고, 트랜잭션 커밋 시 자동으로 데이터베이스에 반영.
User user = em.find(User.class, 1L); // 영속 상태
user.setName("Updated Name"); // 엔티티 수정 (Dirty Checking 발생)
em.getTransaction().commit(); // UPDATE 쿼리 실행
(4) 쓰기 지연 (Write-Behind)
- 엔티티 변경 작업(INSERT, UPDATE, DELETE)은 즉시 실행되지 않고, 쓰기 지연 저장소에 SQL이 누적된 후 트랜잭션 커밋 시점에 한꺼번에 실행.
User user = new User();
user.setName("John");
em.persist(user); // INSERT SQL이 즉시 실행되지 않고 쓰기 지연 저장소에 저장
em.getTransaction().commit(); // INSERT SQL 실행
(5) 지연 로딩 (Lazy Loading) 지원
- 연관된 엔티티를 필요할 때 로드하는 지연 로딩을 지원.
User user = em.find(User.class, 1L); // User만 조회
List<Order> orders = user.getOrders(); // 이 시점에 Order 조회 쿼리 실행
3. 엔티티 생명주기
상태 | 설명 | 전환 방법 |
비영속 (New) | 영속성 컨텍스트와 관계없는 상태. 데이터베이스와 연관되지 않음. | new Entity() |
영속 (Managed) | 영속성 컨텍스트에 저장되어 관리되는 상태. 데이터베이스와 동기화 가능. | em.persist(entity) 또는 em.find() |
준영속 (Detached) | 한때 영속 상태였지만, 현재는 영속성 컨텍스트에서 분리된 상태. 데이터베이스와 동기화되지 않음. | em.detach(entity), em.clear(), em.close() |
삭제 (Removed) | 삭제 상태로 표시되어 트랜잭션 커밋 시 데이터베이스에서 제거됨. | em.remove(entity) |
4. 영속성 컨텍스트의 동작 방식
(1) 엔티티 저장
- 영속성 컨텍스트에 엔티티를 저장하고 관리.
- em.persist(entity) 호출 시 영속 상태로 전환.
(2) 엔티티 조회
- em.find() 호출 시, 영속성 컨텍스트의 1차 캐시에서 먼저 조회.
- 1차 캐시에 없으면 데이터베이스에서 조회 후 1차 캐시에 저장.
(3) 변경 내용 반영
- 영속 상태의 엔티티가 변경되면 변경 감지를 통해 자동으로 데이터베이스에 반영.
(4) 트랜잭션 커밋
- 쓰기 지연 저장소의 SQL을 한꺼번에 실행하고 트랜잭션을 종료.
5. 영속성 컨텍스트의 활용 예시
기본 사용 흐름
EntityManagerFactory emf = Persistence.createEntityManagerFactory("example-unit");
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin();
try {
// 엔티티 저장 (비영속 → 영속)
User user = new User();
user.setName("John");
em.persist(user);
// 엔티티 조회 (1차 캐시 사용)
User foundUser = em.find(User.class, user.getId());
// 엔티티 수정 (Dirty Checking)
foundUser.setName("Updated Name");
// 트랜잭션 커밋 (쓰기 지연 SQL 실행)
transaction.commit();
} catch (Exception e) {
transaction.rollback();
} finally {
em.close();
}
emf.close();
6. 영속성 컨텍스트의 장점
- 1차 캐시로 성능 최적화
- 동일 트랜잭션 내에서 중복 조회를 방지.
- 변경 감지로 데이터 자동 반영
- 코드의 단순화 및 일관성 유지.
- 쓰기 지연으로 효율적 SQL 처리
- 트랜잭션 커밋 시점에 한 번에 SQL 실행.
- 데이터 일관성 보장
- 동일성 보장(==)으로 엔티티 간의 데이터 충돌 방지.
7. 영속성 컨텍스트 사용 시 주의사항
(1) 메모리 사용량 증가
- 영속성 컨텍스트에 관리 중인 엔티티가 많아질 경우 메모리 사용량이 증가할 수 있음.
- 해결 방법:
- em.clear() 또는 em.detach(entity)로 필요하지 않은 엔티티를 준영속 상태로 전환.
(2) LazyInitializationException
- 지연 로딩 사용 시, 트랜잭션이 종료된 상태에서 프록시 객체 접근 시 오류 발생.
- 해결 방법:
- 트랜잭션 내에서 데이터를 모두 로드하거나, FETCH JOIN 사용.
(3) 쓰기 지연의 데이터 충돌
- 쓰기 지연으로 인해 트랜잭션 종료 시점에 SQL 실행 중 충돌 가능.
- 해결 방법:
- 중요한 작업 시 명시적 flush() 호출.
8. 결론
영속성 컨텍스트는 JPA에서 핵심적인 역할을 하며, 데이터의 상태를 관리하고 성능 최적화를 지원합니다. 하지만, 이를 효율적으로 활용하기 위해 N+1 문제, Lazy Loading, 메모리 사용량 등 주의점을 고려해야 합니다. 설계와 구현 단계에서 영속성 컨텍스트의 동작을 명확히 이해하고 사용하는 것이 중요합니다.
1차 캐시
📌 엔티티를 영속성 컨텍스트에 저장할 때 생성되는 메모리 내 캐시이다. 엔티티는 먼저 1차 캐시에 저장되고 이후 같은 엔티티를 요청하면 DB를 조회하지 않고 1차 캐시에서 데이터를 반환하여 성능을 높일 수 있다.
- 영속성 컨텍스트의 1차 캐시에 저장된다.
// 비영속
Tutor tutor = new Tutor(1L, "wonuk", 100);
// 영속, 1차 캐시에 저장
em.persist(tutor);
- 영속된 Entity 조회
- Database가 아닌 1차 캐시에 저장된 Entity를 먼저 조회한다.
// 1차 캐시에서 조회
Tutor findTutor = em.find(Tutor.class, 1L);
public static void main(String[] args) {
// EntityManagerFactory 생성
EntityManagerFactory emf = Persistence.createEntityManagerFactory("test");
// EntityManager 생성
EntityManager em = emf.createEntityManager();
// Transaction 생성
EntityTransaction transaction = em.getTransaction();
// 트랜잭션 시작
transaction.begin();
try {
// 비영속
Tutor tutor = new Tutor(1L, "wonuk", 100);
// 영속
System.out.println("persist 전");
em.persist(tutor);
System.out.println("persist 후");
Tutor findTutor = em.find(Tutor.class, 1L);
System.out.println("findTutor.getId() = " + findTutor.getId());
System.out.println("findTutor.getName() = " + findTutor.getName());
System.out.println("findTutor.getAge() = " + findTutor.getAge());
// transaction이 commit되며 실제 SQL이 실행된다.
transaction.commit();
} catch (Exception e) {
// 실패 -> 롤백
e.printStackTrace();
transaction.rollback();
} finally {
// 엔티티 매니저 연결 종료
em.close();
}
emf.close();
}
z
실행결과
- 1차 캐시의 Entity를 조회한다.
- 조회 SQL이 실행되지 않는다.
- 트랜잭션 Commit 시점에 INSERT SQL이 실행된다.
**트랜잭션(Transaction)**은 데이터베이스 작업에서 데이터의 일관성, 무결성을 보장하기 위해 논리적으로 묶여있는 작업 단위를 의미합니다. |
데이터베이스에 저장된 데이터 조회
- 1차 캐시는 동일한 트랜잭션 안에서만 사용이 가능하다.
- 요청이 들어오고 트랜잭션이 종료되면 영속성 컨텍스트는 삭제된다.
동일성 보장
📌 동일한 트랜잭션 안에서 특정 엔티티를 여러 번 조회해도 항상 같은 객체 인스턴스를 반환한다. 영속성 컨텍스트는 1차 캐시를 사용하여 같은 엔티티를 중복 조회해도 동일한 객체를 참조하게 하여 일관성을 유지한다.
- 동일한 트랜잭션 내에서 조회된 Entity는 같은 인스턴스를 반환한다.
- DB에 저장된 데이터를 조회하여 1차 캐시에 저장한다.
- 1차 캐시에 저장된 데이터를 조회한다.
JPA의 동일성 보장 (Identity)
JPA에서 동일성(Identity)은 영속성 컨텍스트가 같은 엔티티 객체를 하나만 관리하여, 동일한 엔티티를 여러 번 조회해도 항상 같은 객체를 반환하는 특성을 의미합니다. 이를 통해 객체 간의 일관성과 효율적인 데이터 관리가 가능합니다.
주요 개념
구분 | 설명 |
동일성 보장 | 동일한 트랜잭션 내에서 같은 엔티티는 동일한 객체(instance)로 관리. |
비교 방식 | - 동일성(identity): == 연산자를 사용해 객체가 동일한 인스턴스인지 비교.- 동등성(equality): .equals() 메서드를 사용해 논리적으로 같은 값인지 비교. |
영속성 컨텍스트 | 동일성 보장이 가능한 이유는 영속성 컨텍스트가 엔티티를 1차 캐시에 저장하고 관리하기 때문. |
1차 캐시 | - 영속성 컨텍스트 내부에 엔티티 객체를 저장하는 메모리 공간.- 동일한 엔티티를 조회할 경우 1차 캐시에서 반환하므로 동일성을 보장. |
동일성 보장의 동작 방식
- 동일 엔티티를 조회할 때 동일 객체 반환
- 같은 트랜잭션 안에서 같은 식별자(@Id)를 가진 엔티티를 조회하면 영속성 컨텍스트의 1차 캐시에 있는 동일 객체를 반환.
User user1 = em.find(User.class, 1L); // 첫 번째 조회 User user2 = em.find(User.class, 1L); // 두 번째 조회 System.out.println(user1 == user2); // true (동일 객체)
- 변경 감지(Dirty Checking)와의 연계
- 동일 객체가 관리되므로 변경 사항은 영속성 컨텍스트를 통해 자동 감지되고 데이터베이스에 반영.
user1.setName("Updated Name"); // 트랜잭션 커밋 시, 변경 내용이 데이터베이스에 자동 반영.
- 다른 트랜잭션에서는 동일성 보장 불가
- 트랜잭션이 다르면 영속성 컨텍스트가 다르므로 동일 객체로 관리되지 않음.
User user1 = em1.find(User.class, 1L); // 첫 번째 트랜잭션 User user2 = em2.find(User.class, 1L); // 두 번째 트랜잭션 System.out.println(user1 == user2); // false (다른 트랜잭션, 다른 객체)
동일성과 동등성 비교
비교 기준 | 동일성 (Identity) | 동등성 (Equality) |
비교 방식 | == 연산자로 비교 | .equals() 메서드로 비교 |
비교 목적 | 같은 객체인지 확인 | 논리적으로 같은 데이터인지 확인 |
사용 예시 | 동일 엔티티인지 확인 (JPA에서 중요) | 엔티티 내용의 동등성 확인 |
JPA 보장 여부 | 영속성 컨텍스트에서 보장 | 사용자 정의 메서드로 구현 필요 |
동일성 보장의 장점
- 데이터 일관성 유지
- 동일 엔티티를 변경하면 모든 참조 객체가 동일하게 반영.
- 성능 최적화
- 동일 엔티티를 여러 번 조회해도 데이터베이스를 다시 조회하지 않고 1차 캐시 사용.
- 관리의 편리성
- 영속성 컨텍스트가 엔티티의 생명주기를 관리하여 복잡성을 줄임.
주의 사항 및 한계
- 트랜잭션 범위 제한
- 동일성 보장은 같은 영속성 컨텍스트 내에서만 유효.
- Lazy Loading 주의
- 프록시 객체를 사용할 경우 == 연산이 예상대로 동작하지 않을 수 있음.
- 1차 캐시 메모리 관리
- 1차 캐시가 크면 메모리 사용량 증가 및 성능 저하 가능.
결론
JPA의 동일성 보장은 객체 지향적 데이터 관리를 가능하게 하고, 데이터 일관성을 유지하며, 성능을 최적화하는 데 중요한 역할을 합니다. 그러나 트랜잭션과 영속성 컨텍스트 범위에 따라 보장이 달라지므로, 이를 이해하고 활용하는 것이 중요합니다. 😊
쓰기 지연
📌 엔티티 객체의 변경 사항을 DB에 바로 반영하지 않고 트랜잭션이 커밋될 때 한 번에 반영하는 방식으로 이를 통해 성능을 최적화하고 트랜잭션 내에서의 불필요한 DB 쓰기 작업을 최소화한다.
JPA의 쓰기 지연 (Write-Behind, Write-Delay)
쓰기 지연은 JPA에서 영속성 컨텍스트(Persistence Context)가 엔티티 매니저를 통해 데이터베이스와 직접 상호작용하지 않고, 내부 버퍼(쓰기 지연 저장소)에 변경 내역(SQL)을 저장한 후 트랜잭션이 커밋될 때 한꺼번에 데이터베이스에 반영하는 전략을 의미합니다. 이를 통해 성능 최적화와 효율적인 데이터베이스 처리 작업이 가능합니다.
주요 동작 원리
- 엔티티 변경 감지
- 영속성 컨텍스트에서 관리 중인 엔티티의 상태를 지속적으로 감지.
- 변경된 엔티티는 내부 쓰기 지연 저장소에 기록됨.
- 쓰기 지연 저장소에 SQL 누적
- INSERT, UPDATE, DELETE와 같은 SQL 명령문이 즉시 실행되지 않고 저장소에 보관.
- 트랜잭션 커밋 시점에 SQL 실행
- 트랜잭션이 commit될 때 누적된 SQL을 데이터베이스에 한꺼번에 전달하여 실행.
쓰기 지연 동작 예시
코드 예제
EntityManagerFactory emf = Persistence.createEntityManagerFactory("example-unit");
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin(); // 트랜잭션 시작
try {
// 1. 엔티티 생성 및 영속성 컨텍스트 등록
User user1 = new User();
user1.setName("John");
em.persist(user1); // INSERT SQL 생성 (쓰기 지연 저장소에 저장)
User user2 = new User();
user2.setName("Jane");
em.persist(user2); // 또 다른 INSERT SQL 생성 (쓰기 지연 저장소에 저장)
// 2. 트랜잭션 커밋
transaction.commit(); // SQL이 한꺼번에 실행 (INSERT 문 두 개)
} catch (Exception e) {
transaction.rollback(); // 오류 발생 시 롤백
} finally {
em.close();
}
emf.close();
주요 동작 흐름
- em.persist(user1) 및 em.persist(user2) 호출 시, INSERT 문은 바로 실행되지 않고 쓰기 지연 저장소에 보관.
- transaction.commit() 호출 시점에 누적된 INSERT 문이 한꺼번에 실행.
- 결과적으로 데이터베이스에 INSERT가 발생.
쓰기 지연의 장점
- 성능 최적화
- 여러 SQL 명령을 한꺼번에 전송하여 데이터베이스 통신 횟수를 줄임.
- 배치 작업과 함께 사용하면 더 큰 성능 향상 가능.
- 트랜잭션 관리의 일관성
- 트랜잭션 종료 시 SQL 실행이 보장되어 데이터 무결성 유지.
- 효율적인 자원 사용
- 데이터베이스 연결 자원을 효율적으로 사용하며, 불필요한 SQL 실행을 방지.
쓰기 지연 주의점
- 트랜잭션 커밋 전 SQL 실행되지 않음
- 영속성 컨텍스트의 변경 사항은 트랜잭션이 커밋되지 않으면 데이터베이스에 반영되지 않음.
- 커밋 전에 **flush()**를 호출하면 SQL이 즉시 실행됨.
- 쓰기 지연 저장소의 크기 관리
- 많은 엔티티 변경이 있을 경우, 쓰기 지연 저장소가 커질 수 있으므로 메모리 사용량을 주의해야 함.
- 변경 사항 반영 시점 제어 필요
- 특정 시점에 변경 내용을 즉시 반영하려면 flush() 메서드를 명시적으로 호출해야 함.
flush()와 쓰기 지연 비교
구분 | 설명 | 호출 시점 |
쓰기 지연 | SQL을 쓰기 지연 저장소에 모아두고, 트랜잭션 커밋 시 한꺼번에 실행. | 트랜잭션 커밋 시. |
flush() | 쓰기 지연 저장소에 있는 SQL을 데이터베이스에 즉시 실행. | 명시적으로 em.flush() 호출 시. |
쓰기 지연의 동작 확인
Hibernate가 생성하는 SQL 로그
User user = new User();
user.setName("Test");
em.persist(user); // SQL 생성되지만 실행 X (쓰기 지연 저장소에 저장)
// SQL 로그 출력
// No SQL executed yet
transaction.commit(); // 커밋 시점에 SQL 실행
// INSERT INTO User (name) VALUES ('Test')
쓰기 지연과 배치 처리
쓰기 지연은 **배치 처리(batch processing)**와 결합해 성능을 극대화할 수 있습니다. 배치 처리는 쓰기 지연 저장소에 있는 SQL을 그룹화하여 한꺼번에 처리하는 방식으로, 대규모 데이터 처리 시 유용합니다.
배치 처리 예시
- Hibernate 설정에서 배치 크기 지정:
hibernate.jdbc.batch_size=30
결론
쓰기 지연은 JPA의 중요한 최적화 전략으로, 성능을 개선하고 데이터 일관성을 유지합니다. 하지만 트랜잭션 커밋 시점에 모든 SQL이 실행되므로, 이를 적절히 이해하고 활용하는 것이 중요합니다. 😊
flush
- 📌 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 작업입니다. 즉, 쓰기 지연 저장소에 있는 SQL 명령문을 데이터베이스에 반영하는 과정을 의미합니다. 하지만 트랜잭션은 여전히 열려 있으며, 커밋이 이루어진 것은 아닙니다.
Flush의 주요 특징
- 쓰기 지연 저장소의 SQL 실행
- 영속성 컨텍스트에 있는 쓰기 지연 저장소에 누적된 SQL을 데이터베이스로 전송해 실행합니다.
- 트랜잭션 유지
- Flush는 트랜잭션을 종료하지 않습니다. 즉, 데이터베이스에 반영되더라도 트랜잭션 롤백이 가능합니다.
- 자동 호출 시점
- 기본적으로 JPA는 특정 상황에서 Flush를 자동으로 호출합니다:
- 트랜잭션 커밋 시점: commit() 호출 전에 Flush가 실행됩니다.
- JPQL 또는 Criteria 쿼리 실행 전: 쿼리 실행 전에 Flush로 변경 내용을 동기화해 일관성을 유지합니다.
- 기본적으로 JPA는 특정 상황에서 Flush를 자동으로 호출합니다:
Flush 동작 예시
코드 예제
EntityManagerFactory emf = Persistence.createEntityManagerFactory("example-unit");
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin();
User user = new User();
user.setName("John");
em.persist(user); // 영속성 컨텍스트에 저장, SQL은 실행되지 않음
em.flush(); // Flush 호출, INSERT SQL 실행
System.out.println("Flush 이후 실행");
user.setName("Updated John"); // 영속성 컨텍스트에서 Dirty Checking
transaction.commit(); // 트랜잭션 커밋, 변경 내용 반영
동작 흐름
- em.persist(user)로 쓰기 지연 저장소에 INSERT 문이 추가됨.
- em.flush() 호출 시, INSERT 문이 즉시 실행되어 데이터베이스에 반영.
- user.setName()으로 엔티티 변경 후, commit() 시 Dirty Checking으로 변경 사항이 반영됨.
Flush 호출 방식
- 자동 Flush
- 특정 시점에서 JPA가 자동으로 Flush를 호출합니다.
- 자동 호출 시점:
- 트랜잭션 커밋 직전.
- JPQL 실행 직전 (변경 내용과 쿼리 결과의 일관성을 유지하기 위해).
- 명시적 Flush
- 개발자가 EntityManager.flush()를 직접 호출해 변경 내용을 즉시 반영할 수 있습니다.
em.flush();
Flush의 동작 원리
단계 | 설명 |
쓰기 지연 저장소 확인 | 쓰기 지연 저장소에 있는 INSERT, UPDATE, DELETE SQL 문을 확인. |
변경 감지(Dirty Checking) | 영속 상태의 엔티티를 확인하고 변경된 엔티티를 쓰기 지연 저장소에 업데이트. |
SQL 실행 | 쓰기 지연 저장소의 SQL 명령을 데이터베이스로 전송. |
트랜잭션 유지 | Flush 후에도 트랜잭션은 유지되며, 롤백이 가능. |
Flush와 트랜잭션 커밋 비교
구분 | Flush | Commit |
역할 | 영속성 컨텍스트 변경 내용을 데이터베이스에 동기화. | 데이터베이스 트랜잭션을 종료하고, 모든 변경 내용을 확정. |
트랜잭션 유지 | 트랜잭션 유지 (롤백 가능). | 트랜잭션 종료. |
호출 시점 | 필요할 때 수동 호출 또는 자동 호출 (JPQL, Criteria 쿼리, Commit). | transaction.commit() 호출 시. |
Flush의 활용 예시
- JPQL 실행 전 Flush
- JPQL 쿼리를 실행하면 Flush가 자동으로 호출되어 변경 사항을 반영한 최신 데이터를 조회합니다.
User user = new User(); user.setName("John"); em.persist(user); // 영속성 컨텍스트에 저장 em.createQuery("SELECT u FROM User u").getResultList(); // Flush 후 쿼리 실행
- 변경 내용 강제 반영
- 특정 시점에 변경 내용을 강제로 데이터베이스에 반영해야 할 때.
em.flush(); // SQL 실행
- Batch 처리
- Flush를 사용해 주기적으로 데이터베이스에 변경 사항을 반영하여 메모리 사용량을 줄임.
Flush의 주의사항
- Flush 호출 후 롤백 가능
- Flush는 트랜잭션을 종료하지 않으므로, 커밋 전 롤백이 가능합니다.
- Flush로 인해 성능 저하 가능
- Flush가 자주 호출되면 데이터베이스와의 통신이 빈번해져 성능 저하를 초래할 수 있습니다.
- 필요하지 않은 시점에서 Flush가 발생하지 않도록 주의해야 합니다.
- 자동 Flush 관리
- JPQL 실행 시 자동 Flush가 발생하므로 불필요한 변경이 데이터베이스에 반영되지 않도록 주의해야 합니다.
결론
Flush는 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 핵심 메커니즘으로, 데이터의 일관성을 유지하고 데이터베이스 작업을 제어하는 데 중요한 역할을 합니다. 이를 적절히 사용하여 트랜잭션 내에서 원하는 시점에 변경 내용을 반영할 수 있습니다. 😊
Flush는 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 작업입니다. 즉, 쓰기 지연 저장소에 있는 SQL 명령문을 데이터베이스에 반영하는 과정을 의미합니다. 하지만 트랜잭션은 여전히 열려 있으며, 커밋이 이루어진 것은 아닙니다.
즉시 로딩 (Eager Loading)
📌 JPA에서 엔티티를 조회할 때 연관된 모든 데이터(연관된 엔티티)를 즉시 로딩하는 전략입니다. 즉, 부모 엔티티를 조회할 때 연관된 자식 엔티티까지 한 번에 가져오는 방식입니다.
1. Eager Loading의 특징
항목 | 설명 |
로딩 시점 | 부모 엔티티를 조회하는 시점에 연관된 엔티티도 함께 로딩. |
기본값 | - @ManyToOne과 @OneToOne 관계에서는 기본 로딩 전략이 Eager.- 나머지는 Lazy가 기본값. |
쿼리 특징 | 연관된 엔티티를 JOIN을 사용하여 한 번의 SQL로 가져오거나, 별도의 SELECT 쿼리로 가져옴. |
사용 목적 | 연관된 엔티티를 항상 사용할 때 적합. |
2. Eager Loading 설정 방법
기본 설정
- @ManyToOne 및 @OneToOne에서는 기본적으로 Eager 로딩이 적용됩니다.
@ManyToOne(fetch = FetchType.EAGER) // 생략해도 기본값
@JoinColumn(name = "user_id")
private User user;
명시적 설정
- fetch 속성을 FetchType.EAGER로 설정하여 강제로 즉시 로딩을 적용.
@OneToMany(fetch = FetchType.EAGER)
private List<Order> orders;
3. Eager Loading 동작 방식
(1) 단일 SELECT 쿼리 (JOIN FETCH 사용)
- 연관된 엔티티를 한 번의 SQL로 조회.
SELECT o.*, u.*
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.id = 1;
(2) N+1 문제 발생 가능
- Eager Loading으로 인해 추가적으로 불필요한 SELECT 쿼리가 다수 실행될 가능성이 있음.
-- 부모 엔티티 조회
SELECT * FROM orders;
-- 연관된 자식 엔티티 개별 조회 (N번 실행)
SELECT * FROM users WHERE id = ?;
SELECT * FROM users WHERE id = ?;
...
4. Eager Loading의 장점
- 편리한 데이터 로딩
- 연관된 엔티티를 즉시 사용할 수 있어 코드가 단순화됨.
- 한 번에 필요한 데이터 조회
- 적절히 설계하면 한 번의 JOIN으로 필요한 데이터를 모두 가져올 수 있음.
- 일관된 데이터 제공
- 트랜잭션 내에서 연관된 모든 엔티티를 로드해 항상 최신 데이터를 제공.
5. Eager Loading의 주의점
(1) N+1 문제
- 부모 엔티티를 조회한 뒤 연관된 자식 엔티티를 각각 조회하는 추가 쿼리가 발생.
- 데이터가 많아질수록 성능이 급격히 저하됨.
- 예: 부모 엔티티 1개와 자식 엔티티 N개를 조회할 때 N+1개의 쿼리 발생.
해결 방법:
- FETCH JOIN 사용:
- SELECT o FROM Order o JOIN FETCH o.user;
- Batch Fetch 크기 설정 (Hibernate):
- hibernate.default_batch_fetch_size=10
(2) 불필요한 데이터 로딩
- 연관된 엔티티가 항상 필요한 것은 아니기 때문에, 성능이나 메모리 낭비로 이어질 수 있음.
- 예: 연관된 데이터가 매우 크거나, 자주 사용되지 않는 경우.
해결 방법:
- FetchType.LAZY로 변경하여 필요한 시점에 데이터 로딩.
(3) 대량의 데이터 로딩 시 성능 문제
- 연관된 엔티티가 대량일 경우 Eager Loading으로 인해 불필요한 데이터가 메모리에 로드될 수 있음.
해결 방법:
- 페이징을 고려하거나, 연관된 엔티티를 필요한 시점에 로딩.
(4) 복잡한 연관관계에서 JOIN 과다
- 다단계 연관 관계에서 Eager Loading이 적용되면, 불필요한 JOIN으로 인해 SQL이 복잡해지고 성능 저하가 발생.
해결 방법:
- JPQL이나 네이티브 쿼리로 필요한 데이터만 로드.
6. Eager Loading을 사용할 때 고려해야 할 상황
상황 권장 | 사용 여부 |
연관된 엔티티를 항상 사용하는 경우 | Eager Loading 사용 가능 |
연관된 엔티티가 자주 사용되지 않는 경우 | Lazy Loading 사용 권장 |
데이터가 대량이고, 일부만 필요한 경우 | Lazy Loading + Fetch Join |
여러 계층으로 복잡한 연관관계가 있는 경우 | Lazy Loading 사용 권장 |
7. Eager vs Lazy 비교
Eager Loading | Lazy Loading | |
로딩 시점 | 즉시 로딩 (부모 조회 시 자식도 함께 로드) | 지연 로딩 (연관 엔티티를 사용할 때 로드) |
기본값 | @ManyToOne, @OneToOne | @OneToMany, @ManyToMany |
성능 | 데이터가 많으면 성능 저하 (불필요한 데이터 로드) | 필요할 때만 로딩하므로 성능에 유리 |
쿼리 발생 | 추가 JOIN이나 SELECT 쿼리가 발생 | 실제로 접근하기 전까지 쿼리 발생 안 함 |
사용 목적 | 연관 데이터를 항상 사용하는 경우 적합 | 연관 데이터를 자주 사용하지 않는 경우 적합 |
8. 결론
Eager Loading은 연관된 데이터를 항상 사용하는 경우 편리하고 유용하지만, N+1 문제, 불필요한 데이터 로딩, 복잡한 쿼리로 인해 성능 저하를 유발할 수 있습니다. 일반적으로 JPA에서는 **지연 로딩(Lazy Loading)**을 기본으로 설정하고, 필요 시 FETCH JOIN 또는 Batch Fetch 전략을 병행하여 성능을 최적화하는 것이 권장됩니다.
지연 로딩 (Lazy Loading)
📌 JPA에서 연관된 엔티티를 실제 사용할 때 데이터베이스에서 로딩하는 방식입니다. 즉, 부모 엔티티를 조회할 때는 연관된 자식 엔티티를 로드하지 않고, 자식 엔티티에 접근하는 시점에 데이터를 가져옵니다. 이를 통해 성능 최적화와 메모리 사용량 절감이 가능합니다.
1. Lazy Loading의 특징
항목 | 설명 |
로딩 시점 | 연관된 엔티티에 실제 접근하는 순간에 데이터베이스에서 쿼리를 실행. |
기본값 | @OneToMany, @ManyToMany 관계에서는 기본 Fetch 전략이 Lazy. |
쿼리 특징 | 부모 엔티티를 조회할 때 연관된 자식 엔티티를 즉시 로드하지 않음. |
사용 목적 | 연관된 엔티티를 자주 사용하지 않을 경우 성능 최적화에 적합. |
2. Lazy Loading 설정
기본 설정
- @OneToMany, @ManyToMany는 기본적으로 Fetch 전략이 Lazy로 설정되어 있음.
@OneToMany(mappedBy = "parent") // 기본적으로 Lazy Loading
private List<Child> children;
명시적 설정
- FetchType을 명시적으로 **FetchType.LAZY**로 설정.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
3. Lazy Loading 동작 방식
(1) 프록시 객체 활용
- Lazy Loading은 프록시 객체를 사용하여 동작.
- JPA는 연관된 엔티티 대신 프록시 객체를 반환하며, 실제 데이터가 필요할 때 데이터베이스 쿼리를 실행하여 로드.
- 프록시 객체는 초기에는 실제 데이터를 포함하지 않으며, 접근 시 데이터를 로드.
User user = em.find(User.class, 1L); // User만 조회, 연관된 Order는 로드되지 않음
user.getOrders(); // 이 시점에 Order를 조회하는 쿼리가 실행됨
4. Lazy Loading 쿼리 예시
(1) 부모 엔티티 조회
SELECT * FROM users WHERE id = 1;
(2) 자식 엔티티 조회 (프록시 객체 접근 시)
SELECT * FROM orders WHERE user_id = 1;
5. Lazy Loading의 장점
- 성능 최적화
- 연관된 엔티티를 필요할 때만 로드하므로, 초기 로딩 시 성능 저하를 방지.
- 메모리 효율성
- 연관된 데이터가 많을 경우, 사용하지 않는 데이터를 메모리에 로드하지 않아 메모리 사용량 절감.
- 유연성
- 애플리케이션 요구사항에 따라 실제로 필요한 데이터만 로드 가능.
6. Lazy Loading의 주의점
(1) N+1 문제
- Lazy Loading을 사용하는 경우, 부모 엔티티를 조회한 뒤 각 자식 엔티티를 조회하면서 추가 쿼리가 발생.
- 예를 들어, 부모 엔티티 N개에 대해 자식 엔티티를 조회하면 총 N+1개의 쿼리가 실행됨.
문제 발생 예시
List<User> users = em.createQuery("SELECT u FROM User u", User.class).getResultList();
for (User user : users) {
System.out.println(user.getOrders().size()); // 자식 엔티티를 조회할 때마다 쿼리 발생
}
해결 방법
- FETCH JOIN 사용:
- 부모와 자식 데이터를 한 번에 로드.
SELECT u FROM User u JOIN FETCH u.orders;
- Hibernate Batch Fetch:
- Hibernate 설정을 통해 Batch Fetch 크기 조정.
hibernate.default_batch_fetch_size=10
(2) 프록시 초기화 오류 (LazyInitializationException)
- Lazy Loading은 **영속성 컨텍스트(EntityManager)**가 열려 있어야 동작.
- 트랜잭션이 종료된 후에 프록시 객체를 접근하면 데이터베이스를 조회할 수 없으므로 **LazyInitializationException**이 발생.
문제 발생 예시
@Transactional
public User getUser(Long userId) {
User user = em.find(User.class, userId); // Lazy 프록시 반환
return user; // 트랜잭션 종료
}
User user = service.getUser(1L);
System.out.println(user.getOrders().size()); // LazyInitializationException 발생
해결 방법
- 트랜잭션 범위 확장:
- 데이터를 조회하고 필요한 데이터에 접근하는 작업을 같은 트랜잭션 내에서 수행.
- FETCH JOIN 사용:
- 데이터 조회 시 필요한 연관 데이터를 모두 로드.
- DTO로 변환:
- 필요한 데이터를 조회한 후 DTO로 변환하여 반환.
@Query("SELECT new com.example.dto.UserDTO(u.id, u.name, o.id) FROM User u JOIN u.orders o WHERE u.id = :id") UserDTO findUserWithOrders(@Param("id") Long id);
7. Lazy Loading 사용 시 고려 사항
상황 | 권장 전략 |
연관 엔티티가 항상 필요한 경우 | Eager Loading 또는 FETCH JOIN 사용. |
연관 엔티티를 자주 사용하지 않는 경우 | Lazy Loading. |
트랜잭션 외부에서 연관 데이터를 사용할 경우 | DTO 변환 또는 FETCH JOIN 사용. |
데이터 양이 많아 성능 문제가 우려되는 경우 | Lazy Loading + Batch Fetch 사용. |
8. Lazy Loading을 최적화하는 방법
- FETCH JOIN 사용:
- JPQL에서 FETCH JOIN을 통해 필요한 데이터를 한 번에 로드.
SELECT u FROM User u JOIN FETCH u.orders;
- Hibernate Batch Fetch 설정:
- Hibernate의 default_batch_fetch_size 설정으로 한 번에 여러 엔티티를 로드.
hibernate.default_batch_fetch_size=10
- DTO로 필요한 데이터만 변환:
- 복잡한 연관 관계가 있는 엔티티는 DTO로 필요한 필드만 조회하여 반환.
@Query("SELECT new com.example.dto.UserDTO(u.id, u.name, COUNT(o.id)) FROM User u LEFT JOIN u.orders o GROUP BY u.id") List<UserDTO> findUsersWithOrderCount();
9. 결론
Lazy Loading은 메모리와 성능 최적화를 위해 기본적으로 권장되는 로딩 전략입니다. 하지만 N+1 문제와 프록시 초기화 오류에 유의해야 하며, 이를 방지하기 위해 FETCH JOIN, Batch Fetch, DTO 변환 등을 적절히 활용하는 것이 중요합니다.
Proxy
📌 **지연 로딩(Lazy Loading)**을 지원하기 위해 생성되는 가짜 객체로, 엔티티의 실제 데이터 로딩을 지연시키고 필요한 시점에 데이터베이스에서 로딩합니다. 프록시는 JPA가 연관된 엔티티를 즉시 로드하지 않고, 필요한 시점에 쿼리를 실행하여 성능을 최적화하는 데 중요한 역할을 합니다.
1. Proxy란?
- 프록시는 JPA가 생성하는 가짜 객체로, 실제 엔티티 대신 반환됩니다.
- 프록시는 실제 데이터베이스 조회를 지연하고, 엔티티의 메서드가 호출되거나 필드에 접근하는 시점에 데이터베이스에서 데이터를 로드합니다.
- Hibernate는 프록시 객체를 통해 Lazy Loading을 구현합니다.
2. Proxy의 동작 원리
- em.find() 대신 em.getReference()를 호출하거나, 연관 엔티티가 Lazy 로딩으로 설정된 경우 프록시 객체가 반환됩니다.
- 프록시 객체는 실제 엔티티 클래스를 상속받아 생성되며, 데이터가 로드되기 전에는 **필요 최소한의 메타데이터(ID 값)**만 가지고 있습니다.
- 데이터베이스에서 데이터를 로드해야 할 때, 프록시가 내부적으로 데이터베이스에 접근하여 데이터를 로드합니다.
예제 코드
User user = em.getReference(User.class, 1L); // 프록시 객체 반환
System.out.println(user.getClass().getName()); // 프록시 클래스 이름 출력
System.out.println(user.getName()); // 실제 데이터베이스 쿼리가 실행되어 이름 로드
3. Proxy 사용 예제
(1) 프록시 반환
- em.getReference()를 사용하면 프록시 객체가 반환됩니다.
User user = em.getReference(User.class, 1L);
System.out.println(user.getClass().getName()); // 프록시 객체의 클래스 이름 출력
(2) 프록시 초기화
- 프록시 객체의 데이터를 실제로 사용할 때 데이터베이스 쿼리가 실행되어 초기화됩니다.
System.out.println(user.getName()); // 이 시점에 SELECT 쿼리가 실행됨
4. Proxy와 Lazy Loading
- Lazy Loading은 연관된 엔티티를 처음부터 로드하지 않고, 실제로 접근하는 시점에 로드합니다.
- 프록시 객체를 활용해 필요한 시점까지 데이터 로딩을 지연시킵니다.
Lazy Loading 설정
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
5. Proxy 사용 시 주의사항
(1) LazyInitializationException
- 프록시 객체는 **영속성 컨텍스트(EntityManager)**가 열려 있는 동안에만 초기화할 수 있습니다.
- 트랜잭션이 종료된 상태에서 프록시 객체에 접근하려 하면 LazyInitializationException이 발생합니다.
문제 상황
@Transactional
public User getUser(Long id) {
return em.getReference(User.class, id); // 프록시 반환
}
// 트랜잭션 종료 후
System.out.println(user.getName()); // LazyInitializationException 발생
해결 방법
- 트랜잭션 내에서 데이터 초기화
- 트랜잭션이 종료되기 전에 데이터를 로드.
user.getName(); // 트랜잭션 내에서 초기화
- FETCH JOIN 사용
- JPQL로 데이터를 조회할 때 필요한 연관 데이터를 함께 로드.
SELECT u FROM User u JOIN FETCH u.orders WHERE u.id = :id
- DTO 사용
- 필요한 데이터를 조회하여 DTO로 반환.
@Query("SELECT new com.example.dto.UserDTO(u.id, u.name) FROM User u WHERE u.id = :id") UserDTO findUserDTO(Long id);
(2) 프록시 객체 비교
- 프록시는 실제 엔티티와 다른 객체이기 때문에, == 비교가 아닌 equals() 비교를 사용해야 합니다.
User user1 = em.find(User.class, 1L); // 실제 엔티티 반환
User user2 = em.getReference(User.class, 1L); // 프록시 반환
System.out.println(user1 == user2); // false (프록시와 실제 엔티티는 다른 객체)
System.out.println(user1.equals(user2)); // true (equals() 비교는 같음)
(3) 성능 문제
- 프록시 초기화 시 쿼리가 실행되므로, 연관 엔티티가 많을 경우 N+1 문제가 발생할 수 있습니다.
문제 상황
List<User> users = em.createQuery("SELECT u FROM User u", User.class).getResultList();
for (User user : users) {
System.out.println(user.getOrders().size()); // 각 User의 Orders를 조회할 때마다 쿼리 발생
}
해결 방법
- FETCH JOIN 사용:
- SELECT u FROM User u JOIN FETCH u.orders;
- Batch Fetch 설정:
- hibernate.default_batch_fetch_size=10
6. Proxy의 장점
- 성능 최적화
- 초기 로딩 시 필요한 최소한의 데이터만 가져와 성능을 최적화.
- 메모리 효율성
- 대량의 연관 데이터를 지연 로딩하여 메모리 사용량 절감.
- 유연성
- 실제 데이터를 로드하지 않고도 엔티티의 ID와 같은 기본 정보를 사용할 수 있음.
7. Proxy와 관련된 메서드
(1) Hibernate.initialize()
- Hibernate가 제공하는 메서드로, 프록시 객체를 강제로 초기화.
User user = em.getReference(User.class, 1L);
Hibernate.initialize(user); // 강제로 초기화
(2) PersistenceUtil.isLoaded()
- JPA 표준 메서드로, 프록시 객체가 초기화되었는지 확인.
User user = em.getReference(User.class, 1L);
boolean isLoaded = Persistence.getPersistenceUtil().isLoaded(user); // 초기화 여부 확인
8. 결론
JPA의 프록시(Proxy)는 Lazy Loading의 핵심적인 구현 도구로, 성능 최적화와 메모리 효율성을 제공합니다. 그러나 LazyInitializationException, N+1 문제, 프록시 객체 비교 이슈와 같은 주의 사항이 있으므로, 적절한 시점에 데이터를 로드하거나 **FETCH JOIN**과 Batch Fetch를 활용하여 최적화해야 합니다.
N + 1 문제
📌 JPA에서 **지연 로딩(Lazy Loading)**을 사용하는 경우 발생하는 성능 문제로, **하나의 쿼리(N)**로 조회한 엔티티에 대해 연관된 데이터를 조회하면서 **추가적인 쿼리(1)**가 반복적으로 발생하는 현상입니다. 데이터가 많아질수록 추가 쿼리 수가 증가하므로 성능에 큰 영향을 미칩니다.
1. N + 1 문제의 동작 원리
- 1 쿼리: 부모 엔티티를 조회하는 기본 쿼리 실행.
- N 쿼리: 각 부모 엔티티와 연관된 자식 엔티티를 조회하기 위해 N번 추가 쿼리 실행.
예제 코드
List<User> users = em.createQuery("SELECT u FROM User u", User.class).getResultList();
for (User user : users) {
System.out.println(user.getOrders().size()); // 지연 로딩으로 인해 쿼리 발생
}
발생 쿼리
- 부모 엔티티 조회 (1번 실행):
SELECT * FROM User;
- 자식 엔티티 조회 (부모 수만큼 실행):
SELECT * FROM Order WHERE user_id = 1; SELECT * FROM Order WHERE user_id = 2; ...
2. N + 1 문제의 영향
- 데이터의 양이 많아질수록 실행되는 쿼리 수가 기하급수적으로 증가.
- 데이터베이스 부하 증가와 성능 저하 발생.
3. N + 1 문제의 해결 방법
(1) Fetch Join 사용
FETCH JOIN은 JPQL에서 부모와 자식 엔티티를 함께 조회하여 N+1 문제를 해결합니다. 연관된 데이터를 한 번의 JOIN 쿼리로 가져옵니다.
JPQL 사용 예제
SELECT u FROM User u JOIN FETCH u.orders;
장점
- 부모와 자식 데이터를 한 번의 쿼리로 조회.
- Lazy Loading 설정을 무시하고 즉시 로딩.
실행 쿼리
SELECT u.*, o.*
FROM User u
LEFT JOIN Order o ON u.id = o.user_id;
(2) Batch Fetch 설정
Hibernate의 Batch Fetch는 연관된 엔티티를 한 번에 가져오는 쿼리 최적화 방법입니다. N번 쿼리를 Batch Size 크기로 묶어 실행합니다.
Batch Fetch 설정
hibernate.default_batch_fetch_size=10
동작 방식
- 10개 단위로 연관 데이터를 로드.
- 여러 쿼리가 발생하지만, 성능이 크게 개선.
실행 쿼리
SELECT * FROM Order WHERE user_id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
(3) Entity Graph 사용
JPA 2.1에서 제공하는 Entity Graph를 사용하면 특정 연관 데이터를 명시적으로 가져올 수 있습니다.
Entity Graph 설정
@Entity
@NamedEntityGraph(
name = "User.orders",
attributeNodes = @NamedAttributeNode("orders")
)
public class User {
// ...
}
사용 예제
EntityGraph<?> entityGraph = em.getEntityGraph("User.orders");
Map<String, Object> hints = new HashMap<>();
hints.put("javax.persistence.fetchgraph", entityGraph);
List<User> users = em.createQuery("SELECT u FROM User u", User.class)
.setHint("javax.persistence.fetchgraph", entityGraph)
.getResultList();
(4) DTO를 사용한 직접 조회
필요한 데이터만 조회하여 DTO(Data Transfer Object)에 담아 반환합니다. 이는 쿼리를 직접 작성하므로 가장 높은 성능을 제공합니다.
JPQL을 사용한 DTO 조회
List<UserOrderDTO> results = em.createQuery(
"SELECT new com.example.UserOrderDTO(u.name, o.productName) " +
"FROM User u JOIN u.orders o", UserOrderDTO.class)
.getResultList();
쿼리
SELECT u.name, o.product_name
FROM User u
JOIN Order o ON u.id = o.user_id;
(5) Lazy Loading이 필요 없는 경우 Eager Loading 사용
Lazy Loading으로 인해 N+1 문제가 발생하는 경우, Eager Loading으로 변경하여 연관 데이터를 즉시 로딩합니다.
Eager Loading 설정
@OneToMany(fetch = FetchType.EAGER)
private List<Order> orders;
주의사항
- Eager Loading은 항상 모든 연관 데이터를 로딩하므로, 불필요한 데이터가 많아지면 오히려 성능 저하를 유발할 수 있습니다.
4. 해결 방법 비교
장점 | 단점 | |
Fetch Join | 단일 쿼리로 데이터 로드. | 다단계 연관관계에서 복잡한 JOIN으로 인해 성능 저하 가능. |
Batch Fetch | 쿼리 수를 줄이고 성능 개선. | 추가 설정 필요, JOIN보다 많은 쿼리가 발생할 수 있음. |
Entity Graph | 명시적으로 연관 데이터를 로드하여 제어 가능. | 설정 및 관리가 다소 복잡. |
DTO 조회 | 필요한 데이터만 조회하여 성능 최적화. | JPQL 또는 네이티브 쿼리를 직접 작성해야 함. |
Eager Loading | 연관 데이터 항상 로딩, 코드 간결화. | 불필요한 데이터 로딩으로 메모리 및 성능 문제 가능. |
5. N + 1 문제 해결 시 주의사항
- 모든 연관 데이터를 항상 가져오지 말 것
- Lazy Loading과 Fetch Join을 적절히 조합하여 필요할 때만 데이터를 로드.
- 복잡한 연관관계의 Fetch Join 주의
- Fetch Join은 다단계 연관관계에서 SQL 쿼리가 매우 복잡해질 수 있음.
- Batch Fetch 크기 조정
- hibernate.default_batch_fetch_size 값을 적절히 조정하여 최적의 성능을 도출.
- DTO 활용
- 복잡한 연관관계에서는 DTO를 사용하여 필요한 데이터만 조회하는 것이 효과적.
6. 결론
N+1 문제는 JPA를 사용할 때 빈번하게 발생하는 성능 문제 중 하나입니다. 이를 해결하기 위해서는 Fetch Join, Batch Fetch, DTO 조회, Entity Graph 등을 적절히 활용해야 합니다. 상황에 따라 가장 적합한 해결 방법을 선택하여 성능 최적화와 코드 유지보수성을 동시에 확보하는 것이 중요합니다.
변경 감지(Dirty Checking)
📌 영속성 컨텍스트가 엔티티의 초기 상태를 저장하고 트랜잭션 커밋 시점에 현재 상태와 비교해 변경 사항이 있는지 확인하는 기능이다.
- Database에 저장된 데이터가 있는 상태
- xml <property name="hibernate.hbm2ddl.auto" value="none" /> 설정
public static void main(String[] args) {
// EntityManagerFactory 생성
EntityManagerFactory emf = Persistence.createEntityManagerFactory("test");
// EntityManager 생성
EntityManager em = emf.createEntityManager();
// Transaction 생성
EntityTransaction transaction = em.getTransaction();
// 트랜잭션 시작
transaction.begin();
try {
Tutor tutor = em.find(Tutor.class, 1L);
tutor.setName("수정된 이름");
// Java Collection을 사용하면 값을 수정하고 다시 저장하지 않는다.
// em.persist(tutor);
System.out.println("트랜잭션 Commit 전");
// transaction이 commit되며 실제 SQL이 실행된다.
transaction.commit();
System.out.println("트랜잭션 Commit 후");
} catch (Exception e) {
// 실패 -> 롤백
e.printStackTrace();
transaction.rollback();
} finally {
// 엔티티 매니저 연결 종료
em.close();
}
emf.close();
}
- em.persist(tutor); 로 저장하지 않아도 update SQL이 실행된다.
- Entity를 변경하고자 할 때 em.persist() 를 사용하지 않아야 실수를 방지한다.
em.remove() 를 통해 Entity를 삭제할 때도 위와 같은 방식으로 동작한다. DELETE SQL이 트랜잭션 Commit 시점에 실행된다. |
데이터베이스 드라이버
🧩 애플리케이션(클라이언트)과 데이터베이스 서버 간의 통신을 가능하게 하는 소프트웨어 구성 요소입니다.
- 드라이버는 표준화된 프로토콜(JDBC, ODBC 등)을 통해 서로 다른 언어와 시스템 간의 데이터 전송을 중개합니다.
2. 데이터베이스 드라이버의 역할
- 애플리케이션과 데이터베이스 간의 연결
- 애플리케이션이 데이터베이스와 통신할 수 있도록 DBMS 전용 프로토콜을 사용.
- SQL 명령 전달
- 애플리케이션이 작성한 SQL 명령을 데이터베이스 서버가 이해할 수 있는 형식으로 변환.
- 결과 반환
- 데이터베이스 서버에서 반환된 결과를 클라이언트가 이해할 수 있는 형태로 변환.
3. 데이터베이스 드라이버의 종류
1) JDBC (Java Database Connectivity) 드라이버
- JDBC는 자바에서 데이터베이스와 연결하기 위한 표준 인터페이스.
- 다양한 DBMS(MySQL, Oracle, PostgreSQL 등)에 맞는 JDBC 드라이버를 사용.
- 예시:
- MySQL: mysql-connector-java
- Oracle: ojdbc
- PostgreSQL: postgresql
2) ODBC (Open Database Connectivity) 드라이버
- ODBC는 운영 체제 수준에서 다양한 언어와 데이터베이스 간의 연결을 지원하는 표준 API.
- 운영 체제에 따라 ODBC 드라이버 설치 필요.
3) 네이티브 API 드라이버
- 특정 DBMS가 제공하는 네이티브 클라이언트를 사용하는 드라이버.
- 애플리케이션에서 호출된 명령을 네이티브 DBMS API로 변환.
4) DBMS 벤더별 드라이버
- 데이터베이스 제조사에서 제공하는 드라이버.
- 특정 데이터베이스 전용으로 설계되어 최적화된 성능 제공.
- 예: MySQL의 Connector/J, Oracle의 ojdbc 등.
4. JDBC 드라이버의 유형
유형 | 설명 | 장점 | 단점 |
Type 1: JDBC-ODBC 브릿지 | JDBC 호출을 ODBC 호출로 변환. | 단순하고 초기 설정이 쉬움. | 성능이 느리고, ODBC 설치가 필요. |
Type 2: 네이티브 API 드라이버 | JDBC 호출을 네이티브 DBMS API로 변환. | 네이티브 API를 통해 빠른 성능. | 특정 DBMS에 종속. |
Type 3: 네트워크 프로토콜 드라이버 | JDBC 호출을 미들웨어 서버를 통해 DBMS 프로토콜로 변환. | 다양한 DBMS에 연결 가능. | 미들웨어 설치 필요. |
Type 4: 네이티브 프로토콜 드라이버 | JDBC 호출을 네이티브 DBMS 프로토콜로 직접 변환. | 설치가 간단하고, 플랫폼 독립적. | DBMS별로 별도의 드라이버 필요. |
5. 주요 데이터베이스 드라이버
DBMS | JDBC 드라이버 | 드라이버 파일명 |
MySQL | MySQL Connector/J | mysql-connector-java-X.X.X.jar |
Oracle | Oracle JDBC Thin Driver | ojdbc8.jar |
PostgreSQL | PostgreSQL JDBC Driver | postgresql-X.X.X.jar |
SQLite | SQLite JDBC Driver | sqlite-jdbc-X.X.X.jar |
Microsoft SQL Server | Microsoft JDBC Driver for SQL Server | mssql-jdbc-X.X.X.jar |
6. 데이터베이스 드라이버 사용 흐름 (JDBC 예시)
1) JDBC 드라이버 로드
- JDBC 드라이버를 로드하여 애플리케이션과 데이터베이스를 연결.
Class.forName("com.mysql.cj.jdbc.Driver");
2) 데이터베이스 연결
- 드라이버를 통해 데이터베이스와 연결.
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/mydatabase", "username", "password");
3) SQL 실행
- SQL 명령을 실행하고 결과를 가져옴.
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
while (rs.next()) {
System.out.println(rs.getString("name"));
}
7. 데이터베이스 드라이버의 장점
- 다양한 DBMS 지원
- 동일한 애플리케이션에서 여러 데이터베이스를 연결 가능.
- 표준화된 API 제공
- JDBC/ODBC 표준을 통해 통일된 방식으로 DBMS와 통신 가능.
- 다양한 언어 지원
- 자바, C++, 파이썬 등 다양한 언어에서 데이터베이스 접근 가능.
8. 데이터베이스 드라이버의 단점
- DBMS 종속성
- 특정 DBMS에 최적화된 드라이버 사용 시 다른 DBMS로 전환이 어려움.
- 성능 문제
- ODBC와 같은 브릿지 드라이버는 성능이 느릴 수 있음.
- 초기 설정 필요
- 드라이버 파일 설치 및 클래스 경로 설정이 필요.
결론
데이터베이스 드라이버는 애플리케이션이 데이터베이스와 원활히 통신할 수 있도록 지원하는 필수적인 구성 요소입니다.
특히, JDBC 드라이버는 자바 기반 애플리케이션에서 가장 많이 사용되며, DBMS에 맞는 드라이버를 선택하여 사용하면 됩니다.
추가로, 드라이버의 유형과 설정 방법을 잘 이해하면 데이터베이스 연결 작업이 훨씬 수월해집니다.
궁금한 점이 있다면 언제든지 물어보세요! 😊
[아래의 JDBC Template, MyBatis, 그리고 JPA/Hibernate는 DB 드라이버를 기반으로 동작하는 도구로, 데이터베이스 작업을 더 쉽게 처리하도록 돕는 라이브러리입니다. = 매핑하는 친구들입니다.
DB 드라이버는 데이터베이스와 애플리케이션 간의 기본적인 연결과 통신을 담당하는 소프트웨어입니다. 서로 다릅니다. 오히려 아래의 친구들이 DB드라이버를 이용하죠]
JDBC Template
JDBC Template
JDBC Template은 Spring Framework에서 제공하는 데이터베이스 액세스 도구로, JDBC를 사용한 데이터베이스 작업을 단순화하고, 자원 관리 및 예외 처리를 자동으로 처리해주는 역할을 합니다. JDBC API를 기반으로 하지만, 번거로운 작업(커넥션, 스테이트먼트, ResultSet 처리 등)을 자동화하여 개발자의 생산성을 높여줍니다.
주요 특징
- JDBC 작업 단순화:
- JDBC API의 반복적인 코드(자원 관리, 예외 처리 등)를 제거합니다.
- SQL 실행 및 결과 매핑 과정을 간소화합니다.
- 자원 관리 자동화:
- Connection, Statement, ResultSet과 같은 리소스를 자동으로 닫아줍니다.
- 개발자가 명시적으로 자원을 해제할 필요가 없습니다.
- 예외 변환:
- JDBC의 SQLException을 Spring의 DataAccessException으로 변환하여 좀 더 이해하기 쉬운 예외 처리를 제공합니다.
- 유연한 SQL 실행:
- CRUD 작업, 배치 처리, 동적 쿼리 실행 등을 간단히 처리할 수 있습니다.
- 결과 매핑 지원:
- 데이터베이스에서 가져온 결과를 RowMapper, BeanPropertyRowMapper 등을 사용해 Java 객체로 쉽게 변환할 수 있습니다.
JDBC Template 주요 메서드
- queryForObject():
- 단일 행의 결과를 가져와 Java 객체로 매핑합니다.
- 예:
String sql = "SELECT name FROM users WHERE id = ?"; String name = jdbcTemplate.queryForObject(sql, new Object[]{1}, String.class);
- query():
- 다수의 결과 행을 가져와 RowMapper를 통해 매핑합니다.
- 예:
String sql = "SELECT id, name FROM users"; List<User> users = jdbcTemplate.query(sql, new RowMapper<User>() { @Override public User mapRow(ResultSet rs, int rowNum) throws SQLException { User user = new User(); user.setId(rs.getInt("id")); user.setName(rs.getString("name")); return user; } });
- update():
- INSERT, UPDATE, DELETE와 같은 데이터 변경 작업을 수행합니다.
- 예:
String sql = "UPDATE users SET name = ? WHERE id = ?"; int rowsAffected = jdbcTemplate.update(sql, "John Doe", 1);
- batchUpdate():
- 다수의 SQL을 한 번에 실행(배치 처리).
- 예:
String sql = "INSERT INTO users (name) VALUES (?)"; List<Object[]> batchArgs = Arrays.asList( new Object[]{"John"}, new Object[]{"Jane"}, new Object[]{"Jack"} ); jdbcTemplate.batchUpdate(sql, batchArgs);
- queryForList():
- 쿼리 결과를 List 형태로 반환.
- 예:
String sql = "SELECT name FROM users"; List<String> names = jdbcTemplate.queryForList(sql, String.class);
JDBC Template의 동작 원리
- 데이터 소스(DataSource) 사용:
- JDBC Template는 데이터베이스 연결을 관리하기 위해 DataSource를 사용합니다.
- Spring의 DataSource 설정을 통해 커넥션 풀(Connection Pool)과 같은 기능을 활용합니다.
- SQL 실행 및 예외 처리:
- SQL을 실행하고 예외를 Spring의 DataAccessException으로 변환합니다.
- 결과 매핑:
- RowMapper 또는 BeanPropertyRowMapper를 사용해 SQL 결과를 Java 객체로 변환합니다.
JDBC Template 예제
1. 데이터 조회 (단일 결과):
String sql = "SELECT id, name, email FROM users WHERE id = ?";
User user = jdbcTemplate.queryForObject(sql, new Object[]{1}, new BeanPropertyRowMapper<>(User.class));
2. 데이터 조회 (다중 결과):
String sql = "SELECT id, name FROM users";
List<User> users = jdbcTemplate.query(sql, new RowMapper<User>() {
@Override
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
User user = new User();
user.setId(rs.getInt("id"));
user.setName(rs.getString("name"));
return user;
}
});
3. 데이터 삽입:
String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
int rowsInserted = jdbcTemplate.update(sql, "John Doe", "john.doe@example.com");
4. 배치 삽입:
String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
List<Object[]> batchArgs = Arrays.asList(
new Object[]{"Alice", "alice@example.com"},
new Object[]{"Bob", "bob@example.com"}
);
jdbcTemplate.batchUpdate(sql, batchArgs);
JDBC Template의 장점
- 코드 간소화:
- 자원 관리 및 예외 처리를 자동화하여 코드가 간결해집니다.
- 유연성:
- 복잡한 SQL 쿼리 실행과 결과 매핑 작업을 쉽게 처리할 수 있습니다.
- 데이터베이스 독립성:
- 다양한 DBMS에서 사용 가능하며, Spring의 DataSource를 활용해 쉽게 설정할 수 있습니다.
- Spring과의 통합성:
- Spring Framework와 자연스럽게 통합되며, 트랜잭션 관리와도 잘 연동됩니다.
JDBC Template의 단점
- SQL 직접 작성 필요:
- SQL을 직접 작성해야 하므로, SQL 코드가 많아질수록 관리가 복잡해질 수 있습니다.
- 객체-관계 매핑 부족:
- JDBC Template는 ORM 프레임워크(JPA/Hibernate)와 달리 엔티티 관리나 영속성 컨텍스트를 제공하지 않습니다.
- 반복 코드 가능성:
- 매핑 로직이 반복되거나 코드 중복이 발생할 수 있습니다.
JDBC Template과 다른 기술 비교
기술 JDBC Template JDBC (순수) ORM (JPA/Hibernate)SQL 작성 필요 | 직접 작성 | 직접 작성 | 자동 생성 가능 (필요 시 JPQL 작성) |
결과 매핑 방식 | RowMapper/BeanPropertyRowMapper 사용 | ResultSet 직접 처리 | 자동 매핑 (엔티티와 매핑) |
자원 관리 | 자동 | 직접 처리 | 자동 |
학습 곡선 | 중간 | 낮음 | 높음 |
유연성 | SQL 자유롭게 작성 가능 | SQL 자유롭게 작성 가능 | JPQL/네이티브 SQL 사용 가능하지만 제한적 |
결론
- JDBC Template은 SQL 중심 개발을 지원하면서도, 자원 관리와 매핑 작업을 단순화합니다.
- 복잡한 ORM 프레임워크(JPA/Hibernate) 없이도 간단한 데이터베이스 작업에 적합하며, SQL을 직접 제어하고 싶을 때 유용합니다.
- 하지만 대규모 프로젝트에서 객체-관계 매핑을 자동화해야 한다면 ORM 기술(JPA 등)을 사용하는 것이 더 적합할 수 있습니다. 😊
OueryMapper와 RowMapper
🧩 QueryMapper와 RowMapper는 자바 애플리케이션에서 데이터베이스 조회 결과를 객체로 변환하는 데 사용되는 개념입니다. 주로 Spring JDBC에서 사용되며, 데이터베이스에서 가져온 데이터를 자바 객체로 매핑할 때 활용됩니다.
1. RowMapper란?
RowMapper는 Spring JDBC에서 제공하는 인터페이스로,
데이터베이스의 결과 집합(ResultSet)의 한 행(Row)을 자바 객체로 매핑하는 데 사용됩니다.
RowMapper의 메서드
T mapRow(ResultSet rs, int rowNum) throws SQLException;
- ResultSet rs: 쿼리 결과에서 한 행의 데이터.
- int rowNum: 현재 처리 중인 행의 번호.
- T: 매핑된 자바 객체의 타입.
사용 예시
public class UserRowMapper implements RowMapper<User> {
@Override
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
User user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
user.setEmail(rs.getString("email"));
return user;
}
}
2. QueryMapper란?
QueryMapper는 Spring에서 별도로 제공되는 클래스가 아니며,
데이터베이스 쿼리 결과를 자바 객체로 매핑하기 위한 사용자 정의 매퍼 또는 메커니즘을 의미합니다.
- 보통 QueryMapper는 ResultSetExtractor나 RowMapper를 내부적으로 사용하여 쿼리 결과를 처리합니다.
- 특정 애플리케이션에서 QueryMapper라는 이름으로 결과 매핑을 처리하는 사용자 정의 클래스를 작성하는 경우가 많습니다.
RowMapper와 QueryMapper의 차이점
RowMapper | QueryMapper | |
정의 | Spring JDBC에서 제공하는 인터페이스. | 특정 애플리케이션에서 정의하는 사용자 정의 매핑 도구. |
역할 | ResultSet의 한 행을 자바 객체로 변환. | 쿼리 결과 전체를 처리하여 복합 객체나 여러 데이터로 변환. |
Spring 지원 여부 | Spring에서 기본 제공. | Spring에서 제공하지 않음(사용자 정의). |
RowMapper와 QueryMapper 사용 예시
RowMapper 사용
String sql = "SELECT id, name, email FROM users";
List<User> users = jdbcTemplate.query(sql, new UserRowMapper());
QueryMapper로 사용자 정의
public class QueryMapper {
public static List<User> mapToUsers(ResultSet rs) throws SQLException {
List<User> users = new ArrayList<>();
while (rs.next()) {
User user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
user.setEmail(rs.getString("email"));
users.add(user);
}
return users;
}
}
사용:
String sql = "SELECT id, name, email FROM users";
List<User> users = jdbcTemplate.query(sql, (ResultSet rs) -> QueryMapper.mapToUsers(rs));
RowMapper와 QueryMapper를 함께 사용할 때
때로는 QueryMapper를 정의한 후 내부적으로 RowMapper를 사용하는 방식으로도 구현합니다.
이 방법은 복잡한 매핑이나 결과 데이터 조합이 필요할 때 유용합니다.
예: RowMapper를 활용한 QueryMapper
public class QueryMapper {
public static RowMapper<User> userRowMapper = (rs, rowNum) -> {
User user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
user.setEmail(rs.getString("email"));
return user;
};
public static List<User> mapToUsers(ResultSet rs) throws SQLException {
List<User> users = new ArrayList<>();
while (rs.next()) {
users.add(userRowMapper.mapRow(rs, rs.getRow()));
}
return users;
}
}
RowMapper의 장단점
장점
- 코드 재사용성 증가:
- 매핑 로직을 한 곳에 정의하여 여러 쿼리에서 재사용 가능.
- 간결성:
- 한 행만 처리하는 로직이므로 코드가 간단하고 유지보수 용이.
단점
- 단순한 매핑에만 적합:
- 복잡한 조합 결과를 처리하기 어렵다.
결론
- RowMapper는 Spring JDBC에서 한 행의 데이터를 자바 객체로 매핑하기 위한 표준 인터페이스.
- QueryMapper는 특정 프로젝트나 애플리케이션에서 사용자가 정의한 매핑 로직을 나타내며, RowMapper를 내부적으로 활용할 수도 있음.
SQL Mapper
SQL Mapper란 무엇인가?
SQL Mapper는 데이터베이스와 Java 객체 간의 매핑을 처리하는 기술로, SQL 쿼리 실행과 결과 매핑을 단순화하는 데 사용됩니다. SQL Mapper는 직접 작성한 SQL 쿼리를 사용해 데이터베이스와 상호작용하며, SQL 중심의 데이터 접근 방식을 제공합니다.
대표적인 SQL Mapper 프레임워크로는 MyBatis가 있습니다.
SQL Mapper의 주요 특징
- SQL 중심 개발:
- SQL 쿼리를 직접 작성하고, 이를 Java 코드와 연동합니다.
- SQL 작성과 성능 최적화에 대한 자유도가 높습니다.
- 객체 매핑 지원:
- SQL 실행 결과를 Java 객체로 매핑할 수 있습니다.
- 테이블의 각 행(row)을 Java 객체로 변환하는 과정을 자동화합니다.
- 유연성:
- 복잡한 SQL 쿼리나 데이터베이스의 고유 기능(Stored Procedure, 특정 DBMS 문법 등)을 쉽게 사용할 수 있습니다.
- 데이터 매핑 방식(resultType, resultMap 등)을 유연하게 정의할 수 있습니다.
- JDBC 위에서 동작:
- SQL Mapper는 JDBC API를 기반으로 동작하며, SQL 실행과 결과 처리를 단순화합니다.
- JDBC의 번거로운 작업(자원 관리, ResultSet 처리 등)을 줄여줍니다.
SQL Mapper의 동작 방식
SQL Mapper는 다음과 같은 과정을 통해 데이터베이스와 상호작용합니다:
- SQL 작성:
- SQL Mapper를 사용할 때, SQL 쿼리를 직접 작성합니다.
- XML 파일 또는 애노테이션으로 SQL을 정의합니다.
- 쿼리 실행:
- SQL Mapper는 JDBC를 사용하여 SQL을 실행하고 결과를 가져옵니다.
- 실행 시 필요한 파라미터를 동적으로 바인딩합니다.
- 결과 매핑:
- SQL 결과(ResultSet)를 Java 객체로 변환합니다.
- 매핑 방식은 자동 매핑(resultType) 또는 수동 매핑(resultMap)으로 설정할 수 있습니다.
SQL Mapper의 예시 (MyBatis)
1. SQL 매핑(XML 기반)
XML 파일에서 SQL 쿼리와 매핑을 정의합니다:
<mapper namespace="com.example.UserMapper">
<select id="getUserById" parameterType="int" resultType="com.example.User">
SELECT id, name, email FROM users WHERE id = #{id}
</select>
</mapper>
2. Mapper 인터페이스
Java 코드에서 SQL Mapper를 호출하기 위한 인터페이스를 정의합니다:
public interface UserMapper {
User getUserById(int id);
}
3. SQL 실행
SQL을 실행하고 결과를 매핑합니다:
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.getUserById(1);
System.out.println(user.getName());
SQL Mapper의 장단점
장점
- SQL 제어권:
- SQL 쿼리를 직접 작성하기 때문에 복잡한 쿼리나 데이터베이스 최적화 작업에 유리합니다.
- 매핑 자동화:
- XML이나 애노테이션으로 매핑을 정의하면, SQL 결과를 객체로 변환하는 작업을 단순화할 수 있습니다.
- DBMS 의존적 기능 지원:
- 특정 DBMS의 고유 기능(예: Stored Procedure, 트리거 등)을 쉽게 활용할 수 있습니다.
- 유연성:
- 객체 매핑 방식(resultMap)을 유연하게 설계할 수 있어 다양한 데이터베이스 구조와 연동 가능합니다.
단점
- SQL 작성 부담:
- 모든 SQL을 직접 작성해야 하기 때문에 코드량이 많아지고, 유지보수가 어려울 수 있습니다.
- 복잡한 XML 관리:
- SQL 쿼리를 XML로 관리할 경우, 쿼리가 많아지면 파일 관리가 복잡해질 수 있습니다.
- 1차 캐시 부족:
- JPA 같은 ORM 프레임워크의 영속성 컨텍스트(1차 캐시) 기능이 없으므로, 동일한 쿼리를 반복 실행할 경우 비효율이 발생할 수 있습니다.
SQL Mapper의 사용 사례
- 복잡한 SQL 쿼리가 필요한 경우:
- SQL 최적화와 DBMS 고유 기능을 활용해야 하는 프로젝트.
- 예: 대규모 데이터 처리, 집계 및 통계 작업.
- SQL 제어가 중요한 경우:
- 개발자가 SQL 쿼리를 직접 관리하고 싶을 때.
- ORM의 오버헤드가 부담스러운 경우:
- 단순 데이터베이스 작업이 중심인 애플리케이션에서 ORM의 학습 곡선이나 내부 동작 복잡성을 피하고 싶을 때.
결론
- SQL Mapper는 SQL 중심의 개발을 지원하며, 데이터베이스 작업을 유연하게 처리하고, Java 객체로 쉽게 매핑할 수 있도록 돕습니다.
- 대표적으로 MyBatis가 SQL Mapper의 표준적인 구현체로 많이 사용됩니다.
- ORM(JPA)과 비교해 SQL 작성 및 제어에 대한 자유도가 높고, 성능 최적화가 필요한 상황에서 특히 유용합니다. 😊
MyBatis
🧩 Java 기반의 SQL Mapper 프레임워크로, SQL 쿼리를 명시적으로 작성하면서 객체를 데이터베이스 테이블과 매핑해주는 도구입니다. ORM처럼 객체 지향적으로 데이터를 처리하려는 요구를 수용하면서도, SQL 작성에 대한 개발자의 제어권을 보장합니다.
1. MyBatis의 특징
- SQL 매핑 중심:
- MyBatis는 SQL 쿼리를 직접 작성하여 데이터베이스와 통신합니다.
- XML 파일이나 애노테이션을 통해 SQL 쿼리와 객체를 매핑합니다.
- 명시적인 데이터베이스 작업:
- 데이터베이스 작업(INSERT, UPDATE, DELETE 등)은 직접 쿼리를 호출해야 실행됩니다.
- 매핑된 객체를 변경하더라도 데이터베이스에 아무런 영향을 미치지 않습니다.
- 동적 SQL 지원:
- IF, CHOOSE, FOREACH 같은 태그를 사용하여 동적 SQL을 유연하게 작성할 수 있습니다.
- 높은 유연성:
- 복잡한 쿼리와 데이터베이스의 특정 기능(프로시저, 함수 등)을 자유롭게 사용할 수 있습니다.
- 부분적인 객체 매핑:
- 테이블 전체가 아닌, 원하는 컬럼만 객체로 매핑하거나 특정 쿼리 결과를 처리할 수 있습니다.
2. MyBatis의 장점
장점 | 설명 |
직접 SQL 제어 가능 | SQL을 직접 작성하므로 복잡한 쿼리, 최적화, DB 특정 기능을 활용하기 쉽습니다. |
학습 곡선이 낮음 | 기본적인 SQL과 XML 구조만 알면 빠르게 적용할 수 있습니다. |
유연한 매핑 | 객체와 테이블의 구조가 1:1 매핑이 아니더라도, 원하는 필드만 매핑하거나 여러 테이블의 결과를 조합할 수 있습니다. |
동적 SQL 지원 | 조건부 로직과 반복 쿼리 생성을 XML 또는 애노테이션으로 간단히 처리할 수 있습니다. |
DBMS 독립적이지 않음 | 개발자가 직접 SQL을 작성하므로, DBMS 고유의 기능(예: 오라클의 시퀀스, MySQL의 LIMIT 등)을 활용하기 용이합니다. |
3. MyBatis의 단점
단점 | 설명 |
SQL 관리 복잡성 | SQL을 직접 작성하기 때문에, 쿼리가 많아질수록 관리가 어려워질 수 있습니다. |
코드 중복 가능성 | 동일한 쿼리가 여러 곳에서 재사용될 때, 중복 코드를 줄이기 위한 추가 작업이 필요합니다. |
ORM보다 생산성 낮음 | 단순 CRUD 작업조차도 명시적인 쿼리를 작성해야 하므로 ORM(JPA)보다 생산성이 떨어질 수 있습니다. |
유지보수 어려움 | 테이블이나 컬럼 구조가 변경될 경우, 관련된 모든 SQL 문을 수정해야 합니다. |
1차 캐시 부족 | MyBatis는 JPA와 달리 영속성 컨텍스트(1차 캐시)를 관리하지 않으므로, 동일 객체에 대해 중복 쿼리가 발생할 가능성이 있습니다. |
4. MyBatis의 주요 구성 요소
- SQL Mapper XML:
- SQL 쿼리를 정의하고 매핑 설정을 작성하는 XML 파일.
- 예시:
<select id="selectUserById" parameterType="int" resultType="User"> SELECT * FROM users WHERE id = #{id} </select>
- Mapper 인터페이스:
- XML과 연결된 인터페이스로, 메서드를 호출하면 해당 쿼리가 실행됩니다.
- 예시:
public interface UserMapper { User selectUserById(int id); }
- MyBatis 설정 파일:
- 데이터베이스 연결 정보와 MyBatis 설정을 정의하는 파일.
- 예시:
<configuration> <environments default="development"> <environment id="development"> <transactionManager type="JDBC" /> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/testdb" /> <property name="username" value="root" /> <property name="password" value="password" /> </dataSource> </environment> </environments> <mappers> <mapper resource="com/example/mappers/UserMapper.xml" /> </mappers> </configuration>
5. MyBatis의 사용 이유
- SQL 제어가 중요한 프로젝트:
- 복잡한 쿼리를 자주 작성해야 하거나, 성능 최적화를 위해 SQL을 직접 관리해야 하는 경우 적합합니다.
- DBMS 고유 기능 활용:
- MyBatis는 DBMS 특정 기능을 활용하기 쉬우며, 프로시저/함수와의 연계가 필요할 때 강력한 도구가 됩니다.
- 데이터 중심 애플리케이션:
- 간단한 CRUD 외에 대량의 데이터 처리 작업이 필요한 애플리케이션에서 사용됩니다.
6. MyBatis와 ORM 비교
MyBatis | ORM (JPA/Hibernate) | |
SQL 작성 | 직접 작성 | SQL 자동 생성 (필요 시 JPQL 사용) |
유연성 | 높음 (SQL 직접 작성 가능) | 제한적 (복잡한 SQL 작성 시 불편) |
객체 상태 관리 | 없음 (객체와 DB 상태는 별도 관리) | 영속성 컨텍스트로 객체 상태와 DB 상태 동기화 |
러닝 커브 | 낮음 | 높음 (Entity 설계, JPQL 학습 필요) |
성능 최적화 | SQL 튜닝으로 직접 제어 가능 | ORM 내부 최적화에 의존 |
생산성 | 복잡한 작업에 적합 | CRUD 작업에서는 더 높은 생산성 |
7. 결론
- MyBatis는 SQL을 직접 제어하고 싶거나 복잡한 쿼리가 필요한 프로젝트에 적합합니다.
- 데이터베이스와 객체의 동기화 관리가 자동으로 필요하지 않고, 높은 SQL 최적화가 중요한 경우 사용하면 효율적입니다.
- 다만, 관리해야 할 SQL이 많아질수록 유지보수가 어려워질 수 있으니, 프로젝트의 복잡성과 요구 사항에 따라 선택해야 합니다.
ORM
🧩 ORM(Object-Relational Mapping)은 객체 지향 프로그래밍(OOP)의 객체와 관계형 데이터베이스(RDB)의 테이블 간의 데이터 변환 및 매핑을 자동화하는 기술입니다.
- 객체 지향 언어에서 객체를 사용하여 데이터베이스와 상호작용.
- 객체 간의 관계를 테이블 간의 관계로 매핑.
- QueryMapper 의 DB의존성 및 중복 쿼리 문제로 ORM 이 탄생했다.
- Mapper는 "SQL로 데이터를 가져오고, 객체로 변환하는 도구"이고, ORM은 "테이블과 객체를 아예 1:1로 대응시키는 추상화된 설계"로 Mapper의 경우 java에서 매핑한 객체를 수정해도 DB에 영향이 없지만 ORM은 대응이기에 한번에 수정가능하다. 이렇게 DB에 바로 적용되는 걸 지정하는 것 중에 하나가 지연 쓰기이다.
2. ORM의 주요 목적
- 객체 지향과 관계형 데이터베이스 간의 불일치 해결
- 객체 지향 언어에서는 객체와 필드를 사용하지만, 데이터베이스는 테이블과 컬럼으로 데이터를 관리.
- ORM은 이 간극을 메워 객체 지향적인 데이터 처리를 가능하게 함.
- SQL의 자동화
- 개발자가 직접 SQL을 작성하지 않고도 데이터베이스 작업 가능.
- 데이터를 저장하거나 가져올 때 ORM이 SQL을 생성하여 실행.
- 생산성 향상
- 개발자는 비즈니스 로직에 집중할 수 있으며, 반복적인 SQL 작성이 줄어듦.
3. ORM의 주요 특징
- 객체와 테이블 매핑
- 클래스는 테이블, 필드는 컬럼으로 매핑.
- 객체의 필드를 테이블의 컬럼과 자동으로 연결.
- 관계 매핑
- 객체 간의 관계(1:1, 1:N, N:M)를 데이터베이스의 외래 키와 같은 구조로 매핑.
- 데이터베이스 독립성
- 데이터베이스 종류에 관계없이 동일한 코드로 동작 가능 (DBMS 변경 시 큰 수정 불필요).
- 캐싱
- 데이터베이스와의 상호작용을 줄이기 위해 1차 캐시, 2차 캐시 제공.
4. ORM 동작 예시
1) 엔티티 클래스
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
private String email;
// Getters and Setters
}
- @Entity: User 클래스를 데이터베이스의 테이블에 매핑.
- @Id: 기본 키로 사용.
- @Column: 데이터베이스의 컬럼과 필드 매핑.
2) 데이터 저장
User user = new User();
user.setName("John");
user.setEmail("john@example.com");
entityManager.persist(user); // 객체 저장
- entityManager를 통해 User 객체가 자동으로 SQL로 변환되어 데이터베이스에 저장.
3) 데이터 조회
User user = entityManager.find(User.class, 1L); // ID가 1인 데이터 조회
System.out.println(user.getName());
5. ORM의 장단점
장점
- 생산성 향상
- SQL 작성 없이 객체 지향적인 코드로 데이터베이스 작업 가능.
- 가독성 및 유지보수성
- 데이터베이스 작업이 명시적이며 객체 중심이므로 유지보수 용이.
- 데이터베이스 독립성
- DBMS 변경 시 코드 수정 최소화.
- 재사용성
- 객체 매핑 로직을 재사용 가능.
- 트랜잭션 및 캐싱
- ORM 프레임워크가 트랜잭션과 캐싱을 기본적으로 제공.
단점
ORM 문제점
문제점 | 객체의 특징 | RDB의 특징 | 해결방법 |
상속의 문제 | 객체 간 상속관계 가능. | 테이블 간 상속관계 없음, 독립적으로 존재. | - 매핑 정보에 상속 정보 추가.예: @OneToMany, @ManyToOne |
관계 문제 | 객체는 참조를 통해 관계를 가짐.다대다 관계 가능. | 외래키(FK)로 관계 설정.다대다 관계는 매핑 테이블 필요. | - 매핑 정보에 방향 정보 추가.예: @JoinColumn, @MappedBy |
탐색 문제 | 참조로 객체 탐색 가능.콜렉션 순회 가능. | 참조 시 추가 쿼리 또는 Join 발생. | - 탐색 시점 관리.예: @FetchType, fetchJoin() |
밀도 문제 | 객체의 멤버 크기가 클 수 있음. | 기본 데이터 타입만 존재. | - 큰 객체는 테이블로 분리.예: @Embedded |
식별성 문제 | hashCode 또는 equals() 메서드로 식별. | PK로만 식별 가능. | - PK를 객체 ID로 설정.예: @Id, @GeneratedValue, EntityManager로 관리. |
ORM 해결책
- 영속성 컨텍스트와 쓰기 지연
영속성이란? | 데이터를 프로그램 종료 후에도 유지하기 위해 DB나 파일에 저장하여 영구적으로 보존하는 특성. |
영속성 상태 | 1. 비영속 (new/transient) : 객체가 영속성 컨텍스트에 포함되지 않은 상태. 2. 영속 (managed) : 객체가 영속성 컨텍스트에 저장되어 관리되는 상태. 3. 준영속 (detached) : 영속성 컨텍스트에서 분리된 상태. 4. 삭제 (removed) : 객체가 삭제되어 영속성 컨텍스트와 DB에서 제거된 상태. |
상태 전환 메서드 | new > (비영속상태) > persist(),merge() > (영속성 컨텍스트에 저장된 상태) > flush() > (DB에 쿼리가 전송된 상태) > commit() > (DB에 쿼리가 반영된 상태) |
쓰기 지연이란? | - flush() 호출 전까지 SQL 쿼리를 영속성 컨텍스트에 모아두었다가, 한 번에 DB로 전송하는 최적화 메커니즘. |
쓰기 지연 발생 시점 | - 트랜잭션 중 객체 생성, 수정, 삭제 시. - flush() 호출 전까지 쿼리를 최적화하여 보관. |
쓰기 지연 효과 | - 여러 동작을 모아 쿼리를 한번에 전송하여 최소화. - 생성/수정/삭제 작업의 중간 상태가 발생하더라도 실제 DB에는 최적화된 쿼리만 전송. - 불필요한 쿼리 전송 방지. |
주의점 | - GenerationType.IDENTITY 사용 시, 쓰기 지연이 적용되지 않음. - 이유: IDENTITY 전략은 키 생성 시점에 단일 쿼리가 필요하며, 외부 트랜잭션 간의 키 중복을 방지하기 위해 즉시 DB에 반영됨. |
6. 대표적인 ORM 프레임워크
프레임워크 | 언어 | 특징 |
JPA | Java | Java 표준 ORM API. Hibernate가 대표적인 구현체. |
Hibernate | Java | 가장 널리 사용되는 JPA 구현체. 다양한 기능과 확장성을 제공. |
Entity Framework | C#/.NET | Microsoft에서 제공하는 .NET 기반 ORM. |
SQLAlchemy | Python | 파이썬에서 널리 사용되는 ORM. 고급 매핑 및 SQL 표현 지원. |
Django ORM | Python | Django 프레임워크에 기본 제공되는 ORM. |
7. ORM vs SQL (직접 작성)
특징 | ORM | SQL 직접 작성 |
개발 생산성 | SQL을 자동 생성하므로 빠르게 개발 가능. | 직접 SQL을 작성해야 하므로 반복 작업 많음. |
데이터베이스 독립성 | DBMS 변경 시 코드 수정 최소화. | DBMS 특화 SQL 사용 시 독립성 떨어짐. |
쿼리 제어 | ORM이 SQL을 생성하므로 제어가 제한될 수 있음. | 쿼리를 직접 작성하여 완전한 제어 가능. |
복잡한 쿼리 | 복잡한 쿼리는 ORM에서 처리하기 어렵거나 성능 저하 가능. | 복잡한 쿼리 작성에 유리. |
초기 학습 비용 | ORM 자체를 이해해야 하므로 학습 시간이 필요. | SQL만 이해하면 바로 시작 가능. |
8. 언제 ORM을 사용해야 할까?
- CRUD 중심 애플리케이션
- 데이터베이스와 상호작용이 단순한 경우 ORM이 효율적.
- 객체 지향적인 설계
- 도메인 모델 중심으로 설계된 애플리케이션에서 객체-테이블 간 매핑이 적합.
- 복잡한 비즈니스 로직
- 데이터 처리보다는 비즈니스 로직이 복잡한 경우 ORM이 유리.
- 데이터베이스 독립성이 필요한 경우
- DBMS가 자주 변경되거나 여러 데이터베이스를 지원해야 하는 경우.
9. 결론
ORM은 객체 지향 프로그래밍과 관계형 데이터베이스 간의 패러다임 불일치를 해소하여 생산성을 높이고 유지보수를 용이하게 만듭니다.
특히, JPA와 같은 ORM 프레임워크를 사용하면 객체 지향적으로 데이터를 다루면서 SQL 작성 부담을 줄일 수 있습니다.
그러나 복잡한 쿼리나 성능이 중요한 프로젝트에서는 SQL과 ORM을 적절히 병행해서 사용하는 것이 좋습니다.
Hibernate
🎵 자바 애플리케이션에서 관계형 데이터베이스와 객체 간의 매핑(ORM, Object-Relational Mapping)을 처리하는 오픈소스 프레임워크입니다. Hibernate는 JPA(Java Persistence API)의 구현체 중 하나로, 개발자가 객체 중심의 데이터 처리와 SQL 작성 부담을 줄이고 생산성을 높일 수 있게 합니다.
Hibernate: 개요 및 주요 기능
Hibernate는 자바 애플리케이션에서 관계형 데이터베이스와 객체 간의 매핑(ORM, Object-Relational Mapping)을 처리하는 오픈소스 프레임워크입니다. Hibernate는 JPA(Java Persistence API)의 구현체 중 하나로, 개발자가 객체 중심의 데이터 처리와 SQL 작성 부담을 줄이고 생산성을 높일 수 있게 합니다.
Hibernate의 주요 특징
특징 | 설명 |
ORM (객체-관계 매핑) | 자바 객체와 데이터베이스 테이블을 매핑하여 객체 중심 설계 가능. |
JPA 구현체 | Java EE 표준인 JPA를 구현하여 JPA API를 기반으로 동작. |
HQL (Hibernate Query Language) | 객체를 대상으로 하는 SQL 유사 쿼리 언어로 복잡한 SQL 대신 사용 가능. |
1차 캐시 | 동일 트랜잭션 내에서 영속성 컨텍스트를 활용해 엔티티 조회 시 동일 객체를 반환 (동일성 보장). |
Lazy Loading | 연관된 엔티티를 필요할 때만 로딩하여 성능 최적화. |
쓰기 지연 (Write-Behind) | SQL 명령어를 트랜잭션 종료 시점에 한꺼번에 실행. |
데이터베이스 독립성 | Dialect 설정을 통해 특정 데이터베이스에 종속되지 않음. |
자동 DDL 생성 | 엔티티를 기반으로 테이블 구조 자동 생성 가능. |
Hibernate의 주요 장점
- 객체 중심 설계
- 객체 모델과 데이터베이스 모델 간의 불일치를 해결하여 객체 지향적 개발 가능.
- SQL 자동 생성
- 복잡한 SQL 작성 부담을 줄이고, 기본적인 CRUD 작업을 자동화.
- 데이터베이스 독립성
- 다양한 데이터베이스를 지원하며, 데이터베이스 변경 시 애플리케이션 코드 수정을 최소화.
- 1차 캐시
- 동일 트랜잭션 내에서 같은 객체를 반환하여 메모리와 성능 최적화.
- Lazy Loading
- 실제로 필요한 데이터만 로딩하여 불필요한 데이터 로드를 방지.
Hibernate의 주요 구성 요소
구성 요소 | 설명 |
Session | Hibernate의 주요 작업 단위로, 영속성 컨텍스트를 관리. 엔티티 저장, 조회, 업데이트, 삭제 등 수행. |
SessionFactory | Session을 생성하기 위한 팩토리. 애플리케이션에서 하나만 생성하고 재사용. |
Transaction | 트랜잭션을 관리하며, 데이터베이스 작업의 원자성을 보장. |
Configuration | Hibernate 설정 파일(hibernate.cfg.xml)이나 속성 파일을 로드하여 초기 설정 관리. |
Query | HQL 또는 Native SQL 쿼리를 실행하기 위한 인터페이스. |
Hibernate의 주요 애너테이션
애너테이션 | 설명 |
@Entity | 클래스를 Hibernate 엔티티로 선언. |
@Table | 매핑할 데이터베이스 테이블 이름 지정. |
@Id | 엔티티의 기본 키를 지정. |
@GeneratedValue | 기본 키 값의 자동 생성 전략을 설정 (IDENTITY, SEQUENCE 등). |
@Column | 필드를 데이터베이스 컬럼에 매핑하며, 컬럼 이름, 길이, NULL 여부 등을 설정. |
@OneToOne | 1:1 관계 매핑. |
@OneToMany | 1:N 관계 매핑. |
@ManyToOne | N:1 관계 매핑. |
@ManyToMany | N:M 관계 매핑. |
@JoinColumn | 외래 키 컬럼을 지정. |
Hibernate 주요 동작 예시
1. 기본 엔티티
import jakarta.persistence.*;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(unique = true)
private String email;
// Getters and Setters
}
2. Hibernate 설정 파일 (hibernate.cfg.xml)
<hibernate-configuration>
<session-factory>
<property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property>
<property name="hibernate.connection.driver_class">com.mysql.cj.jdbc.Driver</property>
<property name="hibernate.connection.url">jdbc:mysql://localhost:3306/hibernate_db</property>
<property name="hibernate.connection.username">root</property>
<property name="hibernate.connection.password">password</property>
<property name="hibernate.hbm2ddl.auto">update</property>
</session-factory>
</hibernate-configuration>
3. Hibernate 데이터 처리 예시
SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();
Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();
try {
// 데이터 저장
User user = new User();
user.setName("John");
user.setEmail("john@example.com");
session.save(user);
// 데이터 조회
User retrievedUser = session.get(User.class, 1L);
System.out.println(retrievedUser.getName());
// 데이터 수정
retrievedUser.setName("Updated John");
session.update(retrievedUser);
// 데이터 삭제
session.delete(retrievedUser);
transaction.commit();
} catch (Exception e) {
transaction.rollback();
e.printStackTrace();
} finally {
session.close();
}
Hibernate와 JPA의 관계
항목 | Hibernate | JPA |
정의 | ORM 프레임워크로 JPA의 구현체 중 하나. | 자바의 ORM 표준 API. |
표준 여부 | Hibernate는 표준이 아님. | JPA는 Java EE 표준. |
호환성 | JPA 표준을 따르면서, Hibernate 고유 기능 제공. | Hibernate, EclipseLink 등 다양한 구현체와 호환. |
Hibernate의 장단점
장점 | 단점 |
객체 중심 개발 가능. | 설정 복잡도가 높음. |
다양한 데이터베이스 지원 및 독립성 보장. | 대규모 애플리케이션에서 1차 캐시 사용으로 메모리 부담 가능성 있음. |
SQL 작성 부담 감소 (CRUD 작업 자동화). | 잘못된 Lazy Loading 설계로 인해 성능 문제가 발생할 수 있음. |
HQL을 통해 객체 중심 쿼리 작성 가능. | Hibernate 고유 기능 사용 시 JPA 구현체를 변경하기 어려울 수 있음. |
결론
Hibernate는 JPA 표준을 구현하면서도 강력한 ORM 기능과 성능 최적화를 제공하는 프레임워크입니다. JPA의 구현체로 시작해도 고유한 기능을 활용할 수 있으므로, 개발 생산성을 높이고 객체 지향적인 데이터베이스 설계를 구현하는 데 적합합니다. 추가적으로 Hibernate 고유 기능이나 성능 최적화에 대해 궁금한 점이 있다면 알려주세요! 😊
OueryMapper, RowMapper, MYABITS, ORM 차이점
1. 차이점 정리
QueryMapper (Spring JDBC) | RowMapper (Spring JDBC) | MyBatis | ORM (JPA/Hibernate) | |
주요 역할 | SQL 실행과 쿼리 매핑을 단순화 = 원래는 길었던 코드를 어노테이션으로 간단히 처리 |
데이터베이스 결과(ResultSet)를 Java 객체로 변환 QueryMapper가 RowMapper를 포함한다. |
SQL을 직접 작성해 데이터베이스와 상호작용 | 객체와 데이터베이스 테이블 간 매핑을 자동화 |
SQL 작성 방식 | 직접 작성 | 직접 작성 | 직접 작성 | 자동 생성 (필요 시 JPQL/HQL 작성 가능) |
결과 매핑 방식 | 개발자가 ResultSet 처리 | RowMapper 구현체로 각 행을 매핑 | XML 또는 애노테이션으로 매핑 | 엔티티(Entity)와 필드 매핑 자동화 |
유연성 | SQL 중심으로 고도의 유연성을 제공 | SQL에 따라 객체 변환이 유연 | SQL에 대한 높은 제어 가능 | 복잡한 쿼리 작성은 어렵지만 기본 작업은 자동화 |
러닝 커브 | 낮음 | 낮음 | 중간 | 높음 |
캐싱 지원 여부 | 없음 | 없음 | 기본적으로 없음 | 1차 캐시(영속성 컨텍스트) 및 2차 캐시 지원 |
복잡한 쿼리 지원 | 가능 | 가능 | 가능 | JPQL이나 네이티브 SQL로 가능하지만 제약 있음 |
데이터 변경 반영 | 직접 SQL 실행 필요 | 직접 SQL 실행 필요 | 직접 SQL 실행 필요 | 객체 변경 시 트랜잭션 커밋 시점에 자동 반영 |
2. 용어별 간단 설명
JDBC Template
QueryMapper (Spring JDBC)
- Spring JDBC의 기능으로, SQL 실행과 매핑을 단순화하기 위한 API.
- SQL 쿼리와 매핑 코드를 통합적으로 관리하기 위한 역할을 합니다.
- ResultSet을 처리하는 RowMapper와 함께 사용됩니다.
- 사용 예시:
String sql = "SELECT * FROM users WHERE id = ?"; User user = jdbcTemplate.queryForObject(sql, new Object[]{id}, new UserRowMapper());
RowMapper (Spring JDBC)
- ResultSet의 각 행(row)을 Java 객체로 변환하기 위한 인터페이스.
- 개발자가 필요한 매핑 로직을 직접 작성해야 합니다.
- 사용 예시:
public class UserRowMapper implements RowMapper<User> { @Override public User mapRow(ResultSet rs, int rowNum) throws SQLException { User user = new User(); user.setId(rs.getInt("id")); user.setName(rs.getString("name")); return user; } }
SQL Mapper
MyBatis
- SQL 중심의 SQL Mapper 프레임워크.
- SQL을 XML 또는 애노테이션으로 작성하고, 객체와 매핑합니다.
- SQL 작성에 대한 유연성을 보장하면서도, 매핑 작업을 간소화합니다.
- 사용 예시:
<select id="getUserById" parameterType="int" resultType="com.example.User"> SELECT * FROM users WHERE id = #{id} </select>
ORM (JPA/Hibernate)
ORM (JPA/Hibernate)
- 객체-관계 매핑(ORM) 프레임워크.
- 객체와 데이터베이스 테이블을 매핑하여, 객체 지향적으로 데이터베이스 작업을 처리합니다.
- 데이터베이스와 객체 상태를 자동으로 동기화하며, 트랜잭션 커밋 시점에 데이터베이스에 반영됩니다.
- 사용 예시:
@Entity public class User { @Id @GeneratedValue private int id; private String name; } User user = entityManager.find(User.class, 1); user.setName("Updated Name"); // 데이터베이스에 자동 반영
3. 장단점 비교
QueryMapper/RowMapper (Spring JDBC) | MyBatis | ORM (JPA/Hibernate) | |
장점 | - SQL 제어가 쉬움- 간단한 매핑 로직 구현 가능 | - SQL 작성 자유도 높음- 유연한 매핑 가능 | - 생산성 높음- 데이터베이스 작업 자동화- 객체 중심 설계 가능 |
단점 | - 반복 코드 많음- 유지보수 어려움 | - SQL 작성 필요- 코드 중복 가능성 있음 | - 학습 곡선 높음- 복잡한 쿼리 작성 어려움- 오버헤드 가능성 |
4. 추천 사용 시나리오
사용 시점 | |
QueryMapper/RowMapper | 간단한 데이터 처리 작업이나 Spring JDBC 사용 시 |
MyBatis | SQL 중심의 복잡한 데이터베이스 작업이 필요하거나, 성능 최적화가 중요한 경우 |
ORM (JPA/Hibernate) | 객체 지향적인 설계가 중요한 대규모 프로젝트에서 CRUD 중심의 작업을 빠르게 처리해야 하는 경우 |
5. 결론
- QueryMapper/RowMapper: Spring JDBC를 기반으로 한 저수준의 데이터 처리 도구.
- MyBatis: SQL 작성에 자유로우며 복잡한 데이터 처리에 적합한 SQL Mapper.
- ORM (JPA/Hibernate): 객체 중심 설계와 데이터베이스 동기화를 자동화하는 고수준 프레임워크.
JpaRepository
📌 JpaRepository는 Spring Data JPA에서 제공하는 인터페이스로, JPA를 사용한 데이터베이스 작업을 간소화하기 위해 제공됩니다. 기본적인 CRUD(Create, Read, Update, Delete) 기능과 더불어 페이징 처리, 정렬, 그리고 커스텀 쿼리 메서드 정의 등을 지원합니다.
- 실상 ORM 을 사용하는 가장 쉬운 방법입니다.
JpaRepository의 계층 구조
JpaRepository는 Spring Data JPA에서 제공하는 여러 인터페이스 중 하나이며, 계층 구조는 다음과 같습니다:
- Repository
- 모든 Spring Data Repository의 최상위 인터페이스.
- 마커 인터페이스로, 실제로는 기능을 제공하지 않음.
- CrudRepository
- 기본적인 CRUD 작업(Create, Read, Update, Delete)을 제공하는 인터페이스.
- PagingAndSortingRepository
- 페이징 및 정렬 기능을 추가로 제공하는 인터페이스.
- JpaRepository
- JPA에서 사용할 수 있는 고급 기능(배치 처리, 페이징, 정렬 등)을 포함한 인터페이스.
JpaRepository가 제공하는 주요 기능
1. CRUD 기능
기본적인 데이터 생성, 조회, 수정, 삭제를 처리하는 메서드를 제공합니다.
메서드 | 설명 |
save(S entity) | 엔티티를 저장하거나 업데이트합니다. |
findById(ID id) | ID를 기반으로 엔티티를 조회합니다. |
findAll() | 모든 엔티티를 조회합니다. |
findAllById(Iterable<ID> ids) | 주어진 ID 리스트에 해당하는 엔티티들을 조회합니다. |
deleteById(ID id) | 주어진 ID에 해당하는 엔티티를 삭제합니다. |
deleteAll() | 모든 엔티티를 삭제합니다. |
2. 페이징 및 정렬
페이징과 정렬을 위한 메서드를 제공합니다.
메서드 | 설명 |
findAll(Pageable pageable) | 페이징된 결과를 반환합니다. |
findAll(Sort sort) | 정렬된 결과를 반환합니다. |
findAll(Pageable pageable, Sort sort) | 페이징과 정렬을 함께 적용하여 결과를 반환합니다. |
3. 커스텀 쿼리 메서드
JPA는 메서드 이름을 기반으로 자동으로 SQL 쿼리를 생성해주는 기능을 제공합니다.
메서드 예시 | SQL 쿼리로 변환 예시 |
findByName(String name) | SELECT * FROM Entity WHERE name = ? |
findByAgeGreaterThan(int age) | SELECT * FROM Entity WHERE age > ? |
findByEmailAndName(String email, String name) | SELECT * FROM Entity WHERE email = ? AND name = ? |
findByCreatedAtBetween(Date start, Date end) | SELECT * FROM Entity WHERE created_at BETWEEN ? AND ? |
4. 배치 처리
엔티티를 대량으로 저장하거나 삭제할 때 배치 작업을 지원합니다.
메서드 | 설명 |
saveAll(Iterable<S> entities) | 여러 엔티티를 한 번에 저장합니다. |
deleteAllInBatch() | 모든 데이터를 배치 방식으로 삭제합니다. |
deleteInBatch(Iterable<T> entities) | 특정 엔티티들을 배치 방식으로 삭제합니다. |
JpaRepository의 사용 방법
1. 엔티티 클래스 작성
먼저 JPA 엔티티를 정의합니다.
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
private String name;
private String email;
// Getters, Setters, Constructors
}
2. JpaRepository 인터페이스 정의
JpaRepository를 상속받는 인터페이스를 정의합니다.
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
// 추가적으로 사용자 정의 메서드도 선언 가능
User findByEmail(String email);
List<User> findByName(String name);
}
3. Service 또는 Controller에서 사용
UserRepository를 의존성 주입 받아 사용합니다.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public User createUser(String name, String email) {
User user = new User();
user.setName(name);
user.setEmail(email);
return userRepository.save(user);
}
public List<User> getUsers() {
return userRepository.findAll();
}
public User getUserByEmail(String email) {
return userRepository.findByEmail(email);
}
}
JpaRepository의 장점
- 생산성 향상:
- 반복적인 CRUD 작업을 최소화하여 개발 속도를 빠르게 합니다.
- 자동 쿼리 생성:
- 메서드 이름을 기반으로 JPA가 자동으로 SQL을 생성합니다.
- 페이징 및 정렬 지원:
- 페이징과 정렬이 내장되어 있어 별도의 구현 없이도 쉽게 사용할 수 있습니다.
- 배치 처리:
- 대량의 데이터를 처리하는 배치 작업을 쉽게 수행할 수 있습니다.
- 높은 확장성:
- 필요에 따라 사용자 정의 메서드나 쿼리를 추가할 수 있습니다.
JpaRepository의 한계
- 복잡한 쿼리 처리 제한:
- 매우 복잡한 쿼리는 메서드 이름으로 처리하기 어렵습니다. 이 경우 JPQL 또는 네이티브 쿼리를 사용해야 합니다.
- SQL 최적화 제약:
- 자동 생성된 SQL이 항상 최적화된 형태로 실행되는 것은 아닙니다.
- 추상화로 인한 성능 이해 어려움:
- 내부적으로 JDBC를 사용하기 때문에 JPA의 작동 방식을 이해하지 못하면 성능 문제를 분석하기 어려울 수 있습니다.
- N+1 문제:
- 연관된 엔티티를 가져올 때 발생하는 성능 문제를 개발자가 직접 관리해야 합니다.
결론
- JpaRepository는 JPA 기반의 데이터베이스 작업을 간소화하기 위해 제공되는 도구로, 생산성이 매우 높습니다.
- 반복적인 CRUD 작업, 페이징 및 정렬, 배치 처리 같은 작업을 쉽게 처리할 수 있어 대부분의 JPA 기반 프로젝트에서 필수적으로 사용됩니다.
- 하지만 복잡한 쿼리나 최적화가 필요한 경우, JPQL, 네이티브 SQL, 또는 다른 기술과 함께 사용해야 합니다. 😊
[실제 사용]
💁♂️ Repository vs JpaRepository
- 기존 Repository
- @Repository 을 클래스에 붙인다.
- @Component 어노테이션을 포함하고 있어서 앱 실행시 생성 후 Bean으로 등록된다.
- 앞서배운 Repository 기본 기능만 가진 구현체가 생성된다. (DB별 예외처리 등)
- 새로운 JpaRepository
- JpaRepository<Entity,ID> 인터페이스를 인터페이스에 extends 붙인다.
- @NoRepositoryBean 된 ****상위 인터페이스들의 기능을 포함한 구현체가 프로그래밍된다. (@NoRepositoryBean = 빈생성 막음 →상속받으면 생성돼서 사용가능)
- JpaRepository (마스터 셰프): 데이터 액세스를 위한 핵심 기능의 종합적인 요리책(기능) 을 제공합니다.
- @NoRepositoryBean 인터페이스 (셰프): 각 인터페이스는 특정 데이터 액세스 방법을 제공하는 전문적인 기술 또는 레시피를 나타냅니다.
- JpaRepository 상속: 마스터 셰프의 요리책과 셰프의 전문성을 얻습니다.
- SpringDataJpa 에 의해 엔티티의 CRUD, 페이징, 정렬 기능 메소드들을 가진 빈이 등록된다. (상위 인터페이스들의 기능)
- @NoRepositoryBean 된 ****상위 인터페이스들의 기능을 포함한 구현체가 프로그래밍된다. (@NoRepositoryBean = 빈생성 막음 →상속받으면 생성돼서 사용가능)
- JpaRepository<Entity,ID> 인터페이스를 인터페이스에 extends 붙인다.
- Repository 와 JpaRepository 를 통해 얼마나 간단하게 구현하게 될지 미리 확인해볼까요?
- Repository 샘플
- EntityManager 멤버변수를 직접적으로 사용
- Repository 샘플
// UserRepository.java
@Repository
public class UserRepository {
@PersistenceContext
EntityManager entityManager;
public User insertUser(User user) {
entityManager.persist(user);
return user;
}
public User selectUser(Long id) {
return entityManager.find(User.class, id);
}
}
- JpaRepository 샘플
- EntityManager 멤버변수를 간접적으로 사용
// UserRepository.java
public interface UserRepository extends JpaRepository<User, Long> {
// 기본 메서드는 자동으로 만들어짐
}
JpaRepository 페이징 및 정렬
페이징 처리 프로세스
- PageRequest 생성:
- PageRequest.of(page, size)로 페이지 번호와 크기를 설정.
- 정렬이 필요한 경우 추가적으로 Sort를 전달.
- 메서드 호출:
- Pageable 객체를 JpaRepository 메서드에 전달.
- 반환값은 Page<T>, Slice<T>, 또는 List<T>로 설정 가능.
- 응답 처리:
- Page<T> 또는 Slice<T> 객체에서 메타정보와 데이터를 추출하여 비즈니스 로직 처리.
주요 클래스 및 메서드
Pageable 생성
- 기본 생성:
Pageable pageable = PageRequest.of(0, 10); // 페이지 번호: 0, 페이지 크기: 10
- 정렬 추가:
Pageable pageable = PageRequest.of(0, 10, Sort.by("name").ascending());
Pageable 주요 메서드
메서드 | 설명 |
getTotalPages() | 전체 페이지 수 반환. |
getTotalElements() | 전체 요소 수 반환. |
getNumber() | 현재 페이지 번호 반환. |
getSize() | 페이지 크기 반환. |
getContent() | 현재 페이지의 데이터(List<T>) 반환. |
hasNext() | 다음 페이지 존재 여부 반환. |
isFirst() | 현재 페이지가 첫 페이지인지 반환. |
Page 응답 예제
{
"content": [
{ "id": 1, "username": "User1", "address": "Korea", "age": 25 },
{ "id": 2, "username": "User2", "address": "USA", "age": 30 }
],
"pageable": {
"pageNumber": 0,
"pageSize": 2,
"sort": { "sorted": false, "unsorted": true }
},
"totalPages": 10,
"totalElements": 20,
"first": true,
"last": false
}
페이징 반환 타입 비교
반환 타입 설명 장점 단점
설명 | 장점 | 단점 | |
Page | - 전체 페이지 및 요소 수 포함.- 게시판 형태. | - 전체 정보 확인 가능.- 페이징 UI에 적합. | - 추가적인 카운트 쿼리 발생. |
Slice | - 다음 페이지 여부만 확인 가능.- "더보기" 형태. | - 카운트 쿼리 없음.- 성능 최적화. | - 전체 정보 제공 불가. |
List | - 단순 목록 반환.- 정렬 가능. | - 쿼리 단순화.- 카운트 쿼리 없음. | - 페이지 메타정보 제공 불가. |
정렬 처리
컬럼 값 기준 정렬
Sort sort = Sort.by("name").ascending();
Pageable pageable = PageRequest.of(0, 10, sort);
Page<User> page = userRepository.findAll(pageable);
다중 정렬
Sort sort = Sort.by("name").ascending().and(Sort.by("age").descending());
Pageable pageable = PageRequest.of(0, 10, sort);
Page<User> page = userRepository.findAll(pageable);
Alias를 기준으로 정렬
@Query("SELECT u.user_name AS userName FROM User u WHERE u.address = :address")
List<User> findByAddress(@Param("address") String address, Sort sort);
// 호출 시
List<User> users = userRepository.findByAddress("Korea", Sort.by("userName").ascending());
SQL 함수 기반 정렬
List<User> users = userRepository.findByUsername(
"user", JpaSort.unsafe("LENGTH(password)").descending());
실습 코드
페이징 처리
Pageable pageable = PageRequest.of(1, 5, Sort.by("name").descending());
Page<User> page = userRepository.findByAddress("Korea", pageable);
List<User> users = page.getContent();
long totalElements = page.getTotalElements();
int totalPages = page.getTotalPages();
Slice 처리
Pageable pageable = PageRequest.of(1, 5, Sort.by("name").ascending());
Slice<User> slice = userRepository.findByAddress("Korea", pageable);
List<User> users = slice.getContent();
boolean hasNext = slice.hasNext();
추천 사용 전략
- Page와 Slice:
- 게시판: Page<T>를 사용하여 전체 정보 제공.
- "더보기" 기능: Slice<T>로 성능 최적화.
- 정렬:
- 간단한 정렬: Sort 사용.
- 복잡한 정렬: SQL 함수와 Alias 활용.
- 최적화:
- 카운트 쿼리 비용이 큰 경우 Slice 또는 List로 대체.
Raw JPA 테이블 타입 매핑 기능
- JPA 애노테이션으로 설정된 매핑 정보를 기반으로 JpaRepository가 ORM 기능을 수행합니다.
JPA 애노테이션 특징 및 활용 요약
애노테이션 | 특징 | 활용 |
@Entity | - 클래스가 JPA에서 관리하는 엔티티임을 선언.- 데이터베이스 테이블과 매핑. | - JPA가 관리할 객체 정의.- 예: @Entity public class User { ... } |
@Table | - 엔티티와 매핑되는 RDB 테이블 이름을 명시적으로 설정.- 생략 시 기본값은 클래스명. | - 엔티티 이름과 테이블 이름이 다를 때 사용.- 예: @Table(name = "users") |
@Id | - 엔티티의 기본 키(Primary Key)를 지정.- 필수 설정. | - 예: @Id private Long id; |
@GeneratedValue | - 기본 키의 생성 전략 지정.- AUTO, IDENTITY, SEQUENCE, TABLE 지원. | - 데이터베이스 키 생성 방식을 설정.- 예: @GeneratedValue(strategy = GenerationType.IDENTITY) |
@Column | - 컬럼 속성을 세부적으로 지정.- 주요 속성: unique, nullable, length, columnDefinition. | - 컬럼 제약 조건 설정.- 예: @Column(name = "email", unique = true) |
@Temporal | - 날짜/시간 데이터를 DB에 적합한 타입으로 매핑.- DATE, TIME, TIMESTAMP 지원. | - java.util.Date 또는 Calendar 사용 시.- 예: @Temporal(TemporalType.TIMESTAMP) |
@Transient | - 비영속 필드를 선언.- 데이터베이스 컬럼으로 매핑되지 않음. | - DB 저장 없이 비즈니스 로직에서만 사용되는 데이터 정의.- 예: @Transient private String tempData; |
예제 코드
import jakarta.persistence.*;
import java.util.Date;
import java.util.List;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "email", unique = true, nullable = false, length = 255)
private String email;
@Temporal(TemporalType.TIMESTAMP)
private Date createdAt;
@Transient
private String tempData; // DB에 매핑되지 않음
}
JPA 필드 타입 매핑 요약
설명 | 애노테이션 | 예시 코드 | |
기본 타입 | - 문자열, 날짜, 숫자, 불리언 등 기본 데이터 타입.- 사이즈 제한, 필드명 지정 등 가능. | @Column, @Enumerated | @Column(length = 255) private String email; |
Composite Value 타입 | - 여러 필드를 하나의 값 객체로 묶어 매핑.- 복합 객체를 필드로 설정 가능. | @Embeddable, @Embedded, @AttributeOverrides | @Embeddable public class Address { private String city; } |
Collection Value 타입 | - 기본 타입 또는 Composite Value 타입의 컬렉션 매핑.- 주로 @ElementCollection 사용. | @ElementCollection | @ElementCollection private List<String> tags = new ArrayList<>(); |
주요 활용 및 예제
1. 기본 타입 매핑
// User.java
// lombok
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString
// jpa
@Entity
@Table(name = "users")
public class User {
/**
* 컬럼 - 연관관계 컬럼을 제외한 컬럼을 정의합니다.
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
private String username;
private String password;
/**
* 생성자 - 약속된 형태로만 생성가능하도록 합니다.
*/
@Builder
public User(String username, String password) {
this.username = username;
this.password = password;
}
/**
* 연관관계 - Foreign Key 값을 따로 컬럼으로 정의하지 않고 연관 관계로 정의합니다.
*/
@OneToMany
@Exclude
private Set<UserChannel> userChannel;
/**
* 연관관계 편의 메소드 - 반대쪽에는 연관관계 편의 메소드가 없도록 주의합니다.
*/
/**
* 서비스 메소드 - 외부에서 엔티티를 수정할 메소드를 정의합니다. (단일 책임을 가지도록 주의합니다.)
*/
public void updateUserName(String username) {
this.username = username;
}
public void updatePassword(String password) {
this.password = password;
}
}
2. Composite Value 타입 매핑
@Embeddable
public class Address {
private String city;
private String street;
}
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "city", column = @Column(name = "home_city")),
@AttributeOverride(name = "street", column = @Column(name = "home_street"))
})
private Address homeAddress;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "city", column = @Column(name = "company_city")),
@AttributeOverride(name = "street", column = @Column(name = "company_street"))
})
private Address companyAddress;
}
생성되는 테이블:
CREATE TABLE MEMBER (
MEMBER_ID BIGINT NOT NULL AUTO_INCREMENT,
NAME VARCHAR(255) NOT NULL,
home_city VARCHAR(255) NOT NULL,
home_street VARCHAR(255) NOT NULL,
company_city VARCHAR(255) NOT NULL,
company_street VARCHAR(255) NOT NULL,
PRIMARY KEY (MEMBER_ID)
);
3. Collection Value 타입 매핑
@Entity
public class Product {
@Id
@GeneratedValue
private Long id;
@ElementCollection
@CollectionTable(name = "product_tags", joinColumns = @JoinColumn(name = "product_id"))
@Column(name = "tag")
private List<String> tags = new ArrayList<>();
}
생성되는 테이블:
CREATE TABLE product_tags (
product_id BIGINT NOT NULL,
tag VARCHAR(255),
PRIMARY KEY (product_id, tag)
);
특징 및 활용 요약
- 기본 타입: 일반적인 데이터 저장에 사용되며, 설정 옵션으로 제약조건 지정.
- Composite Value 타입: 코드의 응집도를 높이고 복합 데이터를 쉽게 관리.
- Collection Value 타입: 여러 값 관리에 유용하지만, 대규모 데이터에서는 다대일 연관관계를 선호.
정리된 특징
- JPA는 테이블과 객체 간의 매핑을 위해 다양한 애노테이션을 제공하며, 이를 통해 엔티티를 정의하고 데이터베이스 작업을 쉽게 처리할 수 있습니다.
- 주요 애노테이션은 @Entity, @Table, @Id, @Column, @GeneratedValue 등으로, 각 애노테이션은 JPA와 데이터베이스 간의 매핑 설정을 세부적으로 제어합니다.
- 위의 요약은 JPA 애노테이션의 활용 방식과 예제를 포함하여, 데이터베이스 테이블과 객체를 매핑하는 데 필요한 핵심 개념을 다룹니다.
테이블 객체끼리 관계만들기
- JPA 애노테이션으로 설정된 매핑 정보를 기반으로 JpaRepository가 ORM 기능을 수행합니다.
Raw JPA 연관관계 매핑 기능 요약 표
애노테이션 | 설명 | 주요 속성 | 예시 코드 |
@OneToOne | - 1:1 관계를 매핑. - 단방향 및 양방향 매핑 가능. - 테이블 분리 여부를 신중히 검토. |
- mappedBy: 연관관계 주인 지정. - cascade: 영속성 전이 설정. - fetch: 기본 EAGER. |
java<br>@OneToOne @JoinColumn(name = "LOCKER_ID") private Locker locker;<br>@OneToOne(mappedBy = "locker") private Member member;<br> |
@OneToMany | - 1:N 관계를 매핑. - 단방향 사용 시 비효율적. - 양방향은 @ManyToOne과 함께 사용. |
- mappedBy: 연관관계 주인 필드 지정. - fetch: 기본 LAZY. - cascade: 영속성 전이. |
java<br>@OneToMany(mappedBy = "parent") private List<Child> childList;<br>@ManyToOne @JoinColumn(name = "parent_id") private Parent parent;<br> |
@ManyToOne | - N:1 관계를 매핑. - 가장 많이 사용하는 연관관계. - JoinColumn과 함께 사용. |
- optional: 연관 객체 필수 여부. - fetch: 기본 EAGER, 실무에서는 LAZY 권장. - cascade: 영속성 전이. |
java<br>@ManyToOne @JoinColumn(name = "parent_id") private Parent parent;<br> |
@JoinColumn | - 외래키 매핑 시 사용. - 주로 @ManyToOne과 함께 사용. |
- name: 외래키 이름. - referencedColumnName: 참조 대상 컬럼. - unique, nullable, columnDefinition. |
java<br>@JoinColumn(name = "parent_id") private Parent parent;<br> |
@ManyToMany | - N:M 관계 매핑. - 중간 매핑 테이블은 자동 생성. - 실무에서는 사용을 지양. |
- mappedBy: 양방향 매핑 시 주인 지정. - joinTable: 중간 매핑 테이블 이름 지정. |
java<br>@ManyToMany @JoinTable(name = "parent_child", joinColumns = @JoinColumn(name = "parent_id"), inverseJoinColumns = @JoinColumn(name = "child_id")) private List<Parent> parents;<br> |
주요 연관관계 예제
1. @OneToOne 단방향 매핑
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@OneToOne
@JoinColumn(name = "locker_id")
private Locker locker;
}
@Entity
public class Locker {
@Id
@GeneratedValue
private Long id;
}
2. @OneToMany와 @ManyToOne 양방향 매핑
@Entity
public class Parent {
@Id
@GeneratedValue
private Long id;
@OneToMany(mappedBy = "parent")
private List<Child> childList;
}
@Entity
public class Child {
@Id
@GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
}
3. @ManyToMany 매핑
@Entity
public class Parent {
@Id
@GeneratedValue
private Long id;
@ManyToMany(mappedBy = "parents")
private List<Child> childs;
}
@Entity
public class Child {
@Id
@GeneratedValue
private Long id;
@ManyToMany
@JoinTable(
name = "parent_child",
joinColumns = @JoinColumn(name = "parent_id"),
inverseJoinColumns = @JoinColumn(name = "child_id")
)
private List<Parent> parents;
}
실무 권장 사항
- @ManyToMany 지양: 중간 매핑 테이블을 직접 정의하여 관리.
- fetch 기본값 조정: 대부분의 관계를 LAZY로 설정.
- 단방향 vs 양방향: 필요에 따라 양방향 관계를 신중히 설정.
QueryDSL
📌 Java와 Kotlin 기반 애플리케이션에서 타입 안전(Type-safe)하고, 유연한 쿼리를 작성할 수 있도록 도와주는 SQL 쿼리 빌더 라이브러리입니다. JPA, MongoDB, SQL, JDBC, Elasticsearch 등 다양한 데이터 저장소를 지원하며, 코드 기반으로 동적 쿼리를 간결하게 작성할 수 있도록 설계되었습니다.
1. QueryDSL의 특징
특징 | 설명 |
타입 안전 | 컴파일 타임에 쿼리 오류를 감지할 수 있음. |
동적 쿼리 작성 | 복잡한 동적 쿼리를 간결하고 안전하게 작성 가능. |
코드 자동 생성 | 엔티티 클래스를 기반으로 Q클래스를 자동 생성하여 사용. |
JPQL 대체 | JPQL보다 간단하고 직관적인 문법 제공. |
다양한 데이터 저장소 지원 | JPA, SQL, MongoDB, Elasticsearch 등 다양한 환경에서 사용 가능. |
2. QueryDSL 설치 및 설정
(1) Maven 의존성 추가
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>5.0.0</version>
<scope>provided</scope>
</dependency>
(2) Gradle 의존성 추가
dependencies {
implementation "com.querydsl:querydsl-jpa:5.0.0"
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jpa"
}
(3) Q클래스 생성
- QueryDSL은 엔티티 클래스 기반으로 Q클래스를 생성합니다.
- Q클래스는 엔티티 필드에 접근할 수 있는 메타모델 클래스입니다.
Q클래스 생성 방법
- IDE의 Annotation Processor를 활성화하거나,
- 빌드 도구를 사용해 자동 생성:
./gradlew build
3. QueryDSL 기본 문법
(1) Q클래스 사용
- Q클래스는 Q + 엔티티명으로 생성되며, 엔티티의 필드에 접근할 수 있음.
QUser user = QUser.user;
(2) JPQL 대체
QueryDSL은 JPQL과 비슷한 형태로 쿼리를 작성하며, 빌더 패턴으로 동작합니다.
JPQL
String jpql = "SELECT u FROM User u WHERE u.name = :name";
List<User> users = em.createQuery(jpql, User.class)
.setParameter("name", "John")
.getResultList();
QueryDSL
QUser user = QUser.user;
JPAQuery<User> query = new JPAQuery<>(em);
List<User> users = query.select(user)
.from(user)
.where(user.name.eq("John"))
.fetch();
4. QueryDSL의 주요 메서드
메서드 | 설명 |
select() | 조회할 엔티티나 필드를 지정. |
from() | 조회 대상 테이블이나 엔티티를 지정. |
where() | 조건을 추가. (and, or, not 등 지원) |
join() | 조인 조건을 추가. (innerJoin, leftJoin 등 지원) |
groupBy() | 그룹화 조건을 추가. |
having() | 그룹화 조건에 대한 필터 추가. |
orderBy() | 정렬 조건 추가. |
fetch() | 결과를 리스트로 반환. |
fetchOne() | 단일 결과 반환. (결과가 없거나 여러 개일 경우 예외 발생) |
fetchFirst() | 첫 번째 결과 반환. (결과가 없을 경우 null) |
fetchCount() | 조회된 데이터의 개수 반환. |
fetchResults() | 페이징 정보를 포함한 결과 반환 (QueryResults 객체로 반환). |
5. QueryDSL 사용 예제
(1) 기본 조회
QUser user = QUser.user;
List<User> users = new JPAQuery<>(em)
.select(user)
.from(user)
.where(user.name.eq("John"))
.fetch();
(2) 동적 쿼리
String nameParam = "John";
Integer ageParam = null;
BooleanBuilder builder = new BooleanBuilder();
if (nameParam != null) {
builder.and(user.name.eq(nameParam));
}
if (ageParam != null) {
builder.and(user.age.eq(ageParam));
}
List<User> users = new JPAQuery<>(em)
.select(user)
.from(user)
.where(builder)
.fetch();
(3) 페이징 처리
QueryResults<User> results = new JPAQuery<>(em)
.select(user)
.from(user)
.where(user.name.contains("John"))
.offset(0) // 시작 인덱스
.limit(10) // 최대 개수
.fetchResults();
long total = results.getTotal(); // 전체 데이터 개수
List<User> users = results.getResults(); // 조회된 데이터
(4) 조인
QOrder order = QOrder.order;
QUser user = QUser.user;
List<Tuple> result = new JPAQuery<>(em)
.select(user.name, order.amount)
.from(order)
.join(order.user, user)
.where(order.amount.gt(100))
.fetch();
(5) 그룹화
List<Tuple> result = new JPAQuery<>(em)
.select(user.age, user.count())
.from(user)
.groupBy(user.age)
.having(user.count().gt(1))
.fetch();
6. QueryDSL의 장단점
장점
- 타입 안전
- 컴파일 타임에 쿼리 오류를 감지 가능.
- 동적 쿼리 작성
- 복잡한 동적 쿼리를 간결하고 효율적으로 작성 가능.
- 직관적 문법
- JPQL보다 간단하고 직관적인 빌더 패턴 문법 제공.
- 다양한 저장소 지원
- JPA뿐만 아니라 MongoDB, SQL, Elasticsearch 등에서도 사용 가능.
단점
- 초기 설정의 복잡성
- Q클래스 생성 및 Annotation Processor 설정 필요.
- 배우는 데 시간 필요
- QueryDSL 문법과 메서드를 익히는 데 추가적인 학습 시간이 필요.
- 라이브러리 의존성
- QueryDSL에 의존성이 생기므로 JPA 표준이 아닌 부분에서 잠재적 리스크.
7. QueryDSL vs JPQL 비교
QueryDSL | JPQL | |
타입 안전성 | 컴파일 타임에 오류를 감지 가능 | 런타임에 오류 발생 가능 |
동적 쿼리 작성 | 동적 쿼리 작성이 간단하고 직관적 | 동적 쿼리 작성이 복잡하고 번거로움 |
직관성 | 메서드 체인 형태로 직관적 작성 가능 | 문자열로 작성해야 하므로 복잡한 경우 가독성 저하 |
성능 최적화 | 동적 쿼리 최적화가 유리 | 쿼리 최적화는 개발자 몫 |
8. 결론
QueryDSL은 타입 안전하고 동적 쿼리를 간결하게 작성할 수 있는 강력한 도구입니다. 특히, 동적 쿼리가 빈번하거나, 복잡한 조건이 필요한 프로젝트에서 유용합니다. 다만, 초기 설정과 학습 곡선이 존재하므로, 프로젝트 요구사항에 맞게 QueryDSL을 도입하는 것이 중요합니다.
JDBC, JPA, JPQL, QueryDSL 비교
JDBC | JPA | JPQL | QueryDSL | |
정의 | Java에서 DB와 직접 상호작용하기 위한 저수준 API. | 객체와 데이터베이스 간 매핑을 자동화하는 ORM 프레임워크. | JPA에서 사용되는 객체 지향 쿼리 언어. | JPA 기반 타입 안전한 쿼리 빌더. |
작성 방식 | SQL을 직접 작성. | 메서드 호출로 DB 작업. | SQL과 유사한 문법으로 JPA 엔티티를 다룸. | Java 코드로 SQL-like 문법 작성. |
장점 | - 직접 SQL 작성으로 세밀한 제어 가능. | - 자동화로 개발 생산성 증가.- 데이터베이스 독립성 보장. | - SQL 대신 객체 지향적으로 쿼리 작성 가능.- JPA와 자연스럽게 연동 가능. | - 타입 안전성 보장.- 동적 쿼리 작성 용이.- 가독성과 유지보수성 향상. |
단점 | - 코드가 장황하고 반복적임.- 데이터베이스 종속적. | - 초기 학습 곡선 존재.- 성능 최적화를 위해 추가 설정 필요. | - 런타임 시 오류 검출.- 동적 쿼리 작성 어려움.- 가독성이 떨어짐. | - 초기 설정 복잡.- Q 클래스 생성 필요.- 러닝 커브 존재. |
사용 시기 | - 고성능 작업이나 데이터베이스 벤더 특화 기능이 필요한 경우. | - 일반적인 CRUD 작업과 객체 지향적 접근이 필요한 경우. | - 단순하고 고정된 객체 지향 쿼리가 필요한 경우. | - 복잡한 동적 쿼리나 타입 안전성을 중시하는 경우. |
실행 시점 | - SQL 문법이 컴파일 시점에 확인되지 않음(런타임 확인). | - SQL은 자동 생성됨. JPA 설정에 따라 실행.- 런타임 시 SQL 확인 가능. | - 런타임 시 JPQL 파싱 및 실행.- 실행 중 문법 오류 발견. | - SQL 문법을 컴파일 시점에 확인 가능. |
적용 환경 | - 소규모 프로젝트.- SQL 최적화가 중요한 경우. | - 대규모 시스템.- 비즈니스 로직이 복잡한 경우. | - JPA를 사용하는 환경에서 정적 쿼리를 작성할 때. | - 대규모 프로젝트에서 유지보수성 및 가독성이 중요한 경우. |
학습 난이도 | 낮음 (SQL만 익숙하면 사용 가능). | 중간 (ORM 개념 및 설정 학습 필요). | 중간 (SQL과 유사하지만 JPA 개념 필요). | 높음 (JPA와 QueryDSL 문법 학습 필요). |
주요 선택 기준
- JDBC: SQL에 익숙하며, 데이터베이스에 최적화된 세부 작업이 필요한 경우.
- JPA: 객체 지향적으로 데이터를 처리하고, 데이터베이스 독립성을 중시하는 경우.
- JPQL: 객체 지향적 쿼리가 필요하지만, 단순한 정적 쿼리 위주로 사용할 때.
- QueryDSL: 동적 쿼리와 복잡한 조건 쿼리가 빈번히 요구되며, 가독성과 타입 안전성을 중시할 때.
Auditing
📌 데이터 엔티티의 생성, 수정, 삭제 등과 같은 이벤트 발생 시점을 기록하고 관리하는 기능입니다. JPA와 Spring Data JPA는 Auditing 기능을 지원하며, 데이터 변경 이력을 자동으로 추적하고 기록할 수 있습니다. 주로 엔티티의 생성자, 수정자, 생성일, 수정일과 같은 정보를 관리하는 데 사용됩니다.
1. Auditing 주요 개념
항목 | 설명 |
CreatedDate | 엔티티가 생성된 날짜를 기록. |
LastModifiedDate | 엔티티가 마지막으로 수정된 날짜를 기록. |
CreatedBy | 엔티티를 생성한 사용자(주체)를 기록. |
LastModifiedBy | 엔티티를 마지막으로 수정한 사용자(주체)를 기록. |
자동 추적 | 엔티티 상태 변화에 따른 감사를 자동으로 기록. |
2. Spring Data JPA Auditing 설정
(1) 의존성 추가
Auditing은 Spring Data JPA가 제공하는 기능입니다. spring-boot-starter-data-jpa 의존성을 포함하면 자동으로 사용할 수 있습니다.
Maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
Gradle
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
(2) Auditing 활성화
Spring Boot에서 Auditing 기능을 활성화하려면 @EnableJpaAuditing을 설정해야 합니다.
Application 클래스
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@SpringBootApplication
@EnableJpaAuditing
public class AuditingExampleApplication {
public static void main(String[] args) {
SpringApplication.run(AuditingExampleApplication.class, args);
}
}
3. Auditing을 위한 엔티티 설정
Auditing 필드를 기록하려면 엔티티 클래스에서 **@EntityListeners(AuditingEntityListener.class)**를 추가해야 합니다.
(1) 기본 Auditing 필드
import jakarta.persistence.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@Entity
@EntityListeners(AuditingEntityListener.class) // Auditing 활성화
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@CreatedDate // 생성일 자동 기록
@Column(updatable = false) // 생성일은 수정 불가
private LocalDateTime createdDate;
@LastModifiedDate // 수정일 자동 기록
private LocalDateTime lastModifiedDate;
// Getters and Setters
}
(2) 생성자와 수정자 기록 (CreatedBy, LastModifiedBy)
Auditing에서 생성자와 수정자를 기록하려면 @CreatedBy, **@LastModifiedBy**를 사용합니다. 이를 위해 AuditorAware 구현체를 설정해야 합니다.
AuditorAware 구현
import org.springframework.data.domain.AuditorAware;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public class AuditorAwareImpl implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
// 현재 사용자를 반환. SecurityContextHolder 또는 다른 인증 방식 사용 가능
return Optional.of("Admin"); // 예시: "Admin" 사용자로 고정
}
}
Auditing 필드 추가
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.LastModifiedBy;
@Entity
@EntityListeners(AuditingEntityListener.class)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
@CreatedBy // 생성자 자동 기록
@Column(updatable = false)
private String createdBy;
@LastModifiedBy // 수정자 자동 기록
private String lastModifiedBy;
// Getters and Setters
}
4. Auditing 동작 방식
- 엔티티 생성: 엔티티가 저장되기 전 @CreatedDate와 @CreatedBy 필드가 자동으로 설정됩니다.
- 엔티티 수정: 엔티티가 수정되기 전 @LastModifiedDate와 @LastModifiedBy 필드가 자동으로 설정됩니다.
5. Auditing 활용 예제
(1) 데이터 생성
User user = new User();
user.setName("John Doe");
userRepository.save(user); // save 호출 시 createdDate와 createdBy 필드 자동 설정
(2) 데이터 수정
User user = userRepository.findById(1L).orElseThrow();
user.setName("Jane Doe");
userRepository.save(user); // save 호출 시 lastModifiedDate와 lastModifiedBy 필드 자동 업데이트
6. Auditing 주요 어노테이션
어노테이션 | 설명 |
@CreatedDate | 엔티티가 처음 생성될 때 생성일을 자동 기록. |
@LastModifiedDate | 엔티티가 수정될 때 수정일을 자동 기록. |
@CreatedBy | 엔티티가 처음 생성될 때 생성자를 자동 기록. |
@LastModifiedBy | 엔티티가 수정될 때 수정자를 자동 기록. |
@EntityListeners | 엔티티 변경 이벤트(AuditingEntityListener 등)를 수신하기 위한 리스너를 지정. |
7. Auditing 장단점
장점
- 자동화: 데이터 변경 이력을 자동으로 기록하여 개발자가 직접 관리할 필요가 없음.
- 유지보수성: 코드 중복 제거 및 코드의 간결성 향상.
- 일관성 보장: 데이터 생성 및 수정 작업에서 일관된 이력 기록.
단점
- Auditor 설정 필요: 사용자 정보를 기록하려면 AuditorAware를 별도로 구현해야 함.
- 추가 설정 필요: 기본 Spring Data JPA 설정 외에 Auditing을 활성화해야 함.
8. 주의사항
- @CreatedDate와 @LastModifiedDate는 반드시 날짜 타입 필드와 함께 사용해야 함.
- AuditorAware 구현 필수
- @CreatedBy와 @LastModifiedBy를 사용하려면 AuditorAware 인터페이스를 구현해야 합니다.
- 필드 업데이트 제한
- @CreatedDate와 @CreatedBy는 @Column(updatable = false)로 설정하여 수정되지 않도록 하는 것이 일반적입니다.
- 트랜잭션 내에서 동작
- Auditing은 영속성 컨텍스트와 연관되므로 트랜잭션 내에서만 작동합니다.
9. 결론
Spring Data JPA의 Auditing 기능은 데이터의 생성, 수정, 사용자 정보를 자동으로 기록하고 관리할 수 있는 강력한 도구입니다. 이를 활용하면 변경 이력 관리, 보안 감사 및 데이터 추적과 같은 기능을 효율적으로 구현할 수 있습니다.
Dynamic Insert/Update
📌 JPA 또는 Hibernate에서 엔티티의 변경이 발생할 때, 필요한 필드만으로 SQL을 생성하여 효율적으로 데이터베이스에 반영하는 기능입니다. 불필요한 컬럼을 제외하고 동적으로 SQL을 생성하므로 성능 최적화에 도움을 줄 수 있습니다.
1. Dynamic Insert
기본 동작
- Dynamic Insert는 엔티티를 데이터베이스에 저장할 때, NULL이 아닌 값이 있는 필드만 INSERT SQL에 포함합니다.
- 이를 통해, 기본값을 데이터베이스에서 관리하도록 할 수 있습니다.
예제
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
private LocalDateTime createdDate;
// Getters and Setters
}
엔티티 저장 코드
User user = new User();
user.setName("John"); // email과 createdDate는 NULL 상태
userRepository.save(user);
SQL (Dynamic Insert 활성화 시)
INSERT INTO user (name) VALUES ('John');
-- email, createdDate는 제외
SQL (Dynamic Insert 비활성화 시)
INSERT INTO user (name, email, created_date) VALUES ('John', NULL, NULL);
2. Dynamic Update
기본 동작
- Dynamic Update는 엔티티를 수정할 때, 변경된 필드만 UPDATE SQL에 포함합니다.
- 변경되지 않은 필드는 SQL에서 제외되므로, 불필요한 UPDATE 작업을 방지할 수 있습니다.
예제
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
// Getters and Setters
}
엔티티 수정 코드
User user = userRepository.findById(1L).orElseThrow();
user.setName("Updated Name"); // email은 변경되지 않음
userRepository.save(user);
SQL (Dynamic Update 활성화 시)
UPDATE user SET name = 'Updated Name' WHERE id = 1;
-- email은 제외
SQL (Dynamic Update 비활성화 시)
UPDATE user SET name = 'Updated Name', email = NULL WHERE id = 1;
3. Dynamic Insert/Update 활성화 방법
(1) Hibernate 어노테이션 사용
Hibernate에서는 **@DynamicInsert**와 @DynamicUpdate 어노테이션을 제공하여 설정할 수 있습니다.
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
@Entity
@DynamicInsert // INSERT 시 NULL 필드 제외
@DynamicUpdate // UPDATE 시 변경된 필드만 포함
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
// Getters and Setters
}
(2) 기본 Hibernate 설정
Hibernate를 사용하는 프로젝트에서 기본 설정으로 적용할 수도 있습니다.
하지만, 엔티티 단위로 제어하는 것이 일반적입니다.
4. Dynamic Insert/Update 장단점
장점
- SQL 효율성 증가
- NULL 값이나 변경되지 않은 필드를 제외하여 쿼리를 최소화.
- 데이터베이스에서 기본값을 사용할 수 있음.
- 성능 최적화
- 불필요한 작업 감소로 인해 성능 개선.
- 대량 데이터를 처리할 때 효과적.
- 코드 간결성
- NULL 처리를 수동으로 하지 않아도 됨.
단점
- 쿼리 복잡성 증가
- INSERT/UPDATE 쿼리가 매번 동적으로 생성되므로, 디버깅이 어려울 수 있음.
- 미리 정의된 기본값 의존성
- 데이터베이스의 기본값이 변경되거나 의도와 다를 경우 데이터 무결성 문제가 발생할 가능성.
- 데이터베이스 성능
- 동적 SQL 생성으로 인해 데이터베이스 캐싱 효율성이 떨어질 수 있음.
5. Dynamic Insert/Update를 사용해야 할 경우
- NULL 값이 많고 기본값을 활용해야 하는 경우
- 데이터베이스의 기본값 설정을 적극 활용하는 설계에서 유리.
- 대량의 엔티티를 처리할 때 성능이 중요한 경우
- INSERT 또는 UPDATE 쿼리에서 불필요한 필드를 줄여 성능을 최적화.
- 선택적으로 필드를 업데이트해야 하는 경우
- 예: 특정 필드만 수정해야 하는 REST API 설계.
6. Dynamic Insert/Update를 사용하지 않을 경우
- 데이터베이스에서 기본값 관리가 필요하지 않은 경우
- 애플리케이션 레벨에서 NULL 값을 명시적으로 설정하는 경우에는 필요 없음.
- SQL 디버깅과 유지보수성 중시
- 동적 SQL 생성이 디버깅을 어렵게 만들 수 있으므로, 간단한 INSERT/UPDATE 구조를 선호하는 경우.
- 캐싱 최적화가 중요한 경우
- 동적 쿼리는 데이터베이스에서 쿼리 캐싱이 어려울 수 있으므로, 정적인 쿼리를 선호하는 경우.
7. 결론
Dynamic Insert와 Dynamic Update는 필요한 필드만으로 동적 SQL을 생성하여 성능을 최적화하고, 데이터베이스의 기본값을 효과적으로 활용할 수 있게 해줍니다. 특히 대량 데이터 처리나 NULL 값이 많은 필드 처리에서 유용하지만, 디버깅 및 유지보수 복잡성을 고려하여 사용해야 합니다.
'DB > DBMS' 카테고리의 다른 글
[DBMS] SQL (0) | 2025.01.24 |
---|---|
[DBMS] H2 (가벼운 오픈소스 관계형 DB 관리 서비스) (0) | 2025.01.23 |
[DB] DB (Database) (1) | 2024.12.21 |