목차
- 섹션7
- H2 데이터베이스 설치
- 순수 Jdbc
- 스프링 통합 테스트
- 스프링 Jdbc Template
- JPA
- 스프링 데이터 JPA
#섹션 7
H2 데이터베이스 설치
먼저 데이터베이스란 여러 사용자나 응용 프로그램이 공유하고 사용할 목적으로 통합 관리되는 데이터의 집합을 뜻한다.
(그 중 H2는 Java로 작성된 오픈 소스 데이터베이스 관리 시스템이다.)
H2 데이터베이스를 다운받고 실행하면 위와 같은 화면이 뜨는데 JDBC URL에
최초 연결 후 jdbc:h2:~/test -> jdbc:h2:tcp://localhost/~/test 변경한 뒤 접속해주면 된다.
들어가서 테이블을 생성해주고 멤버를 입력했을 때 나온 화면이다.
drop table if exists member CASCADE;
create table member
( id bigint generated by default as identity,
name varchar(255),
primary key (id)
);
(테이블 코드)
sql파일을 따로 만들어서 관리할 수 있다.
순수 Jdbc
Java Database Connectivity는 Java 애플리케이션에서 데이터베이스에 접근하고 조작하기 위한 표준 API이다.
(cf. API는 Application Programming Interface의 약자로 소프트웨어 응용 프로그램이 다른 소프트웨어 구성 요소나 서비스와 상호 작용할 수 있게 해주는 인터페이스이다. 여러 프로그램 및 데이터베이스 등의 통신 매개체라고 보면 된다.)
옛날에는 개발할 때 데이터베이스 연결, 수정 , 조회 등 모든 작업을 직접 개발해야했어야 했는데 그때 사용된 것이
Jdbc라고 보면 된다. 뒤에서 다루겠지만 요새는 JPA (Java Persistence Api) 가 많이 활용된다고 한다.
//Jdbc 회원 repositroy
package hello.hellospring.repository;
import hello.hellospring.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);
}
}
JdbcMemberRepository 클래스를 다음과 같이 작성해준뒤
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);
}
}
SpringConfig에 들어가 DataSource를 추가해주면 된다. (DI)
그렇게 하면 코드 수정 없이 설정만으로 구현 클래스를 변경할 수 있다.
구현 클래스를 MemoryMemberRepository -> JdbcMemberRepository로 바꿔주는 이유는 개방 폐쇄 원칙(OCP) 때문인데
확장에는 열려 있지만 수정 또는 변경에 있어서는 닫혀있기 때문이다.
localhost에 들어가서 회원 목록에 들어가보면 H2데이터베이스와 연결되어있음을 볼 수 있다.
또한, 회원 가입을 통해 추가해도 데이터베이스에서 확인할 수 있다.
스프링 통합 테스트
테스트 디렉토리에 MemberServiceIntegrationTest 클래스를 만들어주고 코드를 작성해준다.
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 MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
public void 회원가입() throws Exception {
//Given
Member member = new Member();
member.setName("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("이미 존재하는 회원입니다.");
}
}
- @SpringBootTest : 스프링 컨테이너와 테스트를 함께 실행하게끔 하는 애노테이션
- @Transactional : 테스트 케이스에 이 애노테이션이 있으면, 테스트 시작 전에 트랜잭션을 시작하고, 테
스트 완료 후에 항상 롤백한다. 이렇게 하면 DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않게 된다.
스프링 Jdbc Template
Spring JDBC Template은 순수 JDBC의 단점(반복 코드, 예외 처리, 리소스 관리 등)을 해결하는데 도움을 준다.
순수 Jdbc는 반복적인 작업이 진행되는데 여기서는 오류가 생길 수도 있고 불필요한 코드들을 줄일 필요가 있다그것에 도움을 주는 것이 spring jdbc template이라고 보면 될 것 같다.
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;
};
}
}
다음과 같이 repository에 JdbcTemplateMemberRepositroy 클래스를 생성해서 코드를 적어주면 된다.
JPA
- JPA는 기존의 반복 코드는 물론이고, 기본적인 SQL도 JPA가 직접 만들어서 실행해준다.
- JPA를 사용하면 개발 생산성을 크게 높일 수 있다.
jpa mapping
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;
}
}
회원 repository
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);
}
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class)
.getResultList();
} public Optional<Member> findByName(String name) {
List<Member> result = em.createQuery("select m from Member m where
m.name = :name", Member.class)
.setParameter("name", name)
.getResultList();
return result.stream().findAny();
}
}
긴 try-catch문과 복잡한 코드 구성을 가지고 있던 Jdbc Repository와 확연히 차이나는 걸 볼 수 있다.
(build.gradle , service에서 @Transcational 추가 , Springconfig 수정 또한 JPA에 맞게 해줘야한다.)
스프링 데이터 JPA
스프링부트와 JPA를 사용했을 때 개발 생산성이 눈에 띄게 증가한 것을 볼 수 있었다.
그러나 , 스프링 데이터 JPA 를 이용하면 구현체 작성 없이 인터페이스만을 작성해서 개발을 완료할 수 있다..
SpringDataJpaMemberRepository생성
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);
}
스프링 데이터 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);
}
}
'WINK-(Web & App) > Spring Boot 스터디' 카테고리의 다른 글
[2025 겨울방학 스프링 스터디] 정채은 #4주차 (0) | 2025.02.03 |
---|---|
[2025 겨울방학 스프링 스터디] 윤아영 #4주차 (0) | 2025.02.03 |
[2025 겨울방학 스프링 스터디] 김재승 #3주차 (0) | 2025.01.29 |
[2025 겨울방학 스프링 스터디] 윤아영 #3주차 (0) | 2025.01.27 |
[2025 겨울방학 스프링 스터디] 정채은 #3주차 (1) | 2025.01.26 |