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

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

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?

 

반응형

프로그래밍 초식 - 나누기 

 

반응형

얼마전 회사 동료분이 JpaRepository가 아닌 Repository를 상속하는 이유에 대해 물어본 적이 있는데 그때 나눈 내용을 요약하면 아래와 같다.

  1. 메서드가 많으면 단위 테스트에서 명령 모델 리포지토리의 가짜 구현을 만들기 어렵다.
  2. 불필요한 메서드가 있으면 잘못 사용할 수 있다.

전체 내용은 아래 유튜브 영상을 참고한다.

 

  1. lee 2020.12.29 18:23

    안녕하세요. 영상 잘 보았습니다.

    혹시 가짜구현 예제의 MemoryUserRepository에서 모든 메소드에 대해 실제 구현을 한다는 것인가요?
    단위테스트에서 가짜 구현 부분을 Test double로는 커버하지 못하는 것이라 실제 구현이 필요한지 궁금합니다!

    • madvirus 2020.12.30 00:12 신고

      네 가능하면 MemoryUserRepository는 모든 메서드에 대해 (기대하는 동작을 하도록) 구현을 한다는 겁니다.
      MemoryUserRepository도 테스트 더블 중 하나인 가짜 구현에 속하구요.

    • lee 2020.12.30 01:13

      혹 mock나 spy같은 것으로는 대처가 불가능 한 환경인가요? 아니면 가짜구현이 더 나은 장점이 있기에 선택하신 것 일까요?

    • madvirus 2020.12.30 22:24 신고

      개인적으로는 가짜 구현이 요소 간 연동 변화에 테스트가 깨질 가능성이 더 적은 것 같아요.

  2. madvirus 2020.12.30 22:23 신고

    개인적으로는 가짜 구현이 요소 간 연동 변화에 테스트가 깨질 가능성이 더 적은 것 같아요.

    • lee 2020.12.30 22:36

      아하 그렇군요.
      연동 부분에서 변화가 생기면 영향을 미치는 모든 곳을 찾아 mocking을 바꿔야 하니 놓치는 부분도 많이 생길 수 있다는 말씀으로 이해가 되었는데 경험이 적어 제대로 이해 했는지 잘 모르겠네요 ㅎㅎ

      mocking을 할때와 가짜구현을 할 때를 나누는 기준을 좀 더 조사해봐야 할 것 같습니다ㅜㅜ

      귀찮으셨을텐데 상세히 답변 기재해주셔서 감사합니다!

  3. 딸랑딸랑 2021.02.17 16:53

    잘 보고 갑니다...

  4. beginagain 2021.04.27 21:43

    다음카페 질문하기에 게시판선택이 되질않습니다...ㅜ
    게시판항목이 안나옵니다.

반응형

kafka 아는 척하기1 영상: (카프카 기본 구조, 토픽/파티션, 성능, 리플리카 등, youtu.be/0Ssx7jJJADI)

kafka 아는 척하기 1

 

kafka 아는 척하기 2 영상: 프로듀서 편 (youtu.be/geMtm17ofPY)

 

 

kafka 아는 척하기 3 영상: 컨슈머 편 (youtu.be/xqrIDHbGjOY)

 

 

 

반응형

MariaDB에서 grant usage ... with max_statement_time 명령어를 사용하면 사용자별로 최대 쿼리 실행 시간을 지정할 수 있다. 다음은 명령어 실행 예를 보여준다. 이때 시간은 초 단위다(MySQL도 동일한 유사한 기능이 있는데 시간 단위는 밀리초이다).

GRANT USAGE ON *.* TO batchuser@'%' WITH MAX_STATEMENT_TIME 60

사용자별로 지정한 최대 쿼리 실행 시간은 글로벌 시간에 우선한다.

반응형

CQRS(Command Query Responsibility Segregation) 아는 척하기1 영상: youtu.be/xf0kXMTFJm8

CQRS 아는 척하기 2 영상: youtu.be/H1IF3BUeFb8

 

반응형

