본문 바로가기
백엔드/스프링

스프링 DB 접근 기술

by dustnn 2025. 5. 18.

1. H2 데이터베이스 설치

2. 순수 JDBC

3. 스프링 통합 테스트

4. 스프링 JdbcTemplate

5. JPA: 객체를 바로 DB에 쿼리 없이 저장 및 관리 가능

6. 스프링 데이터 JPA: JPA를 편리하게 사용할 수 있도록 감싼 기술


H2 데이터베이스 설치

 

 

https://www.h2database.com/html/main.html

 

H2 Database Engine

H2 Database Engine Welcome to H2, the Java SQL database. The main features of H2 are: Very fast, open source, JDBC API Embedded and server modes; in-memory databases Browser based Console application Small footprint: around 2.5 MB jar file size     Supp

www.h2database.com

 

권한 주기 및 실행

chmod 755 h2.sh // 권한 주기
./h2.sh // 실행

 

처음에는 왼쪽처럼, 이후부터는 오른쪽처럼 하고 연결 누르기

 

테이블 생성
drop table if exits member CASCADE;
create table member //H2 데이터베이스에 접근해서 member 테이블 생성
(
    id bigint generated by default as identity, //Member 클래스의 id라는 변수
    name varchar(255),
    primary key (id)
);

 

 

순수 JDBC

 

아주 옛날에 썼던 기술이므로 자세히 적지는 않겠다.

환경 설정

 

"build.gradle"에 JDBC와 H2 라이브러리 추가:

implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'

 

"application.properties"에 DB 접속 정보 설정:

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa

 

 

JDBC 리포지토리 구현

 

JdbcMemberRepository 클래스는 MemberRepository 인터페이스를 구현.

save, findById, findAll, findByName 등의 메서드를 JDBC API로 직접 구현.

 

<주요 메소드 패턴>
Connection conn = getConnection(); 
PreparedStatement pstmt = conn.prepareStatement(sql); 
ResultSet rs = pstmt.executeQuery();

 

연결 및 자원 해제를 위한 getConnection(), close() 메소드 구현.

 

 

스프링 설정 변경 (SpringConfig)

 

@Configuration 클래스에서 @Bean으로 리포지토리와 서비스 등록.

MemoryMemberRepository → JdbcMemberRepository로 변경만 하면 사용 가능.

@Bean 
public MemberRepository memberRepository() { 
	return new JdbcMemberRepository(dataSource); 
}

 

구조 및 원칙 설명
  • OCP (Open-Closed Principle): 확장에는 열려 있고, 수정에는 닫혀 있다.
  • DI (Dependency Injection) 덕분에 코드 수정 없이 설정만으로 구현체 교체 가능.

 

실행 후 확인 사항

 

회원 등록 → DB에 정상 저장되는지 확인.

서버 재시작 후에도 데이터가 유지되는지 확인.

 

스프링 통합 테스트

 

스프링 컨테이너와 DB까지 모두 연결한 통합 테스트

 

@SpringBootTest: 스프링 컨테이너와 테스트를 함께 실행

@Transactional: 테스트 케이스에 이 노테이션이 있으면, 테스트 시작 전에 트랜잭션을 시작하고, 테스트 완료 후에 항상 ROLLBACK함 -> DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않음.

 

package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@SpringBootTest
@Transactional
class MemberServiceIntegrationTest {
	//테스트할 때는 보통 한 번만 하고 끝나기 때문에 Autowired 옆에 바로 써준다.
     @Autowired MemberService memberService;
     @Autowired MemberRepository memberRepository; //MemoryMemberRepository가 아니라 MemberRepository로 해줘야 한다.
    @Test
    public void 회원가입() throws Exception {
         //Given
         Member member = new Member();
         member.setName("hello"); //hello 라는 이름의 회원이 이미 있으면 에러 -> Test 를 위한 데이터베이스에서는 hello 동명이인 회원을 지워줘야 함
         //When
         Long saveId = memberService.join(member);
         //Then
         Member findMember = memberRepository.findById(saveId).get();
         assertEquals(member.getName(), findMember.getName());
     }
     
