Gallery
Search
프로젝트 후기
백엔드 챌린지에 대한 회고
백엔드 입장에서 프론트엔드에 대한 회고
기타 느낀점
[Spring][258] 프로젝트 완료 후기 회고, 배운점과 아쉬운점
Elasticsearch는 또한 내부적으로 왜 안정적이며, 어떤 알고리즘, 자료구조 등등이기 때문에 빠른것이 보장되는지에 대한 이해도 추가 필요
Elasticsearch가 내부적으로 안정적이며 빠른 이유는 여러 가지가 있습니다.
1.
먼저, Elasticsearch는 Apache Lucene을 기반으로 만들어졌습니다. Lucene은 검색 엔진에 사용되는 강력한 오픈 소스 라이브러리로, 텍스트 인덱싱과 검색에 특화돼 있습니다.
2.
Elasticsearch는 대량의 데이터를 효율적으로 처리하기 위해 여러 가지 알고리즘과 자료구조를 사용합니다. 그 중 하나는 역색인화라는 개념입니다. 역색인화는는 단어가 어느 문서에 나타나는지를 기록해 두어 검색 속도를 향상시키는 데 도움이 됩니다.
3.
Elasticsearch는 분산 시스템으로 설계돼 있어서 여러 노드에 데이터를 분산 저장하고 병렬 처리를 가능하게 합니다. 이렇게 함으로써 데이터 검색 및 분석 작업을 효율적으로 처리할 수 있습니다.
4.
Elasticsearch는 실시간 검색을 지원하고 있어서 데이터가 업데이트되더라도 거의 실시간으로 검색 결과를 반영할 수 있습니다. 이는 대규모의 데이터셋에서도 높은 성능을 제공하는 데 기여하고 있습니다.
엘라스틱 서치의 검색이 빠른 이유?
[Spring][258] Elasticsearch의 Lucene 인덱싱
질문이 나올만한 부분은 본격적으로 아키텍처 기술도입배경부터 시작된다.
전체적인 요청의 흐름?
전체적으로 클라이언트의 HTTP 요청이 어떻게 처리되는지 알려주세요.
•
기본 흐름
그 흐름에서 카프카는 어떤 역할을 하나요?
•
기본적으로 저희는 메인서버와, 이벤트만을 위한 독립적인 서버가 별도로 존재합니다.
•
서버가 나뉘어졌고 그 중개 역할로 카프카가 데이터를 전달하는 역할입니다.(메시지큐)
[Spring][258]
예상 질의 응답 목록 - 프로젝트에 대하여 발표흐름에 따른 분류
예상 질의 응답 목록 - 프로젝트에 대하여 발표흐름에 따른 분류JPQL 쿼리로 직접 검색 방식 정하기
@Repository
public interface ElasticBookSearchRepository extends ElasticsearchRepository<ElasticsearchBook, Long>, ElasticCustomBookSearchRepository {
// 이건 개발자가 직접 연산부를 작성하는 방식임
// 제어권이 개발자에 있음
//List<ElasticsearchBook> findByBookNameContains(String keyword);
// Elasticsearch API의 쿼리문을 작성해서 변경해보는 방법
// 이건 제어권이 Spring Data Elasticsearch 에 있음
// 쿼리를 SPE가 분석함
//QueryStringQuery - 엘라스틱서치 내장 기본 쿼리
@Query("{\"query_string\": {\"query\": \"?0\"}}")
List<ElasticsearchBook> findByQueryStringQuery(String queryString, Pageable pageable);
//조건검색(bool)
//@Query("{\"bool\": {\"must\": {\"query_string\": {\"query\": \"?0\"}}}}")
//List<ElasticsearchBook> findByQueryStringQuery(String queryString, Pageable pageable);
/*
must: [필드] AND [컬럼] = [조건]
must_not: [필드] AND [컬럼] != [조건]
should: [필드] OR [컬럼] = [조건]
filter: [필드] [컬럼] IN ( [조건] )
*/
}
아래 쿼리를 @Query 어노테이션을 통해서 작성하여 검색의 방식을 변경 할 수 있다. 기존 직접 내부 로직을 구성하는 방식과 다르게, 마치 JPQL을 사용하는것 처럼 쿼리문을 작성 할 수 있다. 그럼 마찬가지로 쿼리문이 컴파일 단계에서 검사되지 않는 단점이 있을 것으로 보여진다.
QueryDSL로 구현해보기
build.gradle 의존성 추가
dependencies {
implementation 'com.querydsl:querydsl-apt:4.x.x'
implementation 'com.querydsl:querydsl-elasticsearch:4.x.x'
}
QueryDSL 플러그인 및 태스크 설정
plugins {
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}
querydsl {
jpa = false
querydslSourcesDir = 'src/main/generated'
}
sourceSets {
main {
java {
srcDir 'src/main/generated'
}
}
}
configurations {
querydsl.extendsFrom compileClasspath
}
compileQuerydsl {
options.annotationProcessorPath = configurations.querydsl
}
[Spring][258] Elasticsearch APIs
[2:22] ELB 설명하실 때 https를 언급하셨는데, 발표자료에도 그 내용이 반영되어 있으면 좋을 것 같습니다. 그리고 main server와 event server의 spring architecture를 모두 보여줄 필요는 없고, application server로 정의한 뒤에 해당 server의 스펙을 하나만 보여주고 main server와 event server가 application server 구조로 되어있다고 보여주면 더 깔끔할 것 같습니다.
발표자료 수정✅
대본 수정✅
영상 멘트 수정✅
[2:58] mysql DB로 부터 도서 데이터가 동기화된다는 표현보다는, logstash가 전처리한 도서 데이터가 elasticsearch에 저장되고, 이 데이터가 mysql에 동기화되고 있다라고 표현하는게 맞을 것 같습니다.
발표자료 수정 없음✅
대본 수정 ✅
[Spring][258] 발표 피드백 수정[완료]
Spring Boot
Spring Security
JWT
Swagger
Spring Data JPA
Querydsl
Elasticsearch
[Spring][258]
예상 질의 응답 목록 - 기술스택과 관련된 것들
예상 질의 응답 목록 - 기술스택과 관련된 것들
[Spring][258] 프로젝트 마무리 작업
인용
강용영익유헌
•
질의응답 의견결정 관련내용 모두정리
•
API 명세서 표 정리, 스웨거는 결과인 것이므로 표와는 다르다. 이것은 미루었던 일일뿐.
[Spring][258] [중요공지] 프로젝트 마무리단계 현재 필요한 작업 업무분담입니다.[완료]
Q. 검색 성능이 점진적으로 개선되지 않고 한번에 올랐습니다.
A. 점진적으로 개선되거나 그런 테스트 결과를 원했겠지만, 그렇지 않을 수도 있습니다. 실제로 그렇기도합니다. 오히려 성능이 나쁠때도 있습니다. 하지만 중요한건 그 테스트 자료들의 그렇다고해서 필요 없는것이 아닙니다. 오히려 그런 모습을 연출하려고 조작되면안되고 그대로 나타나는것이 맞습니다. 그래서 지금 결과가 맞는것입니다.
Q. 향후 프로젝트에서 어떤 부분을 중점으로 진행하면 좋을 까요?
A. FTI와 Elasticsearch를 도입한 부분에서 풀텍스트인덱스의 편차가 발생했던 부분을 더 깊게 파보았으면 좋았을것 같습니다. 물론 프로젝트 끝나고도 계속해서 찾아볼 필요가 있습니다.
MySQL의 풀텍인덱스에 대해서 알고리즘이나 자료구조까지 확장해보고 더해서 디폴트 엔진인 InnoDB에 대해서까지 그 차이를 발생시키는것이 무엇인지 알아보기까지..
그 편차의 원인, 무엇이 그렇게 만들었는지 찾아가는 과정을 계속해서 공부해보시는게 도움 될 수 있습니다. 그게 그 부분에 대해서 다른 어느 개발자보다 잘 아는, 왜 이런 현상이 나타나는지 아는 개발자가 되는 깊은 공부 방법입니다.
특히 이런 깊은 공부를 할때는 공식문서를 참고하는것이 가장 좋습니다. 가장 명확한 근거이기 때문입니다.
멘토님의 충고
공통적으로 기술 스택들에 대해서 근거를 잘 정리해오고 있지만 또한 계속해서 보다 더 정리하는 것이 중요합니다. 그 이유는 이는 곧 기술을 선택 또는 판단하는 능력이고, 그 기술을 선택한 것이 맞다는 근거를 만드는 능력. 이게 실무에서 실제로 이루어지기 때문입니다.
프로젝트 끝에 안주하면 안됩니다. 개발공부 시작입니다.
마무리 될 수 있는 부분은 마무리해야겠지만, 앞으로 더해야할것이 많고,
스트레스 관리와 번아웃을 조절해야 합니다.
[Spring][258] [5주차 멘토링] 질문내용 정리와 답변 + 조언

