나의 브을로오그으

#6. [스프링 입문] - 스프링 DB 접근 기술 본문

Spring

#6. [스프링 입문] - 스프링 DB 접근 기술

__jhp_+ 2022. 7. 11. 21:39

[스프링 데이터 엑세스]

 

- H2 데이터베이스 설치

- 순수 Jdbc

- 스프링 JdbcTemplate

- JPA

- 스프링 데이터 JPA

 

H2 데이터베이스 설치

- 개발이나 테스트용도로 가볍고 편리한 DB, 웹 화면 제공

 

1. 윈도우의 경우 h2 설치

2. 윈도우 키를 누르고 h2를 검색해서 h2콘솔 앱을 실행

3. db 파일 생성 및 연결

4. 테이블 생성 

drop table if exists member CASCADE;
create table member
(
    id  bigint generated by default as identity,
    name varchar(255),
    primary key(id)
);

테이블 생성 시 값 설정

id는 java에서는 Long, db에서는 bigint 타입

generated by default as idnetity는 db에 데이터를 넣을 때 id값을 넣지 않으면 자동으로 넣어줌.

name은 가변문자열(varchar)

id는 primarykey

 

5. 데이터 삽입

insert into member(name) values('홍길동');
insert into member(name) values('김아무개');

 

6. 테이블 조회

select * from member;

 

 

[순수 JDBC]

- 환경설정

(고대 개발자들의 개발 방식임)

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

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

(java에서 db를 사용하기 위해서는 반드시 jdbc driver가 있어야 함)

스프링 부트 데이터베이스 연결 설정 추가

 

[application.properties]

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

 

여기까지 하면 스프링에서 db접근하기 위한 setting을 전부 해준다.

 

이제 실제 DB와 연동된 Repository와 Config를 수정하면 된다.

(인프런 강의자료 참조)

- 개방-폐쇄 원칙 (OCP, Open-Closed Principle)

  * 확장에는 열려있고, 수정(변경)에는 닫혀있다.

- 스프링의 DI(Dependencies Injection)을 사용하면 기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경 할 수 있다.

- 회원을 등록하고 DB에 결과가 잘 입력되는지 확인하자.

- 데이터를 DB에 저장하므로 스프링 서버를 다시 실행해도 데이터가 안전하게 저장된다.

 

[스프링 통합 테스트]

(통합 테스트 시 실제 DB와 연동하므로 Test DB를 만들어서 그 안에서 테스트를 진행한다.)

1. 먼저 MemberServiceTest파일을 복사해서 MemberServiceIntegrationTest라는 파일로 생성하자.

2. @SpringBootTest, @Transactional을 클래스에 붙여준다.

3. MemberService와 MemberRepository를 @Autowired를 통해 주입받는 코드로 고친다.

4. @BeforeEach, @AfterEach에 해당하는 메소드를 지운다. (@Transactional 때문에)

5. 테이블에 저장된 모든 데이터를 지우고, 회원가입() 메소드를 호출하여 테스트한다.

(여기서 다시 회원가입()을 호출하면 데이터가 중복이기 때문에 @AfterEach의 deleteAll() 메소드를 호출하는 메소드를 만들어서 테스트마다 호출되도록 하는 방법도 있지만!!!!!!

@Transactional을 붙이는 것 만으로도 그 역할을 해준다.!!!!!

 

DB는 Transaction이라는 개념이 존재한다. 클라이언트에서 쿼리를 요청하면 날린 쿼리를 개별적으로 Transaction이라고 하며, DB는 Commit을 하지 않으면 DB에 반영되지 않는다. 이것도 자동Commit과 그냥 Commit을 해야하는 모드가 존재함.)

@Transactional 을 테스트 전에 이 트랜잭션을 시작하고, 테스트 진행 후 끝나면 Rollback(취소)해준다.

 

정리)

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

@Transactional : 테스트 케이스에 이 애노테이션이 있으면, 테스트 시작 전에 트랜잭션을 시작하고, 테스트 완료 후에 항상 롤백한다. 이렇게 하면 DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다.

매 테스트마다 Rollback하는 트랜잭션이 실행되며, 이 Rollback 트랜잭션은 @Test에서만 유효하다.

 

그렇다면 단위테스트(Unit Test)와 통합테스트(Integration Test)를 비교했을 때 실제 DB와 연동된 부분을 테스느 해야 하기 때문에 단위 테스트는 의미가 없지 않나? 그렇지 않다. 우선 테스트 실행 시간에서 엄청나게 차이가 크며, 가급적이면 단위테스트(Unit Test)를 잘 짜야하고 시간도 절약되며, 잘짜면 통합테스트도 잘 동작한다. 따라서 단위 테스트를 정교하게 작성하는것이 좋다.

 

[스프링 JdbcTemplate]

- 순수 Jdbc와 동일한 환경설정을 하면 된다.

- 스프링 JdbcTemplate과 MyBatis 같은 라이브러리는 JDBC API에서 본 반복 코드를 대부분 제거해준다. 하지만 SQL은 직접 작성해야 한다.

