[그린컴퓨터] Server/Spring

앤티티 클레스와 repository 활용 테스트 { CRUD }

Ben의 프로그램 2023. 7. 19. 09:13
728x90
테스트 코드를 통한 CRUD 연습

작성한 MemoRepository 를 이용해서 작성된 테이블에 SQL 없이 CRUD 작업을 테스트해 봅니다. JpaRepository 를 상속받으면 다음 메서드를 활용할 수 있습니다. 

특이하게도 insert와 update 작업에 사용하는 메서드가 동일하게 save( )를 이용하는데 이는 JPA의 구현체가 메모리상(Entity Manager 라는 존재가 엔티티들을 관리하는 방식)에서 객체를 비교(데이터베이스와 entity 를 비교한다)하여 데이터베이스에 존재하지 않는다면 insert, 존재한다면 update 를 동작시키는 방식으로 동작하기 때문입니다. 테스트 코드는 프로젝트 생성 시에 만들어진 test 폴더를 이용해서 'repository' 패키지를 작성하고 MemoRepositoryTests 클래스를 작성해서 진행합니다.  

 

테스트 코드 작성 및 의존성 주입 테스트

test 폴더에 MemoRepositoryTest 라는 클래스 파일을 만들었습니다. 우선 본격적인  테스트에 앞서서 의존성 주입에 문제는 없는지 테스트해보겠습니다. testClass( ) 메서드는 MemoRepository 인터페이스 타입의 실제 객체가 어떤 것인지 확인합니다. 스프링이 내부적으로 해당 클래스를 자동으로 생성하는데(AOP기능) 이때 클래스 이름을 확인해 보고자 합니다. 단위 테스트를 진행해보겠습니다.

출력된 결과를 보니까 작성한 적이 없는 클래스의 이름이 출력되는 것을 확인할 수 있습니다. (동적 프록시라는 방식으로 만들어집니다.)

 

등록 작업 테스트 

등록 작업의 테스트는 한 번에 여러 개의 엔티티 객체를 저장하도록 작성하겠습니다. (데이터베이스에 여러 데이터를 한 번에 밀어넣는 구조로 코드를 작성했다는 의미입니다.) Memo, MemoRepository, MemoRepositoryTest 세 파일을 모두 같이 보면서 코드를 이해해 보겠습니다.

package com.example.demo.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.ToString;

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

	@Column(length = 200, nullable = false)
	private String memoText;
}
package com.example.demo.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.example.demo.entity.Memo;

// repository 는 인터페이스인데, 같은 인터페이스인 JpaRepository 를 상속받는 
// 것만 해도 Spring 이 알아서 필요한 코드를 생성해주는 놀라운 일이 벌어진다. 
// 또한 JpaRepository 를 상속을 하면서 자동으로 MemoRepository 가 컨테이너에 저장된다. 
public interface MemoRepository extends JpaRepository<Memo, Long> {

}
import java.util.stream.IntStream;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.example.demo.entity.Memo;

@SpringBootTest
public class MemoRepositoryTest {
	@Autowired
	MemoRepository memoRepository;
	@Test
	public void testClass() {
		System.out.println(memoRepository.getClass().getName());
		IntStream.range(0, 10).forEach(i -> System.out.println(i));
	}
	@Test
	public void testInsertDummies() {
		IntStream.rangeClosed(1, 100).forEach(i -> {
			Memo memo = Memo.builder()
					.memoText("Sample..." + i)
					.build();
			memoRepository.save(memo);
		});
	}
}

testInsertDummies( ) 의 내용은 100 개의 새로운 Memo 객체를 생성하고 MemoRepository 를 이용해서 이를 insert 하는 것입니다. Memo의 memoText 는 Not null 조건이므로 반드시 데이터를 넣어주고 테스트를 진행합니다(mno 멤버변수는 GeneratedValue 어노테이션으로 자동 값 할당이 진행되어서 신경쓰지 않아도 된다). 테스트가 실행되는 과정에는 JPA 의 구현체인 Hibernate 가 만드는 insert 구문을 확인할 수 있습니다. 테스트의 최종 결과는 데이터베이스를 통해서 확인할 수 있습니다. 

Hibernate 가 insert 구문을 실행하고 있는 모습
실행된 데이터베이스의 결과

 

조회 작업 테스트

조회 작업 테스트는 findById( ) 를 이용해서 처리합니다. 스프링 2.5.0 이상부터는 getOne( ) 메서드는 지원되지 않으므로 findById( ) 를 사용합니다.

@SpringBootTest
public class MemoRepositoryTest {
	// MemoRepository 가 jpaRepository 를 상속받으면서 자동으로 컨테이너에 등록되었고
	// 그것을 Test 파일에서 Autowired 해서 사용하고 있는 것이다.
	@Autowired
	MemoRepository memoRepository;
	
//	 데이터베이스 멤버 변수인 Long mno 는 Primary Key 로 설정되어있다.
//	 PK를 기준으로 값을 찾는 findById 를 사용하기 위해 Long mno 변수를 초기화했다.
//	 여기서는 100 이라는 id 가 존재하는지 여부를 먼저 파악한 후 값이 데이터베이스에 있으면 
//	 값을 콘솔창에 출력하고 있다. 
	@Test
	public void testSelect() {
		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 처리 이후에 실행된 것을 볼 수 있습니다. 이렇게 해서 조회를 진행해보았습니다. 

 

수정 작업 테스트

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

@Test
public void testUpdate() {
	Memo memo = Memo.builder()
				.mno(100L)
				.memoText("Update Text")
				.build();
	memoRepository.save(memo);
	Long mno = 100L;
	Optional<Memo> search = memoRepository.findById(mno);
	System.out.println("============");
	if (search.isPresent()) {
		System.out.println(search.get());
	}
}

@Test
public void testUpdate2() {
	Memo memo = Memo.builder()
				.mno(100L)
				.memoText("Update Text")
				.build();
	System.out.println(memoRepository.save(memo));
}

같은 기능을 수행하는 testUpdate 와 testUpdate2 메서드를 작성하였습니다. 두 메서드 모두 @Id 100 인 튜플을 수정한 다음 수정한 결과를 출력하는 함수입니다. 두 코드의 길이 차이가 발생한 이유는 save( ) 메서드는 반환 값으로 entity 를 반환하는 점을 활용한 차이입니다. save( ) 함수는 수행한 결과 값인 entity 를 반환합니다. 따라서 바로 Sysout 을 실행 시켜도 우리가 원하는 대로 결과 값을 볼 수 있습니다. 

이번에는 출력된 결과 값을 살펴보겠습니다. JPA는 엔티티 객체들을 메모리상에 보관하려고 하기 때문에 특정한 엔티티 객체가 존재하는지 확인하는 select 가 먼저 실행되고 해당 @Id 를 가진 엔티티 객체가 있다면 update, 그렇지 않다면 insert 를 실행하게 됩니다. 

 

삭제 작업 테스트

삭제 작업도 위와 동일한 개념이 적용됩니다. 삭제하려는 번호(mno) 의 엔티티 객체가 있는지 먼저 확인하고, 이를 삭제하려고 합니다.

@Test
public void testDelete() {
    Long mno = 100L;
    memoRepository.deleteById(mno);
}

deleteById( )의 리턴 타입은 void 이고 만일 해당 데이터가 존재하지 않으면 org.spring.framework.dao.EmptyResultDataAccessException 예외를 발생합니다.

테스트 코드의 실행 결과는 select 이후에 delete 구문이 실행되는 방식으로 동작합니다. 

코드로 배우는 스프링 부트 웹 프로젝트를 읽고 공부하는 내용임을 밝힙니다.