728x90

 

지난에는 8월 초까지 이야기 였다.

https://nataekoon.tistory.com/208

 

퇴사후 ~ 8/9 공부 회고

* (주의) 아직 학습 단계라 정확하지 않을 수 있습니다, 편한 말투로 진행됩니다. TMI 이지만, 퇴사 이후에 체중은 한 6키로 이상은 뺀것 같다. 자, 이제 퇴사 후 그동안 뭐했는지 모든걸 기록하진

nataekoon.tistory.com

위의 과정을 보면 잘보면 DB가 없다.(있어도 아마 김영한의 DB 기본편 만 봣을거다)

 

그렇게 하고나니 다음은 뭐하지...? 하다가 공부한 것들을 기록하려한다.

아직 8월이 다 끝나지 않았다.

 

 

 

 

어제 까지 학습한 것은 다음과 같다

 

김영한님의 실전 데이터베이스 성능 최적화를 보았는데 (약 21시간)

 

★ ★ ★ ★ ★ ★ ★ ★ ★  인생강의이다  ★ ★ ★ ★ ★ ★ ★ ★ ★

 

강의 순서는 커리큘럼 순서와는 다르게 설계편은 건너뛰고 기본편, 성능 최적화 이 두개를 이어서 봤다.

설계편은 다음에 1,2 편 봐야겠다.

 

여튼 후기로는 감히 내가 평가할 수 없는.. 

웬만한 DB(A) 강의보다 김영한님의 DB 강의가 압살한다. ㄹㅇ... ( 개인적으로 )

이직하게되면 쿼리 보는 눈이 좀 달라질 것 같다. (제발)

 

 

또 요즘 채용공고를 보니 풀스택도 좀 수요가 있는것 같고. 바이브코딩까지 가면 할만 할 것 같아서.

 

앞으로 학습할 것은 다음과 같다

(사실... 강의를 이미 사놧던 거라서 이럴때 봐야할 수 밖게 없눼...)

 

사실 타입스크립트 강의 DB 강의볼때 보다말다 했었다. 무리하는 것 같아서 DB 강의로 올인 했다.

여튼 다음 순서로 진행하려고 한다

 

TS > Next.js > 리액트 라이브러리 키트

가만 커리큘럼보니  다음 강의로 해당 강의를 추천해서, 근데 분량이 제법있어서 9/2 까지 될지 모르겠다

내용은 마음에 든다

 

TS > Next.js > 한 입 크기로 잘라먹는 React.js 실전 프로젝트 - SNS 편 

이 순서로 해야겠다.

 

전체 목표는 다음달인 9월 4일 까지 잡고있다.. 최소Next.js까지 (수정될 수 있다..) 

 

이 강사님(이정환)의 강의는 react 를 몇년전에 봐봣는데 좋았던 기억이 있어서.

구매했던 것 같다. 너무 딮하지 않아서 아쉬울때도있었지만.

 

또 리액트를 다루는 기술 도서 책도 봣고... 얇은 리액트책도 봣는데.

문제는 연속해서 본게아니라 정말 1년에 한번 명절때 하나씩 봐서 문제다 ㅋㅋ. 

GC를 매해 당한다...

이정환님의 리액트 강의는 3~4년전 만우절때 100원에 인프런 할인 했을때 이건 봐야지! 하고 본 것 같다.

 

그래도 쿠버네티스 카프카.. 레디스.. 영한님의 동시성.. DB 성능최적화... 그 뇌가 너무 아팟다

이럴때 프론트엔드 공부하면서 환기시켜야겠다.

 

아.. 근데 sj-shop 프로젝트는 타임리프인데...

 

 

 

728x90

'일상주저리' 카테고리의 다른 글

퇴사후 ~ 8/9 공부 회고  (0) 2026.08.09
7월 1 ~ 18일까지 공부 회고  (1) 2026.07.19
퇴사날 5/1 부터한것들?  (0) 2026.07.04
회사 동료들에게 블로그를 털렸다  (3) 2024.12.03
쿠버네티스를 공부하자  (0) 2024.10.05
728x90

 

* (주의) 아직 학습 단계라 정확하지 않을 수 있습니다, 편한 말투로 진행됩니다.

 

TMI 이지만, 퇴사 이후에 체중은 한 6키로 이상은 뺀것 같다.

 

자, 이제 퇴사 후 그동안 뭐했는지 모든걸 기록하진 못했지만, 그래도 나름대로의 기록을 소개한다

 

 

처음 퇴사후 했던거는 다음과 같다

https://nataekoon.tistory.com/193

 

퇴사날 5/1 부터한것들?

Brad Kim - Kafka (인프런) ↓ 입문자를 위한 Kubernetes (인프런) ↓ Brad Kim - EKS (인프런) ↓ Brad Kim - Spring Cloud (인프런) ↓ 개발남노 코테 (인프런) ↓ 패스트캠퍼스 Kafka Part 2 ↓ 패스트캠퍼스 Kubernetes ↓

nataekoon.tistory.com

 

 

이때 개인 쿠버네티스 클러스터를 VM 활용하여 만들기도하고,

Docker Desktop을 이용해서도 여러가지 주제를 그때그때마다 진행했다.

 

GNS 라는 네트워크 할때 쓰던 툴로 가성 네트워크 환경에서 OSPF와 BGP 를 연습했다.

 

https://nataekoon.tistory.com/187

 

OSPF

쿠버네티스를 공부하다 온프레미스 환경. 여기서는 내 가상머신에서 클러스터를 구축하고 MetalLB와 ingress nginx controller 이용하여 공부를 했는데. MetalLB에 L2 광고와 BGP 광고가 있다하여 L2 보단 BGP

nataekoon.tistory.com

https://nataekoon.tistory.com/188

 

 

BPG(eBGP)

BGP... 구성도는 다음과 같다 초록색영역이 AS 1 이고주황색이 AS 2 이다 서로다른 AS 끼리 네트워크 연결을 위해 BGP를 사용하는데R2, R3 관계가 서로 경계에 있다, 경계 라우터이다.따라서 이둘이 정

nataekoon.tistory.com

 

 

그다음 쿠너베티스에 로드밸런서를 위해서 각각 L2 방식과 BGP 방식으로 MetalLB 를 사용해보았다.

클라우드 사용자는 이런 걱정없이 쓸 수 있다니 . EKS 는 값어치를 하나보다.

 

https://nataekoon.tistory.com/189

 

metallb 실습 - L2

(참고. 필자는 EKS 사용료가 부담되어. 버츄얼박스로 쿠버네티스 1.36버전을 3대 구성하여 사용하고있다) 온프레미스 환경에서는 쿠버네티스를 사용하다 보면 service 오브젝트에 대해서 로드밸런

nataekoon.tistory.com

https://nataekoon.tistory.com/190

 

metallb 실습 - BGP

지난번에 L2 레벨에서 metallb로 로드밸런서를 생성하여 ip를 부여하고,이를 반복문을 curl을 써서 확인했다.https://nataekoon.tistory.com/189 metallb 실습 - L2(참고. 필자는 EKS 사용료가 부담되어. 버츄얼박

nataekoon.tistory.com

 

 

 

 

쿠버네티스 환경에서 편리하게 SSL 인증서 (TLS) 생성 하기 위해서 Cert Manager 실습도 한번 진행했다

ingress 에 별에별 작업을 다 할 수있다니..

근데 요즘은 ingress 말고 게이트웨이? 이런걸 쓰나보다. nginx ingress 도 이제 더이상 개발하지 않으니..

 

https://nataekoon.tistory.com/194

 

cert-manager 실습 이야기

[주의] 우선 아직 학습하는 과정이여서, 완벽하지 않을 수 있습니다 최근에 퇴사후 폐관수련을 진행중에 EKS 강의에서 cert-manager 라는 놀라운 획기적인 방법을 보게되었다. 하지만 AWS 수련하기전

nataekoon.tistory.com

 

 

 

 

 


 

그이후 

https://nataekoon.tistory.com/197

 

7월 1 ~ 18일까지 공부 회고

들어가기 앞서.... 5~6월 내기억에 5월과 6월에는 크게보면 코테 강의, 쿠버네티스, 카프카, 코테책 한권 이 순서였나? 이 네개를 큰줄기로서 각각 집중해서 본것같다. 처음에는 다른과목이랑 병행

nataekoon.tistory.com

 

그러니 얼추 계획했던 것은 한것 같다.

 

그리고 좀 중요한것 또는 실습을 추가로 해야할 것들은 따로 포스트잇을 모니터에 붙어두어서

