본문 바로가기
Java/Spring입문

Spring 입문(14) 스프링 DB 접근 기술: JPA

by 이쟝 2023. 5. 5.
인프런의 김영한님의 스프링입문 강의를 듣고 정리한 내용입니다.
스프링 입문 강의
1. H2 데이터베이스 설치
2. 순수 Jdbc(스프링 통합테스트)
3. 스프링 Jdbc Template
4. JPA
5. 스프링 데이터 JPA

4. JPA

JPA란? ORM(Object Relational Mapping) 기술로 객체와 관계형 데이터를 mapping 하는 기술
JPA는 기존의 반복 코드는 물론이고, 기본적인 SQL도 JPA가 직접 만들어서 실행해준다.

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

4-1. build.gradle 파일에 JPA, h2 데이터베이스 관련 라이브러리를 추가한다.

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	//implementation 'org.springframework.boot:spring-boot-starter-jdbc'
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // data-jpa추가
	runtimeOnly 'com.h2database:h2'
}

- `spring-boot-starter-data-jpa`는 내부에 jdbc 관련 라이브러리를 추가하기 때문에 jdbc는 제거해도 된다. 

- 라이브러리를 추가하고 gradle refresh(공룡모양 버튼 누르기)를 해준다.

4-2. 스프링 부트에 JPA 설정을 추가한다.(application.properties)

src > main > resources > application.properties

spring.jpa.show-sql=true와 spring.jpa.hibernate.ddl-auto=none을 추가해준다. 
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
  1. `show-sql`: JPA가 생성하는 SQL을 출력한다.
  2. `ddl-auto`: JPA가 테이블을 자동으로 생성(객체를 보고 테이블을 생성)하는 기능을 제공할 때 `none`으로 사용하지 않음을 표시한다. (member 테이블이 있기 때문에 none으로 한다.) 
    • `create`를 사용하면 엔티티 정보를 바탕으로 테이블을 직접 생성해준다.
    • JPA는 인터페이스로, hibernate와 같은 구현체를 우리가 사용한다.

4-3. JPA 라이브러리를 사용하기 위해 Member클래스에 @Entity 추가

src > man > java > hello > hellospring > domain > Member

JPA를 사용하려면 Entity를 Mapping해야한다. 
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를 추가하게 되면  Member 클래스는 JPA가 관리하는 Entity가 되게 된다. 
- pk(primary key)를 설정하기 위해 @Id 애너테이션으로 pk설정을 한다. 
- @GeneratedValue(strategy = GenerationType.IDENTITY: 기존에 설정했던 것처럼 id값을 자동으로 생성해 pk값으로 지정해준다.
-> IDENTITY 전략: 기본 키(primary key) 생성을 DB에 위임하는 전략(DB가 알아서 생성)으로 DB에 값을 저장하고 나서 기본 키 값을 구할 수 있을 때 사용한다. 

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

이렇게 각 애너테이션을 사용해 DB와 mapping을 시도하고, 이를 바탕으로 자동으로 SQL문을 작성한다. 

4-4. JPA를 사용해 회원 레포지토리를 작성한다.

src > main > java > hello > hellospring > JpaMemberRepository 
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) {
        return null;
    }

    @Override
    public Optional<Member> findById(Long id) {
        return Optional.empty();
    }

    @Override
    public Optional<Member> findByName(String name) {
        return Optional.empty();
    }

    @Override
    public List<Member> findAll() {
        return null;
    }
}
JPA는 작동시 EntityManager로 동작한다. 이는 스프링 부트가 JPA 라이브러리를 받으면 생성하는 것으로, 이전 강의에서 dataSource를 주입받은 것처럼(DI) 생성된 EntityManager를 주입받아야 한다.

EntityManager를 사용해 엔티티를 데이터베이스에 등록/수정/삭제 조회할 수 있다.
EntityManager는 내부에 데이터베이스 커넥션(dataSource)를 유지하면서 데이터 베이스와 통신한다.

save( )

    @Override
    public Member save(Member member) {
        em.persist(member);
        return member;
    }
- save( )는 em.persist( )로 구현할 수 있다.
- 인자로 회원 객체를 넘겨주면 JPA가 insert query를 자동으로 작성해서 DB에 넣고 id를 생성, 이를 받아 pk로 세팅한다. 
- id를 부여받아 DB에 저장된 회원 객체를 반환하기만 하면 된다. 

 

findById( ) 

    @Override
    public Optional<Member> findById(Long id) {
        Member member = em.find(Member.class, id);
        return Optional.ofNullable(member);
    }
