영운's 블로그

스프링 입문(3) - 스프링 DB 접근 기술(순수 JDBC, JdbcTemplate, 스프링 통합 테스트, JPA, 스프링 데이터 JPA) 본문

카테고리 없음

스프링 입문(3) - 스프링 DB 접근 기술(순수 JDBC, JdbcTemplate, 스프링 통합 테스트, JPA, 스프링 데이터 JPA)

오영운(you88) 2022. 5. 24. 18:01

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9E%85%EB%AC%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8

 

[무료] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링 웹 애플리케이션 개발 전반을 빠르게 학습할 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

해당 글은 김영한님의 스프링 입문을 강의를 들으며 개인적으로 정리한 내용입니다.

모든 자료의 출처는 김영한님의 스프링 입문 강의임을 미리 밝힙니다.

 

실습 코드: https://github.com/you88311/spring-example1

 

스프링 DB 접근 기술

 1.  h2데이터베이스

h2는 공부할때 사용하기 좋은 작은 데이터베이스

https://www.h2database.com 에서 다운 받을 수 있다.

먼저 접속하면 home 디렉토리(윈도우의 경우 C:\Users\사용자이름)에 test.mv.db만들어진다.

h2.bat 실행하고 jdbc url에 jdbc:h2:tcp://localhost/~/test 를 입력하면

클라이언트로 해당 db 접근 가능하다.

 

sql문으로 테이블 만든다.

자바의 Long은 DB에서는 bigint에 해당.

간단하게 id와 name으로 이루어진 테이블이다.

create table member
(
    id bigint generated by default as identity,
    name varchar(255),
    primary key (id)
);
insert into member(name) values('spring1')
insert into member(name) values('spring2')
insert into member(name) values('spring3')
insert into member(name) values('spring4')

 

김영한 개발자님은 자기가 입력한 sql문을 보통 최상위 디렉토리(우리 실습에서는 hello-spring)에 sql이라는 디렉토리 만들고 ddl.sql이라는 파일 만들어서 저장해 놓는다고 한다. 그러면 git에 올라가며 db변경 내역을 확인할 수 있다.

 

 2. 순수 Jdbc

위에서 만든 DB와 스프링을 연동하는 방법은 크게 두 가지.

  • 순수 jdbc
  • 스프링 JdbcTemplate

 

순수 jdbc는 JDBC API로 직접 코딩하는 것.

jdbc란 java가 DB에 접근하게 해주는 '드라이버'

 

요즘은 사용하지 않는 방식이라고 한다.

김영한 개발자님 말로는 고대(?)의 개발자들이 사용하던 방식이라고..

 

 

어쨌든 이렇게 DB와 스프링을 연동하면 그동안 MemoryRepository 인터페이스를 구현한 메모리만 사용하는 MemberMemoryRepository를 DB를 사용하기에 전원을 껐다가 켜도 계속 사용이 가능한 JdbcMemoryRepository로 바꿔야 한다.

여기서 스프링이 자바의 객체지향의 다형성을 극대화하는 OCP(Open-closed Principle)가 나타남

OCP

 

 

JdbcMemberRepository도 memberService 인터페이스를 구현했기에

SpringConfig에서 MemoryMemberRepository를 JdbcMemberRepository로 

바꿔주기만 하면 됨. 이렇게 조립하는 @Configuration만 바꾸면 되지

별도로 Controller나 Service는 건들지 않아도 된다!!

 

[SpringConfig]

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

 

 3. 스프링 JdbcTemplate

 

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

스프링 JdbcTemplate MyBatis 같은 라이브러리는 JDBC API에서 본 반복 코드를 대부분 제거해준다.

하지만 SQL은 직접 작성해야 한다.

 

[findbyid, findbyname, findall]

 

 

jdbcTemplate.query의 매개변수인 memberRowMapper

람다는 좀 공부해야할 듯하다. 주석처럼 코드 쓰고 alt+Enter누르면 위에처럼 자동으로 람다코드를 만들어 주기도 한다.

 

[save]

