no image
Apache Kafka 중복 메시지를 어떻게 방지할 것 인가
카프카를 사용하다 보면 예상치 못한 시점에서 중복 메시지가 발생할 수 있다. 카프카는 파티션의 각 레코드에 대한 위치를 숫자로 관리하고 위치에 대한 숫자를 Offset이라고 한다. 그리고 그 숫자는 PrimaryKey 역할을 한다. 중복 메시지를 해결을 하기 위해서는 우선적으로 Consumed Offset 과 Committed Offset에 대해 명확하게 알아야 해결이 가능하다.Consumed Offset은 컨슈머가 메시지를 어디 지점까지 읽었는지를 나타낸다.Consumed Offset은 컨슈머가 poll을 받을 때마다 자동으로 업데이트 되기 때문에 컨슈머가 읽어야 할 다음의 메시지 위치를 식별이 가능하다. Committed Offset은 컨슈머가 메시지를 읽고 해당 지점까지 오프셋을 처리했다라고 알려준..
2024.12.03
Dev
no image
Apache Kafka에 대하여
Kafka 는 RabbbitMQ, ActiveMQ와 같은 전통적인 Message Queue 방식과 비교 했을 때 처리량, 처리속도, 가용성, 확장성이 월등하게 앞선다.이러한 장점 때문에 IT 서비스 회사들은 Kafka 도입을 적극적으로 추진하고 있다. 하지만 Kafka 를 잘 모르고 그냥 남들이 다 해서 도입을 하게 되면 오히려 안쓰는게 좋은 선택일 수도 있다.필자의 회사에서도 Kafka 를 도입하고 있는데 더 명확한 개념을 학습하기 위해 이렇게 정리를 한다. Apache Kafka 는 대용량 실시간 데이터 스트리밍을 처리하기 위한 분산형 메시징 플랫폼 이면서 마이크로서비스 아키텍쳐 (Micro Service Architecture) 내에서도 활용되는 오픈소스이다.Apache Kafka는 LinkedIn..
2024.12.03
Dev
no image
MYSQL B-tree Index(인덱스)
인덱스(index)란대표적인 예시로 책의 맨 끝에 있는 “색인"정도로 설명할 수 있다.책의 색인에 표시 된 페이지수는 데이터 파일에 저장된 레코드 번호정도로 이해하면 된다.인덱스는 기본적으로 칼럼의 값과 해당 레코드가 저장된 주소를 key-value 형식으로 인덱스를 만들어둔다. 인덱스는 SortedList 방식과 같은 형태로 저장되기 때문에 저장할 때 마다 정렬을 해줘야 하기 때문에 저장과정이 복잡하고 느리다.인덱스를 사용하는건 데이터의 저장(Insert,Update,Delete) 성능을 어느정도 희생해서 데이터 읽기 속도를 올리는 기능이라고 보면 된다. 그렇다고 무작정 Where 절에 들어가는 칼럼을 모두 인덱스로 설정하게 되면 저장 성능이 현저히 떨어지고 인덱스 크기가 비대해져 역효과만 불러온다.이..
2024.12.03
no image
JPA - Enum 타입 엔티티에서 커스텀 코드 DB에 넣기
JPA entity에서 Enum 값을 사용하는 경우 흔히 아는 건 ORDINAL, STRING 옵션이다.  하지만 서비스를 구성하다 보면 DB에 특정 코드가 들어가야 하는 경우도 분명 있을 거다. 예시를 한번 보자 이런식으로 ENUM이 구성되어 있을 때 별도의 설정 없이 사용하면 ORDINAL 값인 인덱스 값이 들어가고 옵션을 STRING으로 설정하면 ENUM의 이름 자체가 DB에 인서트 될 것이다. 하지만 만약 저 code 값을 DB에 넣어주고 싶다면 어떻게 해야 할까위 이미지는 Hibernate 공식 페이지의 설명이다. 열거형 옵션에는  ORDINAL, STRING만 지원을 하고 다른 옵션은 제공하지 않는다. 하지만 바로 아래로 내려가면  Converters라는 내용이 있다. 대충 설명하자면 주어진 ..
2024.10.27
no image
ApplicationContext - Annotation 기반 Bean register 등록 원리
Spring Context Bean이 등록되는 과정을 한번 보자 우선 ApplicationContext의 구현체인 AnnotationConfigApplicationContext를 보자 이 구현채는 기존 xml로 하나하나 빈으로 등록해주던 동작을 @Configuration 어노테이션이 적용된 자바 설정 파일을 통해 빈을 등록이 가능하도록 해준 클래스이고 @Component, @Service, @Repository, @Controller와 같은 어노테이션이 붙은 클래스들을 자동으로 감지하여 빈으로 등록할 수 있도록 해준다. AnnotationConfigApplicationContext는 Spring의 기본 컨테이너인 ApplicationContext의 구현체로, 자바 설정 파일을 기반으로 동작합니다. 이 컨..
2024.10.23
no image
Mysql Architecture - Mysql Engine에 대해
Mysql architecture 에 대해서 한번 알아보자Mysql은 머리 역할을 하는 Mysql Engine과 손발 역할을 하는 Storage Engine으로 구분 할 수 있다.  이번 포스팅에서는  Mysql server에서 제공되는 Mysql Engine과 Storage Engine을 알아보자. Mysql Server는 다른 DBMS에 비해 구조가 독특하다. 우리가 체감하기는 쉽지 않지만 이러한 구조 때문에 다른 DBMS에서는 가질 수 없는 엄청난 혜택을 누릴수도 있고, 반대로 다른 DBMS에서는 전혀 문제가 되지 않는 것이 문제가 될수도 있다. 아래 사진은 Mysql의 전체 구조이다. 이제 하나씩 알아보자 Handler API(Connection Handler)Mysql Engine의 쿼리 실행기..
2024.10.22
no image
트랜잭션(transaction) 강결합 , 약결합
모놀리식 구조에서 점점 서비스가 커지고 트래픽이 많아지게 되면 이 강결합된 구조가 문제를 일으키는 경우가 발생한다.이벤트 기반 구조로 코드를 구성 하기 전에 트랜잭션 강결합과 약결합에 대해서 알아보자 트랜잭션 강결합(Strong Coupling)과 약결합(Loose Coupling)은 시스템이나 서비스의 의존성 수준을 나타낸다. 이 개념은 MSA, 분산 시스템, 또는 데이터베이스 트랜잭션에서 트랜잭션 경계와 서비스 간의 상호작용 방식에 따라 차이점이 있다. 우선 강결합 트랜잭션에 대해서 알아보자 📍 강결합 트랜잭션 (Strong Coupling Transaction)강결합 트랜잭션은 두 개 이상의 컴포넌트나 서비스가 밀접하게 연결되어 있어서 한 컴포넌트, 서비스의 변경이 다른 컴포넌트, 서비스에 영향을..
2024.10.07
Dev
no image
인증(Authentication)과 인가(Authorization)
사이드 프로젝트를 진행하면서 두 개념에 대해 명확하게 구분되어 동작 해야 하는 로직이 있어서 개념 정리 겸 한번 적어본다. 1. 인증(Authentication)이란?인증은 사용자가 누구인지 확인하는 과정입니다. 예를 들어, 웹 애플리케이션에 로그인할 때 아이디와 비밀번호를 입력하는 것이 바로 인증의 대표적인 예시이다. 이 과정에서 시스템은 사용자가 주장하는 정체성(아이디)이 실제로 맞는지 검증한다. 쉽게 말해 인증(Authentication)은 공항에서 입국 심사정도로 보면 된다. 신원확인(?)정도로 이해하면 편할 것이다. 인증도 방법이 여러가지가 있다.( 아래 방법 외에도 여러가지가 있지만 대표적인것들만 적어본다.)아이디와 비밀번호: 가장 일반적인 방식으로, 아이디와 비밀번호를 통해 사용자를 식별.2..
2024.10.06
Dev
728x90

