반응형
반응형

안녕하세요. 여행하는 개발자입니다.
이번 포스팅에서는 Elasticsearch에서 사용하는 기본필터 4가지 에 대해 알아보겠습니다.

이 필터들은 텍스트를 분석하고, 토큰을 변환하는 데 중요한 역할을 합니다. 아래에서 각 필터의 역할과 예시를 확인해봅시다.

1. lowercase 필터

lowercase 필터는 텍스트 내의 모든 문자를 소문자로 변환하는 필터입니다.

이 필터는 대소문자를 구별하지 않고 검색을 할 수 있도록 도와줍니다.

일반적으로 검색의 일관성을 높이고, 사용자가 대소문자를 신경쓰지 않아도 되도록 하는 데 유용합니다.

 

✅ 예시

입력 텍스트: Apple 2025 맥북 에어

변환 후: apple 2025 맥북 에어

PUT /products/_settings
{
  "analysis": {
    "filter": {
      "lowercase_filter": {
        "type": "lowercase"
      }
    }
  }
}

 

위 설정을 통해 Elasticsearch는 대소문자 구분 없이 "Apple"을 "apple"로 저장하고, 검색 시에도 일관된 결과를 제공합니다.

 

2. html_strip 필터

html_strip 필터는 HTML 태그를 제거하는 필터입니다. 이 필터는 HTML 콘텐츠가 포함된 텍스트에서 불필요한 HTML 태그를 제거하여 순수한 텍스트만을 분석할 수 있게 도와줍니다. 웹 크롤러에서 수집한 데이터나 HTML 콘텐츠를 처리할 때 유용합니다.

 

✅ 예시

입력 텍스트: <h1>아이폰 15 사용 후기</h1>

변환 후: 아이폰 15 사용 후기

PUT /products/_settings
{
  "analysis": {
    "filter": {
      "html_strip_filter": {
        "type": "html_strip"
      }
    }
  }
}

 

HTML 태그를 제거하여 텍스트만을 추출할 수 있게 됩니다. 위 예시에서는 <h1></h1> 태그가 제거됩니다.

 

3. stop 필터

stop 필터는 검색에 의미가 없는 불용어(stop words)를 제거하는 필터입니다.

불용어란 "the", "is", "in" 등의 일반적인 단어로, 이들 단어는 검색에서 제외되어 텍스트 분석에 방해가 될 수 있습니다.

이 필터는 이러한 단어들을 분석에서 제외하여 성능을 최적화합니다.

 

✅ 예시

입력 텍스트: The quick brown fox jumps over the lazy dog

변환 후: quick brown fox jumps lazy dog

PUT /products/_settings
{
  "analysis": {
    "filter": {
      "stop_filter": {
        "type": "stop",
        "stopwords": ["the", "is", "in", "on", "a"]
      }
    }
  }
}

 

위 예시에서 "the", "is", "in" 등의 단어는 분석에서 제외됩니다.

 

4. stemmer 필터

stemmer 필터는 단어를 원형(근본형)으로 변환하는 필터입니다.

예를 들어, "running", "runner" 등의 단어를 "run"으로 변환하여 같은 의미의 단어들을 일관되게 처리할 수 있습니다.

이 필터는 텍스트에서 단어를 표준화하여, 다양한 형태의 단어를 하나의 단어로 묶어 검색 효율성을 높입니다.

 

✅ 예시

입력 텍스트: running runners ran

변환 후: run run run

PUT /products/_settings
{
  "analysis": {
    "filter": {
      "stemmer_filter": {
        "type": "stemmer",
        "language": "english"
      }
    }
  }
}

 

위 설정을 통해 "running", "runners", "ran"과 같은 단어들이 "run"으로 변환됩니다.

이처럼 동사의 다양한 형태를 일관되게 처리할 수 있습니다.

 

결론

Elasticsearch에서 제공하는 다양한 필터들은 텍스트 분석의 정확도와 성능을 향상시키는 데 중요한 역할을 합니다.

각 필터의 역할을 잘 이해하고, 필요에 맞게 적절한 필터를 설정하면 보다 효율적이고 정확한 검색 결과를 얻을 수 있습니다.

반응형
반응형

안녕하세요. 여행하는 개발자입니다.
오늘은 Elasticsearch의 Analyzer에 대해 알아보겠습니다.

1. Analyzer란?

Analyzer는 특정 필드값을 토큰(token)으로 분리해 역인덱스(Inverted index)를 저장하는 과정에서 문자열(text)을 토큰(token)으로 변환시켜주는 장치를 말합니다. Analyzer는 3가지 주요 요소를 활용하여 문자열을 토큰으로 변환합니다:

  • 캐릭터 필터(character filter): 문자열을 토큰으로 자르기 전에 다듬는 역할
  • 토크나이저(tokenizer): 문자열을 토큰으로 자르는 역할
  • 토큰 필터(token filter): 잘린 토큰을 최종적으로 다듬는 역할 (여러 종류의 필터 사용)

 

캐릭터 필터(character filter)

문자열을 토큰으로 자르기 전에 문자열을 다듬는 역할을 합니다.

 

예시:

<h1>아이폰 15 사용 후기</h1> -> 아이폰 15 사용 후기
(mapping 필터, pattern replace 필터)

 

✅ 토크나이저(tokenizer)

문자열을 토큰으로 자르는 역할을 합니다.

 

예시: 

standard 토크나이저 (공백 또는 ',', '.', '!', '?'와 같은 문장 부호를 기준으로 자름)

The Brown-Foxes jumped over the roof.
→ [The, Brown, Foxes, jumped, over, the, roof]

(classic 토크나이저, keyword 토크나이저, pattern 토크나이저 등등)

 

토큰 필터(token filter)

잘린 토큰을 최종적으로 다듬는 역할을 합니다. 다양한 필터가 존재하며, 여러 개의 필터를 적용할 수 있습니다.

 

예시: 

1) lowercase 필터 적용 (소문자로 변환)

[The, Brown, Foxes, jumped, over, the, roof]
→ [the, brown, foxes, jumped, over, the, roof]


2) stop 필터 적용 (a, the, is와 같은 특별한 의미를 가지지 않는 단어 제거)

[the, brown, foxes, jumped, over, the, roof]
→ [brown, foxes, jumped, roof]


3) stemmer 필터 적용 (단어의 원래 형태로 변환)

[brown, foxes, jumped, roof]
→ [brown, fox, jump, roof]

 

2. Elasticsearch에 기본값으로 설정되어 있는 애널라이저(Analyzer)

Elasticsearch에서는 기본적으로 캐릭터 필터(character filter)가 설정되어 있지 않습니다. 토크나이저(tokenizer)는 'standard' 토크나이저로 설정되어 있고, 토큰 필터(token filter)는 lowercase 필터가 설정되어 있습니다.

✅ standard analyzer 표현 방식

방법 1:

"analyzer": "standard"

 

방법 2:

"char_filter": [], 
"tokenizer": "standard", 
"filter": ["lowercase"]

 

3. 이전 포스팅 실습 되짚어보기

이전 포스팅에서는 lowercase 필터가 없다고 가정하고 대문자 그대로 역인덱스에 저장된다고 했습니다.

하지만 실제로는 Elasticsearch에서 소문자로 저장됩니다. 아래는 역인덱스 저장 예시입니다.

토큰(token) 도큐먼트 id
apple [1, 2, 3]
2025 [1]
맥북 [1]
에어 [1]
13 [1]
m4 [1]
10코어 [1]
2024 [2, 3]
에어팟 [2]
4세대 [2]
아이패드 [3]
mini [3]
a17 [3]
pro [3]

 

4. 애널라이저(Analyzer) 디버깅 해보기

기본적으로 특정 문자열(text)이 역인덱스에 저장되는 구조를 확인하려면 _analyze API를 사용합니다.

 

방법 1:

GET /_analyze
{
  "text": "Apple 2025 맥북 에어 13 M4 10코어",
  "analyzer": "standard"
}

 

방법 2:

GET /_analyze
{
  "text": "Apple 2025 맥북 에어 13 M4 10코어",
  "char_filter": [],
  "tokenizer": "standard",
  "filter": ["lowercase"]
}

 

 결과

 

결과적으로, 토크나이저(tokenizer)는 standard가 적용되고, 토큰 필터(token filter)는 lowercase 필터가 적용되어 "Apple"과 "M4"는 각각 "apple", "m4"로 역인덱스에 저장됩니다.

 

이렇게 오늘은 애널라이저(Analyzer)에 대해서 알아보았습니다.

다음 포스팅에서는 Analyzer에 사용되는 기본적인 필터 종류에 대해서 알아보도록 하겠습니다.

 

반응형
반응형

