Spring Boot - H2 데이터베이스 - CRUD 코드 작성, 테스트 (3/3)

반응형

 

  • 이전 포스팅 확인하기

https://luvris2.tistory.com/366

 

Spring Boot - H2 데이터베이스 - JPA 엔티티 설정, 리파지토리 설정 (2/3)

JPA 엔티티 설정 (Entity) Entity란? 사전적 의미로는 개체, 실체의 의미 데이터베이스 논리적 구성요소 유형, 무형의 개체로 현실세계에서 사람이 생각하는 개념이나 정보의 단위 JPA의 엔티티는 쉽

luvris2.tistory.com

 

해당 포스팅은 'JUnit Test'를 이용하여 DB CRUD 테스트를 진행합니다.


테스트 설정

테스트 파일 생성

  • 테스트를 위해서는 테스트 파일이 필요
  • 테스트 파일은 'src/test/java' 경로에서 생성
  • 'src/main/java'의 패키지와 동일한 이름으로 생성
    • 예시) 테스트를 위해 패키지명 설정
      • src/main/java : com.example.h2dbtest
      • src/test/java : com.example.h2dbtest

 

  • 원하는 임의의 이름으로 생성
    • 포스팅에서는 프로젝트의 일관성을 위해 메인 자바 파일 뒤에 Test를 붙임
    • 물론 파일 이름은 중요하지 않으므로 자유롭게 작성


테스트 파일 설정

테스트 파일 선언

  • 생선한 파일을 열고 클래스 이름 위에 @SpringBootTest 입력
  • @SpringBootTest : 테스트를 하기 위한 클래스 선언
@SpringBootTest

테스트용 생성자 설정 (Setter)

@Autowired

  • 객체를 주입하기 위해 사용하는 스프링의 애너테이션
    • 리파지토리 클래스의 객체를 스프링이 자동으로 생성
  • 객체 주입 방식의 종류 : Setter / 생성자 / @Autowired
  • 순환참조 문제와 같은 이유로 생성자를 통한 객체 주입 방식 권장
    • 테스트 코드에서는 생성자를 통한 객체 주입이 불가능하므로 @Autowired 사용
    • 실제 코드에서는 생성자 사용
// H2DbTestApplication.java
// 해당 리파지토리 클래스의 객체 자동 생성
@Autowired
// private Repository repository;
// 공지사항 리파지토리
private H2dbNoticeRepository h2dbNoticeRepository;
// 댓글 리파지토리
private H2dbReplyRepository h2dbReplyRepository;

전체 코드

// H2DbTestApplication.java
@SpringBootTest
class H2DbTestApplicationTest {

	@Autowired
	private H2dbNoticeRepository h2dbNoticeRepository;
	private H2dbReplyRepository h2dbReplyRepository;
	
}

데이터베이스 제어하기 (CRUD)

  • 해당 코드는 테스트 클래스에서 진행합니다.
  • 포스팅에서는 H2DbTestApplicationTest.java 파일로 진행하였습니다.


Create

  • 엔티티 객체화
    • H2dbNotice h= new H2dbNotice();
  • 객체화된 엔티티의 값 입력 (@Autowired, =Setter)
    • h.setColumn("Value");
  • 값이 입력된 객체 리파지토리에 저장 (DB에 데이터 생성)
    • this.repo.save(h);