카프카를 사용하다 보면 예상치 못한 시점에서 중복 메시지가 발생할 수 있다. 카프카는 파티션의 각 레코드에 대한 위치를 숫자로 관리하고 위치에 대한 숫자를 Offset이라고 한다. 그리고 그 숫자는 PrimaryKey 역할을 한다.

 

중복 메시지를 해결을 하기 위해서는 우선적으로 Consumed Offset 과 Committed Offset에 대해 명확하게 알아야 해결이 가능하다.

Consumed Offset은 컨슈머가 메시지를 어디 지점까지 읽었는지를 나타낸다.Consumed Offset은 컨슈머가 poll을 받을 때마다 자동으로 업데이트 되기 때문에 컨슈머가 읽어야 할 다음의 메시지 위치를 식별이 가능하다.

 

Committed Offset은 컨슈머가 메시지를 읽고 해당 지점까지 오프셋을 처리했다라고 알려준다. 만약 프로세스가 실패하고 재시작을 하게 된다면 Committed Offset을 기준으로 재시작을 하게 된다. 고로 Commited Offset은 컨슈머가 다시 메시지를 읽게 될 시작점이 되는 오프셋이기도 하다. Committed Offset은 카프카 내부 __consumer_offsets 토픽에서 관리한다.

 

그럼 이제 이 오프셋들을 컨슈머가 메시지를 읽어들이는 과정을 예시로 어떻게 동작 하는지 한번 보자

  1. 특정 파티션을 컨슈머가 poll() 메서드를 호출하면 아직 읽지 않은 메시지를 반환한다. 그리고 메시지 읽어서 가져 갈 때 마다 Consumed Offset이 업데이트 된다.
  2. 읽어들인 메시지를 정상적으로 처리하고, 이후 Offset Commit 을 실행하고 카프카에게 정상 처리된 메시지의 최종 위치를 알린다. 그 최종 위치가 Committed Offset이다.

이 동작은 정상적으로 처리 됐을 때 예시다. 모든 동작이 100 프로 정상이면 좋겠지만 개발에 100프로는 없다. 그러나 만약 컨슈머에서 장애가 발생하고 컨슈머 그룹에 새로운 컨슈머가 추가 될 경우 리벨런싱이 발생한다. 리벨런싱이 발생 할 경우에는 각 컨슈머에 할당되는 파티션이 바뀔수도 있다. 그 다음 리벨런싱 된 컨슈머에서는 Commited Offset을 기준으로 재시작하게 된다. 하지만 컨슈머에서 발생한 장애 시점이 Offset Commit이 되기 직전이라면? 이 상황에서 중복 메시지 이슈가 발생 할 수 있다.

  1. 컨슈머 A에서 읽어들인 메시지를 모두 읽어서 정상처리 했다고 가정을 했을 때 Commited Offset이 4라고 가정하자
  2. 그리고 그 다음 다시 poll()메서드를 실행하여 Commit Offset 4 이후 부터 실행을 해서 8까지 처리했다고 가정을 하자
  3. 그 다음 8까지 실행 후 Commited Offset 실행 중 오류가 발생해서 리벨런싱이 실행되고 메시지를 가지고 있던 파티션의 소유주가 컨슈머 B라고 생각하자. 여기서 중복 메시지가 발생한다. 소유권을 가진 컨슈머 B는 Committed Offset이 4인줄 알게된다. 위에서 8까지 실행 후 offset commit에 실패 했기 때문이다. 이러면 이미 읽어들인 메시지를 또 읽어 중복으로 메시지를 읽는 현상이 발생한다.
Kafka: The Definitive Guide

 

이러한 문제점이 발생한다고 가정을 하고 컨슈머 쪽에서 대응이 필요하다.

중복된 메시지를 해결 할 만한 방법 중 하나도 중복 메시지를 걸러내는 로직을 구현 하는 것이다. 비즈니스 로직 실행과 처리한 메시지 정보를 insert 하는 것을 하나의 트랜잭션으로 묶어서 메시지가 중복 실행 되지 않도록 구현 해준다. 우선 처리되는 메시지 프로세스 내용을 저장하는 테이블을 만들고 메시지의 식별자를 unique index로 걸어두게 되면 만약 중복으로 실행된다면 해당 테이블에 입력이 되지 않고 전체 트랜잭션이 롤백이 된다.

 

그 다음으로는 transactional outbox pattern이 있다. 이 패턴은 비즈니스 로직과 메시지 발행 로직을 하나의 트랜잭션으로 묶어서 동작하는 방식이다. 메시지 발행 로직에서 message_outbox라는 테이블을 생성하여 외부에 전달할 메시지 정보를 해당 테이블에 insert하는 방식으로 구현한다. 그 다음 구현한 메시지 발행 로직과 비즈니스 로직을 하나의 트랜잭션 단위로 묶어서 구현하게 되면 원자성을 보장하는 트랜잭션 내부에서 비즈니스 로직과 메시지 발행 로직은 항상 함께 성공,실패를 하게 된다. 이후 message_outbox 테이블을 바라보면서 외부로 메시지를 발행하는 로직을 별도로 구현하면, 비즈니스 로직이 실행 완료되면 반드시 외부로 메시지가 발행되는 것이 보장된다.

https://www.eliasbrange.dev/posts/transactional-outbox-pattern-dynamodb/

 

이 방법 이외에도 있지만 아직 필자가 정확하게 다뤄본적 없는 내용이라 명쾌하게 설명이 힘들것 같아 조금 더 공부하고 별도로 써볼려고 한다.

카프카 자체가 우리에게 가져다주는 이점도 분명 있지만 정확하게 알지 못하고 사용하면 오히려 더 독이 될 수도 있다.

필자의 카프카 시리즈는 앞으로 계속될것이다.

728x90

Apache Kafka에 대하여

ryudjae
|2024. 12. 3. 08:29
728x90

Kafka 는 RabbbitMQ, ActiveMQ와 같은 전통적인 Message Queue 방식과 비교 했을 때 처리량, 처리속도, 가용성, 확장성이 월등하게 앞선다.

이러한 장점 때문에 IT 서비스 회사들은 Kafka 도입을 적극적으로 추진하고 있다. 하지만 Kafka 를 잘 모르고 그냥 남들이 다 해서 도입을 하게 되면 오히려 안쓰는게 좋은 선택일 수도 있다.

필자의 회사에서도 Kafka 를 도입하고 있는데 더 명확한 개념을 학습하기 위해 이렇게 정리를 한다.

 

Apache Kafka 는 대용량 실시간 데이터 스트리밍을 처리하기 위한 분산형 메시징 플랫폼 이면서 마이크로서비스 아키텍쳐 (Micro Service Architecture) 내에서도 활용되는 오픈소스이다.

Apache Kafka는 LinkedIn에서 처음 개발되어 현재는 Apache 재단에서 관리되고 있다. LinkedIn 에서는 방대한 양의 데이터를 실시간으로 처리하는 과정에서 어려움을 겪었고 이 문제를 해결하기 위해 중앙화 된 메시지와 데이터의 흐름을 관리하는 구조를 가져가기 위해 만들어진 것이 Kafka 이다.

LinkedIn이 Kafka 를 사용 하기 전 내부 시스템 구조를 보면 매우 복잡한 구조 이면서 개별적인 연결을 하는 구조였기 때문에 확장 에도 어려움이 분명 존재했다.

LinkedIn 의 과거 시스템 구조를 도식화 된 그림으로 한번 보자

https://www.confluent.io/blog/event-streaming-platform-1/

https://www.confluent.io/blog/event-streaming-platform-1/

