웹 프로그래밍/공부, 연습

[강좌]스프링 입문 - 6. 스프링 DB 접근 기술(2)

이 글은 인프런에서 김영한 님의 "스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술"을 수강 후 개인적으로 공부한 내용을 정리한 게시글입니다. 잘못된 점이나 부족한 부분이 있다면 언제든 지적 부탁드립니다.

 

해당 강의는 이곳에서 수강할 수 있습니다.

 

이전 강의는 스프링의 DB 접근 기술 중 첫 번째 강의로 H2 데이터베이스의 사용을 통해 Jdbc와 JdbcTemplate에 대해 알아보았습니다.

이번 강의에서는 스프링 JPA에 대해 학습합니다.

 

 

1. JPA

 

지난 강의에서 스프링과 DB를 Jdbc로 연동하고, JdbcTemplate를 사용해 기존의 코드에서 더 간단한 코드로 DB에 접근해 보았습니다.

 

JPA를 사용하면 기존의 반복 코드를 줄일 뿐 아니라 기본적인 SQL도 JPA가 직접 만들어서 DB에 접근할 수 있다는 장점이 있습니다.

이렇게 JPA를 사용하면 SQL과 데이터 중심 설계에서 객체 중심의 설계로 패러다임을 전환할 수 있고, 개발 생산성을 크게 향상시킬 수 있습니다.

 

이를 실제 코드로 확인해 봅니다.

build.gradle 파일에 다음과 같은 코드를 추가하여 JPA를 사용하기 위한 라이브러리를 추가합니다.

 

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'
	}
}

 

기존 코드에서 'implementation 'org.springframework.boot:spring-boot-starter-data-jpa''부분이 추가되었습니다.

해당 라이브러리에는 jdbc와 관련된 라이브러리 또한 포함되어 있으므로 위의 라이브러리는 주석처리를 해줍니다.

 

라이브러리를 추가하고 gradle refresh를 해줍니다.

 

다음엔 resources/application.properties 파일에 다음과 같은 코드를 추가하여 스프링 부트가 JPA를 사용하는 것과 관련된 설정을 해줍니다.

 

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

 

'show-sql' 설정은 JPA가 SQL을 생성할 때 이를 출력해주며, 'ddl-auto'는 JPA가 테이블을 자동으로 생성(객체를 보고 테이블을 생성)하는 기능을 제공할 때 이를 'none'으로 사용하지 않음을 표시합니다.(이번 실습에서는 이미 생성된 Member 테이블을 사용합니다.) 'none' 대신 'create'로 해당 기능을 사용할 수도 있습니다. 이때는 JPA가 엔티티 정보를 바탕으로 테이블을 직접 생성합니다.

 

참고로 JPA에 대해 조금 깊이 들어가면 JPA는 인터페이스로, hibernate와 같은 구현체를 우리가 사용합니다.

 

JPA는 ORM기술으로 이는 Object Relational Mapping을 의미하며, 객체와 관계형 데이터를 mapping 하는 기술입니다.

이를 사용하기 위해 기존의 domain/Member 클래스에 다음과 같은 애노테이션을 추가해줍니다.

 

package hello.hellospring.domain;

import javax.persistence.*;

@Entity
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    //@Column(name="username")
    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;
    }
}

 

객체(Member)와 DB를 연동하기 위해 @Entity 애노테이션을 추가하였습니다.

pk를 설정하기 위해 id에 @Id 애노테이션으로 pk 설정을 하고, @GeneratedValue(strategy = GenerationType.IDENTITY)를 사용하면 기존에 우리가 설정하였던 것처럼 id값을 자동으로 생성하여 pk값으로 지정해 줍니다.

주석 처리된 @Column 애노테이션을 사용하면 name 멤버 변수를 DB의 'username'과 mapping 할 수도 있습니다.

 

이처럼 각 애노테이션을 사용하여 DB와 mapping을 시도하고 이를 바탕으로 자동으로 SQL문을 작성합니다.

 

이제 JPA를 사용하여 회원 리포지토리를 작성해 봅니다.

repository 폴더에 다음과 같은 JpaMemberRepository 파일을 생성, MemberRepository를 구현하여 다음과 같은 코드를 작성합니다.

 

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

 

JPA는 작동시 EntityManager로 동작합니다. 이는 스프링 부트가 JPA 라이브러리를 받으면 생성하는 것으로, 이전 강의에서 dataSource를 주입받은 것처럼 생성된 manager를 주입받으면 됩니다.

 

save() 함수는 다음과 같이 em.persist()로 간단하게 구현할 수 있습니다. 인자로 회원 객체를 넘겨주기만 하면 JPA가 insert query를 자동으로 작성해서 db에 넣고 id를 생성, 이를 받아 pk로 세팅합니다. id를 부여받아 DB에 저장된 회원 객체를 반환하기만 하면 됩니다.

 

