12. 순수 JDBC
스프링의 JDBC 기능을 활용하여 프로그램과 DB를 연동해본다.
순수 JDBC는 자바 코드 상에서 직접 sql 쿼리를 생성해 DB에게 전달하는 것이다.
데이터베이스 관리자와 어플리케이션 개발자는 사실상 다른 직무이기 때문에,
어플리케이션 측에서 sql 쿼리를 직접 생성해 조회하는 것은 그다지 좋지 못한 개발 방식이다.
또한 JDBC를 이용한 DB 연동 방식은 오늘날 잘 사용되지 않는다. 복잡하고 어렵기 때문이다.
강의에서도 JDBC를 이용한 프로그래밍은 기본적인 구성만 간단히 알아보고 나중에 필요할 때 자세히 찾아보기를 추천하였다.
스프링 JDBC 연동 설정
build.gradle 파일에 jdbc, h2 데이터베이스 관련 라이브러리를 추가한다.
build.gradle 파일을 수정한 뒤에는 항상 우측 위의 코끼리 모양 버튼(Load Gradle Changes)을 눌러야 반영된다.
추가할 라이브러리
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
application.properties 파일에 데이터베이스 연결 설정을 추가한다.
application.properties
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
url 내용은 h2 데이터베이스에 접속할 때 사용하는 url과 같다.
JdbcMemberRepository 구현
Jdbc 기반으로 동작할 리포지토리의 코드를 작성한다.
비교적 길고 복잡하다. 지금 시점에서 깊게 분석할 필요는 없다.
JdbcMemberRepository.java
package hdxian.hdxianspring.repository;
import hdxian.hdxianspring.domain.Member;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceUtils;
import javax.sql.DataSource;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class JdbcMemberRepository implements MemberRepository {
// Jdbc 기반 리포지토리는 DataSource를 의존. DB 접속정보 등이 들어있음.
private final DataSource dataSource;
@Autowired
public JdbcMemberRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
public Member save(Member member) {
String sql = "insert into member2(name) values(?)";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql,
Statement.RETURN_GENERATED_KEYS);
pstmt.setString(1, member.getName());
pstmt.executeUpdate();
rs = pstmt.getGeneratedKeys();
if (rs.next()) {
member.setId(rs.getLong(1));
} else {
throw new SQLException("id 조회 실패");
}
return member;
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
@Override
public Optional<Member> findById(Long id) {
String sql = "select * from member2 where id = ?";
Connection conn = null;
PreparedStatement pstmt = null; ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setLong(1, id);
rs = pstmt.executeQuery();
if(rs.next()) {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return Optional.of(member);
} else {
return Optional.empty();
}
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
@Override
public List<Member> findAll() {
String sql = "select * from member2";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
rs = pstmt.executeQuery();
List<Member> members = new ArrayList<>();
while(rs.next()) {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
members.add(member);
}
return members;
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
@Override
public Optional<Member> findByName(String name) {
String sql = "select * from member2 where name = ?";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, name);
rs = pstmt.executeQuery();
if(rs.next()) {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return Optional.of(member);
} return Optional.empty();
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
private Connection getConnection() {
return DataSourceUtils.getConnection(dataSource);
}
private void close(Connection conn, PreparedStatement pstmt, ResultSet rs) {
try {
if (rs != null) {
rs.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (pstmt != null) {
pstmt.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (conn != null) {
close(conn);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
private void close(Connection conn) throws SQLException {
DataSourceUtils.releaseConnection(conn, dataSource);
}
}
DataSource
- 데이터베이스 커넥션을 획득할 때 사용하는 객체.
- 스프링 부트는 데이터베이스 커넥션 정보를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어둠.
즉 DI받아 사용할 수 있음.
datasource.getConnection()
- DB와의 연결을 활성화. (소켓 정보 등을 가지고 온다.)
close()
- 데이터 조회가 완료되면 반드시 Conn을 끊어야 한다.
끊지 않으면 리소스를 계속 잡아먹다가 무슨 일이 일어날지 모른다.
DB가 장애난다고? 뉴스에 나오고 싶습니까?
스프링 설정 변경
중요한건 이 부분이다.
SpringConfig 파일에 Bean으로 등록된 MemberRepository() 부분을 변경한다.
SpringConfig.java
package hdxian.hdxianspring;
import hdxian.hdxianspring.repository.JdbcMemberRepository;
import hdxian.hdxianspring.repository.MemberRepository;
import hdxian.hdxianspring.repository.MemoryMemberRepository;
import hdxian.hdxianspring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class SpringConfig {
private final DataSource dataSource;
@Autowired
public SpringConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
// return new MemoryMemberRepository()
return new JdbcMemberRepository(dataSource);
}
}
h2 데이터베이스를 실행하고, localhost:8080으로 접속하면 프로그램이 정상적으로 동작한다.
다만, 이전의 MemoryMemberRepository는 회원 정보를 그냥 프로그램 상의 Map 인스턴스에 저장했기 때문에
서버를 내렸다 올리면 데이터가 모두 날아갔지만,
JdbcMemberRepository는 DB에 데이터를 저장하므로 서버를 껐다 켜도 데이터가 그대로 남아있다.
회원 조회 기능
회원 가입 기능
지금까지의 코드를 보면, 다른 코드 (Service, Controller)는 일절 건드리지 않고,
SpringConfig에서 memberRepositroy()가 리턴하는 구현 클래스만 변경하였다.
그 결과 코드 프로그램 기능(회원가입, 조회)은 변경되지 않았지만,
내부 데이터 저장 방식을 메모리에서 DB로 변경하였다. 내부 구조가 완전히 바뀐 것이다.
즉, 스프링의 DI(Dependency Injection)을 사용하면 기존 코드를 변경하지 않고 내부 구조를 변경할 수 있다.
이 같은 특성을 객체지향 프로그래밍의 원칙 중 하나인 개방-폐쇄 원칙(OCP, Open-Closed Principle)이라 한다.
주로 확장에는 열려있고, 수정에는 닫혀있다고 정의되며,
기존 코드를 전혀 손대지 않고 새로운 기능을 추가하거나 변경할 수 있어야 함을 의미한다.