(@Autowired는 단일 생성자의 경우 생략 가능하다.)

(이름이 Template인 이유는 디자인 패턴 중 Template 패턴이 많이 활용되어 그렇다.)

 

[JdbcTemplateMemberRepository]

 

[SpringConfig]

 

 

[JPA]

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

- JPA를 사용하면 SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환을 할  수 있다.

- JPA를 사용하면 개발 생산성을 크게 높일 수 있다.

 

[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-jdbc'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'com.h2database:h2'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

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

 

[spring boot 설정(resources/application.properties)]

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none

 

(JPA는 ORM 기술이다. Object Relational Mapping, 객체와 관계를 매핑)

[JPAMemberRepository]

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import org.springframework.beans.factory.annotation.Autowired;

import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;

public class JPAMemberRepository implements MemberRepository {

    // JPA 사용 시 반드시 필요함. DB관련 처리를 함.
    private final EntityManager em;

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

    // identify로 검색하는 것이 아니라면 jpql을 작성해주어야 함.
    @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();
    }
}

 

[MemberService]

@Transactional 어노테션을 클래스에 추가

 

[SpringConfig]

package hello.hellospring;

import hello.hellospring.repository.JPAMemberRepository;
import hello.hellospring.repository.JdbcTemplateMemberRepository;
import hello.hellospring.repository.MemberRepository;
import hello.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 DataSource dataSource;
//
//    @Autowired
//    public SpringConfig(DataSource dataSource) {
//        this.dataSource = dataSource;
//    }

    private EntityManager em;

    @Autowired
    public SpringConfig(EntityManager em) {
        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만 사용해도 개발 생산성이 정말 많이 증가하고, 개발해야할 코드도 확연히 줄어든다. 여기에 스프링 데이터 JPA를 사용하면, 기존의 한계를 넘어 마치 마법처럼, 리포지토리에 구현 클래스 없이 인터페이스 만으로 개발을 완료할 수 있다. 그리고 반복 개발해온 기본 CRUD 기능도 스프링 데이터 JPA가 모두 제공한다. 스프링 부트와 JPA라는 기반 위에, 스프링 데이터 JPA라는 환상적인 프레임워크를 더하면 개발이 정말 즐거워진다. 지금까지 조금이라도 단순하고 반복이라 생각했던 개발 코드들이 확연하게 줄어든다.

단, 스프링 데이터 JPA는 JPA를 편리하게 사용하도록 도와주는 기술이다. 따라서 JPA를 선행학습 후에 스프링 데이터 JPA를 학습해야 한다.

 

 

[SpringDataJPAMemberRepository]

(인터페이스밖에 없는데 이것을 구현한 구현체는 어디에??? 스프링에서 스프링 빈을 자동으로 만들어준다.)

 

[SpringConfig]

- 동작원리

스프링 데이터 JPA가 SpringDataJPAMemberRepository의 구현체를 스프링 빈으로 자동 생성한다. 이때 프록시라는 기술을 통해 만듬. 이것을 우리는 Injection해서 사용했다. 

SpringDataJPAMemberRepository가 상속하고 있는 JpaRepository 인터페이스 정의를 들어가보면 findAll, findAllById, saveAll 등 기본적인 메소드가 존재하면 JpaRepository<T, ID>가 상속하고 있는 PagindAndSortingRepository -> CrudRepository에 정의를 보면 

save, findById 등의 메소드가 있다. 

해당 강의에서 김영한 강사님이 시그니처를 맞춰놨기 때문에 우리가 따로 구현 없이도 호출해서 사용 가능한 것이다. 다만, findByName()의 경우만 따로 메소드를 선언해주면 된다.

공통 인터페이스에 해당하는 기능들은 이렇게 시그니처만 맞춰주면 바로 사용 가능하다.

반면, 공통적이지 않은 부분들의 기능들은 특정 규칙을 만족시켜주면 선언된 메소드 역시 자동 생성해준다.

규칙은 다음과 같다.

 

// select m from Member where m.name = ?
@Override
Optional<Member> findByName(String name);

주석처리한 쿼리에 맞는 메소드를 만들었다고 해보자.

이때 findBy뒤에 나오는 'Name'을 작성하면 name인자와 mapping하여 그것을 통해 find하게 된다.

만약 name과 id로 찾고 싶다면? And를 이용해서 findByNameAndId(String name, Long id) 이런식으로 작성하게 되면 자동 생성해준다.

 

※ 실무에서는 JPA와 스프링 데이터 JPA를 기본으로 사용하고, 복잡한 동적 쿼리는 Querydsl이라는 라이브러리를 사용하면 된다. Querydsl을 사용하면 쿼리도 자바 코드로 안전하게 작성할 수 있고, 동적 쿼리도 편리하게 작성할 수 있다. 이 조합으로 해결하기 어려운 쿼리는 JPA가 제공하는 네이티브 쿼리를 사용하거나, 앞서 학습한 스프링 JdbcTemplate를 사용하면 된다.