JPA(Java Persistence API)

  • MyBatis(SQL Mapper) vs JPA(Object Relational Mapping)

    • 관계형 데이터베이스와 객체지향 프로그래밍 언어의 패러다임 차이

    • 관계형 데이터베이스는 데이터를 어떻게 저장할지에 초점을 맞춘 기술

      • 데이터베이스 쿼리 작성
      • 객체 모델링보다는 테이블 모델링에만 집중, 객체를 단순히 테이블에 맞추어 데이터 전달 역할만 하는 형태
      • 관계형 데이터베이스가 SQL만 인식 가능하기 때문에 각 테이블마다 기본적인 CRUD SQL를 매번 생성해야하는 상황
    • 객체지향 프로그래밍 언어는 메시지를 기반으로 기능과 속성을 한 곳에서 관리하는 기술

      • 상속, 1:N 등 다양한 객체 모델링을 데이터베이스로는 구현이 불가능
  • JPA

    • 서로 지향하는 바가 다른 2개 영역(객체지향 프로그래밍 언어와 관계형 데이터베이스)을 중간에서 패러다임을 일치를 시켜주기 위한 기술
    • 개발자는 객체지향적으로 프로그래밍을 하고, JPA가 이를 관계형 데이터베이스에 맞게 SQL을 대신 생성해서 실행

Spring Data JPA

  • JPA는 인터페이스로서 자바 표준명세서
    • 인터페이스인 JPA를 사용하기 위해서는 구현체가 필요하다.
      • 대표적으로 Hibernate, Eclipse Link 등
      • 구현체들을 좀 더 쉽게 사용하고자 추상화시킨 Spring Data JPA라는 모듈을 이용하여 JPA 기술을 다룬다.
      • JPA <- Hibernate <- Spring Data JPA
      • Spring 쪽에서는 Spring Data JPA를 개발하고 권장하고 있다.
      • Spring Data JPA가 등장한 이유
      • 구현체 교체의 용이성
        • Hibernate 외에 다른 구현체로 쉽게 교체하기 위함
        • Spring Data Redis를 사용하는 경우, Redis -> Jedis -> Lettuce로 쉽게 가능
      • 저장소 교체의 용이성
        • 관계형 데이터베이스 외에 다른 저장소로 쉽게 교체하기 위함
        • 관계형 데이터베이스에서 MongoDB로 교체가 필요한 경우 Spring Data JPA -> Spring Data MongoDB 의존성 교체로 사용가능
        • Spring Data의 하위 프로젝트들은 기본적인 CRUD의 인터페이스가 같기 때문이다.

Spring Data JPA 적용

  • spring-boot-starter-data-jpa와 com.h2database:h2 의존성 등록
    1. spring-boot-starter-data-jpa
      • 스프링 부트용 Spring Data JPA 추상화 라이브러리
      • 스프링 부트 버전에 맞춰 자동으로 JPA 관련 라이브러리들의 버전을 관리
    2. h2
      • 인메모리 관계형 데이터베이스
      • 별도의 설치가 필요없이 프로젝트 의존성만으로 관리할 수 없다.
      • 메모리에서 실행되기 때문에 어플리케이션을 재시작할 때마다 초기화된다는 점을 이용하여 테스트 용도로 사용

의존성 추가 및 확인

JPA 기능을 테스트 하기위한 기반 코드 작성

  • domain 패키지

    • 소프트웨어에 대한 요구사항 혹은 문제영역
    • xml에 쿼리를 담고, 클래스는 오로지 쿼리의 결과만 담던 일을 모두 도메인 클래스라고 불리는 곳에서 해결
  • Posts

    • 실제 DB의 테이블과 매칭될 클래스로 Entity 클래스라고 한다.
    • DB 데이터에 작업할 경우 실제 쿼리를 날리기보다, Entity 클래스의 수정을 통해 작업한다.
    1. JPA 어노테이션

      • @Entity

        • 테이블과 링크될 클래스

        • 기본값으로 클래스의 CamelCase 이름을 언더스코어 네이밍(_) 으로 테이블 이름을 매칭
          (SalesManager.java -> sales_manager)

        • 해당 클래스의 인스턴스 값들이 언제 어디서 변해야 하는지 코드상으로 명확하게 구분할 수 없기 때문에, 차후 기능 변경 시 복잡해 진다.

        • 그래서 Entity 클래스에서는 Setter 메서드를 만들지 않는다.

        • 대신 해당 필드의 값 변경이 필요할 경우 명확히 그 목적과 의도를 나타낼 수 있는 메서드를 추가

        • Setter가 없음에도 값을 채워 DB에 삽입할 수 있는 이유는 생성자 대신 @Builder를 통해 제공되는 빌더 클래스를 사용한다.

        • 생성자와 빌더의 차이

          • 생성자

            • 지금 채워야 할 필드가 무엇인지 명확히 지정할수 없다.

              public Example(String a, String b) {
                this.a = a;
                this.b = b;
              }
              new Example(b, a); // 로 호출하게 되는 경우 문제
          • 빌더

            • 필드별 set메서드를 제공하고 있어 명확히 구분된다.

              Example.builder()
                .a(a)
                .b(b)
                .build();
      • @Id

        • 해당 테이블 PK 필드를 나타낸다.
      • @GeneratedValue

        • PK의 생성 규칙
        • 스프링부트 2.0에서는 GenerationType.IDENTITY 옵션을 추가해야 auto_increment가 된다.
      • @Column

        • 테이블의 컬럼을 나타내며 굳이 선언하지 않더라도 해당 클래스의 필드는 모두 컬럼이 된다.
        • 사용하는 이유는, 기본 값 외에 추가로 변경이 필요한 옵션이 있으면 사용
        • 문자열의 경우 VARCHAR(255)가 기본값인데, 사이즈를 500으로 늘리고 싶은경우, 타입을 TEXT로 변경하고 싶은 경우 사용
    2. Lombok 어노테이션

      • @Getter

        • 클래스 내 모든 필드의 Getter 메서드를 자동생성
      • @NoArgsConstructor

        • 기본 생성자 자동 추가
        • public Posts() {} 와 같은 효과
      • @Builder

        • 해당 클래스의 빌더 패턴 클래스를 생성
        • 생성자 상단에 선언 시 생성자에 포함된 필드만 빌더에 포함
