A.
JPA(Java Persistence API)는 자바 진영의 ORM(Object-Relational Mapping) 표준 명세(인터페이스) 입니다.
핵심 포인트는 JPA는 인터페이스이고, 실제 구현체는 Hibernate, EclipseLink 등이 있다는 점입니다. Spring Boot에서는 기본적으로 Hibernate를 사용합니다.
JPA를 사용하면 SQL을 직접 작성하지 않고 자바 객체와 DB 테이블을 매핑해 데이터를 다룰 수 있습니다.
// JPA 없이 (JDBC)
String sql = "INSERT INTO member (name, age) VALUES (?, ?)";
pstmt.setString(1, member.getName());
pstmt.setInt(2, member.getAge());
// JPA 사용
em.persist(member); // SQL 자동 생성
A.
ORM(Object-Relational Mapping)은 객체와 관계형 DB 테이블을 자동으로 매핑해주는 기술입니다.
장점
단점
A.
영속성 컨텍스트는 엔티티를 영구 저장하는 환경입니다. EntityManager가 관리하며, 트랜잭션 단위로 생성·소멸됩니다.
핵심 기능은 5가지입니다.
| 기능 | 설명 |
|---|---|
| 1차 캐시 | 같은 트랜잭션 내 동일 PK 조회 시 DB 접근 없이 캐시 반환 |
| 동일성 보장 | 같은 PK의 엔티티는 == 비교 시 true |
| 쓰기 지연 | 트랜잭션 커밋 시점에 INSERT/UPDATE SQL 일괄 전송 |
| 변경 감지(Dirty Checking) | 스냅샷과 비교해 변경된 필드를 자동으로 UPDATE |
| 지연 로딩 | 실제 사용 시점에 SQL 실행 |
Member member = em.find(Member.class, 1L); // DB 조회 후 1차 캐시 저장
Member member2 = em.find(Member.class, 1L); // 1차 캐시에서 반환 (SQL 미발생)
System.out.println(member == member2); // true (동일성 보장)
A.
| 상태 | 설명 | 예시 |
|---|---|---|
| 비영속(Transient) | 영속성 컨텍스트와 무관한 새 객체 | new Member() |
| 영속(Managed) | 영속성 컨텍스트에서 관리되는 상태 | em.persist(member) |
| 준영속(Detached) | 영속성 컨텍스트에서 분리된 상태 | em.detach(member) |
| 삭제(Removed) | 삭제 예약 상태 | em.remove(member) |
비영속 → (persist) → 영속 → (detach/close) → 준영속
↓ (remove)
삭제
영속 상태일 때만 변경 감지, 1차 캐시, 쓰기 지연이 동작합니다.
A.
1차 캐시
2차 캐시 (공유 캐시)
EntityManagerFactory 수준에서 관리@Cacheable 어노테이션으로 엔티티에 적용@Entity
@Cacheable // 2차 캐시 적용
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Member { ... }
A.
변경 감지는 영속 상태의 엔티티를 별도의 update() 호출 없이 자동으로 UPDATE 해주는 JPA 기능입니다.
동작 원리
@Transactional
public void updateMember(Long id, String name) {
Member member = memberRepository.findById(id).orElseThrow();
member.setName(name); // 변경만 하면 자동으로 UPDATE 실행
// save() 호출 불필요
}
A.
| 구분 | 지연 로딩 (LAZY) | 즉시 로딩 (EAGER) |
|---|---|---|
| 시점 | 연관 엔티티 실제 사용 시 | 엔티티 조회 시 연관 엔티티 함께 조회 |
| SQL | 사용 시점에 추가 SELECT 발생 | JOIN 등으로 한 번에 조회 |
| 권장 | ✅ 권장 | ❌ 비권장 |
@Entity
public class Order {
@ManyToOne(fetch = FetchType.LAZY) // 지연 로딩 (권장)
private Member member;
}
즉시 로딩의 문제점
A.
N+1 문제는 1개의 메인 쿼리 실행 후, 연관 엔티티를 가져오기 위해 N개의 추가 쿼리가 발생하는 성능 문제입니다.
발생 예시
List<Member> members = memberRepository.findAll(); // SELECT * FROM member (1번)
for (Member member : members) {
member.getOrders().size(); // 각 member마다 SELECT * FROM orders (N번)
}
// 총 1 + N번의 쿼리 발생
해결 방법
@Query("SELECT m FROM Member m JOIN FETCH m.orders")
List<Member> findAllWithOrders();
@EntityGraph(attributePaths = {"orders"})
List<Member> findAll();
application.yml)spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 100
A.
fetch join은 JPQL에서만 사용할 수 있는 문법으로, 연관된 엔티티나 컬렉션을 SQL 1번으로 함께 조회하는 방법입니다.
// 일반 join: member만 조회 (team은 LAZY)
SELECT m FROM Member m JOIN m.team t
// fetch join: member와 team 함께 조회
SELECT m FROM Member m JOIN FETCH m.team t
주의사항
DISTINCT 또는 @BatchSize 사용A.
| 어노테이션 | 관계 | 예시 |
|---|---|---|
@OneToMany | 1:N | 팀(1) : 멤버(N) — 팀 입장에서 |
@ManyToOne | N:1 | 멤버(N) : 팀(1) — 멤버 입장에서 |
@OneToOne | 1:1 | 회원(1) : 회원 상세정보(1) |
@ManyToMany | M:N | 회원 : 상품 |
@ManyToMany는 실무에서 지양합니다.
M:N 관계는 중간 테이블에 추가 컬럼(등록일, 수량 등)을 넣을 수 없기 때문에, 중간 엔티티를 만들어 @OneToMany + @ManyToOne으로 풀어내는 것을 권장합니다.
// M:N 지양
Member ←@ManyToMany→ Product
// 권장: 중간 테이블을 엔티티로
Member ←@OneToMany→ MemberProduct ←@ManyToOne→ Product
A.
JPA에서 양방향 연관관계를 설정할 때 외래 키(FK)를 관리하는 쪽을 연관관계의 주인이라고 합니다.
mappedBy 속성으로 지정 → 읽기만 가능FK는 항상 N(Many) 쪽에 있으므로, 주인도 항상 @ManyToOne 쪽(N쪽)입니다.
@Entity
public class Member {
@ManyToOne
@JoinColumn(name = "team_id") // 연관관계 주인 (FK 관리)
private Team team;
}
@Entity
public class Team {
@OneToMany(mappedBy = "team") // 주인이 아님, 읽기 전용
private List<Member> members = new ArrayList<>();
}
A.
영속성 전이 (Cascade)
부모 엔티티의 상태 변화를 자식 엔티티에 전이시키는 기능입니다.
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> children = new ArrayList<>();
| 종류 | 설명 |
|---|---|
ALL | 모든 전이 |
PERSIST | 저장 시 자식도 함께 저장 |
REMOVE | 삭제 시 자식도 함께 삭제 |
MERGE | 병합 시 자식도 함께 병합 |
고아 객체 제거 (orphanRemoval)
부모 엔티티와의 연관관계가 끊어진 자식 엔티티를 자동으로 삭제합니다.
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Child> children = new ArrayList<>();
// 컬렉션에서 제거하면 자동으로 DELETE
parent.getChildren().remove(child); // DELETE 발생
A.
JPQL(Java Persistence Query Language)은 JPA에서 사용하는 객체 지향 쿼리 언어입니다.
SQL과 유사하지만 테이블과 컬럼명 대신 엔티티 클래스와 필드명을 사용합니다.
// SQL (테이블명, 컬럼명 사용)
SELECT * FROM member WHERE age > 20
// JPQL (엔티티명, 필드명 사용)
SELECT m FROM Member m WHERE m.age > 20
한계
이런 단점을 보완하기 위해 QueryDSL을 함께 사용합니다.
A.
QueryDSL은 타입 세이프(type-safe)한 쿼리 빌더 라이브러리입니다. 컴파일 시점에 Q-class를 생성하여 쿼리를 코드로 작성할 수 있습니다.
JPQL과의 비교
| 구분 | JPQL | QueryDSL |
|---|---|---|
| 타입 안전성 | ❌ 문자열 | ✅ 컴파일 타임 검사 |
| 자동완성 | ❌ 불가 | ✅ IDE 지원 |
| 동적 쿼리 | ❌ 불편 | ✅ BooleanBuilder 등 편리 |
| 가독성 | 보통 | 높음 |
// JPQL
@Query("SELECT m FROM Member m WHERE m.age > :age AND m.name = :name")
List<Member> findByAgeAndName(int age, String name);
// QueryDSL
public List<Member> findByCondition(int age, String name) {
return queryFactory
.selectFrom(member)
.where(
member.age.gt(age),
member.name.eq(name)
)
.fetch();
}
A.
Spring Data JPA의 Repository 인터페이스 계층은 다음과 같습니다.
Repository (최상위 마커 인터페이스)
└── CrudRepository (기본 CRUD)
└── PagingAndSortingRepository (페이징, 정렬)
└── JpaRepository (JPA 특화 기능)
주로 사용하는 JpaRepository가 제공하는 기능
save(), findById(), findAll(), delete()findAll(Pageable pageable)deleteAllInBatch()flush(), saveAndFlush()public interface MemberRepository extends JpaRepository<Member, Long> {
// 메서드 이름으로 쿼리 자동 생성
List<Member> findByNameAndAge(String name, int age);
Optional<Member> findByEmail(String email);
boolean existsByEmail(String email);
}
A.
JPA에서 데이터 변경은 반드시 트랜잭션 안에서 이루어져야 합니다. 트랜잭션 없이는 영속성 컨텍스트의 핵심 기능(변경 감지, 쓰기 지연)이 동작하지 않습니다.
@Transactional의 동작 원리
AOP 프록시를 통해 메서드 실행 전 트랜잭션 시작, 정상 종료 시 커밋, 예외 발생 시 롤백합니다.
@Service
@Transactional(readOnly = true) // 클래스 레벨: 기본 readOnly
public class MemberService {
@Transactional // 메서드 레벨: 쓰기 트랜잭션
public Member save(Member member) {
return memberRepository.save(member);
}
public Member findById(Long id) { // readOnly 적용
return memberRepository.findById(id).orElseThrow();
}
}
readOnly = true 옵션의 이점
A.
낙관적 락 (Optimistic Lock)
@Version 필드로 충돌 감지OptimisticLockException 발생@Entity
public class Product {
@Version
private int version; // 변경 시 자동으로 버전 증가
}
비관적 락 (Pessimistic Lock)
SELECT FOR UPDATE) 획득@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT p FROM Product p WHERE p.id = :id")
Optional<Product> findByIdWithLock(Long id);
| 구분 | 낙관적 락 | 비관적 락 |
|---|---|---|
| 성능 | 높음 | 낮음 (락 대기) |
| 충돌 빈도 | 낮을 때 적합 | 높을 때 적합 |
| 구현 | @Version | @Lock |
| 예외 | OptimisticLockException | PessimisticLockingFailureException |
A.
관계형 DB는 상속이 없으므로, 객체의 상속 구조를 테이블로 표현하는 3가지 전략이 있습니다.
1. 단일 테이블 전략 (SINGLE_TABLE) — 기본값
모든 데이터를 하나의 테이블에 저장. DTYPE 컬럼으로 구분.
2. 조인 전략 (JOINED)
부모·자식 각각 테이블을 생성하고 PK/FK로 JOIN.
3. 구현 클래스별 테이블 전략 (TABLE_PER_CLASS)
부모 테이블 없이 자식 테이블마다 모든 컬럼 포함.
@Entity
@Inheritance(strategy = InheritanceType.JOINED) // 전략 선택
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item { ... }
@Entity
public class Album extends Item { ... }
A.
임베디드 타입은 새로운 값 타입을 직접 정의해 엔티티의 필드로 사용하는 기능입니다. DB 테이블 구조는 그대로 유지하면서 객체를 의미있는 단위로 묶을 수 있습니다.
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
}
@Entity
public class Member {
@Embedded
private Address homeAddress; // 재사용 가능
@Embedded
@AttributeOverrides({ // 같은 타입을 2번 쓸 때 컬럼명 재정의
@AttributeOverride(name = "city", column = @Column(name = "work_city"))
})
private Address workAddress;
}
장점
homeAddress.getCity())A.
| 구분 | JPA (Hibernate) | MyBatis |
|---|---|---|
| 방식 | ORM (객체 ↔ 테이블 매핑) | SQL Mapper (SQL ↔ 결과 매핑) |
| SQL 작성 | 자동 생성 | 직접 작성 |
| 생산성 | 단순 CRUD에서 높음 | 복잡한 쿼리에서 높음 |
| 학습 난이도 | 높음 (N+1, 연관관계 등) | 낮음 |
| 동적 쿼리 | QueryDSL 필요 | XML로 편리하게 작성 |
| 성능 튜닝 | 상대적으로 어려움 | SQL 직접 최적화 가능 |
언제 무엇을 쓰나요?