[Spring][258] 간단소개 영상 구상[완료]
발표자료의 구성
첫화면
목차
종료화면
발표자료와 영상의 활용
영상은 피티 흐름에 적절한 내용들을 간결하고 충분하게 설명되도록 구성
씽크는 기본
[Spring][258] 발표 영상&PPT 구성[완료]
프로젝트 기획 배경 설명 (30초)
•
해당 프로젝트를 어떠한 배경과 당위성으로 기획하게 되었는지 설명해주세요.
•
“현실에서 어떠한 문제를 발견하여 어떤 기술을 활용하면 이 문제를 해결할 수 있을 것 같았다.”
시연 영상 재생 (1분)
•
서비스의 핵심이 되는 기능만 시연해 주세요. 즉, 회원가입, 로그인 과정 등은 생략해도 됩니다.
•
서비스 시연에는 변수가 많습니다. 가능한 녹화 영상을 활용하여 발표를 진행해주세요.
[Spring][258] 발표 대본 흐름 구상[완료]
초안
저희 프로젝트의 CI/CD와 소프트웨어 아키텍처를 설명드리겠습니다.
저희조는 CI/CD를 Github Action과 AWS의 S3, Codedeploy를 이용해서 구현하였습니다.
저희 GitHub에서 main 브랜치로 pull request를 보내면 GitHub Action을 이용해서 AWS의 S3와 Codedeploy를 통해 AWS EC2 서버로 배포되는 구조입니다.
그럼 이제 저희가 왜 GIthub Action으로 CI/CD를 구성하였는지 말씀드리겠습니다.
저희는 초기 Jenkins와 GitHub Action 사이에서 고민을 하였습니다.
Jenkins는 별도의 서버를 준비하고 유지보수해야 하는 비용과 노력이 필요하며, AWS CI/CD에 특화된 도구들과의 통합을 위해 추가적인 플러그인이나 설정이 요구됩니다.
Jenkins가 강력한 커스터마이징을 제공하지만, GitHub Actions는 AWS와의 통합 및 관리 측면에서 더 간편하고 효율적인 솔루션을 제공합니다.
또한 GitHub Action을 사용하면 GitHub 리포지토리와의 긴밀한 통합으로 이벤트 기반 워크플로를 쉽게 설정할 수 있고 코드 소스와 CI/CD 파이프라인이 동일한 장소에서 관리되므로 개발자는 별도의 CI 도구로 전환할 필요 없이 편리하게 작업할 수 있기에 선택하였습니다.
그럼 이제 저희 소프트웨어 아키텍처에 대하여 설명하겠습니다.
클라이언트가 요청을 할시 우선 ELB ( ALB )에서 클라이언트가 https 프로토콜을 사용 하는지 살펴보고 http면 https 통신을 하도록 상태코드 301을 통해 리디렉션을 합니다. 클라이언트가 HTTPS 프로토콜을 사용하면 Main Server로 전달해 줍니다. 여기서 클라이언트가 여럿이면 요청을 나누어서 한 서버에서 모든 요청을 처리하지 않게 분배해줍니다. 그 후 Main 서버에서는 클라이언트가 요청한 서비스가 마이크로서비스 아키텍처 구조를 적용한 책 나눔 서비스인지 여부를 파악합니다. 책나눔 서비스는 짧은 시간 대규모의 트래픽이 예상되는 서비스이기에 마이크로서비스 아키텍처 구조를 적용하였습니다. 만약 책나눔 서비스가 아니라면 메인서버에서 자체적으로 처리합니다. 그리고 책나눔 서비스인 경우에는 프로듀서를 통해 카프카로 클라이언트의 요청 데이터를 보내고 이벤트 서버에서는 컨슈머로 요청 데이터를 받은 이후 레디스의 분산락을 통해 동시성 제어를 하며 비즈니스 로직을 처리한 결과를 다시 프로듀서를 통해 카프카 서버로 보냅니다. 메인서비스에서는 컨슈머를 통해 보낸 요청에 해당하는 이벤트 서버의 결과를 비동기적으로 처리하여 클라이언트에게 전달해줍니다.
1차 수정 및 정리
프로젝트의 CI/CD 및 소프트웨어 아키텍처에 대해 안내해 드리겠습니다.
저희 팀은 Github Action, AWS의 S3, 그리고 Codedeploy를 활용하여 CI/CD를 구성했습니다.
이를 통해, Github의 main 브랜치로 pull request가 이루어지면 자동으로 AWS EC2 서버로 코드가 배포되는 시스템을 갖추었습니다.
CI/CD 시스템 선택에 있어, 저희는 Jenkins와 GitHub Action 중 고민 끝에 후자를 선택했습니다. Jenkins는 강력한 맞춤 설정이 가능하지만 별도의 서버 유지 및 AWS와의 통합을 위한 추가 작업이 필요한 반면, GitHub Action은 AWS와의 통합이 용이하고 관리가 더 간단하여 개발자가 더 쉽게 작업할 수 있는 장점이 있습니다.
다음으로 저희 소프트웨어 아키텍처를 설명하겠습니다.
클라이언트의 요청이 들어오면 ELB(ALB)가 이를 받아서 HTTP 프로토콜을 사용하는지 확인하고, 필요한 경우 HTTPS로 리디렉션합니다.
안전한 HTTPS 프로토콜을 사용할 경우 요청은 메인 서버로 전달되며, 이 서버는 요청을 적절히 분산하여 처리합니다.
특히 대규모 트래픽을 처리해야 하는 책 나눔 서비스 같은 경우, 마이크로서비스 아키텍처를 적용하여 요청을 처리합니다. 이는 카프카를 통해 이벤트 기반으로 데이터를 처리하고, 레디스의 분산 락을 활용해 동시성을 제어함으로써 효율적인 서비스를 제공합니다.
마지막으로, 메인 서비스는 이벤트 서버의 결과를 비동기적으로 받아 클라이언트에게 전달합니다.
[Spring][258] CI/CD 및 소프트 웨어 아키텍처 동작 흐름 정리

[Spring][258] 프론트 디자인 작업 _ 책나눔 이벤트 페이지
EC2 Kibana의 외부 접속 환경 설정
Kibana는 Elasticsearch의 데이터를 관제하고 각종 분석도구를 제공합니다. 시각화를 담당하는 HTML + Javascript 엔진이라 볼 수 있습니다.
하지만 EC2에서 구축한 경우 해당 서비스에 접속하기 위해서는 외부 사용자 또는 관리자가 EC2서버의 5601포트에 접근 할 수 있어야 하는데 대부분의 DB는 기본적으로 Localhost를 통한 접근만을 허용하고 있습니다. 따라서 외부 접속에 대한 커넥션을 열어주어야 합니다.
Kibana의 설정 파일은 EC2의 다음과 같은 경로에 위치합니다. vi에디터를 통해 CLI환경에서 파일을 수정합니다.
sudo vi /etc/kibana/kibana.yml
server.host 부분이 기본적으로 localhost로 되어있으며, 이것 조차 주석처리되어 default가 localhost임을 알려주고 있습니다. 해당 부분에 외부 접근을 허용 할 수 있도록 0.0.0.0을 입력해줍니다.
Kibana의 기본 포트는 5601포트입니다.
server.host: "0.0.0.0"
[Spring][258] AWS EC2 Elastic Stack의 Kibana 외부 접속 환경 설정

[Spring][258] EventApply 서버 코드 리팩토링 및 불필요 브랜치 제거 작업

[Spring][258] AWS EC2 Elasticsearch 외부 접속 환경 설정

[Spring][258] AWS EC2에 구현된 Elasticsearch 에 내 외부(RDS) MySQL 데이터 전달 테스트 및 EC2 스케일업
테스트 기준
MySQL Workbench 8.0을 사용하고 검색 키워드는 “황토집 짓기”를 사용한다.
변수는 ngram_token_size = 2, innodb_ft_min_token_size = 1을 설정하여 인덱싱한다.
인덱싱 기법은 구분자 기법, N gram 기법을, 검색 방식은 아래 세 명령어를 사용한다.
SELECT * FROM book WHERE MATCH(book_name) AGAINST('황토집 짓기');
SELECT * FROM book WHERE MATCH(book_name) AGAINST('황토집 짓기' IN boolean mode);
SELECT * FROM book WHERE MATCH(book_name) AGAINST('+황토집 +짓기' IN boolean mode)
예상 결과
구분자 기법 : keyword를 구분자로 분류하여 저장, 검색 시에는 입력값이 저장된 값과 정확히 동일한 데이터를 출력
[Spring][258] FULLTEXT index 설정 및 검색 명령어 간 결과 테스트

