[Spring-ELK] Docker: Elastic Search , Kibana를 설치해서 springboot와 연결하자! (2)

kang·2024년 11월 5일

Spring_ELK

목록 보기
3/10

이전글을 보면 docker위에 es와 키바나가 설치된 것 까지 진행되었다. 이번에는 spring boot환경을 세팅할 거다!

1. spring에서 검색 api 만들기

api를 생성하려면? 기존에 Entity를 생성 -> Repository 생성 -> Service를 생성 -> controller 등록한다.

그런데 우리는 지금 검색엔진을 만든다. 따라서 검색에 사용할 api라는 것인데 findById와 같은 query보다는 동적쿼리에 집중해서 알아볼 것이다.

여기서는 Criteria를 사용할 것이다.

1-1.config

package com.sparta.projectblue.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
import org.springframework.beans.factory.annotation.Value;

@Configuration
@EnableElasticsearchRepositories(basePackages = "org.springframework.data.elasticsearch.repository")
public class ElasticSearchConfig extends ElasticsearchConfiguration {

    @Value("${elastic.host}")
    private String elasticHost;

    @Override
    public ClientConfiguration clientConfiguration() {
        return ClientConfiguration.builder().connectedTo(elasticHost).build();
    }
}

properties

elastic:
  host: ${ES_URI}

다른 코드들도 많아서 es 관련 부분만 편집했다.
spring밑에 es를 넣어주고
host는 환경변수로 줬다.
나는 시큐리티는 이용하지 않아서 pw는 없었다!

1-2. 작성내용

내가 만들 검색 api는 다음과 같다.

  • 관리자 시점에서 사용자들이 구매한 구매내역(예매내역)과 결제내역을 검색할 수 있다.
  • 필터 방식으로 검색을 구현하여 필요한 정보를 빠르게 찾을 수 있게 했다.

사실 이래도 되는 지 잘 모르겠다(일단 주먹구구식 구현했음)

  • 검색 API의 구현 순서:

1) Document 생성: 인덱스에 들어갈 데이터 형식을 정의한다.
2) Index 생성: Elasticsearch에서 데이터를 저장할 인덱스를 만든다.
3) 데이터 동기화: DB에서 Elasticsearch로 데이터를 옮기는 동기화 API를 구현한다.
4) 검색 API 구현: 관리자가 검색할 수 있도록 API를 작성한다.

차례대로 구현했다.

1-3. 구현

코드는 다음과 같다. 사실 초반에는 결제부분까지 document에 넣었는데 현재 구현된 건 예매 부분까지밖에 없다.

그래서 후에 확장 가능하면 이 부분을 확장해볼 계획이다.

(document, dto, service, controller)

  • document
package com.sparta.projectblue.domain.search.document;

import java.time.LocalDateTime;

import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@Document(indexName = "user_booking_index_v4")
public class UserBookingDocument {
    @Id private Long reservationId;

    @Field(type = FieldType.Text)
    private String userName;

    @Field(type = FieldType.Long)
    private Long userId;

    @Field(type = FieldType.Text)
    private String performanceTitle;

    @Field(
            type = FieldType.Date,
            format = {},
            pattern = "yyyy-MM-dd'T'HH:mm:ss")
    private LocalDateTime bookingDate; // 예매한 날짜

    @Field(type = FieldType.Long)
    private Long paymentAmount; // 예매한 티켓 금액

    @Field(type = FieldType.Text)
    private String reservationStatus; // 예매 상태

    @Field(type = FieldType.Long)
    private Long paymentId; // 결제id (추가구현 가능하려나)

    @Field(
            type = FieldType.Date,
            format = {},
            pattern = "yyyy-MM-dd'T'HH:mm:ss")
    private LocalDateTime paymentDate; // 결제 된 날짜(추가구현 가능하려나2)

    public UserBookingDocument(
            Long reservationId,
            String userName,
            Long userId,
            String performanceTitle,
            LocalDateTime bookingDate,
            Long paymentAmount,
            String reservationStatus,
            Long paymentId,
            LocalDateTime paymentDate) {
        this.reservationId = reservationId;
        this.userName = userName;
        this.userId = userId;
        this.performanceTitle = performanceTitle;
        this.bookingDate = bookingDate;
        this.paymentAmount = paymentAmount;
        this.reservationStatus = reservationStatus;
        this.paymentId = paymentId;
        this.paymentDate = paymentDate;
    }
}
  • UserBookingDto
package com.sparta.projectblue.domain.search.dto;

import java.time.LocalDateTime;

