Java - Spring &&n SpringBoot

Spring - DB 연동(2) : JPA, Spring Data JPA

TerianP 2021. 12. 25.
728x90

이전 글에서 JDBC 와 JdbcTemplate 를 알아보았다. JDBC -> JdbcTemplate 로 바뀌면서 pstmt, conn, rs 등 중복으로 사용되던 코드가 많이 줄고 간결해졌다. 그러나 아직까지 꼭 해야하는게 있었으니 바로 SQL 문을 꼭 코드에 삽입해야한다는 점이다.

물론 SQL 문을 넣고 코드를 작성할 수 있지만, JPA 라는 최신 기술이 있고, 이를 활용하면 SQL 문을 JPA가 알아서 처리해주기 때문에 훨씬 편하게 개발할 수 있다. 간단하게 정리하자면,

  • JDBC : 어렵다, 복잡하다, 코드 작성하기 힘들다ㅠ
  • JdbcTemplate : jdbc 의 발전 형태, 코드가 훨씬 간결해졌고, 보기 쉬워졌다. 다만 여전히 SQL 을 직접 넣어야한다. 
  • JPA : 기존의 반복 코드는 물론이고, 기본적인 SQL도 JPA가 직접 만들어서 실행해준다. 이 때문에 SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환 할 수 있다.또한 개발 생산성 역시 향상된다.

 

1. ORM 과 JPA, JPA 사용하기 위한 환경설정

1) 개념 설명

  • ORM : ORM(Object-relational mapping) 이란 객체는 객체대로 설계하고, 관계형 데이터베이스(RDB)는 관계형 데이터베이스대로 설계하는 방식이다. 이때 ORM 프레임워크가 중간에서 둘을 매핑해주는 역할을 한다. 즉, 자바 클래스와 DB 테이블을 매핑하는 기능을 한다.

 

  • JPA : JPA(Java Persistence API) 란 JAVA에서 제공하는 ORM 을 사용하기 위한 '인터페이스의 모음' 이다. 이런 JPA 를 사용하기 위해서 JPA를 구현한 ORM 프레임워크들이 Hibernate, EclipseLink, DataNucleus 이다.
    • JPA 도 JDBC API 를 사용하는건 맞는데 개발자가 직접 SQL query JPA 가 중간에 JDBC API 를 사용하여 SQL 을 자동으로 만들어서 DB 에 넣어준다는 점이 일반적인 JDBC 와 다르다.
    • JPA 는 인터페이스!!! -> Hibernate, EclipseLink, DataNucleus 는 JPA 인터페이스를 구현한 구현 class !!
    • 기본적으로 사용되는 프레임워크는 Hibernate 로 생각됨

JPA 사용 시 JPA 내부에서 JDBC API 를 사용하여 SQL 을 호출하며 DB 와 통신한다.

 

  • Spring Data JPA : Spring 프레임워크에서 제공하는 JPA 를 보다 쉽게 사용하기 위한 모듈
    • 구현체 교체의 용이성과 저장소 교체의 용이성을 모두 갖고 있어서 다른 Spring에서는 Hibernate, EclipseLink, DataNucleus 등 보다 더 많이 쓰인다고 한다.

요런 그림

 


2) JPA 사용을 위한 환경 설정

아래 2가지 파일만 고치면 된다!

 

  • build.gradle
// JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

 

  • application.properies
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none

2. JPA : JPQL 사용 

  • JPQL(Java Persistence Query Language)란 테이블이 아닌 엔티티명을 기준으로하는 객체를 대상으로 검색하는 객체지향 쿼리!!
  • 보통 @Entity(name="엔티티명") 어노테이션을 통해 설정하는데 name 속성을 생략하면 해당 어노테이션이 붙은 클래스명이 엔티티 명이 된다.
  • 이 부분은 추후 좀 더 정리 예정입니다

 