[Spring][258] AWS EC2 ubuntu 환경의 Elasticsearch 구축

[Spring][258] 카프카를 통한 서비스 아키텍처 구조 개선
레디스 분산 락이란?
레디스(Redis) 분산 락은 여러 컴퓨터 또는 프로세스 간에 자원의 동시 접근을 제어하기 위한 메커니즘입니다.
분산 시스템에서 여러 노드가 동일한 자원에 접근하려 할 때, 일관성과 순서를 유지하면서 동시성 문제를 해결하는 데 쓰입니다.
레디스 분산 락의 필요성
•
동시성 관리: 복수의 인스턴스가 동일한 데이터를 동시에 수정하려 할 때, 데이터 불일치를 방지합니다.
•
일관성 유지: 분산 시스템 내에서 데이터의 일관성을 유지하며, 작업의 순서를 보장합니다.
[Spring][258] 레디스 분산락을 이용한 동시성 문제 대응
Kafka를 시도하는 이유
1.
현재 진행 중인 프로젝트의 서비스는 대용량 트래픽을 받는 서비스입니다.
2.
확장성이 중요한 경우
3.
탄력성과 고가용성이 중요한 경우
즉 저희 서비스의 확장성과 안정성, 서버 과부하 방지, 동시성 문제를 성능 저하하는 락없이 해결하기 위해 카프카를 시도하였습니다.
[Spring][258] 카프카 도입으로 구조적으로 동시성 문제에 대응

[Spring][258] 소프트웨어 아키텍처 및 CI/CD 가독성 올린 구조로 수정
MySQL DB의 데이터와 Elasticsearch 데이터는 동기화로 일관성을 유지해야 한다.
기존 MVP 기능으로 도서의 정보를 변경하는 부분들이 있습니다. 특히 Elasticsearch와 연결된 도서 목록에서 사용자는 도서 대출, 도서 대출 예약을 진행 할 수 있습니다.
사용자가 도서 대출을 진행하면 도서의 대출 가능 여부를 알 수 있는 Status 컬럼은 POSSIBLE에서 IMPOSSIBLE로 변경됩니다. 해당 기능은 HTML의 Ajax HTTP 요청을 통해 MySQL DB에 있는 대상 book_id의 도서의 상태를 즉시 변경하지만, Elasticsearch는 MySQL의 DB를 Logstash로 가져와서 인덱싱하기 때문에 이 과정에서 새로 목록을 불러와야 할 필요성이 있습니다.
Logstash의 데이터 변경 추적 설정 필요성
기존 Logstash를 통해 MySQL의 DB를 전달하여 색인하는 부분입니다. 709만개의 도서 데이터를 한번에 읽어오는 과정에서 JVM의 메모리 부족 현상이 발생하여 200만개씩 메모리에 로드하고 Elasticsearch로 전달하도록 구성되어있습니다.
# sample.conf
input {
jdbc {
jdbc_connection_string => "jdbc:mysql://localhost:3306/team258"
jdbc_user => "inyongkim"
jdbc_password => "1234"
jdbc_driver_library => "/Users/inyongkim/.gradle/caches/modules-2/files-2.1/mysql/mysql-connector-java/8.0.25/f8b9123acd13058c941aff25f308c9ed8000bb73/mysql-connector-java-8.0.25.jar"
jdbc_driver_class => "com.mysql.cj.jdbc.Driver"
jdbc_paging_enabled => true
jdbc_page_size => 2000000 # 페이징 크기를 조정하세요.
statement => "SELECT * FROM book"
}
}
output {
elasticsearch {
hosts => ["localhost:9200"]
index => "book"
document_id => "%{book_id}"
}
}
하지만 이 부분에서 설정의 문제점이 있습니다.
[Spring][258] MySQL→Logstash→Elasticsearch 의 동기화 처리에서 발생하는 문제

[Spring][258] 현재 프로젝트 내에서 동시성 문제
동시성 제어란?
동시성 제어란 여러 사용자나 시스템이 데이터베이스나 파일 시스템 같은 공유 자원에 동시에 액세스할 때 발생할 수 있는 문제(동시성문제)들을 관리하고 해결하기 위한 기술입니다.
데이터의 일관성과 무결성을 유지하면서, 여러 요청이 서로 간섭하지 않고 동시에 처리될 수 있도록 보장하는 역할을 합니다.
웹 서비스에서의 동시성 문제란?
[Spring][258] 웹 서비스 동시성 제어
1. 개요
동시성 문제를 해결하기 위한 다양한 방법들을 모두 적용해보며 JMeter로 테스트를 하고
최종적으로 결정한 방법을 선택한 과정을 정리하였습니다.
2. 동시성 해결 성능 테스트
책 나눔 신청에 대한 동시성 오류를 해결하기 위해 아래의 4가지 방법을 사용하였습니다.
1.
뮤텍스
[Spring][258] 데이터베이스 및 애플리케이션 레벨에서의 동시성 문제 대응

[Spring][258] CI/CD GitHub Action을 선택한 이유
책 나눔 서비스
책나눔 서비스는 사용자가 지정된 기간 내에 접속하여 원하는 도서를 요청하고 먼저 신청한 사용자가 해당 책을 수령할 수 있는 서비스입니다.
이 서비스는 짧은 시간 내에 많은 사용자가 동시에 도서를 신청하는 방식으로 운영되므로 상당한 양의 트래픽이 발생할 것으로 예상됩니다.
그렇기 때문에, 트래픽의 증가에 유연하게 대응할 수 있도록 서비스 확장성을 고려해야 합니다. 이에 따라, 기존 단일 구조의 모놀리식 아키텍처보다는, 필요에 따라 쉽게 확장할 수 있는 마이크로서비스 아키텍처로 전환하려고 합니다.
[Spring][258] 프로젝트 내 마이크로서비스 아키텍처 적용
마이크로 서비스 아키텍처란?
마이크로서비스 아키텍처는 소프트웨어 개발에서 사용되는 설계 접근 방식 중 하나입니다.
이 아키텍처는 큰 규모의 애플리케이션을 작고 독립적으로 실행 가능한 서비스들의 모음으로 분할합니다.
각 서비스는 특정 비즈니스 기능을 담당하고, 서로 독립적으로 개발, 배포, 운영될 수 있습니다.
마이크로서비스 아키텍처의 주요 특징
1.
분산 구조: 애플리케이션의 기능이 작고 독립적인 서비스로 나뉘어져 있으며, 각각 독립적으로 개발 및 배포될 수 있습니다.
2.
서비스 지향: 각 기능은 서비스로서 개별적으로 기능하며, API를 통해 서로 통신합니다.
[Spring][258] 마이크로서비스 아키텍처

[Spring][258] keyword 검색 시 keyword를 포함한 일부 데이터가 검색되지 않는 문제
MySQL에서 지원하는 FULLTEXT 명령어는 기본적으로 구분자 기법을 사용하고 있기에 단순한 명령어만으로도 인덱싱이 가능하다.
ALTER TABLE book ADD FULLTEXT(book_name);
위 명령어를 입력하면 자동으로 해당 테이블의 해당 컬럼을 기준으로 index를 생성한다.
다만 시간이 오래 걸릴 경우 workbench에서는 timeout이 일어나기에 명령 프롬프트에서 mysql에 접근해 명령어를 입력해 주어야 했다.
FULLTEXT index를 활용한 검색은 LIKE절을 사용하지 않는다. MATCH ~ AGAINST라는 FULLTEXT index를 위한 검색 명령어가 따로 존재한다.
SELECT count(*) FROM book WHERE MATCH(book_name) AGAINST('스프링');
위 명령어를 통해 FULLTEXT index 검색을 활성화할 수 있다.
[Spring][258] 구분자 기법을 활용한 FULLTEXT indexing

