Real MySQL - 읽는이에게 충격과 경악을... 책 이야기

개발자와 DBA를 위한 Real MySQL - 10점
이성욱 지음/위키북스

오랜만에 만나 본 정말 좋은 우리나라 책이다. DBA 뿐만 아니라 MySQL 기반 웹 애플리케이션 개발자도 필수적으로 읽어봐야 할 책이다.

이 책을 읽으면서 함께 읽은 사람들이 "세상에.. 이걸 모르고 우리가 계속 개발하고 있었단 말인가?"를 연발했다.

이는 두 가지를 의미하는데, 이 책이 가려운데를 잘 긁어줬다는 얘기이고, 또 하나는 DBA가 아닌 애플리케이션 개발자들이 DB를 너무 대충 보고 개발한다는 점이다.

MySQL일반 뿐만 아니라 JDBC를 통한 연동 부분에서도 상당히 충격적(?)인 내용들이 있다. 예를들면 MySQL은 특별한 설정을 하지 않으면 PreparedStatement가 기본으로 꺼져 있고(PreparedStatement 객체로 쿼리를 날려도 소용없다. 일반 Statement로 강제 변환된다), setFetchSize()를 모두 무시하는 등 개발자의 예상을 빗나가는 행동을 많이 한다. 그 외에 MySQL 기반 웹 애플리케이션의 필수 항목인 Replication에 관련된 ReplicationDriver도 필독 사항이다(아래에 부가 설명).

예전에 SI가 주를 이루던 시대에는 Oracle이 중요했지만, 요즈음 일반 서비스 웹 애플리케이션 개발이 주를 이루는 현 시점에서는 MySQL이 더욱 중요해진 느낌이다.

MySQL 쿼리만 알고 대충 개발하는 개발자라면 모두 필독해야할 책이다.

단, MySQL에 완전히 무지한 경우에는 초보자 책을 먼저봐야한다. 이 책은 좋은 내용을 풍성하게 담고 있지만, 결코 완전한 SQL 초보자를 위한 책은 아니다. MySQL 서버를 다운로드해 설치하고 DB/테이블을 만들고 쿼리 정도는 날릴 줄 알아야 볼 수 있다.

http://www.wikibook.co.kr/wiki/Wiki.jsp?page=RealMySQL에서 소스를 다운로드하여 모두 압축을 풀고 다음 명령을 실행하면 예제 테이블과 데이터가 생성된다. 이제 책의 예문을 따라 해 볼 수 있게 된다.

$ mysql -uroot -p -t < employees.sql
$ mysql -uroot -p -t < test_employees_sha.sql
$ mysql -uroot -p -t employees < 예제DB\ employees_schema_modification.sql

책 455쪽에 보면 Delayed Join에 대한 내용이 나오는데, 실제로 이를 테스트 하면 결과가 어떻게 나오는지 궁금해서 테스트 코드를 짜 보았다. https://gist.github.com/kwon37xi/5652026 에서 소스를 볼 수 있고, 내 PC에서의 실행 경과는 다음과 같다.
  • 일반 Join : 19209 ms
  • Delayed Join : 8005ms

즉, 책의 말대로 지연된 조인(delayed join)이 훨씬 빠르다.

그리고, 책에서도 나오고, 이미 많이 알려져 있는 Replication Driver가 정말 잘 작동하는지 테스트도 해 보았다. MySQL Replication Driver와 Spring의 @Transactional(readOnly=true/false)을 조합하면 별도의 코딩작업 없이 MySQL Master/Slave DB 접속과 쿼리가 쓰기 트랜잭션에서는 자동으로 Master로, 읽기 트랜잭션에서는 자동으로 Slave로 가도록 만들 수 있다.

그래서 그게 진짜로 되는지 예제를 만들어 보았다(물론 잘 된다).

먼저 mysqlreplicationdriver.groovy 를 실행하여 일반 커넥션에 대해 setReadOnly() 에 따라 제대로 master/slave로 가는지 확인 해보고 그 다음에 ProductService.java에 @Transactional 애노테이션이 잘 작동하는지 확인해보면 된다.

Gradle이 설치 돼 있다면 별도의 IDE에 설치할 필요없이 프로젝트 문서에 나온대로 실행해서 확인해 볼 수 있다.