1) JpaMemberRepository

  • JPA는 EntityManager 로 동작!! => Spring boot 가 여러 세팅해두었던 정보와 DB 연결 정보들을 활용해서 EntityManager 생성
  • 각 컬럼명으로 DB 정보를 검색할때 사용가능한 것이 JPQL, 일반적인 SQL 과 비슷한데 딱 하나 다른점이라고 하면 일반 SQL이 중간에 * 로 테이블을 선택하고 컬럼명을 넣는다면 JPQL에서는 MM 같은 일종의 별칭으로 설정한다.
  • 즉 일반적인 쿼리와는 다르게 객체를 기준으로 검색을 하며, 쿼리문을 작성할 때는 해당 객체의 별칭을 사용해서 데이터를 검색한다.
  • 파라미터를 사용법중 이름 기준 파라미터를 사용하는 경우 JPQL에서 앞에 : 를 붙여준다
import HJproject.Hellospring.domain.Member;
import org.hibernate.annotations.common.reflection.XMember;

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

public class JpamemberRepository implements MemberRepository{

    private final EntityManager em;
    // JPA는 EntityManager 로 동작
    // Spring boot 가 여러 세팅해두었던 정보와 DB 연결 정보들을 활용해서 EntityManager 생성

    public JpamemberRepository(EntityManager em) {
        this.em = em;
    }


    @Override
    public Member save(Member member) {
        em.persist(member); // persist 는 영속화 즉, member 를 저장한다는 의미 => SQL 따위 필요없다
        return member;
    }

    @Override
    public Optional<Member> findByCode(Long code) {
        Member member = em.find(Member.class, code);
        return Optional.ofNullable(member);
    }

    @Override
    public Optional<Member> findByName(String name) {
        // 각 컬럼명으로 DB 정보를 검색할때 사용가능한 것이 JPQL
        // 일반적인 SQL 과 비슷한데 딱 하나 다른점이라고 하면 일반 SQL이 중간에 * 로 테이블을 선택하고 컬럼명을 넣는다면
        // 이때 쿼리에서의 MM은 일종의 별칭으로 어떻게 설정해줘도 상관은 없지만 꼭 필요하다.
        // 또한 파라미터를 사용법중 이름 기준 파라미터를 사용하는 경우 JPQL에서 앞에 : 를 붙여준다
        List<Member> result = em.createQuery("select MM from Member MM where MM.name=:name", Member.class)
                .setParameter("name", name)
                .getResultList();

        return result.stream().findAny();
    }

    @Override
    public List<Member> findAll() {
        List<Member> result = em.createQuery("select m from Member m ", Member.class)
                .getResultList();
        return result;
    }
}

 

 

2) Member : 여기서 엔티티명은 Member

import javax.persistence.*;

@Entity // name 속성을 생략했기 때문에 JPQL에서 사용할 엔티티명은 Member 로 됨
public class Member {

    // @Id : DB 에서 primary key 로 설정된 컬럼 맵핑
    // @GeneratedValue : pk 값의 생성 방식 -> 현재는 DB 에서 자동 생성임으로 이에 해당하는 Identity
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long code; // 시스템에서 저장 & 식별 구분 하기 위한 code

    // @Column : 컬럼명과 코드에서 받는 변수명 매핑 -> (코드변수 = 컬럼명)
    @Column(name = "name")
    private String name; // 고객 이름

    private String id; // 고객 id

    private String passwd; // 고객 passwd

    public Long getCode() {
        return code;
    }

    public void setCode(Long code) {
        this.code = code;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getPasswd() {
        return passwd;
    }

    public void setPasswd(String passwd) {
        this.passwd = passwd;
    }
}

 

3) memberService 수정

@Transactional
public class memberService {

    private final MemberRepository memberRepository;
    
    }

 

4) SpringConfig 수정

@Configuration // 스프링 빈에 등록하기 위한 설정파일이라는 Annotation
public class SpringConfig {

    private DataSource dataSource;

    @PersistenceContext // EntityManager 의 경우 이런 어노테이션이 필요함
    EntityManager em;