하나씩 똥글뱅이 쳤다. 

 

마스터는 아니지만 적어도 찍먹보다는 높은 농도일거라 생각한다.

 

 


 

sj-shop

 

다시 7월 18일 이후에는 직접 sj-shop 이라는 프로젝트를 만들고 적용 해본것  같다

 

관련된 블로그는 이런식으로 Tag 화 시켰다.

 

 

 

 

 

 

 

1. Lock

대략 1000개의 재고가 있는 상품에 대해서 동시성 이슈를 발생하고 이럴 해결하는 과정이다 

v1~v7 까지 테스트 코드를 만들고, v1에서 race condition 이 발생하는 것을 보고 v2~v7 까지 각각의 해결법을 소개했다.

 

이때도 락의 범위와 트랜잭션 범위에 따라서 고민하고 Facade 라는 레이어를 알게되었고

이를 통해서 하나의 레이어를 둠으로서 해결했다.

 

https://nataekoon.tistory.com/199

 

Race Condition - Lock 7가지맛

* (주의) 아직 학습 단계라 정확하지 않을 수 있습니다, 편한 말투로 진행됩니다. 어제 밤에 혼자 락에대해서 실습한걸 블로그에 적어 보려한다. * 대표이미지는 잘뽑혔다. 헤헷... 들어가기 앞서

nataekoon.tistory.com

 

2. Kafka

 

카프카를 통해  적어도 한번 메세지 발송하는 정책과 이를 중복처리하지 않기 위해서 OutBox 라는 패턴을 알게되어

이를 적용했다.

Broker 는 3대로 클러스터를 구성했고 , UI를 위한 대쉬보드도 1대 , Kraft 방식이다

https://nataekoon.tistory.com/200

 

Kafka 요리조리 사용하기 (Spring Boot) 1 - 설정편

* (주의) 아직 학습 단계라 정확하지 않을 수 있습니다, 편한 말투로 진행됩니다. 요 근래에 카프카 강의를 몇개 보았고, 오래된 책이지만 카프카 컨셉을 익히기 위해서 책도 본게있었다. 조금 시

nataekoon.tistory.com

 

 

https://nataekoon.tistory.com/201

 

Kafka 요리조리 사용하기 (Spring Boot) 2 - 테스트 해보기

* (주의) 아직 학습 단계라 정확하지 않을 수 있습니다, 편한 말투로 진행됩니다. 지난 설정 편https://nataekoon.tistory.com/200 Kafka 요리조리 사용하기 (Spring Boot) - 설정편 1* (주의) 아직 학습 단계라 정

nataekoon.tistory.com

 

https://nataekoon.tistory.com/202

 

Kafka 요리조리 사용하기 (Spring Boot) 3 - Outbox 패턴

* (주의) 아직 학습 단계라 정확하지 않을 수 있습니다, 편한 말투로 진행됩니다. 지난 카프카 포스팅에서 Outbox 패턴에 대한 테스트 코드는 스킵했었는데.https://nataekoon.tistory.com/201 Kafka 요리조리

nataekoon.tistory.com

 

 

3. ELK

 

락 테스트 환경이나. 카프카 환경에서 

로그를 발생하고있는데. 이를 중앙 집중하여 보고싶었다.

왜 항상 실무에서 ssh 로 들어가서 로그를 뒤졌을까? 이를 해소하기 위해 ELK 를 적용했다.

 

처음에는 단순히 log 파일을 별도의 위치에두어서 이를 logstash 가 가져가게 했다.

https://nataekoon.tistory.com/203

 

Spring Boot 프로젝트에 ELK 적용해보자 (ssh 로 통한 로그파일 읽기 그만!)

* (주의) 아직 학습 단계라 정확하지 않을 수 있습니다, 편한 말투로 진행됩니다. 버전 정보 및 의존성GradleJdk 25Spring Boot 4.1.0 실무하다보면. 많은 로그를 보게되는데. 그때마다 ssh로 들어가서 본

nataekoon.tistory.com

 

 

 

하지만 예외가 발생했을때 멀티 라인에 대한 처리와 그리고 버퍼에 찻는데 그걸 비우지 못했을때 여러 이슈가 생겼다

이에 따라서 logstash-logback-encoder 라이브러리를 도입하고.

추후에 쿠버네티스에서 이를 인지할 수있게 추가 필드를 넣는것도 확인했다.

그리고 로그 레벨 별로 간단한 대쉬보드를 작성하여 눈으로 좀더 편하게 볼 수 있게 했다.

 

https://nataekoon.tistory.com/204

 

Spring Boot 프로젝트에 ELK - 더 사용해보기

* (주의) 아직 학습 단계라 정확하지 않을 수 있습니다, 편한 말투로 진행됩니다. 버전 정보 및 의존성GradleJdk 25Spring Boot 4.1.0 지난 글에서 ELK 사용을 해봤다. 이대로는 아쉬워서 잠을 잘수가없다.

nataekoon.tistory.com

 

4. Spring Actuator, Prometheus, Grafana

 

이제 로그에 대해서는 얼추 편리함을 찾았더니, 시스템에 대한 어떤 매트릭(지표)를 보고 싶었다

 

Spring Acuator 와 Prometheus 를 통해서 다양한 매트릭을 볼 수 있었다.

(디스크 용량이라든지 JVM 및 등등)

 

https://nataekoon.tistory.com/205

 

Spring Boot Prometheus / Grafana 이용해보자 1 - Actuator 와 Prometheus

* (주의) 아직 학습 단계라 정확하지 않을 수 있습니다, 편한 말투로 진행됩니다. 버전 정보 및 의존성 GradleJdk 25Spring Boot 4.1.0 지난 시간에는 ELK 을 통해 내가 원하는 로그를 빠르게 검색할 수 있

nataekoon.tistory.com

 

 

 

여기서 한번 나아가 프로메테우스에게 이 애플리케이션만의 매트릭을 수집하게 하고싶었다

대상은 최초 맨처음 Race Condtion 으로 v1~v7까지 테스트했을때 각각의 테스트마다 재고를 시각화해서 보고싶었다.

 

이에 따라서 Micrometer 에 각 테스트 별 재고 , 걸린 시간을 매트릭으로 등록하였다

이를 Prometheus 가 수집하고 , Grafana 가 조회한다.

이렇게 하니 한눈으로 내가 원하는 매트릭을 내 입맛대로 볼 수 있게 되었다.

https://nataekoon.tistory.com/206

 

Spring Boot Prometheus / Grafana 이용해보자 2 - Micrometer 부터 Grafana 까지

* (주의) 아직 학습 단계라 정확하지 않을 수 있습니다, 편한 말투로 진행됩니다. 버전 정보 및 의존성 GradleJdk 25Spring Boot 4.1.0지난 시간에는 간단한 설정에 불과했다 Spring Boot Prometheus / Grafana 이용

nataekoon.tistory.com

 


 

번외. RAG

 

RAG 책과 Claude Code 를 활용하여 만든 , 늦은 RAG 이야기를 소개한다.

https://nataekoon.tistory.com/207

 

RAG 로 내 주변사람 돕기 (개발자, 기획자)

* (주의) 아직 학습 단계라 정확하지 않을 수 있습니다, 편한 말투로 진행됩니다. 이걸 보고 작성했을대에는 블로그 올리는걸 깜박했다.. 너무 바쁘기도했고.. 오픈기간이였는데 처음이 책을 봤

nataekoon.tistory.com

 

 

 


느낀점

그동안 내가 있던 조직 환경은 퍼블리셔와 자바 웹개발자만 있던 야생의 환경이였다.

그러기 때문에, 많은 시간이 백엔드 및 인프라 집중되지 못하였다.

가끔은 마치 게임속 스킬포인트를 잘못찍은 것 처럼 느껴졌다.

 

여튼 이번 퇴사하기 전에도 데브웁스로 좀더 힘을 실기 위해서 각오를 다졌다

 

낯선 CountDownLatch 이용하여 락에 관련된것을 할때 v1~v7 까지 계속 늘려가면서 진행하면서 정말 재밋었다.

 

그리고 카프카나 쿠버네티스는 마찬가지였다. 늘 공부할 마음도 있고. 몇몇 강의나 책을 봤지만

금새 까먹기 일수 였다. 실무에서 쓸 수 있는 환경이나 기회가 없었다.

특히 쿠버네티스는 설치 이슈가 많았다. 책마다 버전도 크게크게 다르고. 정말 기능이 방대하다...

 

