순수 JAVA로 MySQL 연동해서 써보기 (JDBC)

BaekGwa·2024년 12월 9일
0

✔️ Java

목록 보기
12/12
post-thumbnail

Proejct Code : 링크

  • Java로 DB와 연결해서 사용하려면, 어떻게 해야될까?
  • 가장 간단한 방법은 Hibernate 같은 DB 연결을 아름답게 제공해주는 Framework를 사용하는 방법일 것 입니다.
  • 저는, 개발자에 입문을 하며, db를 다루기 시작한 시점부터 spring f/w 를 사용하였기 때문에, (DB <-> Spring(HIbernate)) 는 빼놓을 수 없는 관계였습니다.

뭔가... 주객이 전도되었다?

  • spring 은 java의 framework 이고, db 연결은 spring 에서 지원해주는 기능일 뿐, spring 없이는 db 연결을 못하는 java 개발자가 정상인가?
  • 라는 생각이 들어 학습을 하게 되었습니다.

요구사항

  • 요구사항은, 간단히 다음과 같습니다.
    • JDBC + MySQL 을 사용해서, 간단한 CRUD 진행
    • Transaction 관리가 될 것. (Commit / Rollback 등)
    • 다음과 같이 미리 작성된, 테스트 코드들을 통과할 것. (동시성 관련 테스트 추가)
    • +add) CP 추가
  • 개발은, Gradle + Java17로 진행되었습니다.

개발 진행

  • 테스트 코드는 Github 를 참고해주시면 됩니다.

의존성 추가

  • gradle을 사용하여, 다음과 같은 MySQL, JDBC, junit 등의 의존성을 추가해주었습니다. (편의를 위해서 롬복 추가!)
~~

dependencies {
    compileOnly("org.projectlombok:lombok:1.18.36")
    annotationProcessor("org.projectlombok:lombok:1.18.36")

    testImplementation platform('org.junit:junit-bom:5.10.0')
    testImplementation 'org.junit.jupiter:junit-jupiter'

    runtimeOnly("com.mysql:mysql-connector-j:9.1.0")

    testImplementation("org.assertj:assertj-core:3.26.3")

    implementation("com.fasterxml.jackson.core:jackson-databind:2.18.2")
    implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2")
}
~~

DB 연결을 위한 객체 생성

  • 총 두개의 객체가 사용되었습니다.
    • SimpleDB, Sql
  • 각 객체의 역할은 다음과 같습니다.
    • SimpleDB
      • DB연결
      • Connection Pool 관리
      • 트랜잭션 관리
    • Sql
      • 실제 Sql Query 실행 / 결과값 반환
      • devMode 에 따른 Logging
  • 코드를 다 적으면, 너무 길어지기 떄문에, 링크를 통해 확인이 가능합니다. SimpleDB, Sql

Key work

PreparedStatement 를 사용하여, 파라미터 바인딩 처리

  • 테스트코드 중, 다음과 같은 코드가 있습니다.
	private void makeArticleTestData() {
        IntStream.rangeClosed(1, 6).forEach(no -> {
            boolean isBlind = no > 3;
            String title = "제목%d".formatted(no);
            String body = "내용%d".formatted(no);

            simpleDb.run("""
                    INSERT INTO article
                    SET createdDate = NOW(),
                    modifiedDate = NOW(),
                    title = ?,
                    `body` = ?,
                    isBlind = ?
                    """, title, body, isBlind);
        });
    }
  • 체이닝도 체이닝 이지만, ?를 안전파게 바인딩 해서, SQL Injection 공격을 방어할 필요가 있습니다.
  • JPA(Hibernate)를 사용하면, 자연스럽게 방어가 되겠지만, 단순히 ?를 replace 해서는 공격에 취약할 수 있습니다.
  • 따라서, 다음과같이 PreparedStatement를 사용해서, 안전하게 파라미터를 바인딩 해주었습니다.
	@Override
    public void execute(String inputSql, Object... params) {
        try {
            PreparedStatement preparedStatement = connection.prepareStatement(inputSql);
            for (int i = 0; i < params.length; i++) {
                preparedStatement.setObject(i + 1, params[i]);
            }
            preparedStatement.execute();
            printRowSql(preparedStatement.toString());
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            if(autoCloseConnection) {
                simpleDb.returnConnection(connection);
            }
        }
    }