첫번째 사진이 과거 구조이고 두번째 사진은 kafka가 적용되어 kafka가 중앙에서 메시지와 다양한 데이터 흐름을 가져가는 구조로 변경되었다.

kafka 가 도입 되면서 확실히 간결해진 모습을 볼 수 있다.

이제 kafka의 기본적인 개념에 대해서 한번 보자.

https://data-flair.training/blogs/kafka-architecture/

위 사진은 Kafka의 구조를 보여준다.

Kafka Cluster

여러 대의 분산된 Kafka 서버를 네트워크로 연결하여 하나의 서버처럼 동작하게 만드는 개념을 Server Clustering 이라고 한다. 여러대의 Kafka 서버를 클러스터로 묶게 되면 특정 서버에서 장애가 발생해도 다른 서버에서 외부 요청을 처리 할 수 있기 때문에 서비스 전체의 가용성이 올라가게 된다.

Kafka broker

broker 는 Kafka 의 서버를 말한다. broker 는 producer 부터 메시지를 수신하고 Offeset 을 지정한 후 해당 메시지를 디스크에 저장하게 된다. 그리고 컨슈머가 파티션 읽기를 요청하면 그 요청에 대해 디스크에 수록된 데이터를 전송하여 응답한다.

Kafka Broker 는 Kafka Cluster 의 일부 구성원으로 동작된다. 여러 개의 broker 가 하나의 Kafka Cluster 에 포함될 수 있고, 이 중 하나는 컨트롤러 역할을 한다. 컨트롤러는 Broker 들이 정상적으로 동작 하는지 모니터링하고 Broker 에게 담당 파티션을 할당 해준다.

Topic

Kafka의 메시지는 Topic으로 분류가 된다. 하나의 Topic은 여러개의 파티션으로 구성될 수 있다. 메시지는 파티션에 앞에서부터 뒤로 추가 되는 형태로만 기록되고 맨 앞부터 차례대로 메시지가 읽힌다.

Partitio

파티션은 Topic을 여러 파티션으로 나눠서 병렬 처리가 가능하도록 한다. 여기서 각 파티션은 서로 다른 서버에 메시지가 분산되어 요청 될 수 있다. 이 말은 즉 하나의 토픽이 여러 서버에 걸쳐 수평적으로 확장이 가능 하단 뜻이다. 이러한 이유는 Kafka에서 메시지 처리 순서가 Topic이 아닌 파티션 별로 관리가 되기 때문이다.

고로 단일 서버로 처리할 때 보다 훨씬 더 높은 성능을 가질수 있다.

Message

Kafka 에서 데이터의 기본 단위이다. 메시지는 바이트(byte) 배열의 데이터로 간주한다. 바이트 배열로 간주하기 때문에 데이터의 형태는 제약 없이 자유롭게 저장이 가능하고 메시지를 받는 입장에서는 변환하여 사용해야 한다.

메시지는 파티션에 기록된다. 메시지가 어떤 파티션에 기록 할 파티션을 결정하기 위해 메시지에 담긴 key값을 해시 처리하고, 그 값과 일치하는 파티션에 메시지를 기록한다. 여기서 key값을 해시 처리 하는 기능을 해시 파티셔너라고 한다. 고로 같은 키 값을 가진 메시지는 같은 파티션에 저장된다는 의미이다. 만약 키값이 없는 상태 (null)로 넘어가게 되면 Kafka 내부의 기본 파티셔너가 라운드로빈 (Round robin) 방식으로 파티션에 기록되는 메시지 개수의 균형을 맞추게 된다.

Producer

프로듀서는 새로운 메시지를 특정 토픽에 생성 하는 역할을 한다. 프로듀서는 기본적으로 어떤 파티션에 기록 될지는 관여하지 않는다. 어떤 파티션에 기록 될지 결정 하는 것은 위에서 말한 파티셔너의 역할이다.

Consumer

컨슈머는 하나 이상의 토픽을 구독하면서 메시지가 생성된 순서대로 읽는다. 여기서 메시지는 파티션 단위 Offset 을 통해 어디까지 읽었는지 알수있다. 오프셋은 Commit Offset과 Current Offset이 있다. Commit Offset은 컨슈머로 부터 “여기까지 작업을 했다"를 확인 하는 역할을 한다. Current Offset은 컨슈머가 어디까지 읽었는지를 나타내는 오프셋이다.

이러한 오프셋의 기능 덕분에 컨슈머가 읽기를 중단 했다가 다시 시작하더라도 완료된 그 다음 메시지부터 읽을수 있다.

728x90
728x90

인덱스(index)란

대표적인 예시로 책의 맨 끝에 있는 “색인"정도로 설명할 수 있다.책의 색인에 표시 된 페이지수는 데이터 파일에 저장된 레코드 번호정도로 이해하면 된다.

인덱스는 기본적으로 칼럼의 값과 해당 레코드가 저장된 주소를 key-value 형식으로 인덱스를 만들어둔다. 인덱스는 SortedList 방식과 같은 형태로 저장되기 때문에 저장할 때 마다 정렬을 해줘야 하기 때문에 저장과정이 복잡하고 느리다.

인덱스를 사용하는건 데이터의 저장(Insert,Update,Delete) 성능을 어느정도 희생해서 데이터 읽기 속도를 올리는 기능이라고 보면 된다. 그렇다고 무작정 Where 절에 들어가는 칼럼을 모두 인덱스로 설정하게 되면 저장 성능이 현저히 떨어지고 인덱스 크기가 비대해져 역효과만 불러온다.

이제 인덱스의 가장 대표적인 알고리즘 B-Tree알고리즘을 한번 알아보자

B-Tree index

B-Tree index는 데이터베이스에서 가장 일반적으로 사용되고 있고, 가장 먼저 도입된 알고리즘이다. B-Tree 는 칼럼의 원래 값을 변형시키지 않고 인덱스 구조체 내에서 항상 정렬된 상태로 유지한다. 일반적으로 DBMS 에서 주로 B+-Tree 또는 B*-Tree가 사용된다. 다들 위 그림처럼 B-Tree 그림을 봐서 Binary 라고 생각 하는 분들도 계신데 B는 “Balanced”를 의미한다.

구조 및 특징

https://blog.jcole.us/2013/01/10/btree-index-structures-in-innodb/

B-Tree 는 트리 구조이며 최상위에 “root Node” 가 존재하고 그 하위에 자식 노드가 붙어 있는 형태이고 ,“branch Node” 라는 중간 노드와 “leaf Node” 라는 최하위 노드가 존재한다.

데이터베이스에서 인덱스와 실제 데이터는 따로 관리가 되는데 leaf Node는 실제 데이터의 주소를 가지고 있다. leaf Node 아래에 데이터파일이 존재하는데 데이터 파일은 따로 정렬이 되어있지 않다. 데이터 파일은 insert 순서대로 저장이 되긴하지만 무조건 보장이 되는 것은 아니다.

만약 데이터를 한번도 삭제,수정이 없었던 경우는 가능할지도 모르지만 insert 데이터는 들어올때 만약 기존 있던 데이터가 삭제된 공간이 있으면 그 공간을 재활용한다. 그래서 항상 insert 순서대로 입력되는건 아니다.

그리고 인덱스는 키 칼럼값만 가지고 있으므로 나머지 칼럼을 읽으려면 데이터 파일에서 해당 레코드를 찾아와야한다. 그렇다면 만약 인덱스를 걸어둔 칼럼만 가지고 온다면 데이터 파일까지 갈 필요가 없지 않겠는가? 맞다

인덱스가 걸린 칼럼만 가져오거나 count를 한다면 굳이 실제 데이터 파일까지 들어가지 않아 성능이 올라간다.

B-Tree 인덱스 사용에 영향을 미치는 요소

> 인덱스 키 값 크기

