개발놀이터

MySQL 트랜잭션 로그 추적 with 스프링 본문

CS 지식/데이터베이스

MySQL 트랜잭션 로그 추적 with 스프링

마늘냄새폴폴 2024. 11. 26. 20:49

약 20일전에 "메세지 브로커의 근심과 걱정" 이라는 포스팅을 작성하였습니다. 

 

https://coding-review.tistory.com/566

 

메세지 브로커의 근심과 걱정

마이크로 서비스와 메세지 브로커는 뗄 수 없는 사이입니다. 서로 다른 도메인이 여러개의 서버로 나눠져 있는 상황에서 모든 서버에 동일하게 데이터를 전달해야 하는 경우에 메세지 브로커만

coding-review.tistory.com

 

위의 포스팅에서는 데이터베이스의 트랜잭션과 메세지 브로커의 통합을 위해서 어떤 부분을 고려해야 하는지에 대한 내용이었습니다. 

 

정상적인 상황에서는 문제 없지만 만약 예외 상황에서 트랜잭션 롤백이 되었을 때 메세지 브로커도 그에 맞춰서 메세지가 전송되지 않아야하는데 트랜잭션과 메세지 브로커를 통합하는 것은 또 다른 복잡성을 유발하였습니다. 

 

가장 매끄러운 상황은 트랜잭션 로그를 추적해서 커밋되는 경우에만 메세지를 전송하는 것이었습니다. 이 방법의 이름은 Transaction Trailing Pattern이고 위 포스팅에선 이를 쉽게 구현해주는 PGMQ에 대해서 짧게 알아봤습니다. 

 

PGMQ는 PostgreSQL을 사용해야하고 만약 우리의 서비스가 Oracle계열을 사용한다면 쉽게 사용할 수 없을 것이고 그렇기에 저는 MySQL 환경에서 어떻게 트랜잭션 로그를 추적할 것인지에 대해서 조금 공부해봤습니다. 

 

물론 혼자서 해본 토이프로젝트인만큼 완성도면에서는 조금 떨어질 수 있어도 간단한 개요정도라고 생각해주시면 좋을 것 같습니다. 

 

MySQL 트랜잭션 로그

MySQL은 binlog라는 트랜잭션 로그가 존재합니다. 여기에는 어떤 쿼리가 실행되었는지 쿼리가 적혀있지만 트랜잭션 로그의 다른 이름인 바이너리 로그에서 알 수 있듯이 열어보면 우리가 읽을 수 없습니다. 

 

그래서 우리가 읽을 수 있는 쿼리로 변경하기 위해서는 따로 작업을 진행해줘야합니다. 

 

$ cd /var/lib/mysql

$ mysqlbinlog --base64-output=decode-rows -vv binlog.000001 > test.sql

 

mysql> show binary logs;

 

MySQL cli로 바이너리 로그 이름을 검색하고 해당하는 파일을 sql 파일로 변경해주시면 됩니다. 

 

이름이 트랜잭션 로그인 만큼 SELECT 쿼리는 저장하지 않는다는 특징이 있습니다. 

 

보통 binlog를 사용하는 일반적인 경우는 데이터베이스의 데이터가 모두 날아간 경우 사용할 수 있습니다. 사실 이 경우도 일반적이라고 말하기엔 좀 그렇지만... binlog를 사용하는 가장 대표적인 경우이긴 합니다..

 

cf) 실제 대형사고 썰

저도 개발바닥 유튜브 돌아다니다가 들은건데 JPA를 이용해서 개발하던 우아한형제들에서 로컬 환경에 운영 데이터베이스를 물려서 테스트해보고 싶었던 개발자분이 JPA의 속성 중 ddl-auto를 create로 하고 애플리케이션을 실행시키는 바람에 모든 데이터가 삭제되는 참사가 벌어졌다고 합니다. 

 

ddl-auto를 create로 하겠다는 것은 모든 테이블을 지우고 다시 새로 생성해서 멱등성을 유지시키려고 설정하는 개발환경에 최적화된 설정입니다. 

 

때문에 create로 하고 애플리케이션을 실행시키면 drop table ... drop table ... drop table ... 이런 무시무시한 로그가 계속 올라가는 것을 볼 수 있죠. 

 