또한 이번에 스프링 부트4 부터 Retry 가 레질리언스 기반으로 내장된 것도 발견했다.

Spring Retry 의존성을 추가하지 않아도 재시도 로직을 쓸 수 있게 되었다.

Aop 도 Aspect 로 바뀌었고...

 

여튼 sj-shop 프로젝트가 어떤 화면이있고 진행하는 포트폴리오까지는 아니지만

개인으로서 아쉬웠던 그럼 점들만 모아서 나를 위해 만든 포트폴리오 인 샘이다.

 


 

다음으로 ...

2주 동안 "만들기" 를 했으니 

다음으로는 김영한 강사님의 데이터베이스 성능 강의에 집중해야 겠다.

 

 

읽어주셔서 감사합니다.

728x90

'일상주저리' 카테고리의 다른 글

8월 쥐어짜기  (0) 2026.08.23
7월 1 ~ 18일까지 공부 회고  (1) 2026.07.19
퇴사날 5/1 부터한것들?  (0) 2026.07.04
회사 동료들에게 블로그를 털렸다  (3) 2024.12.03
쿠버네티스를 공부하자  (0) 2024.10.05
728x90

* (주의) 아직 학습 단계라 정확하지 않을 수 있습니다, 편한 말투로 진행됩니다.

 

 

이걸 보고 작성했을대에는 블로그 올리는걸 깜박했다.. 너무 바쁘기도했고.. 오픈기간이였는데

 

처음이 책을 봤었을때는 지난 회사 재직중에 봣었다. 그리고 랭스미스는 스킵해도 될것같아서 

스킵했다. 유료였나? 버전이슈였나 기억이 잘안난다. 최소 6개월 이전의 했던거라... 

 

여튼. 오늘 우연히 컴퓨터에서

rag 관련 짤모음을 발견했다. 

아 맞다.. 이거 했었지.. 이것도 블로그에 올리자! 라는 생각이 들었다.

 

 

근데 이걸 첨만들었을때 들뜬 마음으로 회사 사업에 녹이려고, C레벨 관계자에게 말했지만.

정녕 이해하지 못한다. 왜냐면 그분은 개발조직의 C레벨 관계자가 아니기때문에.. 

(개발부서 붕괴 이슈)

 

이런건 대기업에서나 한다고 말씀하셧던게 생각이난다. ㅎㅎ

 

쓰디쓴 마음으로 실무에는 활용하지 못했다. 물론 나또한 그정도로 준비되지않았고

요즘은 Spring AI 2.0 에 관심이 있다.

 

아마 AI 공부는 재직중에나 할것같다.. 백수는 슬퍼요

 

 

여튼, 그럼에도 불구하고 책에는 없는 범위로 스프링 부트에 적용했떤 이야기를 해보려고한다.

 

이걸 만들었을때 목적은 다음과 같았다. 사실상 명분..

 

1. 기획업무가 처음인 기획자를 위한 몇번이고 물어볼 수 있는 기능 

아무래도 신입 기획자가 서비스 기획하기가 쉽지않다. 필자가 겪은 대다수의 경우는

결국 개발자가 기획 및 개발 후 그걸 다시 기획자가 기획문서로 만들고 사용자 메뉴얼로 만드는 경우가 많았다.

이때 대화를 좀 줄이고, 또 몇몇의 질문은 챗봇이 처리하게 하려고 했다.

 

2. 신입 개발자 입장에서 어떤식으로 추가기능 만들때 

어떤 패턴으로 따라해야할지 또 마찬가지로 프로세스를 질문할 수 있는..

 

즉, 나와 함께하는 사람이 모두 재밋게 이 프로젝트를 끝냈으면 하는 마음이 있었다.

개개인 마다 억압적인 상황도있는걸 잘 알아서.

 

 

다시 바야흐로 지난 회사에서 재직중일때 봣던 Ollama 책이다

 

회사에서 사업적으로 적용하고 싶었는데.

이에 대해 무심하게 생각하시던! 당시 C레벨 관계자가 이런건 대기업이나 하는거라고 짤랐다.

허나 요즘 채용공고를 보니 상당히 많은 회사가 RAG를 원하고 AX 라는 용어도 새롭게 나왓다

 

 

 

아쉬운대로 그 당시 나의 친구 Claude Code 와 함께 만들어봤다.

 

그래픽 카드는 필자의 4070ti 을 갈았고. 모델들은 다 Ollama 의 오픈 소스 모델들을 활용했다

 

Queen 모델을 썻던걸로 기억한다 

 

다음은 그당시 하나씩 변해가던 변천사이다.

 

v1. 콘솔형

 

1. Spring 에서 View Controller 와 API Controller 을 나누어서 개발하는 우리팀 (사실상 제가 밀어붙인) 컴베이션인데.

신규 입사자나 신입 개발자가 처음 이 규칙 또는 가이드 코드가있으면 좋다고 생각해서. 

이런식으로 문의하고 대답을 받았다. 참고로 retrieved 는 관련된 문서 6개를 가져왓다는 의미이다

 

 

만약에 새로운 기능개발이 추가되어서 Board Entity 를 추가해야한다면 어떨까? 또 그때의 Controller 작성법은?

참고로, 여기서 말하는 엔티티는 JPA의 엔티티이다.

 

 

v2. web 초기

 

 

v3. web 고도화

(이건 지금 작성중에 다 캡처하고 실행하고 했다.. 찍어둔게 없어서)

우선 처음에 프로그램을 실행하면, 소스를 가지고 임베딩을 진행하는데 엄청난 로그가 촤르르르륵 나온다

 

open ai를 쓰면 1536 차원정도 를 쓰는데 무료는 차원갯수가 작아서 유사도 검색에 조금 불리하다.

요즘은 무료가 몇차원까지해주는 지 모르겠지만. 읍읍

 

 

그리고 별도로 내가아는 모든 프로세스를 미리 정답 md 파일로도 두었다. 이는 코드가 관심사가 아닌 기획자를 위한 내용이다

물론 이 내용도 클로드에게 cli 로 시켜서 만들면 된다 .

 

 

 

이또한 임베딩한다

 

(서로 같은 테이블)

잘보면 cmetadata 에 type 이 process_doc 이다 이게 위의 md 파일 말하는걸로 보면된

 

 

 

이제 브라우저에서보자

 

아무나 사용못하게 패스워드를 걸었다

왜냐면 내 그래픽카드로 동시요청은 최대 2명이다... ㅠ 메모리가 터진다..

 

 

 

패스워드 입력시 볼 수 있는 화면이다.

여기서 대화는 모두 DB에 저장된다.

 

 

개발자 모드로 View Controller 질문을 해보았다.

 

 

 

그리고 저 채팅방은 테이블로 관리가되고

 

 

해당 채팅방에서 대화한 것도 역시 테이블로 관리가 된다

 

 

실제 이런식으로 동작한다 SSE 방식이다

 

 

 

이번에는 기획자 모드로 질문해보자

"로그인 실패 자꾸 발생하면 어떻게되니?" 라고 기획자가 질문한 경우이다

 

 

 

진짜 ... 지금 캡처하는데도 그래픽 카드 메모리가 거의 가득 차서 여러번 캡처했따....

 

 

 

이상으로 과거 회상을 마친다.

728x90
728x90

* (주의) 아직 학습 단계라 정확하지 않을 수 있습니다, 편한 말투로 진행됩니다.

 

버전 정보 및 의존성

 

  • Gradle
  • Jdk 25
  • Spring Boot 4.1.0

지난 시간에는 간단한 설정에 불과했다

 

Spring Boot Prometheus / Grafana 이용해보자 1 - Prometheus

지난 시간에는 ELK 을 통해 내가 원하는 로그를 빠르게 검색할 수 있었다. https://nataekoon.tistory.com/203 Spring Boot 프로젝트에 ELK 적용해보자 (ssh 로 통한 로그파일 읽기 그만!)* (주의) 아직 학습 단계

nataekoon.tistory.com

 

 

그리고 ELK 때와는 로그정도를 빠르게 검색하고 이를 시각화 했다면

비즈니스에 몰입하여 나만의 매트릭을 가지고 시각화를 진행할 거다.

https://nataekoon.tistory.com/204

 

Spring Boot 프로젝트에 ELK - 더 사용해보기

* (주의) 아직 학습 단계라 정확하지 않을 수 있습니다, 편한 말투로 진행됩니다. 버전 정보 및 의존성GradleJdk 25Spring Boot 4.1.0 지난 글에서 ELK 사용을 해봤다. 이대로는 아쉬워서 잠을 잘수가없다.