안녕하세요. 여행하는 개발자입니다.
오늘은 Elasticsearch의 기본 검색 기능 및 작동원리에 대해 알아보겠습니다.

1. Elasticsearch의 검색기능

Elasticsearch는 단어의 순서가 바뀌어도 검색이 가능하다는 장점이 있습니다. 예를 들어, MySQL의 Products 테이블 name이라는 필드에 "맥북 에어 13 M4"라고 저장했다고 가정해봅니다.

MySQL에서 특정 키워드로 데이터를 검색하려면 아래와 같은 SQL문을 작성해야 합니다.

SELECT * FROM products WHERE name like '%맥북 에어 13%';

 

만약 '%맥북 13 에어%'와 같이 순서가 바뀐다면 검색이 되지 않습니다. 하지만 Elasticsearch에서는 이렇게 순서가 바뀌어도 검색이 가능하게 됩니다.

 

2. 검색 테스트

인덱스 생성

PUT /products

 

매핑(mapping) 정의

PUT /products/_mappings
{
	"properties": {
		"name": {
			"type": "text"
		}
	}
}

 

도큐먼트(Document) 삽입

POST /products/_doc
{
	"name": "Apple 2025 맥북 에어 13 M4 10코어"
}

 

도큐먼트(Document) 검색

GET /products/_search
{
	"query":{
		"match":{
			"name": "맥북 에어 13 M4"
		}
	}
}

 

순서 바꿔서 검색

GET /products/_search
{
	"query":{
		"match":{
			"name": "맥북 13 에어 M4"
		}
	}
}

 

 결과

아래와 같이 순서를 바꿔서 검색을 하더라도 잘 검색되는 것을 확인할 수 있습니다.

 

3. 역인덱스란?

역인덱스는 필드 값을 단어마다 쪼개서 찾기 쉽게 정리해놓은 목록입니다.

다음 예시를 통해 역인덱스의 작동 방식을 이해해 봅시다.

POST /products/_create/1
{
  "name": "Apple 2025 맥북 에어 13 M4 10코어"
}

POST /products/_create/2
{
  "name": "Apple 2024 에어팟 4세대"
}

POST /products/_create/3
{
  "name": "Apple 2024 아이패드 mini A17 Pro"
}

 

위와 같이 3개의 도큐먼트를 저장했다고 가정합니다. Elasticsearch는 해당 도큐먼트의 단어 단위로 잘라 역인덱스로 저장합니다.

 

단어 단위로 자르기

"name": "Apple 2025 맥북 에어 13 M4 10코어"

[Apple, 2025, 맥북, 에어, 13, M4, 10코어]

 

"name": "Apple 2024 에어팟 4세대"

[Apple, 2024, 에어팟, 4세대]

 

"name": "Apple 2024 아이패드 mini A17 Pro"

[Apple, 2024, 아이패드, mini, A17, Pro]

 

역인덱스 저장

products의 name 필드에 대한 역인덱스는 다음과 같습니다.

토큰(token) 도큐먼트 ID
Apple [1, 2, 3]
2025 [1]
맥북 [1]
에어 [1]
13 [1]
M4 [1]
10코어 [1]
2024 [2, 3]
에어팟 [2]
4세대 [2]
아이패드 [3]
mini [3]
A17 [3]
Pro [3]

 

이렇게 저장된 단어들을 토큰(token)이라고 부르며, Elasticsearch는 이를 기반으로 검색을 효율적으로 처리합니다.

 

3) 검색 예시

'Apple 2024 아이패드'라고 검색을 할 경우, Elasticsearch는 아래와 같이 토큰을 기반으로 검색을 진행합니다.

토큰(token) 도큐먼트 ID
Apple [1, 2, 3]
2025 [1]
맥북 [1]
에어 [1]
13 [1]
M4 [1]
10코어 [1]
2024 [2, 3]
에어팟 [2]
4세대 [2]
아이패드 [3]
mini [3]
A17 [3]
Pro [3]

 

Elasticsearch는 단어가 많이 일치하는 도큐먼트를 우선적으로 조회하며, 자체적인 로직으로 점수(score)를 매깁니다.

이 점수에 따라 도큐먼트를 조회하며, 점수가 높은 순으로 정렬됩니다.

 

4. 역인덱스 실습

  인덱스 생성 및 매핑 정의

PUT /products
{
    "mappings":{
        "properties":{
            "name":{
                "type": "text"
            }
        }
    }
}

 

  도큐먼트(Document) 삽입

POST /products/_create/1
{
  "name": "Apple 2025 맥북 에어 13 M4 10코어"
}

POST /products/_create/2
{
  "name": "Apple 2024 에어팟 4세대"
}

POST /products/_create/3
{
  "name": "Apple 2024 아이패드 mini A17 Pro"
}

 

 검색

GET /products/_search
{
    "query": {
        "match": {
          "name": "Apple 2024 아이패드"
        }
    }
}

 

 결과

아래와 같이 score점수가 가장 높은 순으로 정렬되어 검색되는 것을 확인해 볼 수 있습니다.

 

이렇게 검색이 잘 작동하는지 실습을 통해 확인해볼 수 있습니다. Elasticsearch는 데이터의 검색과 분석을 더욱 효율적으로 처리할 수 있게 도와줍니다.

반응형
반응형

안녕하세요. 여행하는 개발자입니다.
오늘은 SpringBoot프로젝트로 Elasticsearch를 연결하여 간단한 CRUD를 구현하는 프로젝트를  구현해 보도록 하겠습니다.

1. SpringBoot 프로젝트 셋팅

우선 간단한 SpringBoot 프로젝트를 생성합니다. 아래 사이트로 접속해서 프로젝트 생성해 주도록 합니다.

 

✅ 스프링 사이트

https://start.spring.io/

Java버전은 17버전을 선택하고 패키지 이름은 elasticsearch로 지정했습니다.

Dependencies의 경우는 아래 3가지를 추가해주도록 하겠습니다.

  • Spring Web
  • Spring Boot Dev Tools
  • Spring Data Elasticsearch (Access+Driver)

 

2. 설정파일 셋팅

✅ application.yml

spring:
  elasticsearch:
    uris: http://localhost:9200 # 연결할 Elasticsearch 주소

logging:
  level:
    org.elasticsearch.client: TRACE # Elasticsearch의 쿼리 로그를 출력

 

기존에 생성되어있는 application.properties파일을 yml파일로 변경하여 작업해주도록 하겠습니다.

elasticsearch 접속을 위한 uris와 로그를 출력하기 위한 설정을 진행해 줍니다.

 

3. 소스작업

소스는 UI없이 Class파일만을 가지고 간단한 사용자의 정보를 저장/수정/조회/삭제하는 CRUD 소스입니다.

 

✅ Document생성 (UserDocument.java)

@Document(indexName = "users")
public class UserDocument {
    @Id
    private String id;

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

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

    @Field(type = FieldType.Boolean)
    private Boolean isActive;

    public UserDocument(String id, String name, Long age, Boolean isActive) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.isActive = isActive;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Long getAge() {
        return age;
    }

    public void setAge(Long age) {
        this.age = age;
    }

    public Boolean getIsActive() {
        return isActive;
    }

    public void setIsActive(Boolean active) {
        isActive = active;
    }
}

 

Repository 생성 (UserDocumentRepository.java) - 인터페이스

public interface UserDocumentRepository extends ElasticsearchRepository<UserDocument, String> {
}

 

최소 1개 이상의 ElasticsearchRepository가 존재해야, Spring Boot를 실행시킬 때 Elasticsearch와 연결을 시도합니다.

 

 Controller 생성 (UserController.java)

@RestController
@RequestMapping("users")
public class UserController {

    private UserDocumentRepository userDocumentRepository;

    public UserController(UserDocumentRepository userDocumentRepository) {
        this.userDocumentRepository = userDocumentRepository;
    }

    @PostMapping()
    public UserDocument createUser(@RequestBody UserCreateRequestDto requestDto){
        UserDocument user = new UserDocument(
                requestDto.getId(),
                requestDto.getName(),
                requestDto.getAge(),
                requestDto.getIsActive()
        );
        return userDocumentRepository.save(user);
    }

    @GetMapping()
    public Page<UserDocument> findUsers(){
        return userDocumentRepository.findAll(PageRequest.of(0,10));
    }

    @GetMapping("/{id}")
    public UserDocument findUserId(@PathVariable String id){
        return userDocumentRepository.findById(id).orElseThrow(() -> new RuntimeException("존재하지 않는 사용자입니다."));
    }

