[inflearn] 스프링 입문

15. 스프링 JPA (Java Persistence API)

슬픈 야옹이 2023. 4. 30. 20:27

JPA (Java Persistence API)는 자바 표준 인터페이스로, 자바 프로그램과 DB 사이의 연동 기능을 제공하는 ORM 기술이다.

 

ORM (Object-Relation Mapping) : 객체지향 프로그래밍에서, 객체와 DB 릴레이션(테이블)을 매핑하는 것.

 

JPA는 자바 표준 인터페이스고, 다양한 벤더해서 이를 구현한 라이브러리를 배포한다.

실습에서는 오픈소스 JPA 라이브러리인 hibernate를 이용한다.

 

JPA를 통해 개발자는 데이터 및 SQL에 대해 크게 신경쓰지 않고 객체 중심의 프로그램 설계를 할 수 있어 개발 생산성이 크게 향상된다.

 

JPA는 현재 전세계적으로 널리 쓰이는 API이기 때문에, 이를 다룰 줄 알면 많은 도움이 될 것이다.

 

 

 

 

환경 설정

build.gradle에 jpa 관련 설정을 추가한다.

data-jpa에 jdbc 라이브러리가 포함되어 있기 때문에 기존 jdbc는 없어도 된다.

 

build.gradle

// 제거
implementation 'org.springframework.boot:spring-boot-starter-jdbc'

// 추가
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

 

 

 

스프링 부트에 jpa 관련 설정 추가

 

 

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

 

show-sql : JPA가 생성한 sql 쿼리를 출력한다.

 

ddl-auto : jpa는 엔티티에 대한 테이블 자동 생성 기능을 지원한다.

하지만 실습에서는 미리 만들어놓은 테이블을 사용하므로 none으로 설정한다.

create 를 사용하면 엔티티 정보를 바탕으로 테이블도 직접 생성해준다.

 

 

 

 

 

JPA 엔티티 매핑

Member.java를 다음과 같이 수정한다.

Member.java

 

Member.java

package hdxian.hdxianspring.domain;

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

@Entity
@Table(name = "member2")
public class Member {

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

}

@Entity

- 엔티티로 지정. 이 클래스의 데이터를 바탕으로 테이블을 생성 및 관리한다.

 

@Table(name = "member2")

- 강의에는 없는 내용이지만, 나는 사용하는 테이블 이름이 다르므로 별도로 테이블 이름을 지정했다.

지정하지 않으면 member 테이블 대상으로 sql이 생성된다.

 

@Id

- 테이블의 PK로 지정

 

@GeneratedValue(strategy = GenerationType.IDENTITY)

- identity 전략 : 테이블에 insert할 때 값을 직접 정해서 넣어주는 게 아니라 자동으로 값이 정해지도록 하는 것.

 

 

 

JPA 리포지토리 생성 및 적용

JpaMemberRepository 클래스를 생성한 뒤 다음과 같이 작성한다.

 

 

 

JpaMemberRepository.java

package hdxian.hdxianspring.repository;

import hdxian.hdxianspring.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 {

    private final EntityManager em;

    @Autowired // 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 em

- JPA는 EntityManager를 이용해 DB 관련 정보를 관리한다. EntityManager는 JPA 환경에서 스프링이 자동으로 생성해 관리한다.

- JPA를 사용하려면 이 EntityManager를 인젝션받아 사용한다.

 

 

em.persist()

- @Entity로 지정한 객체를 인수로 전달하면 해당 객체의 데이터를 바탕으로 sql 쿼리를 자동으로 생성해 테이블에 insert 해준다.

 

em.find()

- 객체를 바탕으로 테이블에서 데이터를 조회한다. 전달하는 첫 번째 인수는 class 정보를, 두 번째 인수로는 PK(@Id로 지정된 멤버)를 전달한다.

 

 

pk가 아닌 특정 조건으로 검색할 때는 jpql이라는 객체지향 sql을 작성해서 전달해야 한다. Member 등의 객체를 기준으로 데이터를 조회한다.

 

 

 

MemverService 클래스 선언부에 @Transactional을 추가한다.

 

 

 

MemverService.java

package hdxian.hdxianspring.service;

import hdxian.hdxianspring.domain.Member;
import hdxian.hdxianspring.repository.MemberRepository;
import hdxian.hdxianspring.repository.MemoryMemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;

@Transactional
public class MemberService {

    // private final MemberRepository repository = new MemoryMemberRepository();
    private final MemberRepository repository;

    public MemberService(MemberRepository repository) {
        this.repository = repository;
    }

    // 회원 가입 기능
    public Long join(Member member) {

        // 중복된 이름으로는 가입 불가.
        checkNameDuplication(member);

        repository.save(member);
        return member.getId();
    }

    private void checkNameDuplication(Member member) {
        repository.findByName(member.getName()) // findbyName()의 리턴 타입은 Optional<Member>
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });
    }

    public List<Member> findMembers() {
        return repository.findAll();
    }

    public Optional<Member> findOne(Long memberId) {
        return repository.findById(memberId);
    }


}

스프링은 @Transactional이 붙은 클래스의 메서드를 실행할 때 항상 트랜잭션을 시작하고, 메서드가 정상 종료되면 실행 결과를 커밋한다. 만일 런타임 오류가 발생하면 롤백한다.

 

JPA를 통한 모든 데이터 변경은 트랜잭션 내에서 이루어져야 한다.

 

 

JpaMemberRepository를 사용하도록 SpringConfig를 변경한다.

 

SpringConfig.java

package hdxian.hdxianspring;

import hdxian.hdxianspring.repository.*;
import hdxian.hdxianspring.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;
    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);
    }

}

 

EntityManager를 인젝션받도록 설정하고, JpaMemberRepository를 리턴하도록 변경한다.

 

 

 

테스트 내용은 같으므로 생략한다.

 

+)

테스트 결과 일부

테스트 결과를 보면 Hibernate: ... 라고 뜨면서 실행된 sql 쿼리가 출력된다. JPA가 생성한 sql 쿼리다.

이전에 application.properties에서 show-sql=true 라고 설정했었기 때문에 지금처럼 결과에 sql이 출력된다.