728x90

들어가며,

DB에 접근하며 데이터를 저장하고 호출하는 과정에서 수많은 예외가 발생합니다.

개발자는 발생할 수 있는 예외에 대해 미리 지정하여 안정적으로 어플리케이션이 동작할 수 있도록 처리해야 합니다.

예외 계층

예외 종류

체크 예외 : 

  • SQLException: 데이터베이스에 대한 SQL 쿼리 수행 중 문제가 발생한 경우.
  • IOException: 데이터베이스 연결 또는 파일 입출력과 관련된 문제가 발생한 경우.

언체크 예외 : 프로그램 실행 중에 발생할 수있는 예외 

  • NullPointerException (NPE): 객체 참조가 없는 상태에서 메소드를 호출할 때
  • IllegalArgumentException: 메소드에 전달된 인수가 잘못된 경우

예외 규칙

 

기본적인 MVC 구조에서 예외는 Repository에서 발생하게 되며, 예외는 Repository를 호출한 Service에서 처리를 하거나 다시 던지거나 선택을 할 수 있습니다.

처리를 하는 것은 Catch로 잡고 던질 경우에는 Throw로 던지면 됩니다.

 

Catch

@Test
void checked_catch() {
	Service service = new Service();
    service.callCatch();
    
    static class MyCheckedException extends Exception {
        public MycheckedException(String message) {
            super(message);
        }
    }

    public void callCatch() {
        try {
            repository.call();
        } catch (MyCheckedException e) {
            log.info("예외 처리, message={}", e.getMessage(), e);
        }
    }

    static class Repository {
        public void call() throw MyCheckedException {
            throw new MyCheckedException("ex");
        }
    }
}
catch(MycheckedException e)를 통해 MycheckedException 예외를 잡아 처리하는 Test를 진행했습니다.
해당 예외 처리에서는 log.info를 통해 예외에 대한 정보를 기록하게 됩니다.

 

Throw

@Test
void checked_throw() {
	Service service = new Service();
   	assertThatThrowBy(() -> service.callThrow())
    		.isInstanceOf(MyCheckedException.class);
}

static class MyCheckedException extends Exception {
	public MyCheckedException (String message) {
    	super(message);
    }
}

static class Service {
	Repository repository = new Repository();
    
    public void callThrow() throws MyCheckedException {
    	repository.call();
    }
}

static class Repository {
	public void call() throws My checkedException {
    	throw new MyCheckedException("ex");
        }
    }
}

 

예외를 던지는 경우, assertj의 메서드를 통해 예외를 검증했습니다.
() -> service.callThrow()는 해당 메서드를 호출하는 람다식을 정의합니다.
.isInstanceOf(MyCheckedException.class)에서는 ()안의 예외 유형의 인스턴스와 같은지 검증하게 됩니다.
따라서 service에서 호출한 callThrow 메서드의 결과가 MycheckedException의 인스턴스와 같아야만 테스트가 성공합니다. 

 

체크 예외와 언체크 예외 비교

체크 예외에서는 반드시 던지거나 처리를 해야만 했습니다. 그렇지 않을 경우 시스템이 종료되는 문제가 발생하기 때문입니다. 반면, 언체크 예외는 컴파일러가 예외를 체크하지 않기 때문에 생략이 가능합니다. 생략할 경우에는 자동으로 예외를 던지게 됩니다.

 

실제 적용 시에는?

기본적으로 언체크 예외를 사용하는 것이 좋습니다. 체크 예외는 비즈니스 로직상 의도적으로 던지는 경우에만 사용하는 것이 좋습니다.

 

상위 트리에 있는 Exception을 던지면?

결론을 먼저 말하면 throws Exception을 통해 모든 예외를 던지는 방법은 선택해서는 안됩니다. 앞서 실제 적용 시에 체크 예외는 의도적으로 던지는 경우에 사용하는 것이 좋은데, 모든 예외를 던지게 되면 어떤 예외를 잡고 던지는 것인지 불명확해지기 때문입니다.

 

기존 예외를 포함한 후 스택 트레이스 출력

