728x90

저번에 말했던 실시간으로 수정이 가능한 UpdatableSqlRegistry를 만들어보고자 한다.

 

Sql을 업데이트 할 때, 쿼리를 호출하게 된다면 깨진 Sql이 나타날 수 있기 때문에 동시성 문제에 주의해서 구현해야 한다.

그렇기에 자바에서 동시성 문제를 도와주는 라이브러리를 사용해 만들어보도록 하겠다.

 

ConcurrentHashMap을 이용한 수정 가능 SQL 레지스트리

HashMap에서는 역시 멀티스레드 환경에서 동시성 문제가 발생할 수 있기 때문에, 이런 문제에 최적화된 ConcurrentHashMap을 사용하도록 하겠다.

ConcurrentHashMap은 업데이트시에 해당 부분에만 락을 걸기에, 성능이 많이 떨어지지 않는다.

 

우선 다음과 같은 테스트를 작성한다.

이 테스트가 통과해야 할 것이다.

public class ConcurrentHashMapSqlRegistryTest {

    UpdatableSqlRegistry sqlRegistry;

    @BeforeEach
    public void setUp() {

        sqlRegistry.registerSql("KEY1", "SQL1");
        sqlRegistry.registerSql("KEY2", "SQL2");
        sqlRegistry.registerSql("KEY3", "SQL3");
    }

    @Test
    public void find(){
        Assertions.assertTrue(
                sqlRegistry.findSql("KEY1").equals("SQL1") &&
                        sqlRegistry.findSql("KEY2").equals("SQL2") &&
                        sqlRegistry.findSql("KEY3").equals("SQL3")
        );
    }

    @Test
    public void notFoundKey(){
        Assertions.assertThrows(ChangeSetPersister.NotFoundException.class, () -> sqlRegistry.findSql(UUID.randomUUID().toString()));
    }
    
    @Test
    public void updateSql(){
        sqlRegistry.updateSql("KEY1", "U_SQL1");
        Assertions.assertTrue(
                sqlRegistry.findSql("KEY1").equals("U_SQL1") &&
                        sqlRegistry.findSql("KEY2").equals("SQL2") &&
                        sqlRegistry.findSql("KEY3").equals("SQL3")
        );
    }
    
    @Test
    public void updateMulti(){
        Map<String, String> sqlmap = new HashMap<>();
        
        sqlmap.put("KEY1", "U_SQL1");
        sqlmap.put("KEY3", "U_SQL3");
        
        sqlRegistry.updateSql(sqlmap);
        Assertions.assertTrue(
                sqlRegistry.findSql("KEY1").equals("U_SQL1") &&
                        sqlRegistry.findSql("KEY2").equals("SQL2") &&
                        sqlRegistry.findSql("KEY3").equals("U_SQL3")
        );
    }
}

동시성 관련해서는 멀티쓰레드를 만들어야 하고, 해당 환경에서도 문제가 발생할지 안할지를 잘 모르기에 테스트가 어렵다.

우선은 자바에서 제공해주는 라이브러리를 믿어보도록 하자.

 

그리고 ConcurrentHashMap을 사용하는 Registry를 다음과 같이 구현한다.

public class ConcurrentHashMapSqlRegistry implements UpdatableSqlRegistry {

    private Map<String, String> sqlMap = new ConcurrentHashMap<>();

    @Override
    @SneakyThrows
    public void updateSql(String key, String sql) {
        if(sqlMap.get(key) == null)
            throw new ChangeSetPersister.NotFoundException();

        sqlMap.put(key, sql);
    }

    @Override
    public void updateSql(Map<String, String> sqlmap) {
        sqlmap.forEach(this::updateSql);
    }

    @Override
    public void registerSql(String key, String sql) {
        sqlMap.put(key, sql);
    }

    @SneakyThrows
    @Override
    public String findSql(String key) {
        String sql = sqlMap.get(key);
        if(sql == null) throw new ChangeSetPersister.NotFoundException();
        return sql;
    }
}

 

이거를 바탕을 테스트를 돌려보면

역시 성공하는 것을 볼 수 있다.

 

현재 SqlService에 해당 빈으로 등록 하도록 하자.

<bean id="sqlRegistry" class="seungkyu.ConcurrentHashMapSqlRegistry"/>

 

내장형 데이터베이스를 이용한 SQL 레지스트리 만들기

SqlRegsitry를 ConcurrentHashMap 말고도 데이터베이스로도 만들어 볼 수 있다.

외부로 트래픽이 필요한 외장 데이터베이스보다 애플리케이션에 내장되어 애플리케이션과 함께 실행되고 종료되는 내장 데이터베이스를 사용하는 것이 더 편리하고 빠를 것이다.

 

