JPA(Java Persistence API)는 Java 애플리케이션에서 관계형 데이터베이스를 객체지향적으로 다루기 위한 표준 인터페이스(스펙)이다.
핵심 키워드: ORM(Object-Relational Mapping)
Java 객체(Entity) ↔ JPA(표준 스펙) ↔ Hibernate(구현체) ↔ DB 테이블
JPA는 스펙일 뿐이고, 실제 동작하는 구현체는 Hibernate, EclipseLink 등이 있다. Spring Boot에서는 기본적으로 Hibernate를 사용한다. (업계 비공식 표준)
SQL 중심 개발의 문제
// JDBC 직접 사용 - 반복적이고 실수하기 쉬움
String sql = "INSERT INTO member (name, email) VALUES (?, ?)";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, member.getName());
pstmt.setString(2, member.getEmail());
pstmt.executeUpdate();
테이블에 컬럼 하나가 추가되면 -> 관련된 모든 SQL을 수정해야 한다
// JPA 사용 - SQL을 직접 작성하지 않아도 됨
em.persist(member); // INSERT
Member found = em.find(Member.class, id); // SELECT
found.setName("새이름"); // UPDATE (변경 감지)
em.remove(found); // DELETE
JPA의 핵심 이점
@Entity
@Table(name = "members")
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 50)
private String name;
@Column(unique = true)
private String email;
// 기본 생성자 필수
protected Member() {}
public Member(String name, String email) {
this.name = name;
this.email = email;
}
}
protected로 생성하는 이유는?protected를 사용한다.public으로 해도 JPA는 잘 동작한다. 하지만 그러면 아무데서나 new Member()로 불완전한 객체를 만들 수 있게 된다.protected로 선언하면 JPA(리플렉션)는 접근 가능하지만, 외부 코드에서는 의미 있는 생성자(new Member(name, email))만 사용하도록 유도할 수 있다.private는 안되는가? -> Hibernate 프록시 (지연 로딩 시 상속으로 만드는 가짜 객체)가 기본 생성자에 접근해야 하므로 private는 JPA 스펙 상 허용하지 않는다. 최소 protected만 가능protected Member() {} // JPA용 - 외부 직접 호출 차단, 프록시 접근은 허용
public Member(String name, String email) { // 실제 사용할 생성자
this.name = name;
this.email = email;
}
protected임JPA의 핵심 인터페이스, 엔티티의 CRUD를 담당한다.
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
em.persist(new Member("John", "john@example.com")); // 영속성 컨텍스트에 저장
tx.commit();
em.close();
JPA를 이해하는 데 가장 핵심적인 개념
영속성 컨텍스트(Persistence Context)는 엔티티를 영구 저장하기 전에 머무르는 1차 캐시 공간의 개념이다.
EntityManager가 이 컨텍스트를 통해 엔티티를 관리하게 된다.
엔티티의 4가지 생명주기
비영속(new) → persist() → 영속(managed) → detach()/clear() → 준영속(detached)
↓
remove() → 삭제(removed)
new Member()만 한 상태. JPA는 모름em.persist() 호출 후. 컨텍스트가 관리 중em.remove() 호출. 커밋 시 DELETE 쿼리 나감영속성 컨텍스트가 제공하는 기능 :
// 1차 캐시
Member member = em.find(Member.class, 1L); // DB 조회 → 캐시 저장
Member same = em.find(Member.class, 1L); // 캐시에서 반환 (SQL 안 나감)
System.out.println(member == same); // true (동일성 보장)
동일성 보장 - em.find(Member.class, 1L) == em.find(Member.class, 1L) -> true
변경 감지(Dirty Checking) - setter로 값만 바꿔도 트랜잭션 커밋 시 자동 UPDATE
영속 상태 엔티티의 최초 스냅샷과 현재 상태를 비교해서, 달라졌으면 UPDATE 쿼리 자동 생성
// 변경 감지
Member member = em.find(Member.class, 1L);
member.setName("변경된이름"); // setter만 호출
// em.update() 같은 건 없다!
// 트랜잭션 커밋 시 자동으로 UPDATE 쿼리 발생
// 쓰기 지연
em.persist(memberA); // INSERT SQL을 쓰기 지연 저장소에 모음
em.persist(memberB); // 여기서도 아직 DB에 안 감
transaction.commit(); // 이 시점에 INSERT 2개가 한꺼번에 flush
영속성 컨텍스트는 JPA 이해에 있어 왜 중요한가?
-> 영속성 컨텍스트를 모르면 JPA 동작 구조를 이해하기 어려움
-> 실무에서 겪는 대부분의 JPA 관련 트러블슈팅이 여기서 나옴
LazyInitializationException -> 준영속 상태에서 지연 로딩 시도 ( 컨텍스트가 닫힌 후 프록시 접근 )@Transactional 범위 설계 -> 영속성 컨텍스트의 생존 범위 = 트랙잭션 범위라서, 트랜잭션 설계가 곧 엔티티 관리 전략SpringBoot에서는 @Transactional이 트랜잭션과 영속성 컨텍스트를 함께 열고 닫아주므로, 서비스 계층 설계 시 이 개념을 이해해야 어디서 엔티티가 살아있고, 어디서 죽는지 (생명주기)를 파악, 판단할 수 있다. 이는 곳 API 응답 시점에 엔티티 상태를 예측하는 능력과 직결된다.
//EntityManager의 여러 메서드
EntityManager em = emf.createEntityManager();
em.persist(entity); // 영속화
em.find(...) // 조회
em.remove(entity); // 삭제
em.detach(entity); // 준영속으로 전환
em.merge(entity); // 준영속 → 다시 영속
em.flush(); // 쓰기 지연 SQL 강제 실행
em.clear(); // 1차 캐시 전체 초기화
SpringBoot에서는 직접 EntityManager를 다룰 일은 적음. Spring Data JPA의 JpaRepository가 내부적으로 EntityManager를 감싸서 save(),findById(),delete() 등의 메서드로 추상화해주고 있기 때문.
객체의 참조와 테이블의 FK (Foreign Key)를 매핑하는 것
@Entity
public class Order {
@Id @GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY) // 대부분의 경우 LAZY 사용
@JoinColumn(name = "member_id")
private Member member;
}
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "member") // 읽기 전용 (FK를 관리하지 않음)
private List<Order> orders = new ArrayList<>();
}
| 관계 | Annotation | 예시 |
|---|---|---|
| 다대일 | @ManyToOne | 주문 -> 회원 |
| 일대다 | @OneToMany | 회원 -> 주문목록 |
| 일대일 | @OneToOne | 회원 -> 프로필 |
| 다대다 | @ManyToMany | 사용하지 않음 (중간 테이블 필요) |
Q. FetchType이란 무엇인가?
A. FetchType이란 연관된 엔티티를 언제 DB에서 가져올 것인가를 결정하는 전략이다.
FetchType.EAGER ( 즉시 로딩 )@ManyToOne(fetch = FetchType.EAGER)
private Team team;
FetchType.LAZY ( 지연 로딩 )@ManyToOne(fetch = FetchType.LAZY)
private Team team;
Member만 먼저 조회하고, member.getTeam().getName() 처럼 실제 접근이 필요한 경우 그 때 Team을 SELECT함. 내부적으로는 프록시 객체를 넣어두는 방식
@ManyToOne @OneToOne은 기본적으로 EAGER 타입
가급적 모든 연관관계를 LAZY로 설정하고, 필요시에만 fetch join으로 한번에 가져오는 것이 JPA 성능 최적화의 기본 공식임
// JPQL fetch join으로 필요한 시점에 한 번에 조회
@Query("SELECT m FROM Member m JOIN FETCH m.team")
List<Member> findAllWithTeam();
JPA는 SQL이 아닌 객체를 대상으로 쿼리하는 JPQL을 제공한다.
// SQL: SELECT * FROM members WHERE name = 'Tom'
// JPQL: 테이블이 아닌 엔티티를 대상으로 쿼리
String jpql = "SELECT m FROM Member m WHERE m.name = :name";
List<Member> result = em.createQuery(jpql, Member.class)
.setParameter("name", "Tom")
.getResultList();
Spring Boot에서는 JPA를 더 편하게 쓸 수 있도록 Spring Data JPA를 제공한다.
public interface MemberRepository extends JpaRepository<Member, Long> {
// 메서드 이름만으로 쿼리 자동 생성
Optional<Member> findByEmail(String email); // SELECT * FROM member WHERE email = {email};
List<Member> findByNameContaining(String keyword);
boolean existsByEmail(String email);
}
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MemberService {
private final MemberRepository memberRepository;
@Transactional
public Long join(String name, String email) {
Member member = new Member(name, email);
memberRepository.save(member); // persist() 호출
return member.getId();
}
public Member findById(Long id) {
return memberRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("회원 없음"));
}
}
계층 구조
Spring Data JPA → JPA(표준 인터페이스) → Hibernate(구현체) → JDBC → DB
① N+1 문제
연관된 엔티티를 조회할 때 추가 쿼리가 N번 나가는 문제. fetch join이나 @EntityGraph로 해결한다.
② 즉시 로딩(EAGER) 사용 금지
@ManyToOne의 기본값이 EAGER이므로, 반드시 fetch = FetchType.LAZY로 설정한다.
③ 양방향 매핑은 필요할 때만
단방향으로 충분하면 양방향을 만들지 않는다. 양방향은 복잡도만 높인다.
④ @Transactional 범위
영속성 컨텍스트는 트랜잭션과 생명주기를 같이 한다. Service 계층에 @Transactional을 선언하는 것이 일반적이다.