02. 쿼리 파일 만들기 (JpaRepository)
- Annotation을 사용하여 SQL 쿼리를 클래스나 메소드에 직접 정의하는 방법을 배운다.
- 이를 통해 동적 쿼리 생성과 실행 과정을 이해하고 실습
- QueryMapper 의 DB의존성 및 중복 쿼리 문제로 ORM 이 탄생
- ORM이 해결해야 하는 문제점과 해결책
- ORM을 사용하는 가장 쉬운 방법 : JpaRepository
QueryMapper 의 DB의존성 및 중복 쿼리 문제로 ORM 이 탄생했다.
- ORM 은 DB의 주도권을 뻇어왔다.
- ORM 은 DAO 또는 Mapper 를 통해서 조작하는것이 아니라 테이블을 아예 하나의 객체(Object)와 대응시켜 버렸다.
- 말이 쉽지…. 객체지향(Object) 을 관계형 데이터베이스(Relation) 에 매핑(Mapping) 한다는건 정말 많은 난관이 있다.

ORM 이 해결해야하는 문제점 과 해결책
1. 문제점
상속의 문제
- 객체 : 객체간에 멤버변수나 상속관계를 맺을 수 있다.
- RDB : 테이블들은 상속관계가 없고 모두 독립적으로 존재한다.
💁♂️ 해결방법 : 매핑정보에 상속정보를 넣어준다. (@OneToMany, @ManyToOne)
관계 문제
- 객체 : 참조를 통해 관계를 가지며 방향을 가진다. (다대다 관계도 있음)
- RDB : 외래키(FK)를 설정하여 Join 으로 조회시에만 참조가 가능하다. (즉, 다대다는 매핑 테이블 필요)
💁♂️ 해결방법 : 매핑정보에 방향정보를 넣어준다. (@JoinColumn, @MappedBy)
탐색 문제
- 객체 : 참조를 통해 다른 객체로 순차적 탐색이 가능하며 콜렉션도 순회한다.
- RDB : 탐색시 참조하는 만큼 추가 쿼리나, Join 이 발생하여 비효율적이다.
💁♂️ 해결방법 : 매핑/조회 정보로 참조탐색 시점을 관리한다.(@FetchType, fetchJoin())
밀도 문제
- 객체 : 멤버 객체크기가 매우 클 수 있다.
- RDB : 기본 데이터 타입만 존재한다.
💁♂️ 해결방법 : 크기가 큰 멤버 객체는 테이블을 분리하여 상속으로 처리한다. (@embedded)
식별성 문제
- 객체 : 객체의 hashCode 또는 정의한 equals() 메소드를 통해 식별
- RDB : PK 로만 식별
💁♂️ 해결방법 : PK 를 객체 Id로 설정하고 EntityManager는 해당 값으로 객체를 식별하여 관리 한다.(@Id,@GeneratedValue )
2. 해결책
영속성 컨텍스트(1차 캐시 - 재사용하기 위한 임시 저장소)를 활용한 쓰기지연
- 영속성(Persistenc) 이란
- 데이터를 생성한 프로그램이 종료되어도 사라지지 않는 데이터의 특성을 말한다.
- 영속성을 갖지 않으면 데이터는 메모리에서만 존재하게 되고 프로그램이 종료되면 해당 데이터는 모두 사라지게 된다.
- 그래서 우리는 데이터를 파일이나 DB에 영구 저장함으로써 데이터에 영속성을 부여한다.