[Spring][258] 구분자 기법의 FULLTEXT index가 2자 이하의 단어를 검색할 수 없는 문제
FULLTEXT index
FULLTEXT 인덱스는 문자열 데이터에 대해 전문 검색 기능을 제공하는 인덱스 유형이다. 이를 사용하면 특정 단어 또는 구문이 포함된 문장을 빠르게 찾을 수 있다. FULLTEXT 인덱스는 전체 문장을 토큰으로 분해하여 효과적으로 검색을 수행할 수 있도록 만들어 준다.
FULLTEXT index에는 두 가지 기법이 있다.
구분자(Stopword) 기법
•
공백, 탭, 문장기호, 또는 사용자 정의 문자열을 구분자로 등록
•
구분자 기법은 이렇게 생성한 구분자를 이용하여 내용을 분리, 키워드를 분석하고 결과 단어를 인덱스로 생성해 두고 검색에 이용하는 방법
•
MySQL에는 기본적으로 지정된 구분자가 있으며, 이를 비활성화할 수 있음.
[Spring][258] FULLTEXT indexing
1. 접속 환경 설정
build.gradle 의존성 추가
// Elasticsearch
implementation 'org.springframework.data:spring-data-elasticsearch:5.1.3'
application.properties 연결 주소 입력
# Elasticsearch URL 설정
spring.elastic.url=localhost:9200
ElasticsearchConfig.java를 통한 Elasticsearch 실제 커넥션
application.properties에서 직접 연결 할 수도 있지만 모듈화시켜서 구조를 살펴보고자 했습니다.
@Configuration
public class ElasticsearchConfig extends ElasticsearchConfiguration {
@Value("${spring.elastic.url}")
private String elasticUrl;
@Override
public ClientConfiguration clientConfiguration() {
return ClientConfiguration.builder()
.connectedTo(elasticUrl)
.build();
}
}
[Spring][258] 프로젝트에서 3 Layered Architecture와 Elasticsearch 연결 테스트

[Spring][258] 트러블 슈팅 : SSH 권한 부족 GUI 기반 문제 해결
분산락 이란 동시성 문제를 해결하기 위한 방법 중 하나로 같은 자원에 접근할 시 락을 제공하여 접근이 완료되면 락을 해제하여 다음 순번에게 넘어가는 방식이다.
분산락을 사용하는 이유는 분산된 서버 및 분산된 DB환경에서도 동시성 이슈를 해결하기 위함이다.
분산락을 Redis로 구현하는 방법에는 lettuce, redisson 라이브러리가 있는데 lettuce방식은 스핀락(락을획득하지 못할 경우 계속 요청) 방식으로 redis에 부하가 갈 수 있어 redisson을 사용하여 구현해보았다.
RedissonConfig를 작성하여 Bean에 등록하고
@Configuration
public class RedissonConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private int redisPort;
private static final String REDISSON_HOST_PREFIX = "redis://";
@Bean
public RedissonClient redissonClient() {
RedissonClient redisson = null;
Config config = new Config();
config.useSingleServer().setAddress(REDISSON_HOST_PREFIX + redisHost + ":" + redisPort);
redisson = Redisson.create(config);
return redisson;
}
}
BookApplyDonationService에 아래와 같이 구현하였다.
public MessageDto createBookApplyDonationV4(BookApplyDonationRequestDto bookApplyDonationRequestDto) {
RLock lock = redissonClient.getLock(String.valueOf(bookApplyDonationRequestDto.getBookId()));
try {
if (!lock.tryLock(3, 3, TimeUnit.SECONDS)) {
log.info("락 획득 실패");
throw new IllegalArgumentException("락 획득 실패");
}
log.info("락 획득 성공");
bookApplyDonationService2.createBookApplyDonation(bookApplyDonationRequestDto);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
} finally {
log.info("finally문 실행");
if (lock != null && lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
log.info("언락 실행");
}
}
return new MessageDto("책 나눔 신청이 완료되었습니다.");
}
public class BookApplyDonationService2 {
private final BookRepository bookRepository;
private final BookDonationEventRepository bookDonationEventRepository;
private final BookApplyDonationRepository bookApplyDonationRepository;
private final UserRepository userRepository;
@Transactional
public void createBookApplyDonation(BookApplyDonationRequestDto bookApplyDonationRequestDto) {
Book book = bookRepository.findById(bookApplyDonationRequestDto.getBookId())
.orElseThrow(() -> new IllegalArgumentException("나눔 신청한 책이 존재하지 않습니다."));
if (book.getBookApplyDonation() != null) {
throw new IllegalArgumentException("이미 누군가 먼저 신청했습니다.");
}
BookDonationEvent bookDonationEvent = bookDonationEventRepository.findFetchJoinById(bookApplyDonationRequestDto.getDonationId())
.orElseThrow(() -> new IllegalArgumentException("해당 이벤트가 존재하지 않습니다."));
if (LocalDateTime.now().isBefore(bookDonationEvent.getCreatedAt()) ||
LocalDateTime.now().isAfter(bookDonationEvent.getClosedAt())) {
throw new IllegalArgumentException("책 나눔 이벤트 기간이 아닙니다.");
}
User user = userRepository.findFetchJoinById(SecurityUtil.getPrincipal().get().getUserId()).orElseThrow(
() -> new IllegalArgumentException("해당 사용자는 도서관 사용자가 아닙니다.")
);
BookApplyDonation bookApplyDonation = new BookApplyDonation(bookApplyDonationRequestDto);
bookApplyDonationRepository.save(bookApplyDonation);
bookApplyDonation.addBook(book);
user.getBookApplyDonations().add(bookApplyDonation);
bookDonationEvent.getBookApplyDonations().add(bookApplyDonation);
book.changeStatus(BookStatusEnum.SOLD_OUT);
}
}
[Spring][258] redisson 분산 락 구현

[Spring][258] 동시성 해결 성능 테스트
Redis 분산락 구현 ( AWS Server )
설치 환경
•
AWS EC2 Ubuntu
•
c5.xlarge
[Spring][258] AWS EC2 Redis 분산락 구현

[Spring][258] Elasticsearch의 RestHighLevelClient 사용에 있어 의존성 주입 문제, 버전간의 호환 확인과 버전 명시 주입

