JDBC 방법은 상당히 오래된 방법으로 잘사용하지 않지만 여러가지 방법들을 비교하며 학습하기 위해 JDBC방법으로 DB를 연결해보자.
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
JdbcMemberRepository 생성한다. MemberRepository를 상속하고 메서드들을 override 해준다. 강의해주시는 영한님도 참고만 하라고 하셨기에 코드에대한 특별한 설명은 하지 않겠다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
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 {
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);
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 member 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 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);
}
}
@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);
}
}
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);
}
}
한가지 더 해줄것이 남았는데, springConfig 파일을 수정해줘야 한다.
원래 사용하던 MemoryMemberRepository를 주석처리하고 방금 만든 JdbcMemberRepository를 return 해준다.
springboot가 dataSource를 연결하게하고 생성자를 통해 di해주는 코드도 작성한다.
그 후 localhost:8080에서 회원가입 및 회원목록을 확인해보면 데이터베이스가 잘 연결되있는것을 확인 할 수 있다.
package hello.hellospring;
import hello.hellospring.repository.JdbcMemberRepository;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import hello.hellospring.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 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);
}
}
여기서 스프링을 쓰는 이유를 알 수 있다.
객체지향적인 설계가 좋은 이유이기도 한데, 다형성을 활용한 인터페이스를 두고 구현체를 바꿔끼기같은 작업등을 하기에 좋게되있다.
이걸 편리하게 사용하기 위해 스프링 컨테이너가 di같은 것을 지원해준다.
그리하여 기존의 코드 수정없이 SpringConfig파일만 즉, 애플리케이션을 조립하는 코드만 수정하면된다.
그림을 통해 살펴보자.
MemberService는 MemberRepository를 의존하고 있고, MemberRepository는 구현체로 2개의 레포지토리로 되어있다.

그런데 스프링 컨터이너에서 jdbc로 바꿔서 등록해주었다. 다른 코드는 변경하지 않았지만 다형성의 성질을 이용하여 이를 사용하게 된다.

이를 개방-폐쇄 원칙(OCP, Open-Closed Principle)이라고 한다.
확장에는 열려있고, 수정&변경에는 닫혀있다는 뜻이다.
인터페이스 기반의 다형성. 즉, 객체 지향에서의 다형성의 개념을 활용하면 기능을 완전히 변경한다 하더라도 application 전체의 코드를 수정하지 않아도 된다는 것이다.
이제 데이터를 DB에 저장하므로 스프링 서버를 다시 실행해도 데이터가 안전하게 저장된다.