findById() 함수는 em.find() 함수로 구현할 수 있습니다. 인자로 조회할 type, 조회 시 식별할 pk값이 필요하며, 반환된 회원 객체를 Optional로 감싸 반환합니다. 위와 같은 메서드를 사용 시 자동으로 select query를 작성, 실행합니다.

 

findByName() 함수는 위의 findById()와 같이 pk기반으로 검색하는 함수가 아니기 때문에 다른 방법을 사용합니다.

em.createQuery() 메서드를 사용해 안에 JPQL을 작성하여 검색합니다. 이때 기존에 작성했던 SQL처럼 테이블 대상으로 회원의 id, name을 검색하는 것이 아니라, 회원 객체(엔티티)를 대상으로 쿼리를 보내 해당하는 객체 자체를 검색합니다.

기존에는 id, name을 select 하여 새롭게 회원 객체를 생성해 이를 세팅하고 세팅된 객체를 반환하였다고 한다면, 이제는 검색하여 반환된 회원 객체 자체를 반환할 수 있습니다.

 

findAll() 함수 또한 em.createQuery()를 사용하여 검색된 모든 결과를 리스트 형태로 받아 바로 반환합니다.

 

위와 같이 JPA를 사용하여 리포지토리를 구현하였더니 새로 회원 객체를 생성하고 검색 결과를 이에 세팅하여 반환하는 것과 같은 중복된 과정이 삭제되고, SQL을 직접 작성하는 과정 또한 축소되었습니다.

 

 

추가적으로 JPA의 모든 데이터 변경은 트랜잭션 안에서 수행되어야 합니다. 

이를 위해 회원 서비스에 다음을 추가합니다.

 

import org.springframework.transaction.annotation.Transactional;

@Transactional
public class MemberService {}

 

회원 서비스 안의 join()함수는 데이터를 추가하는 기능이므로 데이터 변경 시 트랜잭션이 필요하고, 이를 @Transational 애노테이션을 추가하여 해결할 수 있습니다.

이때 스프링은 해당 클래스 메서드를 실행할 때 트랜잭션을 시작하고, 메서드가 정상 종료되면 트랜잭션을 커밋, 런타임 예외가 발생한다면 롤백을 수행합니다.

 

이제 SpringConfig 파일을 다음과 같이 수정하여 JpaMemberRepository를 사용하도록 설정합니다.

 

package hello.hellospring;

import hello.hellospring.repository.*;
import hello.hellospring.service.MemberService;
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);
    }
}

 

기존에 JdbcMemberRepository를 사용하던 코드를 주석처리하고 JpaMemberRepository를 사용하도록 해당 리포지토리를 생성, 반환합니다. 이때 스프링에서 생성되는 EntityManager를 주입해 줍니다.

 

이제 JPA를 사용한 리포지토리가 정상적으로 동작하는지 통합테스트를 통해 확인합니다.

'6. 스프링 DB 접근 기술(1)'에서 작성한 통합테스트로 DB와 연동된 리포지토리의 통합 테스트가 가능합니다.

h2 DB를 연결하고 MemberServiceIntegrationTest 파일을 실행하여 테스트 결과를 확인합니다.

 

 

다음과 같이 테스트가 정상적으로 동작하는 것을 확인할 수 있습니다.

또한 앞에서 진행한 설정으로 로그를 통해 JPA가 생성한 SQL 또한 확인할 수 있습니다.

 

 

우리가 직접 작성하지 않은 SQL문이 생성, 실행된 것을 확인할 수 있습니다. 

 

@Commit 애노테이션을 추가 후 테스트를 수행하면 h2 DB에 데이터가 저장되는 것 또한 확인할 수 있습니다.

 

 

 

2. 스프링 데이터 JPA

 

드디어 스프링 DB 접근 기술의 마지막 단계인 스프링 데이터 JPA입니다.

 

스프링 부트와 JPA만을 사용해도 개발해야할 코드가 확연히 줄고, 개발 생산성 또한 많이 증가합니다.

여기서 스프링 데이터 JPA를 사용하면 리포지토리를 구현 클래스 없이 인터페이스만을 작성하여 개발을 완료할 수 있습니다. 이제까지 반복해서 개발해온 CRUD 또한 스프링 데이터 JPA가 기본으로 제공합니다.

 

따라서 개발자는 단순하고 반복적인 코드를 작성하지 않아도 되며 핵심 비즈니스 로직을 구현하는데에만 집중할 수 있습니다.

 

주의해야할 사항은 스프링 데이터 JPA는 JPA를 편리하게 사용하도록 도움을 주는 기술이므로, JPA에 대해 먼저 학습한 후 이를 학습해야 합니다.

 