import com.sparta.projectblue.domain.common.enums.ReservationStatus;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class UserBookingDto {
    // 동기화와 검색에 공통으로 사용되는 필드
    private Long reservationId;
    private String userName;
    private Long userId;
    private String performanceTitle;
    private LocalDateTime bookingDate;
    private Long paymentAmount;
    private ReservationStatus reservationStatus;
    private Long paymentId;
    private LocalDateTime paymentDate;

    // 검색에만 사용되는 필드
    private LocalDateTime bookingDateStart;
    private LocalDateTime bookingDateEnd;
    private Long minPaymentAmount;
    private Long maxPaymentAmount;
    private String searchReservationStatus;

    public UserBookingDto(Long reservationId, String userName, Long userId, String performanceTitle,
                          LocalDateTime bookingDate, Long paymentAmount, ReservationStatus reservationStatus,
                          Long paymentId, LocalDateTime paymentDate) {
        this.reservationId = reservationId;
        this.userName = userName;
        this.userId = userId;
        this.performanceTitle = performanceTitle;
        this.bookingDate = bookingDate;
        this.paymentAmount = paymentAmount;
        this.reservationStatus = reservationStatus;
        this.paymentId = paymentId;
        this.paymentDate = paymentDate;
    }

}
  • service
package com.sparta.projectblue.domain.search.service;

import java.util.List;
import java.util.stream.Collectors;

import com.sparta.projectblue.domain.reservation.repository.ReservationRepository;
import org.springframework.stereotype.Service;

import com.sparta.projectblue.domain.search.document.UserBookingDocument;
import com.sparta.projectblue.domain.search.dto.UserBookingDto;
import com.sparta.projectblue.domain.search.repository.UserBookingEsRepository;

import lombok.RequiredArgsConstructor;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class UserBookingSearchService {
    private final UserBookingEsRepository userBookingEsRepository;
    private final ReservationRepository reservationRepository;

    @Transactional
    public void syncData() {
        // 데이터베이스에서 필요한 데이터 조회
        List<UserBookingDocument> documents = reservationRepository.findUserBookingData().stream()
                .map(this::convertToDocument)
                .collect(Collectors.toList());

        // Elasticsearch 인덱스에 동기화
        userBookingEsRepository.saveAll(documents);
    }

    private UserBookingDocument convertToDocument(UserBookingDto dto) {
        return new UserBookingDocument(
                dto.getReservationId(),
                dto.getUserName(),
                dto.getUserId(),
                dto.getPerformanceTitle(),
                dto.getBookingDate(),
                dto.getPaymentAmount(),
                dto.getReservationStatus().name(), // Enum을 문자열로 변환하여 저장
                dto.getPaymentId(),
                dto.getPaymentDate()
        );
    }
    public List<UserBookingDocument> searchBookings(UserBookingDto searchCriteria) {
        return userBookingEsRepository.searchByCriteria(searchCriteria);
    }

}
  • UserBookingController
// UserBookingController.java
package com.sparta.projectblue.domain.search.controller;

import java.time.LocalDateTime;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import com.sparta.projectblue.config.ApiResponse;
import com.sparta.projectblue.domain.search.dto.UserBookingDto;
import com.sparta.projectblue.domain.search.service.UserBookingSearchService;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/search")
@RequiredArgsConstructor
@Tag(name = "Search", description = "검색 API")
public class UserBookingController {

    private final UserBookingSearchService searchService;

    @GetMapping("/admin")
    @Operation(summary = "관리자 정보 검색", description = "다양한 조건을 이용해 정보를 검색합니다")
    public ResponseEntity<?> searchBookings(
            @RequestParam(required = false) String userName,
            @RequestParam(required = false) String performanceTitle,
            @RequestParam(required = false) LocalDateTime bookingDateStart,
            @RequestParam(required = false) LocalDateTime bookingDateEnd,
            @RequestParam(required = false) String searchReservationStatus,
            @RequestParam(required = false) Long minPaymentAmount,
            @RequestParam(required = false) Long maxPaymentAmount) {

        // 검색 파라미터를 UserBookingDto에 매핑
        UserBookingDto dto = new UserBookingDto(
                null, // reservationId는 검색에 필요하지 않으므로 null로 설정
                userName,
                null, // userId는 검색에 필요하지 않으므로 null로 설정
                performanceTitle,
                null, // bookingDate는 단일 값이 아닌 범위를 사용하므로 null로 설정
                null, // paymentAmount는 범위로 검색하므로 null로 설정
                null, // reservationStatus는 문자열로 처리하여 searchReservationStatus에 매핑
                null, // paymentId는 검색에 필요하지 않으므로 null로 설정
                null, // paymentDate는 검색에 필요하지 않으므로 null로 설정
                bookingDateStart,
                bookingDateEnd,
                minPaymentAmount,
                maxPaymentAmount,
                searchReservationStatus // 검색 상태 문자열
        );

        return ResponseEntity.ok(ApiResponse.success(searchService.searchBookings(dto)));
    }

