build.gradle
파일에 JDBC, h2 데이터베이스 관련 라이브러리 추가implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
application.properties
에 추가spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
MeberRepository
가 하지만MemoryMemberRepository
),JdbcMemberRepository
)에 대한 차이DateSource
는 데이터베이스 커넥션을 획득할 때 사용하는 객체DataSource
를 생성하고,📂 JdbcMemberRepository
public class JdbcMemberRepository implements MemberRepository{
// 📌 DB에 붙으려면 DataSource가 필요 -> 스프링에게 주입 받아야 함
// 📌 application.properties의 내용을 스프링부트가 DataSource 생성 및 주입
private final DataSource dataSource;
public JdbcMemberRepository(DataSource dataSource){
this.dataSource = dataSource;
}
@Override
public Member save(Member member) {
String sql = "insert into member(name) values(?)";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try{
conn = getConnection();
pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
// parameterIndex 1은 SQL의 ?와 매칭
pstmt.setString(1, member.getName());
pstmt.executeUpdate(); // DB에 쿼리(insert)가 날라감
rs = pstmt.getGeneratedKeys(); // 방금 생성된 KEY(1, 2, ..)를 반환
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 member where id = ?";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try{
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setLong(1, id);
// 조회는 executeUpdate()❌, executeQuery⭕
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 Optional<Member> findByName(String name) {
String sql = "select * from member 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);
}
}
@Override
public List<Member> findAll() {
String sql = "select * from member";
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);
}
}
// 📌 커넥션을 얻
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();
}
}
// 📌 dataSource.getConnection()❌, DataSourceUtils 권장 -> 트랜잭션 유지
// 📌 데이터베이스와 연결된 커넥션을 얻음
// 📌 진짜 데이터베이스와 연결되는 열린 소켓을 얻을 수 있음?**
private void close(Connection conn) throws SQLException{
DataSourceUtils.releaseConnection(conn, dataSource);
}
}
📂 SpringConfig
// 스프링이 application.properties를 보고 DataSource를 빈으로 자동 생성
@Configuration
public class SpringConfig {
private DataSource dataSource;
// DI, 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);
}
}
JdbcMemberRepository
는 JDBC를 직접 사용하는 레포지토리 구현의 전형적인 패턴String sql = "select * from member where id = ?";
String
으로 선언?
를 넣고, 나중에 바인딩conn = getConnection();
DataSource
로부터 커넥션을 가져옴DataSourceUtils.getConnection()
사용해야 함pstmt = conn.prepareStatement(sql);
pstmt.setLong(1, id);
PreparedStatement
를 통해 SQL 실행 준비setXxx()
로 파라미터 바인딩 (1
은 ?의 인덱스)rs = pstmt.executeQuery(); // 조회
rs = pstmt.executeUpdate(); // 삽입, 수정, 삭제
if(rs.next()){
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
ResultSet
을 객체로 변환하는 단계 (Manual ORM)getter
호출} catch (Exception e){
throw new IllegalStateException(e);
}
SQLException
을 런타임 예외로 감싸서 던짐DataAccessException
을 사용하는 경우도 많음close(conn, pstmt, rs);
ResultSet → Statement → Connection
JdbcMemberRepository
- Repository 계층DataSource
객체가 필요한데,SpringConfig
에서 설정된 DataSource
가 전달됨SpringConfig
- 설정 및 의존성 주입 관리MemberService
→ MemberRepository
→ DataSource
순서로 의존관계 주입application.properties
- 설정파일application.properties
를 기반으로 DataSource
객체가 빈으로 자동 생성JdbcMemberRepository
와 MemberService
가 빈으로 등록MemberService
에서 memberRepository
를 호출하면,DataSourceUtils
를 통해 커넥션 풀을 관리하며,MemoryMemberRepository
에서 JdbcMemberRepository
로 갈아끼우기