[스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술] 섹션 7. 스프링 DB 접근 기술
“스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술” 강의 바로가기
섹션 7. 스프링 DB 접근 기술
- H2 데이터베이스 설치: 개발과 테스트 용도로 가볍고 편리한 최고의 DB 환경 구축
- 순수 Jdbc: 자바에서 DB에 연결하기 위한 가장 원초적인 방식 이해
- 스프링 통합 테스트: 스프링 컨테이너와 DB를 연결한 실제 구동 테스트 수행
- 스프링 JdbcTemplate: 반복적인 코드를 제거하여 Jdbc 사용성을 높인 라이브러리 활용
- JPA: SQL 중심의 개발에서 객체 중심의 개발로 패러다임을 전환하는 핵심 기술
- 스프링 데이터 JPA: JPA를 한 번 더 감싸 혁신적으로 코드를 줄여주는 현대적인 방식
20. H2 데이터베이스 설치
h2database.com에 접속하여 최신 버전의 플랫폼 독립적(All Platforms) 압축 파일을 다운로드한다.

다운로드한 파일의
bin디렉토리로 이동하여h2.sh파일에 실행 권한(chmod 755 h2.sh)을 부여한 뒤 실행한다. (cf. 윈도우 사용자는h2.bat을 실행한다.)
브라우저에 실행된 콘솔에서 JDBC URL을
jdbc:h2:~/test로 설정하여 최초 연결을 시도한다. 이후 사용자 홈 디렉토리에test.mv.db파일이 생성된 것을 확인한다.

