이 포스트는 김영한 이사님의 스프링 입문 강의를 듣고 작성하였습니다.
이제부턴 자바의 메모리가 아닌 데이터베이스에 직접 데이터를 넣어보는 실습을 진행할 것이다.
먼저 진행할 실습은 순수 JDBC를 이용하여 리포지토리를 만드는 것이다.
H2 설치 관련 세팅이나 sql문은 여기선 생략하겠다.
우선 build.gradle파일의 dependencies에 다음을 추가해주자.
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
위 사진과 같이 추가해준 다음 우측 상단의 코끼리 모양을 눌러주어야 한다. 그렇게 하면 관련 파일들이 설치가 된다.
그 다음 resources/application.properties 파일에 들어가서 밑에 세 줄을 추가해주어야 한다.
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa

url은 db접속주소이며 오류를 방지하기 위해 위와 같은 주소로 접속을 해주어야 한다.
기존에 MemberRepository의 구현체는 자바의 메모리를 사용한 MemortMemberRepository였지만 이번에는 Jdbc를 사용하여 직접 H2 데이터 베이스에 넣어주도록 구현하고자 한다.
package memberpractice.memberpractice.repository;
import memberpractice.memberpractice.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); }
}
코드에 관한 자세한 내용은 추후의 강의에서 자세히 설명해주신다고 하셔서 그때 자세히 다뤄보고 여기서는 간단하게 설명하고자 한다.
public JdbcMemberRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
먼저 DataSource라는 것을 주입받는 것을 확인할 수 있다. DataSource는 세팅에서
application.properties에서 추가해준 내용으로 스프링이 미리 생성해둔 것이다.
우리는 Datasource를 통해 db와의 connection을 얻을 수 있으며 이 connection에 우리가 직접 sql문을 날려서 데이터를 저장하고 얻어올 수 있는 것이다.
save의 동작 원리에 대해서 살펴보면 다음과 같다.
Save의 동작 원리
datasource로 부터getConnection을 통해connection을 얻어온다.connection에서prepareStatement안에 sql문을 넣는다.setString으로 값을 세팅해준다. 이 값은 sql문의?와 매칭된다.excuteUpdate를 하면 쿼리가 db로 날라간다.- 값이 있다면
pk값을 가져와member의 id로 세팅한다.- 동작이 끝나고 나면 연결을 종료해주어야 한다.
위의 전체코드를 살펴보면 중복되는 부분도 많고 예외처리도 신경쓸 사항이 많다. 이를 개선한 것이 다음 시간에 배울 Jdbc Template이라고 한다.
앞서 다룬 스프링 빈의 등록 방법에서 Configuraion을 통한 자바코드로 직접 스프링 빈을 등록하는 것을 다뤘었다. 이러한 등록 방법은 지금과 같이 변경이 필요한 상황에서 굉장히 유용하다.
package memberpractice.memberpractice;
import memberpractice.memberpractice.repository.JdbcMemberRepository;
import memberpractice.memberpractice.repository.MemberRepository;
import memberpractice.memberpractice.repository.MemoryMemberRepository;
import memberpractice.memberpractice.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);
}
}
먼저 스프링 빈에 등록한 MemberRepository의 구현체를 MemoryMemberRepository에서 JdbcMemberRepository로 변경하였다. 그 후 Jdbc에 필요한 DataSource를 스프링으로 부터 받아와 주입해주었다.
정상적으로 회원 저장과 조회가 정상적으로 동작하는 것을 확인해볼 수 있었다.
사실 이 강의는 스프링 입문이기 때문에 Jdbc같은 기술을 핵심적으로 다루지는 않는다.
이 강의에서 말하는 핵심은 스프링이 다형성을 활용하기 굉장히 편리하게 만들어준다는 것이다.
이번처럼 인터페이스로 만든 구현체를 바꿔끼는 것을 다형성을 활용한다고 말하곤 한다. 이러한 것을 스프링 컨테이너와 DI를 이용하여 굉장히 편하게 할 수 있다는 것이다.
이러한 방식이 아니라면 MemberService가 MemberRepository의 구현체에 대해서도 알고 있어야 한다.
따라서 MemberRepository의 구현체가 바뀌면 MemberService의 코드를 바꿔야 하는데 만약 이러한 Service가 수십개가 넘어가면 변경이 굉장히 힘들었을 것이다.
하지만, 우리는 Configuraion만 변경함으로써 기존코드의 변경없이 구현 클래스를 갈아낄 수 있었다. 이것이 스프링의 장점이라고 한다.
의존관계의 변경을 확인하면 다음과 같다.

이렇게 "기존 코드의 변경없이 기능의 확장이 이루어져야 된다. "고 말하는 것이 SOLID의 OCP, 개방 폐쇄의 원칙이라고 부른다. 이것에 대한 내용은 스프링 기본 강의에서 자세히 다뤄보고자 한다.