현재 까지 구현한걸로는 우리가 입력한 데이터들이 프로그램을 종료하면서 날아가기 때문에 실무에서는 데이터베이스를 사용하여 우리가 입력 혹은 저장하고 싶은 정보를 저장한다.
H2 데이터베이스 설치
-h2 데이터베이스는 개발이나 테스트용도로 가볍고 편리한 DB, 웹화면을 제공한다.
- https://www.h2database.com/html/main.html
- 위 링크를 클릭하면 아래 화면으로 이동하게 된다.
파일이 다운로드 되면 파일을 옮겨주고, 터미널로 h2 파일 안에 bin 디렉토리로 이동
h2.sh 파일을 실행해야하는데 맥의 경우, 권한을 줘야함
chmod 755 h2.sh
./h2.sh
권한을 주고 실행
JDBC URL은 현재 파일의 경로를 말해줌 (home에 있는 test파일)
연결을 누르면
[추가적으로 home 디렉토리에서 ls -al 명령어를 실행했을 때 test.mv.db 파일이 존재해야함]
- 강의에서는 JDBC URL의 경로를 바꿔주었음 (이유는 더 여러곳에서 접근 할 수 있도록, 파일에 직접접근이 아닌 소켓을 통한 접근을 위해서)
jdbc:h2:tcp://localhost/~/test
위 경로로 변경해서 연결
해당 실행화면에 아래 SQL문 입력
create table member
(
id bigint generated by default as identity,
name varchar(255),
primary key(id)
);
순수 JDBC
- 환경설정 : build.gradle 파일에 JDBC, h2 데이터베이스 관련 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
- 그 다음에는 src/main/resource/application.properties 에 해당 코드 import
spring.datasource.url = jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name = org.h2.Driver
- 그 다음에는 JdbcMemberRepository class 를 repository에 만들어줌
import com.example.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 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);
}
}
@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);
}
}
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);
}
}
- 위의 코드로 class 를 만들어주었으면 SpringConfig 파일을 수정해준다.
import com.example.hellospring.repository.JdbcMemberRepository;
//import com.example.hellospring.repository.JdbcTemplateMemberRepository;
import com.example.hellospring.repository.MemberRepository;
import com.example.hellospring.repository.MemoryMemberRepository;
import com.example.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class SpringConfig {
private final DataSource dataSource;
@Autowired
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);
}
}
- 위의 코드로 기존에 코드로 만들어두었던 memberRepository의 연결을 끊고, jdbc를 사용한 memberRepository가 만들어진것이다. 아래는 화면 예시이다.
- 개방 폐쇠의 원칙 (OCP, Open-Closed Principle) : 확장을 하는 것에는 열려있고 수정 및 변경에는 닫혀있다.
- Spring의 DI (Dependencies injection)을 사용하면 기존 코드는 손대지 않고 설정만으로 구현 클래스를 변경할 수 있다.
스프링 통합 테스트
- 스프링 컨테이너와 DB까지 연결한 통합 테스트를 진행
- 현재 지금 사용하고 있는 Test들은 순수 자바 코드들을 통해서 테스트해본것이고 이제 실제 DB 를 이용한 테스트를 경험
src/test/service 에 MemberServiceIntegrationTest class 생성
//해당 코드 대입
import com.example.hellospring.domain.Member;
import com.example.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 // Test 할때 붙여줄 수 있는 Annotation
@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("이미 존재하는 회원입니다.");
}
}
- class 위에 있는 Annotation
- @SpringBootTest : 스프링 컨테이너와 테스트를 함께 실행한다.
- @Transactional : 테스트케이스에 이 어노테이션이 있다면 테스트 시작 전에 트랜잭션을 실행하고, 테스트가 완료된 후에 Rollback을 시켜 DB에 데이터를 남기지 않게 된다. (다음 테스트에 영향을 주지 않는다.)
스프링 JDBCTemplate
- 순수 Jdbc와 동일한 환경설정
-
스프링 JdbcTemplate과 MyBatis 같은 라이브러리는 JDBC API에서 본 반복 코드를 대부분 제거해준다. 하지만 SQL은 직접 작성해야 한다.
src/main/java/com.example.hellospring/repository 안에 JdbcTemplateRepository class 생성
import com.example.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 jdbcTemplate) {
this.jdbcTemplate = new JdbcTemplate(jdbcTemplate);
}
@Override
public Member save(Member member) {
SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate); //SimpleJdbcInsert 를 사용하면 query를 사용할 필요가 없다.
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 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());
}
private RowMapper<Member> memberRowMapper(){
return (rs, rowNum) -> {
Member member = new Member();
member.setId(rs.getLong("ig"));
member.setName(rs.getString("name"));
return member;
};
}
}
- 해당 코드 입력
그 다음 SpringConfig 로 넘어가서 JdbcTemplateRepository로 바꿔주면 됨
@Bean
public MemberRepository memberRepository() {
//return new MemoryMemberRepository();
//return new JdbcMemberRepository(dataSource);
return new JdbcMemberRepository(dataSource);
}
해당 코드로 변경
그 후 테스트를 실행 (해당 사항은 고쳐줄 필요가 X 왜냐하면 이미 연결 되어있는 상태 에서 JdbcTemplate으로 바꿔준것이기 때문에)
테스트 케이스가 없었다면 실행 해보고, 데이터를 확인 하고, 에러 메세지를 확인하는 과정을 거쳐야하지만 TestCase를 통해 빠르고 쉽게 확인이 가능하다.
JPA
- JPA 기술을 사용하면 JPA가 자동으로 SQL을 만들어서 실행시켜준다.
- SQL과 데이터 중심의 설계에서 객테지향 중심의 설계로 패러다임을 전환 할 수 있으며, 개발 생산성을 크게 높일 수 있다.
build.gradle 파일에 JPA, h2 데이터베이스 관련 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
해당 코드를 dependencies에 추가! jdbc 주석처리
spring.jpa.show-sql = true //sql 문을 볼 수 있음
spring.jpa.hibernate.ddl-auto=none //테이블을 자동으로 생성하는 기능을 껏음
해당 구문을 src/main/resources/application.properites 에 추가
gradle을 build 해서 External Libraries에 hibernate 와 jpa가 있는 것을 확인
JPA 는 interface 임으로 hibernate 라는 구현체를 사용하는 것!
그렇기 때문에 우리가 사용하는 Member Class 로 가서 @Entity Annotation을 적어줌
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 String getName() {
return name;
}
public void setId(Long id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
}
- Annotation 설명
- @Entity : 위 클래스를 엔티티로 설정
- @Id : PK 설정
- @GeneratedValue(strategy = GenerationType.IDENTITY) : SQL문의 autoincrement와 동일
이렇게 만들어진 Member class 의 Repository 를 만들어준다.
JpaMemberRepository class 생성
import com.example.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;
}
@Override
public Member save(Member member) {
em.persist(member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
Member member = em.find(Member.class, id);
return Optional.ofNullable(member);
}
@Override
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
@Override
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();
}
}
JPA는 모든 데이터를 변경해줄때 트랜잭션 이 있어야 함으로 MemberService에 @Transactional Annotation을 걸어준다.
그 다음 JPA 로 만든 Repository로 변경 해주어야 함 (SpringConfig)로 이동
import com.example.hellospring.repository.*;
//import com.example.hellospring.repository.JdbcTemplateMemberRepository;
import com.example.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
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의 중요성
- 개발 생산성이 많이 증가 하고 개발해야할 코드가 확연히 줄어듬
- 반복 개발 CRUD 기능도 스프링 데이터 JPA가 제공하기 때문에 코드가 확연히 줄어듦
- 이제 JPA는 선택이 아니라 필수
- 스프링 데이터 JPA는 JPA를 편리하게 사용할 수록 도와주는 기술이기 때문에 JPA를 먼저 학습할 것을 권장
실습
repository에 SpringDataJpaMemberRepository 인터페이스 생성
import com.example.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 에 가서
@Configuration
public class SpringConfig {
private final MemberRepository memberRepository;
@Autowired
public SpringConfig(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository);
}
}
해당 코드로 변경
이렇게 짧은 코드로 가능한 이유
- 스프링 데이터 JPA가 SpringDataJpaMemberRepository를 스프링빈으로 자동 등록 해줌
- 상속해준 JpaRepository 를 확인해보면 여러가지 인터페이스를 상당히 많은 함수들을 제공해주고 있다.
위 자료를 보면 기본적인 인터페이스를 통한 기본적인 CRUD를 제공해줌
실습해보면서 본인이 느낀점으로
순수 자바 코드 >>>>>>> 스프링 데이터 JPA 의 코드량을 느낄 수 있었다.
'Spring' 카테고리의 다른 글
[Spring] Naver OAuth2 로그인 + Spring Security + Naver 검색 API 활용 (1) | 2024.01.24 |
---|---|
[Spring] OpenAI API를 활용하여 ChatBot만들기 (0) | 2024.01.23 |
5장 회원관리예제 - 웹 MVC 개발 (0) | 2023.08.29 |
4장 스프링 빈과 의존관계 (0) | 2023.08.25 |
3장 회원관리 예제 (0) | 2023.08.23 |