    @Test
    public void 중복_회원_예외() throws Exception {
         //Given
         Member member1 = new Member();
         member1.setName("spring");
         
         Member member2 = new Member();
         member2.setName("spring");
         //When
         memberService.join(member1);
         IllegalStateException e = assertThrows(IllegalStateException.class,
() -> memberService.join(member2));//예외가 발생해야 한다. assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }
}

 

스프링 JdbcTemplate

 

실무에서 종종 사용

 

jdbc 코드를 아주 압축해놓은 것이 jdbcTemplate 라이브러리

템플릿 메소드 패턴이 많이 들어가있기 때문에 template으로 간주된다.

 

- 환경설정은 순수JDBC와 동일

- 스프링 JdbcTemplate과 MyBatis 등의 라이브러리는 JDBC API에서 본 반복 코드를 대부분 제거해준다.

하지만 SQL은 직접 작성해야 한다.

 

다음과 같이 매개변수로 DataSource를 인젝션받아야 한다.

private final JdbcTemplate jdbcTemplate;

@Autowired //생성자가 하나만 있으면 스프링 빈으로 등록될 시 Autowired 생략 가능
public JdbcTemplateMemberRepository(DataSource dataSource){
	jdbcTemplate = new JdbcTemplate(dataSource);
}

같은 코드

<JdbcTemplateMemberRepository>
package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import javax.sql.DataSource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class JdbcTemplateMemberRepository implements MemberRepository {
    private final JdbcTemplate jdbcTemplate;
    public JdbcTemplateMemberRepository(DataSource dataSource) {
        jdbcTemplate = new JdbcTemplate(dataSource);
}
    @Override
    public Member save(Member member) {
        SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
        jdbcInsert.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;
    }
    @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 List<Member> findAll() {
         return jdbcTemplate.query("select * from member", memberRowMapper());
     }
     
     @Override
     public Optional<Member> findByName(String name) {
         List<Member> result = jdbcTemplate.query("select * from member where
 name = ?", memberRowMapper(), name);
         return result.stream().findAny();
     }
     private RowMapper<Member> memberRowMapper() {
         return (rs, rowNum) -> {
             Member member = new Member();
             member.setId(rs.getLong("id"));
             member.setName(rs.getString("name"));
             return member;
}; }
}

 

<SpringConfig>

 

jdbcTemplate을 사용하도록 스프링 설정 변경(repository 부분 jdbc로 수정)

package hello.hellospring;
 import hello.hellospring.repository.JdbcMemberRepository;
 import hello.hellospring.repository.JdbcTemplateMemberRepository;
 import hello.hellospring.repository.MemberRepository;
 import hello.hellospring.repository.MemoryMemberRepository;
 import hello.hellospring.service.MemberService;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import javax.sql.DataSource;
 
 @Configuration
 public class SpringConfig {
     private final DataSource dataSource;
          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);
         return new JdbcTemplateMemberRepository(dataSource); // 변경 !!!!
     }
}

 

=> db까지 연결된 테스트 성공.

 

JPA

 

: 객체를 바로 DB에 쿼리 없이 저장 및 관리 가능하도록 해주는 인터페이스

cf) 구현은 hibernate 등등이 해줌

: Object와 Relational db table을 매핑해줌 -> "@Entity"

@id

@Generatedvalue()

@Column()

=> 이러한 어노테이션들을 통해 db와 매핑해줌

 

- JPA는 기존의 반복 코드는 물론이고, 기본적인 SQL도 직접 만들어서 실행해준다.

=> SQL과 데이터 중심의 설계가 아니라 객체 중심의 설계 가능

=> 개발 생산성 크게 높이기 가능

 

 

 

<Member>

 

: JPA 엔티티 매핑

* @Entity: @id, @Generatedvalue(), @Column() 어노테이션들을 통해 db와 매핑해줌