- findByID( )는 em.find( )로 구현할 수 있다. 
- 인자로 조회할 type, 조회 식별할 pk값이 필요하고, 반환된 회원 객체가 null일 수 있기 때문에 Optional.ofNullable로 감싸 반환한다. 
- 위와 같은 메서드를 사용 시 자동으로 select query를 작성, 실행한다.

 

findByName( )

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

 

- findByName( )는 pk기반으로 검색하는 함수가 아니기 때문에 다른 방법을 사용한다.
- em.createQuery( ) 를 사용해 JPQL을 작성해서 검색한다. 이때 기존에 작성했던 SQL처럼 테이블을 대상으로 회원의 id, name을 검색하는 것이 아니라, 회원 객체(엔티티)를 대상으로 쿼리를 보내 해당하는 객체 자체를 검색한다. 
- 기존에는 id, name을 select 해서 새롭게 회원 객체를 생성해 이를 세팅하고 세팅된 객체를 반환했다고 한다면, 이제는 검색해서 반환된 회원 객체 자체를 반환할 수 있다.
- 콜론(:)을 사용해서 데이터가 추가될 곳을 지정하고 setParameter( )를 호출해서 데이터를 동적으로 바인딩한다. 
- getResultList( )를 통해서 리스트 형태로 받아 result변수에 저장한다.

- JPQL(Java Persistence Query Language): 테이블이 아닌 엔티티 객체를 대상으로 검색하는 객체지향 쿼리로 SQL과 비슷한 문법을 가지며, JPQL은 결국 SQL로 변환된다.
- JPA는 JPQL을 분석해 SQL을 생성한 후 DB에서 조회한다. 

findAll( )

    @Override
    public List<Member> findAll() {
        return em.createQuery("select m from Member m", Member.class)
                .getResultList();
    }
- em.createQuery( )를 이용해 검색된 모든 결과를 getResultList( )를 사용해 리스트 형태로 받아 바로 반환한다. 

이렇게 JPA를 사용해 레포지토리를 구현하게 되면 새로 회원 객체를 생성하고 검색 결과를 이에 세팅하여 반환하는 것과 같은 중복된 과정이 삭제되고, SQL을 직접 작성하는 과정도 축소되었다. 

 

주의해야 할점!

JPA의 모든 데이터 변경은 트랜잭션 안에서 수행되어야 하기 때문에 MemberService 클래스에 @Transactional을 추가해줘야 한다.

import org.springframework.transaction.annotation.Transactional;

@Transactional
public class MemberService {
- Transaction : DB의 상태를 변화시키기 위해 수행하는 작업의 단위
- Commit : 하나의 트랜잭션이 성공적으로 끝났고, DB가 일관성 있는 상태에 있을 때 하나의 트랜잭션이 끝났다는 것을 알려주는 연산. 수행했던 트랙잭션이 로그에 저장되고, 후에 트랜잭션 단위로 Rollback하는 것을 도와준다.
- RollBack : 하나의 트랜잭션 처리가 비정상적으로 종료되었을 경우, 트랜잭션을 처음부터 다시 시작하거나, 부분적으로만 연산된 결과를 다시 취소 시킨다. 
- @Transactional : 데이터를 저장하거나 변경할 때 항상 트랜잭션이 있어야 한다. (데이터 변경 시(회원 가입)에 트랜잭션 필요)

- 이때 스프링은 해당 클래스 메서드를 실행할 때 트랜잭션을 시작하고, 메서드가 정상 종료되면 트랜잭션을 커밋, 런타임 예외가 발생한다면 롤백을 수행한다.
-> JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행되어야 한다!!!

4-5. SpringConfig 파일을 수정해서 JpaMemberRepository를 사용하도록 설정한다.

 src > main > java > hello > hellospring > SpringConfig
package hello.hellospring;

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

    private final 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);
    }
}
- 기존에 JdbcMemberRepository를 사용하던 코드를 주석처리하고, JpaMemberRepository를 사용하도록 해당 레포지토리를 생성, 반환한다.
- 이때 스프링에서 생성되는 EntityManager를 주입한다.

4-6. JPA를 사용한 레포지토리가 정상적으로 작동하는지 통합 테스트로 확인한다.

src > test > service > MemberServiceIntegerationTest

테스트가 정상적으로 동작하는 것을 확인할 수 있고, JPA가 생성한 SQL 또한 확인할 수 있다.

@Commit 애너테이션을 추가 후 테스트를 수행하면 h2 DB에 데이터가 저장되는 것을 볼 수 있다!!

    @Test
    @Commit
    void 회원가입() {