๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐Ÿ’ป ํ”„๋กœ์ ํŠธ/๐Ÿ‘‘ VIP ์ดˆ๋Œ€์žฅ ๐Ÿ’Œ

[Spring, React] Elasticsearch ์Šคํ”„๋ง ์ฝ”๋“œ ์ž‘์„ฑํ•˜๊ธฐ!

by hyeong._.ing 2026. 5. 3.

 

 

Elasticsearch๋ฅผ ์Šคํ”„๋ง์— ์‚ฌ์šฉํ•˜๋„๋ก
๊ธฐ์ดˆ ์ž‘์—…๊นŒ์ง€ ์™„๋ฃŒํ–ˆ๋‹ค.
์ด์ œ ์ฝ”๋“œ์— ์ง์ ‘ ์ž‘์„ฑํ•ด๋ณด์ž!

 

 

 

1. ๋™์ž‘ ํ™”๋ฉด

  • GIF

ํ‹ฐ์Šคํ† ๋ฆฌ ๋™์˜์ƒ ๊ธฐ๋Šฅ ๋ถ€ํ™œ์‹œ์ผœ์ค˜....

 

  • ์ด๋ฏธ์ง€

์ดˆ์„ฑ๋งŒ ์ ์—ˆ๋Š”๋ฐ
์จ”์ž”! ์ด๋ ‡๊ฒŒ ๊ฒ€์ƒ‰์ด ๋˜์—ˆ์Šต๋‹ˆ๋‹ค~

 

 

 


 

 

 

 

2. ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ ๊ณผ์ •

  • ํ”„๋กœ์ ํŠธ ์„ค๋ช…
    ๊ณ ๊ฐ ์ •๋ณด๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ํŽ˜์ด์ง€๋กœ ๊ณ ๊ฐ ์ •๋ณด๋ฅผ ์ถ”๊ฐ€ํ•  ๋•Œ ์ด๋ฆ„, ๋“ฑ๊ธ‰, ์—ฐ๋ฝ์ฒ˜, ์ดˆ๋Œ€์ฝ”๋“œ, ๋ฉ”๋ชจ๋ฅผ ์ ์„ ์ˆ˜ ์žˆ๋‹ค. 

 

  • (์˜ˆ์‹œ) ์‚ฌ์šฉ์ž๊ฐ€ ๋ณด๋‚ธ JSON ๋ฐ์ดํ„ฐ
{
  "name": "ํ™๊ธธ๋™",
  "phone": "010-1234-5678",
  "code": "VIP777"
}

 

  • CustomerSearchEntity
// MySQL์€ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•  ๋•Œ ์–ด๋–ค ํ…Œ์ด๋ธ”์— ๋„ฃ์„์ง€ ๊ฒฐ์ •ํ•œ๋‹ค.
// Elasticsearch์—์„œ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์ธ๋ฑ์Šค ๋‹จ์œ„๋กœ ๊ด€๋ฆฌํ•œ๋‹ค.
@Document(indexName = "customers")
@Getter
@Setter
public class CustomerSearchEntity {
    @Id
    private Long id;

    // FieldType.Text๋Š” ๋ฌธ์žฅ์ด๋‚˜ ๊ธด ํ…์ŠคํŠธ๋ฅผ ์ €์žฅํ•  ๋•Œ ์“ด๋‹ค.
    // analyzer = "nori"๋Š” ๊ณต์‹ ํ•œ๊ธ€ ํ˜•ํƒœ์†Œ ๋ถ„์„๊ธฐ์ด๋‹ค.
    // ํ™๊ธธ๋™ ์ด๋ฆ„์„ ์ €์žฅํ•˜๋ฉด [ํ™,๊ธธ,๋™] ํ˜น์€ [ํ™๊ธธ๋™] ์ด๋Ÿฐ์‹์œผ๋กœ ์ €์žฅ๋˜๋„๋ก ํ•œ๋‹ค.
    @Field(type = FieldType.Text, analyzer = "nori")
    private String name;