    @Autowired
    public SpringConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Bean // Bean 에 등록되어야하는 객체라는 의미의 Annotation
    public memberService memberService(){
        return new memberService(memberRepository());
        // 아래에서 생성된 memberRepository 객체를 넣어줌. 의존성 주입 DI
    }

    @Bean
    // 여기서 MemberRepository 는 인터페이스, 이번에는 JpamemberRepository 가 구현체
    // 따라서 구현체를 객체로 가져와야함
    public MemberRepository memberRepository(){
//        return new MemoryMemberRepository();
//        return new JdbcMemberRepository(dataSource);
//        return new JdbcTemplateMemberRepository(dataSource);
        return new JpamemberRepository(em);

    }
}

3. Spring Data JPA

  • 스프링 프레임워크에서 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트 => 즉 위에서는 JPA 만 딱 사용했다면 여기는 스프링 프레임워크에서 추가로 지원하는 JPA 라이브러리를 활용해서 개발하는 것.
  • 데이터 접근 계층(DAO)을 개발할 때 따로 구현 클래스 없이 인터페이스만 작성해도 개발을 완료!! => 지루하게 반복되는 CRUD 문제를 세련된 방법으로 해결
  • 이는 인터페이스만 작성하면 실행시점에 스프링 데이터 JPA가 구현 객체를 동적으로 생성해서 주입하는 방법이라고 한다.
  • 쉽게 말해서 JpaRepository 를 상속받는 인터페이스 하나와 내가 메인으로 갖고 있던 MemberRepository 2개만 상속받은 후 Configration 에서 memberRepsoitory 만 의존관계 설정해주면 그대로 끝!

 

1) SpringDataJpaMemberRepository

import HJproject.Hellospring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository{
    // JpaRepository 인터페이스를 상속받아옴 => 이때 첫번째 파라미터는 Entity 명 , 두번째 파라미터는 PrimaryKey 타입
    // 추가로 MemberRepository 로 상속

    // JpaRepository 를 상속받은 인터페이스가 있다면 JpaRepository 가 자동으로 구현체를 생성해서 springBean 에 등로함


    // SpringDataJpa 가 메서드 이름을 통해 JPQL 을 자동으로 만들고 DB에 질의해준다.
    // 예를 들어 findById 의 경우 findBy 뒤에 나오는 이름(Id)을 검색 하고자하는 기준으로 잡고, 쿼리가 생성되고 질의된다.

    // 아래 내용을 JPQL 로 만들면 : select m from Member m where m.id = ?
    @Override
    Optional<Member> findById(String ID);

    // JPQL : select m.code , m.id from Member m = ?
    Optional<Member> findByCodeAndId(Long code, String ID);
}

 

2) SpringConfig

@Configuration // 스프링 빈에 등록하기 위한 설정파일이라는 Annotation
public class SpringConfig {

    // JpaRepository 가 SpringDataJpaMemberRepository 구현체를 자동으로 생성해서 Bean에 등록을 했기 때문에
    // memberRepository 만 의존관계 설정해주면 됨!!!
    private final MemberRepository memberRepository;

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

    @Bean // Bean 에 등록되어야하는 객체라는 의미의 Annotation
    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);
//    }
}

저장
완료!


4. 내 맘대로 수정하기 : JPA 사용해서 추가하기

  • Spring Data JPA 말고 일반적인 JPA만 사용해서 내 맘대로 코드를 짜보도록 한다.
    • 이번에 추가할 것은 역시 유저 ID 와 Passwd 가 저장되도록하고, 이를 이용한 통합테스트까지 완료해보겠다.
  • 사실 Spring Data JPA 가 더 쉽지만, 솔직히 강의를 들어도 Spring Data JPA 가 어떤건지 아직 잘 이해가 안되고, Spring Data JPA 를 이해하기 위해서는 기본적인 JPA 를 공부하는게 우선이라고 생각되었기 때문이다.

 

1) member class


import javax.persistence.*;

@Entity
public class Member {

