스프링 입문 Section6. 스프링 DB 접근 기술

Bae YuSeon·2024년 4월 17일
0

spring스터디

목록 보기
6/15
post-thumbnail

1.H2 데이터베이스 설치

1.1) H2 데이터베이스 설치

H2 데이터베이스 참고 링크

H2 데이터베이스란?

  • H2는 자바로 작성된 관계형 데이터베이스 관리 시스템
  • 스프링 부트가 지원하는 인메모리 관계형 데이터베이스
  • 용량이 작고, 가벼움
  • 교육용으로 좋은 데이터베이스

설치
H2 데이터베이스 설치 사이트 이 사이트에 들어가서 1.4.200 버전 Window installer을 설치한다.
h2 db

H2 데이터베이스의 위치를 설정하고 다운로드를 마무리해 준다.

H2 데이터베이스 실행 시키는 방법 2가지
1). 실행파일 선택
H2를 설치했던 위치로 가 H2/bin/h2.hat 파일을 실행한다.
h2 실행
2). cmd로 실행
cmd 창을 열고 H2를 설치했던 위치로 접근 한 후, H2 폴더 안의 bin 폴더로 이동한다. h2.bat 파일을 실행시킨다.

h2.bat 파일을 실행하면
이런 화면이 뜬다. H2 데이터베이스 경로와 사용자명 등을 입력해주고 연결 버튼을 누른다. 그러면 H2 데이터베이스여 접속 된다. db 연결

다른 데이터베이스와 다르게 H2 데이터베이스는 파일에 직접 접근하는 방식(jsbc:h2:~/파일이름)을 제공한다.
하지만 파일에 직접 접근하는 방식을 사용하면 동시성 문제가 발생할 수 있으므로 H2 데이터베이스 사용 시 처음 DB 파일 생성할 때만 파일에 직접 접근하고, 이후 DB 파일 접슨 시에는 DB 서버를 통하도록 한다.
따라서 데이터베이스 파일 처음 생성 시 JDBC URL을 최초 한번은 jdbc:h2:~/test로 해야 하고 이후부터는 jdbc:h2:tcp://localhost/~/test 이렇게 접속해야 한다.

1.2) 테이블 생성하기

H2 데이터베이스에 접근해서 member 테이블 생성한다.

create table member
(
 id bigint generated by default as identity,
 name varchar(255),
 primary key (id)
);
  • bigint: java로 하면 long 타입
  • generated by default as identityid: id값으로 null이 들어오면 자동적으로 값을 채움
  • varchar: 가변 길이 문자열
  • member 테이블은 id를 primary key로 가짐

select 명령어로 만든 member 테이블을 조회 가능!

SELECT * FROM MEMBER 

member확인

insert 명령어로 member 테이블에 값을 추가

insert into member(name) values('spring1')

값을 추가한 후 다시 select 명령어로 member 테이블을 조회하면 정상적으로 데이터가 추가된 것을 확인할 수 있다.

1.3) DB 관리

spring스터디/hello-spring/sql/ddl.sql 파일을 새로 만들어 sql을 관리한다.


2. 순수 JDBC

이번에는 memory가 아닌 DB에 연동해서 직접 데이터를 추가하고, 저장하고, 빼는 것을 할 것이다.

2.1) 환경 설정

build.gradle 파일에 jdbc, h2 데이터베이스 관련 라이브러리 추가

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-jdbc'
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	runtimeOnly 'com.h2database:h2'
	testImplementation('org.springframework.boot:spring-boot-starter-test') {
		exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
	}
}

스프링 부트 데이터베이스 연결 설정 추가
src/main/resources/application.properties 파일에

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

코드를 추가한다.

2.2) Jdbc 리포지토리 구현

⚠️이렇게 JDBC API로 직접 코딩하는 것은 20년 전 이야기이다! 참고만 하고 넘어가자!

Jdbc 회원 리포지토리
java/hello/hellospring/repository/JdbcMemberRepository.java 파일을 만든다.

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);
    }
}

build.gradle 파일에 jdbc 관련 라이브러리를 추가했음에도

import org.springframework.jdbc.datasource.DataSourceUtils;

에서 에러가 계속 발생했는데 gradle refresh를 하고 Project 재시작을 통해 해결했다.

2.3) 스프링 설정 변경

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);
 }
 
}