    // FieldType.Keywords๋Š” ์ •ํ™•ํžˆ ์ผ์น˜ํ•ด์•ผํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•œ๋‹ค.
    // ์ „ํ™”๋ฒˆํ˜ธ๋‚˜ ๊ณ ๊ฐ ์ฝ”๋“œ์ฒ˜๋Ÿผ ์ˆซ์ž๋กœ ์ด๋ค„์ง„ ๊ฒฝ์šฐ Keyword๋ฅผ ์“ด๋‹ค.
    // ์˜์–ด๋„ keyword๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.
    @Field(type = FieldType.Keyword)
    private String grade;

    @Field(type = FieldType.Keyword)
    private String phone;

    @Field(type = FieldType.Keyword)
    private String code;

    @Field(type = FieldType.Text, analyzer = "nori")
    private String note;

    @Field(type = FieldType.Text)
    private String nameChosung;
}
ํ”„๋กœ์ ํŠธ์— CustomerEntity์™€ CustomerSearchEntity๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค. CustomerSearchEntity๋Š” Elasticsearch ๊ฒ€์ƒ‰ ์—”์ง„ ์ „์šฉ ์ €์žฅ์†Œ์— ๋ฐ์ดํ„ฐ๋ฅผ ๋„ฃ์„ ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ์—”ํ‹ฐํ‹ฐ์ด๋‹ค. ๋‘๊ฐ€์ง€ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๋งŒ๋“  ์ด์œ ๋Š” CustomerEntity์€ ์ง„์งœ ์›๋ณธ์ธ MySQL์šฉ ์—”ํ‹ฐํ‹ฐ๋กœ ์›๋ณธ์€ ์•ˆ์ „ํ•˜๊ฒŒ ๋ณด๊ด€ํ•˜๋Š” ์—ญํ• ์„ ํ•œ๋‹ค. 

 

  • CustomerSearchRepository
// Spring Data๊ฐ€ ์ œ๊ณตํ•˜๋Š” ํŠน์ˆ˜ ์ธํ„ฐํŽ˜์ด์Šค์ธ ElasticsearchRepository
// ์ด๊ฑธ ์ƒ์†ํ•˜๋ฉด save, delete, findById ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ๋ฐ”๋กœ ์“ธ ์ˆ˜ ์žˆ๋‹ค.
// CustomerSearchEntity๋Š” ๊ฒ€์ƒ‰ ๋Œ€์ƒ์ด ๋˜๋Š” ๋ฐ์ดํ„ฐ ํƒ€์ž…์ด๊ณ  ๊ทธ ๋ฐ์ดํ„ฐ์˜ ID(Long)์„ ์ง€์ •ํ–ˆ๋‹ค.
public interface CustomerSearchRepository extends ElasticsearchRepository<CustomerSearchEntity, Long> {
   
   // @Query๋Š” Elasticsearch์— ๋ณด๋‚ด๋Š” ์งˆ๋ฌธ์ง€์ด๋‹ค.
    // ์กฐ๊ฑด์„ ์กฐํ•ฉํ•˜๊ธฐ ์œ„ํ•ด bool์€ ์‚ฌ์šฉํ–ˆ๋‹ค. shold๋Š” or ์—ฐ์‚ฐ์ž์™€ ๋น„์Šทํ•˜๋‹ค.
    // ๋‚˜์—ด๋œ ์กฐ๊ฑด๋“ค ์ค‘ ํ•˜๋‚˜๋ผ๋„ ๋งŒ์กฑํ•˜๋ฉด ๊ฒฐ๊ณผ์— ํฌํ•จํ•ด๋‹ฌ๋ผ๋Š” ๋ง์ด๋‹ค.
    
    // ๊ฒ€์ƒ‰ ๋ฐฉ๋ฒ•์€ match์™€ wildcard๊ฐ€ ์žˆ๋‹ค.
    // match๋Š” ํ…์ŠคํŠธ ๊ฒ€์ƒ‰(์ด๋ฆ„, ๋ฉ”๋ชจ)์— ๋” ์ ํ•ฉํ•˜๋‹ค. ํ˜•ํƒœ์†Œ ๋ถ„์„๊ธฐ๋กœ ํ™๊ธธ๋™์ด ์žˆ์œผ๋ฉด ๊ทธ๊ฑธ ์ฐพ์•„๋‚ธ๋‹ค.
    // wildcard๋Š” ๋ถ€๋ถ„ ์ผ์น˜ ๊ฒ€์ƒ‰์„ ํ•œ๋‹ค. ๊ทธ๋ž˜์„œ ๋“ฑ๊ธ‰ ์ „ํ™”๋ฒˆํ˜ธ ์ฝ”๋“œ ์ดˆ์„ฑ์— ๋” ์ ํ•ฉํ•˜๋‹ค.
    