Replication Driver @Transactional 예제 소스 코드 : https://github.com/kwon37xi/jdbc-mysql-test


궁금한 사항이 있으면 저자의 Real MySQL 까페에 들러서 물어보면 친절히 답해주신다.


공유하기 버튼

 
 

한 종류의 DB에 대해 두 개 이상 버전의 JDBC 드라이버를 사용할 수 있나? 프로그래밍

하나의 종류의 DB에 두 개의 JDBC 드라이버를 사용해야 하는 경우가 얼마나 있는지는 모르겠으나 없지는 않다.
회사와 관련없는 외부 시스템과 연동하거나(이럴 경우 DB 직접 연동이 필요한 것 자체가 잘못된 것이다. SOAP, REST API 등 많은 중립적인 방법을 사용하는게 낫다), 혹은 한 회사내에서도 레거시 시스템과 최신 시스템이 섞일 경우가 그렇다.

예를들면 Oracle 11g와 Oracle 8i에 하나의 애플리케이션에서 접속할 일이 있을 때, 최신 Oracle 11g용 JDBC 드라이버는 Oracle 8i DB에 접속하지 못한다. 사실 양쪽에 모두 잘 되는 버전을 찾아내면 될지도 모르지만, 조금 어려운 길을 가보기로 했다.

자, 그렇다면 빠르게 결론으로 가서, 하나의 애플리케이션에서 동일 종류의 DB용 JDBC 드라이버를 여러 버전으로 사용할 수 있는가?
답은 원칙적으로는 안 된다.
하지만 나는 살짝 머리를 굴려 해결책을 찾아냈다.

왜 안될까? 첫번째 난관과 두번째 난관이 존재한다.

첫번째 난관, 동일한 JDBC Driver Class FQCN

기본적으로 JDBC 드라이버의 클래스 FQCN(Fully Qualified Class Name)이 동일하다. 예를들면 Oracle은 oracle.jdbc.OracleDriver 라는 클래스를 JDBC 드라이버 버전에 상관없이 동일하게 사용한다. 따라서 두 개 버전의 JDBC 드라이버를 동시에 CLASSPATH에 넣으면 둘 중의 하나만 제대로 로딩 된다.
MySQL의 경우 일반 드라이버와 Replication 드라이버의 FQCN은 서로 다르다. 하지만 두번째 난관을 맞게 된다.

두번째 난관, 동일한 JDBC URL

첫번째를 통과해도 두번째 문제인 동일한 JDBC URL 문제가 남는다. 이는 java.sql.DriverManager 의 작동 방식을 디버깅하다가 알게 되었다. 링크로 건 DriverManager의 소스를 봐보자.
JDK 버전마다 위치는 다르겠지만 어쨌든 링크건 소스의 268줄을 보면 다음과 같다.

if (di.driver.acceptsURL(url)) {
    // Success!
    println("getDriver returning " + di);
    return (di.driver);
}


즉, DriverManager는 다음과 같이 작동하는 것이다.
  1. Class.forName("JDBC Driver ClassName"); 코드가 실행되면 JDBC 드라이버는 스스로를 DriverManager.registerDriver()메소드를 통해 등록한다.
  2. DriverManager.getConnection(jdbcUrl) 호출을 통해 커넥션을 얻게 되는데, 이 때 저 위에 있는 Driver.acceptsURL(jdbcUrl) 코드가 번갈아 호출되면서 true를 리턴하는 최초의 드라이버에게 커넥션을 생성하도록 지시한다.

따라서 동일한 JDBC URL 포맷을 사용하는 두 개 이상의 드라이버가 등록되면 드라이버 등록 순서에 따라서 먼저 acceptsURL이 호출되는 드라이버가 우선권을 갖게 된다. 이것이 키포인트이다.

이제 이 난관을 뚫고 동일한 두개 이상 버전의 JDBC 등록 방법의 꼼수를 찾아보면,

첫번째 난관의 해결책, Repackaging