[Spring][258] 서버 스케일 업: AWS EC2 인스턴스 업그레이드 및 성능 향상
1. 문제
처음에 bookApplyService.java 에서 redisson으로 분산락을 구현한 코드는 아래와 같다
@Transactional
public MessageDto createBookApplyDonationV4(BookApplyDonationRequestDto bookApplyDonationRequestDto) {
RLock lock = redissonClient.getLock(String.valueOf(bookApplyDonationRequestDto.getBookId()));
try {
if (!lock.tryLock(3, 3, TimeUnit.SECONDS)) {
log.info("락 획득 실패");
throw new IllegalArgumentException("락 획득 실패");
}
log.info("락 획득 성공");
Book book = bookRepository.findById(bookApplyDonationRequestDto.getBookId())
.orElseThrow(() -> new IllegalArgumentException("나눔 신청한 책이 존재하지 않습니다."));
if (book.getBookApplyDonation() != null) {
throw new IllegalArgumentException("이미 누군가 먼저 신청했습니다.");
}
BookDonationEvent bookDonationEvent = bookDonationEventRepository.findFetchJoinById(bookApplyDonationRequestDto.getDonationId())
.orElseThrow(() -> new IllegalArgumentException("해당 이벤트가 존재하지 않습니다."));
if (LocalDateTime.now().isBefore(bookDonationEvent.getCreatedAt()) ||
LocalDateTime.now().isAfter(bookDonationEvent.getClosedAt())) {
throw new IllegalArgumentException("책 나눔 이벤트 기간이 아닙니다.");
}
User user = userRepository.findFetchJoinById(SecurityUtil.getPrincipal().get().getUserId()).orElseThrow(
() -> new IllegalArgumentException("해당 사용자는 도서관 사용자가 아닙니다.")
);
BookApplyDonation bookApplyDonation = new BookApplyDonation(bookApplyDonationRequestDto);
bookApplyDonationRepository.save(bookApplyDonation);
bookApplyDonation.addBook(book);
user.getBookApplyDonations().add(bookApplyDonation);
bookDonationEvent.getBookApplyDonations().add(bookApplyDonation);
book.changeStatus(BookStatusEnum.SOLD_OUT);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
} finally {
log.info("finally문 실행");
if (lock != null && lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
log.info("언락 실행");
}
}
return new MessageDto("책 나눔 신청이 완료되었습니다.");
}
위와 같이 작성 시 기존 10개정도 발생하는 동시성 문제가 2~3개로 줄어들었으나,
2~3개의 동시성 문제가 발생되는 문제가 있었다.
2. 원인
@Transactional
public MessageDto createBookApplyDonationV4(BookApplyDonationRequestDto bookApplyDonationRequestDto) {
//생략
log.info("언락 실행");
}
}
return new MessageDto("책 나눔 신청이 완료되었습니다.");
}
위에서 작성한 코드의 일부이다.
[Spring][258] redisson 분산 락 구현 문제
Q. 한 주간 카프카에 대해서 아래와 같이 탐구하고 프로젝트에 적용해 보았습니다.
이러한 방법이 올바른 접근인지 피드백을 받고 싶습니다.
‣
A. 멘토님답변정리!
접근 O, 더 심도있게 다룰 필요 O
Q. 아키텍처 구성과 관련된 질문 메인서버와 트래픽서버의 구분 이 방식에 대한 의견?
A. 멘토님답변정리!
아키텍처의 구성
메인,트래픽 서버를 나누는것보다는 도메인을 기준으로 나누는것이 확장, 유지보수적으로 유리 할 수 있다. → 추후 메인과 트래픽서버의 모호함이 발생하게 되므로
Q. Elasticsearch의 도서 데이터를 넣기 위해서 Logstash를 설치하여 설정 파일을 통해 MySQL DB의 도서 데이터를 전달해보고있습니다.
우선 이러한 방법이 올바른 접근 방법인지 알고 싶습니다.
대량 데이터는 아래처럼 나누어서 로드하는게 맞는지? 아니면 JVM의 메모리를 증가시키는지?
처음엔 별도의 설정 없이 710만건의 도서를 한번에 메모리에 올리고 전달하는 설정을 했고 JVM Heap의 OutOfMemory 문제가 나타났습니다.
따라서 jdbc_paging_enabled 옵션을 추가하여 50000건씩 로드하는 과정으로 나누니 메모리 문제는 나타나지 않았습니다.
설정파일의 주기에 대한 이해도 부족)
하지만 710만건을 다 로드하는데 다시 0부터 로드하고 있습니다.
→ 수가 데이터 카테고리로 테스트해보니 계속 로드하는것이 동일하게 나타났고, 무엇인지 생각해보니 설정 파일의 schedule => "* * * * *" # Query주기 설정에 따른것이고,
계속해서 그래서 매 분마다 SELECT * FROM book_category 쿼리를 실행하여 데이터를 가져오고 Elasticsearch로 전송하고 있는 것인것 같습니다. Logstash가 계속해서 데이터를 가져오고 Elasticsearch에 전송하는 동작을 수행하는것으로 정리.
그럼 계속 도서는 50000건씩 710만을 모두 돌면 계속해서 데이터를 가져오기 위한 동작인것이고, 이해가 됩니다.
그럼 주기가 길면 어떤 문제가 발생하는지 알고싶습니다. 그 사이 업데이트, 삭제 등 없어진 데이터가 있으면 아직 로드되지 않은 상태에서 검색의 오류가 나타날지 등..
[Spring][258] [4주차 멘토링] 질문내용 정리와 답변
[Spring][258] Jmeter을 통한 대용량 트래픽 발생 성능 테스트

[Spring][258] BookRent 동시성 문제 해결

[Spring][258] 웹 서비스 카프카 적용 주간 정리

[Spring][258] Elasticsearch 검색 성능 개선 05(X) - 대량 데이터 넣기 실패과정과 데이터 재가공을 통한 문제 해결
Elasticsearch 설치 이후, Logstash와 Kibana 설치 이유
Elasticsearch를 단순 DB로 보고 데이터를 넣어보고자 했으나, 궁극적으로 Elasticsearch를 사용하는 목적에 큰 영향을 주는 부분이 아니라고 판단되었습니다. Logstash를 사용하여 다양한 데이터 소스에서 데이터를 수집하고 Elasticsearch에 전송하는 것은 일반적인 방법이므로 곧바로 적용해보기로 결정했습니다.
또한 Kibana라는 분석 툴에서 어떤것들을 확인 할 수 있는지 확인하고자 이 또한 설치하면서 ELK 스택을 모두 설치해보는 기본 환경 구축을 진행하고자 합니다.
Logstash, Kibana 설치
Elasticsearch 설치 과정은 아래 링크
brew tap elastic/tap
brew install elastic/tap/logstash-full
[Spring][258] Elasticsearch 검색 성능 개선 04 Logstash, Kibana 추가 설치 ELK stack 구성
MySQL - Logstash - Elasticsearch의 연결을 하는 이유?
간단히 mysql의 데이터를 사용해서 elasticsearch에서 검색을 하기위해서는 elasticsearch DB에 데이터를 담아야합니다. 이를위한 도구로서 Logstash를 사용하게 됩니다.
Logstash을 사용하여 MySQL과 Elasticsearch 간의 데이터 전송을 쉽게 설정할 수 있습니다. Logstash은 다양한 데이터 소스에서 데이터를 수집하고 처리하여 다른 저장소로 전송하는 역할을 합니다.
해당 과정은 Elasticsearch, Logstash, Kibana인 ELK 스택을 설치한 이후로 진행되는 과정입니다. 아래 링크를 통해 설치와 실행 상태를 확인후 진행해야 합니다.
설치와 관련된 링크 참고
우선 Logstash를 통해 Elasticsearch에 데이터가 전달되어야 한다.
Logstash를 통한 Elasticsearch 데이터 전달 설정 절차
1.
Logstash 설치: 먼저 Logstash을 설치합니다.
[Spring][258] Elasticsearch 검색 성능 개선 05 MySQL→Logstash-Elasticsearch 연결 설정, Heap Space 부족 트러블 슈팅

[Spring][258] Elasticsearch 설치 이후, Logstash와 Kibana 설치 ELK Stack 환경 구축

[Spring][258] 트러블 슈팅 : CodeDeploy 배포 중 발생한 sh 파일 생성 오류 해결 사례

[Spring][258] Elasticsearch란?
문제 상황
웹 서비스 구성
카프카 서버를 사용하는 서비스에서 메인 서버와 트래픽 서버가 각각 2개씩 존재합니다.
메인 서버에서는 프로듀서를 통해 메시지를 보내고, 트래픽 서버에서는 컨슈머를 사용해 메시지를 받아 비즈니스 로직을 처리한 후 다시 프로듀서로 결과를 보냅니다.
메인 서버에서는 비동기 처리로 결과를 받아 클라이언트에게 뷰를 반환합니다.
서버와 토픽의 파티션을 확장한 후에 메시지 처리에 문제가 발생했습니다.
메인 서버1에서 트래픽 서버1로 데이터를 보냈을 때, 결과를 메인 서버1 쪽으로 전달하지 못하는 문제를 파악했습니다.
[Spring][258] 트러블 슈팅 : 카프카 컨슈머 그룹 설정 오류로 인한 메시지 처리 문제
Elasticsearch를 활용한 성능개선을 위해서 진행해 볼 순서를 정리해보았다.
1.
Elasticsearch 설치
2.
도서 데이터를 Elasticsearch에 넣고 JMeter로 테스트해보기
3.
MySQL과 Elasticsearch의 성능 차이점 비교하기
4.
Beats 를 추가해보고 JMeter로 테스트해보기
5.
이전과 차이점 비교하기
6.
Logstash 추가해보고 JMeter로 테스트해보기
7.
이전과 차이점 비교하기
[Spring][258] Elasticsearch 검색 성능 개선 02 - Elasticsearch 활용해보기 순서 정리
Apache Kafka는 대용량의 실시간 데이터 스트림 처리를 위한 분산 메시징 시스템입니다.
Kafka를 통해 서버 간 비동기 통신을 구현하고, 대량의 데이터를 빠르고 안정적으로 처리할 수 있습니다.
Kafka 메시지 처리의 기본 흐름
1.
메시지 생성 및 전송
프로듀서는 메시지를 생성하고 이를 Kafka 토픽의 특정 파티션에 전송합니다.
메시지는 correlationId 같은 식별 정보를 포함하고 있어, 나중에 처리 결과를 정확히 매칭할 수 있습니다.
1.
메시지 저장
Kafka 서버는 받은 메시지를 파티션에 저장합니다.
[Spring][258] Kafka에서의 효율적인 메시지 처리: 컨슈머 그룹 ID의 활용

[Spring][258] Elasticsearch 사용을 위한 로컬 환경 구축 기본

[Spring][258] 트러블 슈팅 : AWS CodeDeploy를 이용한 웹서비스 배포 중 환경 변수 문제