이렇게 코드를 수정해주고 실행한 다음 http://localhost:8080/members 회원 목록 페이지에 들어가면 아까 sql에 저장했던 회원의 목록을 볼 수 있다.
회원 목록
추가적으로 회원 가입 창에서 새롭게 회원을 등록하고

H2 데이터베이스에서 확인을 해보면 정상적으로 추가가 된것도 확인할 수 있다.

🤔스프링을 왜 사용하는가?
⇒다형성을 활용할 수 있기 때문에!


2.4)원리 설명


스프링 컨테이너에서 설정을 바꿔, 기존 MemoryMemberRepository을 spring bean에서 빼고 Jdbc MemberRepository를 새로운 spring bean으로 등록

  • 개방-폐쇄 원칙(OCP, Open-Closed Principle)
    • 확장에는 열려있고, 수정, 변경에는 닫혀있다.
  • 스프링의 DI (Dependencies Injection)을 사용하면 기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경할 수 있다.

3. 스프링 통합 테스트

스프링 컨테이너와 DB까지 연결한 통합 테스트를 진행해 볼 것이다.

3.1) 회원 서비스 스프링 통합 테스트

java/hello/hellospring/service/MemberServiceIntegrationTest.java 파일을 만든다.
기존에 만들었던 MemberServiceTest의 내용을 가져와서 수정한다.

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 join() 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("이미 존재하는 회원입니다.");
    }
}

테스트를 진행하기 전에 데이터베이스에 있는 내용을 모두 지워준다.
delete
데이터베이스가 비워진 것을 확인하고 테스트를 진행한다.

테스트를 실행하면 정상적으로 동작하는 것을 확인할 수 있다.
테스트통과

3.2) 설명

  • @SpringBootTest : 통합 테스트를 제공하는 기본적인 스프링 부트 애노테이션. 스프링 컨테이너와 테스트를 함께 실행한다.
  • @Transactional: : 테스트 케이스에 이 애노테이션이 있으면, 테스트 시작 전에 트랜잭션을 시작하고, 테스트 완료 후에 항상 롤백한다. 이렇게 하면 DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다.
단위 테스트통합 테스트
코드의 가장 작은 단위(예: 메서드, 함수)에서 독립적으로 실행DB까지 연동해서 여러 코드 단위가 함께 작동하는 것을 테스트

순수한 단위 테스트가 훨씬 좋은 테스트일 확률이 높다


4. 스프링 JdbcTemplate

4.1) 스프링 JdbcTemplate 개념

  • 순수 Jdbc와 동일한 환경설정을 하면 된다.
    • build.gradle의 dependencies에 implementation 'org.springframework.boot:spring-boot-starter-jdbc' 추가
  • 스프링 JdbcTemplate과 MyBatis 같은 라이브러리는 JDBC API에서 본 반복 코드를 대부분 제거해준다. 하지만 SQL은 직접 작성해야 한다.

4.2) 스프링 JdbcTemplate 회원 리포지토리

JdbcTemplate을 사용한 회원 리포지토리 java/hello/hellospring/repository/JdbcTemplateMemberRepository.java 파일을 만든다.

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import org.springframework.beans.factory.annotation.Autowired;
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.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public class JdbcTemplateMemberRepository implements MemberRepository {

    private final JdbcTemplate jdbcTemplate;

    @Autowired
    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 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("id"));
            member.setName(rs.getString("name"));
            return member;
        };
    }
}

4.3) 스프링 설정 변경

작성한 JdbcTemplate을 사용할 수 있도록 스프링 설정을 변경해준다.
java/hello/hellospring/SpringConfig.java 파일을

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

이렇게 고쳐준다.
코드를 작성한 후 아까 만들었던 통합 테스트 코드를 그대로 가져와서 실행하면
테스트 통과
정상적으로 작동하는 것을 확인할 수 있다.


5. JPA

5.1) JPA 개념

JPA란?
⇒ 자바 기반 애플리케이션에서 데이터베이스와의 상호작용을 쉽게 하고, 데이터 영속성(Persistence)을 관리할 수 있도록 하는 표준 API

  • JPA는 기존의 반복 코드는 물론이고, 기본적인 SQL도 직접 만들어서 실행해준다.
  • JPA를 사용하면, SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환을 할 수 있다.
  • JPA를 사용하면 개발 생산성을 크게 높일 수 있다.