SimpleJdbcInsert는 별도로 sql문을 안짜도 자동으로 sql 쿼리를 만들어 주는 함수이다.

이건 일단 이렇게 넘어가라고 하셨다. Jdbc template doc보면 보다 수월하게 이해할 수 있다고 하심.

 

이제 조립하는 부분인 SpringConfig를 바꿔줘야한다.

마찬가지로 JdbcTemplateMemberRepository도 MemberRepository를 상속받은 것이기에

메소드 memberRepository()의 return 값만 바꿔주면 된다.

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

 

4. 스프링 통합 테스트

단위 테스트 vs 스프링 통합 테스트

 

단위 테스트는 스프링 이용하지 않고 자바만 사용하여 최소 단위별로 하는 테스트

별도로 db연동을 하지 않는다.

 

스프링 통합 테스트 스프링 이용해서 아예 test용 db와 연결해서 테스트하는 방식이다.

테스트 앞단에 @SpringBootTest annotation을 붙여주면 됨.

 

@Transactional은 우리가 순수 자바코드로 테스트 만들었을 때 @AfterEach, @BeforeEach처럼

@Test하나 하고 다음 테스트에 영향 없도록 롤백하게 하는 annotation.

실제로는 @Transactional을 하면 아예 DB에 데이터를 커밋하지 않는다고 한다.

 

만약에 @Test이후에 그걸 DB에 유지하고 싶으면

해당 Test annotation 위에 @Commit을 추가해 주면 됨

 

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

 

+

resources/applications.properties 에

우리가 사용하는 h2 DB의 주소를 적어줌.

 

build.gradle에도 h2추가해주고

 

+

중복회원 test할 때 우리 db에 있는 회원 내용을 먼저 다 지워줘야한다.

아니면 test용 db에 있는 거랑 내용 겹쳐서 test가 오류발생가능.

원래 실제 db랑 test용 db는 별도로 있고 test용 db는 아무것도 저장되어 있지 않은 상태에서 돌리기도 한다.

 

 

결론: 근데 의외로 김영한 개발자님은 스프링이 제공하는 통합 테스트보다 단위 테스트가 더 좋다고 하셨다.

통합 테스트로만 테스트할 수 있는 경우는 애초에 설계가 잘못된 경우가 많다고 한다.

 

단위 테스트는 최소의 단위로 테스트하기에 속도가 훨씬 빠르다.

우리처럼 조그맣게 실습용 db의 경우 말고 실제 운영중인 서비스의 경우 테스트db라도 테스트할게 많기에 시간이 오래걸림. 따라서 단위 테스트가 훨씬 효율적이다.

 

5. JPA

JPA는 기존의 반복 코드는 물론이고, 기본적인 SQL문 직접 만들어서 실행해주기에 개발 생산성을

크게 높일 수 있다.

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

JPA 표준 인터페이스고 그걸 구현한게 hibernate

물론 hibernate 같은 구현체는 여러 개가 있지만 가장 범용적으로 사용되는 것이 hibernate

 

JPA는 ORM(Object(객체), Relational database(관계형 데이터베이스) Mapping)

gradle.build에서starter-data-jpa 추가

이거 자체도 jdbc, jpa등을 포함하는 라이브러리이기에  starter-jdbc를 지워도 무방하다.

이후 gradle-refresh(오른쪽 위 코끼리 모양)로 적용해줘야함.

 

[resources/application.properties]

두 줄 추가.

show-sql은 jpa가 날리는 sql문을 보여주고

hibernate.ddl-auto는 객체를 자동으로 table로 만드는 기능이다.

우리는 테이블 만들고 시작하기에 해당 기능을 none으로 설정

 

[JpaMemberRepository]

 

JPA는 EntityManager로 모든 동작을 한다.

스프링부트가 EntityManger을 우리가 연동시킨 DB와 연결시켜서 만들어주면

우리는 그걸 여기서 insert 받는 것(생성자의 매개변수로)

 

 

 