[Spring][258] Elasticsearch 검색 성능 개선 03 - Elasticsearch 설치와 테스트 실행 및 설치과정 사소한 트러블슈팅

[Spring][258] Elasticsearch 검색 성능 개선 01 - Elasticsearch란?

[Spring][258] CI/CD AWS Ec2 서버 다중 배포

[Spring][258] BookApplyDonation에서의 N+1 문제 해결 및 성능 테스트

[Spring][258] 트러블 슈팅 : SSH 키 파일 권한 문제로 인한 연결 실패

[Spring][258] 트러블 슈팅 : 컨슈머 서버가 카프카 서버 토픽의 파티션을 할당 받지 못하는 문제
초기에 Kafka를 도입할 때 Kafka의 구조와 특성으로 동시성 문제를 자연스럽게 해결할 수 있을 것이라 기대하였습니다.
Kafka는 메시지를 파티션 단위로 관리하며, 각 파티션은 순서를 가진 로그로 구성되어 있습니다.
따라서 단일 파티션에 대해서는 프로듀서와 컨슈머 간의 동시성 문제가 크게 발생하지 않습니다
그러나 Kafka 환경이 확장되면서 여러 파티션이 생성되고, 각각의 파티션에 대해 병렬 처리가 필요해지기 시작하면서 동시성 문제가 구조적으로 해결되지 않는다는 것을 알게 되었습니다.
각각의 파티션에 대해 독립적으로 메시지를 처리하지만, 전체 시스템의 관점에서 보면 동시에 여러 작업이 이루어지기 때문에 동시성 문제가 발생할 수 있기 때문입니다
이러한 문제를 해결하기 위해 방법을 찾던 도중 분산 락이라는 것을 알게 되었습니다.
redis나 ZooKeeper와 같은 분산 락 서비스를 사용하여 리소스에 대한 접근을 제어함으로써 동시성 문제를 해결할 수 있을 것으로 추정됩니다.
여러 컨슈머가 동일한 리소스에 접근해야 하는 상황에서 ZooKeeper를 사용하여 분산 락을 구현함으로써, 한 번에 하나의 컨슈머만이 리소스에 접근할 수 있도록 제어하는 방향으로 말입니다.
결론적으로 Kafka를 통한 분산 처리와 함께 분산 락을 적절히 사용함으로써, 데이터베이스 락에서 발생할 수 있는 데드락의 위험을 줄이면서 동시성 문제를 해결하는 방향으로 진행할려구 합니다.
[Spring][258] Kafka를 활용한 분산 처리 환경에서의 동시성 문제 해결을 위한 고찰
1. 개요
팀 프로젝트를 진행하며 동시성 문제를 해결하기 위해 다양한 방법들을 사용해 보았으며
이 방법들을 모두 포함한 프로젝트를 AWS를 이용한 서버에 올려서 구동하였다.
서버에서 동시성 부하 테스트를 진행하며 성능을 측정해보았다.
2. 테스트 환경
메인서버 : t2.micro(??)
트래픽(카프카) 서버 : t2.micro(??)
카프카서버 : t3.small(??)
[Spring][258] 동시성 해결 기법들 Jmeter 부하 성능 테스트

[Spring][258] 뷰페이지 접속시 user쿼리 추가로 2번 발생하는 문제 해결

[Spring][258] 트러블 슈팅 : Kafka 리스너(컨슈머)가 Kafka 서버에서 데이터를 읽지 못하는 문제 발생 및 해결

[Spring][258] 무한스크롤(Infinity Scroll) 기능 구현

[Spring][258] Pagenation→Slice→LoadMore→InfinityScroll 변화로 보는 성능 및 사용자경험의 선택과정

[Spring][258] EC2 Kafka 서버 적용 ( 테스트 환경 : 로컬 )
개요
문화빅데이터포털에서 공공도서관 소장도서 목록을 csv 파일들로 받아 정리하고
서적분류코드를 BookCategory로 정리하여
DB에 삽입한 과정을 작성하였다.
1. Book 데이터 다운로드 및 가공
문화빅데이터 플랫폼에서 공공도서관 소장도서 목록을 다운로드하였다.
22년 3월도 데이터를 다운받았으며 csv 파일 10개 가량이다.
팀원분이 파이썬을 이용하여 필요한 데이터셋(저자, 출판년도, 책이름, 분류기호) 만 추출하고 중복을 제거하였다.
[Spring][258] bookData 가공 및 bookCategory MySQL 삽입

[Spring][258] 더보기(No Offset) 기능 구현
프로젝트에 수백만 건의 데이터를 불러오는 페이지가 있다. 그런데 이 모든 데이터를 불러오면 속도도 느리고 사용자 입장에서 이를 보는 경험도 좋지 않다. 그래서 일반적으로는 데이터를 불러올 때 일부만 불러오는 것이 일반적이다. 스프링에서는 Slice와 Page를 통해 이를 구현할 수 있다.
Page
JPA에서 데이터를 호출하면 Pageable을 기준으로 List를 생성하여 그 양식에 맞게 데이터를 저장한다. 데이터를 호출할 때 Pageable이라는 인스턴스를 생성해 입력해 주는데, Pageable에는 페이지 번호, 페이지 크기, 그리고 정렬 기준과 순서의 데이터를 갖는 Sort라는 객체를 필드로 갖는다.
Pageable을 통해 데이터를 호출하면, 전체 데이터를 PageSize만큼 자른 후, PageNumber번째의 데이터들을 불러오게 된다. PageNumber는 0부터 시작하여 순차적으로 올라가게 된다.
Page 객체를 통해 데이터를 불러오는 경우, 총 데이터 갯수를 함께 저장하여 총 Page 수를 계산하게 된다.
프로젝트에서 Page를 사용해 Book데이터를 구현하였다.
public Page<BookResponseDto> getAllBooksByCategoryOrKeyword3(String bookCategoryName, String keyword, int page) {
QBook qBook = QBook.book;
BooleanBuilder builder = new BooleanBuilder();
List<BookCategory> bookCategories = null;
if (bookCategoryName != null) {
BookCategory bookCategory = bookCategoryRepository.findByBookCategoryName(bookCategoryName);
bookCategories = saveAllCategories(bookCategory);
}
if(keyword != null)
builder.and(qBook.bookName.contains(keyword));
if(bookCategories != null)
builder.and(qBook.bookCategory.in(bookCategories));
Sort sort = Sort.by(Sort.Direction.ASC, "bookId");
Pageable pageable = PageRequest.of(page, 20, sort);
Page<BookResponseDto> bookList = bookRepository.findAll(builder, pageable).map(BookResponseDto::new);
System.out.println(bookList.getTotalElements());
return bookList;
}
[Spring][258] Page와 Slice를 통한 데이터 호출 제어

[Spring][258] Kafka 적용한 서버 구조 변경

[Spring][258] 프로젝트에 Kafka 서비스 적용

[Spring][258] Kafka Consumer Group 설정에 대한 고찰

[Spring][258] 트러블 슈팅 : Jackson 라이브러리와 LocalDateTime 타입 처리 문제 해결

[Spring][258] Kafka를 활용한 실시간 데이터 처리: 시스템 구조 및 개선 방향 고찰

[Spring][258] 트러블 슈팅 : Spring Kafka를 사용하여 메시지를 송수신하는 과정에서 org.springframework.kafka.KafkaException: Seek to current after exception 오류가 발생

[Spring][258] 카프카 적용의 적정선: 효율적인 시스템 구조 설계를 위한 고찰

