개발하는 햄팡이

[ES, SpringBoot] 공지사항 게시판 검색 기능 구현하기 (2) : 데이터 저장 및 삭제 본문

Back-End/Spring

[ES, SpringBoot] 공지사항 게시판 검색 기능 구현하기 (2) : 데이터 저장 및 삭제

hampangee 2024. 11. 12. 13:16

 

저번에 이어 인덱스를 설계했으니 이제 CRUD차례.

우리 프로젝트는 U가 없기 때문에 CRD만 하면 되는데

일단 테스트용으로 필요한 api인 CD를 간단하게 사용하고 다음에 R을 구현할 것이다.

 

구현 전에 연결이 잘 되는지 확인하기 위해 서버 돌려보기는 필수

 

삽입, 삭제 서비스 테스트를 위해 만드는 컨트롤러라서

필요한 값들을 다른 서비스에서 받아온다고 생각하고 만든 컨트롤러이다.


 

(1) Controller

package com.jetty.ssafficebe.search.esnotice.controller;

import com.jetty.ssafficebe.common.payload.ApiResponse;
import com.jetty.ssafficebe.search.esnotice.payload.ESNoticeRequest;
import com.jetty.ssafficebe.search.esnotice.service.ESNoticeService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/search/notice")
@RequiredArgsConstructor
public class ESNoticeController {

    private final ESNoticeService esNoticeService;

    @PostMapping
    public ResponseEntity<ApiResponse> saveNotice(@RequestBody ESNoticeRequest request) {
        return ResponseEntity.ok(esNoticeService.saveNotice(request));
    }

    @DeleteMapping("/{noticeId}")
    public ResponseEntity<ApiResponse> deleteNotice(@PathVariable Long noticeId) {
        return ResponseEntity.ok(esNoticeService.deleteNotice(noticeId));
    }
}

 

 

우리 프로젝트는 딱히 결과를 보낼 필요가 없는 컨트롤러는 ApiResponse라는 payload 객체를 만들어 리턴하고 있다. 

ESNoticeRequest형태는 아래와 같다.

 

package com.jetty.ssafficebe.search.esnotice.payload;

import java.time.LocalDateTime;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class ESNoticeRequest {

    private Long noticeId;
    private String title;
    private String content;
    private LocalDateTime createdAt;
    private LocalDateTime startDateTime;
    private LocalDateTime endDateTime;
    private String isEssentialYn;
    private String noticeTypeCd;
    private Long createUserId;
    private String createUserEmail;
    private String createUserName;
    private String createUserProfileImgUrl;

}

 

서비스에서 넘겨주는 정보들을 다 필드에서 넣었다.

createUser와 관련된 부분은 따로 객체로 감쌀까 생각도 해봤는데,

현재 index에는 객체로 감싸지 않은 형태로 구성했다.

 

이유는 객체로 감싸면 nested 형태로 타입을 지정하는데 그렇게 되면

조회나 수정이 있을 때 text가 저장되듯이 nested 객체 또한 필드 하나하나 분해하여 저장하기 때문에 저장 삭제 수정 부분에서 비용이 좀 더 많이 들게 된다.

 

+ 조회시 noticeId, nested객체 아이디 총 두번 거쳐 조회를 시도하기 때문에 보다 비효율적이다.

 

 

(2) Repository

package com.jetty.ssafficebe.search.esnotice.repository;

import com.jetty.ssafficebe.search.esnotice.document.ESNotice;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

public interface ESNoticeRepository extends ElasticsearchRepository<ESNotice, Long> {

}

 

 

 

Repository를 만드는 방법은 JPA와 유사하다.

그냥 ElasticsearchRepository<인덱스명, 인덱스의 Id 타입>를 상속하면 된다.

 

ElasticsearchRepository가 CRUDRepository를 상속하기 때문에 메소드이름이나 규칙같은 게 JPA와 똑같다.

현재는 저장, 삭제만 하기 때문에 세세하게 건들 필요가 없어서 딱 저정도만 작성하고 넘어갈거다

 

(3) Service

package com.jetty.ssafficebe.search.esnotice.service;

import com.jetty.ssafficebe.common.payload.ApiResponse;
import com.jetty.ssafficebe.search.esnotice.payload.ESNoticeRequest;

public interface ESNoticeService {

    ApiResponse saveNotice(ESNoticeRequest request);

    ApiResponse deleteNotice(Long noticeId);
}

 

package com.jetty.ssafficebe.search.esnotice.service;

import com.jetty.ssafficebe.common.payload.ApiResponse;
import com.jetty.ssafficebe.search.esnotice.converter.ESNoticeConverter;
import com.jetty.ssafficebe.search.esnotice.document.ESNotice;
import com.jetty.ssafficebe.search.esnotice.payload.ESNoticeRequest;
import com.jetty.ssafficebe.search.esnotice.repository.ESNoticeRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@RequiredArgsConstructor
public class ESNoticeServiceImpl implements ESNoticeService {

    private final ESNoticeRepository esNoticeRepository;
    private final ESNoticeConverter esNoticeConverter;

    @Override
    public ApiResponse saveNotice(ESNoticeRequest request) {

        ESNotice esNotice = esNoticeConverter.toESNotice(request);
        ESNotice saved = esNoticeRepository.save(esNotice);

        return new ApiResponse(true, "ES에 추가 성공", saved.getNoticeId());
    }

    @Override
    public ApiResponse deleteNotice(Long noticeId) {
        esNoticeRepository.deleteById(noticeId);
        return new ApiResponse(true, "삭제 성공", noticeId);
    }
}

 

Service쪽은 뭐 딱히 설명할게 없고...

MapStruct 라이브러리를 사용하여 형변환을 해주는 Converter 코드를 자동으로 작성해주는 인터페이스를 사용중이다.

 

참고. ESNotice Converter

package com.jetty.ssafficebe.search.esnotice.converter;

import com.jetty.ssafficebe.search.esnotice.document.ESNotice;
import com.jetty.ssafficebe.search.esnotice.payload.ESNoticeRequest;
import org.mapstruct.Mapper;
import org.mapstruct.ReportingPolicy;

@Mapper(componentModel = "spring", unmappedSourcePolicy = ReportingPolicy.IGNORE, unmappedTargetPolicy = ReportingPolicy.WARN)
public interface ESNoticeConverter {

    ESNotice toESNotice(ESNoticeRequest request);

}

 

메소드 명명 규칙에 따라 자동으로 ConverterImpl class가 만들어지는데

이건 나중에 해당 라이브러리를 설명할 때 관련 블로그 글을 작성할 예정이다.

 


이제 다음부터는 본격 검색 시작...

 

그래도 예전에 막 이것저것 필터에 점수넣고 할 때보다 훨씬 쉬워서 검색도 바로 끝날 것 같다.

 

elasticsearch의 쿼리 중 각 항목별 점수를 주고,

그 점수를 계산하고,

단어 유사도 체크도 하고,

그 결과로 다른 필드에서 집계도 한 결과를 리턴하는 쿼리를 작성한 적도 있었는데

 

그거에 비하면 그냥 검색 구현은 너무 쉬워서 부담이 적다

 

얼른 끝내버려야지...