코드의 개선 사항이 필요해보인다면 언제든 댓글로 지적해주세요 :)
현재 개발하고자 하는 서비스는 흔히들 하는 상상인 '로또 당첨되면 뭐하지?'를 실제로 체험해볼 수 있는 로또 시뮬레이션 웹이다. 이에 필요한 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 : 단일 리소스 식별과 같은 요청에 적합.
결과 화면
'Dev Tool > Spring boot' 카테고리의 다른 글
[Spring] 로또 API 단위 테스트 코드 작성 (0) | 2025.01.23 |
---|---|
[Spring] 테스트 코드 기본 개념 (0) | 2025.01.22 |
[Spring] 로또 API 구현(2) & 예외 처리(@validate) (0) | 2025.01.22 |
[Spring] Swagger 연동 (1) | 2025.01.22 |
[Spring] 프로젝트 생성 기초 (1) | 2024.11.13 |