[Spring][258] 트러블 슈팅 : Kafka Consumer 서비스가 `bookDonationEventApplyOutput` 토픽으로부터 메시지를 받아 처리하는 도중 `List<UserResponseDto>` 타입으로의 변환 과정에서 `ListenerExecutionFailedException` 오류가 발생
Q. 문제 이번주부터 성능 개선에 대한 진행 방법과 진지하게 고민해보고 있습니다. 문제 해결하는 과정을 아래와 같은 방식으로 스토리를 정리해나가면 될까요?
문제상황 → 해결방안 → 의견조율 → 의견결정 → 비교 결과들 수집과 정리..
문제상황)
기획 당시 검색 기능과 페이지 기능 개선이 가장 중요한 토픽이 될 것으로 예측했었습니다.
현재 MVP 구현 단계에서는 실제 운영되고 있는 서비스들과 비교했을때 사용자가 불편함을 느낄 수 있을 수준으로 성능에 이상이 있음을 느끼고 있습니다.
(좀 더 구체적이여야 하는지?)
해결방안)
기본적으로 저희가 할 수 있는 방안 탐색은 구글링, 타 프로젝트 레퍼런스 체크 정도가 맞는지.
저희가 하고있는 수준은 구글링, ChatGPT를 통한 키워드를 최대한 탐색하고 적용할 수 있는 기술스택이나 라이브러리가 어떤것이 있는지 찾고 있습니다.
의견조율)
팀적으로 회의를 통해서 위 내용에 대해서 공유하면서 적절한 기술스택인지를 판단해봅니다. 기본적으로 경험자가 없기때문에 사실 이 부분의 신뢰도가 부족합니다. 더 많은 자료를 탐색해보는 것이 해답인지 궁금합니다.
의견결정)
어떠한 개선사항 토픽 하나에 대해 몇가지 기술 스택을 사용해보자는 의견이 모였다면,
예로 검색기능 개선에서는 아래와 같은 의견들을 모을 수 있었습니다.
‣
업무 분담은 어떤식으로 하는 것이 효율적이고 팀전체가 성능개선에 참여한 느낌을 줄지 궁금합니다.
팀원이 모두 각자 적용해보고 비교해보는 것은 뭔가 팀적인 움직임이 아닌것 같은 느낌입니다. 모두가 동일한것을 해보니 실력적인 향상은 가장 좋을 것 같지만 마치 그냥 동일한 프로젝트를 개인프로젝트처럼 진행하는 느낌일 것 같습니다..
아니면 일관성을 유지하기 위해서 전체 토픽을 맡아서 진행해보는게 맞는지
아니면 제가 구분해 둔 중간 토픽 정도로 분담하는것이 좋을지
A. 멘토님답변정리!
팀적으로 회의를 통해서 위 내용에 대해서 공유하면서 적절한 기술스택인지를 판단해봅니다. 기본적으로 경험자가 없기때문에 사실 이 부분의 신뢰도가 부족합니다. 더 많은 자료를 탐색해보는 것이 해답인지 궁금합니다.
A. 멘토님답변정리!
업무 분담은 어떤식으로 하는 것이 효율적이고 팀전체가 성능개선에 참여한 느낌을 줄지 궁금합니다.
현재는 분담하더라도 모든것을 혼자 할 수도 없으며, 모든것을 같이 하는것도 문제가 있다.
결국 팀프로젝트기 때문에 분담해야 하며, 프로젝트가 종료되더라도 다른 팀원의 코드, 기능을 내가 직접 다뤄보고 차이점을 느끼고, 그 선택과정을 살펴볼 필요가 있다.
이러한 프로젝트 복기를 해보면서 장기적으로 나만의 정리를 해보면서 공부해야 한다.
Q. 문제 해결을 위한 많은 기술적 해결 방안들이 있었는데, 블로그든 공식 문서든 각 기술들의 장 단점들을 설명해도 와닿지 않는 부분들이 많아 여러 기술들을 프로젝트에 적용해 보는 것이 좋을 것 같다는 생각이 들었습니다.
프로젝트를 할 때, 각 기술 간의 장 단점을 기준으로 가장 필요한 것을 선택한 후 하나만 적용해 프로젝트 코드를 작성하는 것이 좋을지, 아니면 각 기술별 메소드를 생성해서 일단 코드를 작성해 보는 것이 좋을지 궁금합니다.
또한 프로젝트에서 해당 기술들에 대한 분석을 프로젝트 내에서 끝낸 후에, 분석 과정들을 코드로 남겨두는 것이 좋은지, 그 중 가장 좋다고 판단한 버전을 제외하면 모두 삭제하는 것이 옳은 것인지 궁금합니다.
A. 멘토님답변정리!
기술스택을 써봐야 아는것이기 때문에, 학습과정에서 현재 사용하는것을 우선하는것은 어쩔수없는 선택이고, 틀린 선택이 아니다. 직접 해보는 것이 맞다.
만약 효과가 없더라도, 그것 자체도 경험으로 쌓아두는게 좋다. 실무에서는 데드라인이있기 때문에, 그 실패 경험도 추후 기술 선택에서의 실수를 줄일 수 있는 경험이기 때문에 쌓아두는게 좋다.
과거 코드 및 주석도 우선 모두 남겨놓는게 좋다. 스터디 과정의 프로젝트기 때문에, 브랜치도 삭제할 필요는 없다.
배포 최종본만 가장 깔끔하게 release 태그화 시켜놓을 필요는 있다.
Q. 현재 구현된 책 대출/예약 프로세스는 아래와 같습니다.
대출 : 로그인한 유저가 직접 버튼을 눌러 책을 대출/반납하는 방식
예약 : 대출된 책이 반납되는 즉시 첫번째 예약자가 자동으로 책을 대출
ebook을 대출하는 것이 아니기 때문에 위 방식은 실물 책을 사용하는 도서관 서비스에는 부적합하다고 생각됩니다.
학습용 프로젝트로서 위의 대출/예약 프로세스를 유지해도 상관없는지, 혹은 실제 서비스에 대입 가능하도록 프로세스를 바꿔야 하는지 문의드립니다.
[Spring][258][3주차 멘토링] 질문내용 정리와 답변

[Spring][258] 트러블슈팅 : 프로듀서에서 메시지를 전송하는 과정에서 역직렬화 문제 발생

[Spring][258] Kafka를 활용한 실시간 데이터 처리: 시스템 구조 및 개선 방향 고찰

[Spring][258] 카프카 기초 구현