    // *?0*์€ ์•ž๋’ค์— ์–ด๋–ค ๊ธ€์ž๊ฐ€ ์™€๋„ ์ƒ๊ด€์—†์œผ๋‹ˆ ํ‚ค์›Œ๋“œ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์œผ๋ฉด ์ฐพ์œผ๋ผ๋Š” ๊ฒƒ์ด๋‹ค.
    // ๊ทธ๋Ÿฌ๋‹ˆ๊นŒ ์ˆซ์ž์˜ ์ผ๋ถ€๋ถ„, ์ „ํ™”๋ฒˆํ˜ธ์ด๋ฉด ๋’ท์ž๋ฆฌ๋งŒ ๊ฒ€์ƒ‰ํ•ด๋„ ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜ํƒ€๋‚œ๋‹ค.
    
    // should๋Š” ๊ฐ„ํ˜น ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ๋„“๊ฒŒ ํ•œ๋‹ค. 
    // ๊ทธ๊ฑธ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์ตœ์†Œํ•œ์˜ ์ •ํ™•๋„๋ฅผ ํ™•๋ณดํ•˜๊ธฐ ์œ„ํ•ด์„œ minimum_should_match:1์„ ์ ์—ˆ๋‹ค.
    // ๋‚˜์—ด๋œ ์กฐ๊ฑด๋“ค ์ค‘์—์„œ ์ ์–ด๋„ 1๊ฐœ ์ด์ƒ ๋งŒ์กฑํ•ด์•ผํ•œ๋‹ค๋Š” ์˜๋ฏธ์ด๋‹ค.
    @Query("""
    {
      "bool": {
        "should": [
          { "match": { "name": { "query": "?0" } } },
          { "match": { "note": { "query": "?0" } } },

          { "wildcard": { "grade": { "value": "*?0*" } } },
          { "wildcard": { "phone": { "value": "*?0*" } } },
          { "wildcard": { "code":  { "value": "*?0*" } } },
          { "wildcard": { "nameChosung": { "value": "*?0*" } } }
        ],
        "minimum_should_match": 1
      }
    }
    """)
    // String keyword๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๊ฒ€์ƒ‰์ฐฝ์— ์ž…๋ ฅํ•œ ๋‹จ์–ด์ด๋‹ค.
    List<CustomerSearchEntity> searchAll(String keyword);

    // ์ด ์ฝ”๋“œ์— ๋Œ€ํ•ด์„œ ์งง๊ฒŒ ๋‹ค์‹œ ์ •๋ฆฌํ•˜์ž๋ฉด
    // ์‚ฌ์šฉ์ž๊ฐ€ ๊ณต์ฃผ๋ฅผ ๊ฒ€์ƒ‰ํ–ˆ์œผ๋ฉด '๊ณต์ฃผ'๊ธฐ ํฌํ•จ๋œ ๋ชจ๋“  ๋ฆฌ์ŠคํŠธ๋ฅผ ๋‹ค ๋“ค๊ณ ์˜ค๋„๋ก ํ•˜๋Š” ๊ฒƒ์ด๋‹ค.
}

 

  • CustomerController 
    @PostMapping
    @PreAuthorize("hasAnyAuthority('ROLE_SUPER_ADMIN', 'CUSTOMER_ADD')")
    public Customer saveCustomer(@RequestBody Customer customer) {
        return customerService.save(customer);
    }
