JPA 처음 시작하는 개발자, 예제 코드로 바로 익히는 법
2026. 7. 29. 22:07ㆍDev & Tech/Java
반응형
JPA 처음 시작하는 개발자, 예제 코드로 바로 익히는 법
JPA는 개념 설명만 읽어서는 좀처럼 손에 잡히지 않는 기술입니다. 엔티티, 영속성 컨텍스트, 더티 체킹 같은 용어를 아무리 외워도, 정작 코드를 짜보지 않으면 감이 오지 않죠. 이번 글에서는 이론은 최소화하고, Oracle 데이터베이스를 기준으로 실제 조회·수정·저장이 동작하는 예제 코드와 함께, 그 과정에서 데이터가 실제로 어떤 경로로 옮겨 다니고 처리되는지까지 순서대로 짚어보겠습니다.
📎 이 글에서 다루는 예제 코드는 실행 가능한 전체 프로젝트로 만들어 ZIP 파일로 첨부해두었습니다. 글만 읽어도 흐름은 이해할 수 있지만, 직접 실행해보고 싶으신 분은 하단(첨부파일)에서 내려받아 바로 열어보시면 됩니다.
목차
1. JPA란 무엇이고 왜 배워야 할까
2. Oracle 연동 환경 설정하기
3. 예제로 배우는 조회·수정·저장과 데이터 흐름
4. 마무리