InnoDB 스토리지 엔진은 디스크에 데이터를 저장하는 가장 기본 단위를 페이지 또는 블록이라고 부른다. 디스크에서 모든 읽기 및 쓰기 작업의 최소 작업 단위이다. 이 페이지 안에 인덱스 키 값이 들어가게 되는데 페이지 용량은 무한하지 않기 때문에 만약 키 값이 커지게 되면 디스크로 부터 읽어야 할 횟수가 늘어나게 되고 그만큼 느려지게 된다.

만약 페이지(16KB)안에 넣는다고 가정을 하면 키값이 12바이트라고 가정하면 약 585개의 키값 저장이 가능하다. 하지만 키 값이 32바이트일 경우에는 372개의 키값만 한 페이지에 저장이 가능하다. 만약 500개의 레코드를 스캔한다면 전자는 1번만 하면 되고 후자는 2개의 페이지를 디스크로 부터 읽어야 한다.

> 기수성(Cardinality)

모든 인덱스 값 가운데 유니크한 값의 수를 의미한다. 우선 결론부터 말하자면 기수성은 높을수록 좋다. 1000개의 인덱스 키값중 100개가 유일값이라면 기수성은 10이 된다. 왜 기수성이 높으면 좋은지 보자.

만약 1000개의 데이터에 10개의 유일한 키값을 가진 인덱스가 있다하면 특정 데이터를 조회시 1개의 값 조회를 위해 99개의 값을 더 읽어야한다.

만약 100개의 유일한 키 값을 가진 인덱스가 존재한다면 데이터를 읽을 때 9개만 더 읽게 된다.

이것만 봐도 확 느껴진다. 지금은 수가 그렇게 크지 않지만 데이터가 많아지게 되면 더더욱 신경을 써야하는 부분이다.

그리고 인덱스를 통해 읽는 데이터는 전체 레코드에 20%–25%를 넘어서면 테이블 데이터를 전체를 읽어서 필터링 해내는게 더 효율적이다.

인덱스 키 추가

새로운 키 값이 B-Tree 에 저장될 때 테이블의 스토리지 엔진에 따라 새로운 키 값이 저장될 수도 있고 아닐수도 있다. B-Tree에 저장될 때는 저장될 키값을 통해 적절한 위치를 찾아서 키값과 대상 레코드의 주소값을 leaf Node에 저장한다.

만약 leaf Node가 꽉 차서 넣을수 없다면 리프노드를 분리해서 저장을 해줘야한다. 이런 동작들 때문에 인덱스를 사용하면 입력 작업 성능이 떨어지게 된다.

인덱스 키 변경

인덱스의 키 값은 leaf Node를 결정하는 값이기도 해서 단순 변경은 불가능하다.만약 변경을 하려면 우선 키값을 삭제하고 새로운 키 값을 입력해주는 형태이다.

변경이라곤 하지만 실질적으로 삭제 후 새로 입력이다.

인덱스 키 삭제

B-Tree의 키 값이 삭제되는 경우는 해당 키 값이 저장된 B-Tree 의 리프 노드를 찾아서 그냥 삭제 마크만 하면 작업이 완료 된다. 이렇게 삭제 마킹이 된 인덱스 키 공간은 재활용이 가능하다.

위에서 말한것 처럼 입력된 순서대로 저장이 보장되지 않는 이유가 이런 재활용 때문이다.

인덱스 키 검색

우리가 인덱스를 사용하는 이유는 입력 동작은 조금 성능이 떨어지더라도 읽기 성능을 올리기 위해서이다.

B-Tree는 Root Node부터 시작해 Branch Node를 거쳐 leaf Node까지 이동하면서 비교작업을 수행하는데, 이 과정을 “트리 탐색"이라고 한다. 인덱스 검색은 부등호 (<,>)비교 조건에서도 인덱스를 활용할 수 있지만, 인덱스를 구성하는 키 값의 뒷부분만 검색하는 용도로는 인덱스를 사용할 수 없다.

또한 인덱스를 이용한 검색에서 키값이 변형이 가해진 경우에는 인덱스에 존재하는 값이 아니기 때문에 사용이 불가능하고 B-Tree의 장점을 활용할 수 없게 된다.

추가로 Inno DB 테이블에서 지원하는 Record Lock이나 Next Key Lock이 검색을 수행한 인덱스를 잠근 후 테이블의 레코드를 잠그는 방식으로 구현되어 있기 때문에 수정이나 삭제 과정을 수행할 때 적절한 인덱스가 없으면 불필요하게 많은 데이터가 잠기게되고 최악의 상황은 전체 데이터를 잠글수도 있다는 것이다. Inno DB에서는 인덱스 설계가 매우 중요하다.

인덱스를 통한 데이터 읽기

index range scan

인덱스의 접근 방법 중 가장 대표적인 방법이고 index full scan, loose index scan 보다 빠른 방식이다. 인덱스 레인지 스캔은 검색해야 할 인덱스의 범위를 우선적으로 결정한 후 탐색을 한다.

루트 노드에서부터 비교를 시작해 브랜치 노드를 거쳐서 최종적으로 리프 노드까지 들어가야만 필요한 레코드의 시작 지점을 알 수 있다. 리프노드에서 시작점을 찾았다면 거기서부터 순차적으로 읽는다. 그리고 해당 리프노드에 데이터가 없다면 리프노드간 링크를 이용해 다음 리프노드로 넘어가서 다시 스캔한다.

그리고 범위가 끝났다면 조건에 맞는 레코드를 반환하고 쿼리가 마무리 된다. 여기서 필요한 레코드를 리턴하게 되면 인덱스 주소를 통해 랜덤I/O가 데이터 갯수 만큼 발생한다.

index full scan

이름 그대로 인덱스를 전체 스캔하는 방식이다. 이 경우는 쿼리에 조건절이 사용된 칼럼이 첫번째 칼럼이 아닌 경우 인덱스 풀 스캔으로 동작한다. 인덱스 풀 스캔은 레코드를 가져오는 경우에는 절대로 사용하지 않고 인덱스에 포함된 칼럼만 가지고 올 경우에만 사용된다.

loose index scan

MYSQL 8.0 버전부터 최적화 된 버전으로 나온 스캔이다. 루스 인덱스 스캔은 인덱스 레인지 스캔과 비슷하지만 중간에 필요하지 않은 데이터는 스킵하고 다음으로 넘어간다. 일반적으로 Groupby나 MAX(),MIN()함수에 대해 최적화 시 사용한다.

index skip scan

Real Mysql 8.0 -238p

이것도 MYSQL 8.0 버전부터 나온 스캔이다. 기존에는 인덱스의 첫번째 칼럼이 없으면 새로 만들거나 했어야 했다. 하지만 인덱스 스킵 스캔은 첫번째 칼럼이 아닌 두번째 칼럼으로도 인덱스 스캔이 가능하도록 해준다.

인덱스 스킵 스캔은 만약 두번째 칼럼으로 조회하게 된다면 첫번째 칼럼의 유니크한 값을 모두 조회해서 첫번째 칼럼에 조건을 추가해서 쿼리를 다시 실행하는 형태로 처리한다.

 

마치며

인덱스는 진짜 양날의 검이다. 사용을 잘못하면 오히려 가만히 두는거보다 더 안좋은 성능을 낼수도 있다. DBA가 따로 없는 경우에는 백엔드가 그냥 크게 고민 안해보고 구성하는 경우가 더러있다. 하지만 이왕이면 잠깐만 고민하고 공부해도 효율적으로 사용 할 수 있는데 안할 이유는 없는것 같다.

참고

Real Mysql 8.0 (꼭 사세요. 진짜 내용 좋음) — https://product.kyobobook.co.kr/detail/S000001766482

728x90
728x90

JPA entity에서 Enum 값을 사용하는 경우 흔히 아는 건 ORDINAL, STRING 옵션이다.  하지만 서비스를 구성하다 보면 DB에 특정 코드가 들어가야 하는 경우도 분명 있을 거다. 