@PostMapping ๋ฉ”์„œ๋“œ๊ฐ€ ์‹คํ–‰๋˜๋ฉฐ ์‚ฌ์šฉ์ž๊ฐ€ ๋ณด๋‚ธ JSON ๋ฐ์ดํ„ฐ๋ฅผ Customer ๊ฐ์ฒด๋กœ ๋ฐ›์Šต๋‹ˆ๋‹ค. ์ด ๋ถ€๋ถ„ ์ฝ”๋“œ๊ฐ€ (@RequestBody Customer customer)์ด๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ์ปจํŠธ๋กค๋Ÿฌ์—์„œ customerService์— ์ ์–ด๋‘” save ๋ฉ”์„œ๋“œ๋กœ customer ๊ฐ์ฒด๋ฅผ ๋ณด๋‚ธ๋‹ค. ์ฐธ๊ณ ๋กœ @PreAuthorize๋Š” ๊ด€๋ฆฌ์ž์˜ ์—ญํ• ์— ๋”ฐ๋ผ ๊ถŒํ•œ์„ ๋ถ€์—ฌํ•œ ์ฝ”๋“œ๋กœ elasticsearch์™€ ๋ฌด๊ด€ํ•˜๋‹ค.

 

  • CustomerService
    // MySQL ์ €์žฅ์—๋Š” ์„ฑ๊ณตํ–ˆ๋Š”๋ฐ Elasticsearch ์ €์žฅ์—์„œ ์—๋Ÿฌ๊ฐ€ ๋‚˜๋ฉด
    // MySQL ์ €์žฅ๋„ ์—†๋˜ ์ผ๋กœ ๋˜๋Œ๋ฆฐ๋‹ค. (Rollback)
    // ์–‘์ชฝ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ถˆ์ผ์น˜ํ•˜๋Š” ์ƒํ™ฉ์„ ๋ฐฉ์ง€ํ•˜๊ณ ์ž ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ–ˆ๋‹ค.
    @Transactional
    public Customer save(Customer customer) {

        // customer ๊ฐ์ฒด์— ํฌํ•จ๋˜์–ด์žˆ๋Š” code๋ฅผ ๊บผ๋‚ด์„œ code์— ๋„ฃ๋Š”๋‹ค.
        String code = customer.getCode();

        // ๊ทธ๋ฆฌ๊ณ  ์ฝ”๋“œ๊ฐ€ ์ค‘๋ณต๋˜์—ˆ๋Š”์ง€ ์ฒดํฌํ•œ๋‹ค.
        if (code != null && customerRepository.existsByCode(code)) {
            throw new DuplicateCodeException("์ดˆ๋Œ€์ฝ”๋“œ๊ฐ€ ์ค‘๋ณต๋˜์—ˆ์Šต๋‹ˆ๋‹ค.");
        }

        // customerRepository์—๋„ ์ €์žฅ๋œ๋‹ค.
        // customerRepository์—.save()๊ฐ€ ์‹คํ–‰๋˜๋ฉด DB์— ์ €์žฅ๋œ ์‹ค์ œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐ˜ํ™˜๋˜๋Š”๋ฐ
        // ๊ทธ๊ฑธ saved๋ผ๋Š” ์ด๋ฆ„ํ‘œ๋ฅผ ๋ถ™์—ฌ์„œ ๋ณด๊ด€ํ•œ๋‹ค.
        Customer saved = customerRepository.save(customer);

        // elasticsearch ๊ฒ€์ƒ‰์„ ์œ„ํ•ด์„œ ๊ฒ€์ƒ‰ ์—”์ง„์—๋„ ๋”ฐ๋กœ ์ €์žฅํ•œ๋‹ค.
        CustomerSearchEntity ela = new CustomerSearchEntity();
        // ๋ณ€์ˆ˜ saved์—์„œ ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๊บผ๋‚ด์„œ ela์— ๋„ฃ๋Š”๋‹ค.
        ela.setId(saved.getId());
        ela.setName(saved.getName());
        ela.setGrade(saved.getGrade());
        ela.setPhone(saved.getPhone());
        ela.setCode(saved.getCode());
        ela.setNote(saved.getNote());
        // ์ดˆ์„ฑ๋„ ์ €์žฅํ•œ๋‹ค. 
        // ๊ทธ๊ฑด ๋ฐ”๋กœ ๋ฐ‘์— ์ฝ”๋“œ์—์„œ ์„ค๋ช…!
        ela.setNameChosung(getChosung(saved.getName()));

        customerSearchRepository.save(ela);

        // ๋ชจ๋“  ์ž‘์—…์ด ๋๋‚˜๋ฉด ์ตœ์ข…์ ์œผ๋กœ saved๋ฅผ ์ปจํŠธ๋กค๋Ÿฌ์—๊ฒŒ ๋Œ๋ ค์ค€๋‹ค.
        return saved;
    }
    // elasticsearch๋กœ ์ดˆ์„ฑ ๊ฒ€์ƒ‰์œผ๋กœ๋„ ์ฐพ๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด์„œ
    // ํ•œ๊ธ€์˜ ์œ ๋‹ˆ์ฝ”๋“œ ๊ทœ์น™์„ ์ด์šฉํ•ด์„œ ์ž์Œ์„ ๋ถ„๋ฆฌํ–ˆ๋‹ค.
    private String getChosung(String text) {
        if (text == null) return "";

        String[] CHO = {
                "ใ„ฑ","ใ„ฒ","ใ„ด","ใ„ท","ใ„ธ","ใ„น","ใ…","ใ…‚","ใ…ƒ","ใ……",
                "ใ…†","ใ…‡","ใ…ˆ","ใ…‰","ใ…Š","ใ…‹","ใ…Œ","ใ…","ใ…Ž"
        };

        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < text.length(); i++) {
            char c = text.charAt(i);

            if (c >= '๊ฐ€' && c <= 'ํžฃ') {
                int uniVal = c - 0xAC00;
                int choIdx = uniVal / 588;
                sb.append(CHO[choIdx]);
            } else if (c != ' ') {
                sb.append(c);
            }
        }
        return sb.toString();
    }

 