ORM(Object-Relational Mapping)
⇒ 자바 객체와 관계형 데이터베이스의 테이블을 매핑

5.2) 실습

5.2.1) build.gradle 파일에 JPA, h2 데이터베이스 관련 라이브러리 추가

// 프로젝트의 의존성을 선언
dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	runtimeOnly 'com.h2database:h2'
	testImplementation('org.springframework.boot:spring-boot-starter-test') {
		exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
	}
}

spring-boot-starter-data-jpa 는 내부에 jdbc 관련 라이브러리를 포함해서 jdbc는 제거해도 된다.

5.2.2) 스프링 부트에 JPA 설정 추가

resources/application.properties에

spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none

내용을 추가해줘야 한다.

  • show-sql : JPA가 생성하는 SQL을 출력
  • ddl-auto : JPA는 테이블을 자동으로 생성하는 기능을 제공하는데 none 를 사용하면 해당 기능을 끈다.
    • create 를 사용하면 엔티티 정보를 바탕으로 테이블도 직접 생성해준다

5.2.3) JPA 엔티티 매핑

java/hello/hellospring/domain/Member.java 파일에 JPA 엔티티 매핑 내용을 추가해준다.

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    
    ...
}   

엔터티(Entity)란?

  • 데이터베이스의 테이블에 매핑되는 자바 객체

5.2.4) JPA 회원 리포지토리 생성

java/hello/hellospring/repository/JpaMemberRepository.java 파일을 만든다.

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import jakarta.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 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();
    }

    @Override
    public List<Member> findAll() {
        return em.createQuery("select m from Member m", Member.class)
                .getResultList();
    }

}
  • EntityManager: 엔티티 매니저는 JPA의 핵심 인터페이스로, 엔터티의 영속성 상태를 관리하고, 내부적으로 데이터베이스와의 상호작용을 담당

  • JPQL(Java Persistence Query Language)

    • JPA가 제공하는 객체 지향적인 쿼리 언어
    • SQL과 비슷하지만, 테이블이 아닌 자바 객체를 대상으로 쿼리를 작성할 수 있도록 설계

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

java/hello/hellospring/service/MemberService.java 파일에

import org.springframework.transaction.annotation.Transactional

@Transactional
public class MemberService {}

트랜잭션을 추가한다.

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

java/hello/hellospring/SpringConfig.java 파일을

@Configuration
public class SpringConfig {

    private final DataSource dataSource;
    private final EntityManager em;

    @Autowired
    public SpringConfig(DataSource dataSource, EntityManager em) {
        this.dataSource = dataSource;
        this.em = em;
    }
    
    ...
    
    @Bean
    public MemberRepository memberRepository() {
        //return new MemoryMemberRepository();
        //return new JdbcMemberRepository(dataSource);
        //return new JdbcTemplateMemberRepository(dataSource);
        return new JpaMemberRepository(em);
    }

JPA를 사용할 수 있게 수정해준다.

정상적으로 동작하는지 확인하기 위해 테스트를 돌려 확인한다.
테스트확인
정상적으로 동작되는 것을 볼 수 있다.


6. 스프링 데이터 JPA

스프링 부트와 JPA에 스프링 데이터 JPA까지 사용하면, 리포지토리에 구현 클래스 없이 인터페이스 만으로 개발을 완료할 수 있다. 기본 CRUD 기능도 제공해줘 개발 코드들이 줄어든다.

앞의 JPA 설정을 그대로 사용한다.

6.1) 실습

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

java/hello/hellospring/repository/SpringDataJpaMemberRepository.java 파일을 인터페이스로 만든다.

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가 JpaRepository를 받고 있으면 구현체를 자동으로 만들어서 스프링 빈에 자동으로 등록한다. 우리는 자동으로 만들어진 걸 가져와서 사용하면 된다.

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

SpringConfig.java 파일을 수정해준다.

@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 를 스프링 빈으로 자동 등록해준다.
정상적으로 동작하는지 확인하기 위해 또 테스트를 돌려 확인한다.

정상적으로 동작되는 것을 볼 수 있다.

6.2) 스프링 데이터 JPA

  • 스프링 데이터 JPA 제공 기능
    • 인터페이스를 통한 기본적인 CRUD
    • findByName() , findByEmail() 처럼 메서드 이름 만으로 조회 기능 제공
    • 페이징 기능 자동 제공

0개의 댓글

관련 채용 정보