JDBC를 사용하면서 DriverManager에서 매번 DB로 접속하면서 새로운 커넥션을 얻어 사용했다. 그런데 이 방법은 개발자 입장에서도 URL, UserName, Password 등 반복되는 요소들이 많아 사용하기에 불편했다. 그렇기 때문에 DataSource라는 인터페이스를 이용해서 구현체를 할당하여 사용한다. 이렇게 하면 커넥션 풀의 경우, 매번 커넥션을 새로 생성하는 것이 아닌 미리 만들어둔 커넥션을 가지고 사용 후 반환하는 과정을 통해 빠르게 DB와 연결하고 질의문을 실행할 수 있다. 또한 커넥션 풀을 변경할 때도 다형성을 이용하여 내부 코드 변경없이 객체 할당 코드 부분만 변경하여 사용할 수 있다.
DataSource의 대표적인 구현체로는 Apache Commons DBCP의 BasicDataSource, Oracle의 UCP(Universal Connection Pool), HikariCP가 있다. 직접 테스트할 때는 HikariCP를 사용하였다.
@Data
@Builder
public class UserDto {
public String name;
public Integer money;
}
UserDto 클래스
public abstract class DBConst {
public static final String URL = "jdbc:h2:tcp://localhost/~/test";
public static final String USER_NAME = "sa";
public static final String PASSWORD = "";
}
DBConst 클래스
DB와 커넥션을 얻을 때 필요한 정보들을 상수로 선언한 부분으로 추상클래스를 이용하여 따로 인스턴스로 생성할 수 없도록 한다.
@Slf4j
@RequiredArgsConstructor
public class UserRepository {
private final DataSource dataSource;
public void save(UserDto userDto) throws SQLException {
String sql = "INSERT INTO USERS(name, money) VALUES(?, ?);";
Connection conn = null;
PreparedStatement pstmt = null;
int r = 0;
try {
conn = dataSource.getConnection();
log.info("conn = {}, class = {}", conn, conn.getClass());
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, userDto.name);
pstmt.setInt(2, userDto.money);
r = pstmt.executeUpdate();
} catch (SQLException e) {
throw e;
} finally {
if (r < 1) log.debug("저장 실패");
JdbcUtils.closeConnection(conn);
JdbcUtils.closeStatement(pstmt);
}
}
public UserDto findByName(String name) throws SQLException {
UserDto user = null;
String sql = "SELECT * FROM USERS WHERE name LIKE ?";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = dataSource.getConnection();
log.info("conn = {}, class = {}", conn, conn.getClass());
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, name);
rs = pstmt.executeQuery();
if (rs.next()) {
user = UserDto.builder()
.name(rs.getString("name"))
.money(rs.getInt("money"))
.build();
}
} catch (SQLException e) {
throw e;
} finally {
JdbcUtils.closeConnection(conn);
JdbcUtils.closeStatement(pstmt);
JdbcUtils.closeResultSet(rs);
}
return user;
}
public void deleteUser(String name) throws SQLException {
String sql = "DELETE FROM USERS WHERE name LIKE ?";
Connection conn = null;
PreparedStatement pstmt = null;
int r = 0;
try {
conn = dataSource.getConnection();
log.info("conn = {}, class = {}", conn, conn.getClass());
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, name);
r = pstmt.executeUpdate();
} catch (SQLException e) {
throw e;
} finally {
if (r < 1) log.debug("삭제 실패");
else log.debug("삭제 성공");
JdbcUtils.closeConnection(conn);
JdbcUtils.closeStatement(pstmt);
}
}
}
UserRepository 클래스
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public void save(UserDto userDto) throws SQLException {
userRepository.save(userDto);
}
public UserDto findByName(String name) throws SQLException {
return userRepository.findByName(name);
}
public void deleteUser(String name) throws SQLException {
userRepository.deleteUser(name);
}
}
UserService 클래스
@Slf4j
class UserServiceTest {
HikariDataSource dataSource = new HikariDataSource();
UserRepository userRepository = new UserRepository(dataSource);
UserService userService = new UserService(userRepository);
@BeforeEach
private void setConnectionPool() {
dataSource.setJdbcUrl(URL);
dataSource.setUsername(USER_NAME);
dataSource.setPassword(PASSWORD);
}
@Test
@DisplayName("회원 저장")
public void save() {
// given
UserDto user1 = UserDto.builder().name("둘리").money(50000).build();
// when
try {
userService.save(user1);
} catch (SQLException e) {
log.error("!error", e);
}
UserDto user2 = null;
try {
user2 = userService.findByName("둘리");
} catch (SQLException | NullPointerException e) {
log.error("!error", e);
}
// then
assertThat(user2).isNotNull().isEqualTo(user2);
}
@Test
@DisplayName("회원 삭제")
public void delete() {
// given
UserDto user = null;
try {
user = userService.findByName("둘리");
} catch (SQLException e) {
log.error("!error", e);
}
// when
try {
userService.deleteUser(user.name);
} catch (SQLException | NullPointerException e) {
log.error("!error", e);
log.error("회원 정보가 없습니다.");
}
// then
UserDto user3 = null;
try {
user3 = userService.findByName("둘리");
} catch (SQLException e) {
log.error("!error", e);
}
assertThat(user3).isNull();
}
}
테스트 실행 코드
데이터가 이미 있거나 없는 등의 케이스는 지금 중요한게 아니라서 항상 있거나 없는 상태에서 진행한다고 가정하고 테스트하였다.


커넥션 정보를 보면 conn0이 계속 사용된 것을 볼 수 있다. conn0을 사용하고 난 뒤에는 다시 커넥션 풀로 해당 커넥션을 반납하고 나중에 다시 사용할 때 해당 커넥션을 다시 사용하기 때문이다.
그러면 DriverManager를 사용했을 때는 다른 결과가 나올까?
//HikariDataSource dataSource = new HikariDataSource();
DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
UserRepository userRepository = new UserRepository(driverManagerDataSource);
UserService userService = new UserService(userRepository);
@BeforeEach
private void setConnectionPool() {
driverManagerDataSource.setUrl(URL);
driverManagerDataSource.setUsername(USER_NAME);
driverManagerDataSource.setPassword(PASSWORD);
//dataSource.setJdbcUrl(URL);
//dataSource.setUsername(USER_NAME);
//dataSource.setPassword(PASSWORD);
}
위의 테스트 코드 중에 HikariDatasource를 사용한 부분을 DriverManagerDataSource로 변경하였다. DriverManager가 아닌 DriverManagerDataSource를 이용한 이유는 DriverManager가 DataSource 인터페이스를 구현하지 않기 때문에 다형성을 이용해서 해당 클래스로 변경할 수 없기 때문이다.
위의 코드로 변경한 후 결과를 살펴보면


conn을 보면 숫자가 0부터 연결할 때마다 계속 증가하는 것을 알 수 있다.
'공부 > Spring' 카테고리의 다른 글
| [Spring Web] Spring MVC 컨트롤러 요청/응답 가능한 여러가지 방법 (0) | 2024.03.21 |
|---|---|
| [Spring Web] http body 내용 가져오기 (MapStruct 안 되는 이유) (0) | 2024.03.16 |
| [Spring Web] 컨트롤러에서 String만 리턴해도 되는 이유 (0) | 2024.03.13 |
| [Spring] IoC, Bean Factory, Applicatin Context (0) | 2024.02.18 |
| [Spring] 트랜잭션(Transaction) 과정 (0) | 2024.01.18 |