[spring] Spring 에서 Elastic Search 활용 (1) 글의 리팩토링을 다루는 포스팅입니다.
Spring Boot 3.0.6
Spring Core 6.0.8
Spring Data Elasticsearch 4.2.2
Elastic Search 7.10.2
implementation 'org.springframework.data:spring-data-elasticsearch:4.2.2'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
@Builder
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Document(indexName = "members")
public class MemberDocument {
@Id
private Long id;
private String nickname;
private String profileUrl;
...
}
@Repository
public interface MemberSearchRepository extends ElasticsearchRepository<MemberDocument, Long> {
Optional<MemberDocument> findById(Long id);
...
}
@Service
@RequiredArgsConstructor
public class MemberSearchService {
private final MemberSearchRepository memberSearchRepository;
private final ElasticsearchOperations elasticsearchOperations;
public void save(Member member) {
elasticsearchOperations.save(MemberDocument.from(member));
}
public void update(Member member) {
MemberDocument document = memberSearchRepository.findById(member.getId())
.orElseThrow(RuntimeException::new);
document.updateProfileUrl(member.getProfilePath());
Document updateDocument = elasticsearchOperations.getElasticsearchConverter().mapObject(document);
elasticsearchOperations.update(UpdateQuery.builder(document.getId().toString())
.withDocument(updateDocument)
.withDocAsUpsert(true)
.build(), IndexCoordinates.of("members"));
}
💡 위 코드의 개선점
- Service 단에서 쿼리를 생성하고 날리는 역할까지 담당한다.
- 코드 가독성의 문제
- 책임이 명확히 분리되지 않음
➡️ 좀 더 객체지향적으로 개선해보자- 업데이트 과정
MemberDocument
객체는elasticsearch.core.document.Document
로 변환된다.Document
와 도큐먼트 아이디, 도큐먼트에 설정해준 indexName을 토대로 쿼리를 생성한다.- elasticsearch에 명령어를 날린다.
ElasticsearchRepository
의 도메인은 공통으로 가질 BaseDocument
를 만든다.
ElasticsearchRepository
의 도메인이어야 이용할 수 있는 메서드를 만들기 위함이다.
@SuperBuilder
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class BaseDocument {
@Id
private Long id;
}
ElasticsearchRepository
의 도메인이 BaseDocument
를 상속하도록 한다.
@SuperBuilder
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Document(indexName = "members")
public class MemberDocument extends BaseDocument {
private String nickname;
private String profileUrl;
}
@SuperBuilder
는 부모 객체가 가진 필드까지 빌더 패턴으로 객체를 생성할 수 있게 하는 어노테이션이다. 이때, 부모 객체에도 @SuperBuilder
어노테이션을 달아주어야 한다.@Builder
로는 BaseDocument
의 id 필드가 빌드되지 않음ElasticSearchClient
는 ElasticsearchRepository
의 도메인 객체를 받아 쿼리를 생성한다.
@Component
@RequiredArgsConstructor
public class ElasticSearchClient<T extends BaseDocument> {
private final ElasticsearchOperations elasticsearchOperations;
public void save(T document) {
elasticsearchOperations.save(document);
}
public void update(T document) {
Document esDocument = elasticsearchOperations.getElasticsearchConverter().mapObject(document);
UpdateQuery updateQuery = UpdateQuery.builder(esDocument.getId())
.withDocument(esDocument)
.withDocAsUpsert(true)
.build();
elasticsearchOperations.update(updateQuery, IndexCoordinates.of(esDocument.getIndex()));
}
}
ElasticsearchRepository
의 도메인만으로 한정한다.ElasticSearchClient
내에서 이뤄진다.elasticsearch.core.document.Document
객체에서 index와 id를 추출할 수 있어 수정했다.@Service
@RequiredArgsConstructor
public class MemberSearchService {
private final MemberSearchRepository memberSearchRepository;
private final ElasticSearchClient elasticSearchClient;
public void save(Member member) {
elasticSearchClient.save(MemberDocument.from(member));
}
public void update(Member member) {
MemberDocument memberDocument = memberSearchRepository.findById(member.getId())
.orElseThrow(RuntimeException::new);
memberDocument.updateProfileUrl(member.getProfilePath());
elasticSearchClient.update(memberDocument);
}
...
}
책임을 분리하며 코드를 작성하면서, 훨씬 비즈니스 로직이 간결해졌다.
기존 코드는 내가 봐도 한참을 살펴봐야 했다면, 리팩토링한 로직은 한눈에 무슨 의미인지 들어온다.
또한 ElasticSearch 관련 정책이 바뀌어도 ElasticSearchClient
수정하면 된다는 것이 큰 장점이다. 기존에는 서비스 로직을 하나하나 확인하며 수정해야 했을 것이다.