Q. getChosung๊ณผ nori์˜ ์ฐจ์ด์ ์€?

A. getChosung์€ ๋ง ๊ทธ๋Œ€๋กœ ์ดˆ์„ฑ์„ ์ €์žฅํ•œ๋‹ค. ํ™๊ธธ๋™์ด๋ž€ ๋ฐ์ดํ„ฐ๊ฐ€ ๋“ค์–ด์˜ค๋ฉด ใ…Žใ„ฑใ„ท๋ฅผ ์ €์žฅํ•˜๋Š” ๊ฒƒ์ด๋‹ค. name ํ•„๋“œ์—์„  ๋ถ„์„๊ธฐ nori๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค. nori๋Š” ํ™๊ธธ๋™์„ ์ชผ๊ฐ ๋‹ค. ํ™,๊ธธ,๋™ ํ˜น์€ ํ™๊ธธ,๊ธธ๋™, ์ด๋Ÿฐ์‹์œผ๋กœ ๋ง์ด๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ๊ฒ€์ƒ‰ํ•˜๋ฉด ์ดˆ์„ฑ ํ˜น์€ ์ผ๋ถ€๋ถ„๋งŒ ๊ฒ€์ƒ‰ํ•ด๋„ ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜ํƒ€๋‚˜๋„๋ก ํ–ˆ๋‹ค.

 

 

 


 

 

 

 

3. ๋ฐ์ดํ„ฐ ๊ฒ€์ƒ‰ ๊ณผ์ •

  • (์˜ˆ์‹œ) ์‚ฌ์šฉ์ž๊ฐ€ ๋ณด๋‚ธ ๊ฒ€์ƒ‰ ์š”์ฒญ