영속성 4가지 상태 ( 비영속 > 영속 > 준영속 | 삭제)
- 비영속 (New/Transient) → JPA가 관리하지 않는 상태(객체)
- new로 생성된 상태
- 영속 (Managed) → 엔티티가 영속성 컨텍스트에 저장되어 JPA가 관리하는 상태
- persist() 호출 후 JPA가 관리 (flush() 시 DB 반영)
- 준영속 (Detached) → 한때 영속 상태였지만, 현재는 영속성 컨텍스트에서 분리된 상태
- detach(), clear(), close() 호출 후 관리 종료
- 삭제 (Removed) → 엔티티가 삭제되면서 영속성 컨텍스트에서도 제거된 상태
- remove() 호출 후 삭제 예정 (트랜잭션 완료 시 DB 반영)
- 객체의 영속성 상태는 Entity Manager 의 메소드를 통해 전환된다.
- 💁♂️ Raw JPA 관점에서 순서대로 요약정리 해보자면
- new > (비영속상태) > persist(),merge() > (영속성 컨텍스트에 저장된 상태) > flush() > (DB에 쿼리가 전송된 상태) > commit() > (DB에 쿼리가 반영된 상태)
- remove() 호출 후 삭제 예정 (트랜잭션 완료 시 DB 반영)