예시를 한번 보자

 

이런식으로 ENUM이 구성되어 있을 때 별도의 설정 없이 사용하면 ORDINAL 값인 인덱스 값이 들어가고 옵션을 STRING으로 설정하면 ENUM의 이름 자체가 DB에 인서트 될 것이다. 하지만 만약 저 code 값을 DB에 넣어주고 싶다면 어떻게 해야 할까

위 이미지는 Hibernate 공식 페이지의 설명이다. 열거형 옵션에는  ORDINAL, STRING만 지원을 하고 다른 옵션은 제공하지 않는다.

 

하지만 바로 아래로 내려가면  Converters라는 내용이 있다.

 

대충 설명하자면 주어진 자바 유형을 ORDINAL, STRING으로 변환하거나 데이터베이스에서 작업이 일어날 때 사전사후 처리를 해준다고 한다.

이 기능을 사용해서 ENUM이 인서트 되기 전 ENUM 내부 특정값으로 바꿔주고 가지고 와서는 ENUM 값으로 보이도록 구성이 가능해진다.

 

AttributeConverter 인터페이스를 통해 컨버터 구현체를 만들어 줄 거다. 우선 내부가 어떤 식으로 구성이 되어있는지 보자 


AttributeConverter 내부를 보면 따로 구현할 구현체 외에도 Hibernate내부 동작에서도 사용을 하고 있다.

 

이제 우리가 ENUM에서 사용할 컨버터를 구성해 보자

 

이런 식으로 우선 간단하게 컨버터를 작성해줬다. convertToDatabaseColumn은 DB에 입력될 때 전처리 해주는 것이고 convertToEntityAttribute는 DB에서 값을 가지고 왔을때 ENUM값으로 표시해주는 컨버터이다.

DB 에서 가져온 값을 ENUM으로 표시하기 위해 ENUM 클래스에 변환로직을 작성해 준다.

 

그리고 @Converter(autoApply = true)를 컨버터 클래스 위에 명시해 주면  아래처럼 특별한 설정 없이 해당 ENUM에 자동으로 적용된다. 하지만 지금 필요한 것은 아니라 코드에선 빼둔 상태이다.

 

그다음 엔티티에 설정을 해주자

 

이런 식으로 Converter 클래스를 입력해 주면 

 

 

DB에 코드 값으로 정상 입력 된다. 

 

하지만 fromCode에서 for문을 보고 좀 찜찜함을 느끼는 사람도 있었겠지만 저게 지금 당장은 문제가 안되지만 나중에 DB 요청 자체가 많아지게 되면 저거 자체가 매우 불필요한 낭비다. 이걸 사용하려면 잘 고민을 해보고 사용하면 좋을 것 같다.

 


REFERENCE

https://docs.jboss.org/hibernate/orm/6.3/introduction/html_single/Hibernate_Introduction.html#enums

728x90

'Dev > SpringData' 카테고리의 다른 글

[JPA] 더티 체킹(Dirty Checking)  (0) 2021.12.22
[JPA] N + 1 이슈  (0) 2021.12.17
[JPA] 양방향 연관관계(무한 루프)  (0) 2021.12.14
[Querydsl] Projection 정리  (0) 2021.12.12
[Querydsl] QueryDSL 사용법 정리  (0) 2021.12.03
728x90

Spring Context Bean이 등록되는 과정을 한번 보자 우선 ApplicationContext의 구현체인 AnnotationConfigApplicationContext를 보자

 

이 구현채는 기존 xml로 하나하나 빈으로 등록해주던 동작을 @Configuration 어노테이션이 적용된 자바 설정 파일을 통해 빈을 등록이 가능하도록 해준 클래스이고 @Component, @Service, @Repository, @Controller와 같은 어노테이션이 붙은 클래스들을 자동으로 감지하여 빈으로 등록할 수 있도록 해준다.

 

AnnotationConfigApplicationContext는 Spring의 기본 컨테이너인 ApplicationContext의 구현체로, 자바 설정 파일을 기반으로 동작합니다. 이 컨텍스트는 BeanFactory를 포함하여 빈의 라이프사이클을 관리해준다.

    public void register(Class<?>... componentClasses) {
        Assert.notEmpty(componentClasses, "At least one component class must be specified");
        StartupStep registerComponentClass = this.getApplicationStartup().start("spring.context.component-classes.register").tag("classes", () -> {
            return Arrays.toString(componentClasses);
        });
        this.reader.register(componentClasses);
        registerComponentClass.end();
    }

 

코드를 간단하게 설명하자면 스프링 애플리케이션이 시작되면 @Bean이 붙은 모든 클래스를 불러와서 register를 통해 Bean을 등록한다.

 

AnnotationConfigApplicationContext에서 받아온 Class들을 AnnotatedBeanDefinitionReader register를 통해 Bean에 등록하는 작업을 해준다.

 

이제 AnnotatedBeanDefinitionReader register 의 동작을 한번 보자 

public void register(Class<?>... componentClasses) {
     Class[] var2 = componentClasses;
     int var3 = componentClasses.length;

     for(int var4 = 0; var4 < var3; ++var4) {
         Class<?> componentClass = var2[var4];
          this.registerBean(componentClass);
    }
}

 

    public void registerBean(Class<?> beanClass) {
        this.doRegisterBean(beanClass, (String)null, (Class[])null, (Supplier)null, (BeanDefinitionCustomizer[])null);
    }

 

private <T> void doRegisterBean(Class<T> beanClass, @Nullable String name, @Nullable Class<? extends Annotation>[] qualifiers, @Nullable Supplier<T> supplier, @Nullable BeanDefinitionCustomizer[] customizers) {
        AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(beanClass);
        if (!this.conditionEvaluator.shouldSkip(abd.getMetadata())) {
            abd.setAttribute(ConfigurationClassUtils.CANDIDATE_ATTRIBUTE, Boolean.TRUE);
            abd.setInstanceSupplier(supplier);
            ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd);
            abd.setScope(scopeMetadata.getScopeName());
            String beanName = name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry);
            AnnotationConfigUtils.processCommonDefinitionAnnotations(abd);
            int var10;
            int var11;
            if (qualifiers != null) {
                Class[] var9 = qualifiers;
                var10 = qualifiers.length;

                for(var11 = 0; var11 < var10; ++var11) {
                    Class<? extends Annotation> qualifier = var9[var11];
                    if (Primary.class == qualifier) {
                        abd.setPrimary(true);
                    } else if (Lazy.class == qualifier) {
                        abd.setLazyInit(true);
                    } else {
                        abd.addQualifier(new AutowireCandidateQualifier(qualifier));
                    }
                }
            }

            if (customizers != null) {
                BeanDefinitionCustomizer[] var13 = customizers;
                var10 = customizers.length;

                for(var11 = 0; var11 < var10; ++var11) {
                    BeanDefinitionCustomizer customizer = var13[var11];
                    customizer.customize(abd);
                }
            }

            BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName);
            definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
            BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry);
        }
    }

 

AnnotationConfigApplicationContext에서 넘어온 클래스 목록이 doRegister에 의해 등록된다. 

코드를 보면서 내려오다 보면 Primary, Lazy가 보인다. Primary는 우선순위를 설정하는 것이고 Lazy는 애플리케이션 시작 때 생성이 아닌 필요시에 생성되기때문에 따로 설정을 해준다.

그 외에는 Qualifier 어노테이션 같은 것이 처리된다. AutowireCandidateQualifier 객체로 래핑하여 해당 어노테이션을 추가하고 이는 빈이 자동 주입될 때 어떤 조건으로 주입할지 결정하는 데 사용한다.

 