    // @Id : DB 에서 primary key 로 설정된 컬럼 맵핑
    // @GeneratedValue : pk 값의 생성 방식 -> 현재는 DB 에서 자동 생성임으로 이에 해당하는 Identity
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long code; // 시스템에서 저장 & 식별 구분 하기 위한 code

    // @Column : 컬럼명 코드 변수 매핑 -> name = "컬럼명"
    @Column(name = "name")
    private String name; // 고객 이름


    /*5. 내 맘대로 구현하기*/
    @Column(name = "id")
    private String id; // 고객 id

    @Column(name = "passwd")
    private String passwd; // 고객 passwd

    public Long getCode() {
        return code;
    }

    public void setCode(Long code) {
        this.code = code;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getPasswd() {
        return passwd;
    }

    public void setPasswd(String passwd) {
        this.passwd = passwd;
    }
}

 

2) JPAmemberRepository class


import HJproject.Hellospring.domain.Member;
import org.hibernate.annotations.common.reflection.XMember;

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

public class JpamemberRepository implements MemberRepository{

    private final EntityManager em;
    // JPA는 EntityManager 로 동작
    // Spring boot 가 여러 세팅해두었던 정보와 DB 연결 정보들을 활용해서 EntityManager 생성

    public JpamemberRepository(EntityManager em) {
        this.em = em;
    }


    @Override
    public Member save(Member member) {
        em.persist(member); // persist 는 영속화 즉, member 를 저장한다는 의미 => SQL 따위 필요없다
        return member;
    }

    @Override
    public Optional<Member> findByCode(Long code) {
        Member member = em.find(Member.class, code);
        return Optional.ofNullable(member);
    }

    @Override
    public Optional<Member> findByName(String name) {
        // 각 컬럼명으로 DB 정보를 검색할때 사용가능한 것이 JPQL
        // 일반적인 SQL 과 비슷한데 딱 하나 다른점이라고 하면 일반 SQL이 중간에 * 로 테이블을 선택하고 컬럼명을 넣는다면
        // 이때 쿼리에서의 MM은 일종의 별칭으로 어떻게 설정해줘도 상관은 없지만 꼭 필요하다.
        // 또한 파라미터를 사용법중 이름 기준 파라미터를 사용하는 경우 JPQL에서 앞에 : 를 붙여준다
        List<Member> result = em.createQuery("select MM from Member MM where MM.name=:name", Member.class)
                .setParameter("name", name)
                .getResultList();

        return result.stream().findAny();
    }

    
    /* 5. 내 맘대로 구현하기 */
    @Override
    public Optional<Member> findById(String ID) {
        List<Member> result = em.createQuery("select m from Member m where m.id=:id", Member.class)
                .setParameter("id", ID)
                .getResultList();

        return result.stream().findAny();
    }

    @Override
    public Optional<Member> findByPasswd(String passwd) {
        List<Member> result = em.createQuery("select m from Member m where m.passwd=:passwd", Member.class)
                .setParameter("passwd", passwd)
                .getResultList();

        return result.stream().findAny();
    }
    /* 여기까지 */
    
    @Override
    public List<Member> findAll() {
        List<Member> result = em.createQuery("select m from Member m ", Member.class)
                .getResultList();
        return result;
    }
}

 

3) SpringConfig 파일은 이전 2.JPA 부분과 동일

 

4) memberServiceIntergrationTest class

  • 크게 달라진 부분은 없다.

import HJproject.Hellospring.domain.Member;
import HJproject.Hellospring.repository.JpamemberRepository;
import HJproject.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.test.annotation.Commit;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.Column;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;


@SpringBootTest // 실제 spring boot가 실행되며 동작하는 테스트
@Transactional // DB 트랜잭션 시 테스트 코드가 동작한 후 다시 Rollback 되도록 함
class memberServiceIntegrationTest {


    // 아래는 DI 의 또다른 방식인 Field 선언 방식
    // 필드에서 생성자 필요없이 Autowired 로 엮어서 DI 하는 방식
    @Autowired memberService memberService;
    @Autowired MemberRepository memberRepository;