그리고 모든 데이터가 삭제되었고 이를 해결하기 위해서 매일 특정 시간에 돌아가던 데이터베이스 풀백업으로 어느정도는 복구했지만 특정 시간과 현재 시간 사이에 존재하던 데이터 공백이 존재했습니다. 

 

이 공백시간의 데이터를 binlog를 이용해서 밀어넣었고 잘 마무리되었다는 후문이 있습니다. 

 

binlog는 일정 시간이 지나면 삭제되기 때문에 이런 특별한 상황을 제외하고는 잘 사용할 일이 없는 파일이긴 합니다. 

 

 

MySQL 트랜잭션 로그 추적

그래서 이번엔 이 binlog를 추적해서 메세지 브로커가 트랜잭션 커밋일 경우에만 메세지를 보내도록 설계해보겠습니다. 

 

implementation 'com.zendesk:mysql-binlog-connector-java:0.30.1'

 

저는 빌드 도구로 gradle을 사용했습니다. 위와 같은 의존성을 추가해줍니다. 

 

또한, 데몬 스레드를 이용해서 커밋 이벤트를 등록하고 이 스레드를 애플리케이션 시작시 빈으로 등록해서 스프링이 관리하도록 해보겠습니다. 

 

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "USER")
public class User {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "USER_ID")
    private Long id;

    private String name;

    private int age;

    private int money;
}

 

간단하게 USER 테이블을 만들어줍니다. 

 

@Configuration
public class ExecutorConfig {

    @Bean
    public ExecutorService daemonExecutorService() {
        return Executors.newSingleThreadExecutor((runnable) -> {
            Thread thread = new Thread(runnable);
            thread.setDaemon(true);
            return thread;
        });
    }
}

 

그리고 데몬 스레드를 위한 ExecutorService를 빈으로 등록해줍니다. setDaemon을 true로 등록하는 것을 빼먹으면 안됩니다!

 

@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {

    private final ExecutorService daemonExecutorService;

    @Value(value = "${spring.datasource.username}")
    private String username;

    @Value(value = "${spring.datasource.password}")
    private String password;

    @PostConstruct
    public void startDaemonThread() {
        String hostname = "localhost";
        int port = 3306;

        daemonExecutorService.submit(() -> {
            log.info("daemon thread started");
            try {
                BinaryLogClient client = new BinaryLogClient(hostname, port, username, password);

                client.registerEventListener((event) -> {
                    EventType eventType = event.getHeader().getEventType();

                    if (eventType == EventType.EXT_WRITE_ROWS) {
                        WriteRowsEventData data = event.getData();
                        log.info("write rows event: {}", data);
                    }
                });

                client.connect();
            }
            catch (Exception e) {
                log.error("Error starting BinaryLogClient", e);
            }
        });
    }
}

 

준비는 끝!

 

여기서 중요한 점은 BinaryLogClient를 만들고 EventListener를 등록하고 connect()를 호출하면 자동으로 해당 binlog event를 등록해줍니다. 

 

 

데몬 스레드가 실행되었고 로그에 localhost:3306이 연결되었고 mysql-bin.00001에 포지션 3298번이 연결된 것입니다. 

 

만약 본인이 원한다면 다른 binlog나 다른 포지션을 등록할 수 있지만 자동으로 해주니까 믿어보겠습니다. 

 

그리고 이제 MySQL cli로 트랜잭션을 시작하고 커밋해보겠습니다. 

 

 

그리고 이제 애플리케이션 로그를 확인해보면?

 

 

잘 나오네요. 

 

 

마치며

저는 INSERT 이벤트만 잡았지만 INSERT 말고도 UPDATE, DELETE 이벤트도 잡을 수 있습니다. 

EventType.EXT_UPDATE_ROWS
EventType.EXT_DELETE_ROWS

 

이렇게 트랜잭션을 사용하는 INSERT, UPDATE, DELETE 이벤트를 등록해서 사용할 수 있습니다. 

 

binlog를 추적하는 것이 문서가 잘 안되어있어서 고생했는데 잘 마무리돼서 후련합니다. 이제 PGMQ를 해야하는데... 다음 시간에 한번 도전해보겠습니다..

 

긴 글 읽어주셔서 감사합니다! 오늘도 즐거운 하루 되세요~