    @PutMapping("/{id}")
    public UserDocument updateUser(@PathVariable String id, @RequestBody UserUpdateRequestDto requestDto){
        UserDocument user = userDocumentRepository.findById(id).orElseThrow(() -> new RuntimeException("존재하지 않는 사용자입니다."));

        user.setAge(requestDto.getAge());
        user.setName(requestDto.getName());
        user.setIsActive(requestDto.getIsActive());

        return userDocumentRepository.save(user);
    }

    @DeleteMapping("/{id}")
    public void deleteUser(@PathVariable String id){
        UserDocument user = userDocumentRepository.findById(id).orElseThrow(() -> new RuntimeException("존재하지 않는 사용자입니다."));
        userDocumentRepository.delete(user);
    }
}

 

사용자 정보 생성을 위한 Request DTO 생성 (UserCreateRequestDto.java)

public class UserCreateRequestDto {
    private String id;
    private String name;
    private Long age;
    private Boolean isActive;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Long getAge() {
        return age;
    }

    public void setAge(Long age) {
        this.age = age;
    }

    public Boolean getIsActive() {
        return isActive;
    }

    public void setIsActive(Boolean isActive) {
        this.isActive = isActive;
    }
}

 

 사용자 정보 수정을 위한 Request DTO 생성 (UserUpdateRequestDto.java)

public class UserUpdateRequestDto {
    private String name;
    private Long age;
    private Boolean isActive;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Long getAge() {
        return age;
    }

    public void setAge(Long age) {
        this.age = age;
    }

    public Boolean getIsActive() {
        return isActive;
    }

    public void setIsActive(Boolean isActive) {
        this.isActive = isActive;
    }
}

 

4. Docker로 Elasticsearch 및 kibana 실행

✅ compose.yml파일 작성

services:
  elastic:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.17.4 # 8.17.4 버전
    ports:
      - 9200:9200 # 9200번 포트에서 Elasticsearch 실행
    environment:
      # 아래 설정은 개발/테스트 환경에서 간단하게 테스트하기 위한 옵션 (운영 환경에서는 설정하면 안 됨)
      - discovery.type=single-node # 단일 노드 (지금은 알 필요 없음)
      - xpack.security.enabled=false # 보안 설정
      - xpack.security.http.ssl.enabled=false # 보안 설정
  kibana:
    image: docker.elastic.co/kibana/kibana:8.17.4 # 8.17.4 버전
    ports:
      - 5601:5601 # 5601번 포트에서 kibana 실행
    environment:
      - ELASTICSEARCH_HOSTS=http://elastic:9200 # kibana가 통신할 Elasticsearch 주소 알려주기

 

위 설정으로 elasticsearch와 kibana를 docker컨테이너에 띄우도록 하겠습니다.

해당 파일을 작성한 경로로 들어가 아래 명령어를 수행해줍니다.

docker compose up -d

 

※ 해당 포스팅은 Docker를 사용할 줄 아는 가정하에 작성하였습니다.

Docker에 대한 정보가 필요하신 분들은 아래 포스팅을 확인해주시기 바랍니다.

https://developer-trip.tistory.com/category/%EA%B0%9C%EB%B0%9C%20Story/Docker

 

5. SpringBoot 프로젝트 실행 및 테스트

✅ SpringBoot 프로젝트 실행

 

기본적으로 Elasticsearch가 실행이 되어있는 상태여야합니다. 필자는 Docker를 통해 실행했고 위와 같이 Elasticsearch가 잘 연결되어 SpringBoot프로젝트가 정상적으로 실행된 것을 확인 할 수 있습니다.

 

✅ Postman을 통해 테스트

 

사용자 정보를 삽입하기 위해 POST메서드를 사용하고 Body에 사용자 정보를 json형태로 담아 전달합니다.

200응답코드로 정상적으로 삽입이 되었다는 것을 확인할 수 있습니다.

 

✅ Kibana로 데이터 확인

위에서 저장한 데이터가 정상적으로 저장이 되었는지 Kibana를 통해 확실하게 확인해도록 합시다.

 

아래 접속주소로 접속 후 좌측 네비게이션바 하단에 Dev Tools로 접속합니다.

  • http://localhost:5601

 

* 명령어 : GET /users/_doc/1

정상적으로 데이터가 조회됨을 확인할 수 있습니다.

 

이로써 SpringBoot프로젝트로 Elasticsearch를 연결하여 간단한 CRUD를 구현하는 API를 만들어봤습니다.

다음 포스팅에서는 Elasticsearch의 검색기능에 대해 자세하게 알아보겠습니다.

 

반응형
반응형

안녕하세요. 여행하는 개발자입니다.
오늘은 Elasticsearch란 무엇인지부터 기본명령어 까지 한번 알아보겠습니다.

1. ElasticSearch란?

Elasticsearch는 검색 및 데이터 분석에 최적화된 분산형 데이터베이스입니다. 대규모 데이터를 실시간으로 검색하고 분석할 수 있는 기능을 제공합니다.

활용사례

  • 데이터 수집 및 분석 : Elasticsearch는 대규모 데이터를 수집하고 분석하는 데 최적화되어 있습니다. Logstash와 Kibana와 함께 사용하여 데이터를 수집, 처리 및 시각화합니다.
  • 검색 최적화 : Elasticsearch는 대량의 데이터를 다루면서도 빠른 검색 속도를 제공하며, 오타나 동의어를 고려한 유연한 검색 기능을 제공합니다. 쿠팡과 배달의민족도 Elasticsearch를 활용해 검색 기능을 구현하고 있습니다.

 

2. ElasticSearch 작동 방식

✅ 작동방식

MySQL과 소통하려면 SQL문이라는 방식으로 통신해야 합니다. 하지만 Elasticsearch와 소통하려면 REST API를 사용해야 합니다. Elasticsearch는 HTTP를 통해 JSON 형식의 데이터를 전송받고 처리합니다.

 

예시)

MySQL:

INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');

 

ElasticSearch:

curl -X POST "localhost:9200/users/_doc" -H 'Content-Type: application/json' -d'
{
  "name": "Alice",
  "email": "alice@example.com"
}'

 

4. ElasticSearch의 기본 용어 정리

  • 인덱스 : MySQL의 데이터베이스 테이블에 해당
  • 필드 : MySQL의 컬럼
  • 도큐먼트 : MySQL의 레코드 또는 로우 (실제 데이터)
  • 매핑 : MySQL의 스키마

 

5. 인덱스 생성, 매핑 정의, 도큐먼트 삽입

✅ 인덱스 생성 및 조회

# 인덱스 생성
PUT /users

# 인덱스 생성 여부 확인
GET /users

# 없는 인덱스를 조회했을 때
GET /abc

 

✅ 인덱스 삭제

# 인덱스 생성
PUT /boards

# 인덱스 생성 여부 확인
GET /boards

# 인덱스 삭제
DELETE /boards

# 삭제 여부 확인
GET /boards

 

✅ 매핑(mapping) 정의

# 매핑 정의
PUT /users/_mappings
{
  "properties": {
    "name": { "type": "keyword" },
    "age": { "type": "integer" },
    "is_active": { "type": "boolean" }
  }
}

# 매핑 확인
GET /users

 

✅ 도큐먼트(Document) 삽입 및 조회

# 도큐먼트 삽입
POST /users/_doc
{
  "name": "Alice",
  "age": 28,
  "is_active": true
}

POST /users/_doc
{
  "name": "Bob",
  "age": 30,
  "is_active": false
}

# 모든 도큐먼트 조회
GET /users/_search

 

결과:

'hits'는 저장한 데이터의 배열을 의미합니다. 하위에 '_id'값은 데이터를 저장하면서 자동으로 생성된 '랜덤 고유 ID'를 의미하며

'_source'값은 저장한 데이터가 들어있는 필드를 의미합니다.

 

6. 도큐먼트 저장, 조회, 수정, 삭제하기

✅ 도큐먼트(Document) 저장 및 수정

# id 자동 생성 후 저장
POST /users/_doc
{
  "name": "Gyun",
  "age": 30,
  "is_active": true
}

# id를 직접 지정하여 저장
POST /users/_create/1
{
  "name": "Gyun",
  "age": 30,
  "is_active": true
}

# id가 존재할 경우 수정 (덮어쓰기)
PUT /users/_doc/1
{
  "name": "Gyun",
  "age": 30,
  "is_active": true
}

# 일부 필드 수정
POST /users/_update/1
{
  "doc": {
    "age": 10,
    "is_active": false
  }
}

 

✅ 도큐먼트(Document) 조회

# 모든 도큐먼트 조회
GET /users/_search

# 특정 id 도큐먼트 조회
GET /users/_doc/1

 

✅ 도큐먼트(Document) 삭제

# id로 도큐먼트 삭제
DELETE /users/_doc/1

 