nataekoon.tistory.com

 

 

 

 

일단 프로메테우스에서 자주 쓰는 타입은 다음과 같고 (필자로 다 써보진 않았다.

 

 

 

마찬가지로, 여기서 이번에 Micrometer  타입 기준으로 Counter, Gauge, Timer 를 사용하려고한다

 

 

그리고 위의 경우 어노테이션 지원되는게 있고 안되는게 있다.

 

간단한건 어노테이션으로 해도되지만, 중간 로직에서 예외가발생할수있는경우에는 프로그래밍 방식이 더 낫다.

 

 

우선 간단한 특정 Countroller 생성한뒤 여기서 하나씩 맛보자

 

1. 항상 증가만하는 호출 횟수는 Counter 타입의 매트릭으로 정의

2. 동적으로 변하는 수는 Gauge 타입의 매트릭으로 정의

3. 메서드 실행 시간은 Timer 타입의 매트릭으로 정의

 

참고로 해보니. 카운터는 조회했을때만 매트릭이 생성되어서 

/actuator/prometheus 에서 확인할 수 있고. 게이지의 경우는 미리 생성된다.

 

 

코드를보자 정말 간단하다.

package com.example.sj_shop.web.orders.controller;

import com.example.sj_shop.web.orders.service.OrderService;
import io.micrometer.core.annotation.Counted;
import io.micrometer.core.annotation.Timed;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.atomic.AtomicInteger;

@Slf4j
@RestController
@RequiredArgsConstructor
public class ApiOrderController {

    private final OrderService orderService;
    private final MeterRegistry meterRegistry;

    private final AtomicInteger activeRequests = new AtomicInteger();

    @PostConstruct
    public void initGauge() {
        Gauge.builder(
                        "order.controller.active", // 메트릭 이름
                        activeRequests,            // 관찰할 객체
                        value -> value.get()       // 이 객체에서 숫자를 어떻게 꺼낼지
                )
                .register(meterRegistry);
    }

    // 프로메테우스에서는 . 구분자가 _ 로 바끼고 마지막에  _total 로 suffix 가 붙는다
    @Counted("order.controller.response")
    @GetMapping("/order")
    public ResponseEntity<?> order() {

        meterRegistry.counter("order.controller.request").increment();

        log.info("[log.info level] /order 호출");
        log.warn("[log.warn level] /order 호출");
        log.error("[log.error level] /order 호출", new RuntimeException("런타임에러 발생"));

        return ResponseEntity.status(HttpStatus.OK).build();
    }


    @GetMapping("/order/gauge/up")
    public ResponseEntity<?> gaugeUp() {
        activeRequests.incrementAndGet();
        return ResponseEntity.ok().build();
    }

    @GetMapping("/order/gauge/down")
    public ResponseEntity<?> gaugeDown() {
        activeRequests.decrementAndGet();
        return ResponseEntity.ok().build();
    }
        
    
    @Timed("order.controller.timer")
    @GetMapping("/order/timer")
    public ResponseEntity<?> timer() throws InterruptedException {
    
        // 처리 시간이 보이도록 일부러 1초 대기
        Thread.sleep(1000);
    
        return ResponseEntity.ok().build();
    }    
}

 

 

1. Counted

우선 어노테이션기반으로 완전히 메서드가 종료가되면  order.controller.response 의 매트릭값을 증가시키게했다

왜냐면 AOP 기반으로 동작한다. 따라서 response 로 지정했다. 완전히 호출이 종료됨을 보장했을때 카운터를 증가시키려고.

그리고 메서드 시작할때는 프로그래밍 방식으로 증가 시켯다.

// 프로메테우스에서는 . 구분자가 _ 로 바끼고 마지막에  _total 로 suffix 가 붙는다
@Counted("order.controller.response")
@GetMapping("/order")
public ResponseEntity<?> order() {

    meterRegistry.counter("order.controller.request").increment();

    log.info("[log.info level] /order 호출");
    log.warn("[log.warn level] /order 호출");
    log.error("[log.error level] /order 호출", new RuntimeException("런타임에러 발생"));

    return ResponseEntity.status(HttpStatus.OK).build();
}

 

 

자 호출하고 결과를 보자

카운터를 사용할 경우 마지막에 _total 이 붙는다.

 

 

한번더 요청해보자 

 

 

번외로, 메서드 내에서 logback 을 활용하여 로그를 info, warn, error 레벨로 남겼는데 이또한 수집된다.

마이크로 미터가 등록되면서 자동으로 모아준거다.

 

 

2. Gauge

 

이것도 보면 단순하자 원자성 보장되는 AtomicInter 만 사용했을뿐이다.

 

Gauge 는 프로그래밍 방식만 지원되므로 다음처럼 만들어야한다

이름과, 관찰할 객체와 여기서 어떻게 값을 꺼낼지 정의한다. value 가 AtomicInter 변수를 가르킨다

private final AtomicInteger activeRequests = new AtomicInteger();

@PostConstruct
public void initGauge() {
    Gauge.builder(
                    "order.controller.active", // 메트릭 이름
                    activeRequests,            // 관찰할 객체
                    value -> value.get()       // 이 객체에서 숫자를 어떻게 꺼낼지
            )
            .register(meterRegistry);
}

 

올리거나 내리거나하는 api 이다.

@GetMapping("/order/gauge/up")
public ResponseEntity<?> gaugeUp() {
    activeRequests.incrementAndGet();
    return ResponseEntity.ok().build();
}

@GetMapping("/order/gauge/down")
public ResponseEntity<?> gaugeDown() {
    activeRequests.decrementAndGet();
    return ResponseEntity.ok().build();
}

 

 

호출전에 메트릭을 보자 게이지는 미리 생성된다

 

 

 

up api 를 호출하고 메트릭 값을 보자 

# HELP order_controller_active  
# TYPE order_controller_active gauge
order_controller_active{application="sj-shop",hostname="DESKTOP-UOTDLQQ"} 1.0

 

 

down api 를 호출하고 메트릭 값을 보자

# HELP order_controller_active  
# TYPE order_controller_active gauge
order_controller_active{application="sj-shop",hostname="DESKTOP-UOTDLQQ"} 0.0

 

 

 

3. Timer

이제 타이머를 보자 이는 프로그래밍 방식, 어노테이션 방식 둘다 지원하는데

안전하게측정하려면 프로그래밍 방식을 추천한다. 예외가 터지면 종료를해야하니까..

우선 전자의 방식은 좀따 알아보고, 우선 어노테이션 방식먼저 보자 

 

Timer 역시 호출되어서 매트릭이 생성되어야 볼 수 있다.

 

참쉽다.

 

@Timed("order.controller.timer")
@GetMapping("/order/timer")
public ResponseEntity<?> timer() throws InterruptedException {

    // 처리 시간이 보이도록 일부러 1초 대기
    Thread.sleep(1000);

    return ResponseEntity.ok().build();
}

 

api 를 호출 하고 매트릭을봐보자.  두개의 지표가 나오는데.

 

_count : 실행 횟수

_sum : 실행 시간 누적합

_max: 제일 오래걸린 시간

 

이라는것을 알 수 있다.

# HELP order_controller_timer_seconds  
# TYPE order_controller_timer_seconds summary
order_controller_timer_seconds_count{application="sj-shop",class="com.example.sj_shop.web.orders.controller.ApiOrderController",exception="none",hostname="DESKTOP-UOTDLQQ",method="timer"} 1
order_controller_timer_seconds_sum{application="sj-shop",class="com.example.sj_shop.web.orders.controller.ApiOrderController",exception="none",hostname="DESKTOP-UOTDLQQ",method="timer"} 1.0010387
# HELP order_controller_timer_seconds_max  
# TYPE order_controller_timer_seconds_max gauge
order_controller_timer_seconds_max{application="sj-shop",class="com.example.sj_shop.web.orders.controller.ApiOrderController",exception="none",hostname="DESKTOP-UOTDLQQ",method="timer"} 1.0010387

 

한번더 요청해보자

# HELP order_controller_timer_seconds  
# TYPE order_controller_timer_seconds summary
order_controller_timer_seconds_count{application="sj-shop",class="com.example.sj_shop.web.orders.controller.ApiOrderController",exception="none",hostname="DESKTOP-UOTDLQQ",method="timer"} 2
order_controller_timer_seconds_sum{application="sj-shop",class="com.example.sj_shop.web.orders.controller.ApiOrderController",exception="none",hostname="DESKTOP-UOTDLQQ",method="timer"} 2.0024442
# HELP order_controller_timer_seconds_max  
# TYPE order_controller_timer_seconds_max gauge
order_controller_timer_seconds_max{application="sj-shop",class="com.example.sj_shop.web.orders.controller.ApiOrderController",exception="none",hostname="DESKTOP-UOTDLQQ",method="timer"} 1.0014055

 

 

 

이제는 그라파나까지 연결해보자.

 

필자의 경우에는 최근에 만들었던 V1 ~ V7 까지 있던 락 관련 메서드에  적용할 것이다

 

https://nataekoon.tistory.com/199

 

Race Condition - Lock 7가지맛

* (주의) 아직 학습 단계라 정확하지 않을 수 있습니다, 편한 말투로 진행됩니다. 어제 밤에 혼자 락에대해서 실습한걸 블로그에 적어 보려한다. * 대표이미지는 잘뽑혔다. 헤헷... 들어가기 앞서

nataekoon.tistory.com

 

 

여기에 각 테스트 메서드마다 1000 개의 재고를 준비하고 100개를 소모했을때

900개가 되어야하는 테스트 코드이다.

 

 

이 블로그를 작성하는 환경에서

일단 서버가 가진 매트릭은 서버가 종료되면 소멸되기 때문에 별도의 저장공간이 필요했다.

따라서 redis에 이를 저장했다가, 스프링 부트를 실행하면 그 레디스 값을 가져와 프로메테우스에게 전해주려고한다.

 

이때 만드려고하는 지표는 다음과 같다

 

1. 테스트 메서드 이후 남은 재고

2. 테스트 메서드의 걸린 시간( 단위는 초 )

 

 

그리고 여기서 재고는 Gauge  라는 프로메테우스의 타입을 사용할 것이다. 동적으로 변화하는 값이다

만약에 api 조회수 같은경우는 Counted 를 사용하면된다. 

Gauge 를 제외한 다른 타입들은 스프링에서 어노테이션 기반으로도 사용할 수 있다.

 

 

우선 레디스에 저장하고, 레디스값을 게이지로 변환하기 위해서 다음과같은 Bean 이 필요했다.

package com.example.sj_shop.config.actuator.race_condition;

import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class RaceConditionMetrics {

    // Micrometer + Redis 공통 이름
    public static final String METRIC_TIMER = "order.race.timer";
    private static final String METRIC_STOCK = "order.race.stock.remaining";
    private static final String METRIC_DURATION = "order.race.duration";

    private final MeterRegistry meterRegistry;
    private final StringRedisTemplate redisTemplate;

    @PostConstruct
    public void initGauge() {

        for (int i = 1; i <= 7; i++) {

            String version = "v" + i;

            Gauge.builder(
                            METRIC_STOCK,
                            version,
                            this::getRemainingStock
                    )
                    .tag("version", version)
                    .register(meterRegistry);

            Gauge.builder(
                            METRIC_DURATION,
                            version,
                            this::getDurationSeconds
                    )
                    .baseUnit("seconds")
                    .tag("version", version)
                    .register(meterRegistry);
        }
    }

    // Redis - 최종 재고 저장
    public void setRemainingStock(String version, int quantity) {
        redisTemplate.opsForValue()
                .set(METRIC_STOCK + ":" + version, String.valueOf(quantity));
    }

    // Redis - 수행 시간 저장
    public void setDuration(String version, long durationNanos) {
        redisTemplate.opsForValue()
                .set(METRIC_DURATION + ":" + version, String.valueOf(durationNanos));
    }

    // Redis - 최종 재고 조회
    private double getRemainingStock(String version) {

        String value = redisTemplate.opsForValue()
                .get(METRIC_STOCK + ":" + version);

        return value == null ? 0 : Double.parseDouble(value);
    }

    // Redis - 수행 시간 조회
    private double getDurationSeconds(String version) {

        String value = redisTemplate.opsForValue()
                .get(METRIC_DURATION + ":" + version);

        if (value == null) {
            return 0;
        }

        long durationNanos = Long.parseLong(value);

        return durationNanos / 1_000_000_000.0;
    }
}

 

 

 

그리고 각 테스트 코드에 다음처럼 적용 했다.

V1 기준은 다음과 같다. 기존 로직 제외하고 보면 위와 아래에 코드가 추가되었다.

 

그리고 타이머를 프로그래밍 방식으로 진행했다.

 

    @Autowired
    private MeterRegistry meterRegistry;


/**
 * 주문 1개당 제품 딱 1개만 주문하는 경우
 */
@Test
@DisplayName("[단일 상품 주문] Race Condtion - pojo")
public void 단일_상품_주문_V1() throws Exception {

    // 타이머 시작
    Timer.Sample sample = Timer.start(meterRegistry);

    // 시작 재고 Gauge
    raceConditionMetrics.setRemainingStock("v1", beforeQuantity);




    ... 기존 로직 ....
    
    

    // 타이머 종료
    Timer timer = Timer.builder(RaceConditionMetrics.METRIC_TIMER)
            .tag("version", "v1")
            .register(meterRegistry);

    long durationNanos = sample.stop(timer);

    // 레디스에 저장
    raceConditionMetrics.setDuration("v1", durationNanos);
    raceConditionMetrics.setRemainingStock("v1", quantity);

    // 남은 재고가 1000 - 100 = 900
    assertThat(quantity).isEqualTo(beforeQuantity - 100);

    es.shutdown();
}

 

 

1. 전체 테스트 코드를 실행

 

 

2. Redis 확인

각 테스트 별로 시간과 남은 재고가 저장되었다.

 

 

 

2. /actuator/prometheus 매트릭 확인

매트릭을 확인하자

 

 

 

3. Prometheus 매트릭 확인

 

 

 

 

4. Grafana 대쉬보드 생성 및 확인

 

일단 대쉬보드를 생성하고

 

각 패널을 하나씩 만들었다. 재고는 그냥 숫자이고. 걸린시간도 그냥 숫자이니, Stat 패널을 이용했다.

 

 

매트릭 값이 900 인경우에는 초록색을 주었고.

아닌경우에는 빨간색을 주어서 눈으로 볼 수 있게 했다.

 

 

 

결과적으로 대쉬보드에서 두개의 패널을 확인할 수 있다. 지난 ELK에서 그라파나에서 보던것과는 좀 다른 맛이다.

따라서 이 화면만 봐도 v1 을 제외한 테스트 코드를 모두 성공적으로 끝났다는걸 알 수 있다.

 

 

아까 테스트코드에서 측정된 시간과 거의 유사하게 측정되었다.

 

 


 

 

그리고 이미 그라파나에 유명한 대쉬보드가 많은데 import 할 수있다

 

 

 

 

 

 

여기 id 값을 복사해서

 

 

여기 넣고 Load 하면 불러 올 수 있다.

 

 

다음은 3가지 대쉬보드를 한번 import 하여 결과 사진이다

 

1. 온갖 스프링 부트에 대한 매트릭을 시각화가 되어있다. 참 많다!

잘보면 Logback 에 대한 통계도 나온다. 아까위에서 logger 레벨별로 카운트가 집계되었었다.

시각화된게 제일 많았다.

 

 

 

2. JVM 관련

 

 

3. HikariCP와 JDBC 관련

 

728x90
728x90

* (주의) 아직 학습 단계라 정확하지 않을 수 있습니다, 편한 말투로 진행됩니다.

 

버전 정보 및 의존성

 

  • Gradle
  • Jdk 25
  • Spring Boot 4.1.0

 

지난 시간에는 ELK 을 통해 내가 원하는 로그를 빠르게 검색할 수 있었다.

 

https://nataekoon.tistory.com/203

 

Spring Boot 프로젝트에 ELK 적용해보자 (ssh 로 통한 로그파일 읽기 그만!)

* (주의) 아직 학습 단계라 정확하지 않을 수 있습니다, 편한 말투로 진행됩니다. 버전 정보 및 의존성GradleJdk 25Spring Boot 4.1.0 실무하다보면. 많은 로그를 보게되는데. 그때마다 ssh로 들어가서 본

nataekoon.tistory.com

 

 

 

이번시간에는  Actuator / Prometheus / Grafana 을 활용해 시각화에 집중하려고한다

 

우선 Prometheus 을 먼저 알아보자. 뭔지는 GPT 에게 물어보자 !

 

아래처럼 프로메테우스는 수집하고 저장하는 역할을 하며. 필요에 따라 조회까지 할 수 있다.

그리고 그라파나를 이를 시각화 시켜주는 놈이다.

 

 

 

 

스프링 진영에서는 액츄에이터로 메트릭 정보를 수집할 수 있다.

그리고 그걸 마이크로미터가 프로메테우스에게 전달해주는데. 따라서 두 의존성을 추가하자.

 

dependencies {



    implementation 'io.micrometer:micrometer-registry-prometheus'

    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    
    ...

}

 

 

그리고 application.yaml 로 돌아와

어떤 매트릭을 노출 시킬것인지 정하고. 그렇게하면 그게 엔드포인트로도 만들어지는데.

이때 그 각 엔드포인트에  정보노출을 얼마나 할 것인지 정한다. 일단 이정도해보자.

management:

  # Actuator 엔드포인트를 HTTP로 외부에 공개할 범위를 설정한다.
  endpoints:
    web:
      exposure:

        # health   : 애플리케이션 상태 확인
        # info     : 애플리케이션 기본 정보 확인
        # prometheus : Prometheus가 수집할 메트릭 제공
        include: health,info,prometheus

  # 개별 Actuator 엔드포인트 설정이다.
  endpoint:

    # /actuator/health 응답 설정이다.
    health:

      # 단순히 UP/DOWN만 보여주지 않고
      # DB, Redis, 디스크 공간 등 상세 상태도 함께 보여준다.
      show-details: always

 

서버 재실행 후 다음 엔드포인트로 접속해보면 , 별도의 설명대신 보자마자 알 수 있을 것이다

 

 

개별 엔드포인트를 다시 클릭해보자

 

 

헬스 엔드포인트로 접속하니 mysql, redis 부터 다양한 헬스체크를 할 수 있다. show-details 가 always 라 많이 노출되었다.

 

 

이제 프로메테우스 엔드포인트 보자

기본적으로 마이크로 미터가 모아다 준 정보를 볼 수 있다. 경우에 따라 개발자가 특정 매트릭도 생성할 수 있다.

 

# HELP .... 는  매트릭 설명이고

# TYPE ... 은 매트릭 타입이다

그리고 그 아래줄에 써져있는게 값이다

 

 

이 정보들이 프로메테우스에게 전달된 매트릭 정보이다.

 

그리고 프로메테우스 컨테이너는 다음과같은 설정정보가 있어야

저 데이터를 가져올 수 있다.

 

 

 

그리고 프로메테우스 서버를 실행해서, 주어진 포트로 접속해보자

 

9090 포트에서 /targets 에 해당하는 페이지를보면 현재 로컬 스프링부트가 추가되어있는것을 확인할 수 있다.

 

 

 

이제 Query 탭 으로 가서 조회를 해보자

아까 위에서 빨간색 네모로 표시되었던 application_ready_time_seconds 이 매트릭을 조회하면 자동완성도 될 뿐만아니라 잘 조회 된다 

 

참고로 자바에서는 application.ready.time.seconds 이런 점 표기법이라면

프로메테우스는 _ 표기법을 쓴다 

 

 

간소하게 그래프로도 볼 수 있다. (추후에는 Grafana 에서 더 이쁘게 볼 수 있다.)

 

 

 


다음으로 ...

다음으로는 그라파나까지 사용하여 데이터 시각화를 진행해보자.

사실 지금 Micrometer 에 대해서는 의존성만 추가했지 별다른것을 진행하지 않았는데

 

1. Micrometer 을 이용하여 커스텀 매트릭을 생성

2. 이를 Prometheus 에서 조회하고 (또는 /actuator/prometheus )

3. Grafana 에서 Prometheus 가 수집한 매트릭 조회 하여 시각화

 

쓰리콤보를 진행할 것이다

 

 

728x90
728x90

* (주의) 아직 학습 단계라 정확하지 않을 수 있습니다, 편한 말투로 진행됩니다.

 

버전 정보 및 의존성

  • Gradle
  • Jdk 25
  • Spring Boot 4.1.0

 

 

지난 글에서 ELK 사용을 해봤다. 이대로는 아쉬워서 잠을 잘수가없다.

 

https://nataekoon.tistory.com/203

 

Spring Boot 프로젝트에 ELK 적용해보자 (ssh 로 통한 로그파일 읽기 그만!)

실무하다보면. 많은 로그를 보게되는데. 그때마다 ssh로 들어가서 본적이 다들 한번씩 있을거다 그런사람들에게 ELK 스택은 정말 어렵지않은 난이도로 편하게 사용할 수 있는 스택같다. ELK 는 사

nataekoon.tistory.com

 

 

사실 한번 작성중에 한계를 느껴서.. 다시 블로그를 작성하고있다.

 

 

1. logstash.conf  -TCP 변경

TCP 기반으로 바꾸어 다시 작성하게된 이유는 로그 파일 기반으로 다루면 아래와같은 상황에서 문제가 좀 있다.

 

보통 예외가 발생하고 로그를 보면 다음과 같은 패턴이다

 

 

첫번째줄 - 개발자가 logger 로 적은 로그고 

두번째줄 -  Stack Trace 가 나온다.

 

그리고 이 두줄은 하나의 traceId가 발생한 로그이다. 따라서 이를 둘다 온전히 가져가고 싶었다.

하지만, 파일 기반으로 설정하니 이 멀티라인에 대한 문제점이 많았고.

최종적으로 이방식은 별로 추천하지않는다는 것을 알았다.

 

그래서 지난 포스팅에서 언급한 TCP 기반의 인코더 라이브러리를 사용하기로 했다

 

이를 사용하면 이 문제가 해결된다.

 

build.gradle 에 추가

implementation "net.logstash.logback:logstash-logback-encoder:8.1"

 

 

logstash.conf 파일이다.

 

1.  TCP 방식을 사용하고, 어떤 서버로부터 로그를 받을것인지 설정한다. 그리고 타입은 json , json_lines 등 다양하게있다.

 

2.  filter 쪽에서 커스텀 필드를 생성하거나 복사하거나 할수도있으며. 로그레벨을 ERROR 만 필터링도 할 수있다.

     실습을 위해 다 받기로했다. 커스텀 필드의 경우 logback-spring.xml 에서도 할 수 있지만 한곳에서 집중하는게 나을것 같다.

 

 

# 1. Input
input {
  tcp {
    # Spring Boot 애플리케이션에서 전송하는 TCP 로그를 받는다.
    host => "0.0.0.0"

    # Spring Boot의 LogstashTcpSocketAppender와 동일한 포트다.
    port => 5044

    # 줄바꿈으로 구분된 JSON 로그를 이벤트 하나씩 변환한다.
    codec => json_lines
  }
}

# 2. Filter
filter {
  # JSON 로그의 기본 필드명을
  # 기존 Grok 실습에서 사용하던 필드명으로 변경한다.
#  mutate {
#    rename => {
#      "level"       => "log_level"
#      "logger_name" => "logger"
#      "message"     => "log_message"
#      "traceId"     => "trace_id"
#    }
#  }

  # Elasticsearch의 실제 시간 필드인 @timestamp는 유지하면서
  # 기존에 사용하던 log_time 필드도 함께 만든다.
#  mutate {
#    copy => {
#      "@timestamp" => "log_time"
#    }
#  }

  # 애플리케이션과 실행 환경을 구분하기 위한 필드를 추가한다.
  mutate {
    add_field => {
      "application" => "sj-shop"
      "environment" => "local"
    }
  }


  # 현재는 INFO, WARN, ERROR 로그를 모두 저장한다.
  # 로그 레벨별 차트를 사용하려면 아래 코드는 주석 상태로 둔다.


  # ERROR 로그만 Elasticsearch에 저장할 때 사용한다.
  #
  # if [level] != "ERROR" {
  #   drop { }
  # }
}

# 3. Output
output {
  # Elasticsearch로 로그를 전송한다.
  elasticsearch {
    hosts => ["http://sj-shop-elasticsearch:9200"]

    # 날짜별 인덱스로 저장한다.
    index => "sj-shop-%{+YYYY.MM.dd}"
  }

  # Logstash 콘솔에서도 가공 결과를 확인한다.
  stdout {
    codec => rubydebug
  }
}

 

 

2. logback 설정

 

logback-spring.xml 에 appender 를 추가해줬다. 지난시간에 수정한건 굳이 변경안해도 상관없다.

 

참고로, 필자는 인텔리제이는 로컬로 돌리고 나머지는다 docker 로 돌린다.

 

    <!-- 로그를 JSON 형태로 Logstash에 TCP 전송한다. -->
    <appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">


        <!-- Docker Compose의 Logstash 서비스명과 TCP 포트 -->
<!--        <destination>sj-shop-logstash:5044</destination>-->
        <!-- 호스트에서 Docker 컨테이너의 공개 포트로 접속한다. -->
        <destination>localhost:5044</destination>

        <!-- 네트워크 전송 실패가 애플리케이션 요청 처리를 막지 않도록 비동기로 전송한다. -->
        <keepAliveDuration>5 minutes</keepAliveDuration>

        <!-- 로그 이벤트를 JSON 한 줄로 변환한다. -->
        <encoder class="net.logstash.logback.encoder.LogstashEncoder">

            <!-- MDC의 traceId 등을 JSON 필드로 포함한다. -->
            <includeMdc>true</includeMdc>

            <!-- 애플리케이션 이름을 고정 필드로 추가한다. -->
<!--            <customFields>-->
<!--                {"application":"sj-shop"}-->
<!--            </customFields>-->

            <!-- 예외 스택 트레이스를 하나의 JSON 필드 안에 담는다. -->
            <throwableConverter class="net.logstash.logback.stacktrace.ShortenedThrowableConverter">

                <!-- 예외별 최대 출력 깊이 -->
                <maxDepthPerThrowable>100</maxDepthPerThrowable>

                <!-- 스택 트레이스 문자열의 최대 길이 -->
                <maxLength>50000</maxLength>

                <!-- 실제 원인 예외를 먼저 출력한다. -->
                <rootCauseFirst>true</rootCauseFirst>
            </throwableConverter>
        </encoder>
    </appender>

 

 

그리고, 반드시 root 에 추가해주자 .. 까먹었다가 로그가 안나왓다.

<root level="INFO">
    <appender-ref ref="CONSOLE"/>
    <appender-ref ref="FILE"/>
    <appender-ref ref="LOGSTASH"/>
</root>

 

이제 모든 서버를 재시작해주자

 

 

3. Error 로그 생성

 

아무 컨트롤러에 다음 로그를 추가하고, 요청하여 로그를 생성한다.

@Slf4j
@RestController
@RequiredArgsConstructor
public class ApiOrderController {

    private final OrderService orderService;


    @GetMapping("/order")
    public ResponseEntity<?> order() {
//        log.trace("[log.trace level] /order 호출");
//        log.debug("[log.debug level] /order 호출");
        log.info("[log.info level] /order 호출");
        log.warn("[log.warn level] /order 호출");
        log.error("[log.error level] /order 호출", new RuntimeException("런타임에러 발생"));
        return ResponseEntity.status(HttpStatus.OK).build();
    }
}

 

위에서 info, warn 로그같은경우 1줄이다.

하지만 error 로그는 멀티 라인이다. 파일 기반 logstash 를 하면 이때 좀 골치아팠다. 

이를 어떻게 인코더가 극복하는지 보자.

 

자! 로그가 발생했다.

 

현재 traceId 는 8cf19478 이다

 

 

4. Logstash  로그 확인

INFO, WARN, ERROR 로그 순서로 로그를 캡쳐했다. 

 

 

[INFO]

 

 

[WARN]

 

 

 

 

[ERROR]

stack_trace 가 보인다 !!, 별다른 노력없이 이를 해결했다!

 

 

이 결과가 키바나에서 볼 생각하니 가슴이 두근 두근 거린다!

 

 

5. kibana 확인해보기

 

키바나에서 냅다 검색해보자!, 세개의 로그가 바로 보인다!

하나씩 확인해보자, 특히 Log Level 이 Error 인게 제일 관심사이다.

 

 

 

 

마찬가지로 Level 별로 확인해보자

 

 

[INFO]

 

 

 

[WARN]

 

 

[ERROR]

 

level 필드, stack_trace 그외 필드까지 라이브러리가  생성하고 값도 넣어줬다. 라이브러리 승리이다. !

그외에 application, environment 커스텀으로 추가한것도 잘 나왔다 

 

 

 

 

 

 

6. kibana lens 사용해보기

 

키바나는 대시보드도 생성할 수 있는데. 관심있는 데이터는 대시보드를 생성하여 눈으로보자!

lens 란 대시보드에 각각의 패널 이다?

별도의 지식없어도 다루면서 gpt 에게 질문해서 만들면서 하니 할만했다.

 

 

 

대쉬보드

 

 

 

결과적으로 만든 대쉬보드는 log level 을 지정해서 

 

정상로그 / 경고로그 / 에러로그 순을 강제하여 잡았다.

 

 

 

이 대쉬보드에 하나하나 요소가 lens 인가보다. 따라서 클릭하면  수정할 수있다.

 

 

 

패널은 한번더 공부할겸 다시 만들어봤다.

 

 


느낀점

 

다양한 애플리케이션의 로그를 한곳에서 중앙 집중식으로 수집하면.

여러가지 프로젝트를 했을때 한곳에서 볼 수 있다는 장점이 매우클것같다.

 

각 서버마다 ssh 로 연결하려면 계정정보를 계속 다루어야하고.

로그를 보는 권한보다. 서버의 접근하는 권한이 더 큰 권한이라고 생각된다.

마치 집에있는 내 책을 보려고 집 비밀번호를 알아야하는 샘이니.

 

그리고 대시보드를 잘만 사용하면,

 

비개발자들은 Kibana 에서 대시보드를 활용해서 보고.

 

개발자들은 프로메테우스, 그라파나 까지 보면 될 것 같다

 

이런식으로 하면 괜찮게 쓸수 있지 않을까? 하는 생각이 든다.

 

아 재밋다.

728x90
728x90

* (주의) 아직 학습 단계라 정확하지 않을 수 있습니다, 편한 말투로 진행됩니다.

 

버전 정보 및 의존성

  • Gradle
  • Jdk 25
  • Spring Boot 4.1.0

 


 

실무하다보면. 많은 로그를 보게되는데. 그때마다 ssh로 들어가서 본적이 다들 한번씩 있을거다

 

그런사람들에게 ELK 스택은 정말 어렵지않은 난이도로 편하게 사용할 수 있는 스택같다.

 

ELK 는 사실 다음과같이 앞글자를 따서 만든 용어이다. 버전은 반드시 통일시키자

 

  1. Elasticsearch:9.1.0
  2. Logstash:9.1.0
  3. Kibana:9.1.0

 

Elasticsearch (엘라스틱서치) 란, 되게 검색엔진으로 유명한데. 검색엔진을 넘어 다양한 환경에도 쓰인다. 요즘은 AI에서도...

 

보통 검색엔진은 데이터를 수집하고. 그걸 색인하는 과정을 거친다. 이때 획기적으로 데이터 용량이 줄어드는데

이를 가지고 우수한 알고리즘으로 조회하면 빠르게 찾을 수 있다.

 

그리고 엘라스틱서치는 역인덱스를 사용해서 특정 키워드를 기준으로 마치.. 일대다 관계로 문서를 찾는다.

너무 방대해서 나도 공부할때마다 새롭다.

 

하지만 ELK 스택에서는 엘라스틱서치 지식은 부족해도 충분히 사용할 수 있다.

 

Logstash (로그스태시) 는 로그를 긁어다가 엘라스틱 서치로 넘겨준다.

 

Kibana (키바나) 또한 엘라스틱 서치 학습할때 쿼리를 날리는 용으로 많이 사용했다. 그외 시각화도 할수있다.

 

 

 

 

ELK 스택을 사용하면,  더이상 ssh 로 서버로 접속하여 tail -f 라든지 일자별 파일 털어서 검색할 필요가 없게된다.

 

그외에도 이점이 많다.

애플리케이션 서버에서는 하드디스크 용량 제한때문에 로그를 평생 보관할 수 없다.

하지만 엘라스틱서치를 활용하면 좀더 가벼운용량으로 서버 로그삭제 주기보다 오래 간직할 수 있고

검색또한 빠르다.

또 웹으로 볼수있으니 눈으로 보기도 편하고, 여러명이 사용할 수 있다.

 

전체흐름을 보고 시작하자, 빨간색 박스안의 범위인데,

혹시몰라서 프로메테우스와 그라파나도 같이 넣었다.

방향을 제대로 그린건가 ?.

 

 

실습을 진행하려면 ELK 스택의 서버가 필요하니 각자 준비해보자. 실행하고 다음을 이어가자.

 

 

Spring Boot

 

logback-spring.xml 파일이다. 로그 스태시가 특정 파일 가져갈 수 있게.

로그위치를 지정한다.

 

<?xml version="1.0" encoding="UTF-8"?>
<configuration>


    <!-- 로그를 저장할 경로 -->

    <property name="LOG_DIR" value="./logs" />
    <!-- 로그 파일 prefix -->
    <property name="LOG_FILE_NAME" value="sj-shop-log" />

    <!-- 로그를 출력할 곳을 정하는것이 appender -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) [%X{traceId}] %cyan(%logger{36}) : %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_DIR}/${LOG_FILE_NAME}.log</file>

        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level [%X{traceId}] %logger{36} : %msg%n</pattern>
        </encoder>

        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_DIR}/${LOG_FILE_NAME}-%d{yyyy-MM-dd_HH-mm}.log.gz</fileNamePattern>
            <maxHistory>5</maxHistory>
        </rollingPolicy>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="FILE" />
    </root>