@Getter
@NoArgsConstructor
@Entity
public class Posts {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 500, nullable = false)
    private String title;

    @Column(columnDefinition = "TEXT", nullable = false)
    private String content;

    private String author;

    @Builder
    public Posts(String title, String content, String author) {
        this.title = title;
        this.content = content;
        this.author = author;
    }
}
  • PostsRepository
    • JpaRepository를 상속받은 PostsRepository 인터페이스를 생성
    • MyBatis등에서 Dao라 불리는 DB Layer로 JPA에서는 Repository라 부른다.
    • JpaRepositoy<Entity 클래스, PK 타입> 를 상속하면 기본적인 CRUD 메서드가 자동으로 생성된다.
    • @Repository를 추가할 필요가 없다.
    • 다만, Entity 클래스와 기본 Entity Repository 클래스는 같은 위치(domain 패키지)에 있어야 한다.
    • Entity 클래스는 기본 Repositorty 없이 제대로된 역할을 할 수 없다.
public interface PostsRepository extends JpaRepository<Posts, Long> {
}

Spring Data JPA 테스트 코드 작성

  • PostsRepositoryTest 클래스
    1. @SpringBootTest
      • 해당 어노테이션을 선언하는 경우 H2 데이터베이스를 자동으로 실행
      • 테스트도 H2 데이터베이스를 기반으로 실행된다.
    2. @After
      • Junit에서 단위 테스트가 끝날 때마다 수행되는 메서드를 지정
      • 보통 배포 전 전체 테스트를 수행할 때 테스트간 데이터 침범을 막기 위해서 사용
      • 여러 테스트가 동시에 수행되면 테스트용 데이터베이스인 H2에 데이터가 그대로 남아 있어 다음 테스트 실행 시 테스트 실패할 수 있다.
    3. postsRepository.save
      • 테이블 posts에 insert/update 쿼리를 실행
      • id 값이 있다면 update, id 값이 없다면 insert 쿼리가 실행된다.
    4. postsRepository.findAll
      • 테이블 posts에 있는 모든 데이터를 조회해오는 메서드
@RunWith(SpringRunner.class)
@SpringBootTest
public class PostsRepositoryTest {

    @Autowired
    PostsRepository postsRepository;

    @After
    public void cleanup() {
        postsRepository.deleteAll();
    }

    @Test
    public void getBoard() {
        // given
        String title = "테스트 게시글";
        String content = "테스트 본문";

        postsRepository.save(
                Posts.builder()
                .title(title)
                .content(content)
                .author("seok@gmail.com")
                .build()
        );

        // when
        List<Posts> postsList = postsRepository.findAll();

        // then
        Posts posts = postsList.get(0);
        Assertions.assertThat(posts.getTitle()).isEqualTo(title);
        Assertions.assertThat(posts.getContent()).isEqualTo(content);
    }
}

쿼리로그 확인 방법

  • src/main/resources 경로의 application.properties 파일에 설정 추가
# jpa setting
spring.jpa.show_sql=true

spring.jpa.show_sql=true 설정

  • create table 쿼리에 id bigint generated by default as identity라는 옵션으로 생성
    • 이는 H2의 쿼리 문법이 적용되었기 때문이다.
    • H2 쿼리 문법에서 MySQL 버전으로 변경
# jpa setting
spring.jpa.show_sql=true
# change QueryLog format from h2 to mysql
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect

  • id bigint not null auto_increment로 변경됨을 확인

+ Recent posts