GET /api/customers/search?keyword=ใ…Žใ„ฑใ„ท

 

  • CustomerController
    @GetMapping("/search")
    // ๊ถŒํ•œ ๊ฒ€์‚ฌ
    @PreAuthorize("hasAnyAuthority('ROLE_SUPER_ADMIN', 'CUSTOMER_SEARCH')")
    // ์—ฌ๋Ÿฌ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ๋ฐ˜ํ™˜๋  ์ˆ˜ ์žˆ์œผ๋‹ˆ List๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
    // @RequestParam String keyword๋Š” ์š”์ฒญ์— ๋ถ™์€ keyword=ใ…Žใ„ฑใ„ท ๋ถ€๋ถ„์ด๋‹ค.
    // Spring์ด ์ž๋™์œผ๋กœ keyword="ใ…Žใ„ฑใ„ท" ์ด๋ผ๊ณ  ๋„ฃ๋Š”๋‹ค.
    public List<CustomerSearchEntity> searchCustomer(@RequestParam String keyword) {
    
        // keyword๋ฅผ ๋“ค๊ณ ๊ฐ€์„œ ์‹ค์ œ ๊ฒ€์ƒ‰์ด ์‹คํ–‰๋œ๋‹ค.
        return customerSearchRepository.searchAll(keyword);
    }

 

  • CustomerSearchRepository
{
  "bool": {
    "should": [
      { "match": { "name": { "query": "?0" } } },
      { "match": { "note": { "query": "?0" } } },

      { "wildcard": { "grade": { "value": "*?0*" } } },
      { "wildcard": { "phone": { "value": "*?0*" } } },
      { "wildcard": { "code":  { "value": "*?0*" } } },
      { "wildcard": { "nameChosung": { "value": "*?0*" } } }
    ],
    "minimum_should_match": 1
  }
}
?0์— ใ…Žใ„ฑใ„ท์ด ๋“ค์–ด๊ฐ„๋‹ค. ์‰ฝ๊ฒŒ ์„ค๋ช…ํ•˜๊ธฐ ์œ„ํ•ด ์•„๋ž˜ ์ฝ”๋“œ์ฒ˜๋Ÿผ ์ž‘์„ฑํ–ˆ๋‹ค.
{
  "bool": {
    "should": [
      { "match": { "name": { "query": "ใ…Žใ„ฑใ„ท" } } },
      { "match": { "note": { "query": "ใ…Žใ„ฑใ„ท" } } },

      { "wildcard": { "grade": { "value": "*ใ…Žใ„ฑใ„ท*" } } },
      { "wildcard": { "phone": { "value": "*ใ…Žใ„ฑใ„ท*" } } },
      { "wildcard": { "code":  { "value": "*ใ…Žใ„ฑใ„ท*" } } },
      { "wildcard": { "nameChosung": { "value": "*ใ…Žใ„ฑใ„ท*" } } }
    ],
    "minimum_should_match": 1
  }
}

 

  • CustomerService
ela.setNameChosung(getChosung(saved.getName()));
CustomerService์— ์œ„์˜ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ–ˆ๋‹ค. ๊ทธ๋ž˜์„œ ํ™๊ธธ๋™์„ ์ €์žฅํ–ˆ์„ ๋•Œ ์•„๋ž˜์™€ ๊ฐ™์ด ์ €์žฅ๋์„๊ฑฐ๋‹ค.
{
  "id": 1,
  "name": "ํ™๊ธธ๋™",
  "grade": "VIP",
  "phone": "010-1234-5678",
  "code": "1234",
  "note": "์ค‘์š” ๊ณ ๊ฐ",
  "nameChosung": "ใ…Žใ„ฑใ„ท"
}
์ด๋ ‡๊ฒŒ ์ดˆ์„ฑ์„ ๋ฏธ๋ฆฌ ์ €์žฅํ•ด๋‘ฌ์„œ ๊ฒ€์ƒ‰์„ ํ•˜๋ฉด ์ฐพ์„ ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ด๋‹ค.

 

 

 


 

 

 

 

4. MySQL์„ ๊ธฐ์ค€์œผ๋กœ Elasticsearch ๊ฒ€์ƒ‰ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์‹œ ๋งž์ถ”๊ธฐ

  • CustomerService
    // MySQL์— ์ €์žฅ๋œ ๋ชจ๋“  ๊ณ ๊ฐ ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ธ์–ด์™€์„œ ํ•˜๋‚˜์”ฉ Elasticsearch์— ๋„ฃ๋Š”๋‹ค.
    // elasticsearch๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์ „์— DB์— ์ €์žฅ๋˜์–ด ์žˆ๋Š”๊ฑธ ๊ฐ€์ ธ์˜ค๋А๋ผ ์ ์—ˆ๋˜ ์ฝ”๋“œ์ธ๋ฐ,
    // ๋™๊ธฐํ™” ์˜ค๋ฅ˜ ๋“ฑ์œผ๋กœ DB์™€ ๊ฒ€์ƒ‰์—”์ง„์ด ์•ˆ๋งž๋Š” ๊ฑธ ๋Œ€๋น„ํ•ด์„œ ํ•ด๋‹น ์ฝ”๋“œ๋ฅผ ๋†”๋’€๋‹ค.
    @Transactional
    public void syncAllToElasticsearch() {
        //customerRepository์— ์žˆ๋Š” ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ๋“ค์–ด์™€์„œ list์— ๋„ฃ๋Š”๋‹ค.
        List<Customer> list = customerRepository.findAll();

        // list์— ์žˆ๋Š” ํ•ญ๋ชฉ์„ ๋Œ๋ฉด์„œ customerSearchRepository์— ์ €์žฅํ•œ๋‹ค.
        // ์—ฌ๊ธฐ์„œ customerSearchRepository๋Š” elasticsearch๋ฅผ ์œ„ํ•ด ๋งŒ๋“  ๊ฒ€์ƒ‰์—”์ง„์ด๋‹ค.
        for (Customer c : list) {
            CustomerSearchEntity e = new CustomerSearchEntity();
            e.setId(c.getId());
            e.setName(c.getName());
            e.setGrade(c.getGrade());
            e.setPhone(c.getPhone());
            e.setCode(c.getCode());
            e.setNote(c.getNote());
            e.setNameChosung(getChosung(c.getName()));

            customerSearchRepository.save(e);
        }
    }

 

 

 


 

 