</configuration>

 

 

 

LogStash 

 

LogStash 가 사용할 logstash.conf 설정 파일도 필요하다.

필터 로직이없어서. 일단 모든 로그가 다 넘어간다.

# 1. Input
# Spring Boot가 남긴 로그 파일을 Logstash가 읽어오는 단계다.
input {
  file {
    # Docker 컨테이너 내부에서 바라보는 애플리케이션 로그 파일 경로다.
    path => "/usr/share/logstash/logs/sj-shop-log.log"

    # Logstash가 이 파일을 처음 발견했을 때 파일의 처음부터 읽는다.
    start_position => "beginning"

    # 로그 파일에 새로운 내용이 추가됐는지 1초마다 확인한다.
    stat_interval => 1
  }
}

# 2. Filter
# 수집한 로그를 가공하거나 필드별로 분리하는 단계다.
# 현재는 원본 로그를 그대로 Elasticsearch로 전달한다.
filter {

}

# 3. Output
# 가공이 끝난 로그를 Elasticsearch와 콘솔로 전달하는 단계다.
output {
  elasticsearch {
    # 같은 Docker Compose 내부의 Elasticsearch 서비스명으로 접근한다.
    hosts => ["http://sj-shop-elasticsearch:9200"]

    # 로그를 날짜별 인덱스로 나누어 저장한다.
    index => "sj-shop-%{+YYYY.MM.dd}"
  }

  # Logstash가 처리한 이벤트를 컨테이너 콘솔에도 출력한다.
  # 초기 설정이나 수집 여부를 확인할 때 유용하다.
  stdout {
    codec => rubydebug
  }
}

 

 

 