CP 구현

  • 위에서 설명한 대로, SimpleDb 에서 Connection pool 을 관리하도록 하였습니다.
  • 기본 전략은 Lazy Initialization을 채택하였습니다.
  • 생성된 Connection은 바로 distory 되지 않고, 지속 연결하여 재활용 할 수 있도록 구성하였습니다.
  • 또한, SimpleDB 객체는, 멀티스레드에서 안전해야 하기에, concurrent 관련된 클래스를 적극 활용하였습니다. (ConcurrentLinkedQueue, AtomicInteger)
  • SQL 문을 실행하기 위해서, Connection을 생성 or 할당 받기 위해서 getConnection() 메서드를 사용하게 하고, 이 메서드는 Queue를 사용해 컨트롤 하도록 구성하였습니다.
	SimpleDb.java
	~~~
    private Connection getConnection() throws SQLException {
        if(!pool.isEmpty()) {
            return pool.poll();
        } else if(currentConnections.get() < maxConnections) {
            currentConnections.incrementAndGet();
            return newConnection();
        } else {
            throw new SQLException("Now connection is Max, maxConnections:" + this.maxConnections);
        }
    }
    ~~~
    
    Sql.java
    ~~
    @Override
    public void execute(String inputSql) {
        try {
            Statement statement = connection.createStatement();
            statement.execute(inputSql);
            printRowSql(inputSql);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            if(autoCloseConnection) {
                simpleDb.returnConnection(connection);
            }
        }
    }
    ~~
  • 추가적으로, 작업이 완료되었다면, returnConnection()을 사용하여 Queue에 반납해서 재활용하도록 구성하였습니다.

Transaction 관리

  • 트랜잭션 관리를 위해서 다음과 같은 테스트 코드가 있습니다.
	@Test
    @DisplayName("rollback")
    public void t018() {
        // SimpleDB에서 SQL 객체를 생성합니다.
        long oldCount = simpleDb.genSql()
                .append("SELECT COUNT(*)")
                .append("FROM article")
                .selectLong();

        // 트랜잭션을 시작합니다.
        Sql sql = simpleDb.genSql();
        simpleDb.startTransaction(sql.getConnection());

        sql
                .append("INSERT INTO article ")
                .append("(createdDate, modifiedDate, title, body)")
                .appendIn("VALUES (NOW(), NOW(), ?)", "새 제목", "새 내용")
                .insert();

        simpleDb.rollback(sql.getConnection());

        long newCount = simpleDb.genSql()
                .append("SELECT COUNT(*)")
                .append("FROM article")
                .selectLong();

        assertThat(newCount).isEqualTo(oldCount);
    }
  • 트랜잭션(작업의 범위)에 맞게, 진행되도록 트랜잭션이 시작할 경우, 해당 Connection의 autoCommit 기능을 끄고, 별도의 flag를 둬서, 반납또한 진행하지 않도록 구성하였습니다.
  • 만약, 트랜잭션이 롤백 된다면, autoCommit 기능을 다시 켜고, 트랜잭션 범위 안에있던 모든 작업을 connection.rollback() 을 통해 롤백 시켜줍니다.
	public void startTransaction(Connection connection) {
        try {
            connection.setAutoCommit(false);
            setAutoCloseConnection(false);
        } catch (SQLException e) {
            setAutoCloseConnection(true);
            throw new RuntimeException(e);
        }
    }

    public void rollback(Connection connection) {
        try {
            connection.rollback();
            connection.setAutoCommit(true);
            setAutoCloseConnection(true);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            setAutoCloseConnection(true);
        }
    }
  • 추가적으로, 현재는 commit을 명시적으로 해줘야만 반영이 되지만, 실제 JPA(Hibernate)를 사용하면 자동적으로 메서드가 끝날때, commit을 진행하는 등 (프록시 활용할듯) 많은 기능들이 들어가 있습니다.

정리 및 회고

  • 위에 소개드린 key-work 외에도 많은 부분을 고려하여 만들었으나, 주로 작업하고 고민한 부분은 위의 3포인트 였습니다.
  • 이 프로젝트를 진행하면서, DB Access 의 기술 부분을 많이 채웠다고 생각합니다.
    • 이론적으로 알고 있던, CP, JDBC, 트랜잭션 관리 등
  • 진짜 시간이 남는다면, Hibernate에 버금가는 ORM 을 한번 만들어 보고 싶어집니다.
    • 100%는 몰라도, 향은 나게 만들 수 있을 것 같은 자신감이 조금 생김.
profile
현재 블로그 이전 중입니다. https://blog.baekgwa.site/

0개의 댓글