회원 정보를 저장하기 위한
MEMBER테이블을 생성한다.id는 시스템이 자동으로 생성하는generated by default as identity전략을 사용한다.
테이블 생성하기
21. 순수 JDBC
환경 설정
Jdbc 리포지토리 구현
주의! 이렇게 JDBC API로 직접 코딩하는 것은 20년 전 이야기이다. 따라서 고대 개발자들이 이렇게 고생하고 살았구나 생각하고, 정신건강을 위해 참고만 하고 넘어가자.
- Jdbc 회원 리포지토리 -
src/main/java/hello.hello_spring/repository/JdbcMemberRepository
(캡쳐하기에는 양이 많아 코드만 적겠다.)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
package hello.hello_spring.repository;
import hello.hello_spring.domain.Member;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.stereotype.Repository;
import javax.sql.DataSource;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Repository
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);
}
}
스프링 설정 변경 -
src/main/java/hello.hello_spring/SpringConfig
⇒ DataSource는 데이터베이스 커넥션을 획득할 때 사용하는 객체다. 스프링 부트는 데이터베이스 커넥션 정보를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어둔다. 그래서 DI를 받을 수 있다.구현 클래스 추가 이미지
⇒ MemberRepository인터페이스를 상속받아 실제 DB와 연동하는JdbcMemberRepository구현체를 새롭게 추가한 클래스 다이어그램이다. 인터페이스를 분리함으로써 서비스 계층의 코드 수정 없이 저장소 기술을 확장할 수 있는 구조를 갖췄다.스프링 설정 이미지
⇒ 스프링 컨테이너 내에서 의존관계가 재설정되는 과정을 시각화한 자료다. 기존 MemoryMemberRepository와의 연결을 끊고, 새롭게 등록된JdbcMemberRepository를MemberService에 주입(Injection)하여 런타임 시점에 저장소 교체가 이루어지는 것을 보여준다.- 개방-폐쇄 원칙(OCP, Open-Closed Principle)
- 확장에는 열려있고, 수정, 변경에는 닫혀있다.
- 스프링의 DI (Dependencies Injection)을 사용하면 기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경할 수 있다.회원을 등록하고 DB에 결과가 잘 입력되는지 확인하자.
- 데이터를 DB에 저장하므로 스프링 서버를 다시 실행해도 데이터가 안전하게 저장된다.
22. 스프링 통합 테스트
스프링 컨테이너와 DB까지 연결한 통합 테스트를 진행해보자.
회원 서비스 스프링 통합 테스트
⇒ 스프링 부트 환경에서 실행되는 통합 테스트 코드다. @SpringBootTest를 통해 실제 애플리케이션 컨텍스트를 로드하며, 서비스와 리포지토리를 직접 주입받아 테스트를 수행한다.@SpringBootTest: 스프링 컨테이너를 실제 구동하여 테스트를 진행하도록 지원한다. 이를 통해 의존성 주입(DI) 등 스프링의 모든 기능을 테스트 환경에서 활용할 수 있다.@Transactional: 테스트 케이스에 적용 시 테스트 시작 전에 트랜잭션을 시작하고, 테스트 완료 후 데이터베이스를 항상 롤백(Rollback) 처리한다. 이로 인해 실제 DB에 데이터가 적재되지 않아, 각 테스트 간의 독립성을 보장하고 반복적인 테스트 수행이 가능해진다.
23. 스프링 Jdbc Template
→ 순수 Jdbc와 동일한 환경설정을 하면 된다. → 스프링 JdbcTemplate과 MyBatis 같은 라이브러리는 JDBC API에서 본 반복 코드를 대부분 제거해준다. 하지만 SQL은 직접 작성해야 한다.
스프링 JdbcTemplate 회원 리포지토리 -
src/main/java/hello.hello_spring/repository/JdbcTemplateMemberRepository
⇒ 인터페이스인 MemberRepository를JdbcTemplate기반으로 구현한 클래스다.RowMapper를 통해 조회된 데이터를 객체로 변환하며, 순수 JDBC 대비 훨씬 간결해진 코드를 확인할 수 있다.JdbcTemplate을 사용하도록 스프링 설정 변경
⇒ 애플리케이션의 설정 파일(SpringConfig)에서 기존 리포지토리 대신JdbcTemplateMemberRepository를 스프링 빈(Bean)으로 등록하는 과정이다. 이를 통해 서비스 코드의 변경 없이 런타임 시점에 저장소 기술이 교체된다.
24. JPA
→ JPA는 기존의 반복 코드는 물론이고, 기본적인 SQL도 JPA가 직접 만들어서 실행해준다. → JPA를 사용하면, SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환을 할 수 있다. → JPA를 사용하면 개발 생산성을 크게 높일 수 있다.
build.gradle 파일에 JPA, h2 데이터베이스 관련 라이브러리 추가
⇒ spring-boot-starter-data-jpa는 내부에 jdbc 관련 라이브러리를 포함한다. 따라서 jdbc는 제거해도 된다.- 스프링 부트에 JPA 설정 추가 -
resources/application.properties
show-sql: JPA가 생성하는 SQL을 출력한다.ddl-auto: JPA는 테이블을 자동으로 생성하는 기능을 제공하는데none를 사용하면 해당 기능을 끈다.create를 사용하면 엔티티 정보를 바탕으로 테이블도 직접 생성해준다.
JPA 엔티티 매핑 -
src/main/java/hello.hello_spring/domain/Member
⇒ @Entity어노테이션을 사용하여 자바 객체와 DB 테이블을 매핑한다.@Id와@GeneratedValue를 활용해 PK(Primary Key) 생성 전략을 데이터베이스에 위임(Identity 전략)하도록 설정한 코드다.JPA 회원 리포지토리 -
src/main/java/hello.hello_spring/repository/JpaMemberRepository
⇒ JPA의 핵심인 EntityManager를 주입받아 데이터를 관리한다.persist,find등 내장 메서드를 사용하여 SQL 작성 없이 데이터를 저장 및 조회하며, 복잡한 조회는 JPQL(Java Persistence Query Language)을 사용한다.- JPA를 사용하도록 스프링 설정 변경 -
src/main/java/hello.hello_spring/SpringConfig
⇒ 스프링 컨테이너에 EntityManager를 주입하고, 기존의JdbcTemplateMemberRepository대신JpaMemberRepository를 스프링 빈으로 등록하여 의존관계를 교체한다.
25. 스프링 데이터. JPA
스프링 데이터 JPA는 JPA를 한 단계 더 추상화하여, 구현 클래스 없이 인터페이스만으로 리포지토리를 완성할 수 있게 해주는 강력한 프레임워크다. 반복적인 CRUD 로직을 프레임워크가 자동화하므로 개발자는 비즈니스 로직에 온전히 집중할 수 있다. 실무 현대 자바 백엔드 개발에서 관계형 데이터베이스를 사용한다면 필수적인 선택이다.
주의: 스프링 데이터 JPA는 JPA를 편리하게 사용하도록 도와주는 기술이다. 따라서 JPA를 먼저 학습한 후에 스프링 데이터 JPA를 학습해야 한다.
앞의 JPA 설정을 그대로 사용한다.
스프링 데이터 JPA 회원 리포지토리 -
src/main/java/hello.hello_spring/repository/SpringDataJpaMemberRepository
⇒ JpaRepository인터페이스를 상속받은 코드다. 별도의 구현체 작성 없이 인터페이스 정의만으로도 공통 CRUD 기능이 제공되며,findByName과 같은 관례적인 메서드 이름만으로도 쿼리를 자동 생성한다.스프링 데이터 JPA 회원 리포지토리를 사용하도록 스프링 설정 변경 -
src/main/java/hello.hello_spring/SpringConfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package hello.hell_ospring;
import hello.hello_spring.repository.*;
import hello.hello_spring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SpringConfig {
private final MemberRepository memberRepository;
public SpringConfig(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository);
}
}
⇒ 스프링 데이터 JPA가 제공하는 리포지토리를 주입받도록 설정을 변경한다. 스프링 데이터 JPA가 인터페이스의 구현체를 실시간으로 생성하여 스프링 빈으로 자동 등록해 준다.
스프링 데이터 JPA 제공 클래스
⇒ Repository부터 시작해CrudRepository,PagingAndSortingRepository, 그리고 최하단의JpaRepository로 이어지는 계층 구조를 보여준다. 이 구조를 통해 페이징, 정렬 등 다양한 데이터 접근 기능을 상속받아 사용할 수 있다.스프링 데이터 JPA 제공 기능
- 인터페이스를 통한 기본적인 CRUD
findByName(),findByEmail()처럼 메서드 이름 만으로 조회 기능 제공- 페이징 기능 자동 제공
참고: 실무에서는 JPA와 스프링 데이터 JPA를 기본으로 사용하고, 복잡한 동적 쿼리는 Querydsl이라는 라이브러리를 사용하면 된다. Querydsl을 사용하면 쿼리도 자바 코드로 안전하게 작성할 수 있고, 동적 쿼리도 편리하게 작성할 수 있다. 이 조합으로 해결하기 어려운 쿼리는 JPA가 제공하는 네이티브 쿼리를 사용하거나, 앞서 학습한 스프링 JdbcTemplate를 사용하면 된다.
📝 요약 및 정리
이번 섹션에서는 메모리 저장소의 한계를 넘어 실제 데이터베이스를 연동하고, 기술의 발전에 따른 다양한 DB 접근 방식을 학습했다.
1. 데이터베이스 환경 구축 및 연동
H2 데이터베이스: 개발과 테스트 용도로 가볍고 편리한 최고의 DB 환경을 구축했다.
h2database.com에서 설치 후jdbc:h2:~/test접속을 통해 로컬 환경을 구성했다.DataSource: 스프링 부트는 데이터베이스 커넥션 정보를 바탕으로
DataSource를 생성하고 스프링 빈으로 관리하여 의존성 주입(DI)이 가능하도록 지원한다.
2. DB 접근 기술의 발전 과정
순수 Jdbc: 자바에서 DB에 연결하기 위한 원초적인 방식으로, 반복적인 코드가 매우 많아 현대 개발에서는 흐름만 이해하고 넘어가는 단계다.
스프링 JdbcTemplate: JDBC API에서 발생하는 반복 코드를 대부분 제거해주지만, SQL은 개발자가 직접 작성해야 한다.
JPA: SQL 중심의 개발에서 객체 중심의 설계로 패러다임을 전환한다. 반복 코드는 물론 기본적인 SQL도 JPA가 직접 만들어 실행하므로 생산성을 크게 높여준다.
스프링 데이터 JPA: JPA를 한 번 더 감싸 혁신적으로 코드를 줄여주는 현대적인 라이브러리다.
3. 스프링의 핵심 원칙과 테스트
개방-폐쇄 원칙(OCP): 확장에는 열려 있고 수정·변경에는 닫혀 있어야 한다는 원칙이다.
DI를 통한 기술 교체: 스프링의 DI를 사용하면 기존 서비스 코드를 전혀 수정하지 않고, 오직 설정만으로 저장소 구현 클래스(
Memory→Jdbc)를 변경할 수 있다.스프링 통합 테스트:
@SpringBootTest로 스프링 컨테이너를 구동하고,@Transactional을 통해 테스트 후 데이터를 자동 롤백함으로써 DB의 독립성과 반복적인 테스트를 보장한다.
**핵심 키워드:
** H2 Database, JdbcTemplate, JPA, OCP, Spring Integration Test