구체적인 메소드의 경우 순수 jdbc와 달리 sql문을 save와 findById에서는 사용하지 않는 것을 볼 수 있음. findByName, findAll의 경우 JPA내에서 별도로 요구하는게 있어서 간단하게 SQL문 사용하지만 그래도 순수 jdbc보다는 훨씬 작성할 코드가 적음.

+

이후 배우는 스프링 데이터 JPA는 findByID, findAll에서도 sql문 사용하지 않는다고 한다.

 

[MemberService]

 

JPA를 사용할 경우 데이터를 저장하거나 변경할 때 Transactional을 사용해야 함.

MemberService가 데이터 건드리는건 join이니까 join메소드 위에만 붙여도 되기는 함.

 

[SpringConfig]

 

EntityManager 만들고 생성자 만들고 마지막 bean인 MemberRepository 고쳐주고

 

Test 실행하면

아래와 같이 우리가 작성한 적 없은 SQL문을 자동으로 만들어 주는 것을 알 수 있음.

 

6. 스프링 데이터 JPA

 

JPA를 더 쉽게 해주는 것이 스프링 데이터 JPA라는 라이브러리

리포지토리에 구현 클래스 없이 인터페이스 만으로 개발할 수 있고 반복 개발해온 기본 CRUD 기능도 스프링 데이터 JPA가 모두 제공한다. 그래서 단순 반복 개발 코드들을 크게 줄일 수 있음.

주의할 점은 스프링 데이터 JPA만을 공부한다면 실제 JPA의 오류를 해결하지 못할 가능성이 높다. 

따라서 JPA를 먼저 공부하고 그걸 베이스로 스프링 데이터 JPA를 공부하는 것을 추천한다고 하심.

 

[SpringDataJpaMemberRepository]

 

자바는 인터페이스의 상속은 다중 상속이 가능. 여기서는 JpaRepository, MemberRepository 다중 상속

이렇게 JpaRepository를 상속하면 스스로 구현체를 만들어서 스프링 빈에 자동으로 등록

 

[SpringConfig]

 

 

SpringConfig의 memberRepository()를 주석처리한다.

그리고 MemoryMemberRepository의 @Repository 어노테이션도 주석처리한다.

그렇지 않으면 MemoryMemberRepository와 SpringDataJpaRepository가

둘다 빈으로 등록되어 Member에 대한 저장소가 2개가 괴기에 애러 발생

 

이후 MemberServiceIntegrationTest를 진행

 

우리는 save, findbyId, findall등을 만든적이 없음에도 test가 통과되는 것을 확인할 수 있다.

  

이는 SpringDataJpaMemberRepository가 JpaRepository를 상속했기 때문에 가능하다.

JpaRepository는 spring에서 제공하는 repository로 실제 구현된 것을 살펴보면

 

[JpaRepository]

 

이미 많은 기본적인 함수들이 JpaRepository에 구현되어 있음을 확인할 수 있다.

 

 

하지만 findByName 등은 구현되어 있지 않다. 이것은 해당 함수는 findAll 같은 함수와 다르게 비즈니스 별로 구현 방법이 다를 수 있기 때문이다.

이러한 것들은 별도로 구현해야 한다.

그런데 이것도 모두 구현할 필요는 없다. 메소드 이름만 치면 어느 정도 구현해 주는 기능이 있기 때문이다.

 

 

아래처럼 findByName이라고 설정만 하면 알아서

select m from Member m where m.name = ? 이라는 sql을 만들어 제공해준다.

이렇게 메서드 이름만으로도 조회 기능을 제공해준다!!

 

결론

순수 Jdbc : SQL문 일일히 모두 만들어야 함. 반복 코드도 엄청 많다. 요즘 절대 사용 안  함.

JdbcTemplate: JDBC의 반복 코드를 대부분 제거해준다. 하지만 SQL문은 직접 작성해야 한다.

JPA : SQL문의 작성도 줄여준다. 여전히 select에 한해서는 여전이 SQL문을 직접 작성해야 한다.

스프링 데이터 JPA: 아예 class 구현할 필요 없고 인터페이스선에서 구현 끝.

 

 

 

 

Comments