
자바, 스프링에서 JPA를 사용해서 데이터베이스에 접근할 때, 그 구조는 이렇게 생겼다.
JDBC
가장 DB에 가까운 계층
데이터베이스는 MySQL, 오라클, postgre 등.. 여러 데이터베이스들이 있는데, 문제는 이 데이터베이스의 종류마다 SQL을 날리고, 응답을 받아오는 방식들이 천차만별이다. 만약 MySQL을 쓰다가 모종의 사유로 오라클DB로 DB를 교체하면 코드수정이 많아질 것이다.
따라서 데이터베이스의 종류에 상관없이 하나의 표준화된 인터페이스가 필요하다는 의견이 생겼고, 그결과 도입된 인터페이스가 JDBC.


MySQL이라면 MySQL 드라이버, 오라클DB라면 오라클 드라이버 등, JDBC의 구현체를 통해서 DB마다 연결방법, SQL을 보내는 방법, 결과를 받아오는 방법들이 다르지만 하나의 인터페이스만으로 여러 종류의 데이터베이스를 사용할 수 있었다.
package Repository;
import Domain.Member;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class MemberRepository {
private final Connection con;
public MemberRepository(Connection con) {
this.con = con;
}
public void save(Member member) throws IOException {
try {
con.setAutoCommit(false);
saveMember(member);
con.commit();
} catch (SQLException e) {
try {
con.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
e.printStackTrace();
} catch (IOException e){
try {
con.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
e.printStackTrace();
} finally {
try {
con.setAutoCommit(true);
} catch (SQLException ex) {
ex.printStackTrace();
}
}
}
public boolean existsByUserid(String userid) throws IOException {
String sql = "SELECT * FROM members WHERE USERID = ?";
try (PreparedStatement preparedStatement = con.prepareStatement(sql)){
preparedStatement.setString(1, userid);
try (ResultSet resultSet = preparedStatement.executeQuery()){
if (resultSet.next()) {
return true;
}
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
return false;
}
}
JDBC의 단점으로는 비즈니스 로직에 불필요한 DB 커넥션과 DB 접근 로직들을 적어야하는 경우가 많다. 위는 JDBC를 사용한 예시이며, DB와의 커넥션을 담당하는 Connection 객체를 통해서 DB 접근 및 수정 로직이 이루어진다. 딱 보기에도 다소 복잡해보이는 구조를 가지고있다.
Hibernate와 JPA
따라서 반복적이며 불필요한 SQL 작성이 많아지다보니 SQL의 작성은 줄이고 DB 접근기능만 남겨두려는 의도로 생긴 것이 ORM.
그 전에, 객체지향 프로그래밍은 객체를 기반으로, SQL은 테이블을 기반으로 동작하는데, ORM은 객체와 테이블을 서로 연결시켜주기 위해 등장했다. 기존에는 자바 객체를 테이블 형태로 바꾼 후에 DB에 저장해야했지만, 이제는 객체를 바로 저장해도 ORM 덕분에 테이블 형태로 전환되어 DB에 저장된다.
하지만 ORM은 Hibernate, EclipseLink, OpenJPA 등의 여러 종류가 있는데, 이 때문에 또 ORM 간 호환성 문제가 대두되었다. 역시 ORM마다 동작방식이 다르기 때문. 따라서 JPA가 등장해 ORM에 종류에 관계없이 공통적인 인터페이스를 지원하기 시작했다.
EntityManager
JDBC에서는 JDBC Driver가 DB에 접근하는 주체였다. JPA에서는 EntityManager가 DB에 접근하는 주체가 된다.

EntityManager는 영속성 컨텍스트를 관리한다.
영속선 컨텍스트란 애플리케이션에서 엔티티를 DB에 저장하기 전 잠시 저장해두는 임시저장소의 개념이다. 만약 EntityManager를 사용해 DB에 엔티티를 저장, 수정, 삭제 등의 역할을 수행하면 먼저 영속성 컨텍스트에 변경사항이 반영되고, EntityManager가 flush()를 진행하면 그제야 DB에 바뀌는 식으로 동작한다.
그래서 영속성 컨텍스트는 DB까지 조회할 필요 없이 하나의 1차 캐시로 사용할 수도 있다. DB로의 조회가 필요할 경우 먼저 영속성 컨텍스트를 뒤져보고, 만약 영속성 컨텍스트에 엔티티가 없다면 DB에서 값을 가져오는 형태로.
for (int idx = 0; idx < 1000000; idx++) {
entityManager.persist(member);
}
그리고 매번 DB에 접근하다보면 성능에 문제가 생길 수도 있는데, EntityManager가 DB접근 메서드를 불러올 때마다 DB에 쿼리를 날리는게 아니라 트랜잭션의 커밋이나 flush 때에만 DB에 접근하므로 불필요한 DB접근을 줄일 수 있다. 이를 쓰기 지연이라고하며, flush 전까지 명령어들은 쓰기 지연 저장소에서 대기한다.
엔티티의 상태
엔티티는 크게 4가지 상태가 존재한다.
1. 영속
EntityManagerFactory emf = Persistence.createManagerFactory("example");
EntityManager em = emf.createEntityManager();
em.persist(member);
엔티티가 영속성 컨텍스트에 등록된 상태를 뜻한다. flush()를하면 DB에 저장된다.
영속 상태에 있는 엔티티의 중요한 특징은 바로 식별자가 반드시 존재해야한다는 것이다.
2. 준영속
em.detach(member);
em.clear();
em.close();
객체 또는 엔티티가 영속성 컨텍스트에 등록되었으나, 모종의 사유로 영속성 컨텍스트에서 떨어져나온 경우에 해당한다. 이 모종의 사유에는 영속성 컨텍스트에서 엔티티를 제외하거나(detach), 영속성 컨텍스트에 있는 내용을 비우거나(clear), 영속성 컨텍스트가 닫혀버리는 경우(close)가 이에 해당한다.
3. 비영속
Member member = new Member();
객체가 생성되었으나 영속성 컨텍스트가 관리하고 있지는 않은 상태다.

준영속과 비영속의 상태는 영속상태가 된 적이 있는가로 구분한다. 만약 준영속이라면 식별자가 반드시 존재해야한다. 이외에는 다른 점이 없다.
4. 삭제
em.remove(member);
엔티티가 영속성 컨텍스트에서도 사라지고, DB에서도 제거된다.
준영속과 삭제의 차이는 detach는 영속성 컨텍스트에서만 제외되어 DB에는 남아있지만 삭제 시에는 DB에서까지 삭제된다.
하나의 트랜잭션이 끝나면 EntityManager는 영속성 컨텍스트의 변경사항을 DB에 반영하고 영속성 컨텍스트를 비우는데, 이 때 영속성 컨텍스트에 있던 엔티티는 영속성 컨텍스트에서는 빠지지만 DB에는 존재한다. 반대로 삭제 시에는 영속성 컨텍스트에서도 사라지고, DB에서도 사라진다.
커밋과 flush
플러시가 일어나면 영속성 컨텍스트의 변경사항을 그대로 DB에 반영하되, 영속성 컨텍스트를 비우지 않는다. 즉, 쓰기 지연 저장소에 있던 DB 명령들이 DB로 날아가기만하며, 영속성 컨텍스트는 그대로 유지된다.
반면 트랜잭션을 커밋하면 변경사항을 즉시 DB에 반영하고, 영속성 컨텍스트도 비운다. 그래서 DB 쪽에 문제가 발생했을 때 flush만 사용했다면 롤백이 가능한 반면, 커밋까지 해버렸다면 롤백이 불가능하다.
스프링 JPA
기본 JPA는 EntityManager를 사용하거나, 직접 JPQL 쿼리를 작성해야하는 경우도 있었다. 따라서 스프링 JPA가 등장해 SQL이나 JQPL을 자동으로 작성해주고, EntityManager를 사용하지 않고도 그냥 레포지토리 계층에서 엔티티를 다룰 수 있다는 장점이 생겼다.
package com.ceos23.spring_cgv_23rd.User;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String username;
private String password;
public User() {}
public User(String username, String password){
this.username = username;
this.password = password;
}
}
DB에 저장되는 엔티티 객체
스프링 JPA는 JPA 위에서 동작하므로 엔티티는 식별자가 반드시 필요하며, 이는 @Id 어노테이션을 통해서 명시해준다.
@GenteratedValue 어노테이션은 Id를 어떻게 관리할지를 다루며, AUTO (Hibernate에게 전적으로 위임), IDENTITY(DB에게 전적으로 위임, 주로 1부터 하나씩 증가함), SEQUENCE (특정한 DB 시퀀스에 따라서 값 증가), TABLE (특정한 DB 테이블에 따라서 값 증가) 등이 있다.
package com.ceos23.spring_cgv_23rd.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
}
그리고 레포지토리는 JpaRepository를 상속한다.
나는 예전에는 CrudRepository를 확장해서 사용했는데, CrudRepository에 정렬, 페이징 등 JPA 기능을 추가한 인터페이스가 JpaRepository라고한다. JPA를 사용하면 JpaRepository를 사용하는게 더 나은듯.
제너릭은 <T, ID>로 이루어져있는데, T값은 저장할 엔티티의 타입, ID는 엔티티의 ID 컬럼의 타입이다.

마지막으로 스프링 JPA를 사용하다보면, 내가 따로 메서드를 작성하지 않아도 레포지토리 계층에서 선언만하면 DB 작업이 수행되는 일이 있다.
public void test(){
System.out.println(memberRepository.getClass());
}

이는 레포지토리 인터페이스를 호출하면 레포지토리의 구현체가 바로 호출되는게 아니라 프록시 객체가 대신 호출된다, 그래서 메서드 이름을 토대로 SQL문을 적당히 만들어서 쿼리를 날리는 형태로 동작한다.
이번에는 이렇게 DB와 JPA의 기초, 그리고 영속성에 대해서 간단히 알아봤는데, 이 다음으로는 JPA에서의 연관관계와 join, N + 1 문제에 대해 다뤄볼까한다. 여기는 약간 사전준비? 암튼 글 올리면 링크 첨부하겠음
'CS > 데이터베이스' 카테고리의 다른 글
| [DB] 정규화 (1) | 2026.05.14 |
|---|---|
| [DB] 기초데이터베이스 개념 간단정리 (0) | 2026.05.13 |
| [DB] DB 설계 및 구축 독서 #1 - 데이터모델링과 데이터모델링 3요소, 엔티티, 속성, 관계 (0) | 2026.01.14 |