// H2DbTestApplication.java
// 데이터 생성하기
void testJpa_create() {
    // 공지사항 엔티티 객체 'noticeCreate1' 생성
    H2dbNotice noticeCreate1 = new H2dbNotice();
    // 공지사항 테이블의 컬럼 값 입력
    // 해당 코드는 SQL문의 INSERT와 같은 의미를 지님
    	// insert into H2db_notice (subject, content, createdate)
        //     values ("첫번째 공지사항입니다.", "첫번째 공지사항이 작성되었습니다.", now());
    noticeCreate1.setSubject("첫번째 공지사항입니다.");
    noticeCreate1.setContent("첫번째 공지사항이 작성되었습니다.");
    noticeCreate1.setCreateDate(LocalDateTime.now());
    // 엔티티 객체에서 설정한 데이터 리파지토리에 저장 (= DB에 Insert문 실행)
    this.h2dbNoticeRepository.save(noticeCreate1);
    // 첫번째 공지사항 저장

    // 공지사항 엔티티 객체 'noticeCreate2' 생성
    // 위의 주석과 같음
    H2dbNotice noticeCreate2 = new H2dbNotice();
    noticeCreate2.setSubject("두번째 게시판 공지");
    noticeCreate2.setContent("두번째 글 입니다.");
    noticeCreate2.setCreateDate(LocalDateTime.now());
    this.h2dbNoticeRepository.save(noticeCreate2);
    // 두번째 공지사항 저장
}

Read

  • 데이터 확인을 위해 JUnit의 assertEquals 사용
    • assertEquals(기댓값, 실제값)
    • 기댓값과 실제값이 같으면 테스트 성공, 다르면 실패
    • 해당 메소드는 테스트 전용으로 사용
  • 리파지토리에 n번 ID의 데이터 옵셔널 타입으로 반환
    • Optional<H2dbNotice> op = this.repo.findById(n);
  • 데이터 null 체크
    • op.isPresent() : 데이터 존재 1 (if문 실행), null 0
  • 옵셔널 타입으로 반환된 데이터 엔티티 객체에 저장
    • H2dbNotice h = op.get(); 
  • 데이터 확인
    • assertEquals("기댓값", "실제값");
      • 맞으면 성공(테스트시 녹색 결과), 틀리면 실패(테스트시 빨간색 결과)

findAll (모든 데이터 조회)

// H2DbTestApplication.java
// !!! 데이터 확인을 위해 JUnit의 assertEquals 사용 !!!
// !!! assertEquals(기댓값, 실제값) !!!

// 데이터 조회하기 findById
void testJpa_read_findById() {
    // 조회 findById
    // findById는 Optional 타입으로 값 반환
    // isPresent : 데이터 객체의 Null 확인
    Optional<H2dbNotice> noticeData_findById = this.h2dbNoticeRepository.findById(1);
    if(noticeData_findById.isPresent()) {
        H2dbNotice noticeFindById = noticeData_findById.get();
        assertEquals("두번째 게시판 공지", noticeFindById.getSubject());
    }
}

findById (특정 ID 데이터 조회)

// H2DbTestApplication.java
// !!! 데이터 확인을 위해 JUnit의 assertEquals 사용 !!!
// !!! assertEquals(기댓값, 실제값) !!!

// 데이터 조회하기 findById
void testJpa_read_findById() {
    // 조회 findById
    // findById는 Optional 타입으로 값 반환
    // isPresent : 데이터 객체의 Null 확인
    Optional<H2dbNotice> noticeData_findById = this.h2dbNoticeRepository.findById(2);
    if(noticeData_findById.isPresent()) {
        H2dbNotice noticeFindById = noticeData_findById.get();
        assertEquals("두번째 게시판 공지", noticeFindById.getSubject());
    }
}

findBy + Column (특정 컬럼 데이터 조회)

// H2DbTestApplication.java
// !!! 데이터 확인을 위해 JUnit의 assertEquals 사용 !!!
// !!! assertEquals(기댓값, 실제값) !!!