Kibana

 

우선 키바나 사이트로 접속하자 5601 포트를 사용한다. 여기서부터는 브라우저에서 진행한다.

 

다음 사진 순서에 따라서 진행하자

 

 

키바나의 Data Views 클릭

 

 

신규 데이터 만들기위해 Check for new data 클릭

 

 

 

 

 

 

로그스태시에서 엘라스틱 서치에 자동으로 인덱스를 생성하게했으니

와일드 카드로 매치하면 현재 일자로 생성된 인덱스에 매치되는걸 알수있다

(여기서 오른쪽에 인덱스가 안보이면 생성이안된것이다. 혹시모르니 서버를 껏다켜봐라 그럼생성될 거다!)

 

 

일단 키바나의 데이터뷰가 생성되었고. 

 

 

 

이제 실시간 수집되는 로그를 보기위해서 다음 메뉴를 클릭한다

 

 

 

아무거나 spring boot 테스트 코드를 실행하여 의도적으로 에러를 만들었다.

 

 

 

 

 

실무에서 이정도 검색만 되도 . 많은 이점을 준다고 생각한다.!

 


참고로

logback 관련된 logstash 라이브러리를 활용하면 

 

build.gradle 일부

 

// spring boot 4 기준
implementation "net.logstash.logback:logstash-logback-encoder:8.1"

 