이로써 Elasticsearch의 기본 개념과 주요 작업을 정리해보았습니다. Elasticsearch를 사용하면 대규모 데이터를 빠르게 처리하고, 유연한 검색을 제공할 수 있습니다.

반응형
반응형

안녕하세요, 여행하는 개발자입니다.
이번 포스팅에서는 MySQL 데이터베이스에서 성능을 개선하는 실습예제를 다뤄보겠습니다.

해당 예제는 "좋아요 많은 순으로 게시글 조회하기"에 관련된 SQL문 예제 성능개선입니다.

기본 테이블 셋팅

CREATE TABLE posts (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE likes (
    id INT AUTO_INCREMENT PRIMARY KEY,
    post_id INT,
    user_id INT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (post_id) REFERENCES posts(id),
    FOREIGN KEY (user_id) REFERENCES users(id)
);

 

더미 데이터 추가

이제 더미 데이터를 추가해볼 차례입니다. 먼저, 더미 데이터를 삽입하기 위한 설정을 하고, 각 테이블에 데이터를 삽입하는 SQL 문을 준비했습니다. 여기서는 100만 개의 더미 데이터를 삽입합니다.

-- 높은 재귀(반복) 횟수를 허용하도록 설정
-- (아래에서 생성할 더미 데이터의 개수와 맞춰서 작성하면 된다.)
SET SESSION cte_max_recursion_depth = 1000000;

-- posts 테이블에 더미 데이터 삽입
INSERT INTO posts (title, created_at)
WITH RECURSIVE cte (n) AS
(
  SELECT 1
  UNION ALL
  SELECT n + 1 FROM cte WHERE n < 1000000 -- 생성하고 싶은 더미 데이터의 개수
)
SELECT 
    CONCAT('Post', LPAD(n, 7, '0')) AS name,  
    TIMESTAMP(DATE_SUB(NOW(), INTERVAL FLOOR(RAND() * 3650) DAY) + INTERVAL FLOOR(RAND() * 86400) SECOND) AS created_at
FROM cte;

-- users 테이블에 더미 데이터 삽입
INSERT INTO users (name, created_at)
WITH RECURSIVE cte (n) AS
(
  SELECT 1
  UNION ALL
  SELECT n + 1 FROM cte WHERE n < 1000000 -- 생성하고 싶은 더미 데이터의 개수
)
SELECT 
    CONCAT('User', LPAD(n, 7, '0')) AS name,  
    TIMESTAMP(DATE_SUB(NOW(), INTERVAL FLOOR(RAND() * 3650) DAY) + INTERVAL FLOOR(RAND() * 86400) SECOND) AS created_at
FROM cte;

-- likes 테이블에 더미 데이터 삽입
INSERT INTO likes (post_id, user_id, created_at)
WITH RECURSIVE cte (n) AS
(
  SELECT 1
  UNION ALL
  SELECT n + 1 FROM cte WHERE n < 1000000 -- 생성하고 싶은 더미 데이터의 개수
)
SELECT 
    FLOOR(1 + RAND() * 1000000) AS post_id,    
    FLOOR(1 + RAND() * 1000000) AS user_id,    
    TIMESTAMP(DATE_SUB(NOW(), INTERVAL FLOOR(RAND() * 3650) DAY) + INTERVAL FLOOR(RAND() * 86400) SECOND) AS created_at
FROM cte;

 

성능 개선해야 할 SQL문 예제

SELECT
    p.id,
    p.title,
    p.created_at,
    COUNT(l.id) AS like_count
FROM
    posts p
INNER JOIN
    likes l ON p.id = l.post_id
GROUP BY
    p.id, p.title, p.created_at
ORDER BY
    like_count DESC
LIMIT 30;

 

 

대략 16초정도 소요되는 것을 확인할 수 있습니다. 그렇다면 아래 실행계획을 통해 어느 부분이 느린지 확인해보도록 해봅시다.

 

실행 계획 세부 내용 조회

EXPLAIN ANALYZE SELECT
    p.id,
    p.title,
    p.created_at,
    COUNT(l.id) AS like_count
FROM
    posts p
INNER JOIN
    likes l ON p.id = l.post_id
GROUP BY
    p.id, p.title, p.created_at
ORDER BY
    like_count DESC
LIMIT 30;

결과

-> Limit: 30 row(s)  (actual time=17653..17653 rows=30 loops=1)
    -> Sort: like_count DESC, limit input to 30 row(s) per chunk  (actual time=17653..17653 rows=30 loops=1)
        -> Table scan on   (actual time=17244..17541 rows=583012 loops=1)
            -> Aggregate using temporary table  (actual time=17244..17244 rows=583011 loops=1)
                -> Nested loop inner join  (cost=449599 rows=997632) (actual time=0.183..6114 rows=1e+6 loops=1)
                    -> Table scan on p  (cost=100428 rows=997632) (actual time=0.111..649 rows=1e+6 loops=1)
                    -> Covering index lookup on l using post_id (post_id=p.id)  (cost=0.25 rows=1) (actual time=0.00435..0.00514 rows=1 loops=1e+6)

세부 실행 계획을 보니 INNER JOIN과 GROUP BY(Aggreagte using temporary table)에 시간을 많이 사용했습니다.
이 이유를 추측해보니 INNER JOIN, GROUP BY를 수행할 때 풀 테이블 스캔으로 조회한 데이터 100만개를 가지고 처리를 해서
오래 걸렸다고 추측할 수 있습니다. 그렇다면 해당 쿼리를 개선해보도록 합시다.

 

개선쿼리

SELECT p.*, l.like_count
FROM posts p
INNER JOIN
    (SELECT post_id, count(post_id) AS like_count FROM likes l
    GROUP BY l.post_id
    ORDER BY like_count DESC
    LIMIT 30) l
ON p.id = l.post_id;

이전 SQL문은 INNER JOIN을 한 뒤에 GROUP BY를 했었습니다. 하지만 위 SQL문은 likes 테이블만 가지고 먼저 GROUP BY를 한 뒤 좋아요 수가 많은 post_id를 찾아냅니다. 이 때, GROUP BY 과정에서 post_id만 사용하기 때문에 풀 테이블 스캔을 하지 않고 인덱스만 활용해서 데이터를 조회할 수 있어서 빠릅니다.(커버링 인덱스)

 

 

실행결과 확인시 0.5초 정도로 성능이 많이 개선된 부분을 확인할 수 있고 이는 이전 쿼리 16초 보다 대략 32배 성능개선이

되었다고 볼 수 있습니다. 그렇다면 개선된 쿼리의 실행계획을 통해 상세하게 확인해보도록 합시다.

 

성능 개선 후 실행 계획

EXPLAIN SELECT p.*, l.like_count
FROM posts p
INNER JOIN
    (SELECT post_id, count(post_id) AS like_count FROM likes l
    GROUP BY l.post_id
    ORDER BY like_count DESC
    LIMIT 30) l
ON p.id = l.post_id;

 

 

개선된 쿼리의 실행 계획을 확인해보면, 풀 테이블 스캔으로 액세스한 데이터 수가 30으로 줄어든 것을 확인할 수 있습니다.

또한, 서브쿼리에서 인덱스 풀 스캔을 사용함으로써 성능이 대폭 개선되었습니다.

 

이 포스팅을 통해 SQL 성능 최적화 방법을 배워보았습니다.

여러분도 쿼리 성능을 분석하고 최적화해보세요! 도움이 되셨기를 바랍니다. 😊

반응형
반응형

안녕하세요, 여행하는 개발자입니다.
이번 포스팅에서는 MySQL에서 EXPLAIN 명령어를 통해 인덱스가 잘 사용되고 있는지 확인하는 방법에 대해 정리하겠습니다.

1. EXPLAIN이란?

MySQL에서 EXPLAIN 키워드를 SELECT 문 앞에 붙이면, 해당 쿼리가 실행될 때 어떤 방식으로 데이터를 접근하는지를 보여주는 실행계획을 확인할 수 있습니다.

EXPLAIN SELECT * FROM users WHERE age = 25;

 

2. type 컬럼의 의미

EXPLAIN 결과 중 type조인 또는 테이블 접근 방식을 의미하며, 성능에 매우 중요한 요소입니다.

type 설명 성능
system 테이블에 1개의 row만 존재 ✅ 가장 빠름
const PK나 UNIQUE 조건으로 row 하나 조회 ✅ 매우 빠름
eq_ref 조인에서 PK/UNIQUE를 통한 정확한 매칭 ✅ 빠름
ref 일반적인 인덱스 키 조인 ✅ 양호
range 범위 조건으로 인덱스 사용 ⭕ 쓸만함
index 전체 인덱스를 순회 ❌ 느림 (테이블보단 낫다)
ALL 풀 테이블 스캔 ❌ 매우 느림

 

3. 샘플 쿼리로 확인

✅ ALL (Full Table Scan)

EXPLAIN SELECT * FROM users WHERE age = 25;

→ age 컬럼에 인덱스가 없을 경우 ALL로 나옵니다.

 

✅ range (범위 검색, 인덱스 사용)

EXPLAIN SELECT * FROM users WHERE id BETWEEN 10 AND 50;

id가 PK이면 range 혹은 const로 나옵니다.

 

✅ const (PK를 통한 단일 row 조회)

EXPLAIN SELECT * FROM users WHERE id = 1;

→ id가 PK이기 때문에 const로 출력됩니다.

 

✅ ref (인덱스 기반 조인)

SELECT * FROM orders 
JOIN users ON orders.user_id = users.id 
WHERE users.name = '홍길동';

→ users.name 컬럼에 인덱스가 있다면 ref로 나옵니다.

 

4. 쿼리 튜닝 팁

  • type이 ALL인 경우, 반드시 인덱스 추가 여부를 검토하세요.
  • range 이상 수준의 type이 되도록 조건절 설계하는 것이 좋습니다.
  • 불필요한 LIKE 검색, IS NULL 조건 등은 피하는 것이 성능에 이롭습니다.

 

5. 마무리

MySQL에서 실행 계획을 분석하면 인덱스 사용 여부 및 쿼리 성능을 미리 예측할 수 있습니다.
개발자라면 EXPLAIN 명령어는 필수로 익혀야 할 도구입니다! 👍

반응형
반응형

안녕하세요, 여행하는 개발자입니다.
이번 포스트에서는 데이터베이스 성능 최적화를 위한 중요한 개념인 인덱스(Index)에 대해 정리해보겠습니다.

1. 인덱스란?

인덱스는 데이터베이스 테이블에 대한 검색 속도를 향상시키는 자료 구조입니다.
쉽게 말하면, 특정 컬럼을 기준으로 미리 정렬해놓은 검색용 표라고 이해하면 됩니다.

 

2. 기본 인덱스: Primary Key (PK)

테이블에서 각 데이터를 고유하게 식별하기 위해 기본키(Primary Key)를 설정합니다.
PK가 설정되면 해당 컬럼에 자동으로 인덱스가 생성되며, 이 인덱스를 기준으로 테이블이 정렬됩니다.

※ PK에는 인덱스가 기본 적용됩니다.

 

3. 제약 조건으로 생성되는 인덱스: UNIQUE

MySQL에서는 UNIQUE 제약 조건을 설정하면 해당 컬럼에 대해 자동으로 인덱스가 생성됩니다.

✅ 예제 쿼리

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) UNIQUE
);