package hello.hellospring.domain;
 import javax.persistence.Entity;
 import javax.persistence.GeneratedValue;
 import javax.persistence.GenerationType;
 import javax.persistence.Id;
 @Entity
 public class Member {
     @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
     private Long id;
     private String name;
     public Long getId() {
         return id;
     }
     public void setId(Long id) {
         this.id = id;
     }
     public String getName() {
              return name;
     }
     public void setName(String name) {
         this.name = name;
     } 
}

 

<JpaMemberRepository>

 

JPA 회원 레포지토리

 

EntityManager가 다 묶어서 sql 처리해주기 때문에 JPA를 쓰기 위해 꼭 넣어줘야 함

-> JPA가 insert 쿼리를 넣어주고 id를 만들어주는 등 모든 작업을 다 해준다.

 

package hello.hellospring.repository;
 import hello.hellospring.domain.Member;
 import javax.persistence.EntityManager;
 import java.util.List;
 import java.util.Optional;
 public class JpaMemberRepository implements MemberRepository {
     private final EntityManager em;
     public JpaMemberRepository(EntityManager em) {
         this.em = em;
     }
     public Member save(Member member) {
         em.persist(member);
         return member;
     }
     public Optional<Member> findById(Long id) {
         Member member = em.find(Member.class, id);
         return Optional.ofNullable(member); //findById는 pk기반이므로 쿼리 작성 안 해도 ok
     }
     public List<Member> findAll() {
         return em.createQuery("select m from Member m", Member.class) //findAll은 pk기반이 아니므로 쿼리 작성해야 함
     }
     public Optional<Member> findByName(String name) {
         List<Member> result = em.createQuery("select m from Member m where
 m.name = :name", Member.class) //findByName은 pk기반이 아니므로 쿼리 작성해야 함
                 .setParameter("name", name)
                 .getResultList();
         return result.stream().findAny();
     }    
}

 

단축키 "control+T": inline으로 합쳐줌

 

* findById는 pk기반이므로 쿼리 작성 안 해도 ok

* findAll, findByName은 pk기반이 아니므로 쿼리 작성해야 함

<MemberService>

 

서비스 계층에 트랜잭션 추가

import org.springframework.transaction.annotation.Transactional //이걸 사용해야

@Transactional
public class MemberService {}

- 스프링은 해당 클래스의 메서드를 실행할 때 트랜잭션을 시작하고, 메서드가 정상 종료되면 트랜잭션을 커밋한다.

만약 런타임 예외가 발생하면 롤백한다. 
- JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행해야 함.

 

<SpringConfig>

 

JPA를 사용하도록 스프링 설정 변경

package hello.hellospring;
 import hello.hellospring.repository.*;
 import hello.hellospring.service.MemberService;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import javax.persistence.EntityManager;
 import javax.sql.DataSource;
 @Configuration
 public class SpringConfig {
     private final DataSource dataSource;
     private final EntityManager em;
     public SpringConfig(DataSource dataSource, EntityManager em) {
         this.dataSource = dataSource;
         this.em = em;
     }
     @Bean
     public MemberService memberService() {
         return new MemberService(memberRepository());
     }
     @Bean
     public MemberRepository memberRepository() {
         //return new MemoryMemberRepository();
         //return new JdbcMemberRepository(dataSource);
         //return new JdbcTemplateMemberRepository(dataSource);
         return new JpaMemberRepository(em);
     }
}

 

 

스프링 데이터 JPA

 

: JPA를 편리하게 사용할 수 있도록 감싼 기술

 

리포지토리에 구현클래스 없이 인터페이스만으로 개발 가능

조금이라도 단순하고 반복이라 생각했던 개발 코드들이 확연하게 줄어듦

-> 개발자는 핵심 비즈니스 로직을 개발하는 데 집중 가능

 

<MemberRepository>

스프링 데이터 JPA 회원 리포지토리

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
     Optional<Member> findByName(String name);
}

 

<SpringConfig>

스프링 데이터 JPA 회원 리포지토리를 사용하도록 스프링 설정 변경

 

package hello.hellospring;
import hello.hellospring.repository.*;
import hello.hellospring.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);
     }
}

 

=> 스프링 데이터 JPASpringDataJpaMemberRepository 를 스프링 빈으로 자동 등록해준다.