JPA/Hibernate 패턴
Spring Boot에서 데이터 모델링, 리포지토리, 성능 튜닝에 사용합니다.
활성화 시점
- JPA 엔티티 및 테이블 매핑 설계
- 관계 정의 (@OneToMany, @ManyToOne, @ManyToMany)
- 쿼리 최적화 (N+1 방지, fetch 전략, 프로젝션)
- 트랜잭션, 감사 또는 소프트 삭제 구성
- 페이징, 정렬 또는 커스텀 리포지토리 메서드 설정
- 커넥션 풀링(HikariCP) 또는 2차 캐싱 튜닝
엔티티 설계
@Entity
@Table(name = "markets", indexes = {
@Index(name = "idx_markets_slug", columnList = "slug", unique = true)
})
@EntityListeners(AuditingEntityListener.class)
public class MarketEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 200)
private String name;
@Column(nullable = false, unique = true, length = 120)
private String slug;
@Enumerated(EnumType.STRING)
private MarketStatus status = MarketStatus.ACTIVE;
@CreatedDate private Instant createdAt;
@LastModifiedDate private Instant updatedAt;
}
감사 기능 활성화:
@Configuration
@EnableJpaAuditing
class JpaConfig {}
관계와 N+1 방지
@OneToMany(mappedBy = "market", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PositionEntity> positions = new ArrayList<>();
- 기본적으로 지연 로딩 사용; 필요 시 쿼리에서
JOIN FETCH사용 - 컬렉션에
EAGER사용 자제; 읽기 경로에는 DTO 프로젝션 사용
@Query("select m from MarketEntity m left join fetch m.positions where m.id = :id")
Optional<MarketEntity> findWithPositions(@Param("id") Long id);
리포지토리 패턴
public interface MarketRepository extends JpaRepository<MarketEntity, Long> {
Optional<MarketEntity> findBySlug(String slug);
@Query("select m from MarketEntity m where m.status = :status")
Page<MarketEntity> findByStatus(@Param("status") MarketStatus status, Pageable pageable);
}
- 가벼운 쿼리를 위해 프로젝션 사용:
public interface MarketSummary {
Long getId();
String getName();
MarketStatus getStatus();
}
Page<MarketSummary> findAllBy(Pageable pageable);
트랜잭션
- 서비스 메서드에
@Transactional애너테이션 사용 - 읽기 경로에는
@Transactional(readOnly = true)를 사용하여 최적화 - 전파(propagation)를 신중히 선택하고, 장기 실행 트랜잭션은 피할 것
@Transactional
public Market updateStatus(Long id, MarketStatus status) {
MarketEntity entity = repo.findById(id)
.orElseThrow(() -> new EntityNotFoundException("Market"));
entity.setStatus(status);
return Market.from(entity);
}
페이징
PageRequest page = PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").descending());
Page<MarketEntity> markets = repo.findByStatus(MarketStatus.ACTIVE, page);
커서 기반 페이징의 경우 JPQL에 id > :lastId 조건과 정렬을 포함합니다.
인덱싱과 성능
- 자주 사용하는 필터(
status,slug, 외래 키)에 인덱스 추가 - 쿼리 패턴과 일치하는 복합 인덱스 사용 (
status, created_at) select *사용 자제; 필요한 컬럼만 프로젝션saveAll과hibernate.jdbc.batch_size를 사용하여 쓰기 작업 배치 처리
커넥션 풀링 (HikariCP)
권장 프로퍼티:
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.validation-timeout=5000
PostgreSQL LOB 처리를 위해 다음을 추가:
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
캐싱
- 1차 캐시는 EntityManager마다 존재하므로 트랜잭션 간에 엔티티를 유지하지 않도록 주의
- 읽기가 많은 엔티티의 경우 2차 캐시를 신중히 고려하고, 제거(eviction) 전략을 검증
마이그레이션
- Flyway 또는 Liquibase 사용; 프로덕션에서 Hibernate 자동 DDL에 절대 의존하지 말 것
- 마이그레이션은 멱등성(idempotent)을 유지하고 추가(additive) 방식으로 작성; 계획 없이 컬럼을 삭제하지 말 것
데이터 액세스 테스트
- 프로덕션 환경과 유사한 테스트를 위해
@DataJpaTest와 Testcontainers 사용 선호 - SQL 효율성 확인을 위해 로그 설정:
logging.level.org.hibernate.SQL=DEBUG및 파라미터 값 확인을 위해logging.level.org.hibernate.orm.jdbc.bind=TRACE
명심하세요: 엔티티는 간결하게, 쿼리는 의도적으로, 트랜잭션은 짧게 유지하세요. fetch 전략과 프로젝션으로 N+1을 방지하고, 읽기/쓰기 경로에 맞게 인덱스를 구성하세요.