[Spring][258] 카프카를 통한 동시성 문제 해결 고민
공통 - Controller, 검색 키워드와 페이징 처리를 위한 페이저블 객체
우선 공통적으로 적용된 컨트롤러 코드는 다음과 같습니다.
@Controller
@RequiredArgsConstructor
public class BooksViewController {
private final AdminCategoriesService adminCategoriesService;
private final AdminBooksService adminBooksService;
@GetMapping("/admin/booksManage")
public String adminBooksManageView( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size, // 현재 페이지, 페이지 크기
@RequestParam(defaultValue = "bookId") String sort, @RequestParam(defaultValue = "ASC") String direction, // 정렬 기준, 정렬 방향
@RequestParam(value = "keyword", required = false) String keyword, // 검색 키워드
@AuthenticationPrincipal UserDetailsImpl userDetails, Model model) {
long startTime = System.currentTimeMillis();//실행시간 측정
Sort.Direction sortDirection = Sort.Direction.fromString(direction);
PageRequest pageRequest = PageRequest.of(page, size, sortDirection, sort);
BooksPageResponseDto booksPageResponseDto =
adminBooksService.findBooksWithPaginationAndSearching(userDetails.getUser(), keyword, pageRequest);
model.addAttribute("books", booksPageResponseDto.getAdminBooksResponseDtos());
model.addAttribute("currentPage", page);
model.addAttribute("totalPages", booksPageResponseDto.getTotalPages());
long endTime = System.currentTimeMillis();
long durationTimeSec = endTime - startTime;
System.out.println(durationTimeSec + "m/s"); // 실행시간 측정
return "admin/booksManage";
}
프론트엔드로부터 keyword 를 QueryString으로 …?keyword=abc와 같은 형태로 HTML폼으로부터 특정 값들을 추출 할 수 있으며, HTTP요청에 담아 컨트롤러로 문자열을 전달 할 수 있습니다.
@RequestParam(value = "keyword", required = false) String keyword, // 검색 키워드
페이지 처리를 위해서는 순수 JPA를 사용하게 되면 Total Count를 뽑아와서 현재 내가 몇번째 페이지인지를 계산해서 표시를 해주어야 합니다. 그런데 이런 계산을 모두 직접 계산하는 메소드, 변수를 직접 생성하고 매개변수로 Service에 전달하게 되면 코드가 복잡해지게 됩니다! 페이저블 객체를 사용하지 않는 경우 추가되어야 하는 코드는 다음과 같습니다.
// 직접 offset과 limit 계산
int offset = page * size;
int limit = size;
// 전체 페이지 수 계산 메서드
private int calculateTotalPages(long totalCount, int pageSize) {
return (int) Math.ceil((double) totalCount / pageSize);
}
BooksPageResponseDto booksPageResponseDto =
adminBooksService.findBooksWithPaginationAndSearching(userDetails.getUser(), keyword, offset, limit, sortDirection, sort);
[Spring][258] JPA → JPQL → QueryDSL 변화과정

[Spring][258] 프로젝트 성능 개선 : 대용량 데이터 처리를 위한 페이징에서 슬라이스로의 전환
이전 글에서 우리는 세마포어를 사용하여 동시성 문제를 해결하는 방법을 살펴보았습니다.
그러나 동시성 제어에 대한 탐구는 여기서 그치지 않고, 이번 글에서는 뮤텍스(Mutex)를 활용하여 동시성 문제를 해결해보려고 합니다.
뮤텍스란?
뮤텍스(Mutex)는 여러 스레드가 동시에 공유 자원에 접근하는 것을 막아주는 동기화 메커니즘입니다.
뮤텍스는 한 번에 하나의 스레드만 공유 자원에 접근할 수 있게 하여 데이터의 일관성을 유지할 수 있도록 도와줍니다.
뮤텍스를 사용한 동시성 문제 해결 과정
[Spring][258] Spring 뮤텍스를 활용한 동시성 문제 해결
검색 구현 후 느린 검색 속도
•
도입 이유
•
문제 상황
•
해결 방안 의견
•
의사 결정
검색 결과가 다수 나타날 때 [페이징] 자체가 성능을 좌우하는 문제
•
도입 이유
[Spring][258] 검색관련 문제해결 및 성능 개선 방안 수집
1.
락공유 자원에 접근할 때 그 자원을 사용하는 스레드만이 접근하도록 제한한다.다른 스레드들은 대기한다.
•
뮤텍스(Mutex), 세마포어(Semaphore)
1.
트랜잭션
2.
데드락 회피
[Spring][258] 동시성 문제 해결 및 성능 개선 방안 수집

[Spring][258] 프로젝트 성능 개선 : 대용량 데이터 처리를 위한 페이징에서 슬라이스로의 전환
세마포어란?
세마포어는 다중 스레드 프로그래밍에서 동시에 공유 자원에 접근하는 스레드의 수를 제한하여 동시성 문제를 방지하는 데 사용되는 변수나 추상 데이터 타입입니다.
세마포어는 특정한 수의 토큰을 가지며, 스레드가 세마포어를 획득하려면 토큰을 얻어야 합니다.
토큰이 없다면, 스레드는 대기 상태에 들어갑니다.
세마포어는 주로 두 가지 종류가 있습니다
이진 세마포어와 카운팅 세마포어이진 세마포어는 0 또는 1의 값을 가지며, 상호 배제를 위해 사용됩니다.
카운팅 세마포어는 0 이상의 값을 가질 수 있으며, 제한된 수의 자원에 대한 접근을 관리합니다.
현제 이 글에서 동시성 문제 해결을 위해서 이진 세마포어를 사용했습니다.
[Spring][258] Spring 세마포어를 통한 동시성 오류 해결
문제
Spring Boot에서 Controller 계층의 메서드를 테스트하던 중 NullPointerException이 발생했다.
@RequestParam을 사용하여 요청 파라미터를 받아오는 과정에서 파라미터의 기본값이 null로 설정되어 있었고, 이로 인해 테스트 중에 null 값이 메서드로 전달되었다.
Mockito를 사용하여 서비스 계층의 메서드를 모의(Mock)할 때 any(클래스.class) 형태로 작성하였지만, 실제로는 null 값이 전달되어 문제가 발생했다.
문제 분석
@RequestParam의 기본값이 null로 설정되어 있고, Mockito를 사용하여 서비스 계층의 메서드를 모의할 때 any(클래스)를 사용했기 때문에, null 값을 받아들일 수 없었다.
[Spring][258] 트러블 슈팅 : Spring Boot 테스트: @RequestParam의 기본값과 Mockito any() 메서드 사용 시 주의점

[Spring][258] 트러블 슈팅: `BookServiceTest`에서 `NullPointerException` 발생 문제
Q. 페이징 처리에 관련된 코드 질문
우선 페이저블 객체를 사용하는것이 표준화되었다고 해서 페이저블 객체로 페이징을 구현해보았습니다.
A. 멘토님답변정리!
Q. 페이징에 대해 아래와 같이 접근해보았습니다.
책 나눔 이벤트와 도서 사이에 1대N의 관계가 입니다.
처음에는 책 나눔 이벤트에서 도서를 패치 조인하여 페이징을 시도했는데 메모리에서 페이징이 되어 오작동의 위험이 있다고 알게 되었습니다.
그래서 도서 쪽(N)에서 패치 조인하여 페이징 처리를 진행했습니다.
이런 방식이 옳은 접근인지 궁금하고, 책 나눔 이벤트(1)에서 JPQL을 사용하여 직접 쿼리를 작성해 페이징을 시도할 때 발생하는 구체적인 문제점이 무엇인지 알려주실 수 있을까요?
그리고 도서 쪽(N)에서 페이징을 처리할 때 겹치는 문제나 다른 이슈가 있을 수 있는지도 여쭤보고 싶습니다
A. 멘토님답변정리!
우선 페이징 처리는 1이던 N이던 어디에서든 필요하다. 쿼리문이 어떤 식으로 작동되는지 보고 파악 할 필요가 있다.
페이징 처리를 하는 대상을 직접 가져오는 방법이 맞다.
Q. 코드 디버깅 중 특정 이슈에 직면했습니다. 디버깅을 통해 프로그램의 흐름을 멈췄음에도 불구하고 디버거를 통해 해당 객체를 조회하니 실제 쿼리가 전달되었습니다.
그 결과 디버거에서와 콘솔에서의 결과가 달라 혼란을 겪었습니다.
제가 문제를 탐색한 결과, Java와 ORM 프레임워크를 사용할 때 디버거로 객체의 상태를 확인하려고 할 때 그 객체에 설정된 지연 로딩이 동작하게 되어 이러한 문제가 발생하는 것을 알게 되었습니다.
이를 "디버거 지연로딩 트랩"이라고 하는 것 같더군요.
A. 멘토님답변정리!
객체에 어떤 데이터가 있을 지 디버거 커서가 넘어갈때 발생하는 문제인데,
실제 코드 실행과 디버깅 환경은 다를 수 있다. 문제가 아니라, 디버거를 사용하여 객체의 상태를 확인하면서 발생하는 데이터베이스 쿼리가 있을 수 있음에 주의해야 하며, 이로 인한 부작용을 방지하기 위해 충분한 주의가 필요합니다.
[Spring][258] [2주차 멘토링] 질문내용 정리와 답변
문제 상황
1.
JPA 페이징 처리 문제BookDonationEvent와 Book 간의 1대다 관계에서 BookDonationEvent 중심의 페이징 쿼리 실행 시 원하는 결과를 얻지 못하는 현상이 발생했다.
커스텀 커리를 작성하였지만 정상적으로 동작하지 않는다.
도서와 event가 1대 다 관계라서 정상적으로 작동하지 않는 걸로 추정된다.
@GetMapping("{donationId}")
public String bookApplyDonationEventPage(Model model, @PathVariable Long donationId) {
BookDonationEvent bookDonationEvent = bookDonationEventRepository.findById(donationId).orElseThrow(
() -> new IllegalArgumentException("해당 이벤트가 존재하지 않습니다.")
);
List<Book> books = bookDonationEvent.getBooks().stream().filter(book -> book.getBookStatus().equals(BookStatusEnum.DONATION)).toList();
List<BookResponseDto> bookResponseDtos = books.stream()
.map(BookResponseDto::new)
.toList();
BookDonationEventResponseDto bookDonationEventResponseDto = new BookDonationEventResponseDto(bookDonationEvent);
model.addAttribute("bookDonationEvent", bookDonationEventResponseDto);
model.addAttribute("books", bookResponseDtos);
return "/users/bookApplyDonation";
}
@Query(value = "select bde from BookDonationEvent bde" +
" join fetch bde.books book" +
" where bde.id = :donationId and book.bookStatus = :status")
Page<BookDonationEvent> findPageByDonationId(@Param("donationId") Long donationId, @Param("status") BookStatusEnum status, Pageable pageable);
원인 분석
페이징 처리의 복잡성
JPA의 페이징 처리는 논리적으로 간단하지만 join fetch를 사용하면서 발생하는 문제는 각 BookDonationEvent가 여러 Book을 가질 수 있기 때문이다.
따라서 한 BookDonationEvent 내부의 여러 Book들이 페이징 처리의 대상이 되면서 원하는 페이징 결과를 얻기 어려웠던 것 같다.
해결
[Spring][258] 트러블 슈팅 : JPA 페이징 처리

[Spring][258] 트러블 슈팅 : 디버거에서의 지연 로딩(Lazy Loading) 트랩

[Spring][258] 트러블 슈팅 : Spring JPA에서 페이징 쿼리 오류 해결
Load more


