엔티티 클래스와 JpaRepository

Crow·2022년 6월 19일
0

Spring Boot

목록 보기
3/6

4. 엔티티 클래스와 JpaRepository

Sprinmg Data JPA가 개발에 필요한것은 단지 두 종류의 코드만으로 가능함

  • JPA를 통해서 괸리하게 되는 객체(이하 엔티티객체(Entity Object))를 위한 엔티티 클래스
  • 엔티티 객체들을 처리하는 기능을 가진 Repository

이중 Repository는 Spring Data JPA에서 제공하는 인터페이스로 설계함 스프링 내부에서 자동으로 객체를 생성하고 실행하는 구조라서 단순히 인터페이스 하나 정의하는 작업만으로 충분함

(기존에 Hibernate는 모든코드를 직접 구현하고 트랜잭션 처리가 필요했음 하지만 Spring Data JPA는 자동으로 생성되는 코드를 이용하므로 단순한 CRUD나 페이지 처리등에 코드를 개발하지 않아도 됨)

간단한 메모 기능을 하나 정의해서 이를 구현하고, 테스트 해보겠음

엔티티 클래스 작성

프로젝트에 entity패키지를 추가하고, Memo 클래스 정의함

Memo 클래스

package com.example.bootex2.entity;

import lombok.*;

import javax.persistence.*;

@Entity
@Table(name = "tbl_memo")
@ToString
public class Memo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long mno;

}

Memo 클래스는 엔티티 클래스로 마치 데이터베이스의 테이블과 같은 구조로 작성해줌

@Entity

엔티티 클래스는 Spring Data JPA에서는 반드시 @Entity라는 어노테이션을 추가해야한 함

해당 클래스가 엔티티를 위한 클래스이며, 해당 클래스의 인스턴스들이 JPA로 관리되는 엔티티 객체라는 것을 의미함

또한 @Entity가 붙은 클래스는 옵션에 따라서 자동으로 테이블을 생성할 수 있고 이 경우엔 클래스의 멤버 변수에 따라서 자동으로 칼럼들도 생성됨

@Table

@Entity 어노테이션과 같이 사용할 수 있는 어노테이션

말 그대로 데이터베이스 상에서 엔티티 클래스를 어떠한 테이블로 생성할 것인지에 대한 정보를 담기 위한 어노테이션임

EX: @Table(name="tbl_board")의 경우
tbl_board라는테이블로 생성됨

또한 테이블 이름뿐 아니라 인덱스등을 생성하는 설정도 가능함

@Id와 @GeneratedValue

@Entity가 붙은 클래스는 Primary Key(PK)에 해당하는 특정 필드를 @Id로 지정해야함

이때 사용자가 입력하는 값을 사용하는 경우가 아니라면 자동으로 생성되는 번호를 사용하기 위해서
@GeneratedValue 어노테이션을 활용함

@GeneratedValue(strategy = GenerationType.IDENTITY)
부분은 PK를 자동으로 생성하고자 할 때 사용함
(키 생성 전략이라고 부름)

만일 연결되는 DB가 오라클이면 번호를 위한 별도의 테이블을 생성하고,
MariaDB면 auto incerment를 기본으로 사용해서 새로운 레코드가 기록될 때 마다 다른 번호를 가질수 있도록 처리됨

키 생성 전략은 다음과 같음

  • AUTO(default) - JPA 구현체(스프링 부트에선 Hibernate)가 생성 방식을 결정
  • IDENTITY - 사용하는 DB가 키 생성을 결정 MySQL이나 MariaDB의 경우 auto incerment 방식을 이용
  • SEQUENCE - DB의sequence를 이용해서 생성
    @SequenceGenerator와 같이 사용
  • TABLE - 키 생성 전용 테이블을 생성해서 키 생성. @TableGenerator와 함께사용

@column

만일 추가적인 필드(칼럼)가 필요한 경우에 사용
이때는 @column을 이용해서 다양한 속성을 지정가능함

주로 nullable, name, length 등을 이용해서 DB칼럼에 필요한 정보를 제공함

속성중 columnDefinition을 이용해서 기본값을 지정할 수도 있음(시간에 대한 기본값은 조금 다름)

위의 어노테이션을 활용하기 위해서 Memo 클래스를 수정함

수정된 Memo 클래스

package com.example.bootex2.entity;

import lombok.*;

import javax.persistence.*;

@Entity
@Table(name = "tbl_memo")
@ToString
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Memo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long mno;

    @Column(length = 200, nullable = false)
    private String memoText;
}

변경된 Memo 클래스는 Lombok의 @Getter를 이용해서 Getter 메서드를 생성하고, @Builder를 이용해서 객체를 생성할 수 있게 처리함