logback 에서 TCP 로 바로 엘라스틱 서치로 보낼수도있다.

 

즉, 로그 파일 대신에 TCP로 직접보내니 더 빠를 것같다. 관심있는 사람은 참고!

 

 

 

logback-spring.xml 일부

<appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
    <destination>localhost:5044</destination>
    <encoder class="net.logstash.logback.encoder.LogstashEncoder" />
</appender>

 

logstash.conf 일부

input {
    tcp {
        port => 5044
        codec => json
    }
}

output {
    elasticsearch {
        hosts => ["http://elasticsearch:9200"]
        index => "application-logs-%{+YYYY.MM.dd}"
    }
}

다음으로...

spring actuator 와 prometheus, grapana 를 작성해보겠다

 

728x90
728x90

* (주의) 아직 학습 단계라 정확하지 않을 수 있습니다, 편한 말투로 진행됩니다.

 

 

지난 카프카 포스팅에서 Outbox 패턴에 대한 테스트 코드는 스킵했었는데.

https://nataekoon.tistory.com/201

 

Kafka 요리조리 사용하기 (Spring Boot) 2 - 테스트 해보기

* (주의) 아직 학습 단계라 정확하지 않을 수 있습니다, 편한 말투로 진행됩니다. 지난 설정 편https://nataekoon.tistory.com/200 Kafka 요리조리 사용하기 (Spring Boot) - 설정편 1* (주의) 아직 학습 단계라 정