    @Test
    void 회원가입() { // 테스트 코드에서는 한글로 메서드 잡아도 무방 -> 실제코드에 포함 X

        // given : 주어지는 데이터
        Member member = new Member();
        member.setName("통합테스트JPA");

        /*  내 맘대로 구현하기   */
        member.setId("통합테스트ID JPA");
        member.setPasswd("통합테스트PW JPA");

        // when : 검증하고자 하는 코드드
       Long saveCode = memberService.join(member);


        // then : 예상 결과(기댓값)
        Member findCode = memberService.findOne(member.getCode()).get();
        // findOne 메서드를 사용해 memberCode 가져오기

        assertThat(member.getId()).isEqualTo(findCode.getId());
        // 내가 작성한 값과 실제 저장된 값 비교 -> member.getId : findCode.getId

        System.out.println(findCode.getCode()+ " " +findCode.getName() + " "+ findCode.getId()+ " "+findCode.getPasswd());


    }

    @Test
    public void 중복_아이디_체크(){

        //given
        Member member1 = new Member();
        member1.setName("spring");
        member1.setId("spring123");
        member1.setPasswd("4567");

        Member member2 = new Member();
        member2.setName("자바");
        member2.setId("spring123");
        member2.setPasswd("1234");



        /* 예외상황 확인 방법 2 */
        memberService.join(member1);

        // when : 예외가 발생하는 상황은 동일한 아이디로 회원가입을 요청하는 경우

        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));

        // then
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 ID 입니다");
    }

}

5) 결과 확인하기

회원 가입
DB 조회 결과
JPA 로 DB 에 데이터 저장 시 인텔리J에 출력되는 내용 // Hibernate 를 통해 insert 되는 것을 확인할 수 있다.


내가 지정한 name, ID, passwd 로 저장되는 것을 확인!
트랜젝션 시작, 중복확인, 롤백 모두 잘 되는 것을 확인!


이번 글로 JDBC, JDBCTemplate, jpa , spring-data-jpa 에 대해서 간단하게 알아보았다.

사실 모두 내용이 엄청나게 정말 엄청나게 많은데 줄이고 줄인 가운데 글을 적어보았다. 앞으로 정말 공부할게 한가득인게 살짝 겁도 난다ㅠㅠ

 

어쨌거나 다음 내용인 AOP 까지해서 스프링에 대해서 대략 맛보기가 끝나면 라즈베리로 이동해서 본격적인 토이프로젝트를 시작할지 아니면 기본 공부를 더 할지 정하려고 한다.

 

그러고보니 오늘 크리스마스네ㅠㅠㅠ 늦었지만 메리크리스마스!! 

 


참고

https://gmlwjd9405.github.io/2019/08/04/what-is-jpa.html

 

[JPA] JPA란 - Heee's Development Blog

Step by step goes a long way.

gmlwjd9405.github.io

 

https://velog.io/@adam2/JPA%EB%8A%94-%EB%8F%84%EB%8D%B0%EC%B2%B4-%EB%AD%98%EA%B9%8C-orm-%EC%98%81%EC%86%8D%EC%84%B1-hibernate-spring-data-jpa

 

JPA는 도대체 뭘까? (orm, 영속성, hibernate, spring-data-jpa)

JPA는 도대체 무엇일까요? orm, jdbc, 영속성, hibernate, ... 관련 지식까지 모두 파해쳐봅니다.

velog.io

https://suhwan.dev/2019/02/24/jpa-vs-hibernate-vs-spring-data-jpa/

 

JPA, Hibernate, 그리고 Spring Data JPA의 차이점

개요 Spring 프레임워크는 어플리케이션을 개발할 때 필요한 수많은 강력하고 편리한 기능을 제공해준다. 하지만 많은 기술이 존재하는 만큼 Spring 프레임워크를 처음 사용하는 사람이 Spring 프레

suhwan.dev

 

댓글