여기서 @Builder를 이용하기 위해선 @AllArgsConstructor, @NoArgsConstructor를 항상 같이 처리해야 컴파일 에러가 발생하지 않음

마지막으로 DB테이블에서 칼럼으로 생성되지 않는 필서의 경우엔 @Transient 어노테이션을 적용하고

위에서 잠깐 설명한 @Column으로 기본값을 지정할때는
@Column(columnDefinition = "varcher(255) default 'Yes'")
라는 코드를 사용하면됨

Spring Data JPA를 위한 스프링 부트 설정

현재 까지의 DB의 설정과 엔티티 클래스를 추가한 것 만으로 프로젝트의 실행엔 문제가 없음
하지만, 자동으로 필요한 테이블을 생성하거나 JPA를 이용할 때 발생하는 SQL을 확인하기 위해서는 약간의 추가 설정이 필요함

이 설정역시 다른 설정과 같이 application.properties에 추가해주겠음

JPA관련 설정이 추가된 application.properties 파일

spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://localhost:****/bootex
spring.datasource.username=*****
spring.datasource.password=*****

spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.show-sql=true

추가된 내용은 크게 3가지로 다음과 같음

  • spring.jpa.hibernate.ddl-auto: 프로젝트 실행시 자동으로 DDL(create, alter, drop 등)을 생성 할 것인지를 결정하는 설정임
  • spring.jpa.properties.hibernate.format_sql: 실제 JPA의 구현체인 Hibernate가 동작하면서 발생하는 SQL을 포맷팅해서 출력함
    (실행되는 SQL의 가독성을 높여줌)
  • spring.jpa.show-sql: JPA 처리 시에 발생하는 SQL을 보여줄 것인지를 결정함

위의 3가지 항목을 추가한 후 프로젝트를 실행하면 데이터베이스에 어떤 테이블이 생성되는지 로그를 통해서 확인할 수 있음

실행된 DDL을 보면 엔티티 클래스의 @Table의 name 속성값과 동일하게 테이블이름이 지정된것을 볼 수 있음

만약 @Table의 name속성값이 없는 경우에는 클래스의 이름과 동일한
이름으로 테이블을 생성함
(물론 이때도 DB의 예약어로 클래스 이름을 지정시 오류가 발생함)

JpaRepository 인터페이스

Spring Data JPA는 여러 종류의 인터페이스의 기능을 통해서 JPA관련 작업을 별도의 코드 없이 처리할 수 있게 지원함

또한 기능에 따라서 상속 구조로 추가적인 기능을 제공함

Repository
↑
CrudRepository
↑
PagingAndSortRepository
↑
JpaRepository

일반적이 기능만을 사용할 때는 CrudRepository를 사용하는 것이 좋고, 모든 JPA관련 기능을 사용하고 싶을 때는 JpaRepository를 이용하지만

특별한 경우가 아니라면 JpaRepository를 이용하는 것이 가장 무난한 선택임

JpaRepository사용하기

JpaRepository는 인터페이스 형이고, 이를 상속하는 인터페이스를 선언하는 것만으로도 모든 처리가 끝나는 일이 벌어짐

그 이유는 실제 동작 시에는 스프링이 내부적으로 해당 인터페이스에 맞는 코드를 생성하는 방식을 이용하기 때문임

memoRepository 인터페이스

package com.example.bootex2.repository;

import com.example.bootex2.entity.Memo;
import org.springframework.data.jpa.repository.JpaRepository;

public interface Memorepository extends JpaRepository<Memo, Long> {
}

작성된 코드는 repository 패키지를 생성하고 memoRepository를 인터페이스형으로 선언해줬음
JpaRepository를 사용할때는 엔티티 타입 정보(memo 클래스의 타입)와 @Id의 타입을 지정해주면됨
(이때 스프링이 내부적으로 인터페이스 타입에 맞는 객체를 생성해서 빈으로 등록함)
다음과 같이 인터페이스를 상속해주는 것만으로 모든 작업이 끝났음

테스트 코드를 통한 CRUD

작성한 MemoRepository를 이용해서 작성된 테이블에 SQL없이 CRUD 작업을 테스트 해 보려면 다음과 같은 메서드를 활용해야함

  • Insert: save(엔티티 객체)
  • select: findById(키 타입), getOne(키 타입)
  • update: save(엔티티 객체)
  • delete: deleteById(키 타입), delect(엔티티 객체)

특이한점은 insert와 update작업에 사용하는 메서드가 동일하게 save()를 이용하는데 이는 JPA 구현체가 메모리상에서 객체를 비교하고 없다면 insert, 존재한다면 update를 동작시키는 방식을 사용하기 때문임(Entity Manager라는 존재가 엔티티들을 관리하는 방식)

test폴더를 이용해서 repository패키지를 작성하고 MemoRepositoryTests클래스를 작성해서 진행

MemoRepositoryTests 클래스

