JPA(Java Persistence API)는 Java 객체와 데이터베이스 테이블 간의 매핑을 위한 Java 표준 ORM(Object-Relational Mapping) 기술이다.
// JPA 없이 (JDBC)
String sql = "SELECT * FROM users WHERE id = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setLong(1, userId);
ResultSet rs = pstmt.executeQuery();
User user = new User();
if (rs.next()) {
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
user.setEmail(rs.getString("email"));
}
// JPA 사용
User user = userRepository.findById(userId).orElse(null);
데이터베이스 테이블과 매핑되는 Java 클래스
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_name", nullable = false)
private String name;
@Column(unique = true)
private String email;
// getters and setters
}
엔티티를 영구 저장하는 환경으로, 엔티티의 생명주기를 관리
엔티티를 관리하는 인터페이스로, CRUD 작업을 수행
데이터베이스 작업의 원자성을 보장하는 단위
| 구분 | JPA | MyBatis | JDBC |
|---|---|---|---|
| 레벨 | ORM | SQL Mapper | Database API |
| SQL 작성 | 자동 생성 | 수동 작성 | 수동 작성 |
| 객체 지향 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐ |
| 성능 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 학습 곡선 | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ |
| 유지보수 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
// JPA 사용 시
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// 기본 CRUD 메서드 자동 제공
// findById, save, delete, findAll 등
}
// 사용 예시
User user = userRepository.findById(1L).orElse(null);
userRepository.save(newUser);
userRepository.deleteById(1L);
@Entity
public class User {
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private List<Order> orders = new ArrayList<>();
public void addOrder(Order order) {
orders.add(order);
order.setUser(this);
}
}
# H2 (개발)
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
# MySQL (운영)
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
# PostgreSQL (운영)
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
// 컴파일 타임에 오류 검출
List<User> users = userRepository.findByName("John"); // 타입 안전
// 복잡한 쿼리는 JPQL이나 네이티브 쿼리 필요
@Query("SELECT u FROM User u JOIN u.orders o WHERE o.total > :amount")
List<User> findUsersWithHighValueOrders(@Param("amount") BigDecimal amount);
Spring Data JPA는 JPA를 더욱 쉽게 사용할 수 있도록 도와주는 Spring 모듈이다.
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// 기본 CRUD 메서드 자동 제공
// findById, save, delete, findAll, count 등
// 메서드 이름으로 쿼리 생성
List<User> findByName(String name);
List<User> findByEmailContaining(String email);
Optional<User> findByEmail(String email);
// @Query 어노테이션으로 커스텀 쿼리
@Query("SELECT u FROM User u WHERE u.age > :age")
List<User> findUsersOlderThan(@Param("age") int age);
}
Spring Data JPA는 런타임에 Repository 인터페이스의 구현체를 자동으로 생성한다.
// 개발자가 작성하는 코드
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByName(String name);
}
// Spring이 자동으로 생성하는 구현체 (개념적)
public class UserRepositoryImpl implements UserRepository {
@Override
public List<User> findByName(String name) {
// JPA를 사용한 실제 구현
return entityManager.createQuery(
"SELECT u FROM User u WHERE u.name = :name", User.class)
.setParameter("name", name)
.getResultList();
}
}
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_name", nullable = false, length = 100)
private String name;
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private String password;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private UserRole role = UserRole.USER;
@Column(name = "created_at")
@CreationTimestamp
private LocalDateTime createdAt;
@Column(name = "updated_at")
@UpdateTimestamp
private LocalDateTime updatedAt;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<RefreshToken> refreshTokens = new ArrayList<>();
}
@Entity
@Table(name = "refresh_tokens")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RefreshToken {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String token;
@Column(name = "expiry_date", nullable = false)
private LocalDateTime expiryDate;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
}
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// 메서드 이름으로 쿼리 생성
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
List<User> findByRole(UserRole role);
// @Query로 커스텀 쿼리
@Query("SELECT u FROM User u WHERE u.createdAt >= :startDate")
List<User> findUsersCreatedAfter(@Param("startDate") LocalDateTime startDate);
// 네이티브 쿼리
@Query(value = "SELECT * FROM users WHERE user_name LIKE %:keyword%", nativeQuery = true)
List<User> findUsersByNameKeyword(@Param("keyword") String keyword);
}
@Repository
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
Optional<RefreshToken> findByToken(String token);
@Query("SELECT rt FROM RefreshToken rt WHERE rt.user.id = :userId AND rt.expiryDate > :now")
List<RefreshToken> findValidTokensByUserId(@Param("userId") Long userId, @Param("now") LocalDateTime now);
void deleteByUser(User user);
@Modifying
@Query("DELETE FROM RefreshToken rt WHERE rt.expiryDate < :now")
void deleteExpiredTokens(@Param("now") LocalDateTime now);
}
@Service
@Transactional
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final RefreshTokenRepository refreshTokenRepository;
private final PasswordEncoder passwordEncoder;
public User createUser(User user) {
// 이메일 중복 검사
if (userRepository.existsByEmail(user.getEmail())) {
throw new RuntimeException("이미 존재하는 이메일입니다.");
}
// 비밀번호 암호화
user.setPassword(passwordEncoder.encode(user.getPassword()));
return userRepository.save(user);
}
public User findByEmail(String email) {
return userRepository.findByEmail(email)
.orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다."));
}
public List<User> findUsersByRole(UserRole role) {
return userRepository.findByRole(role);
}
public void deleteUser(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다."));
// 연관된 refresh token도 함께 삭제
refreshTokenRepository.deleteByUser(user);
userRepository.delete(user);
}
}
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
User user = userService.findById(id);
return ResponseEntity.ok(user);
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody @Valid User user) {
User createdUser = userService.createUser(user);
return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
}
@GetMapping("/role/{role}")
public ResponseEntity<List<User>> getUsersByRole(@PathVariable UserRole role) {
List<User> users = userService.findUsersByRole(role);
return ResponseEntity.ok(users);
}
}
@Entity // 엔티티 클래스임을 명시
@Table(name = "users") // 테이블 이름 지정
@Id // 기본키 지정
@GeneratedValue // 기본키 자동 생성
@Column // 컬럼 매핑
@Transient // 데이터베이스에 저장하지 않음
@OneToOne // 1:1 관계
@OneToMany // 1:N 관계
@ManyToOne // N:1 관계
@ManyToMany // N:N 관계
@JoinColumn // 외래키 컬럼 지정
@JoinTable // 조인 테이블 지정
@Inheritance // 상속 매핑
@DiscriminatorColumn // 구분 컬럼
@DiscriminatorValue // 구분 값
@Query // JPQL 쿼리 정의
@NamedQuery // 이름이 있는 쿼리
@NamedQueries // 여러 개의 이름이 있는 쿼리
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// 기본 패턴
List<User> findByName(String name);
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
// 조건 조합
List<User> findByNameAndEmail(String name, String email);
List<User> findByNameOrEmail(String name, String email);
// 비교 연산자
List<User> findByAgeGreaterThan(int age);
List<User> findByAgeLessThanEqual(int age);
List<User> findByAgeBetween(int minAge, int maxAge);
// 문자열 패턴
List<User> findByNameContaining(String name);
List<User> findByNameStartingWith(String prefix);
List<User> findByNameEndingWith(String suffix);
List<User> findByNameLike(String pattern);
// 정렬
List<User> findByNameOrderByAgeAsc(String name);
List<User> findByNameOrderByAgeDesc(String name);
// 페이징
Page<User> findByName(String name, Pageable pageable);
// 제한
List<User> findTop3ByOrderByAgeDesc();
List<User> findFirstByName(String name);
}
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// JPQL 쿼리
@Query("SELECT u FROM User u WHERE u.age > :age")
List<User> findUsersOlderThan(@Param("age") int age);
// 복잡한 조건
@Query("SELECT u FROM User u WHERE u.name LIKE %:keyword% OR u.email LIKE %:keyword%")
List<User> searchUsers(@Param("keyword") String keyword);
// 조인 쿼리
@Query("SELECT u FROM User u JOIN u.orders o WHERE o.total > :amount")
List<User> findUsersWithHighValueOrders(@Param("amount") BigDecimal amount);
// 집계 함수
@Query("SELECT COUNT(u) FROM User u WHERE u.role = :role")
long countUsersByRole(@Param("role") UserRole role);
// 네이티브 쿼리
@Query(value = "SELECT * FROM users WHERE created_at >= :date", nativeQuery = true)
List<User> findUsersCreatedAfter(@Param("date") LocalDate date);
}
@Repository
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
}
// 사용 예시
@Service
public class UserService {
public List<User> findUsers(UserSearchCriteria criteria) {
Specification<User> spec = Specification.where(null);
if (criteria.getName() != null) {
spec = spec.and((root, query, cb) ->
cb.like(root.get("name"), "%" + criteria.getName() + "%"));
}
if (criteria.getMinAge() != null) {
spec = spec.and((root, query, cb) ->
cb.greaterThanOrEqualTo(root.get("age"), criteria.getMinAge()));
}
if (criteria.getRole() != null) {
spec = spec.and((root, query, cb) ->
cb.equal(root.get("role"), criteria.getRole()));
}
return userRepository.findAll(spec);
}
}
// 1. New (비영속)
User user = new User();
user.setName("John");
// 2. Managed (영속)
entityManager.persist(user); // 또는 repository.save(user)
// 3. Detached (준영속)
entityManager.detach(user); // 또는 트랜잭션 종료
// 4. Removed (삭제)
entityManager.remove(user); // 또는 repository.delete(user)
@Entity
@EntityListeners(AuditingEntityListener.class)
public class User {
@PrePersist
public void prePersist() {
System.out.println("저장 전 호출");
this.createdAt = LocalDateTime.now();
}
@PostPersist
public void postPersist() {
System.out.println("저장 후 호출");
}
@PreUpdate
public void preUpdate() {
System.out.println("수정 전 호출");
this.updatedAt = LocalDateTime.now();
}
@PostUpdate
public void postUpdate() {
System.out.println("수정 후 호출");
}
@PreRemove
public void preRemove() {
System.out.println("삭제 전 호출");
}
@PostRemove
public void postRemove() {
System.out.println("삭제 후 호출");
}
@PostLoad
public void postLoad() {
System.out.println("조회 후 호출");
}
}
// ❌ N+1 문제 발생
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findAll(); // 각 User마다 orders를 별도 쿼리로 조회
}
// ✅ Fetch Join 사용
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT DISTINCT u FROM User u JOIN FETCH u.orders")
List<User> findAllWithOrders();
}
// ✅ @EntityGraph 사용
@Entity
public class User {
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
@EntityGraph(attributePaths = {"orders"})
private List<Order> orders;
}
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// 페이징
Page<User> findByName(String name, Pageable pageable);
// 슬라이싱 (카운트 쿼리 없음)
Slice<User> findByName(String name, Pageable pageable);
// 커스텀 카운트 쿼리
@Query(value = "SELECT u FROM User u WHERE u.name = :name",
countQuery = "SELECT COUNT(u.id) FROM User u WHERE u.name = :name")
Page<User> findByNameWithCustomCount(@Param("name") String name, Pageable pageable);
}
@Service
@Transactional
public class UserService {
public void batchInsert(List<User> users) {
int batchSize = 100;
for (int i = 0; i < users.size(); i += batchSize) {
List<User> batch = users.subList(i, Math.min(i + batchSize, users.size()));
userRepository.saveAll(batch);
entityManager.flush();
entityManager.clear();
}
}
}
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String name;
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private String password;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private UserRole role;
@CreatedDate
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@Version
private Long version; // 낙관적 락
// 연관관계는 LAZY 로딩
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Order> orders = new ArrayList<>();
// 편의 메서드
public void addOrder(Order order) {
orders.add(order);
order.setUser(this);
}
}
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// 기본 CRUD는 JpaRepository에서 제공
// 비즈니스 로직에 맞는 메서드명
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
// 복잡한 쿼리는 @Query 사용
@Query("SELECT u FROM User u WHERE u.createdAt >= :startDate AND u.role = :role")
List<User> findActiveUsersByRole(@Param("startDate") LocalDateTime startDate,
@Param("role") UserRole role);
// 성능 최적화를 위한 페이징
Page<User> findByNameContaining(String name, Pageable pageable);
}
@Service
@Transactional(readOnly = true) // 읽기 전용 트랜잭션
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@Transactional // 쓰기 트랜잭션
public User createUser(User user) {
// 비즈니스 로직 검증
if (userRepository.existsByEmail(user.getEmail())) {
throw new RuntimeException("이미 존재하는 이메일입니다.");
}
// 데이터 변환
user.setPassword(passwordEncoder.encode(user.getPassword()));
return userRepository.save(user);
}
public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다."));
}
@Transactional
public void deleteUser(Long id) {
User user = findById(id);
userRepository.delete(user);
}
}
JPA는 Java 개발자에게 객체 지향적인 데이터베이스 접근 방식을 제공하는 강력한 ORM 기술이다.
핵심 포인트:
JPA를 올바르게 사용하면 개발 생산성을 크게 향상시키고, 유지보수 가능한 코드를 작성할 수 있다. 다만 성능 최적화와 복잡한 쿼리 처리에 대한 이해가 필요하므로, 프로젝트의 요구사항에 맞게 적절히 선택하여 사용하는 것이 중요하다.