주요글: 도커 시작하기
반응형

1년 전 작년 이맘때 새 시스템에 대한 개발을 시작했다. 새 시스템과 기존 시스템 간 데이터 연동이 필요했는데 몇 가지 이유(선택한 이유는 https://youtu.be/guVFK09yjXw 영상 참고)로 CDC(Change Data Capture)를 연동 기술로 선택했다.

MariaDB에 대한 CDC 모듈을 조사했는데 검색 결과로 많이 나온 게 Debezium이었다. 흥미롭긴 했는데 원하는 것을 하기에는 알맞지 않았다. 필요한 것은 다음과 같았다.

  • 여러 테이블이 변경되어도 한 개 이벤트 발생
    • 예를 들어 한 트랜잭션에서 A 주문의 주문 개요, 주문 상세, 계약 상세 등의 테이블이 변경되었을 때 각 테이블의 변경 내역을 이벤트로 만들지 않고 A 주문의 요약 정보를 한 개 이벤트로 생성해서 기존 시스템에 전달 필요
  • 한 모델의 변경 시 영향받는 다른 모델 데이터도 전파
    • 예를 들어 B회원의 결제 수단 정보가 변경되면 B회원과 관련된 모든 주문의 요약 정보를 이벤트로 생성해서 기존 시스템에 전달(주문 요약 정보에 결제 수단 정보가 포함되어 있어 전파 필요)
  • 변경 데이터를 칼럼명으로 접근하고 싶음
    • 변경 데이터를 칼럼 인덱스가 아닌 칼럼명으로 접근하고 싶음

이런 걸 하기에 Debezium은 적합하지 않아 요구에 맞는 CDC 모듈을 직접 만들기로 했다.

CDC 모듈 개발

다행히 MariaDB의 바이너리 로그를 읽을 수 있는 mysql-binlog-connector-java가 있어 이걸 활용해서 모듈을 만들었다(MySQL과 MariaDB가 프로토콜이 같아서 다행이었다). 다음 그림은 구현하는 과정에서 도출된 설계 결과물이다.

MariadbCdc에는 CDC 시작/중지 처리와 변경 데이터를 MariadbCdcListener에 전달하는 기능을 넣었다. 변경 데이터의 칼럼명 조회를 위해 ColumnNameGetter 인터페이스를 추가했고, (위 그림엔 표시하지 않았지만) 테이블 스키마 변경 시 칼럼명을 다시 조회할 수 있도록 스키마 변경 감지 기능을 넣었다.

MariadbCdcListener는 onDataChanged() 메서드로 변경 데이터를 RowChangedData에 담아서 전달받도록 했다. RowChangedData 클래스는 변경 데이터와 관련된 변경 타입, 변경된 테이블 이름, 변경 데이터, (UPDATE면) 변경 전 데이터를 갖도록 했다. DataRow는 데이터를 표현하며 칼럼명을 이용한 조회 기능과 칼럼 인덱스를 이용한 조회 기능을 제공하도록 구현했다.

한 트랜잭션에서 변경된 데이터를 모아서 한 번에 처리할 수 있도록 하기 위해 onXid() 메서드도 추가했다. 트랜잭션 커밋 로그를 읽을 때 이 메서드를 호출하도록 했다.

CDC 모듈 초기 버전을 만들고 https://github.com/madvirus/mariadb-cdc 리포지토리에 올리고 메이븐 의존 설정에 쉽게 추가할 수 있도록 jitpack.io를 사용했다.

개밥 먹기 1년

개발 환경에 적용한 뒤 1년이 지났다. 그 사이에 새 시스템을 오픈해서 운영 환경에도 적용했다. 이 과정에서 몇 가지 놓친 점을 발견했고 점진적으로 보완해 나갔다. 보완한 것 중에서는 다음이 기억에 남는다.

  • 동시에 두 프로세스를 실행하면 서로 연결이 끊기는 증상: 같은 슬레이브 ID를 사용하면서 발생한 것으로 슬레이브ID를 지정할 수 있는 기능 추가하고 각 CDC 모듈이 겹치지 않는 슬레이브ID를 사용하도록 처리
  • 한 테이블에서 많은 행이 변경될 때 감지가 누락되는 증상: 연속된 변경 ROWS 이벤트 발생 시 두 번째 이벤트부터 테이블 이름 처리가 잘못되어 발생한 것으로 테이블 이름 매핑 처리 버그 수정
  • 칼럼 이름을 제대로 가져오지 못하는 버그(다른 테이블에 참조키를 제공하는 테이블 변경 시 발생): TABLE_MAP 이벤트가 연속될 때 발생한 버그 수정

이 중 두 번째는 운영 환경에서 알게 되었다. 관련 데이터가 일정 개수 이상으로 증가하면서 발생하기 시작했다. 처음에는 원인을 몰라 주기적으로 손으로 보정하는 짓을 좀 했다.

세 번째는 최근에 다른 DB에 CDC를 적용하면서 발견했다. 문제를 해결하는 과정에서 리플리케이션 프로토콜에 대해 조금 더 알 수 있었다.

두 번째 개밥 먹기 중

보완을 하면서 동시에 실험 목적의 바이너리 로그 리더 구현도 병행했다. mysql-binlog-connector-java가 충분히 좋았지만 리플리케이션 프로토콜에 대한 호기심이 발동해 바이너리 로그를 직접 구현해 보고 싶은 마음이 생겼기 때문이다. 주로 주중 저녁이나 주말에 구현했는데 토요일과 일요일에 이틀 동안 꼬박 이것만 했던 날도 있었다.

MariaDB/MySQL 사이트에서 제공하는 프로토콜 문서를 주로 참조했고 프로토콜 문서만으로 감이 안 올 때는 mysql-binlog-connector-java 코드를 뒤져가면서 점진적으로 구현했고 최근에 필요한 최소 기능 개발을 완료했다. 모듈을 설계할 때 mysql-binlog-connector-java의 타입 노출을 최소화하고 MariadbCdc, MariadbCdcListener, DataRow 등으로 한 번 감싸서 구현했기에 실험용 버전을 사용하도록 변경하는 것도 간단한 설정으로 진행할 수 있었다.

마침 CDC를 다른 곳에 적용해야 할 일이 생겨 새로 구현한 바이너리 로그 리더를 사용해보고 있다. 이 과정에서 또 다른 보완점을 찾을 수 있었는데 그건 바로 enum 타입에 대한 처리였다. enum 타입 처리를 위한 코드를 보완하고 있는데 MySQL과 MariaDB에서는 enum 타입을 최대한 쓰면 안 되겠다는 생각을 갖게 되었다. 내가 결정에 영향을 줄 수 있는 한 최대한 못 쓰게 할 거다!

다음 목표는 직접 구현한 바이너리 로그 리더를 별도 모듈로 분리하고 SSL이나 GTID 등에 대한 지원을 추가하는 것이다. 생각나면 하고 틈나면 하고 그런지라 언제 될지 모르지만 언젠가는 다음 목표를 달성하고 싶다.

반응형

https://youtu.be/bBFi48Azvks

마리아DB 10.3, 10.4, 10.5에 있는 파티션 테이블과 관련된 치명적인 버그를 살펴봅니다.

반응형

 

https://youtu.be/xqqjCTt_l3E

 

반응형

동영상 주소: https://youtu.be/ZNDDy77WInY

 

반응형

https://youtu.be/fnH_SR3n9Ew

 

반응형

youtu.be/gd9aeUywGcM

 

반응형

프로그래밍 왕초식: if 조건 역으로 바꾸기

youtu.be/z4qE_IfSrD4

 

반응형

CodeMetrics 플러그인 소개: 복잡도 점수로 자극 받기

youtu.be/8NtKoANOezI

 

반응형

어제 이어 오늘도 사소하게 코드를 정리했다. 정리하기 전에 코드 형태는 다음과 같다.

if(obj.getData().equals("01") || obj.getData().equals("02") || obj.getData().equals("03")) {
    ....
}

문자열이 여러 값 중 하나인지 비교하는 코드이다. 이런 형태의 코드가 곳곳에 있어 어떻게 변경할까 고민하다가 SQL의 in 구문이 떠올라 다음과 같이 변경했다.

if ( Cond.str(obj.getData()).in("01", "02", "03") ) {
    ...
}

코틀린이었다면 확장 함수를 사용해서 더 간결하게 표현할 수 있었을텐데 하는 아쉬움이 살짝 들었지만 컴파일 속도를 생각하면 이것도 괜찮다.

다음처럼 여러 값이 모두 아닌지 비교하는 코드도 빈번하게 출현한다.

if( !obj.getData().contentEquals("01") && !obj.getData().contentEquals("02") ) {
    ....
}

이를 위해 notIn() 메서드도 추가했다.

if ( Cond.str(obj.getData()).notIn("01", "02") ) {
    ...
}

다음은 Str 클래스의 구현 코드다.

public class Cond {
    public static StrCond str(String s) {
        return new StrCond(s);
    }

    public static class StrCond {
        private String value;

        public StrCond(String value) {
            this.value = value;
        }

        public boolean in(String ... values) {
            for (String v : values) {
                if (v.equals(value)) return true;
            }
            return false;
        }

        public boolean notIn(String ... values) {
            for (String v : values) {
                if (v.equals(value)) return false;
            }
            return true;
        }
    }
}

 

  1. 김광수 2021.04.13 11:30

    구현코드를 변경해 보왔습니다. 그런데 Cond<T> 가 별로 의미 없어 보이긴 합니다 😅
    <code>
    public class CondTest {
    @Test
    void testStr() {
    assertThat(new Cond.Str("a").in("a", "b", "c")).isTrue();
    assertThat(new Cond.Str("d").in("a", "b", "c")).isFalse();
    assertThat(new Cond.Str("d").notIn("a", "b", "c")).isTrue();
    assertThat(new Cond.Str("a").notIn("a", "b", "c")).isFalse();
    }

    interface Cond<T> {
    boolean in(T... values);

    default boolean notIn(T... value) {
    return !in(value);
    }

    class Str implements Cond<String> {
    private final String value;

    public Str(String value) {
    this.value = value;
    }

    @Override
    public boolean in(String... values) {
    for (String v : values) {
    if (v.equals(value))
    return true;
    }
    return false;
    }
    }
    class Int implements Cond<Integer> {
    ...
    }
    }
    }
    </code>

반응형

곧 전달 받을 코드를 이리 저리 훑어 보다가 아래 형태 코드가 눈에 띄었다.

// someData.getIdList()는 String 타입으로 "id1|id2|id3"과 같은 형식
List<String> idList = Arrays.stream(someData.getIdList().split("[|]"))
        .collect(Collectors.toList());

split("[|]") 문자열로 검색해 보니 7 군데에서 완전 똑같은 형태의 코드를 사용하고 있다. 중복이 3번 이상 나고 있어서 이를 위한 보조 클래스 Splits을 만들었다.

public class Splits {
    public static List<String> splitByVbar(String str) {
        return Arrays.asList(str.split("[|]"));
    }
}

그리고 검색한 7 곳의 코드를 다음과 같이 바꿨다.

List<String> idList = Splits.splitByVbar(someData.getIdList().split("[|]"));

테스트 코드가 없어서 Splits.splitByVbar()를 사용하는 코드는 테스트를 할 수 없었다. 대신 기존의 분리 코드와 새 분리 코드가 같은 결과를 내는지 확인하는 테스트 코드를 작성했다.

@Test
void same() {
    assertThat(Splits.splitByVbar("1|2|3")).isEqualTo(oldSplitCode("1|2|3"));
    assertThat(Splits.splitByVbar("1|2|3|")).isEqualTo(oldSplitCode("1|2|3|"));
    assertThat(Splits.splitByVbar("|1|2|3|")).isEqualTo(oldSplitCode("|1|2|3"));
    assertThat(Splits.splitByVbar("1|2||3")).isEqualTo(oldSplitCode("1|2||3"));
}

private List<String> oldSplitCode(String str) {
    return Arrays.stream(str.split("[|]")).collect(Collectors.toList());
}

통과 됨을 확인하고 코드를 푸시했다.

반응형

아키텍트에게 기대하는 8가지 핵심 역량 영상: youtu.be/FbWHw6GBCrU

 

요약

  • 아키텍처 결정
  • 지속적인 아키텍처 분석
  • 최신 트렌드 유지
  • 결정 사항 준수 확인
  • 다양한 경험
  • 도메인 지식
  • 대인 관계 기술
  • 정치

 

반응형

1편: DB 트랜잭션의 원자성에 대해 조금 이해하는 시간을 가져봅니다. youtu.be/urpF7jwVNWs

 

2편: 동시성 문제(dirty read, dirty write, read skew, lost update, write skew)와 격리read committed, repeatable read, serializable 등)에 대해 이야기합니다. youtu.be/poyjLx-LOEU

 

반응형

여러 메서드에서 하나의 파라미터 타입을 사용하는 것에 대해 얘기해 봅니다.

 

반응형

좋은 코드를 만들 수 있는 간단한 방법 중 하나인 변수 아끼기에 대해 살펴봅니다.

 

반응형

프로그래밍 초식: WHAT? HOW?

 

+ Recent posts