package com.example.bootex2.repository;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class MemoRepositoryTests {

    @Autowired
    Memorepository memorepository;

    @Test
    public void testClass() {
        System.out.println("============================");
        System.out.println(memorepository.getClass().getName());

    }
}    

본격적인 테스트에 앞서 MemoRepository가 정상적으로 스프링에서 처리되고, 의존성 주입에 문제가 없는지를 먼저 확인하기 위한
testClass()를 작성함

testClass()는 MemoRepository 인터페이스 타입의 실체 객체가 어떤 것인지 확인하는 메서드임

Spring이 내부적으로 해당 클래스를 자동으로 생성하는데(AOP기능) 이때 클래스 이름은 jdk.proxy3.$ProxyXXX이며 동적 프록시 방식으로 만들어짐

등록 작업 테스트

MemoRepositoryTest 클래스의 일부

package com.example.bootex2.repository;

import com.example.bootex2.entity.Memo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;
import java.util.stream.IntStream;

@SpringBootTest
public class MemoRepositoryTests {

    @Autowired
    Memorepository memorepository;
    
        @Test
    public void testInsertDummies() {
        IntStream.rangeClosed(1, 100).forEach(i -> {
            Memo memo = Memo.builder().memoText("Sample..." + i).build();
            memorepository.save(memo);
        });

    }
}   

TestDummies()는 100개의 새로운 Memo 객체를 생성하고 MemoRepository를 이용해서 이를 insert함

이때 Memo의 memoText는 nullable = false속성을 지정해둬서 반드시 데이터를 넣어주고 테스트 해야함
테스트가 실행되는 과정에서 JPA 구성체인 Hibernate가 발생하는 insert 구문을 확인할 수 있고, 최종결과는 DB에 100개의 데이터를 insert해줌

조회작업 테스트

조회 작업 테스트는 findById()나 getOne()을 이용해서 엔티티 객체를 조회함
두개의 차이는 DB를 먼저 이용하는지 나중에 필요한 순간까지 미루는지에 있음

MemoRepositoryTests의 findById()

    @Test
    public void testSelect() {

        // DB에 존재하는 mno
        Long mno = 100L;

        Optional<Memo> result = memorepository.findById(mno);

        System.out.println("===================================");

        if (result.isPresent()) {
            Memo memo = result.get();

            System.out.println(memo);
        }
    }

findById의 경우 java.util 패키지의 Optional 타입으로 반환되기 때문에 한번 더 결과가 존재하는지를 체크하는 형태로 작성

실행 로그를 보면 findById()를 호출하는 순간에 이미 SQL은 처리가 되었고
====부분은 SQL처리 이후에 실행되는 것을 볼 수 있음

MemoRepositoryTests의 getOne()

    @Transactional
    @Test
    public void testSelect2() {

        Long mno = 99L;

        Memo memo = memorepository.getOne(mno);

        System.out.println("===================================");

        System.out.println(memo);
    }

getOne의 경우 추가로 @Transactional 어노테이션이 필요함
(@Transactional은 트랜잭션 처리를 위해 사용함)

실행 로그를 보면 getOne()을 호출한 후에 ====부분이 먼저 실행되고 sout()이 실행됨 한마디로 실제 객체가 필요한 순간까지 SQL을 실행시키지 않음

수정 작업 테스트

수정 작업은 등록 작업과 동일하게 save()를 이용해서 처리함
내부적으로 해당 엔티티의 @Id값이 일치하는지를 확인해서 insert or update를 처리함

MemoRepositoryTests의 testUpdate()


    @Test
    public void testUpdate() {

        Memo memo = Memo.builder().mno(99L).memoText("업데이트 테스트한 내용 100번은 삭제함").build();

        System.out.println(memorepository.save(memo));
    }

update()를 보면 99번 Memo 객체를 만들고, save()를 호출하고 있음
호출 결과 내부적으로 select 쿼리로 해당 번호의 Memo 객체를 확인하고, 이를 update하는 것을 볼 수 있음

삭제 작업 테스트

삭제 작업도 update와 동일한 개념이 적용됨
삭제하려는 번호의 엔티티 객체가 있는지 먼저 확인하고, 이를 삭제하려고함

MemoRepositoryTests의 testDelete()

    @Test
    public void testDelete() {
        Long mno = 100L;

        memorepository.deleteById(mno);
    }

deleteById()의 리턴 타입은 void이고 만일 해당 데이터가 존재하지 않으면
EmptyResultDataAccessException 예외를 발생시킴
실행결과는 select 이후에 delete 구문이 실행되는 방식으로 동작함

profile
어제보다 개발 더 잘하기 / 많이 듣고 핵심만 정리해서 말하기 / 도망가지 말기 / 깃허브 위키 내용 가져오기

0개의 댓글