#1. JPA란 무엇이고 왜 배워야 할까
1) SQL을 직접 쓰지 않고 객체로 다루는 방식
JPA(Java Persistence API)는 자바 객체와 관계형 데이터베이스 테이블을 매핑해주는 표준 인터페이스입니다. 기존에는 테이블 하나를 다루려면 INSERT, UPDATE, SELECT 쿼리를 직접 작성해야 했지만, JPA를 쓰면 엔티티 객체를 저장하거나 값을 바꾸는 것만으로 SQL이 자동으로 생성·실행됩니다. 실무에서는 JPA 구현체로 Hibernate를 주로 사용하고, Spring Boot 환경이라면 Spring Data JPA를 함께 써서 반복 코드를 더 줄이는 것이 일반적입니다.
· · · · ·
2) 꼭 알아야 할 핵심 개념 3가지
① 엔티티(Entity) - 테이블과 매핑되는 자바 클래스
② 영속성 컨텍스트(Persistence Context) - 엔티티를 관리하는 1차 캐시 공간
③ 더티 체킹(Dirty Checking) - 영속 상태인 엔티티의 필드 값이 바뀌면 트랜잭션 종료 시점에 자동으로 UPDATE 쿼리를 실행하는 동작
② 영속성 컨텍스트(Persistence Context) - 엔티티를 관리하는 1차 캐시 공간
③ 더티 체킹(Dirty Checking) - 영속 상태인 엔티티의 필드 값이 바뀌면 트랜잭션 종료 시점에 자동으로 UPDATE 쿼리를 실행하는 동작
이 세 가지만 이해하고 있어도, 뒤에 나오는 예제 코드에서 데이터가 왜 그렇게 옮겨 다니는지 자연스럽게 이해할 수 있습니다.
#2. Oracle 연동 환경 설정하기
1) 의존성 추가 (build.gradle)
Spring Boot 프로젝트 기준으로, JPA와 Oracle JDBC 드라이버 의존성을 아래와 같이 추가합니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// Oracle 공식 JDBC 드라이버
runtimeOnly 'com.oracle.database.jdbc:ojdbc11'
}
· · · · ·
2) 데이터소스 설정 (application.yml)
Oracle 접속 정보와 Hibernate 관련 설정은 application.yml에 아래처럼 작성합니다. ddl-auto는 운영 환경에서는 절대 update나 create로 두지 말고 validate나 none으로 설정해야 합니다.
spring:
datasource:
url: jdbc:oracle:thin:@localhost:1521:XE
username: myuser
password: mypassword
driver-class-name: oracle.jdbc.OracleDriver
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
dialect: org.hibernate.dialect.OracleDialect
format_sql: true
show-sql: true
#3. 예제로 배우는 조회·수정·저장과 데이터 흐름
1) 엔티티 클래스 작성
직원 정보를 담는 EMPLOYEE 테이블이 있다고 가정하고, 이를 매핑하는 엔티티를 작성해보겠습니다.
@Entity
@Table(name = "EMPLOYEE")
@Getter
@NoArgsConstructor
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "emp_seq")
@SequenceGenerator(name = "emp_seq", sequenceName = "EMP_SEQ", allocationSize = 1)
@Column(name = "EMP_ID")
private Long empId;
@Column(name = "EMP_NAME", nullable = false)
private String empName;
@Column(name = "DEPT_NAME")
private String deptName;
@Column(name = "SALARY")
private Long salary;
public Employee(String empName, String deptName, Long salary) {
this.empName = empName;
this.deptName = deptName;
this.salary = salary;
}
// 더티 체킹을 위한 값 변경 메서드
public void updateSalary(Long salary) {
this.salary = salary;
}
}
· · · · ·
2) Repository와 Service 작성
Spring Data JPA는 인터페이스만 정의해도 기본적인 CRUD 메서드를 자동으로 만들어줍니다. 여기에 서비스 계층을 하나 더 만들어 조회·수정·저장 로직을 정리해보겠습니다.
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
List<Employee> findByDeptName(String deptName);
}
@Service
@RequiredArgsConstructor
public class EmployeeService {
private final EmployeeRepository employeeRepository;
// 1. 저장 (INSERT)
@Transactional
public Long saveEmployee(String name, String dept, Long salary) {
Employee employee = new Employee(name, dept, salary);
Employee saved = employeeRepository.save(employee);
return saved.getEmpId();
}
// 2. 조회 (SELECT)
public Employee findEmployee(Long empId) {
return employeeRepository.findById(empId)
.orElseThrow(() -> new IllegalArgumentException("해당 직원이 없습니다. id=" + empId));
}
// 3. 수정 (UPDATE, 더티 체킹 방식)
@Transactional
public void updateSalary(Long empId, Long newSalary) {
Employee employee = employeeRepository.findById(empId)
.orElseThrow(() -> new IllegalArgumentException("해당 직원이 없습니다. id=" + empId));
employee.updateSalary(newSalary);
}
}
· · · · ·
3) 저장할 때, 데이터는 어떻게 흘러갈까
saveEmployee()를 호출하면 아래와 같은 순서로 데이터가 이동합니다. Controller에서 넘어온 값이 자바 객체(Employee)로 조립되고, 그 객체가 그대로 DB의 한 행(row)으로 옮겨간다는 흐름만 잡으면 됩니다.
① Controller가 요청값을 받아 Service의 saveEmployee() 호출
② Service에서 new Employee(...)로 순수 자바 객체(비영속 상태) 생성
③ employeeRepository.save(employee) 호출 → EntityManager.persist() 실행
④ 엔티티가 영속성 컨텍스트(1차 캐시)에 등록되고, PK는 시퀀스(EMP_SEQ)에서 채번
⑤ 트랜잭션이 커밋되는 시점에 실제 INSERT 쿼리가 Oracle로 전달되어 실행
⑥ 생성된 empId가 Service → Controller → 클라이언트 응답까지 반환
② Service에서 new Employee(...)로 순수 자바 객체(비영속 상태) 생성
③ employeeRepository.save(employee) 호출 → EntityManager.persist() 실행
④ 엔티티가 영속성 컨텍스트(1차 캐시)에 등록되고, PK는 시퀀스(EMP_SEQ)에서 채번
⑤ 트랜잭션이 커밋되는 시점에 실제 INSERT 쿼리가 Oracle로 전달되어 실행
⑥ 생성된 empId가 Service → Controller → 클라이언트 응답까지 반환
여기서 중요한 포인트는, persist() 호출 시점과 실제 INSERT 쿼리가 나가는 시점이 다를 수 있다는 것입니다. Hibernate는 트랜잭션 커밋 직전이나 조회 쿼리가 필요한 순간에 모아둔 변경사항을 한 번에 DB로 반영(flush)합니다.
· · · · ·
4) 조회할 때, 데이터는 어떻게 흘러갈까
findEmployee()는 겉보기엔 단순한 SELECT처럼 보이지만, 내부적으로는 1차 캐시를 먼저 확인하는 단계가 숨어 있습니다.
① Controller가 empId를 받아 Service의 findEmployee() 호출
② employeeRepository.findById(empId) 호출 → EntityManager.find() 실행
③ 같은 트랜잭션 안에서 이미 조회한 적 있는 empId라면, DB에 다시 쿼리를 보내지 않고 영속성 컨텍스트(1차 캐시)에 있는 객체를 그대로 반환
④ 캐시에 없다면 Oracle로 SELECT 쿼리 실행 후, 결과를 Employee 객체로 매핑해 캐시에 저장
⑤ 완성된 Employee 객체가 Service → Controller까지 그대로 전달되고, Controller에서 DTO로 변환해 응답
② employeeRepository.findById(empId) 호출 → EntityManager.find() 실행
③ 같은 트랜잭션 안에서 이미 조회한 적 있는 empId라면, DB에 다시 쿼리를 보내지 않고 영속성 컨텍스트(1차 캐시)에 있는 객체를 그대로 반환
④ 캐시에 없다면 Oracle로 SELECT 쿼리 실행 후, 결과를 Employee 객체로 매핑해 캐시에 저장
⑤ 완성된 Employee 객체가 Service → Controller까지 그대로 전달되고, Controller에서 DTO로 변환해 응답
즉 조회 한 번에 "DB까지 갔다 오는 경우"와 "캐시에서 바로 꺼내오는 경우"가 나뉘는데, 이 차이를 이해해두면 나중에 조회 성능을 점검할 때도 도움이 됩니다.
· · · · ·
5) 수정할 때, 데이터는 어떻게 흘러갈까
updateSalary()가 이번 글에서 가장 헷갈리기 쉬운 부분입니다. 코드 어디에도 UPDATE나 save() 호출이 보이지 않는데, 실제로는 값이 바뀝니다. 그 이유를 데이터 흐름으로 풀어보면 이렇습니다.
① @Transactional로 트랜잭션 시작, findById()로 엔티티 조회 → 영속 상태가 되면서 Hibernate가 이 시점의 값(스냅샷)을 별도로 저장
② employee.updateSalary(newSalary) 호출 → 자바 객체의 필드 값만 변경 (이 시점까지는 DB에 아무 일도 일어나지 않음)
③ 메서드 종료, 트랜잭션 커밋 직전 Hibernate가 ①의 스냅샷과 현재 엔티티 상태를 비교(더티 체킹)
④ salary 필드가 달라진 것을 감지하면, 변경된 컬럼만 담아 UPDATE 쿼리를 자동으로 생성
⑤ 생성된 UPDATE 쿼리가 Oracle로 전달되어 실행, 트랜잭션 커밋 완료
② employee.updateSalary(newSalary) 호출 → 자바 객체의 필드 값만 변경 (이 시점까지는 DB에 아무 일도 일어나지 않음)
③ 메서드 종료, 트랜잭션 커밋 직전 Hibernate가 ①의 스냅샷과 현재 엔티티 상태를 비교(더티 체킹)
④ salary 필드가 달라진 것을 감지하면, 변경된 컬럼만 담아 UPDATE 쿼리를 자동으로 생성
⑤ 생성된 UPDATE 쿼리가 Oracle로 전달되어 실행, 트랜잭션 커밋 완료
정리하면 "값을 바꾸는 코드"와 "실제 UPDATE 쿼리가 나가는 시점"이 분리되어 있고, 그 사이를 이어주는 것이 스냅샷 비교(더티 체킹)라는 것이 이번 예제의 핵심입니다.
#4. 마무리
JPA는 처음에는 "왜 쿼리를 안 짜도 되지?"라는 낯섦으로 다가오지만, 엔티티·영속성 컨텍스트·더티 체킹이라는 세 가지 개념과 그 사이를 오가는 데이터의 흐름만 이해하면 생각보다 단순합니다. 오늘 살펴본 저장(persist), 조회(find, 1차 캐시), 수정(더티 체킹) 흐름을 직접 코드로 확인해보면서 익혀보시길 권합니다.
다음 단계로는 N+1 문제, 연관관계 매핑(@OneToMany, @ManyToOne), QueryDSL을 활용한 동적 쿼리 작성까지 넓혀가시면, 실무에서 마주치는 대부분의 JPA 이슈에 대응할 수 있는 기초가 마련될 것입니다.
📎 위에서 다룬 코드는 실행 가능한 전체 프로젝트(Gradle, Controller, 테스트 코드 포함)로 정리해 ZIP 파일로 첨부해두었습니다. Oracle 없이 H2로도 바로 실행해볼 수 있으니, 필요하신 분은 다운로드해서 직접 돌려보시길 바랍니다.
jpa-oracle-example.zip
14.5 kB
긴 글 읽어주셔서 감사합니다.
끝.
끝.

반응형
'Dev & Tech > Java' 카테고리의 다른 글
| 자바 개발자라면 JPA를 왜 꼭 알아야 할까? (0) | 2026.07.10 |
|---|---|
| [Java] Spring Bean 생명주기와 InitializingBean 이해하기 - 생성부터 초기화까지 (2) | 2025.11.24 |
| [Java] System.arraycopy vs Arrays.copyOfRange 차이점 비교 - 성능과 사용법 (0) | 2025.03.25 |
| [Java] Java throw와 throws 차이점 비교 - 예외 처리 핵심 정리 (0) | 2024.07.24 |
| [Java] Java Class 파일 DeCompile (0) | 2020.04.08 |