필터링 기본 흐름
[컨트롤러]
↓ @RequestParam or DTO
[서비스]
↓
[레포지토리 or Mapper]
→ 단순 조건이면 JPA 쿼리
→ 동적 필터 많으면 QueryDSL / Criteria
→ SQL이 중요하면 MyBatis
필터링의 여러방법
1. 쿼리 파라미터 (Query Parameter)
- 프론트에서 /flights?departure=ICN&arrival=NRT&direct=true 처럼 쿼리 파라미터로 요청 → 백엔드에서 조건을 파악해 DB 쿼리 생성.
- Spring에서는 @RequestParam, @RequestBody, 또는 DTO로 받아서 처리.
작동 예시
controller
@GetMapping("/flights")
public List<FlightDto> getFlights(
@RequestParam String departure,
@RequestParam String arrival,
@RequestParam(required = false) Boolean direct
) {
return flightService.getFilteredFlights(departure, arrival, direct);
}
service
public List<FlightDto> getFilteredFlights(String departure, String arrival, Boolean direct) {
List<Flight> flights = flightRepository.findByFilter(departure, arrival, direct);
return flights.stream().map(FlightDto::from).collect(Collectors.toList());
}
repository
@Query("SELECT f FROM Flight f WHERE f.departure = :departure AND f.arrival = :arrival"
+ " AND (:direct IS NULL OR f.direct = :direct)")
List<Flight> findByFilter(
@Param("departure") String departure,
@Param("arrival") String arrival,
@Param("direct") Boolean direct
);
2. 동적 쿼리 (Dynamic Query)
- 사용자가 입력한 필터 조건만 조합해서 SQL 쿼리를 동적으로 생성.
- Spring에서는 QueryDSL, JPA Criteria API, 또는 MyBatis에서 if 태그로 구현.
커스텀 인터페이스 구조
FlightController
↓
FlightService
↓
FlightRepository (Spring Data JPA)
└─ FlightRepositoryCustom (interface)
└─ FlightRepositoryImpl (API 구현)
public interface FlightRepositoryCustom {
List<Flight> searchFlights(String departure, String arrival, Boolean direct);
}
@RequiredArgsConstructor
public class FlightRepositoryImpl implements FlightRepositoryCustom {
각 방식 코드 구현
}
public interface FlightRepository extends JpaRepository<Flight, Long>, FlightRepositoryCustom {
}
구현한 인터페이스 사용 예
@RequiredArgsConstructor
@Service
public class FlightService {
private final FlightRepository flightRepository;
public List<FlightDto> getFlights(String dep, String arr, Boolean direct) {
List<Flight> flights = flightRepository.searchFlights(dep, arr, direct);
return flights.stream()
.map(FlightDto::from)
.collect(Collectors.toList());
}
}
queryDsl 방식(qclass오류주의)
- 타입 안정성(IDE에서 필드 자동완성)
- 조건이 많아져도 깔끔하게 분기 가능
- 실무에서 자주 사용
사용흐름
1. Gradle 설정 → Q클래스 생성
2. JPAQueryFactory 사용해서 동적 조건 조립
3. selectFrom(Q타입).where(조건).fetch()로 데이터 조회
service및 custom repositry
private final JPAQueryFactory queryFactory;
public List<Flight> findFlights(String departure, String arrival, Boolean direct) {
QFlight flight = QFlight.flight;
BooleanBuilder builder = new BooleanBuilder();
if (departure != null) {
builder.and(flight.departure.eq(departure));
}
if (arrival != null) {
builder.and(flight.arrival.eq(arrival));
}
if (direct != null) {
builder.and(flight.direct.eq(direct));
}
return queryFactory.selectFrom(flight)
.where(builder)
.fetch();
}
querydsl설정
@Configuration
public class QuerydslConfig {
@PersistenceContext
private EntityManager em;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(em);
}
}
JPA Criteria API 방식
- 순수 JPA 방식 (Spring Boot에서 추가 라이브러리 필요 없음)
- 타입 안정성은 있지만 복잡하고 비직관적
- 실무에서는 보통 QueryDSL로 대체
service 및 customrepository
@RequiredArgsConstructor
public class FlightRepositoryImpl implements FlightRepositoryCustom {
private final EntityManager em;
@Override
public List<Flight> searchFlights(String departure, String arrival, Boolean direct) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Flight> cq = cb.createQuery(Flight.class);
Root<Flight> flight = cq.from(Flight.class);
List<Predicate> predicates = new ArrayList<>();
if (departure != null) {
predicates.add(cb.equal(flight.get("departure"), departure));
}
if (arrival != null) {
predicates.add(cb.equal(flight.get("arrival"), arrival));
}
if (direct != null) {
predicates.add(cb.equal(flight.get("direct"), direct));
}
cq.where(predicates.toArray(new Predicate[0]));
return em.createQuery(cq).getResultList();
}
}
MyBatis (동적 SQL with XML)
- SQL을 직접 제어 가능 (쿼리 성능 튜닝에 유리)
- if, choose, where 태그로 유연한 쿼리 작성 가능
- JPA보다 복잡한 SQL에서 유리하지만, XML 양이 많아짐
xml파일
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.FlightMapper">
<select id="searchFlights" resultType="com.example.domain.Flight">
SELECT * FROM flight
<where>
<if test="departure != null">
AND departure = #{departure}
</if>
<if test="arrival != null">
AND arrival = #{arrival}
</if>
<if test="direct != null">
AND direct = #{direct}
</if>
</where>
</select>
</mapper>
mapper interface
@Mapper
public interface FlightMapper {
List<Flight> searchFlights(@Param("departure") String departure,
@Param("arrival") String arrival,
@Param("direct") Boolean direct);
}
service단에서 실사용
@Service
@RequiredArgsConstructor
public class FlightService {
private final FlightMapper flightMapper;
public List<Flight> getFilteredFlights(String dep, String arr, Boolean direct) {
return flightMapper.searchFlights(dep, arr, direct);
}
}
3. Specification (Spring JPA)
- JpaSpecificationExecutor를 이용해 조건을 조립식으로 작성 가능.
- 필터 조건이 많거나 복잡할 때 유용.
기본설정
public interface FlightRepository extends JpaRepository<Flight, Long>,
JpaSpecificationExecutor<Flight> {
}
필터 조건 정의 파일
public class FlightSpecifications {
public static Specification<Flight> hasDeparture(String departure) {
return (root, query, cb) -> departure == null
? null
: cb.equal(root.get("departure"), departure);
}
public static Specification<Flight> hasArrival(String arrival) {
return (root, query, cb) -> arrival == null
? null
: cb.equal(root.get("arrival"), arrival);
}
public static Specification<Flight> isDirect(Boolean direct) {
return (root, query, cb) -> direct == null
? null
: cb.equal(root.get("direct"), direct);
}
}
service단에서 사용
@RequiredArgsConstructor
@Service
public class FlightService {
private final FlightRepository flightRepository;
public List<Flight> searchFlights(String departure, String arrival, Boolean direct) {
Specification<Flight> spec = Specification
.where(FlightSpecifications.hasDeparture(departure))
.and(FlightSpecifications.hasArrival(arrival))
.and(FlightSpecifications.isDirect(direct));
return flightRepository.findAll(spec);
}
}
4. ElasticSearch / NoSQL 필터링
- 데이터가 많고 복잡한 검색 조건 (ex. 상품 검색, 항공편 다중 조건 등)이 있다면 RDB가 아닌 검색 엔진을 도입.
- 필터링 조건을 JSON 형태로 보내고, ElasticSearch에서 처리.
기본설정(의존성추가)
implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
도메인 매핑(문서)
ElasticSearch에서 문서는 하나의 json객체 형식의 데이터 단위
"문서" = "검색/필터링/정렬 대상 데이터"
Spring에서 ElasticSearch를 쓸 때, 우리가 JSON 대신 사용하기 쉽게
Java 클래스 하나에 매핑해서 사용하는데, 이걸 흔히 Document 클래스, ES 엔티티라고 부름
@Document(indexName = "flights") // ES의 flights 인덱스에 저장됨
public class FlightDocument {
@Id
private String id;
private String departure;
private String arrival;
private String departureDate;
private String airline;
private Integer price;
private Boolean direct;
}
문서 예시
{
"id": "F1234",
"departure": "ICN",
"arrival": "NRT",
"departureDate": "2025-04-10",
"airline": "Asiana",
"price": 320000,
"direct": true
}
repository정의
public interface FlightSearchRepository extends ElasticsearchRepository<FlightDocument, String> {
List<FlightDocument> findByDepartureAndArrivalAndDirect(String departure, String arrival, Boolean direct);
}
복잡한 쿼리 일시 커스텀 repository사용
예시
- 출발지: ICN
- 도착지: NRT
- 직항만
- 가격은 50만원 이하
- 가격 오름차순 정렬
- 1페이지(0번), 10개씩
SearchHits<FlightDocument> searchFlights(String departure, String arrival, Boolean direct) {
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
if (departure != null) {
boolQuery.must(QueryBuilders.termQuery("departure", departure));
}
if (arrival != null) {
boolQuery.must(QueryBuilders.termQuery("arrival", arrival));
}
if (direct != null) {
boolQuery.must(QueryBuilders.termQuery("direct", direct));
}
boolQuery.filter(QueryBuilders.rangeQuery("price").lte(500000));
NativeSearchQuery query = new NativeSearchQueryBuilder()
.withQuery(boolQuery)
.withSort(Sort.by("price").ascending())
.withPageable(PageRequest.of(0, 10))
.build();
return elasticsearchRestTemplate.search(query, FlightDocument.class);
}
추가설명 더 복잡한 쿼리 전체 작동 방식(정렬 및 페이징도 포함해서 처리 할 수 있음)
구조
- FlightSearchRepositoryCustom ← 인터페이스
- FlightSearchRepositoryImpl ← 구현 (검색 쿼리 조립)
- FlightSearchRepository ← 통합 리포지토리
- FlightSearchService ← 비즈니스 호출
public interface FlightSearchRepositoryCustom {
List<FlightDocument> advancedSearch(String departure, String arrival, List<String> airlines,
Integer minPrice, Integer maxPrice, Boolean direct);
}
@RequiredArgsConstructor
public class FlightSearchRepositoryImpl implements FlightSearchRepositoryCustom {
private final ElasticsearchRestTemplate elasticsearchRestTemplate;
@Override
public List<FlightDocument> advancedSearch(String departure, String arrival, List<String> airlines,
Integer minPrice, Integer maxPrice, Boolean direct) {
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// ① match 검색 (부분 검색 가능)
if (departure != null) {
boolQuery.must(QueryBuilders.matchQuery("departure", departure));
}
if (arrival != null) {
boolQuery.must(QueryBuilders.matchQuery("arrival", arrival));
}
// ② in 조건
if (airlines != null && !airlines.isEmpty()) {
boolQuery.must(QueryBuilders.termsQuery("airline", airlines));
}
// ③ 범위 검색 (가격)
if (minPrice != null || maxPrice != null) {
RangeQueryBuilder range = QueryBuilders.rangeQuery("price");
if (minPrice != null) range.gte(minPrice);
if (maxPrice != null) range.lte(maxPrice);
boolQuery.filter(range);
}
// ④ 직항 여부
if (direct != null) {
boolQuery.must(QueryBuilders.termQuery("direct", direct));
}
// 전체 쿼리 조립
NativeSearchQuery query = new NativeSearchQueryBuilder()
.withQuery(boolQuery)
.withSort(Sort.by("price").ascending())
.withPageable(PageRequest.of(0, 20))
.build();
SearchHits<FlightDocument> hits = elasticsearchRestTemplate.search(query, FlightDocument.class);
return hits.stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
}
}
public interface FlightSearchRepository extends ElasticsearchRepository<FlightDocument, String>,
FlightSearchRepositoryCustom {
}
@RequiredArgsConstructor
@Service
public class FlightSearchService {
private final FlightSearchRepository flightSearchRepository;
public List<FlightDocument> searchFlights(String departure, String arrival,
List<String> airlines, Integer minPrice,
Integer maxPrice, Boolean direct) {
return flightSearchRepository.advancedSearch(departure, arrival, airlines, minPrice, maxPrice, direct);
}
}
5. 필터 DTO를 활용한 통합 처리
프론트에서 모든 필터 조건을 하나의 객체로 전달 → 백엔드는 이를 받아 내부에서 분기 처리
public class FlightFilterRequest {
private String departure;
private String arrival;
private LocalDate departureDate;
private Boolean direct;
private String seatClass;
private Integer adultCount;
// ...
}
필터링 추가 고려사항
- 정렬 옵션 (price ASC, departureTime DESC)
- 페이징 (Pageable)
- 필터 조건 validation
- 검색 속도 (인덱스 설정)
'Spring Boot' 카테고리의 다른 글
| 서블릿 비교 (0) | 2024.12.01 |
|---|---|
| 스프링 내장 톰캣 연결방법 및 서블릿 (0) | 2024.12.01 |