JDBC는 Java가 H2, MySQL 같은 데이터베이스와 통신하는 기본 API다.
순수 JDBC를 사용하면 연결을 얻고, SQL을 준비하고, 실행 결과를 객체로 바꾸고, 연결을 닫는 일을 직접 해야 한다. JdbcTemplate은 이 반복 작업을 대신 처리하고, 개발자는 SQL과 결과 변환에 집중하게 해준다.
MemberService
↓
JdbcTemplateMemberRepository
↓
JdbcTemplate
↓
DataSource (DB 연결 정보)
↓
H2 Database
DataSource는 DB에 접속하기 위한 URL, 사용자명, 비밀번호 등의 연결 정보를 가진 객체다.
현재 프로젝트에서는 H2 드라이버와 JDBC/JPA 관련 의존성이 있으므로 Spring Boot가 DataSource Bean을 자동으로 만들어 준다.
application.yaml에 DB 정보를 지정하면 Spring Boot는 그 주소로 연결한다.
spring:
datasource:
url: jdbc:h2:tcp://localhost/~/test
username: sa
password:
driver-class-name: org.h2.Driver
위 설정은 H2 콘솔의 JDBC URL도 똑같이 jdbc:h2:tcp://localhost/~/test로 설정했을 때 같은 DB를 바라본다.
URL을 설정하지 않으면 Spring Boot는 보통
jdbc:h2:mem:...형태의 임시 인메모리 DB를 자동 생성한다. 이 DB는 애플리케이션이 종료되면 사라지고, H2 콘솔에서 별도로 연 DB와 다를 수 있다.
현재 프로젝트는 컴포넌트 스캔이 아니라 SpringConfig에서 직접 Bean을 등록한다.
@Configuration
public class SpringConfig {
@Bean
public MemberService memberService(MemberRepository memberRepository) {
return new MemberService(memberRepository);
}
@Bean
public MemberRepository memberRepository(DataSource dataSource) {
return new JdbcTemplateMemberRepository(dataSource);
}
}
스프링은 memberRepository() 메서드의 DataSource 파라미터에 자동으로 DataSource Bean을 넣는다. 그 결과 MemberService에는 JdbcTemplateMemberRepository가 MemberRepository 타입으로 주입된다.
이 방식에서는 Repository 클래스의 생성자에 @Autowired를 붙일 필요가 없다.
public JdbcTemplateMemberRepository(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
생성자가 하나인 Bean은 @Autowired 없이도 생성자 주입이 가능하다. 특히 현재처럼 new JdbcTemplateMemberRepository(dataSource)로 직접 생성하는 경우, 생성자에 붙인 @Autowired는 의미가 없다.
DB 조회 결과는 ResultSet이라는 행 단위 데이터다. RowMapper는 ResultSet의 한 행을 Member 객체 하나로 바꾸는 함수다.
private RowMapper<Member> memberRowMapper() {
return (rs, rowNum) -> {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return member;
};
}
예를 들어 DB의 한 행이 아래와 같다면:
| id | name |
|---|---|
| 1 | spring |
RowMapper는 다음과 같은 Member 객체를 만든다.
Member member = new Member();
member.setId(1L);
member.setName("spring");
findById, findByName, findAll@Override
public Optional<Member> findById(Long id) {
List<Member> result = jdbcTemplate.query(
"select * from member where id = ?",
memberRowMapper(),
id
);
return result.stream().findAny();
}
@Override
public Optional<Member> findByName(String name) {
List<Member> result = jdbcTemplate.query(
"select * from member where name = ?",
memberRowMapper(),
name
);
return result.stream().findAny();
}
@Override
public List<Member> findAll() {
return jdbcTemplate.query("select * from member", memberRowMapper());
}
?는 반드시 값과 함께 전달한다SQL의 ?는 파라미터 자리다.
select * from member where name = ?
따라서 JdbcTemplate 호출의 마지막 인자로 name을 넘겨야 한다.
jdbcTemplate.query(sql, memberRowMapper(), name);
값을 넘기지 않으면 아래처럼 오류가 발생한다.
Parameter "#1" is not set
#1은 첫 번째 ?에 값이 없다는 뜻이다.
SimpleJdbcInsertSimpleJdbcInsert는 INSERT SQL을 직접 길게 쓰지 않고 저장하도록 도와준다.
@Override
public Member save(Member member) {
SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate)
.withTableName("member")
.usingGeneratedKeyColumns("id");
Map<String, Object> parameters = new HashMap<>();
parameters.put("name", member.getName());
Number key = jdbcInsert.executeAndReturnKey(
new MapSqlParameterSource(parameters)
);
member.setId(key.longValue());
return member;
}
동작 순서:
member.getName() 추출
↓
MEMBER 테이블에 INSERT 실행
↓
DB가 자동 생성한 ID 반환
↓
member.setId(...)로 Java 객체에도 ID 반영
↓
저장된 Member 반환
save()가 return null; 상태라면 Repository는 연결되어 있어도 INSERT SQL을 실행하지 않으므로 DB에 저장되지 않는다.
@Transactional 테스트와 기존 데이터@SpringBootTest
@Transactional
class MemberServiceIntegrationTest {
}
@SpringBootTest: 실제 Spring Boot 컨테이너와 Bean을 띄워서 테스트한다.@Transactional: 각 테스트에서 수행한 DB 변경을 테스트 종료 후 롤백한다.하지만 @Transactional은 테스트 시작 전에 이미 DB에 있던 데이터를 지우지는 않는다.
예를 들어 H2 콘솔에서 이미 spring 회원을 넣어 두었다면, 테스트의 첫 가입 시도도 중복 회원 예외가 난다.
DELETE FROM MEMBER;
테스트 전에 데이터를 비우고 싶을 때는 위 SQL을 사용할 수 있다. ID 자동 증가값까지 초기화하려면:
TRUNCATE TABLE MEMBER RESTART IDENTITY;
실무에서는 애플리케이션용 DB와 테스트용 DB를 분리하는 것이 좋다. 그래야 콘솔에서 넣은 데이터 때문에 테스트 결과가 달라지지 않는다.
DataSource를 만들고, JdbcTemplate은 그 DataSource로 DB와 통신한다.SpringConfig에서 JdbcTemplateMemberRepository를 Bean으로 등록하면 서비스가 해당 구현체를 사용한다.RowMapper는 조회한 DB 행을 Java Member 객체로 변환한다.?에는 반드시 호출 시 값을 전달해야 한다.SimpleJdbcInsert는 INSERT 후 자동 생성된 ID를 받아 Member에 넣어 준다.@Transactional 테스트는 테스트 중 변경을 롤백하지만, 기존 DB 데이터까지 초기화하지는 않는다.