5. ์ „์ฒด ์ฝ”๋“œ

  • CustomerSearchEntity
@Document(indexName = "customers")
@Getter
@Setter
public class CustomerSearchEntity {
    @Id
    private Long id;

    @Field(type = FieldType.Text, analyzer = "nori")
    private String name;

    @Field(type = FieldType.Keyword)
    private String grade;

    @Field(type = FieldType.Keyword)
    private String phone;

    @Field(type = FieldType.Keyword)
    private String code;

    @Field(type = FieldType.Text, analyzer = "nori")
    private String note;

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

 

  • CustomerSearchRepository
public interface CustomerSearchRepository extends ElasticsearchRepository<CustomerSearchEntity, Long> {
    @Query("""
    {
      "bool": {
        "should": [
          { "match": { "name": { "query": "?0" } } },
          { "match": { "note": { "query": "?0" } } },

          { "wildcard": { "grade": { "value": "*?0*" } } },
          { "wildcard": { "phone": { "value": "*?0*" } } },
          { "wildcard": { "code":  { "value": "*?0*" } } },
          { "wildcard": { "nameChosung": { "value": "*?0*" } } }
        ],
        "minimum_should_match": 1
      }
    }
    """)

    List<CustomerSearchEntity> searchAll(String keyword);
}

 

  • CustomerController
@RestController
@RequestMapping("/api/customers")
@RequiredArgsConstructor
public class CustomerController {

    private final CustomerService customerService;
    private final CustomerSearchRepository customerSearchRepository;

    @GetMapping
    @PreAuthorize("hasAnyAuthority('ROLE_SUPER_ADMIN', 'CUSTOMER_READ')")
    public List<Customer> effectCustomers() {
        return customerService.findAllCustomers();
    }

    @PostMapping
    @PreAuthorize("hasAnyAuthority('ROLE_SUPER_ADMIN', 'CUSTOMER_ADD')")
    public Customer saveCustomer(@RequestBody Customer customer) {
        return customerService.save(customer);
    }

    @PutMapping("/{id}")
    @PreAuthorize("hasAnyAuthority('ROLE_SUPER_ADMIN', 'CUSTOMER_EDIT')")
    public ResponseEntity<Customer> updateCustomer(@PathVariable Long id, @RequestBody Customer data) {
        if (!customerService.exists(id)) {
            return ResponseEntity.notFound().build();
        }
        Customer updated = customerService.update(id, data);
        return ResponseEntity.ok(updated);
    }

    @DeleteMapping("/{id}")
    @PreAuthorize("hasAnyAuthority('ROLE_SUPER_ADMIN', 'CUSTOMER_DELETE')")
    public ResponseEntity<Void> deleteCustomer(@PathVariable Long id) {
        if (!customerService.exists(id)) {
            return ResponseEntity.notFound().build();
        }
        customerService.delete(id);
        return ResponseEntity.ok().build();
    }

    @GetMapping("/search")
    @PreAuthorize("hasAnyAuthority('ROLE_SUPER_ADMIN', 'CUSTOMER_SEARCH')")
    public List<CustomerSearchEntity> searchCustomer(@RequestParam String keyword) {
        return customerSearchRepository.searchAll(keyword);
    }

    @PostMapping("/sync")
    @PreAuthorize("hasRole('SUPER_ADMIN')")
    public ResponseEntity<String> sync() {
        customerService.syncAllToElasticsearch();
        return ResponseEntity.ok("sync ok");
    }
}

 

