-
Notifications
You must be signed in to change notification settings - Fork 0
최세민 6주차 학습일지 ‐ JSP cafe (2)
지금까지 개발을 하면서 스스로 의사결정하기 보다는 '일반적으로 좋은 것'을 선택하는 경우가 많았던 것 같습니다.
클린코드 책을 읽고 비즈니스 로직이 아무리 복잡해도 주석을 달지 않으려고 했습니다. 그리고 테스트 커버리지가 중요하다는 말을 듣고 '동작을 이해할 수 있고 오류를 발견할 수 있는 테스트 코드'가 아닌 '당장 커버리지가 높은 테스트 코드'를 작성하기도 했습니다.
하지만 비즈니스 로직이 복잡할 때에는 주석이 코드를 이해하는데 큰 도움이 될 수 있습니다. 또, 커버리지가 높지 않더라도 이해하기 쉽고, 오류를 쉽게 발견할 수 있는 좋은 테스트 코드를 작성할 수도 있습니다.
이런 깨달음을 통해 어떤 방법론을 정답으로 여기고 맹신하기 보다는 현재 상황에 맞는 최선의 방법을 적용하는 것이 옳다고 느꼈습니다.
마지막 프로젝트에서는 기술 스택을 선택하거나 개발 방법론을 적용할 때 '다들 이렇게 하니까' 가 아닌 '우리 프로젝트에는 이게 적합하니까' 라는 근거를 가지고 적용할 수 있도록 주체적인 의사결정을 하고자 노력하려 합니다.
게시글, 답글 기능을 구현하고 게시글 삭제를 Soft Deletion으로 변경하면서 트랜잭션 보장을 해줘야겠다는 생각이 들었습니다.
만약 단순하게 Connection 객체만을 가지고 트랜잭션을 보장하기 위해서는 둘 중 하나를 선택해야 할 것입니다.
- Service Layer에서 Connection에 의존하며, DAO 객체가 Connection을 인자로 전달 받음.
- 하나의 DAO 메소드에서 하나의 Connection으로 트랜잭션이 보장되어야 할 모든 로직 수행.
하지만 저의 프로젝트 구조에서 DAO 객체는 DataSource에 직접적인 의존성을 가지지 않았습니다.
public class JdbcTemplate {
private final DataSource dataSource;
public <T> T queryForObject(String sql, RowMapper<T> resultSetMapper, Object... values) {
try (Connection connection = dataSource.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
...
}
위와 같이 직접 만든 JdbcTemplate에 의존하고 있고, JdbcTemplate 객체가 DataSource를 통해 Connection을 관리하는 형태로 만들어져 있었습니다. 따라서, 위 1, 2번 모두 적용하기에 적절하지 않았습니다.
이를 위해 Transaction이 진행중인 Connection을 ThreadLocal 변수에 저장하고 트랜잭션 수행을 관리하는 TransactionManager를 구현했습니다.
private static final ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();
public static void executeTransaction(DataSource dataSource, int isolationLevel, Runnable runnable) {
try {
startTransaction(dataSource, isolationLevel);
runnable.run();
commit();
} catch (Exception e) {
try {
rollback();
throw e;
} catch (SQLException rollbackException) {
log.error(rollbackException.getMessage(), rollbackException);
}
} finally {
closeConnection();
}
}
트랜잭션은 위와 같은 템플릿을 통해서 진행될 수 있도록 하였습니다.
트랜잭션을 시작하면 autoCommit(false)
를 주고 ThreadLocal
에 커넥션을 저장합니다.
그런다음 트랜잭션이 보장되어야 하는 작업을 Runnable로 수행합니다.
작업이 문제없이 완료되면 트랜잭션을 commit()
하고, 예외가 발생하였다면 rollback()
합니다.
finally 구문에서 커넥션을 close()
하고 트랜잭션 작업을 마무리 합니다.
다음은 JdbcTemplate에서 트랜잭션 진행 여부에 따라서 적절한 Connection을 가져올 수 있도록 DataSourceUtils 클래스를 구현했습니다.
public class DataSourceUtils {
public static Connection getConnection(DataSource dataSource) {
...
}
public static void releaseConnection(Connection connection) {
....
}
}
해당 클래스는 getConnection
과 releaseConnection
public api를 가지고 있습니다.
getConnection
반환하려는 Connection이 Transaction 진행 중이라면 아무런 작업을 하지 않습니다.
그렇지 않으면 Connection을 close 합니다.
releaseConnetion 반환하려는 Connection이 Transaction 진행 중이라면 아무런 작업을 하지 않습니다. 그렇지 않으면 Connection을 close 합니다.
마지막으로 JdbcTemplate에서는 DataSourceUtils를 이용해 Connection을 얻고 쿼리를 수행할 수 있도록 코드를 수정했습니다.
public class JdbcTemplate {
public <T> T queryForObject(String sql, RowMapper<T> resultSetMapper, Object... values) {
Connection connection = DataSourceUtils.getConnection(dataSource);
....
}
}
이를 통해 트랜잭션을 보장할 수 있는 간단한 구조를 만들었습니다.
다음은 게시글을 Soft Deletion의 Transcation 작업에서 고려해야 할 점입니다.
- A 트랜잭션에서 삭제할 1번 게시글을 조회한다.
- A 트랜잭션에서 삭제할 1번 게시글의 삭제 조건을 검사한다. (검사 완료)
- 외부에서 다른 사용자가 1번 게시글에 답글을 작성한다.
- A 트랜잭션에서 게시글 및 답글을 모두 삭제한다.
이러한 문제를 해결하기 위해 제가 정의한 세부적인 요구사항은 게시글 삭제를 시도하는 동안은 새로운 답글을 작성할 수 없어야 합니다. 또한, 게시글 삭제가 완료 되었다면 해당 게시글에 요청한 답글 작성은 완료되지 못해야 합니다.
우선 A 트랜잭션에서 삭제할 게시글을 조회할 때, SELECT ... FOR UPDATE
쿼리로 배타락을 걸어서 관련된 레코드가 생성되거나 수정되지 못하도록 하였습니다.
하지만, A 트랜잭션에서 배타락을 걸어도 Soft Deletion이기 때문에, 답글 작성에 조건을 작성하지 않으면 락을 대기한 후 삭제된 글에 답글이 추가될 수 있습니다.
이를 고려해서 답글을 작성할 때 트랜잭션 작업으로 게시글의 상태가 activate 인지 확인 한 후 저장할 수 있게 구현하였습니다.