요즘 MariaDB 바이너리 로그를 이용한 간단한 라이브러리를 만들고 있는데(mariadb-cdc 라이브러리) 다양한 버전의 MariaDB와 몇 가지 다른 설정으로 테스트할 필요가 있었다. 로컬에 설치한 MariaDB로는 다양한 테스트를 수행하는데 불편함이 있어 방법을 찾다가 Testcontainers가 있다는 것을 알게 되었다.

 

Testcontainers는 JUnit에서 테스트하는 동안 도커 컨테이너를 임시로 생성하고 제거해 주는 라이브러리다. Testcontainers를 사용하면 MariaDB, MongoDB, MySQL 등 다양한 DB에 대한 통합 테스트 환경을 쉽게 구성할 수 있다.

 

유튜브로 보기: youtu.be/eZbLAD2yUfE

의존 설정(JUnit5 기준)

JUnit에서 Testcontainers를 사용하려면 다음 의존 설정을 추가한다.

  • org.testcontainers:testcontainers:1.14.3
  • org.testcontainers:mariadb:1.14.3
  • org.testcontainers:junit-jupiter:1.14.3

그레이들 설정 예

dependencies {
    implementation("org.mariadb.jdbc:mariadb-java-client:2.7.0")
    implementation("ch.qos.logback:logback-classic:1.2.3")
    testImplementation(platform('org.junit:junit-bom:5.7.0'))
    testImplementation('org.junit.jupiter:junit-jupiter')
    testImplementation("org.testcontainers:testcontainers:1.14.3")
    testImplementation("org.testcontainers:mariadb:1.14.3")
    testImplementation("org.testcontainers:junit-jupiter:1.14.3")
}

test {
    useJUnitPlatform()
}

메이븐 설정 예

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.junit</groupId>
            <artifactId>junit-bom</artifactId>
            <version>5.7.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.mariadb.jdbc</groupId>
        <artifactId>mariadb-java-client</artifactId>
        <version>2.7.0</version>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.3</version>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>testcontainers</artifactId>
        <version>1.14.3</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>mariadb</artifactId>
        <version>1.14.3</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>1.14.3</version>
        <scope>test</scope>
    </dependency>
</dependencies>

JUnit에서 MariaDB 컨테이너 구동하기

의존을 추가했다면 간단하게 MariaDB 컨테이너를 구동하고 연동할 수 있다. 다음 세 가지만 하면 된다.

  • @Testcontainers 애노테이션 테스트 클래스에 적용
  • MariaDBContainer 타입 필드를 선언하고 객체 생성
  • MariaDBContainer 필드에 @Container 애노테이션 적용

아래 코드는 작성 예이다.

import org.testcontainers.containers.MariaDBContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@Testcontainers
public class MariadbTest {
    Logger logger = LoggerFactory.getLogger(getClass());

    @Container
    MariaDBContainer mariaDB = new MariaDBContainer();

    @Test
    void connect() {
        logger.info("host: {}", mariaDB.getHost());
        logger.info("port: {}", mariaDB.getMappedPort(3306));
        logger.info("username: {}", mariaDB.getUsername());
        logger.info("password: {}", mariaDB.getPassword());
        logger.info("jdbc url: {}", mariaDB.getJdbcUrl());
        try (Connection conn = DriverManager.getConnection(
                    mariaDB.getJdbcUrl(), 
                    mariaDB.getUsername(), 
                    mariaDB.getPassword())
        ) {
            logger.info("got connection");
            // 코드
        } catch (SQLException ex) {
            ex.printStackTrace();
        } catch (InterruptedException e) {
        }
    }
}

위 테스트 코드를 실행하면 테스트 메서드를 실행하기 전에 MariaDB 이미지를 이용해서 컨테이너를 시작한다. 컨테이너가 시작되면 MariaDBContainer가 제공하는 다양한 메서드를 이용해서 DB 연결에 필요한 정보를 구할 수 있다. 실제 위 테스트 코드를 실행하면 다음 로그 메시지가 출력된다(예제에서 사용하는 로그 포맷은 깃헙 코드를 참고한다).