그리고 마지막줄 3줄을 보면 AnnotatedGenericBeanDefinition,ScopeMetadata,BeanDefinitionHolder가 있다.

무슨 역할을 하는지 보자

 

AnnotatedGenericBeanDefinition BeanDefinition의 구현체, Bean의 속성, 생성자 등의 정보를 갖는다.

ScopeMetadata는 Bean Scope의 정보이다.

BeanDefinitionHolder BeanDefinition와 name, alias 정보를 추가적으로 갖는다.

 

위 동작을 수행 후 BeanDefinitionReaderUtils의 registerBeanDefinition에 BeanDefinitionHolder와 registry를 인자로 메서드를 호출하면 Bean이 등록된다.

public static void registerBeanDefinition(BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) throws BeanDefinitionStoreException {
    String beanName = definitionHolder.getBeanName();
    registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
    String[] aliases = definitionHolder.getAliases();
    if (aliases != null) {
        String[] var4 = aliases;
        int var5 = aliases.length;

        for(int var6 = 0; var6 < var5; ++var6) {
            String alias = var4[var6];
            registry.registerAlias(beanName, alias);
        }
    }

}

 

메서드를 호출하면 넘어온 registry에 의해 등록된다. registry는 어떤 값인지 알아보자 

 

private final BeanDefinitionRegistry registry;

public AnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry) {
    this(registry, getOrCreateEnvironment(registry));
}

우선 BeanDefinitionReaderUtils의 registerBeanDefinition 호출 시에 넘어가는 registry에 어떤값이 들어가는지 보자

AnnotatedBeanDefinitionReader를 생성 할때 주입 받는다. 이건 아마 AnnotationConfigApplicationContext에서 생성자로 생성을 할때 registry를 넘겨주는듯 하다.

 

AnnotationConfigApplicationContext를 보면 생성자에서 자기 자신을 registry로 넣어서 AnnotatedBeanDefinitionReader를 생성한다. 

private final AnnotatedBeanDefinitionReader reader;


public AnnotationConfigApplicationContext() {
        StartupStep createAnnotatedBeanDefReader = this.getApplicationStartup().start("spring.context.annotated-bean-reader.create");
        this.reader = new AnnotatedBeanDefinitionReader(this);
        createAnnotatedBeanDefReader.end();
        this.scanner = new ClassPathBeanDefinitionScanner(this);
}

AnnotationConfigApplicationContext 생성자를 보면 AnnotatedBeanDefinitionReader 생성에 자기 자신(AnnotationConfigApplicationContext)을 인자로 한다.

 

이건 AnnotationConfigApplicationContext가 BeanDefinitionRegistry(GenericApplicationContext) 의구현체이기도 하기 때문이다. 

public class AnnotationConfigApplicationContext extends GenericApplicationContext implements AnnotationConfigRegistry {
.
.
.
}

 

그럼 마지막으로 GenericApplicationContext에 registerBeanDefinition로직을 보자 

public class GenericApplicationContext extends AbstractApplicationContext implements BeanDefinitionRegistry {
    private final DefaultListableBeanFactory beanFactory;
    
    public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) throws BeanDefinitionStoreException {
        this.beanFactory.registerBeanDefinition(beanName, beanDefinition);
    }
    public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) throws BeanDefinitionStoreException {
        Assert.hasText(beanName, "Bean name must not be empty");
        Assert.notNull(beanDefinition, "BeanDefinition must not be null");
        if (beanDefinition instanceof AbstractBeanDefinition abd) {
            try {
                abd.validate();
            } catch (BeanDefinitionValidationException var8) {
                BeanDefinitionValidationException ex = var8;
                throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName, "Validation of bean definition failed", ex);
            }
        }

        BeanDefinition existingDefinition = (BeanDefinition)this.beanDefinitionMap.get(beanName);
        if (existingDefinition != null) {
            if (!this.isBeanDefinitionOverridable(beanName)) {
                throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition);
            }

            if (existingDefinition.getRole() < beanDefinition.getRole()) {
                if (this.logger.isInfoEnabled()) {
                    this.logger.info("Overriding user-defined bean definition for bean '" + beanName + "' with a framework-generated bean definition: replacing [" + existingDefinition + "] with [" + beanDefinition + "]");
                }
            } else if (!beanDefinition.equals(existingDefinition)) {
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Overriding bean definition for bean '" + beanName + "' with a different definition: replacing [" + existingDefinition + "] with [" + beanDefinition + "]");
                }
            } else if (this.logger.isTraceEnabled()) {
                this.logger.trace("Overriding bean definition for bean '" + beanName + "' with an equivalent definition: replacing [" + existingDefinition + "] with [" + beanDefinition + "]");
            }

            this.beanDefinitionMap.put(beanName, beanDefinition);
        } else {
            if (this.isAlias(beanName)) {
                String aliasedName = this.canonicalName(beanName);
                if (!this.isBeanDefinitionOverridable(aliasedName)) {
                    if (this.containsBeanDefinition(aliasedName)) {
                        throw new BeanDefinitionOverrideException(beanName, beanDefinition, this.getBeanDefinition(aliasedName));
                    }

                    throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName, "Cannot register bean definition for bean '" + beanName + "' since there is already an alias for bean '" + aliasedName + "' bound.");
                }

                this.removeAlias(beanName);
            }

            if (this.hasBeanCreationStarted()) {
                synchronized(this.beanDefinitionMap) {
                    this.beanDefinitionMap.put(beanName, beanDefinition);
                    List<String> updatedDefinitions = new ArrayList(this.beanDefinitionNames.size() + 1);
                    updatedDefinitions.addAll(this.beanDefinitionNames);
                    updatedDefinitions.add(beanName);
                    this.beanDefinitionNames = updatedDefinitions;
                    this.removeManualSingletonName(beanName);
                }
            } else {
                this.beanDefinitionMap.put(beanName, beanDefinition);
                this.beanDefinitionNames.add(beanName);
                this.removeManualSingletonName(beanName);
            }

            this.frozenBeanDefinitionNames = null;
        }

        if (existingDefinition == null && !this.containsSingleton(beanName)) {
            if (this.isConfigurationFrozen()) {
                this.clearByTypeCache();
            }
        } else {
            this.resetBeanDefinition(beanName);
        }

    }
    
  }

 

beanDefinitionMap에 beanName, beanDefinition을 저장한다.

 

이제 Bean 등록까진 되었으니 아마 다음은 Bean이 실제로 생성되는 부분을 뜯어보지 않을까싶다.

728x90
728x90

Mysql architecture 에 대해서 한번 알아보자

Mysql은 머리 역할을 하는 Mysql Engine과 손발 역할을 하는 Storage Engine으로 구분 할 수 있다. 

 

이번 포스팅에서는  Mysql server에서 제공되는 Mysql Engine과 Storage Engine을 알아보자. 

Mysql Server는 다른 DBMS에 비해 구조가 독특하다. 우리가 체감하기는 쉽지 않지만 이러한 구조 때문에 다른 DBMS에서는 가질 수 없는 엄청난 혜택을 누릴수도 있고, 반대로 다른 DBMS에서는 전혀 문제가 되지 않는 것이 문제가 될수도 있다.

 

아래 사진은 Mysql의 전체 구조이다. 이제 하나씩 알아보자

Real Mysql 8.0

 

Handler API(Connection Handler)

  • Mysql Engine의 쿼리 실행기에서 데이터를 쓰거나 읽어야 할 때는 각 스토리지 엔진에 쓰기 또는 읽기를 요청하는데, 이 요청을 Handler 요청이라고 하고 여기서 사용하는 API이기도 하고 Mysql Engine이 Storage Engine을 조정하기 위해 사용 하는 것이다. Handler API라고 한다.
  • InnoDB 스토리지 엔진 또한 이 Handler API 를 이용해 Mysql Engine과 데이터를 주고 받기도 한다. 