많이 사용되는 내장 데이터베이스는 Derby, HSQL, H2가 있다고 한다.

이 친구들은 스프링에서 돌아가기에 따로 SQL 스크립트를 만들어서 같이 실행해야 한다고 한다.

초기 Sql을 통해 데이터를 삽입하고 나면 그 뒤에는 JDBC와 같은 기술들을 동일하게 사용 할 수 있다고 한다.

 

일단 이 내장 데이터베이스를 간단하게 사용해보도록 하자.

내장 데이터베이스는 DB가 실행될때마다 테이블을 새로 생성하기에, 테이블을 생성하는 SQL을 같이 넣어줘야 한다.

 

schema.sql

CREATE TABLE SQLMAP(
    KEY_ VARCHAR(100) PRIMARY KEY,
    SQL_ VARCHAR(100) NOT NULL
);

 

그리고 초기에 데이터도 넣어주기 위해 insert로 구성된 data.sql을 작성한다.

INSERT INTO SQLMAP(KEY_, SQL_) values('KEY1', 'SQL1');
INSERT INTO SQLMAP(KEY_, SQL_) values('KEY2', 'SQL2');

 

간단하게 테스트를 작성해보니

package seungkyu;

import org.junit.jupiter.api.*;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;

import java.util.concurrent.atomic.AtomicInteger;

public class EmbeddedDbTest {

    EmbeddedDatabase db;
    JdbcTemplate jdbcTemplate;

    @BeforeEach
    public void setUp() {

        db = new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .addScript("/schema.sql")
                .addScript("/data.sql")
                .build();

        jdbcTemplate = new JdbcTemplate(db);
    }

    @AfterEach
    public void tearDown() {
        db.shutdown();
    }

    @Test
    public void initData(){
        Assertions.assertEquals(2, jdbcTemplate.queryForObject("select count(*) from sqlmap", Integer.class));

        AtomicInteger index = new AtomicInteger();

        jdbcTemplate.queryForList("SELECT * FROM sqlmap").forEach(
                (mapEntry) -> {
                    Assertions.assertEquals("KEY" + (index.get() + 1), mapEntry.get("key_"));
                    Assertions.assertEquals("SQL" + (index.get() + 1), mapEntry.get("sql_"));
                    index.getAndIncrement();
                }
        );
    }

}

 

 통과하는 것을 볼 수 있었다.

 

이제 이 내장 데이터베이스를 사용해서 SqlRegistry를 수정해보자.

 

일단 이 내장 데이터베이스를 사용하기 위해서 빈으로 등록하도록 하자.

 

    <jdbc:embedded-database id="embeddedDatabase" type="H2">
        <jdbc:script location="classpath:schema.sql"/>
    </jdbc:embedded-database>

schema는 실행하면서 생성만 하면 되기에 여기에 넣어준다.

 

이제 embeddedDatabase 빈이 등록되어 그냥 이 데이터베이스를 사용하면 된다.

 

이 빈을 사용해서 UpdatableSqlRegistry를 만들어보자.

 

주입받은 내장 데이터베이스의 DataSource를 통해 저장하고 업데이트 하도록 만든다.

package seungkyu;

import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.data.crossstore.ChangeSetPersister;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;
import java.util.Map;

public class EmbeddedDbSqlRegistry implements UpdatableSqlRegistry {

    private final JdbcTemplate jdbcTemplate;

    public EmbeddedDbSqlRegistry(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    @Override
    public void registerSql(String key, String sql) {
        jdbcTemplate.update("INSERT INTO sqlmap (key_, sql_) values (?, ?)", key, sql);
    }

    @Override
    public String findSql(String key) {
        try
        {
            return jdbcTemplate.queryForObject("SELECT sql_ FROM sqlmap WHERE key_ = ?", String.class, key);
        }
        catch(EmptyResultDataAccessException e)
        {
            throw new RuntimeException();
        }
    }

    @Override
    public void updateSql(String key, String sql) {
        int changed = jdbcTemplate.update("UPDATE sqlmap SET sql_ = ? WHERE key_ = ?", sql, key);
    }

    @Override
    public void updateSql(Map<String, String> sqlmap) {
        for(Map.Entry<String, String> entry : sqlmap.entrySet()) {
            this.updateSql(entry.getKey(), entry.getValue());
        }
    }
}

 

 

이제 이 EmbeddedDbSqlRegistry를 테스트 해봐야 한다.

근데 재미있게도 ConcurrentHashMapSqlRegistry와 구현한 인터페이스가 같다.

그리고 우리는 그 인터페이스의 내용만 테스트하면 끝이다.

 

그렇기에 해당 테스트 코드를 공유해보자.

 

이런식으로 추상 클래스를 만들고

public abstract class AbstractUpdatableSqlRegistryTest {