nataekoon.tistory.com

 

 

Outbox 패턴에 대해서 알아보자

 

필자의 경우 AI 와 대화중에 이 패턴을 알게되었고, 포스팅을 통해 정리해보려한다

 

우선 일반적인 Kafka, Application(Spring Boot), DB(Mysql) 상황을 보자 

 

그냥 보면 이해가 되는 구조일거다

 

아래의 그림처럼 비즈니스 메서드에서.

 

1,2 번이 순서를 나타내는건 아니다.

 

시스템 관점에서보면 다음과 같다

 

 

그렇다면 트랜잭션 범위는 어떨까?

 

일반적으로 트랜잭션을 말하면 DB와의 트랜잭션이므로 다음과 같은 범위일것이다.

 

따라서 Method에서 예외가 발생하면 골치 아파진다

 

문제 1. kafka 로 메세지를 먼저 발행한 경우
이때 db 는 롤백이 일어나도 kafka 는 롤백이 일어나지않는다

 

문제 2. db 에 insert 및 update 를 먼저한 경우

kafka 의 send 메서드는 기본적으로 비동기 메서드라. 트랜잭션이 커밋되어도.

이 카프카 발행이 성공적으로 일어난지 알수가없다

 

 

어쨋거나, DB 트랜잭션과 kafka 의 발행이 하나의 원자적 작업이 아니기 떄문이다.

 

 

그렇다면 outbox 패턴에 대해서 알아보자

 

1. 카프카에게 메시지를 발행을 하지않는다. 

대신에 outbox 테이블 또는 특정 비즈니스의 outbox 전용 테이블을 만들어

카프카로 보낼 데이터를 준비한다

 

 

아래의 사진에서는 order 와 order 전용 outbox 테이블을 두었다.

 

 

 

2. 별도의 스케줄러를 통해 outbox 의 데이터를 조회해 kafka 로 발행한다.

 

메세지 발행이 성공할때 콜백에서 트랜잭션을 열어 DB에 작업을한다. 즉 이 전체가 하나의 트랜잭션이다.

결국 전제 조건이 카프카에 메세지가 발행 성공 이다.

 

 

 

이쯤에서 결국 한가지 의문이 들거다

콜백에서 작업중 서버가 셧다운되거나하면 결국 카프카에게만 발행되고. DB는 변경사항이 없을탠데 ? 

맞다. 콜백에서 예외가 발생되면. 트랜잭션은 롤백될거고. kafka 메세지는 발행된채 있을것이다.

 

다시 이 스케줄러가 동작하면 또 카프카에게 발행할 것이다. 즉, 중복이 발생한다.

 

 


 

이때부터 메세지 전달 보장을 알아야하는데...

 

사실 지금 방식은 "At Least Once" 적어도 한번은 메시지가 보내진다는 정책이다.

(실무에서 가장많이 사용한다!)

 

카프카에 메세지가 중복이 일어 날 수 있으니, 이를 소모하는 컨슈머에 멱등성이 반드시 필요하다.

카프카에도 트랜잭션있긴한데 다루진않겠다.

 

따라서 order_outbox 에 event_id 가 유니크이다 (뭐가되든..)

 

컨슈머입장에서 중복이라는걸 알 수 있게 특정 칼럼이 유니크여야 편하다.

컨슈머는 컨슈머만의 테이블에 event_id 를 마찬가지로 최소 유니크로 잡고

작업전에 조회하여 이미 처리된 이벤트인지 아닌지 판단해야한다.

 

이상으로 마친다.

 

 

 

 

728x90

+ Recent posts