위 사진은 핸들러 API를 통해 얼마나 많은 데이터 작업이 있었는지를 표시한다.

Mysql Engine

  • Mysql engine은 요청된 SQL 문장을 분석하거나 최적화하는 등 DBMS의 두뇌 역할을 한다.
  • Mysql Engine은 Handler API 통해 접속된 클라이언트로부터의 접속 및 쿼리 요청을 처리하는 Connection Handler,SQL Interface, SQL parser , 전처리기, 쿼리에 최적화된 실행을 위한 옵티마이저가 중심을 이룬다.

쿼리 요청시 처리하는 세부 실행 구조에 대해서 알아보자

SQL Interface

  • 사용자가 입력하는 SQL 쿼리를 MySQL로 전달하는 계층입니다. 이 인터페이스는 MySQL이 SQL 쿼리를 받아들이는 첫 번째 지점이다.

SQL Parser

  • 쿼리 파서는 사용자 요청이 들어온 쿼리 문장을 토큰으로 분리해 트리 형태의 구조로 만들어내는 작업이다.
  •  위 과정에서 SQL 쿼리를 구문 분석(Parsing)하여 쿼리의 구조가 올바른지 확인하고, 이를 MySQL이 처리할 수 있는 구조로 변환한다.
  • 만약 오류가 존재한다면 사용자에게 오류메시지를 전달한다.

전처리기

  • 파서 과정에서 만들어진 파서 트리를 기반으로 쿼리 문장에 구조적인 문제점이 있는지 확인한다. (위 쿼리 파서는 문법에 대한 검증이고 이 과정은 구조에 대한 검증이다.)
  • 내장 팜수와 같은 개체를 매핑해 해당 객체의 존재 여부와 객체의 접근 권한 들을 확인하는 과정을 전처리기에서 수행한다.
  • 권한상 사용이 불가한 개체는 전처리기에서 걸러진다.

8.0에서는 제거된 MYSQL 서버의 캐시

  • 캐시와 버퍼 (Cache and Buffer)는 실행된 쿼리나 데이터 페이지를 메모리에 캐싱하여 성능을 최적화한다. MySQL은 결과를 저장해 재사용할 수 있도록 쿼리 캐시 또는 버퍼를 사용한다.

하지만 Mysql 8.0부터는 캐시 기능이 완전히 제거되고 시스템 변수도 모두 제거되었다. 가져다 주는 이점보다 버그가 더 많았기 때문이다.

 

SQL Optimizer 

  • 사용자의 요청으로 들어온 쿼리를 가장 효율적으로 실행할 수 있도록 최적화한다. 옵티마이저는 여러 가지 실행 계획을 비교하고, 가장 빠르게 쿼리를 실행할 방법과 저렴한 비용으로 처리할지 결정한다. 
  • SQL 옵티마이저: 같은 의미로, SQL 쿼리를 최적화하는 데 사용되는 구성 요소이다.

Storage Engine

  • Mysql Engine에서 SQL 문장을 분석하거나 최적화하는 등 처리를 수행하면 Storage Engine은 실제 데이터를 디스크 스토리지에 저장하거나 디스크 스토리지로부터 데이터를 읽어오는 역할을 담당한다. Mysql 서버에서 Mysql Engine은 하나지만 Storage Engine은 여러 개를 동시에 사용할 수 있다.
  • 스토리지 엔진에는 대표적으로 InnoDB storage Engine 이 있다. 이 부분은 다음 포스팅에서 조금 더 상세하게 정리를 할 예정이다.
728x90
728x90

모놀리식 구조에서 점점 서비스가 커지고 트래픽이 많아지게 되면 이 강결합된 구조가 문제를 일으키는 경우가 발생한다.

이벤트 기반 구조로 코드를 구성 하기 전에 트랜잭션 강결합과 약결합에 대해서 알아보자

 

트랜잭션 강결합(Strong Coupling)과 약결합(Loose Coupling)은 시스템이나 서비스의 의존성 수준을 나타낸다. 이 개념은 MSA, 분산 시스템, 또는 데이터베이스 트랜잭션에서 트랜잭션 경계와 서비스 간의 상호작용 방식에 따라 차이점이 있다.

 

우선 강결합 트랜잭션에 대해서 알아보자

 

📍 강결합 트랜잭션 (Strong Coupling Transaction)

강결합 트랜잭션은 두 개 이상의 컴포넌트나 서비스가 밀접하게 연결되어 있어서 한 컴포넌트, 서비스의 변경이 다른 컴포넌트, 서비스에 영향을 미치는 경우를 의미한다. 이 경우 트랜잭션 일관성을 보장하기 위해 각 컴포넌트가 같은 트랜잭션 경계 내에서 실행된다.

강결합 트랜잭션 (Strong Coupling Transaction)일반적으로 분산 트랜잭션(Distributed Transaction)이나 2PC (Two-Phase Commit)와 같이 동기적 트랜잭션처리 방식에서 발생한다.

 

강결합 트랜잭션(Strong Coupling Transaction) 의 특징으로는 일관성 보장이 쉽고 ACID 속성을 유지하기가 쉽다. 그리고 동기적으로 수행되기 때문에 하나의 트랜잭션이 완료될 때까지 모든 참여 트랜잭션이 block 상태가 된다.

 

일관성 보장이 쉬운 이유는 Transation 경계가 서로 공유되기 때문이다. 그리고 ACID 속성이 유지가 쉬운 이유는 하나의 트랜잭션 단위 내에서 처리가 되기 때문에 속성 자체를 유지하기가 수월하다.

 

우선 강결합 트랜잭션(Strong Coupling Transaction)의 장점을 보자

우선 가장 큰 장점은 데이터의 일관성이 보장된다. 그리고 모든 작업이 하나의 Transaction 내에서 이루어지기 때문에 데이터 손실이나 충돌이 방지된다. 예외 상황 발생 시 롤백이 가능해서 위에서 말한 특징 ACID 유지 중 하나인 원자성을 유지할 수  있다.

 

하지만 단점도 꽤 있는편이다.

Transaction 간의 변경이 발생하면 다른 Transaction들도 영향을 받고 시스템이 커지면 커질수록 이 문제가 크게 작용한다. 이러한 문제 때문에 확장성이 용이하지 못하다. 그리고 이 외에도 하나의 트랜잭션이 많은 작업을 하게 되면 네트워크 지연이나 lock이 발생하면서 퍼포먼스가 떨어지는 건 시간문제이다.

 

하지만 이런 강결합 트랜잭션(Strong Coupling Transaction)을 사용하는 이유는 ACID를 보장하고 데이터 일관성이 매우 중요한 서비스는 어쩔 수 없이 이러한 방식을 채택해야 한다.

 

대표적으로 은행에서 이체서비스를 예시로 들 수 있다. 예를 들어 하나의 계좌에서 이체하면 잔액을 감소시키는  Transaction이 있고 이체가 완료했다면 수취 계좌의 잔액을 증가시키는 Transaction이 있다고 가정을 해보자

 

1. A 계좌에서 100만 원이 차감되는 트랜잭션이 먼저 실행되고 성공.

2. B 계좌에 100만 원을 추가하는 트랜잭션이 실행 중 네트워크 오류 또는 데이터베이스 충돌로 인해 실패.

 

이 경우, A 계좌의 금액은 차감되었지만, B 계좌에는 금액이 추가되지 않아서 두 계좌 간의 데이터 불일치(Inconsistent State)가 발생한다. 결과적으로 A 계좌의 고객은 돈이 사라졌다고 생각할 수 있고, B 계좌의 고객은 돈이 들어오지 않았다고 주장할 수 있다. 그러므로 이런 동작들은 오히려 강결합으로 묶는 것이 더 안전하지 않을까?