- 비영속 (Transient)
- 엔티티가 JPA의 관리 대상이 아닌 상태
- new 키워드로 객체를 생성했지만 아직 persist()를 호출하지 않음
- 영속성 컨텍스트에 저장되지 않으므로, DB와 아무런 연관이 없음
- 변경해도 JPA가 추적하지 않으며, 트랜잭션이 종료되어도 데이터베이스에 반영되지 않음
📌 예제
Store store = new Store(); // 비영속 상태 store.setName("카페");
💡 store 객체는 영속성 컨텍스트와 관계없는 상태로, JPA가 관리하지 않음
- 영속 (Managed)
- 엔티티가 영속성 컨텍스트에 저장되어 JPA가 관리하는 상태
- persist()를 호출하면 영속 상태로 전환됨
- 1차 캐시, 변경 감지(Dirty Checking), 지연 로딩 가능
- 트랜잭션이 끝나면 변경 사항이 자동으로 DB에 반영됨 (flush() 실행)
📌 예제
em.persist(store); // 영속 상태로 전환됨
💡 persist()를 호출하면 JPA가 관리하는 객체가 되어, 변경 사항을 자동 추적
- 준영속 (Detached)
- 한때 영속 상태였지만 더 이상 영속성 컨텍스트에서 관리되지 않는 상태
- detach(), clear(), close() 등을 호출하면 발생
- 변경 사항이 DB에 반영되지 않음
- merge()를 사용하면 다시 영속 상태로 변경 가능
📌 예제
em.detach(store); // store는 더 이상 JPA가 관리하지 않음 store.setName("베이커리"); // 변경해도 DB에 반영되지 않음
💡 detach() 호출 후 변경 사항이 DB에 반영되지 않음
- 삭제 (Removed)
- 엔티티가 삭제됨과 동시에 영속성 컨텍스트에서도 제거된 상태
- remove()를 호출하면 삭제 예정 상태가 되며, 트랜잭션이 완료되면 DB에서도 삭제됨
📌 예제
em.remove(store); // store는 삭제됨
💡 remove() 호출하면 DB에서 실제로 삭제됨 (트랜잭션 완료 시 적용)
예제 코드
Item item = new Item(); // 1
item.setItemNm("테스트 상품"); // 멤버 변수
//JPA에서 데이터베이스와 연결된 EntityManager를 생성하는 역학(Factory)
EntityManager em = entityManagerFactory.createEntityManager(); // 2
EntityTransaction transaction = em.getTransaction(); // 3
transaction.begin(); // 트랜잭션 시작
em.persist(item); // 4-1 // 영속성 상태
em.flush(item). // 4-2 (DB에 SQL 보내기/commit시 자동수행되어 생략 가능함)
transaction.commit(); // 5
em.close(); // 6
1️⃣ 영속성 컨텍스트에 담을 상품 엔티티 생성
2️⃣ 엔티티 매니저 팩토리로부터 엔티티 매니저를 생성
3️⃣ 데이터 변경 시 무결성을 위해 트랜잭션 시작
4️⃣ 영속성 컨텍스트에 저장된 상태, 아직 DB에 INSERT SQL 보내기 전
5️⃣ 트랜잭션을 DB에 반영, 이 때 실제로 INSERT SQL 커밋 수행
6️⃣ 엔티티 매니저와 엔티티 매니저 팩토리 자원을 close() 호출로 반환 (리소스 해제)
- 쓰기 지연이 발생하는 시점
- flush() 동작이 발생하기 전까지 최적화한다.
- flush() 동작으로 전송된 쿼리는 더이상 쿼리 최적화는 되지 않고, 이후 commit()으로 DB에 반영만 가능하다.
- 쓰기 지연 효과
- 여러개의 객체를 생성할 경우 모아서 한번에 쿼리를 전송한다.
- 영속성 상태의 객체가 생성 및 수정이 여러번 일어나더라도 해당 트랜잭션 종료시 쿼리는 1번만 전송될 수 있다.
- 영속성 상태에서 객체가 생성되었다 삭제되었다면 실제 DB에는 아무 동작이 전송되지 않을 수 있다. (Qeury에 전송X)
- 즉, 여러가지 동작이 많이 발생하더라도 쿼리는 트랜잭션당 최적화 되어 최소쿼리만 날라가게된다. -> flush()
- why? IDENTITY는 단일 쿼리로 수행함으로써 외부 트랜잭션에 의한 중복키 생성을 방지하여 단일키를 보장한다.
- 객체 생성하자마자 단일 키 보장을 받아야 한다.
- 쓰기지연 실습코드
@Getter
@Entity
public class Team {
@Id @GeneratedValue // 기본 키 자동생성하게 해줘.
private Long id;
private String name;
// Setter methods
}
@Getter
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY) // 이 관계를 가진다.
@JoinColumn(name = "team_id") // 방향성
private Team team;
// Setter methods
} // 한 트랜잭션이 끝나게 되는거네
Team teamA = new Team();
teamA.setName("TeamA"); // 위에서 객체와 팀이 연관 정보를 만들었으니
em.persist(teamA); // 영속 상태가 되었다.
Team teamB = new Team();
teamB.setName("TeamB"); // 영속 상태가 되었다.
em.persist(teamB);
Member member_A = new Member();
member_A.setName("memberA");
member_A.setTeam(teamA);
em.persist(member_A);
// em.flush(); //하게 되면 db에 저장이 된다. 영속 관계를 갖게 된다.
// 만약 안해주면 조회를 먼저하게 된다. 영속컨텍스트에서
Member findMember = em.find(Member.class, member_A.getId()); // 객체 조회
Team findTeam= findMember.getTeam();
System.out.println(findTeam.getName());
flush가 있는 경우
create member
create team
insert team // flush로 인해 쓰기지연이 발생하지 않음
insert member // flush로 인해 쓰기지연이 발생하지 않음
print "TeamA" (memberA.getTeam())
flush가 없는 경우
create member
create team
print "TeamA" (memberA.getTeam()) // 쓰기 지연이 발생하더라도 영속성 컨텍스트에서 조회해옴
insert team // 쓰기 지연이 발생한 부분
insert member // 쓰기 지연이 발생한 부분
ORM 을 사용하는 가장 쉬운 방법 : JpaRepository (이제 이거를 쓸거다.)
💁♂️ Repository vs JpaRepository(엔티티를 대상으로 동작하는 인터페이스)
- 기존 Repository
- @Repository 을 클래스에 붙인다.
- @Component 어노테이션을 포함하고 있어서 앱 실행시 생성 후 Bean으로 등록된다.
- 앞서배운 Repository 기본 기능만 가진 구현체가 생성된다. (DB별 예외처리 등)
- 새로운 JpaRepository
- JpaRepository<Entity,ID> 인터페이스를 인터페이스에 extends 붙인다.
- @NoRepositoryBean 된 ****상위 인터페이스들의 기능을 포함한 구현체가 프로그래밍된다. (@NoRepositoryBean = 빈생성 막음 →상속받으면 생성돼서 사용가능) - 기능을 다 쓰진 않을거기 때문에
- JpaRepository (마스터 셰프): 데이터 액세스를 위한 핵심 기능의 종합적인 요리책(기능) 을 제공합니다.
- @NoRepositoryBean 인터페이스 (셰프): 각 인터페이스는 특정 데이터 액세스 방법을 제공하는 전문적인 기술 또는 레시피를 나타냅니다.
- JpaRepository 상속: 마스터 셰프의 요리책과 셰프의 전문성을 얻습니다.
- SpringDataJpa 에 의해 엔티티의 CRUD, 페이징, 정렬 기능 메소드들을 가진 빈이 등록된다. (상위 인터페이스들의 기능)
- @NoRepositoryBean 된 ****상위 인터페이스들의 기능을 포함한 구현체가 프로그래밍된다. (@NoRepositoryBean = 빈생성 막음 →상속받으면 생성돼서 사용가능) - 기능을 다 쓰진 않을거기 때문에
- 제네릭 타입으로 Entity와 그 Entity의 ID타입을 넣어주게 된다.
- JpaRepository<Entity,ID> 인터페이스를 인터페이스에 extends 붙인다.
Repository 와 JpaRepository 를 통해 얼마나 간단하게 구현하게 될지 미리 확인해볼까요?
- Repository 샘플
- EntityManager 멤버변수를 직접적으로 사용
// 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> {
// 기본 메서드는 자동으로 만들어짐
}
✅ 마무리 – 영속성과 JPA 정리
JPA의 핵심은 영속성(Persistence) 관리입니다. 이를 이해하면 효율적인 데이터 관리와 최적화된 애플리케이션 설계가 가능하다.
영속성(Persistenc)은 데이터를 생성한 프로그램이 종료되어도 사라지지 않는 데이터의 특성을 말한다.
JPA의 엔티티 생명주기 (비영속 → 영속 → 준영속 → 삭제)
- 비영속 (Transient): JPA가 관리하지 않는 상태 (new로 객체 생성)
- 영속 (Managed): persist() 호출 후 JPA가 관리하는 상태, 변경 사항이 자동 반영됨 (Dirty Checking)
- 준영속 (Detached): detach(), clear(), close() 등을 호출하여 영속성 컨텍스트에서 분리된 상태, 변경이 반영되지 않음
- 삭제 (Removed): remove() 호출 시 영속성 컨텍스트와 DB에서 삭제되는 상태
영속성 컨텍스트의 역할
- 1차 캐시: 동일한 엔티티 조회 시 DB 조회 없이 캐시에서 가져옴 (성능 향상)
- 변경 감지(Dirty Checking): 엔티티의 변경 사항을 자동으로 감지하여 업데이트
- 지연 로딩(Lazy Loading): 필요한 시점까지 데이터베이스 조회를 지연하여 성능 최적화
- 트랜잭션 커밋 시 flush() 자동 실행: 변경된 엔티티를 자동으로 DB에 반영
JPA가 제공하는 핵심 기능
- EntityManagerFactory → EntityManager를 생성하는 공장 (애플리케이션에서 한 번만 생성)
- EntityManager → 트랜잭션 단위로 생성 및 종료, DB와 직접 연결하여 CRUD 수행
- @GeneratedValue → 기본 키(PK) 자동 생성 (IDENTITY, SEQUENCE, AUTO 등)
✔ JPA의 영속성 개념을 정확히 이해하면, 성능 최적화와 유지보수성을 높일 수 있다.
'Sparta(JAVA심화3기) - TIL > 스파르타 강의 - JPA, Docker, 입문, 숙련, 심화' 카테고리의 다른 글
| JPA(JAVA Persistence API) - 3-3 주차 (TIL) ⭐️ (0) | 2025.02.14 |
|---|---|
| JPA(JAVA Persistence API) - 3-1 주차 (TIL) (2) | 2025.02.11 |
| JPA(Java Persistence API) - 2-2주차 (TIL) (1) | 2025.02.11 |
| JPA (Java Persistence API) - 2-1주차 (TIL) (0) | 2025.02.11 |
| JPA (Java Persistence API) - 1주차 (1) | 2025.02.11 |