7.2 단위 테스트와 통합 테스트


단위 테스트: 애플리케이션 개별 모듈을 독립적으로 테스트하는 방식

통합 테스트: 애플리케이션을 구성하는 다양한 모듈을 결합해 전체적인 로직이 의도한 대로 동작하는지 테스트하는 방식

시스템 테스트: 소프트웨어 개발 프로세스의 일부로서 전체 시스템이 요구 사항을 충족하는지 확인하는 테스트방식
( 일반적으로 개발자가 아닌 테스트 엔지니어에 의해 수행되며, 사용자가 실제로 시스템을 사용하는 것과 유사한 환경에서 수행 )

인수 테스트: 사용자 또는 고객이 소프트웨어가 요구 사항을 충족하는지 확인하는 테스트
( 일반적으로 시스템 테스트 이후에 수행되며, 사용자가 실제로 소프트웨어를 사용하는 것과 유사한 환경에서 수행 )

7.2.1

단위 테스트의 특징

  • 테스트 대상의 범위를 기준으로 가장 작은 단위의 테스트 방식
  • 일반적으로 메서드 단위로 테스트를 수행하게 되며, 메소드 호출을 통해 의도한 결과값이 나오는지 확인하는 수준으로 테스트 진행
  • 테스트 비용이 적게 들기 때문에 테스트 피드백을 빠르게 받을 수 있음

7.2.2

통합 테스트의 특징

  • 모듈을 통합하는 과정에서의 호환성 등을 포함해 애플리케이션이 정상적으로 동작하는지 확인하기 위해 수행하는 테스트 방식
  • 단위 테스트는 모듈을 독립적으로 테스트하는 반면 통합 테스트는 여러 모듈을 함께 테스트해서 정상적인 로직 수행이 가능한지 확인
  • DB나 네트워크 등 외부 요인들을 포함하고 테스트를 진행하여 애플리케이션이 온전히 동작하는지 테스트함
  • 수행할 때마다 모든 컴포넌트가 동작해야 하기 때문에 태스트 비용이 큼

DAO 설계

DAO(Data Access Object) : 데이터베이스에 접근하기 위한 로직을 관리하는 객체
비즈니스 로직의 동작 과정에서 데이터를 조작하는 기능은 DAO 객체가 수행함
스프링 데이터 JPA에서 DAO의 개념은 리포지토리가 대체하고 있음

DAO 클래스 생성

  • DAO 클래스는 일반적으로 '인터페이스 - 구현체' 구성으로 생성
  • DAO 클래스는 의존성 결합을 낮추기 위한 디자인 패턴으로, 서비스 레이어에서 DAO 객체를 주입받을 때 인터페이스를 선언하는 방식으로 구성할 수 있음
    1. DAO 인터페이스 생성 ( ProductDAO 인터페이스 )
    public interface ProductDAO {
      // 제품 삽입 메서드
      Product insertProduct(Product product);
      // 단일 제품 조회 메서드
      Product selectProduct(Long number);
      // 제품명 수정 메서드
      Product updateProductName(Long number, String name) throws Exception;
      // 제품 삭제 메서드
      void deleteProduct(Long number) throws Exception;
    }
    1. DAO 구현체 생성 ( ProductDAOImpl 클래스 )
    // 클래스를 스프링 빈으로 등록 ( @Componet 또는 @Service 사용)
    @Component
    public class ProductDAOImpl implements ProductDAO {
    private final ProductRepository productRepository;
    
      // 생성자를 이용한 의존성 주입
      @Autowired
      public ProductDAOImpl(ProductRepository productRepository) {
          this.productRepository = productRepository;
      }
    
      @Override
      public Product insertProduct(Product product) {
          return null;
      }
    
      @Override
      public Product selectProduct(Long number) {
          return null;
      }
    
      @Override
      public Product updateProductName(Long number, String name) throws Exception {
          return null;
      }
    
      @Override
      public void deleteProduct(Long number) throws Exception {
      }
    }
    1. 인터페이스 메소드 구현
    • 저장 메서드 : insertProduct()
      @Override
      public Product insertProduct(Product product) {
          // spring data JPA가 제공하는 기본 메소드 save() 
          Product savedProduct = productRepository.save(product);
          
          return savedProduct;
      }
    • 조회 메서드 : selectProduct()
      - getById(): 내부적으로 EntityManager의 getReference() 메서드를 호출함. 해당 메서드는 proxy 객체를 리턴하기 때문에 실제 쿼리는 proxy 객체를 통해 최초로 데이터에 접근하는 시점에 실행됨.

      - findById(): 내부적으로 EntityManager의 find() 메서드를 호출함. 해당 메서드는 먼저 영속성 컨텍스트의 캐시에서 값을 조회하고, 영속성 컨텍스트에 값이 존재하지 않는다면 실제 데이터베이스에서 조회함.
      @Override
      public Product selectProduct(Long number) {
          Product selectedProduct = productRepository.getById(number);
          return selectedProduct;
      }
    • 업데이트 메서드 : updateProductName()
      // 상품명 업데이트 메서드
      @Override
      public Product updateProductName(Long number, String name) throws Exception {
          Optional<Product> selectedProduct = productRepository.findById(number);
    
          Product updatedProduct;
          if (selectedProduct.isPresent()) {
              Product product = selectedProduct.get();
    
              // 별도의 update() 메서드가 없고, save() 호출 시 JPA의 더티 체크(Dirty Check)를 통해 변경 감지를 수행
              product.setName(name);
              product.setUpdatedAt(LocalDateTime.now());
    
              updatedProduct = productRepository.save(product);
          } else { // 데이터가 존재하지 않는 경우 예외처리
              throw new Exception();
          }
          return updatedProduct;
      }
    • 삭제 메서드 : deleteProduct()
      @Override
      public void deleteProduct(Long number) throws Exception {
          // 객체를 가져오는 작업
          Optional<Product> selectedProduct = productRepository.findById(number);
    
          if (selectedProduct.isPresent()) {
              Product product = selectedProduct.get();
    
              productRepository.delete(product);
          } else {
              throw new Exception();
          }
      }  

    - SimpleJpaRepository의 delete()메서드는 delete() 메서드로 전달 받은 엔티티가 영속성 컨텍스트에 있는지 파악하고, 해당 엔티티를 영속성 컨텍스트에 영속화하는 작업을 거쳐 데이터베이스의 레코드와 매핑한 후, 삭제 요청을 수행하는 메서드를 실행해 작업을 마치고 커밋(Commit) 단계에서 삭제를 진행함.

    삭제 과정 정리
    1) em.find()를 실행하여 엔티티가 내부 영속 컨텍스트 내부 캐시에 등록되지 않은 경우, DB를 조회해 객체를 영속성 컨텍스트에 저장함
    2) em.remove() 실행시, 캐시에 등록되어 있던 엔티티를 삭제하고, delete sql문을 쓰기 지연 SQL 저장소에 저장
    3) 커밋 단계에서 delete sql문 실행

