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);
}
}
=> 스프링 데이터 JPA가 SpringDataJpaMemberRepository 를 스프링 빈으로 자동 등록해준다.
'백엔드 > 스프링' 카테고리의 다른 글
코딩 자율학습 스프링 부트 3 자바 백엔드 개발 입문 1~2장 (1) | 2025.06.25 |
---|---|
코딩 자율학습 스프링 부트 3 자바 백엔드 개발 입문 3~5장 (1) | 2025.06.01 |
회원 관리 예제 - 웹 MVC 개발 (0) | 2025.05.18 |
스프링 빈과 의존관계 (1) | 2025.05.18 |
비즈니스 예제 (0) | 2025.05.12 |