코드의 개선 사항이 필요해보인다면 언제든 댓글로 지적해주세요 :)

 

현재 개발하고자 하는 서비스는 흔히들 하는 상상인 '로또 당첨되면 뭐하지?'를 실제로 체험해볼 수 있는 로또 시뮬레이션 웹이다. 이에 필요한 api명세는 다음과 같다. 나는 로또와 장바구니 관련된 API 구현 & 서버 배포 및 CICD 구현을 담당하였다. 이에 대한 개발 과정을 포스팅할 예정이다.


엘라스틱 서치 연동

 

ES 사용 이유와 대략적인 개념 정리

구현하고자 하는 서비스는 조회가 대부분의 기능을 이루는 서비스이다. 이 때문에 당연히 검색 성능을 높이는 방법에 가장 집중을 많이 했다. 인덱싱과 반정규화 등등 여러 방안에 대해 얘기를 하다가 결정하게 된 것이 엘라스틱 서치였다. 엘라스틱 서치는 분산형 RESTful 검색 및 분석 엔진이다. 검색에 최적화된 엔진인 만큼 로깅 서치 등에 많이 활용된다.

엘라스틱 서치는 흔히 사용되는 RDBMS 개념과 다음과 같이 매핑된다. 엘라스틱 서치의 가장 주된 특징은 아무래도 역색인이라고 생각된다. RDBMS 의 일반적인 색인이 책의 총 목차와 같은 개념이라면 엘라스틱 서치의 역색인은 책 가장 후반에 존재하는 단어 별 색인 페이지 같은 개념이다. 이러한 개념 덕분에 빠른 검색이 가능한 것이다.

 

 

Elastic Search 연동

build.gradle - 의존성 추가

    implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'

 

application.yml

spring:
  ...
  
  elastic:
    url: ${ELASTIC_URL} # ex. 000.00.00.000:9200

 

ElasticSearchConfig.java

@Configuration
public class ElasticSearchConfig extends ElasticsearchConfiguration {

    @Value("${spring.elastic.url}")
    private String elasticUrl;

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


다음과 같이 작성해 놓으면 대략적인 연동은 마무리 된 것이다. 이제 es에서 데이터를 사용해 api를 구현한 과정을 정리해보자.

 


Entity

@Getter
@Setter
@Mapping(mappingPath = "lotto-mapping.json")
@Setting(settingPath = "elastic-setting.json")
@Document(indexName = "dhlottery")
public class LottoDocument {
    @Id
    private String id;

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

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

    @Field(type = FieldType.Integer)
    private Integer round;

    @Field(type = FieldType.Integer)
    private Integer winnerNum;

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

    @Field(type = FieldType.Text) // 최종 당첨 번호
    private String finalNumbers;
}
  • @Document 어노테이션 : 엘라스틱서치의 "dhlottery" 인덱스에 매핑
  • @Mappging 어노테이션 : 엘라스틱서치 인덱스의 필드 구조와 데이터 유형 정의. 경로는 resource 디렉토리를 기준으로 함
  • @Setting 어노테이션 : 엘라스틱서치 인덱스의 설정의 정의

DTO

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LottoDto {
    private String id;
    private Long actualWinnings;
    private String prizeDate;
    private Integer round;
    private Integer winnerNum;
    private Long winnings;
    private String finalNumbers;
}

로직에서 사용되는 엔티티를 클라이언트에게 직접적으로 노출하지 않는 것이 더 좋기 때문에 dto를 선언해주었다. 이 외에도 데이터 응답 형식과 같은 것들을 조정할 수 있기 때문에 편리하기도 하다.

 


Repository

@Repository
public interface LottoRepository extends ElasticsearchRepository<LottoDocument, String> {
    Optional<LottoDocument> findByRound(Integer round);
}

정렬된 전체 값을 가져오는 기능은 Pagination을 통해 구현할 예정이어서 특정 회차를 통해 값을 가져오는 쿼리만 작성하였다.

 


Service

@Service
@RequiredArgsConstructor
public class LottoService {

    private final LottoRepository lottoRepository;
    private final StatNumRepository statNumRepository;

    public Page<LottoDto> getLottoList(int page, int size, String sortBy) {
        Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, sortBy));
        return lottoRepository.findAll(pageable).map(this::toLottoDTO);
    }

    public Optional<LottoDto> getLottoByRound(Integer round) {
        return lottoRepository.findByRound(round).map(this::toLottoDTO);
    }

    private LottoDto toLottoDTO(LottoDocument doc) {
        return new LottoDto(
                doc.getId(),
                doc.getActualWinnings(),
                doc.getPrizeDate(),
                doc.getRound(),
                doc.getWinnerNum(),
                doc.getWinnings(),
                doc.getFinalNumbers()
        );
    }
}

로또 전체 정보를 조회할 땐 Pagination을 통해 정렬도 되게끔 작성하였다. 그냥 JPA 쿼리를 통해 정렬을 할 수도 있으나, 추후 정렬 기준이 늘어나게 될 경우도 고려하여 확장성이 높은 해당 방식을 선정하였다. page(조회하고자 하는 페이지 번호), size(페이지 당 보여질 정보 수), sort(정렬 기준)를 파라미터로 받는다.

회차별로 로또 정보를 가져오는 것은 간단하게 JPA로 구현하였다.

 


Controller

@RestController
@RequestMapping("/api/lotto")
@RequiredArgsConstructor
public class LottoController extends CommonController {

    private final LottoService lottoService;

    @GetMapping("/list")
    public Page<LottoDto> getAllLotto(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(defaultValue = "round") String sortBy) {
        return lottoService.getLottoList(page, size, sortBy);
    }

    @GetMapping("/{round}")
    public ResponseEntity<LottoDto> getLottoByRound(@PathVariable Integer round) {
        LottoDto lottoDto = lottoService.getLottoByRound(round)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Lotto not found for round: " + round));
        return ResponseEntity.ok(lottoDto);
    }
}

Pagination을 할 Pagable 객체를 만들기 위해 값들을 입력 받는다. 이때 입력값이 들어오지 않을 경우를 대비하여 default 값도 설정해주었다. 

  • @RequestParam검색 조건 혹은 필터링과 같은 요청에 적합. default, required 등 세세한 설정 가능.
  • @PathVariable단일 리소스 식별과 같은 요청에 적합. 

 


결과 화면