첫번째 문재는 Repackaging으로 풀 수 있다. jarjar 라는 리패키징 툴을 이용하면 하나의 jar에 있는 패키지 구조를 일괄적으로 다른 패키지로 변경할 수 있다. 즉, a.jar 에 있는 클래스들이 모두 com.example.XXX 로 시작한다면 이것을 모두 org.government.XXX 형태로 일괄 변경할 수 있다.
단지 패키지 디렉토리 구조만 바꾸는 것이 아니라 해당 jar 파일내의 모든 *.class의 호출구조에서 나타나는 com.example도 모두 org.govenrment로 바꾸는 것이다.
이것에 대한 힌트는 토비의 스프링 3 책의 부록을 읽다가 asm이 이런 형식으로 리패키징 돼 있다는 사실에서 얻었다.
이제는 FQCN이 완전히 다른 또다른 JDBC 드라이버가 생긴 것이다.

두번째 난관의 해결책, 나만의 JDBC URL을 인지하는 JDBC Driver 클래스를 만들자

여기서부터 코딩이 들어간다. 자 그렇다면 이제 기본 드라이버와는 다른 버전이 사용할 그 자신만의 JDBC URL구조를 만들어주면 된다. 나는 그냥 기존 URL에 간단한 Prefix 문자열만 추가하였다.
Driver.acceptsURL() 메소드를 고쳐서 내가 스스로 만든 JDBC URL에 반응하게 한다.
그리고 나서 그 prefix된 JDBC URL을 받는 모든 메소드에서 prefix 부분을 제거한 상태의 URL을 실제 JDBC 드라이버(첫번째 난관 해결에서 FQCN을 변경한 JDBC 드라이버)에게 모두 위임하는 것이다.

이론은 간단한데 어떻게 만들지? 이때 오픈소스가 큰일을 해주었다. 나는 MySQL JDBC 드라이버의 소스를 까보고서 그 구조를 따라서 JDBC 드라이버 등록방식을 그대로 따라 구현하였다.

방법은 두가지인데 원하는 JDBC 드라이버(리패키징한것)을 상속하여 구현하고 JDBC URL을 인자로 받는 모든 메소드에서 prefix를 제거한 URL을 super로 호출해 넘겨주거나, 아니면 따로 java.sql.Driver인터페이스를 직접 구현하고, 내부에서 리패키징한 JDBC Driver Class의 인스턴스를 만들어 위임시켜주면 된다.

자세한 소스는 MySQL 드라이버 소스를 보면 되지만 핵심이 되는 드라이버 등록부분 코드를 옮겨보면 다음과 같다. 매우 간단함을 알 수 있다.

static {
    try {
        java.sql.DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {
        throw new RuntimeException("Can't register driver!");
    }
}


여기서 주의할 점
자, 이렇게 해서 원래 JDBC 드라이버와 내가 직접 만든 JDBC 드라이버를 직접 등록하면 잘 될까? 잘될 확률이 높지만 위험하다.
왜냐하면 드라이버를 두개 등록한것 같지만 실제로는 세개가 등록되기 때문이다.

- 원래 JDBC 드라이버
- 내가 만든 JDBC 드라이버
- 내가 만든 JDBC 드라이버가 위임해서 일을 처리하는 리패키징된 드라이버

리패키징 드라이버가 등록되는 이유는 등록 과정이 static {} 블럭으로 처리되기 때문에다. 위임 하려고 해당 클래스의 인스턴스를 생성하는 순간 static 블럭이 실행되면서 자기 자신을 드라이버로 등록해버린다.
내가 만든 JDBC 드라이버 등록시에 자신을 등록한 뒤, DriverManager에 등록된 리패키징된 드라이버를 제거하는 과정을 거쳐야 한다. 안그러면 리패키징된 드라이버와 원래 JDBC 드라이버가 동일한 JDBC URL을 가지고 경합을 벌이는 상태가 된다. 어떤게 우선 선택될지는 드라이버 로딩 순서에 따라 달라지게 될 것이다.

이 또한 DriverManager 소스를 보면 즉시 이해될 만한 사안이므로 굳이 여기서 보여주진 않는다.

이렇게 해서 무사히 동일 DB 제품에 대한 두개 이상 버전의 JDBC Driver 등록을 마쳤다.

공유하기 버튼

 
 

1 2 3 4 5 6 7 8 9 10 다음