name 컬럼은 중복이 허용되지 않으며, 자동으로 인덱스가 생성되어 조회 성능도 향상됩니다.

 

4. 멀티 컬럼 인덱스

멀티 컬럼 인덱스는 2개 이상의 컬럼을 묶어 인덱스를 생성하는 방식입니다.

CREATE INDEX idx_부서_이름 ON users (부서, 이름);

→ 먼저 부서 컬럼 기준으로 정렬한 후, 같은 부서 내에서 이름 기준으로 정렬합니다.

 

멀티컬럼 생성시 중요한 점

- 멀티 컬럼 인덱스에서 처음에 배치된 컬럼들은 일반 인덱스처럼 활용할 수 있다.

- 멀티 컬럼 인덱스를 구성할 때 데이터 중복도가 낮은 컬럼이 앞쪽으로 오는 게 좋다.

 

5. 커버링 인덱스 (Covering Index)

SELECT 쿼리에서 필요한 모든 컬럼이 인덱스에 포함되어 있을 경우, 실제 테이블에 접근하지 않고도 데이터를 조회할 수 있습니다. 이런 인덱스를 커버링 인덱스라고 부릅니다.

✅ 예시 테이블

📌 users 테이블

id name regDt
1 나길동 2025-06-01
2 박길동 2025-06-02
3 김길동 2025-06-03

 

📌 name 인덱스

name id (PK)
김길동 3
나길동 1
박길동 2

 

✅ 쿼리 비교

-- 커버링 인덱스가 적용되지 않는 쿼리
SELECT id, regDt FROM users;

-- 커버링 인덱스가 적용되는 쿼리
SELECT id, name FROM users;

id, name 쿼리는 name 인덱스만으로도 모든 정보를 조회할 수 있기 때문에 실제 테이블을 읽지 않습니다.

 

6. 인덱스 실습

✅ 테스트 환경 구성

-- 1. 테이블 생성
CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100),
    age INT
);

-- 2. 100만 건의 랜덤 데이터 삽입
SET SESSION cte_max_recursion_depth = 1000000;

INSERT INTO users (name, age)
WITH RECURSIVE cte (n) AS (
  SELECT 1
  UNION ALL
  SELECT n + 1 FROM cte WHERE n < 1000000
)
SELECT 
    CONCAT('User', LPAD(n, 7, '0')),  
    FLOOR(1 + RAND() * 1000) AS age
FROM cte;

-- 확인
SELECT COUNT(*) FROM users;

 

✅ 조회 쿼리

SELECT * FROM users
WHERE age = 23;

위 쿼리는 age 컬럼에 인덱스가 없는 경우, 전체 테이블을 스캔하게 됩니다.
하지만 CREATE INDEX idx_age ON users(age); 명령으로 인덱스를 추가하면, 해당 조건 검색 시 탐색 성능이 비약적으로 향상됩니다.

 

✅ 실행결과

기존 0.4s 정도 소요되던 쿼리가 인덱스 추가 후 0.04s정도 소요되는 것을 알 수 있습니다.

우리는 여기서 인덱스 추가 후 쿼리 검색 속도가 10배 정도 성능개선이 되었다고 표현할 수 있습니다.

 

7. 인덱스를 과도하게 많이 생성할 경우 성능 테스트

인덱스를 사용하면 데이터를 조회할 때의 성능이 향상됩니다.
하지만 인덱스를 무작정 많이 추가하는 것은 오히려 삽입, 수정, 삭제 성능을 저하시킬 수 있습니다.

✅ 테이블 생성

-- 테이블 A: 인덱스가 없는 테이블
CREATE TABLE test_table_no_index (
    id INT AUTO_INCREMENT PRIMARY KEY,
    column1 INT,
    column2 INT,
    column3 INT,
    column4 INT,
    column5 INT,
    column6 INT,
    column7 INT,
    column8 INT,
    column9 INT,
    column10 INT
);

-- 테이블 B: 인덱스가 많은 테이블
CREATE TABLE test_table_many_indexes (
    id INT AUTO_INCREMENT PRIMARY KEY,
    column1 INT,
    column2 INT,
    column3 INT,
    column4 INT,
    column5 INT,
    column6 INT,
    column7 INT,
    column8 INT,
    column9 INT,
    column10 INT
);

 

✅ 인덱스 추가

-- 각 컬럼에 인덱스를 추가
CREATE INDEX idx_column1 ON test_table_many_indexes (column1);
CREATE INDEX idx_column2 ON test_table_many_indexes (column2);
CREATE INDEX idx_column3 ON test_table_many_indexes (column3);
CREATE INDEX idx_column4 ON test_table_many_indexes (column4);
CREATE INDEX idx_column5 ON test_table_many_indexes (column5);
CREATE INDEX idx_column6 ON test_table_many_indexes (column6);
CREATE INDEX idx_column7 ON test_table_many_indexes (column7);
CREATE INDEX idx_column8 ON test_table_many_indexes (column8);
CREATE INDEX idx_column9 ON test_table_many_indexes (column9);
CREATE INDEX idx_column10 ON test_table_many_indexes (column10);

 

✅ 데이터 삽입 성능 테스트

-- 높은 재귀(반복) 횟수를 허용하도록 설정
SET SESSION cte_max_recursion_depth = 100000; 

-- 인덱스가 없는 테이블에 데이터 10만 건 삽입
INSERT INTO test_table_no_index (column1, column2, column3, column4, column5, column6, column7, column8, column9, column10)
WITH RECURSIVE cte AS (
    SELECT 1 AS n
    UNION ALL
    SELECT n + 1 FROM cte WHERE n < 100000
)
SELECT
    FLOOR(RAND() * 1000),
    FLOOR(RAND() * 1000),
    FLOOR(RAND() * 1000),
    FLOOR(RAND() * 1000),
    FLOOR(RAND() * 1000),
    FLOOR(RAND() * 1000),
    FLOOR(RAND() * 1000),
    FLOOR(RAND() * 1000),
    FLOOR(RAND() * 1000),
    FLOOR(RAND() * 1000)
