[Spring] JPA 기초 — SQL 없이 DB를 다루는 방법

Raha·2026년 3월 21일

Spring

목록 보기
4/7

들어가며

지난 글에서 Spring MVC의 흐름을 살펴봤다. HTTP 요청이 DispatcherServlet에 들어와서 Controller → Service → Repository 계층을 거쳐 응답으로 나가는 구조였다.

그런데 Repository 계층에서 실제로 DB와 어떻게 통신하는지는 다루지 않았다. MyBatis를 써본 사람이라면 XML에 SQL을 직접 작성하는 방식에 익숙할 것이다.

이번 글에서는 그 방식과 완전히 다른 접근법인 JPA를 다룬다. 읽으면서 이런 질문들을 생각해보자.

  • 왜 MyBatis 대신 JPA를 쓰는가?
  • SQL을 직접 안 쓰면 JPA는 어떻게 DB를 다루는가?
  • 영속성 컨텍스트가 뭐고, 왜 중요한가?

1. MyBatis의 현실

MyBatis로 개발하다 보면 반복되는 패턴이 있다.

<!-- UserMapper.xml -->
<select id="findById" resultType="User">
    SELECT id, name, email FROM users WHERE id = #{id}
</select>

<insert id="save">
    INSERT INTO users (name, email) VALUES (#{name}, #{email})
</insert>

<update id="update">
    UPDATE users SET name = #{name}, email = #{email} WHERE id = #{id}
</update>

<delete id="deleteById">
    DELETE FROM users WHERE id = #{id}
</delete>

users 테이블이든 orders 테이블이든 products 테이블이든, CRUD 쿼리의 구조는 거의 똑같다. 테이블 이름과 컬럼만 다를 뿐이다.

이 반복을 없앨 수 없을까? 그 질문에서 JPA가 시작된다.


2. ORM이란 무엇인가

JPA는 ORM(Object-Relational Mapping) 기술이다.

Object(Java 객체) ↔ Relational(관계형 DB 테이블) 을 Mapping(연결)한다는 뜻이다.

핵심 아이디어는 간단하다. Java 클래스를 보면 DB 테이블에 필요한 정보가 다 있다.

public class User {
    private Long id;      // 컬럼 이름 + 타입
    private String name;  // 컬럼 이름 + 타입
    private String email; // 컬럼 이름 + 타입
}
  • 클래스 이름 → 테이블 이름
  • 필드 이름/타입 → 컬럼 이름/타입

JPA는 이 정보를 읽어서 SQL을 자동으로 만들어낸다.


3. Entity, Repository, Query Method

@Entity — 이 클래스가 DB 테이블이다

모든 Java 클래스가 테이블은 아니다. JPA에게 "이 클래스를 테이블로 취급해라"고 알려주는 어노테이션이 @Entity다.

@Entity
public class User {
    @Id // 이 필드가 PK다
    @GeneratedValue(strategy = GenerationType.IDENTITY) // DB가 자동으로 PK 생성
    private Long id;
    private String name;
    private String email;
}

@Id로 어떤 필드가 Primary Key인지 명시한다. PK 자동 생성 전략은 @GeneratedValue로 지정할 수 있다.

JpaRepository — 기본 CRUD는 공짜다

MyBatis에서는 Mapper 인터페이스에 XML 쿼리를 연결했다. JPA에서는 JpaRepository를 상속받는 것으로 끝이다.

public interface UserRepository extends JpaRepository<User, Long> {
    // 아무것도 안 써도 아래 메서드들이 전부 동작한다
}

JpaRepository<User, Long> 에서

  • User → 어떤 Entity를 다룰지
  • Long → 그 Entity의 PK 타입

이 두 가지 정보만 넘겨주면, save, findById, findAll, deleteById 등이 자동으로 제공된다.

구현 클래스는 필요 없다. Spring이 앱 시작 시점에 SimpleJpaRepository라는 구현체를 자동으로 만들어서 Bean으로 등록해준다.

Query Method — 조건 쿼리도 메서드 이름으로

기본 CRUD 외에 조건이 필요한 쿼리는 메서드 이름으로 선언한다.

public interface UserRepository extends JpaRepository<User, Long> {
    List<User> findByName(String name);             // WHERE name = ?
    List<User> findByNameAndEmail(String name, String email); // WHERE name = ? AND email = ?
}

XML도 없고, SQL도 없다. 메서드 이름 규칙에 맞게 선언하면 JPA가 알아서 쿼리를 만들어준다.

MyBatis vs JPA 비교

항목MyBatisJPA
SQL 작성개발자가 직접 XML에 작성JPA가 자동 생성
CRUD4개 쿼리 직접 작성JpaRepository 상속으로 자동 제공
조건 쿼리XML에 WHERE절 직접 작성메서드 이름으로 선언
결과 매핑resultMap 설정 필요Entity 클래스로 자동 매핑

4. 영속성 컨텍스트 — DB와 앱 사이의 책상

JPA에서 가장 중요한 개념이다.

영속성 컨텍스트는 DB와 Java 애플리케이션 사이에 있는 임시 저장 공간이다. 이걸 관리하는 객체가 EntityManager다.

도서관(DB) ←→ 책상(EntityManager/영속성 컨텍스트) ←→ 나(애플리케이션)

도서관에서 책을 빌려오면 책상 위에 올려두는 것처럼, DB에서 Entity를 가져오면 영속성 컨텍스트에 보관된다. 책상 위에 있는 동안은 도서관까지 안 가도 바로 꺼내볼 수 있다.

Spring Data JPA를 쓰면 EntityManager를 직접 다룰 일은 거의 없다. JpaRepository가 내부적으로 대신 처리해준다. 하지만 영속성 컨텍스트가 어떻게 동작하는지는 알아야 한다.

Dirty Checking — 변경 감지

영속성 컨텍스트의 핵심 기능이다.

Entity를 가져올 때 원본 상태를 스냅샷으로 찍어둔다. 트랜잭션이 끝날 때 현재 상태와 스냅샷을 비교해서 달라진 부분이 있으면 자동으로 UPDATE 쿼리를 실행한다.

// Before: MyBatis
userMapper.update(user); // UPDATE 쿼리를 직접 호출해야 한다

// After: JPA
User user = userRepository.findById(1L).get();
user.setName("김철수"); // 값만 바꾸면 끝
// save() 호출 없음. 트랜잭션 종료 시 자동으로 UPDATE 실행

5. @Transactional — 전부 성공하거나, 전부 실패하거나

계좌 이체를 생각해보자.

  1. A 계좌에서 10만원 출금
  2. B 계좌에 10만원 입금

1번은 성공했는데 2번에서 오류가 나면 어떻게 돼야 할까? 1번 출금도 취소되어야 한다. 이게 트랜잭션이다.

  • 전부 성공 → Commit (DB에 영구 반영)
  • 하나라도 실패 → Rollback (전부 없던 일로)

Spring에서는 @Transactional 어노테이션으로 선언한다.

@Service
public class UserService {

    @Transactional
    public void transfer(Long fromId, Long toId, int amount) {
        // 메서드 시작 → 트랜잭션 시작
        // 중간에 예외 발생 → 자동 Rollback
        // 정상 종료 → Commit
    }
}

@Transactional과 영속성 컨텍스트를 합치면 전체 흐름이 이렇다.

@Transactional 시작
    ↓
findById() → DB에서 Entity 조회 + 스냅샷 저장
    ↓
Entity 값 수정 (save() 호출 없음)
    ↓
@Transactional 종료
    ↓
Dirty Checking → 스냅샷과 비교 → UPDATE 쿼리 자동 실행
    ↓
Commit → DB에 반영

예외가 발생하면 Commit까지 가지 않고 Rollback으로 처음 상태로 돌아간다.


마치며

JPA의 핵심은 SQL을 직접 쓰는 대신 Java 클래스와 DB 테이블을 대응시키는 것이다. @Entity로 테이블을 선언하고, JpaRepository로 CRUD를 얻고, 영속성 컨텍스트가 변경을 감지해서 자동으로 쿼리를 실행한다.

profile
Backend Developer | Aspiring Full-Stack Enthusiast

0개의 댓글