    UpdatableSqlRegistry sqlRegistry;

    abstract protected UpdatableSqlRegistry injectSqlRegistry();

    @BeforeEach
    public void setUp() {
        sqlRegistry = injectSqlRegistry();

        sqlRegistry.registerSql("KEY1", "SQL1");
        sqlRegistry.registerSql("KEY2", "SQL2");
        sqlRegistry.registerSql("KEY3", "SQL3");
    }
    //.....
}

 

기존의 ConcurrentHashMapSqlRegistryTest도 이렇게 간단하게 변경했다.

public class ConcurrentHashMapSqlRegistryTest extends AbstractUpdatableSqlRegistryTest{

    @Override
    protected UpdatableSqlRegistry injectSqlRegistry() {
        return new ConcurrentHashMapSqlRegistry();
    }
}

이렇게만 작성하고 테스트를 해봐도

 

기존에 작성한 4개의 테스트를 수행하는 것을 볼 수 있다.

 

이거를 그대로 상속받아 EmbeddedDbSqlRegistryTest를 만들어보자.

public class EmbeddedDbSqlRegistryTest extends AbstractUpdatableSqlRegistryTest{

    EmbeddedDatabase db;

    @Override
    protected UpdatableSqlRegistry injectSqlRegistry() {
        db = new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .addScript("/schema.sql")
                .build();

        return new EmbeddedDbSqlRegistry(db);
    }

    @AfterEach
    public void tearDown() {
        db.shutdown();
    }
}

 

이렇게 작성하고 테스트를 실행해보니, 다행하게 통과하는 것을 볼 수 있었다.

 

이제 저 EmbeddedDbSqlRegistry에 DataSource를 주입해줘야 한다.

아무생각없이 DataSource를 찾아 넣으면 MySql로 들어가기에 내장 데이터베이스를 잘 찾아서 넣어줘야 한다.

 

    <bean id="sqlRegistry" class="seungkyu.EmbeddedDbSqlRegistry">
        <constructor-arg ref="embeddedDatabase"/>
    </bean>

    <jdbc:embedded-database id="embeddedDatabase" type="H2">
        <jdbc:script location="classpath:schema.sql"/>
    </jdbc:embedded-database>

이렇게 embeddedDatabase의 빈을 또 만들고 그 빈으로 주입을 해줘야 내장데이터베이스로 설정이 된다.

 

트랜잭션 적용

만약 쿼리들이 등록되고 있는 중간에, 해당 쿼리를 조회하면 어떻게 될까?

반복문을 통해 등록하는 중간에 아직 등록되지 않은 쿼리를 조회할 수도 있을 것이다.

이런 이유로 각 메서드에 알맞은 트랜잭션 설정을 해줘야 한다.

 

우선 테스트 먼저 작성한다.


    @Test
    public void transactionTest(){
        Map<String, String> sqlmap = new HashMap<>();
        sqlmap.put("KEY1", "U_SQL1");
        sqlmap.put(UUID.randomUUID().toString(), UUID.randomUUID().toString());

        try
        {
            sqlRegistry.updateSql(sqlmap);
            Assertions.fail();
        }
        catch (RuntimeException e)
        {
            Assertions.assertTrue(
                    sqlRegistry.findSql("KEY1").equals("SQL1") &&
                            sqlRegistry.findSql("KEY2").equals("SQL2") &&
                            sqlRegistry.findSql("KEY3").equals("SQL3")
            );
        }
    }

 

중간에 없는 key로 조회를 하면 에러가 발생하는지 테스트이다.

그리고 만약 에러가 발생했다면 롤백도 되는지 테스트한다.

 

당연히 지금은 롤백이 되지 않아 실패한다.

 

트랜잭션은 마지막 부분에만 필요하기에

package seungkyu;

import org.jetbrains.annotations.NotNull;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

import javax.sql.DataSource;
import java.util.Map;

public class EmbeddedDbSqlRegistry implements UpdatableSqlRegistry {

    private final JdbcTemplate jdbcTemplate;
    private final TransactionTemplate transactionTemplate;

    public EmbeddedDbSqlRegistry(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
        this.transactionTemplate = new TransactionTemplate(new DataSourceTransactionManager(dataSource));
    }

    @Override
    public void updateSql(Map<String, String> sqlmap) {
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            protected void doInTransactionWithoutResult(@NotNull TransactionStatus status) {
                for(Map.Entry<String, String> entry : sqlmap.entrySet()) {
                    updateSql(entry.getKey(), entry.getValue());
                }
            }
        });
    }
}

해당 부분만 트랜잭션으로 감싸준다.

 

그리고 테스트를 수행해보면 다음과 같이 성공하는 것을 볼 수 있다.

+ Recent posts