FROM cte;

-- 인덱스가 많은 테이블에 데이터 10만 건 삽입
INSERT INTO test_table_many_indexes (column1, column2, column3, column4, column5, column6, column7, column8, column9, column10)
WITH RECURSIVE cte AS (
    SELECT 1 AS n
    UNION ALL
    SELECT n + 1 FROM cte WHERE n < 100000
)
SELECT
    FLOOR(RAND() * 1000),
    FLOOR(RAND() * 1000),
    FLOOR(RAND() * 1000),
    FLOOR(RAND() * 1000),
    FLOOR(RAND() * 1000),
    FLOOR(RAND() * 1000),
    FLOOR(RAND() * 1000),
    FLOOR(RAND() * 1000),
    FLOOR(RAND() * 1000),
    FLOOR(RAND() * 1000)
FROM cte;

 

✅ 실행 결과

인덱스가 많은 테이블에 데이터를 삽입하는 데 더 많은 시간이 소요되었음을 확인할 수 있습니다.

인덱스를 추가하면 조회 속도는 빨라지나, 쓰기(삽입, 수정, 삭제) 속도는 느려짐을 항상 기억해야 합니다.

 

8. 마무리

  • 인덱스는 데이터 검색 속도를 비약적으로 향상시키는 도구입니다.
  • 하지만 쓰기 성능과 저장공간 증가라는 단점도 있으므로, 신중하게 설계해야 합니다.
반응형
반응형

안녕하세요, 여행하는 개발자입니다.
이번 포스트에서는 소프트웨어 개발에서 가장 핵심적인 개념인 객체지향 프로그래밍(OOP)에 대해 자세히 알아보겠습니다.

1. 객체지향 프로그래밍이란?

객체지향 프로그래밍(OOP: Object-Oriented Programming)은 현실 세계의 사물을 객체로 보고 그 객체들의 상호작용을 프로그램으로 모델링하는 방식입니다.

즉, 프로그램을 구성하는 단위를 함수나 명령어가 아니라 객체로 구성하고, 객체 간의 메시지 교환을 통해 동작하게 합니다.

 

2. OOP의 4가지 핵심 특징

✅ 캡슐화 (Encapsulation)

객체의 내부 상태를 외부에서 직접 접근하지 못하도록 하고, 필요한 기능만 외부에 노출합니다.

public class User {
  private String password;

  public void setPassword(String pw) {
    if (pw.length() >= 8) this.password = pw;
  }
}

 

✅ 상속 (Inheritance)

기존 클래스의 속성과 기능을 자식 클래스가 물려받아 재사용할 수 있도록 합니다.

 

✅ 다형성 (Polymorphism)

동일한 메서드 호출이 객체에 따라 다른 동작을 하도록 만듭니다.

Animal a = new Dog();
a.sound();  // Dog의 sound()가 실행됨

 

✅ 추상화 (Abstraction)

불필요한 내부 구현은 숨기고, 중요한 동작만 인터페이스로 정의합니다.

 

3. 객체지향의 장점

  • 유지보수성: 모듈화 덕분에 수정이 용이
  • 재사용성: 상속을 통해 중복 최소화
  • 확장성: 새로운 기능을 쉽게 추가
  • 현실 세계와의 유사성: 개발자가 이해하기 쉬움

 

4. SOLID 원칙

SOLID는 객체지향 설계를 위한 5가지 핵심 원칙입니다.

✅ SRP - 단일 책임 원칙

하나의 클래스는 오직 하나의 책임만 가져야 합니다.

 

✅ OCP - 개방-폐쇄 원칙

확장에는 열려있고, 변경에는 닫혀있어야 합니다. (코드 수정 없이 기능 확장)

 

✅ LSP - 리스코프 치환 원칙

자식 클래스는 부모 클래스의 기능을 대체할 수 있어야 합니다.

 

✅ ISP - 인터페이스 분리 원칙

특정 클라이언트를 위한 작은 인터페이스로 나누는 것이 좋습니다.

 

✅ DIP - 의존 역전 원칙

구현 클래스가 아닌 인터페이스에 의존해야 합니다.

 

5. 객체지향 설계 방법론

  • 클래스를 작게 설계하여 단일 책임 부여
  • 인터페이스 기반 개발로 유연한 구조
  • 디자인 패턴을 적극 활용

 

6. 객체지향 실무 예시

아래는 Java에서 게시판 시스템을 만들 때 BoardServiceBoardRepository를 분리하는 구조입니다.

public interface BoardRepository {
    List<Board> findAll();
}

@Service
public class BoardService {
    private final BoardRepository boardRepo;

    public BoardService(BoardRepository repo) {
        this.boardRepo = repo;
    }

    public List<Board> getBoards() {
        return boardRepo.findAll();
    }
}

✅ 설명

  • BoardService는 Repository에 의존하지만, 구체 구현이 아닌 인터페이스에 의존 (DIP)
  • 책임이 명확하게 분리되어 유지보수 용이 (SRP)

 

7. 결론

OOP는 현실 세계와 유사한 방식으로 소프트웨어를 모델링하며, 유지보수와 확장이 편리한 구조를 제공합니다.

OOP의 원칙과 SOLID 설계 원칙을 잘 이해하면, 더 유연하고 견고한 코드를 작성할 수 있습니다.

반응형
반응형

안녕하세요. 여행하는 개발자입니다.
이번 포스팅에서는 인증(Authentication)에서 자주 쓰이는 OAuth2와 JWT에 대해 알아보겠습니다.

1. OAuth란?

OAuth는 제3자 애플리케이션이 리소스 소유자를 대신해 리소스에 접근할 수 있도록 권한을 위임하는 인증 방식입니다.

예를 들어, 어떤 앱이 사용자의 구글 캘린더 정보에 접근할 수 있도록 사용자의 동의로 권한을 위임하는 방식입니다.

✅ 주요 구성 요소

  • Resource Owner: 사용자 (권한을 가진 주체)
  • Client: 권한을 요청하는 애플리케이션
  • Authorization Server: 인증 및 토큰 발급
  • Resource Server: 보호된 API를 제공 (ex. Google Calendar API)

 

2. JWT란?

JWT(JSON Web Token)는 서버가 클라이언트에게 인증 정보를 담아 발급하는 토큰입니다.
토큰 자체에 정보가 포함되기 때문에, 서버는 세션 상태를 유지하지 않아도 됩니다 (Stateless).

✅ 구조

Header.Payload.Signature
  • Header: 토큰 타입과 해싱 알고리즘 (ex. HS256)
  • Payload: 사용자 정보, 권한 등 claims 포함
  • Signature: 위조 방지용 서명 (비밀키 기반)

 

3. 실전 흐름 예시

  1. 사용자가 로그인 요청
  2. 서버가 사용자 정보를 검증 후 JWT 토큰을 생성하여 응답
  3. 클라이언트는 이 토큰을 Authorization 헤더에 담아 이후 요청에 첨부
  4. 서버는 토큰을 검증해 요청을 처리

 

4. JWT 생성 예제 (Java)

String secretKey = "mySecretKey";

String jwt = Jwts.builder()
    .setSubject("user123")
    .setIssuer("myApp")
    .setIssuedAt(new Date())
    .setExpiration(new Date(System.currentTimeMillis() + 3600000)) // 1시간
    .signWith(SignatureAlgorithm.HS256, secretKey)
    .compact();

System.out.println("JWT 토큰: " + jwt);

✅ 코드 설명

  • setSubject(): 사용자 ID, 로그인 정보
  • setIssuer(): 토큰 발급자 정보
  • setExpiration(): 유효 기간 설정
  • signWith(): 비밀키로 서명하여 위조 방지

 

5. JWT 검증 예제

Claims claims = Jwts.parser()
    .setSigningKey(secretKey)
    .parseClaimsJws(jwt)
    .getBody();

System.out.println("사용자 ID: " + claims.getSubject());

✅ 설명

토큰을 파싱해서 Claims를 얻고, 사용자 ID 등 필요한 정보를 추출합니다.

 

6. OAuth2 + JWT 구조

OAuth2로 사용자 인증 → 서버가 JWT 토큰 발급 → 클라이언트가 API 요청 시 토큰 사용

  • 인증은 OAuth2 방식으로 처리
  • 권한/세션 관리는 JWT로 처리

 

7. 실무에서 유의할 점

  • 토큰 탈취 방지를 위해 HTTPS 필수
  • JWT 자체에는 로그아웃 처리 개념이 없음 → 만료시간(TTL) 설정 필요
  • 민감한 정보는 Payload에 저장 X (암호화 되지 않음)

 

💡 참고