// 데이터 조회하기 findBySubject
void testJpa_read_findBySubject() {
  // 조회 findBy + Column
  // findBy + 엔티티 속성명(컬럼) 메소드 : 해당 속성의 값으로 데이터 조회
  // 리파지토리에 해당 메소드를 정의해야 사용 가능
  H2dbNotice noticeData_findBySubject = this.h2dbNoticeRepository.findBySubject("첫번째 공지사항입니다.");
  assertEquals(1, noticeData_findBySubject.getId());

findBy + Column + Like (특정 컬럼의 문자열이 포함되는 데이터 검색)

// H2DbTestApplication.java
// !!! 데이터 확인을 위해 JUnit의 assertEquals 사용 !!!
// !!! assertEquals(기댓값, 실제값) !!!

// 데이터 조회하기 findByLike
void testJpa_read_findByLike() {
  // 특정 문자열 조회 findBy + Column + Like
  // findBy + 엔티티 속성명(컬럼) Like : 특정 문자열로 해당 속성의 데이터 조회
  // 리파지토리에 해당 메소드를 정의해야 사용 가능
  // %검색어 : 검색어로 끝나는 문자열
  // 검색어% : 검색어로 시작하는 문자열
  // %검색어% : 검색어가 포함되는 문자열
  List<H2dbNotice> noticeData_findLike = h2dbNoticeRepository.findBySubjectLike("%공지%");
  H2dbNotice noticeFindLike = noticeData_findLike.get(0);
  assertEquals("첫번째 공지사항입니다.", noticeFindLike.getSubject());
}

Update

  • 리파지토리에 n번 ID의 데이터 옵셔널 타입으로 반환
    • Optional<H2dbNotice> op = this.repo.findById(1);
  • 데이터 null 체크
    • assertTrue(op.isPresent());
      • isPresent() : 데이터가 존재하면 1 반환
      • assertTrue(값) : 값이 참(true/1)이면 테스트 성공
  • 옵셔널 타입으로 반환된 데이터 엔티티 객체에 저장
    • H2dbNotice h = op.get();
  • 객체화된 엔티티의 값이 있는 ID에 수정 할 값 입력
    • h.setColumn("update Value");
  • 값이 입력된 객체 리파지토리에 저장 (DB에 데이터 생성)
    • this.repo.save(h);
// H2DbTestApplication.java
// 데이터 수정하기
void testJpa_update() {
    // 조회와 비슷하지만 save를 이용하여 해당 속성을 저장
    // 데이터베이스에서는 update문을 실행
    Optional<H2dbNotice> noticeData_update = this.h2dbNoticeRepository.findById(1);
    assertTrue(noticeData_update.isPresent());
    H2dbNotice noticeUpdate = noticeData_update.get();
    noticeUpdate.setSubject("수정된 제목");
    this.h2dbNoticeRepository.save(noticeUpdate);

Delete

  • 데이터 확인
    • assertEquals("기댓값", "실제값");
  • 리파지토리에 n번 ID의 데이터 옵셔널 타입으로 반환
    • Optional<H2dbNotice> op = this.repo.findById(1);
  • 데이터 null 체크
    • assertTrue(op.isPresent());
      • isPresent() : 데이터가 존재하면 1 반환
      • assertTrue(값) : 값이 참(true/1)이면 테스트 성공
  • 옵셔널 타입으로 반환된 데이터 엔티티 객체에 저장
    • H2dbNotice h = op.get();
  • 값이 입력된 객체 리파지토리에서 삭제 (DB에 데이터 삭제)
    • this.repo.delete(h);
  • 삭제되었는지 데이터 확인
    • assertEquals("기댓값", "실제값");
// H2DbTestApplication.java
// 데이터 삭제하기
void testJpa_delete() {
    // 리파지토리.delete(데이터) : 데이터베이스의 해당 데이터 삭제
    assertEquals(2, this.h2dbNoticeRepository.count());
    Optional<H2dbNotice> noticeData_delete = this.h2dbNoticeRepository.findById(1);
    assertTrue(noticeData_delete.isPresent());
    H2dbNotice noticeDelete = noticeData_delete.get();
    this.h2dbNoticeRepository.delete(noticeDelete);
    assertEquals(1, this.h2dbNoticeRepository.count());
}

자식테이블(Reply) Create

  • 부모테이블(Notice)의 외래 키(ID)를 참조하여 데이터 호출
    • Optional<h2dbNotice> ref_h = this.notice_repo.findById(n)
    • assertTrue(ref_h.isPresent());
    • H2dbNotice data = ref_h.get()
  • 엔티티 객체를 생성하여 외래 키와 연결하여 리파지토리에 데이터 입력 (DB에 저장)
    • H2dbReply rep = new H2dbReply();
    • rep.setColumn("Message");
    • rep.setH2dbNotice(data);
      • 부모 테이블(Notice)의 키와 연결
    • this.repo_reply.save(rep);
// H2DbTestApplication.java
// 공지사항에 댓글 작성하기
void testJpa_create_replyFromNotice() {
    // 참조 할 데이터 호출
    Optional<H2dbNotice> refData_notice = this.h2dbNoticeRepository.findById(1);
    assertTrue(refData_notice.isPresent());
    H2dbNotice refData = refData_notice.get();

    // 데이터 생성
    // 참조한 엔티티와 참조한 엔티티의 데이터를 세터로 연결
    // 실제 데이터베이스에서는 외래 키가 되어있으므로 참조되는 테이블의 세터 설정이 가능
    // 외래 키 설정은 엔티티에서 ManyToOne을 설정하였기 때문
    H2dbReply replyCreate1 = new H2dbReply();
    replyCreate1.setContent("첫번째 공지사항이네요!");
    replyCreate1.setH2dbNotice(refData);
    replyCreate1.setCreateDate(LocalDateTime.now());    	
    this.h2dbReplyRepository.save(replyCreate1);
}

자식 테이블(Reply) Read

  • 위에서 언급한 Read와 동일하므로 설명은 생략합니다.
  • 자식 테이블 객체는 .get 메소드에 부모 테이블의 참조된 값을 불러 올 수 있음
    • 예시) replyData.getH2dbNotice().getId()
// 공지사항의 댓글 조회하기
void testJpa_read_replyFromNotice() {
    Optional<H2dbReply> replyData_read = this.h2dbReplyRepository.findById(1);
    assertTrue(replyData_read.isPresent());
    H2dbReply replyData = replyData_read.get();
    assertEquals(1, replyData.getH2dbNotice().getId());

DB 테스트

  • 테스트를 위해서는 테스트 할 메소드 위에 @Test 입력
    • 해당 포스팅에서는 테스트 파일에 CRUD를 모두 작성하였습니다.
    • 때문에 모든 메소드에 테스트 애너테이션을 사용하면 에러가 날 확률이 높습니다.
    • 그래서 메소드를 하나씩 나누어서 실행하도록 합니다.
      • 전체 소스 코드를 복사하여 하나의 테스트 애너테이션만 주석을 풀고 테스트
      • 다른 테스트는 주석 처리
@Test

테스트 파일 전체 소스 코드

// H2DbTestApplication.java
package com.example.h2dbtest;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

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

import com.example.h2dbtest.notice.H2dbNotice;
import com.example.h2dbtest.notice.H2dbNoticeRepository;
import com.example.h2dbtest.reply.H2dbReplyRepository;

@SpringBootTest
class H2DbTestApplicationTest {

    // 리포지터리 객체 주입
    @Autowired
    private H2dbNoticeRepository h2dbNoticeRepository;
    
    @Autowired
    private H2dbReplyRepository h2dbReplyRepository;
	
    @Test
    // 데이터 생성하기
    void testJpa_create() {
        H2dbNotice noticeCreate1 = new H2dbNotice();
        noticeCreate1.setSubject("첫번째 공지사항입니다.");
        noticeCreate1.setContent("첫번째 공지사항이 작성되었습니다.");
        noticeCreate1.setCreateDate(LocalDateTime.now());
        this.h2dbNoticeRepository.save(noticeCreate1);
        // 첫번째 공지사항 저장

        H2dbNotice noticeCreate2 = new H2dbNotice();
        noticeCreate2.setSubject("두번째 게시판 공지");
        noticeCreate2.setContent("두번째 글 입니다.");
        noticeCreate2.setCreateDate(LocalDateTime.now());
        this.h2dbNoticeRepository.save(noticeCreate2);
        // 두번째 공지사항 저장
	}

    // !!! 데이터 확인을 위해 JUnit의 assertEquals 사용 !!!
    // !!! assertEquals(기댓값, 실제값) !!!
	
    //@Test
    // 데이터 조회하기 findAll
    void testJpa_read_findAll() {
        // 모든 데이터 조회 findAll
        // 값이 동일한테 확인, 동일하지 않으면 테스트는 실패 처리
        List<H2dbNotice> noticeData_findAll = this.h2dbNoticeRepository.findAll();
        assertEquals(2, noticeData_findAll.size());
        H2dbNotice noticeFindAll = noticeData_findAll.get(0);
        assertEquals("첫번째 공지사항입니다.", noticeFindAll.getSubject());
    }
    
    //@Test
    // 데이터 조회하기 findById
    void testJpa_read_findById() {
		// 조회 findById
		// findById는 Optional 타입으로 값 반환
		// isPresent : 데이터 객체의 Null 확인
		Optional<H2dbNotice> noticeData_findById = this.h2dbNoticeRepository.findById(2);
		if(noticeData_findById.isPresent()) {
			H2dbNotice noticeFindById = noticeData_findById.get();
			assertEquals("두번째 게시판 공지", noticeFindById.getSubject());
		}
    }
    
	//@Test
    // 데이터 조회하기 findBySubject
    void testJpa_read_findBySubject() {
      // 조회 findBy + Column
      // findBy + 엔티티 속성명(컬럼) 메소드 : 해당 속성의 값으로 데이터 조회
      // 리파지토리에 해당 메소드를 정의해야 사용 가능
      H2dbNotice noticeData_findBySubject = this.h2dbNoticeRepository.findBySubject("첫번째 공지사항입니다.");
      assertEquals(1, noticeData_findBySubject.getId());
	}
	
    //@Test
    // 데이터 조회하기 findByLike
    void testJpa_read_findByLike() {
      // 특정 문자열 조회 findBy + Column + Like
      // findBy + 엔티티 속성명(컬럼) Like : 특정 문자열로 해당 속성의 데이터 조회
      // 리파지토리에 해당 메소드를 정의해야 사용 가능
      // %검색어 : 검색어로 끝나는 문자열
      // 검색어% : 검색어로 시작하는 문자열
      // %검색어% : 검색어가 포함되는 문자열
      List<H2dbNotice> noticeData_findLike = h2dbNoticeRepository.findBySubjectLike("%공지%");
      H2dbNotice noticeFindLike = noticeData_findLike.get(0);
      assertEquals("첫번째 공지사항입니다.", noticeFindLike.getSubject());
	}
    
    //@Test
    // 데이터 수정하기
    void testJpa_update() {
    	// 조회와 비슷하지만 save를 이용하여 해당 속성을 저장
    	// 데이터베이스에서는 update문을 실행
        Optional<H2dbNotice> noticeData_update = this.h2dbNoticeRepository.findById(1);
        assertTrue(noticeData_update.isPresent());
        H2dbNotice noticeUpdate = noticeData_update.get();
        noticeUpdate.setSubject("수정된 제목");
        this.h2dbNoticeRepository.save(noticeUpdate);
    }
    
    //@Test
    // 데이터 삭제하기
    void testJpa_delete() {
    	// 리파지토리.delete(데이터) : 데이터베이스의 해당 데이터 삭제
    	assertEquals(2, this.h2dbNoticeRepository.count());
        Optional<H2dbNotice> noticeData_delete = this.h2dbNoticeRepository.findById(1);
        assertTrue(noticeData_delete.isPresent());
        H2dbNotice noticeDelete = noticeData_delete.get();
        this.h2dbNoticeRepository.delete(noticeDelete);
        assertEquals(1, this.h2dbNoticeRepository.count());
    }
    
    //@Test
    // 공지사항에 댓글 작성하기
    void testJpa_create_replyFromNotice() {
    	// 참조 할 데이터 호출
    	Optional<H2dbNotice> refData_notice = this.h2dbNoticeRepository.findById(1);
    	assertTrue(refData_notice.isPresent());
    	H2dbNotice refData = refData_notice.get();
    	
    	// 데이터 생성
    	// 참조한 엔티티와 참조한 엔티티의 데이터를 세터로 연결
    	// 실제 데이터베이스에서는 외래 키가 되어있으므로 참조되는 테이블의 세터 설정이 가능
    	// 외래 키 설정은 엔티티에서 ManyToOne을 설정하였기 때문
    	H2dbReply replyCreate1 = new H2dbReply();
    	replyCreate1.setContent("첫번째 공지사항이네요!");
    	replyCreate1.setH2dbNotice(refData);
    	replyCreate1.setCreateDate(LocalDateTime.now());    	
    	this.h2dbReplyRepository.save(replyCreate1);
    }
    
    //@Test
    // 공지사항의 댓글 조회하기
    void testJpa_read_replyFromNotice() {
    	Optional<H2dbReply> replyData_read = this.h2dbReplyRepository.findById(1);
    	assertTrue(replyData_read.isPresent());
    	H2dbReply replyData = replyData_read.get();
    	assertEquals(1, replyData.getH2dbNotice().getId());
    }
    
    //@Test
    // 공지사항의 댓글 조회하기
    void testJpa_read_replyFromNotice() {
    	Optional<H2dbReply> replyData_read = this.h2dbReplyRepository.findById(1);
    	assertTrue(replyData_read.isPresent());
    	H2dbReply replyData = replyData_read.get();
    	assertEquals(1, replyData.getH2dbNotice().getId());
    }
}

프로젝트 디렉토리 구조


Error

Setter Error

  • set이 먹히지 않을 경우 (setContent 등등)
  • 롬복(lombok)이 제대로 작동하는지 확인
  • 그래도 안된다면 수동으로 게터/세터(Getter/Setter)를 설정
// ID 컬럼 Getter/Setter 예시
// 엔티티 클래스에서 정의
public Integer getId() {
    return id;
}
public void setId(Integer id) {
    this.id = id;
}

JUnit Test Error

  • 실패한 메소드를 누르면 Failure Trace로 확인 할 수 있음

 

  • 해당 오류에서 에러 확인 가능
    • 포스팅에서 발생한 오류는 누르면 결과 비교 창 출력
    • 내가 입력한 값 ' 두번째 게시판 공지'
    • 실제 값 ' 첫번째 공지사항입니다.'

 

  • 에러 확인
    • 데이터베이스에서 1번 ID 값을 가져왔는데, 내가 확인한 값은 2번 ID 값이였음


DB ID Index Error

  • 잦은 테스트로 ID의 넘버링이 맞지 않을 경우
    • 테스트로 작성, 삭제를 반복하다보면 ID가 1, 2가 아닐 때 인덱스를 초기화하여 1부터 시작
  • H2 인덱스 초기화
  • H2 로컬 DB 쿼리창에 아래의 코드를 입력하면 인덱스가 1부터 다시 시작
ALTER TABLE H2DB_NOTICE ALTER COLUMN ID RESTART WITH 1


참고

  • 웹페이지 생성하고 DB의 데이터 목록 조회하기
    • 포스팅의 내용이 길어 다음 포스팅으로 대체합니다.

https://luvris2.tistory.com/412

 

Spring Boot - 웹 페이지에 DB 데이터 조회하기 (컨트롤러, 모델, html)

포스팅 정보 - 해당 포스팅에서 사용한 툴, 라이브러리 IDE : STS4 Project : Gradle Project DB : H2 필요 라이브러리 추가 의존성 추가 (dependencies) build.gradle dependencies { implementation 'org.springframework.boot:spring-b

luvris2.tistory.com

 

  • 위키독스 - 점프 투 스프링부트 - 리포지터리

https://wikidocs.net/160890

반응형