데이터베이스 연동

1. 프로젝트 생성

  • project build : maven
  • Language : java
  • Spring Boot : 2.7.9에서 2.5.6로 버전 수정 (pom.xml)
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.5.6</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
  • Dependencies
    1. Developer Tools: Lombok, Spring Configuration Processor
    2. Web: Spring Web
    3. SQL: Spring Data JPA, MariaDB Driver
  • swagger 의존성 추가 (pom.xml)
		<!--Swagger 의존성 추가-->
		<dependency>
			<groupId>io.springfox</groupId>
			<artifactId>springfox-swagger2</artifactId>
			<version>2.9.2</version>
		</dependency>

		<dependency>
			<groupId>io.springfox</groupId>
			<artifactId>springfox-swagger-ui</artifactId>
			<version>2.9.2</version>
		</dependency>
  • SwaggerConfiguration 파일 수정
package com.springboot.jpa.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class SwaggerConfiguration {

  @Bean
  public Docket api() {
    return new Docket(DocumentationType.SWAGGER_2)
            .apiInfo(apiInfo())
            .select()
            .apis(RequestHandlerSelectors.basePackage("com.springboot.jpa"))
            .paths(PathSelectors.any())
            .build();
  }

  private ApiInfo apiInfo() {
    return new ApiInfoBuilder()
            .title("Spring Boot Open API Test with Swagger")
            .description("설명 부분")
            .version("1.0.0")
            .build();
  }
}

2. 데이터베이스 관련 설정 추가 ( application.properties)

spring.datasource.driverClassName=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://{DB IP주소}:3306/{데이터베이스이름}
spring.datasource.username={DB 사용자명: root}
spring.datasource.password={비밀번호}

spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
  • line1 : mariaDB 드라이버 정의
  • line2 : 마리아 DB의 경로임을 명시하고 경로와 데이터베이스명 입력
  • line3, line 4 : 데이터베이스 계정 정보 기입 ( 일반적으로는 보안상 암호화해서 사용 )
  • line6 : ddl-auto( DDL 자동 생성 ) 전략
    1. create : 애플리케이션이 가동되고 SessionFactory가 실행될 때 기존 테이블을 지우고 새로 생성
    2. create-drop : create와 동일한 기능을 수행하나 애플리케이션을 종료하는 시점에 테이블을 지움
    3. update : SessionFactory가 실행될 때 객체를 검사해서 변경된 스키마를 갱신함 ( 기존에 저장된 데이터는 유지됨 )
    4. validate : update처럼 객체를 검사하지만 스키마는 건드리지 않음 (검사 과정에서 데이터베이스의 테이블 정보와 객체 정보가 다르면 에러 발생)
    5. none : ddl-auto 기능을 사용하지 않음
  • line7 : show-sql 설정시 로그에 하이버네이트가 생성한 쿼리문 출력
  • line8 : 7번에서 출력되는 쿼리문을 보기 좋게 포매팅


* ddl-auto
운영환경 : 대체로 validate, none 사용
개발환경 : 대체로 create, update 사용

JPA (Java Persistence API)

JPA는 JAVA 진영의 ORM(Object-Relational Mapping) 기술 표준으로 채택된 인터페이스 모음입니다.

특징

  • 내부적으로 JDBC를 사용
  • JPA 기반의 구현체로 하이버네이트(Hibernate), 이클립스 링크(EclipseLink), 데이터 뉴클리어스(DataNucleus)가 존재

장점

  1. 특정 Database에 종속되지 않음
    • ORM을 통해 자동 생성된 SQL문 기반으로 DB 테이블을 관리함
    • 추상화한 데이터 접근 계층을 제공하여 설정 파일에 DB 정보만 수정하면 손쉽게 DB를 변경할 수 있음
  2. 객체지향적 프로그래밍
  3. 유지보수 및 재사용이 편리함

단점

  1. 복잡한 쿼리 처리가 어렵다
    • Native SQL : 직접 쿼리문을 작성할 수 있다 ( DB에 종속적 )
    • JPQL : 객체를 대상으로 하는 쿼리 (인라인 뷰, 특정 DB 함수 등은 사용 불가)
  2. 성능 저하 위험
    • 자동으로 쿼리가 생성되므로 설계를 잘못하면 성능 문제가 발생할 수 있음
  3. 러닝커브가 높음

+ Recent posts