Spring Boot에서는 spring-security-oauth2, jjwt 등을 활용하면 편리하게 구현 가능합니다.

📘 OAuth와 JWT의 개념을 제대로 이해하면 토큰 기반 인증 시스템을 안전하게 설계할 수 있습니다.

반응형

'개발 Story > Web' 카테고리의 다른 글

[Web] 서블릿(Servlet) 동작 방식 완전 가이드  (2) 2025.06.26
[Web] Spring Framework란?  (1) 2025.06.18
반응형

안녕하세요. 여행하는 개발자입니다.
이번 포스팅에서는 서블릿(Servlet)의 동작 방식과 Spring MVC, DispatcherServlet, Filter의 차이에 대해 알아보겠습니다.

1. 서블릿(Servlet)이란?

Servlet은 Java 기반 웹 애플리케이션에서 클라이언트의 요청을 처리하고 응답을 생성하는 서버 측 프로그램입니다.
Tomcat과 같은 Servlet 컨테이너 위에서 동작합니다.

 

2. 서블릿 동작 순서

  1. 사용자가 브라우저에서 URL 요청
  2. 웹 서버가 해당 요청을 @WebServlet 또는 web.xml에 등록된 서블릿으로 매핑
  3. 서블릿 컨테이너가 service() 메서드를 실행
  4. HTTP 요청 방식에 따라 doGet(), doPost() 등의 메서드 실행
  5. 응답 데이터를 생성하고 클라이언트에게 전달

✅ 예제 코드

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
          throws ServletException, IOException {
      response.setContentType("text/html;charset=UTF-8");
      PrintWriter out = response.getWriter();
      out.println("<html><body><h1>Hello, Servlet!</h1></body></html>");
  }
}

✅ 코드 설명

  • @WebServlet("/hello"): URL 경로와 서블릿을 매핑
  • doGet(): GET 방식의 요청 처리
  • response.getWriter(): 클라이언트에게 응답을 출력

 

3. Filter란?

Filter는 서블릿 앞단에서 요청을 가로채는 역할을 합니다.
로그 기록, 인증 검사, 인코딩 설정 등의 공통 작업에 주로 사용됩니다.

@WebFilter("/*")
public class LogFilter implements Filter {
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        System.out.println("요청이 필터를 통과합니다.");
        chain.doFilter(request, response); // 다음 필터 or 서블릿 호출
    }
}

 

4. DispatcherServlet이란?

DispatcherServlet은 Spring MVC에서 모든 HTTP 요청을 받아 컨트롤러로 분기 처리하는 핵심 컴포넌트입니다.

기본적으로 서블릿의 개념을 확장하여 Spring의 @Controller, @RequestMapping을 자동으로 처리합니다.

 

5. Servlet과 Spring MVC의 차이

구분 Servlet Spring MVC
구현 방식 doGet(), doPost() 메서드 직접 작성 @Controller, @RequestMapping 등 어노테이션 기반
구조 절차지향적 MVC 구조화된 설계
확장성 낮음 높음 (의존성 주입, 트랜잭션 등 제공)
학습 난이도 쉬움 상대적으로 높음

 

6. 요약

  • Servlet: Java의 기본 웹 기술, 가장 낮은 수준의 HTTP 처리 단위
  • Filter: 서블릿 요청 전/후 로직 삽입 가능
  • DispatcherServlet: Spring MVC의 중심 서블릿으로 MVC 구조 분기 처리

 

💬 실무 팁

  • Spring MVC를 사용하는 경우라도 DispatcherServlet은 내부적으로 서블릿입니다.
  • 로그인 인증, CORS 처리, 인코딩 설정 등은 Filter 또는 Interceptor로 구현합니다.

🌐 Java 웹 구조의 기반이 되는 Servlet 개념을 제대로 이해해두면 Spring 프레임워크를 훨씬 쉽게 다룰 수 있습니다.

반응형

'개발 Story > Web' 카테고리의 다른 글

[Web] OAuth 2.0과 JWT에 대해서  (2) 2025.06.27
[Web] Spring Framework란?  (1) 2025.06.18
반응형

안녕하세요. 여행하는 개발자입니다.
이번 포스팅에서는 자바스크립트의 중요한 개념인 이벤트 루프(Event Loop)에 대해 알아보겠습니다.

1. 이벤트 루프란?

이벤트 루프는 자바스크립트의 비동기 처리 메커니즘입니다.
자바스크립트는 단일 스레드 기반 언어이기 때문에 동시에 여러 작업을 처리할 수 없습니다. 하지만 비동기 작업을 효율적으로 처리하기 위해 이벤트 루프를 사용합니다.

 

2. 구성 요소

  • 콜 스택(Call Stack): 현재 실행 중인 함수가 쌓이는 공간
  • 웹 API: setTimeout, fetch 등 브라우저가 제공하는 비동기 기능
  • 태스크 큐(Task Queue): setTimeout, 이벤트 리스너 콜백이 대기
  • 마이크로태스크 큐(Microtask Queue): Promise의 then/catch/finally 콜백이 대기

 

3. 실행 순서

  1. 코드가 순차적으로 실행되며, 함수는 콜 스택에 쌓임
  2. setTimeout 등 비동기 작업은 Web API에서 처리됨
  3. 처리된 콜백은 태스크 큐 또는 마이크로태스크 큐로 이동
  4. 이벤트 루프는 콜 스택이 비면 큐에서 작업을 하나씩 꺼내 실행

✅ 예제 코드

console.log('start');