로그를 출력할 때 마지막 파라미터에 기존 예외를 꼭 포함해야 한다.

log.info("message={}", "message", ex)` , 여기에서 마지막에 `ex` 를 전달하는 것을 확인할 수 있다.

728x90

들어가며,

 

JdbcTemplate은 스프링 프레임워크에서 제공하는 JDBC의 간소화된 버전으로, 템플릿 콜백 패턴을 사용하여 반복적인 JDBC 코드를 더 간편하게 작성할 수 있도록 도와줍니다.

JdbcTemplate이 제공하는 메서드들은 내부적으로 Connection, Statement, ResultSet 등의 자원을 생성하고 관리하여, 이러한 자원을 직접 다룰 필요가 없습니다.

 

템플릿 콜백 패턴?

템플릿 콜백 패턴은 클래스의 메서드가 호출되면서 특정 동작을 수행하는 디자인 패턴입니다.

JDBC Template에서는 JdbcTemplate 클래스를 사용하며, 이 패턴은 주로 execute() 메서드와 함께 사용됩니다.

 

Row Mapper를 콜백 인터페이스로 사용해, DB에서 조회한 결과를 객체에 매핑하는 역할을 합니다.

 

import org.springframework.jdbc.core.RowMapper;
import java.sql.ResultSet;
import java.sql.SQLException;

public class EmployeeRowMapper implements RowMapper<Employee> {

    @Override
    public Employee mapRow(ResultSet resultSet, int rowNum) throws SQLException {
        Employee employee = new Employee();
        employee.setId(resultSet.getLong("id"));
        employee.setName(resultSet.getString("name"));
        employee.setSalary(resultSet.getDouble("salary"));
        // 필요한 다른 속성들 설정
        return employee;
    }
}

 

기존의 JDBC에서 ResultSet을 사용해 처리하던 부분을 Row Mapper를 통해 해결한다.
// 반복문을 통해 ResultSet 결과 처리
while (resultSet.next()) {
    // 결과 처리
    String columnValue = resultSet.getString("column_name");
    // 추가로 처리할 작업 수행
}

// rowMapper를 이용해 구현
while(resultSet 이 끝날 때까지) {
rowMapper(rs, rowNum)
}

 

Statement는 어떻게 대신 처리해주나?

update(): INSERT, UPDATE, DELETE 등의 쿼리를 실행할 때 사용됩니다. 내부적으로 Statement 관리를 처리합니다.

String sql = "update item set item_name=?, price=?, quantity=? where id=?";
template.update(sql,
    itemName,
    price,
    quantity,
    itemId);

 

추가로 batchUpdate()메서드를 이용하면 여러 개의 SQL 문을 일괄로 처리할 수 있습니다.

update 메서드를 이용해서 파라미터를 바인딩하여 SQL문을 처리할 수 있습니다.
그러나, 이렇게 순서대로 바인딩 하는 경우, 순서의 변경으로 인해 데이터 장애가 발생할 위험이 있습니다.

NamedParameterJdbcTemplate

이름을 지정해서 바인딩하여 위와 같은 문제를 해결할 수 있습니다.

@Slf4j
@Repository
public class JdbcTemplateItemRepositoryV2 implements ItemRepository {
    
    private final NamedParameterJdbcTemplate template;
    
    public JdbcTemplateItemRepositoryV2(DataSource dataSource) {
    	this.template = new NamedParameterJdbcTemplate(dataSource);
    }
    
    @Override
    public Item save(Item item) {
        String sql = "insert into item (item_name, price, quantity) values (:itemName, :price, :quantity)";
        SqlParameterSource param = new BeanPropertySqlParameterSource(item);
        KeyHolder keyHolder = new GeneratedKeyHolder();
        template.update(sql, param, keyHolder);
        
        Long key = keyHolder.getKey().longValue();
        item.setId(key);
        return item;
    }

 

이름 지정 파라미터를 사용하는 방식에서는 Map과 같이 Key, Value 데이터 구조로 전달해야 합니다.

자주 사용하는 종류는 아래와 같습니다.
Map
SqlParameterSource
 - MapSqlParameterSource
 - BeanPropertySqlParameterSource

 

Map은 단순히 Map<String, Object>를 Map.of("id", id)로 받아 param 으로 사용합니다.

MapSqlParameterSource는 .addValue를 통해 부분적으로 조정이 가능하다. Update()메서드와 같은 경우에 사용이 가능합니다.

대부분의 경우는 BeanPropertySqlParameterSource를 사용합니다.

이는 데이터 값을 자동으로 생성해주기 때문에 

 

SimpleJdbcInsert

private final SimpleJdbcInsert jdbcInsert;
public JdbcTemplateItemRepositoryV3(DataSource dataSource) {
    this.template = new NamedParameterJdbcTemplate(dataSource);
    this.jdbcInsert = new SimpleJdbcInsert(dataSource)
    .withTableName("item")
    .usingGeneratedKeyColumns("id");
public Item save(Item item) {
    SqlParameterSource param = new BeanPropertySqlParameterSource(item);
    Number key = jdbcInsert.executeAndReturnKey(param);
    item.setId(key.longValue());
    return item;
}

 

jdbcInsert.excuteAndReturnKey(param)을 사용해 Insert SQL을 실행할 수 있습니다.

 

 

728x90

들어가며,

2024.01.13 - [Spring Framework/JDBC, Mybatis,JPA, Querydsl] - JDBC의 이해, JDBC란? 개념

 

JDBC의 이해, JDBC란? 개념

JDBC란? JDBC는 자바에서 데이터베이스에 접속할 수 있도록 하는 자바 API다. SQL Command를 보내면 Result Set 응답을 받는 식으로 동작하고 있다. 그러나 JDBC를 통해 데이터베이스에 접속할 수는 있지만,

wooltech.tistory.com

 

JDBC를 통해 DB에 접근하면서 Conntection -> Statement -> ResultSet으로 동작하는 것에 대해 공부했습니다.

DB에 접근할 때마다 Connection을 생성해 연결하고, 연결을 종료하는 과정을 거쳐야 했습니다.

그러나, 이러한 방식은 DB에 자주 접근하는 경우에 리소스 사용과 관련해 많은 단점이 있습니다.

 

이번 편에서는 위 문제를 해결하기 위한 Connectio Pool이라는 개념에 대해 정리하려고 합니다.

 

Connection Pool 이란?

 

Connectio Pool이란 미리 Pool에 Connection을 생성해두고 필요한 경우에 꺼내 사용하는 것을 말합니다.

이 Connection들은 이미 DB와 연결이 되어 있기 때문에, Driver를 통해 꺼내서 SQL를 전달할 수 있습니다.

사용이 완료된 후에도 이전처럼 연결을 종료하는 것이 아닌 그대로 Pool에 반환하게 됩니다.

이로써 연결된 Connection들을 지속적으로 사용할 수 있습니다.

 

Spring Boot에서는 기본적으로 HikariCP를 통해 Connection Pool을 관리하고 있습니다.

 

Data Source란?

 

Data Source는 Connection을 조회하는 역할을 합니다.

하단의 검은색 박스처럼 Connection을 획득하는데는 다양한 방법이 있습니다. 

항상 JAVA에서는 이런 여러 방식의 사용에 유연함을 주기 위해 추상화를 통해 해결하고 있습니다.

 

Driver Manager와 Data Source 사용 비교

@Slf4j
public class ConnectionTest {
    @Test
    void driverManager() throws SQLException {
        Connection con1 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
        Connection con2 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
        log.info("connection={}, class={}", con1, con1.getClass());
        log.info("connection={}, class={}", con2, con2.getClass());
    }
    @Test
    void dataSourceDriverManager() throws SQLException {

	DriverManagerDataSource dataSource = new DriverManagerDataSource(URL,
        USERNAME, PASSWORD);
        useDataSource(dataSource);
    }
    private void useDataSource(DataSource dataSource) throws SQLException {
        Connection con1 = dataSource.getConnection();
        Connection con2 = dataSource.getConnection();
        log.info("connection={}, class={}", con1, con1.getClass());
        log.info("connection={}, class={}", con2, con2.getClass());
    }
}

 

Driver Manager는 항상 URL, USERNAME, PASSWORD를 입력하여 커넥션을 획득하지만, 
Data Source는 처음에 Driver Manager Data Source 객체를 생성할때만 파라미터를 넘겨주고나면 커넥션은 get메서드를 통해 호출할 수 있다.

 

 

 

728x90

JDBC란?

JDBC는 자바에서 데이터베이스에 접속할 수 있도록 하는 자바 API다.

 

 

SQL Command를 보내면 Result Set 응답을 받는 식으로 동작하고 있다.
그러나 JDBC를 통해 데이터베이스에 접속할 수는 있지만, MySQL, Oracle 등 많은 종류의 DB가 존재하고 각 사용법이 달라 DB를 바꾸게 되면 쿼리를 새로 작성해야 하는 번거로움이 있다.

따라서 SQL쿼리를 작성하면, 각 DB 성질에 맞게 변환해주는 SQL Mapper가 등장했다. Mybatis, Jdbc Template이 그 예시다. 그 후로 SQL쿼리마저 작성해주는 JPA의 등장, 동적 쿼리도 만들어주는 Querydsl까지 발전해온 내용을 기록하려고 한다.

 

데이터베이스를 변경하게 된다면?

각 DB는 어플리케이션과 Connection을 통해 연결된 후에 SQL과 결과를 주고 받으며 통신한다.

만약 데이터베이스를 변경하는 경우에는 코드를 모두 리팩토링해야 하는 일이 생기게 된다. 이를 해결하기 위해 등장한 것이 JDBC 표준 인터페이스다.

 

DB 연결 환경 설정

package hello.jdbc.connection;

public abstract class ConnectionConst {
    public static final String DB_URL = "jdbc:h2:tcp://localhost/~/test";
    public static final String DB_USER = "sa";
    public static final String DB_PASSWORD = "";
}
@Log4j2
public class DBConnectionUtil {

    public static Connection getConnection() {
        try {
            Connection connection = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
            log.info("connection = {}, class = {}", connection, connection.getClass());
            return connection;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}

 

별도의 클래스를 생성해 어플리케이션이 DB에 접근할 수 있도록 Connection을 연결한다.

DriverManager는 각 DB에 맞게 커넥션 연결을 도와주는 역할을 한다.
기입된 url, user, password 등의 정보를 전달하고 커넥션을 반환한다.

테스트 코드 작성

@Slf4j
class DBConnectionUtilTest {
    @Test
    void connection() {
        Connection connection = DBConnectionUtil.getConnection();
        assertThat(connection).isNotNull();
	}
}

 

위에서 작성한 DBConnectionUtil을 통해 Connection이 제대로 동작하는지 테스트해 볼 수 있다.

 

Connection 이후에 Statement와 ResultSet

 

Connection으로 연결이 되면, Statement 객체를 생성해서 SQL 쿼리를  전달하게 된다.
전달 받은 SQL 쿼리문은 ResultSet 객체에서 결과 반환을 위해 동작하게 되는데, ResultSet은 Cursor를 이용하는 동작하는 방식을 따른다.
// DriverManager를 통해 Connection 연결
Connection connection = DriverManager.getConnection(url, username, password);

// Connection 객체를 통해 Statement 객체 생성
Statement statement = connection.createStatement();

//executeQuery메서드를 통해 SQL쿼리를 전달하고 ResultSet 생성
String sqlQuery = "SELECT * FROM your_table";
ResultSet resultSet = statement.executeQuery(sqlQuery);

// 반복문을 통해 ResultSet 결과 처리
while (resultSet.next()) {
    // 결과 처리
    String columnValue = resultSet.getString("column_name");
    // 추가로 처리할 작업 수행
}

// 연결 종료 : 마지막에 동작한 순서대로 연결을 종료
resultSet.close();
statement.close();
connection.close();​


Cursor를 통해 row를 돌며 동작하는 ResultSet

 

TODO: 예외 처리 및 트랜잭션 관리 등을 고려해야 한다.

 

 

+ Recent posts