  • CustomerService
@Service
public class CustomerService {

    private CustomerRepository customerRepository;
    private final CustomerSearchRepository customerSearchRepository;

    @Autowired
    public CustomerService(CustomerRepository customerRepository,  CustomerSearchRepository customerSearchRepository) {
        this.customerRepository = customerRepository;
        this.customerSearchRepository = customerSearchRepository;
    }

    public List<Customer> findAllCustomers() {
        return customerRepository.findAll();
    }

    public boolean exists(Long id) {
        return customerRepository.existsById(id);
    }

    public boolean isCodeDuplicatedForCreate(String code) {
        if (code == null || code.isBlank())
            return false;
        return customerRepository.existsByCode(code);
    }


    @Transactional
    public Customer save(Customer customer) {

        String code = customer.getCode();

        if (code != null && customerRepository.existsByCode(code)) {
            throw new DuplicateCodeException("์ดˆ๋Œ€์ฝ”๋“œ๊ฐ€ ์ค‘๋ณต๋˜์—ˆ์Šต๋‹ˆ๋‹ค.");
        }

        Customer saved = customerRepository.save(customer);

        CustomerSearchEntity ela = new CustomerSearchEntity();
        ela.setId(saved.getId());
        ela.setName(saved.getName());
        ela.setGrade(saved.getGrade());
        ela.setPhone(saved.getPhone());
        ela.setCode(saved.getCode());
        ela.setNote(saved.getNote());
        ela.setNameChosung(getChosung(saved.getName()));

        customerSearchRepository.save(ela);

        return saved;
    }


    @Transactional
    public Customer update(Long id, Customer updateData) {
        Customer customer = customerRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("๊ณ ๊ฐ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."));

        String newCode = updateData.getCode();

        if (newCode != null) {
            customerRepository.findByCode(newCode).ifPresent(found -> {
                if (!found.getId().equals(id)) {
                    throw new DuplicateCodeException("์ดˆ๋Œ€์ฝ”๋“œ๊ฐ€ ์ค‘๋ณต๋˜์—ˆ์Šต๋‹ˆ๋‹ค.");
                }
            });
        }

        customer.setName(updateData.getName());
        customer.setGrade(updateData.getGrade());
        customer.setPhone(updateData.getPhone());
        customer.setCode(updateData.getCode());
        customer.setNote(updateData.getNote());

        CustomerSearchEntity ela = new CustomerSearchEntity();
        ela.setId(customer.getId());
        ela.setName(customer.getName());
        ela.setGrade(customer.getGrade());
        ela.setPhone(customer.getPhone());
        ela.setCode(customer.getCode());
        ela.setNote(customer.getNote());
        ela.setNameChosung(getChosung(customer.getName()));

        customerSearchRepository.save(ela);

        return customer;
    }

    @Transactional
    public void delete(Long id) {
        customerRepository.deleteById(id);
        customerSearchRepository.deleteById(id);
    }

    @Transactional
    public void syncAllToElasticsearch() {
        List<Customer> list = customerRepository.findAll();

        for (Customer c : list) {
            CustomerSearchEntity e = new CustomerSearchEntity();
            e.setId(c.getId());
            e.setName(c.getName());
            e.setGrade(c.getGrade());
            e.setPhone(c.getPhone());
            e.setCode(c.getCode());
            e.setNote(c.getNote());
            e.setNameChosung(getChosung(c.getName()));

            customerSearchRepository.save(e);
        }
    }

    private String getChosung(String text) {
        if (text == null) return "";

        String[] CHO = {
                "ใ„ฑ","ใ„ฒ","ใ„ด","ใ„ท","ใ„ธ","ใ„น","ใ…","ใ…‚","ใ…ƒ","ใ……",
                "ใ…†","ใ…‡","ใ…ˆ","ใ…‰","ใ…Š","ใ…‹","ใ…Œ","ใ…","ใ…Ž"
        };

        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < text.length(); i++) {
            char c = text.charAt(i);

            if (c >= '๊ฐ€' && c <= 'ํžฃ') {
                int uniVal = c - 0xAC00;
                int choIdx = uniVal / 588;
                sb.append(CHO[choIdx]);
            } else if (c != ' ') {
                sb.append(c);
            }
        }
        return sb.toString();
    }

}