스프링 데이터 JPA를 사용하기 위해서는 앞에서 설정했던 JPA 설정을 그대로 사용합니다.

 

repository 폴더에 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{
    @Override
    Optional<Member> findByName(String name);
}

 

SpringDataJpaMemberRepository 인터페이스는 스프링 데이터 JPA에서 제공하는 JpaRepository를 받습니다. 이때 저장되는 Member 클래스와 pk의 type인 Long을 명시해주고, JpaRepository와 같이 MemberRepository 또한 받습니다.

 

findByName() 메서드만 인터페이스를 생성해 줍니다.

 

이렇게 인터페이스를 생성하면 구현체를 만들 필요는 없습니다.(?!)

이제 스프링 데이터 JPA 리포지토리를 사용하도록 스프링 설정을 변경합니다.

SpringConfig 파일을 다음과 같이 변경합니다.

 

package hello.hellospring;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.*;
import hello.hellospring.service.MemberService;
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;
    
    private final MemberRepository memberRepository;

    public SpringConfig(MemberRepository memberRepository){
        this.memberRepository = memberRepository;
    }

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

 

리포지토리의 구현체를 만들지 않았으니 생성할 필요가 없습니다. 스프링 부트가 실행할 때 스프링 컨테이너에서 MemberRepository를 가져와 회원 서비스에 이를 주입하는 것이 전부입니다.

 

여기까지만 수정을 마치고 통합테스트로 동작을 확인해 봅니다.

 

 

테스트가 정상적으로 잘 동작하는 것을 확인할 수 있습니다. 로그로 출력되는 SQL문 또한 JPA 리포지토리를 사용할 때의 테스트 결과와 동일합니다.

 

이렇게 단순히 인터페이스만을 생성하여 동작이 가능한 것은 인터페이스를 생성할 때 JpaRepository를 extends했기 때문입니다. 해당 인터페이스를 내려받으면 스프링 데이터 JPA에서 자동으로 인터페이스의 구현체를 만들고, 스프링 빈으로 자동으로 등록합니다. 

우리는 자동으로 등록된 회원 리포지토리 스프링 빈을 SpringConfig에서 회원 서비스에 주입, 이를 사용하였습니다.

 

스프링 데이터 JPA가 제공하는 클래스를 확인해 봅니다.

 

이미지 출처는 해당 강의 자료입니다

 

우리가 인터페이스를 생성할 때 내려받은 JpaRepositoryPagingAndSortingRepository인터페이스를 내려받고, 이는 CrudRepository, Repository를 내려받습니다. 해당 인터페이스들에는 save(), findOne(), findAll(), findById()과 같이 우리가 이전에 직접 구현했던 기능들 뿐 아니라, 이 외에도 기본적인 CRUD를 수행하거나 단순 조회를 수행하는 기능과 count(), delete()와 같은 기능 또한 구현되어있습니다.

 

findByName() 이나 findByEmail()과 같이 공통으로 뽑을 수 없는 메서드들은 따로 인터페이스 안에 위와 같이 

'Optional<Member> findByName(String name);' 처럼 명시하기만 하면 스프링 데이터 JPA에서 자동으로 

'select m from Member m where m.name =?'과 같은 JPQL 쿼리를 생성하여 이를 사용할 수 있습니다.

(예를 들어 findByEmail()이라면 'Optional findByEmail(String email);' 과 같은 코드를 추가하기만 하면 됩니다)

 

스프링 데이터 JPA는 위와 같이 인터페이스를 통한 기본적인 CRUD 기능뿐 아니라 findByName()과 같이 메서드 이름만으로 조회를 가능하게 하고, 페이징 기능 또한 제공하며 구현 클래스를 작성하지 않고 인터페이스만을 작성하여 개발이 끝나도록 합니다.

 

 

드디어 스프링 DB 접근 기술에 대한 강의가 끝났습니다.

실무에서는 JPA와 스프링 데이터 JPA를 기본으로 사용하고 복잡한 동적 쿼리는 Querydsl과 같은 라이브러리를 사용합니다. 이것만으로 해결하기 어려운 쿼리는 JPA가 제공하는 네이티브 쿼리 또는 앞서 학습한 JdbcTemplate을 활용할 수 있습니다.(그렇기 때문에 스프링 데이터 JPA 뿐만 아니라 앞서 학습한 기술이나 SQL문 작성과 같은 전반적인 DB기술을 다룰 수 있어야 합니다.)

 

김영한 강사님의 JPA 강의 또한 인프런에서 수강하실 수 있습니다.

 

이번 강의에서는 JPA와 스프링 데이터 JPA를 활용한 스프링 DB 접근 기술을 학습하였습니다.

다음 강의에서는 AOP에 대해 학습합니다.