setTimeout(() => {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(() => {
  console.log('promise');
});

console.log('end');

 

✅ 출력 결과

start
end
promise
setTimeout

 

✅ 실행 순서 설명

  1. console.log('start'): 즉시 실행. 콜 스택에 올라갔다가 바로 출력 후 사라짐.
  2. setTimeout(..., 0): 브라우저 Web API 영역으로 보내지고, 0초 후 태스크 큐로 이동함 (즉시 실행 아님).
  3. Promise.then(): 마이크로태스크 큐에 등록됨. 태스크 큐보다 먼저 실행.
  4. console.log('end'): 동기 코드이므로 즉시 실행됨.
  5. 콜 스택이 비면 마이크로태스크 큐에서 promise 로그가 실행됨.
  6. 그 다음 태스크 큐에서 setTimeout 로그가 실행됨.

💡 정리 포인트

  • 동기 코드 → 마이크로태스크 → 태스크 큐 순서로 실행된다.
  • setTimeout(..., 0)는 0초로 설정해도 즉시 실행되지 않는다.
  • Promise는 내부적으로 Microtask Queue를 사용하기 때문에 먼저 실행된다.

 

✅ 실행 결과

start
end
promise
setTimeout

 

✅ 왜 이런 순서로 출력될까?

  • console.log('start')end는 동기적으로 즉시 실행
  • Promise.then()마이크로태스크 큐에 들어감 → 다음 tick에서 실행
  • setTimeout(..., 0)태스크 큐로 이동 → 마이크로태스크보다 나중에 실행

 

4. 실무에서의 활용

  • setTimeout / setInterval: 딜레이 처리를 위한 타이머
  • Promise / async-await: 비동기 API 처리 및 데이터 흐름 제어
  • 이벤트 리스너: 클릭, 입력 등의 UI 이벤트 대기

 

5. 마무리

자바스크립트는 싱글 스레드 언어지만 이벤트 루프 구조 덕분에 비동기 처리가 가능합니다.
실제로는 Promise, async/await, DOM 이벤트 등에서 매우 자주 사용되므로 반드시 이해하고 있어야 합니다.

🚀 콘솔 로그의 순서가 이상해 보일 때, 이벤트 루프를 떠올려 보세요!

반응형

'개발 Story > Javascript' 카테고리의 다른 글

[Javascript] var / let / const 차이  (0) 2025.06.02
반응형

안녕하세요. 여행하는 개발자입니다.
이번 포스팅에서는 자료구조의 기본 중 기본인 Queue와 Stack의 차이점과 Java에서의 사용 예시를 정리해보겠습니다.

1. Stack이란?

Stack(스택)LIFO (Last In, First Out) 구조를 가진 자료구조입니다.
가장 나중에 들어온 데이터가 가장 먼저 나가게 됩니다.

  • push: 데이터 삽입
  • pop: 데이터 꺼내기
  • peek: 최상단 데이터 확인
Stack<Integer> stack = new Stack<>();
stack.push(1);
stack.push(2);
System.out.println(stack.pop()); // 2

 

2. Queue란?

Queue(큐)FIFO (First In, First Out) 구조를 가진 자료구조입니다.
먼저 들어온 데이터가 먼저 나가게 됩니다.

  • offer: 데이터 삽입
  • poll: 데이터 꺼내기
  • peek: 맨 앞 데이터 확인
Queue<Integer> queue = new LinkedList<>();
queue.offer(1);
queue.offer(2);
System.out.println(queue.poll()); // 1

 

3. Stack vs Queue 비교

구분 Stack Queue
구조 LIFO (후입선출) FIFO (선입선출)
삽입 push() offer()
삭제 pop() poll()
탐색 peek()로 최상단 확인 peek()로 맨 앞 확인
사용 예 수식 계산, 되돌리기 기능 인쇄 대기열, BFS 탐색

 

4. Java에서의 실무 활용 예

✅ Stack 실전 예제 - 괄호 검사

public boolean isValid(String s) {
    Stack<Character> stack = new Stack<>();
    for (char c : s.toCharArray()) {
        if (c == '(') stack.push(c);       // 여는 괄호는 스택에 넣음
        else {
            if (stack.isEmpty()) return false; // 닫는 괄호가 왔는데 스택이 비어있으면 실패
            stack.pop();                   // 괄호 짝이 맞으므로 스택에서 제거
        }
    }
    return stack.isEmpty(); // 스택이 비어있으면 모든 괄호 짝이 맞음
}

이 코드는 문자열에 포함된 괄호가 올바르게 짝을 이루는지 검사하는 예제입니다.
Stack 자료구조를 사용하여 후입선출(LIFO) 구조로 여는 괄호를 넣고, 닫는 괄호가 올 때마다 짝을 검사합니다.

 

✅ Queue 실전 예제 - BFS 탐색

public void bfs(Node start) {
    Queue<Node> queue = new LinkedList<>(); // BFS 탐색을 위한 큐 선언
    Set<Node> visited = new HashSet<>();     // 중복 방문 방지용 Set

    queue.offer(start);   // 시작 노드를 큐에 삽입
    visited.add(start);   // 시작 노드를 방문 처리

    while (!queue.isEmpty()) {
        Node current = queue.poll(); // 현재 노드 꺼냄
        for (Node next : current.neighbors) { // 인접 노드 순회
            if (!visited.contains(next)) {
                visited.add(next);    // 방문 처리
                queue.offer(next);    // 큐에 추가
            }
        }
    }
}

이 코드는 그래프의 너비 우선 탐색(BFS)을 구현한 예제입니다.
Queue를 사용하여 선입선출(FIFO) 방식으로 탐색을 진행하며, 이미 방문한 노드는 Set을 사용해 중복 방문을 방지합니다.

 

5. 마무리

StackQueue는 각각의 구조와 특징에 따라 문제 해결 방식이 달라지는 핵심 자료구조입니다.
올바른 상황에 올바른 자료구조를 선택하는 것이 효율적인 코드의 첫걸음입니다.

실무에서는 Stack으로 되돌리기(undo), 괄호 매칭 등
Queue로는 메시지 큐, BFS, 요청 처리 대기열 등을 구현합니다.

반응형
반응형

안녕하세요. 여행하는 개발자입니다.
이번 포스팅에서는 자바 컬렉션 프레임워크 중 HashMap과 HashSet의 차이에 대해 정리해보겠습니다.

1. HashMap과 HashSet의 개념

✅ HashMap

HashMapKey-Value 쌍으로 데이터를 저장하는 자료구조입니다.
각 Key는 고유하며, 중복을 허용하지 않습니다. Value는 중복 가능하며, null도 허용됩니다.

Map<String, Integer> map = new HashMap<>();
map.put("apple", 3);
map.put("banana", 5);

 

✅ HashSet

HashSet중복을 허용하지 않는 데이터 집합을 저장하는 자료구조입니다.
내부적으로는 HashMap을 사용하여 구현되어 있으며, 값만 저장하고 Key는 존재하지 않습니다.

Set<String> set = new HashSet<>();
set.add("apple");
set.add("banana");

 

2. 주요 차이점

항목 HashMap HashSet
저장 방식 Key + Value Value만 저장 (Key 없음)
중복 여부 Key는 중복 불가, Value는 중복 허용 모든 값이 중복 불가
내부 구조 배열 + LinkedList (해시 충돌 처리) HashMap에 key만 저장
사용 예 사전, 이름별 나이 저장 등 중복 제거, 유일값 필터링

 

3. 성능

  • 검색/추가/삭제 시간복잡도: 평균 O(1) (해시 충돌 시 O(n))
  • 두 구조 모두 Hashing 기반이기 때문에 성능은 유사

 

4. 실무 사용 예

✔ HashMap이 유리한 경우

  • 학생 번호에 해당하는 학생 객체 저장
  • 카테고리별 상품 목록 정리

✔ HashSet이 유리한 경우

  • 중복된 사용자 아이디 필터링
  • 이미 처리한 데이터 여부 확인

 

5. 마무리

HashMapHashSet은 구조와 목적이 다르지만, 모두 빠른 탐색과 저장을 지원하는 해시 기반 자료구조입니다.

사용 목적에 따라 정확하게 선택하면 성능과 코드의 명확성을 높일 수 있습니다.

반응형
반응형

안녕하세요. 여행하는 개발자입니다.
이번 포스팅에서는 자바 백엔드 개발의 표준이라 불리는 Spring Framework에 대해 정리해보겠습니다.

1. Spring Framework란?

Spring Framework는 자바 기반의 애플리케이션을 쉽게 개발할 수 있도록 도와주는 오픈소스 프레임워크입니다.

IoC(제어의 역전)DI(의존성 주입)을 핵심으로 하며, 다양한 기술(JPA, JDBC, Redis 등)과 연동이 용이합니다.

📌 한 줄 요약:

객체 생성, 관리, 연결까지 자동화해주는 자바 플랫폼

 

 

2. 왜 사용하는가?

  • ✔️ 생산성 향상 - 반복 코드 제거, 설정 자동화
  • ✔️ 유지보수성 강화 - 계층 구조와 모듈 분리
  • ✔️ 기술 통합 - JDBC, JPA, Kafka 등과 유기적 연동
  • ✔️ IoC/DI 기반 - 객체간 의존성을 효율적으로 관리

 

3. 주요 특징

  • IoC (Inversion of Control): 객체의 생성과 생명주기를 프레임워크가 제어
  • DI (Dependency Injection): 의존 객체를 외부에서 주입받음
  • AOP (Aspect-Oriented Programming): 로깅, 트랜잭션 등 공통 관심사를 분리
  • 트랜잭션 관리: 선언적 트랜잭션 지원
  • 모듈화된 아키텍처: 필요한 기능만 선택적으로 사용 가능

 

✅ MVC 기반 웹 애플리케이션 지원

Spring은 MVC 패턴 기반 웹 애플리케이션 구조를 지원하여 웹 계층을 명확히 분리할 수 있습니다.

  • Model: 데이터와 비즈니스 로직 (Service, DTO)
  • View: 사용자에게 보여지는 화면 (Thymeleaf, JSP 등)
  • Controller: 요청 처리 및 Model과 View 연결
@Controller
public class HelloController {
    @GetMapping("/hello")
    public String hello(Model model) {
        model.addAttribute("message", "Hello Spring!");
        return "hello"; // 뷰 이름
    }
}

DispatcherServlet → HandlerMapping → Controller → ViewResolver 등의 구조로 웹 요청 흐름을 자동화해줍니다.

 

4. 장점

  • 🔧 생산성 - 반복 작업 최소화
  • 🧩 유연성 - 모듈 단위 사용 가능
  • 🧠 객체 지향 설계 강화 - SOLID 원칙 적용에 유리
  • 🌐 통합성 - 다양한 기술과 쉽게 연동 가능
  • 🧪 테스트 편의성 - MockBean, SpringBootTest 지원

 

5. 단점

  • 📚 학습 난이도 - 생명주기, 빈 설정 등 진입장벽 존재
  • ⚙️ 초기 설정 부담 - Spring Boot가 많이 완화시켜줌
  • 🔍 복잡한 추상화 - 디버깅이 어려울 수 있음

 

6. 결론

Spring Framework는 단순한 프레임워크를 넘어서 자바 애플리케이션의 아키텍처 기반입니다.

Spring을 잘 이해하면 Spring Boot, JPA, Spring Security, REST API까지 확장 기술도 자연스럽게 익힐 수 있습니다.

🚀 시작은 어렵지만, 제대로 익히면 모든 자바 백엔드 개발의 중심이 될 수 있습니다.

반응형

'개발 Story > Web' 카테고리의 다른 글

[Web] OAuth 2.0과 JWT에 대해서  (2) 2025.06.27
[Web] 서블릿(Servlet) 동작 방식 완전 가이드  (2) 2025.06.26

+ Recent posts