23:13:30.742 [Test worker] INFO  docker[mariadb:10.3.6] - Creating container for image: mariadb:10.3.6
23:13:31.060 [Test worker] INFO  docker[mariadb:10.3.6] - Starting container with ID: 79fb2e93168247a2d9643256aba2f8fad39445be855d96e7fecb92d8fba9593e
23:13:31.656 [Test worker] INFO  docker[mariadb:10.3.6] - Container mariadb:10.3.6 is starting: 79fb2e93168247a2d9643256aba2f8fad39445be855d96e7fecb92d8fba9593e
23:13:31.692 [Test worker] INFO  docker[mariadb:10.3.6] - Waiting for database connection to become available at jdbc:mariadb://localhost:32769/test using query 'SELECT 1'
23:13:44.089 [Test worker] INFO  docker[mariadb:10.3.6] - Container is started (JDBC URL: jdbc:mariadb://localhost:32769/test)
23:13:44.090 [Test worker] INFO  docker[mariadb:10.3.6] - Container mariadb:10.3.6 started in PT17.215S
23:13:44.102 [Test worker] INFO  mariadb.MariadbTest - host: localhost
23:13:44.102 [Test worker] INFO  mariadb.MariadbTest - port: 32769
23:13:44.102 [Test worker] INFO  mariadb.MariadbTest - username: test
23:13:44.102 [Test worker] INFO  mariadb.MariadbTest - password: test
23:13:44.102 [Test worker] INFO  mariadb.MariadbTest - jdbc url: jdbc:mariadb://localhost:32769/test
23:13:44.120 [Test worker] INFO  mariadb.MariadbTest - got connection

출력한 결과를 보면 Testcontainers 1.14.3 기준으로 MariaDB 버전이 10.3.6이고 DB 사용자와 암호가 test인 것을 알 수 있다. JDBC URL을 보면 사용할 DB 이름도 test이다. 포트 번호는 32769인데 이는 컨테이너의 3306 포트에 매핑된 로컬 포트가 32769인 것을 의미한다. 실제 컨테이너가 구동될 때마다 이 포트는 바뀐다.

 

테스트 코드를 실행하는 중간에 docker ps 명령어를 실행하면 mariadb:10.3.6 이미지를 사용한 컨테이너가 생성된 것을 확인할 수 있다.

C:\work\any\testcontainer-sample>docker ps
CONTAINER ID        IMAGE                               COMMAND                  CREATED             STATUS              PORTS                     NAMES
c45c731d82ae        mariadb:10.3.6                      "docker-entrypoint.s…"   2 seconds ago       Up 1 second         0.0.0.0:32771->3306/tcp   modest_shannon
92d9b2fd807d        testcontainersofficial/ryuk:0.3.0   "/app"                   4 seconds ago       Up 2 seconds        0.0.0.0:32770->8080/tcp   testcontainers-ryuk-c2d44a10-9754-4133-830e-53c6147a1b22

@Container 애노테이션이 붙은 필드가 static이 아니면 각 테스트 메서드를 실행할 때마다 도커 컨테이너를 시작하고 종료한다. 테스트 클래스에 있는 테스트 메서드가 동일한 도커 컨테이너를 사용하게 하고 싶다면 @Container 필드를 static으로 선언하면 된다.

커스텀 설정

MariaDB 버전을 쉽게 바꿀 수도 있다. 생성자에 사용할 이미지만 입력하면 된다. 이 외에 DB 사용자와 암호를 포함해 몇 가지 설정을 쉽게 변경할 수 있다. 다음은 변경 예이다.

@Testcontainers
public class MariadbInitTest {
    
    @Container
    JdbcDatabaseContainer mariaDB = new MariaDBContainer("mariadb:10.5") // 이미지
            .withConfigurationOverride("conf.d.105") // DB 서버 추가 설정
            .withUsername("myuser") // DB 사용자
            .withPassword("mypassword") // 암호
            .withDatabaseName("mydb") // 사용할 데이터베이스
            .withInitScript("init.sql"); // 초기 실행 쿼리

withConfigurationOverride()가 아닌 다른 메서드는 (MariaDBContainer 클래스의 상위 클래스인) JdbcDatabaseContainer 클래스에 정의되어 있다. 그래서 필드 타입을 JdbcDatabaseContainer 클래스로 선언했다.

 

withInitScript()는 클래스패스에서 실행할 쿼리를 찾는다. 예제 코드는 src/test/resources 폴더에 위치한 init.sql 파일을 실행한다.

 

withConfigurationOverride() 메서드는 MariaDB 설정 파일을 변경할 때 사용한다. 이 메서드는 클래스패스에 위치한 폴더명을 인자로 받는다. MariaDBContainer는 이 폴더의 파일을 컨테이너의 "/etc/mysql/conf.d" 폴더에 복사한다(withConfigurationOverride() 메서드를 설정하지 않으면 Testcontainers MariaDB 모듈이 제공하는 my.cnf 파일을 사용한다).

참고 자료

반응형

서로 다른 MariaDB 간에 데이터를 복제할 일이 자꾸 생겨서 MariaDB 바이너리 로그 기반 CDC(Change Data Capture)를 활용하기로 했다. 이미 mysql-binlog-connector-java라는 훌륭한 라이브러리가 존재하는데 이를 직접 사용하면 코드가 다소 복잡해져서 이 라이브러리를 기반으로 한 mariadb-cdc라는 라이브러리를 만들었다.

MariaDB 설정

MariaDB 바이너리 로그 활성화

mariadb-cdc는 바이너리 로그를 사용하므로 binlog_format을 row로 설정한다.

binlog_format = row  
binlog_row_image = full  

binlog_row_image 값이 FULL이면 변경 전/후 모든 칼럼 값을 기록한다.

CDC를 위한 사용자 생성

CDC를 위한 사용자를 생성한다. 이 사용자에 REPLICATION SLAVE, REPLICATION CLIENT, SELECT 권한을 부여한다.

CREATE USER cdc@'%' IDENTIFIED BY 'password'  
GRANT REPLICATION SLAVE, REPLICATION CLIENT, SELECT ON *.* TO cdc@'%'  

메이븐 리포지토리 설정

mariadb-cdc를 사용하려면 아래 리포지토리 설정과 의존 설정을 추가한다.

<repositories>
    <repository>
        <id>jitpack.io</id>
        <url>https://jitpack.io</url>
    </repository>
</repositories>

<dependencies>
    <dependency>
        <groupId>com.github.madvirus</groupId>
        <artifactId>mariadb-cdc</artifactId>
        <version>0.11.0</version>
    </dependency>
</dependencies>

그레이들을 사용한다면 아래 설정을 추가한다.

repositories {
    mavenCentral()
    maven { url 'https://jitpack.io' }
}

dependencies {
    implementation 'com.github.madvirus:mariadb-cdc:0.11.0'
}

MariadbCdc 사용하기

기본 사용법

단계 1, MariadbCdcConfig 생성

MariadbCdcConfig config = new MariadbCdcConfig(
                "localhost", // MariaDB 호스트
                3306, // 포트
                "cdc", // CDC를 위한 사용자
                "password", // 암호
                "bin.pos"); // 바이너리 로그 위치 추적용 파일 경로

"bin.pos" 파일이 없으면 현재 바이너로 로그 위치부터 읽기 시작한다.
"bin.pos" 파일에 바이너로 로그 위치가 기록되어 있으면 해당 위치부터 읽기 시작한다.
MariadbCdc는 바이너로 로그를 읽으면 다음 위치를 "bin.pos" 파일에 기록한다.

단계 2, MariadbCdc 생성

JdbcColumnNamesGetter columnNamesGetter = new JdbcColumnNamesGetter(
            "localhost", // host
            3307, // port
            "cdc", // cdc user
            "password"); // password 

MariadbCdc cdc = new MariadbCdc(config, columnNamesGetter);

MariadbCdc는 테이블의 칼럼 이름을 구하기 위해 ColumnNamesGetter를 사용한다.

기본 제공하는 JdbcColumnNamesGetter는 다음 쿼리를 이용해서 칼럼 이름을 구한다:

select COLUMN_NAME, ORDINAL_POSITION, COLUMN_DEFAULT, IS_NULLABLE, DATA_TYPE 
from INFORMATION_SCHEMA.COLUMNS 
WHERE table_schema = '?' and TABLE_NAME = '?' 
order by ORDINAL_POSITION

단계 3, MariadbCdcListener 설정

cdc.setMariadbCdcListener(new MariadbCdcListener() {
    @Override
    public void started(BinlogPosition binlogPosition) {
        // CDC가 시작되면 호출됨
    }

    @Override
    public void startFailed(Exception e) {
        // CDC 시작에 실패하면 호출됨
    }

    @Override
    public void onDataChanged(List<RowChangedData> list) {
        // 변경된 데이터 목록(행 목록)을 받음
        list.forEach(data -> { // 변경된 각 행에 대해
            String database = data.getDatabase(); // 변경된 행의 DB 이름
            String table = data.getTable(); // 변경된 행의 테이블 이름
            DataRow dataRow = data.getDataRow(); // 변경된 데이터 행(변경 후 이미지)
            if (data.getType() == ChangeType.INSERT) { // 추가된 데이터면
                Long id = dataRow.getLong("id"); // id 칼럼 값을 Long으로 구한다.
                // ...
            } else if (data.getType() == ChangeType.UPDATE) { // 변경된 데이터면
                String name = dataRow.getString("name"); // 변경된 name 칼럼 값을 구한다.
                DataRow dataRowBeforeUpdate = data.getDataRowBeforeUpdate(); // 변경전 데이터 행(변경 전 이미지)
                String nameBeforeUpdate = dataRowBeforeUpdate.getString("name"); // 변경전 name 칼럼 값을 구한다.
                // ...
            } else if (data.getType() == ChangeType.DELETE) {
                String email = dataRow.getString("email"); // 삭제된 행의 email 칼럼 값을 구한다.
                // ...
            }
        });
    }

    @Override
    public void onXid(Long xid) {
        // 트랜잭션 커밋 로그
    }

    @Override
    public void stopped() {
        // CDC를 중지하면 호출됨
    }
});

단계 4, MariadbCdc 시작/중지

cdc.start(); // 별도 쓰레드로 바이너리 로그 조회 시작

...

cdc.stop(); // 조회 중지

MariadbCdc가 시작되면 setMariadbCdcListener()로 설정한 listener에 읽은 바이너리 로그 데이터를 전달한다.

 

MariadbCdc#start()는 별도 쓰레드로 바이너리 로그 조회를 시작한다.

특정 테이블만 포함하기/제외하기

MariadbCdcListener#onDataChanged()는 기본적으로 모든 변경에 대해 불린다. 만약 특정 테이블만 포함하거나 제외하고 싶다면 필터를 사용하면 된다.

// test.user 테이블 변경에 대해서만 onDataChanged() 호출
config.setIncludeFilters("test.user");
// test.member 테이블 변경은 onDataChanged() 호출 제외
config.setIncludeFilters("test.member");

수정한 칼럼만 포함하기

binlog_row_image가 full 이면 UPDATE/DELETE 시에 모든 칼럼 값을 포함한다. 변경된 칼럼 값만 포함하고 싶다면 binlog_row_image를 minimal로 설정한다.

binlog_format = row
binlog_row_image = minimal

MINIMAL은 수정 전 이미지에는 PK 칼럼만 기록하고(PK 칼럼이 없으면 전체 칼럼) 수정 후 이미지에는 변경된 칼럼만 기록한다.

binlog_row_image 가 minimal일 때 아래 쿼리를 실행하면

update member set name = 'newname' where id = 10

RowChangedData#getDataRowBeforeUpdate()는 PK 칼럼만 포함한 DataRow를 리턴하고
RowChangedData#getDataRow()는 변경된 칼럼만 포함한 DataRow를 리턴한다.

@Override
public void onDataChanged(List<RowChangedData> list) {
    // handle changed data
    list.forEach(data -> { // each
        String database = data.getDatabase(); // test
        String table = data.getTable(); // member
        DataRow afterDataRow = data.getDataRow(); // after image
        if (data.getType() == ChangeType.UPDATE) {
            DataRow beforeDataRow = data.getDataRowBeforeUpdate(); // before image
            Long id = beforeDataRow.getLong("id"); // before image includes only pk fields
            String name = afterDataRow.getString("name"); // after image includes only updated fields
            // ...
        }
    });
}

MariaDB 10.5: binlog_row_metadata = full

MariaDB 10.5는 binlog_row_metadata 변수를 지원한다.
binlog_row_metadata 값이 FULL이면 칼럼 이름을 포함한 모든 메타데이터가 바이너리 로그에 기록된다.
그래서 binlog_row_metadata가 FULL이면 ColumnNamesGetter가 필요 없다.

참고자료

반응형

DB를 사용하는 기능에 대한 통합 테스트를 진행하려면 테스트할 상황에 맞게 데이터를 구성해야 한다. 예를 들어 주문의 상태를 변경하는 기능을 통합 테스트한다고 하자. 이 경우 주문이 DB에 존재해야 상태가 올바르게 바뀌는지 확인할 수 있다. 즉 테스트 대상 기능을 실행하기 전에 DB에 테스트할 상황에 맞는 데이터를 INSERT 해야 한다. 이를 위해 다음과 같은 보조 클래스를 만들어 사용하면 테스트 코드 작성이 조금 편리해진다.

public class OrderGivenHelper {
    private JdbcTemplate jdbcTemplate;
    
    public OrderGivenHelper(DataSource dataSource) {
    	this.jdbcTemplate = new JdbcTemplate(dataSource);
    }
    
    public void givenOrder(Order order) {
        List<String> cols = new ArrayList<>();
        List<Object> values = new ArrayList<>();
        if (order.getId() != null) {
            cols.add("order_id");
            values.add(order.getId());
        }
        if (order.getTotalAmount() != null) {
            cols.add("total_amount");
            values.add(order.getTotalAmount());
        }
        if (order.getStatus() != null) {
            cols.add("status");
            values.add(order.getStatus());
        }

        ...생략
        String insCols = cols.stream().collect(Collectors.joining(",", "(", ")"));
        String bindParams = cols.stream().map(x -> "?").collect(Collectors.joining(",", "(", ")"));
        jdbcTemplate.update("insert into PURCHASE_ORDER " + insCols + " values " + bindParams,
                values.toArray());
    }
    
    public void clearOrder() {
    	jdbcTemplate.update("delete from PURCHASE_ORDER");
    }
}

위 헬퍼를 사용하면 giveOrder() 메서드에 전달할 Order 객체를 생성할 때 INSERT 쿼리를 실행할 때 NOT NULL인 필수 값만 지정하면 된다.

 

통합 테스트 코드는 위 코드를 사용해서 상황에 맞는 테스트를 실행한다.

@SpringBootTest
public class OrderTest {
    @Autowired
    private OrderCancelService cancelService;
    @Autowired
    private OrderRepository repo;
    private OrderGivenHelper givenHelper;
    private DataSource dataSource = ...생략;

    @BeforeEach
    public void setUp() {
        givenHelper = new OrderGivenHelper(dataSource);
    }
    
    public void cancelOrder() {
        givenHelper.clearOrder();
        givenHelper.givenOrder(Order.builder().id(10L).status(PAID).build());
        
        cancelService.cancelOrder(10L);
        
        Order order = repo.findById(10L);
        assertThat(order.getStatus()).isEqualTo(CANCEL);
    }
}

 

  1. 익명 2020.09.12 22:06

    비밀댓글입니다

+ Recent posts