그 외에도 모놀리틱 시스템에서 하나의 DB를 여러 서비스가 직접 연결되어 작업을 수행할 때는 이런 강결합 구조가 조금 더 적합하다.

 

이제 강결합 트랜잭션(Strong Coupling Transaction)을 어느 정도 알아봤으니  약결합 트랜잭션 (Loose Coupling Transaction)에 대해서 한번 알아보자 

 

📍약결합 트랜잭션 (Loose Coupling Transaction)

 

약결합 트랜잭션 (Loose Coupling Transaction)서로 다른 서비스나 트랜잭션이 독립적으로 동작하며, 직접적인 의존성이 낮은 경우를 의미한다. 약결합은 서비스 간의 독립성을 보장하기 위해 대표적으로 비동기 방식이나 이벤트 기반 통신을 사용한다. 약결합 트랜잭션에서는 트랜잭션 일관성을 전체 시스템 수준에서 보장하지 않고, 서비스 간의 이벤트 전파를 통해 궁극적인 일관성(Eventual Consistency)을 달성한다

 

약결합 트랜잭션 (Loose Coupling Transaction)은 각 서비스나 트랜잭션은 독립적으로 동작하고, 트랜잭션 경계를 공유하지 않는다.

하지만 전체 시스템의 일관성은 즉시 보장 되지 않지만 점진적으로 일관성을 보장한다. 그리고 주로 비동기적 시스템 또는 이벤트 기반 통신 방식을 사용하여 결합도를 낮춘다.

 

약결합 트랜잭션 (Loose Coupling Transaction)은 확장성이 뛰어나고 서비스 간의 의존성이 낮은 게 장점이다. 그리고 비동기 방식으로 구현 가능 하기 때문에 시스템 성능을 높일 수 있다.

 

하지만 데이터 일관성이 보장이 어렵고 데이터 정합성을 맞추는 것이 어렵다. 위에서 말한 것처럼 일관성이 즉시 보장은 되지 않지만 점진적으로 보장한다. 이게 무슨 말이냐 하면 상태 추적과 보상 트랜잭션이 필요하다는 말이다. 이러한 동작이 구현되어 있어야 문제가 발생했을 때도 추적, 해결이 가능하다.

그리고 이러한 동작들이 실행되기 위해서는 복잡한 상태관리와 예외처리가 필요하다. 상태관리와 예외처리가 제대로 되어있지 않다면 오류를 역추적하기가 매우 어려워진다.

 

 

약결합 트랜잭션 (Loose CouplingTransaction)을 사용하는 환경으로는 MSA 나 Saga Pattern 환경이 있다.

최근 IT 회사들이 많이 채택하는 MSA 가 대표적인 예시이다. MSA는 서비스 간 의존성을 줄이기 위해 각 서비스가 독립적인 Transaction을 가지며, Kafka, RabbitMQ, AWS SNS/SQS 등을 통해 서비스 간의 이벤트를 비동기적으로 주고받아 트랜잭션을 처리한다.

 

그리고 Saga Pattern은 분산 환경에서 서비스 간 트랜잭션을 관리하기 위해 보상 트랜잭션을 사용하여 일관성을 달성하고 한 서비스의 상태가 변경되면 다른 서비스에 이벤트를 전파하여 연쇄적으로 상태를 변경하고, 예외가 발생했을 경우 보상 트랜잭션을 통해 롤백하는 방식이다.

 


마무리하며

 

다들 요즘 이벤트 기반에 대해서 관심이 많은 것 같아서 한번 정리를 해봤다. 나도 MSA 환경에서 개발을 하지만 이런 건 본인이 정리하지 않으면 누가 구축해 둔 구조를 사용만 할 줄 아는 사람이 되고 활용을 잘하지 못하게 된다. 어떤 사람은 이런 원초적인 개념을 굳이 이렇게 정리할 필요 있냐고 하지만 난 이렇게 밑바닥부터 다져가는 게 뭔가 심적으로도 편하고 개발했을 때 퀄리티도 잘 나오는 것 같다. 만약에 이런 개념이 얕은 상태로 개발을 하게 된다면 MSA로 구축해 두고 모놀리식처럼 사용하는 끔찍한 상황이 발생할 수도 있고 MSA로 전환한 의미가 없어지는 순간이 올 것이다.

그런 순간이 오지 않으려면 화려하게 개발을 하기보단 밑바닥을 점진적으로 다져가면서 성장하는 게 좋다고 생각한다.

 

 

728x90
728x90

사이드 프로젝트를 진행하면서 두 개념에 대해 명확하게 구분되어 동작 해야 하는 로직이 있어서 개념 정리 겸 한번 적어본다.

 

1. 인증(Authentication)이란?

인증은 사용자가 누구인지 확인하는 과정입니다. 예를 들어, 웹 애플리케이션에 로그인할 때 아이디와 비밀번호를 입력하는 것이 바로 인증의 대표적인 예시이다. 이 과정에서 시스템은 사용자가 주장하는 정체성(아이디)이 실제로 맞는지 검증한다.

 

쉽게 말해 인증(Authentication)은 공항에서 입국 심사정도로 보면 된다. 신원확인(?)정도로 이해하면 편할 것이다.

 

인증도 방법이 여러가지가 있다.( 아래 방법 외에도 여러가지가 있지만 대표적인것들만 적어본다.)

  1. 아이디와 비밀번호: 가장 일반적인 방식으로, 아이디와 비밀번호를 통해 사용자를 식별.
  2. 2단계 인증 (2FA): 비밀번호 외에 SMS 코드, 이메일, OTP 등의 추가 인증을 요구.
  3. 생체 인식: 지문, 홍채, 안면 인식 등을 통해 사용자를 식별.
  4. OAuth: 구글, 페이스북 등 외부 인증 서비스 제공자를 통한 인증.

그 다음은 인가(Authorization)이다.

 

2. 인가(Authorization)란?

인가는 사용자가 어떤 자원에 접근할 수 있는지를 결정하는 과정이다. 인증을 통해 사용자가 누구인지 확인했다면, 인가를 통해 해당 사용자가 무엇을 할 수 있는지를 판단한다. 

 

이것도 쉽게 해석하자면 비행기 탑승 수속정도로 이해하면 된다. 이 사람이 비행기에 탑승이 가능한지 아니면 다른 비행기로 가거나 탑승이 불가한지 확인 하는 과정이다.

 

인가(Authorization) 의 대표적인 방식이다.

 

  1. Role-Based Access Control (RBAC)
    • 사용자의 역할(Role)을 기준으로 접근 권한을 설정하는 방식.
    • 예: ROLE_ADMIN, ROLE_USER
  2. Attribute-Based Access Control (ABAC)
    • 시간, 위치, 리소스 타입 등의 속성을 기반으로 접근을 제어.
    • 예: 특정 시간대에만 접근 허용, 특정 IP 주소에서만 접근 가능.
  3. Policy-Based Access Control
    • 사전 정의된 정책(Policy)을 기반으로 권한을 제어.
    • 예: 조직 내의 규칙 또는 규정에 따른 접근 허용/제한.

보면 명확하게 구분이 된다. 인증은 그 주체가 맞는지 검증을 하는 것이고 인가는 특정 주체의 접근성에 대해서 검증을 하는 과정이다.

 

 

정리

인증(Authentication)과 인가(Authorization)는 웹 애플리케이션의 보안에서 매우 중요한 두 가지 개념이다. 인증은 사용자의 신원을 확인하는 과정이고, 인가는 사용자의 권한을 확인하여 자원 접근을 제어하는 과정이다. 이 두 요소를 올바르게 설계하고 구현함으로써 애플리케이션의 보안을 한층 강화할 수 있다. 

본인이 인증,인가 관련 서비스를 운영하거나 구현 하는게 아니더라도 꼭 알고 있어야한다.

 

728x90