01. Hello Spring Data JPA
- -
이번 포스트에서는 Entity 작성법과 간단한 C/R/U/D 동작을 테스트해보자.
객체와 테이블의 매핑
JPA는 OR Mapping 프레임워크이다. OR Mapping은 자바 객체인 Object(=Entity)와 DB의 테이블(=Relation)을 매핑시키는 작업으로 JPA의 출발점이라고 볼 수 있는 매우 중요한 작업이다.
Entity 클래스 작성
Entity 클래스를 작성할 때는 몇 가지 반드시 지켜야 하는 규칙들이 있다.
- 기본 생성자: Enttiy 클래스는 반드시 기본 생성자를 가져야 하며 생성자의 접근자는 public 또는 protected만 가능하다.
- @Entity: 클래스 선언부에 반드시 @Entity가 필요하다.
- 기본 키: 모든 Entity는 반드시 하나 이상의 필드를 기본키로 지정해야 하며 이를 위해 @Id를 사용한다.
이 외에 다음과 같은 권장 사항들이 있다.
- equals(), hashCode(), toString() 메서드를 재정의한다.
- encapsulation을 위해 field를 private으로 선언하고 필요 시 getter/setter를 추가한다.
- Entity를 불변 객체로 만드는 것이 권장되기도 하는데 이때는 수정 시 기존 Entity를 참조해서 새로운 Entity를 만들어야 하는 번거로움이 있을 수 있다.
일반적으로 Entity를 생성할 때 lombok을 이용한다면 annotation 기반으로 손쉽게 만들어 볼 수 있다. lombok 애너테이션 중 @Data은 다른 Entity와 관계 생성 시 toString, equahs, hashCode 등 메서드에서 순환 참조에 의한 오류를 처리하기 힘들기 때문에 사용을 지양하는 것이 좋다.
ORMapping을 위한 annotation과 적용 예
OR Mapping에 사용되는 주요 에너테이션을 살펴보자.
annotation | 설명 |
@Entity | 현재 클래스가 테이블과 매핑될 대상임을 알려주며 필수 항목이다. |
@Table | Entity 클래스가 매핑될 테이블 이름을 name 속성으로 지정한다. 생략 시는 Entity 이름이 테이블 이름 |
@Id | 테이블의 기본 키에 매핑할 컬럼에 지정하며 필수 항목이다. |
@Column | 테이블의 컬럼에 매핑할 field에 설정하며 name 속성에 컬럼의 이름을 지정할 수 있다. 생략 시 field 명이 컬럼 명과 연결된다. |
앞서 작성해봤던 Member Entity에는 @Table과 @Column이 생략된 형태이다. 추가로 기존에는 String id를 @Id로 사용했었는데 키의 관리를 용이하기 위해서 Long mno를 추가해서 @Id로 처리하고 id는 일반 @Column으로 수정해주자.
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name="member")
public class Member {
@Id // 기본 키와 연결되는 컬럼이다.
private Long mno;
@Column("id")
private String id;
private String name;
private String pass;
}
CRUD 실행하기
Repository 생성
이제 C/R/U/D 처리를 위해 Spring Data JPA의 핵심인 JpaRepository를 사용해보자. 사용자 정의 Repository를 만들기 위해서는 JpaRepository를 상속받는 interface를 정의하면 되는데 다음 코드처럼 사용법이 매우 어려우므로 긴장하며 바라보자.
import org.springframework.data.jpa.repository.JpaRepository;
import com.doding.boardapp.member.entity.Member;
public interface MemberRepo extends JpaRepository<Member, Long> {}
엥? 끝이다. 단지 JpaRepository를 상속받고 Entity 타입과 P.K로 사용될 필드의 타입을 타입 파라미터로 넘겨주면 CRUD는 끝이다. 정말일까? 어떻게 이런 일이 가능할까?
JpaRepository
JpaRepository는 Spring Data JPA에서 JPA를 간소화해서 사용할 수 있게 도와주는 핵심 interface로 다음과 같은 계층 구조를 갖는다.
- CrudRepository: Spring Data에 소속된 interface로 이름에서 부터 어떤 목적인지를 잘 나타나 있다. save, find, delete 등 기본 CRUD 기능이 선언되어있다.
- PagingAndSortingRepository: 역시 Spring Data에 소속되어있으며 Sort와 Pageable 을 이용해서 각각 정렬과 페이징을 처리하는 기능을 선언하고 있다. 이제 번거롭게 페이징 쿼리를 어떻게 작성해야하는지 고민할 필요가 없어질것 같다.
- JpaRepository: Spring Data JPA에 소속된 녀석으로 실제 사용할 interface이다. 비지니스 로직을 구성할 때에는 이 interface를 상속해서 사용자 정의 interface를 구성하면 된다. 이 인터페이스에 대한 구현체는 Proxy에 의해 생성되므로 신경 쓸 필요가 없는데 만약 JpaRepository의 동작을 알고 싶다면 SimpleJpaRepository라는 클래스를 살펴보는 것도 공부 차원에서 괜찮다.
따라서 고작 이 한줄의 코드지만 JpaRepository로 부터 물려받은 기능은 실로 막강하다. 벌써 기본적인 C/R/U/D, 페이징은 끝이다.
public interface MemberRepo extends JpaRepository<Member, Long> {}
종류 | 메서드 | 설명 |
C | T save(T) | 새로운 Entity 저장 |
U | T save(T) | 기존 Entity 수정 |
D | void deleteById(id) | 기존 Entity 삭제 |
R | Optional<T> findById(id) | id에 해당하는 Entity 조회 |
T getReferenceById(id) | id에 해당하는 Entity의 프록시 반환 | |
Iterable<T> findAll() | Entity의 목록 반환, 파라미터로 정렬, 페이징 조건 전달 가능 |
테스팅
@DataJpaTest
간단한 Repository 만으로 CRUD가 정말 가능한지 살펴보기 위해 테스트 클래스를 작성해보자.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.ActiveProfiles;
import com.doding.boardapp.member.repo.MemberRepo;
@DataJpaTest
@ActiveProfiles("dev")
public class MemberRepoTest {
@Autowired
MemberRepo mrepo;
...
}
위 테스트에서 가장 눈에 띄는 부분은 @DataJpaTest이다. 이전까지는 SpringBoot에서 테스트를 위해 @SpringBootTest를 썼는데 이 애너테이션이 사용되면 스프링 애플리케이션이 완전히 동작해야한다. 즉 통합 테스트가 진행된다. 우리는 단지 Repository만 동작하는지 살펴보면 되는데 불필요한 리소스가 너무 많이 로딩 된다. 이런 문제를 해결하기 위해서 Spring에서는 다양한 Slice 테스트 기법을 제공한다. Slice 테스트는 테스트에 필요한 리소스만 로딩해서 비용을 절감하는 효과가 있다.
Slice 테스트를 위한 대표적인 애너테이션에는 @DataJpaTest 외에도 @WebMvcTest, @RestClientTest등이 있다.
어노테이션 | 설명 |
@WebMvcTest | Spring MVC 컨트롤러를 테스트하기 위해 사용. 컨트롤러 계층과 관련된 빈들만 로드 |
@RestClientTest | REST 클라이언트를 테스트하기 위해 사용. REST 클라이언트와 관련된 구성 요소를 로드하며, 외부 서비스를 가상으로 만들어서 통신 가능 |
애너테이션 분석
@DataJpaTest에 대해서 좀 더 살펴보자. @DataJpaTest는 DB 및 JPA와 관련된 다양한 자동 설정을 로딩한다.
@Target(ElementType.TYPE)
@BootstrapWith(DataJpaTestContextBootstrapper.class)
@ExtendWith(SpringExtension.class)
@OverrideAutoConfiguration(enabled = false)
@TypeExcludeFilters(DataJpaTypeExcludeFilter.class)
@Transactional
@AutoConfigureCache
@AutoConfigureDataJpa
@AutoConfigureTestDatabase
@AutoConfigureTestEntityManager
@ImportAutoConfiguration
public @interface DataJpaTest {
@PropertyMapping("spring.jpa.show-sql")
boolean showSql() default true;
}
@DataJpaTest는 아래와 같은 특징을 갖는다.
- @TypeExcludeFilters(DataJpaTypeExcludeFilter.class): JPA 테스트를 위한 빈을 Entity, JpaRepository 구현체, EntityManager 구현체등으로 한정한다.
- @Transactional: 테스트가 끝나면 자동으로 rollback 된다.
- @AutoConfigurerCache: 기본 CacheType으로 CacheType.NONE을 설정해서 데이터를 캐싱하지 않는다.
- @AutoConfigurerDataJpa: JpaRepository를 사용할 수 있는 환경을 로드한다.
- @AutoConfigurerTestDatabase: 프로젝트에 설정된 DB 대신 메모리 기반의 내장 DB를 사용해서 테스트 한다. 따라서 H2등 메모리 기반 DB가 의존성에 추가되어야 한다. 만약 원래의 DB를 사용하려면 @AutoConfigureTestDatabase 의 replace 속성을 Replace.NONE으로 재정의 해야 한다.
- @AutoConfigurerTestEntityManager: TestEntityManager를 사용할 수 있게 한다.
- spring.jpa.show-sql 속성: 자동으로 true로 설정된다. 만약 변경하고 싶다면 showSql 속성을 false로 설정하면 된다.
테스트 진행
다음은 실제 테스트를 작성하고 실행해보자. 테스트 하려는 내용은 주석을 참조한다.
@Test
void insertTest() {
// Given
log.debug("repo type: {}", mrepo.getClass()); // repo type: class jdk.proxy2.$Proxy132
Assertions.assertTrue(Proxy.isProxyClass(mrepo.getClass())); // repo는 proxy 클래스
long preCnt = mrepo.count(); // 기존 데이터 개수 확인
// When
Member m = Member.builder()
.mno(100L)
.id("new")
.name("name")
.pass("1234")
.build();
mrepo.saveAndFlush(m); // 원래는 save(), 실행 쿼리 확인을 위해 saveAndFlush 사용
// Then
Assertions.assertEquals(preCnt + 1, mrepo.count()); // 기존보다 1 증가 확인
}
테스트는 아마 성공할 텐데 로그를 살펴보면 놀라운 일이 발생한다. 우리는 한 줄의 SQL을 작성한 적이 없는데...
# 초기 데이터 개수 구하기
Hibernate:
select count(*) from member m1_0
# P.K 인 newmember로 저장된 자료가 있는지 확인해보기
Hibernate:
select m1_0.id,m1_0.created, ~~~
from member m1_0
where m1_0.id=?
binding parameter (1:VARCHAR) <- [newmember]
# 값이 없었으므로 저장하기
Hibernate:
insert into member
(created, email_domain, email_id, gender, name, pass, id)
values
(?, ?, ?, ?, ?, ?, ?)
# 저장 후 데이터 개수 구하기
Hibernate:
select count(*) from member m1_0
실제로는 위와 같은 코드가 Database와 소통하는 것을 확인할 수 있다. 이것이 Spring Data JPA의 편의성이다. 다만 주의할 점은 자동으로 생성되는 코드가 언제나 합리적일 것이라는 생각을 하면 안된다. 자동으로 생성되는 코드를 사용할 때는 생성되는 SQL을 확인하고 적합한지 확인하는 습관이 중요하다.
AbstractTestClass 생성
테스트를 진행하다보면 매번 테스트 클래스에 @DataJpaTest나 @ActiveProfiles를 선언해야 하는데 다행히 이런 애너테이션들은 모두 @Inherited 속성을 가진다. 따라서 상속받는 클래스에서 그대로 재활용할 수 있다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited // ActiveProfiles가 적용된 클래스를 상속받으면 여전히 @Inherited가 적용된다.
public @interface ActiveProfiles {}
다음과 같이 AbstractBoardTest를 만들고 테스트 클래스에서 상속받아서 사용하도록 하자.
@DataJpaTest
@ActiveProfiles("dev")
public class AbstractBoardTest {}
참고로 @Slf4j는 @Inherited를 포함하고 있지 않으므로 필요 할 때마다 작성해 줘야 한다.
'Spring Model > 02. 객체 매핑과 P.C' 카테고리의 다른 글
06. 엔티티의 상태 관리 (0) | 2022.04.12 |
---|---|
05. EntityManager와 Persistence Context (0) | 2022.03.19 |
04.자동 키 매핑 전략 (0) | 2020.06.07 |
03. Value Type (0) | 2020.06.07 |
02. OR-Mapping과 상속 (0) | 2020.06.02 |
소중한 공감 감사합니다