    @GetMapping("/sync")
    @Operation(summary = "데이터 동기화", description = "예약 정보를 Elasticsearch와 동기화합니다")
    public ResponseEntity<ApiResponse<String>> syncData() {
        searchService.syncData();
        return ResponseEntity.ok(ApiResponse.success("데이터 동기화가 완료되었습니다."));
    }
}

동기화와 검색 api 들은 전부 이렇게 구현했다.

1-4. repository

  • UserBookingEsRepository
package com.sparta.projectblue.domain.search.repository;

import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

import com.sparta.projectblue.domain.search.document.UserBookingDocument;

public interface UserBookingEsRepository
        extends ElasticsearchRepository<UserBookingDocument, Long>, UserBookingEsCustomRepository {}
  • UserBookingEsCustomRepository
package com.sparta.projectblue.domain.search.repository;

import java.util.List;

import com.sparta.projectblue.domain.search.document.UserBookingDocument;
import com.sparta.projectblue.domain.search.dto.UserBookingDto;

public interface UserBookingEsCustomRepository {
    List<UserBookingDocument> searchByCriteria(UserBookingDto request);
}
  • UserBookingEsRepositoryImpl
package com.sparta.projectblue.domain.search.repository;

import java.util.List;
import java.util.stream.Collectors;

import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
import org.springframework.stereotype.Repository;

import com.sparta.projectblue.domain.search.document.UserBookingDocument;
import com.sparta.projectblue.domain.search.dto.UserBookingDto;

import lombok.RequiredArgsConstructor;

@Repository
@RequiredArgsConstructor
public class UserBookingEsRepositoryImpl implements UserBookingEsCustomRepository {

    private final ElasticsearchOperations elasticsearchOperations;

    @Override
    public List<UserBookingDocument> searchByCriteria(UserBookingDto request) {
        Criteria criteria = new Criteria();

        // 사용자 이름 조건 추가
        if (request.getUserName() != null && !request.getUserName().isEmpty()) {
            criteria = criteria.and("userName").is(request.getUserName());
        }

        // 공연 제목 조건 추가
        if (request.getPerformanceTitle() != null && !request.getPerformanceTitle().isEmpty()) {
            criteria = criteria.and("performanceTitle").is(request.getPerformanceTitle());
        }

        // 예약 날짜 범위 조건 추가
        if (request.getBookingDateStart() != null && request.getBookingDateEnd() != null) {
            criteria = criteria.and("bookingDate")
                    .between(request.getBookingDateStart(), request.getBookingDateEnd());
        } else if (request.getBookingDateStart() != null) {
            criteria = criteria.and("bookingDate").greaterThanEqual(request.getBookingDateStart());
        } else if (request.getBookingDateEnd() != null) {
            criteria = criteria.and("bookingDate").lessThanEqual(request.getBookingDateEnd());
        }

        // 결제 금액 범위 조건 추가
        if (request.getMinPaymentAmount() != null && request.getMaxPaymentAmount() != null) {
            criteria = criteria.and("paymentAmount")
                    .between(request.getMinPaymentAmount(), request.getMaxPaymentAmount());
        } else if (request.getMinPaymentAmount() != null) {
            criteria = criteria.and("paymentAmount").greaterThanEqual(request.getMinPaymentAmount());
        } else if (request.getMaxPaymentAmount() != null) {
            criteria = criteria.and("paymentAmount").lessThanEqual(request.getMaxPaymentAmount());
        }

        // 예약 상태 조건 추가
        if (request.getSearchReservationStatus() != null && !request.getSearchReservationStatus().isEmpty()) {
            criteria = criteria.and("reservationStatus").is(request.getSearchReservationStatus());
        }

        CriteriaQuery query = new CriteriaQuery(criteria);

        return elasticsearchOperations
                .search(query, UserBookingDocument.class)
                .getSearchHits()
                .stream()
                .map(hit -> hit.getContent())
                .collect(Collectors.toList());
    }
}

여기서 의존성 문제가 엄청 터졌다...
꼭 스프링과 es 관련 의존성 문제 확인하고 설치하시길... (나는 후반에 알기도 했고, 이미 팀원과 환경을 동일시 해놔서...방법이 없었따)

1-5. mapping

PUT /user_booking_index_v4
{
  "mappings": {
    "properties": {
      "reservationId": {
        "type": "long"
      },
      "userName": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      },
      "userId": {
        "type": "long"
      },
      "performanceTitle": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      },
      "bookingDate": {
        "type": "date",
        "format": "yyyy-MM-dd'T'HH:mm:ss"
      },
      "paymentAmount": {
        "type": "long"
      },
      "reservationStatus": {
        "type": "text"
      },
      "paymentId": {
        "type": "long"
      },
      "paymentDate": {
        "type": "date",
        "format": "yyyy-MM-dd'T'HH:mm:ss"
      }
    }
  }
}

코드는 이러하고 다음에는 결과물을 들고오겠다.

profile
뉴비 개발 공부중

0개의 댓글