[박우빈, Practical Testing #5]Spring & JPA 기반 테스트

dev_lee·2024년 11월 16일
post-thumbnail

Layered Architecture

분리하는 이유는 관심사의 분리
관심사를 분리해서 책임을 나누고 유지보수 하기 용이하게

테스트 하기 복잡해보인다?
테스트 하기 어려운 영역을 분리해서 테스트 하고자하는 영역에 집중한다.
명시적이고 이해할수 있는 문서 형태로 테스트를 깔끔하게 작성한다.

Spring

Library vs Framework

스프링은 프레임워크이다.

라이브러리

  • 내 코드가 주체가 된다.
  • 필요한 기능이 있다면 외부에서 끌어와서 사용한다. (라이브러리)

프레임워크

  • 이미 갖춰진 동작할 수 있는 환경들이 구성되어 있다.
  • 내 코드가 주체가 아니고 수동적으로 이 프레임워크 안에 들어가서 역할을 한다.

스프링은 프레임워크로써 이미 갖춰진 것들, 제공하고 있는 환경들이 있고 그걸 맞춰서 우리의 코드를 작성해서 끼워넣으면 원하는 대로 동작한다.

Spring 3대 요소

  • IoC(Inversion of Control) : 제어의 역전. 객체의 생명주기에 대한 관리를 제 3자(IoC컨테이너)에 위임
  • DI(Dependency Injection) : 의존성 주입. 제 3자가 주입해준 객체를 사용
  • AOP(Aspect Oriented Programming) : 비지니스 흐름과 관계 없는 부분들을 하나로 모아서 분리 시킨 것

JPA

ORM (Object - Relation Mapping)

  • 객체 지향 패러다임과 관계형 DB 패러다임의 불일치
  • 이전에는 개발자가 객체의 데이터를 한땀한땀 매핑하여 DB에 저장 및 조회(CRUD)
  • ORM일 사용함으로써 개발자는 단순 작업을 줄이고, 비지니스 로직에 집중할 수 있다.

JPA (Java Persistence API)

  • Java 진영의 ORM 기술 표준
  • 인터페이스이고, 여러 구현체가 있지만 보통 Hibernate를 많이 사용한다.
  • 반복적인 CRUD SQL을 생성 및 실행해주고, 여러 부가 기능들을 제공한다.
  • 편하지만 쿼리를 직접 작성하지 않기 때문에, 어떤 식으로 쿼리가 만들어지고 실행되는지 명확하게 이해하고 있어야 한다.
  • Spring 진영에서는 JPA를 한번 더 추상화한 Spring Data JPA 제공
  • QueryDSL과 조합하여 많이 사용한다. (타입체크, 동적쿼리)

사용하는 어노테이션

  • @Entity, @Id, @Column
  • @ManyToOne
  • @OneToMany
  • @OneToOne
  • @ManyToMany → 일대다 - 다대일 관계로 풀어서 사용

테스트에 사용하는 어노테이션

  • @DataJpaTest
  • @SpringBootTest

@DataJpaTest는 jpa와 관련된 빈들만 주입해서 서버를 띄우기 때문에 @SpringBootTest보다 가볍다.
그러나 강사님은 @SpringBootTest 어노테이션 선호

Persistence Layer

  • Data Access의 역할
  • 비지니스 가공 로직이 포함되어서는 안된다.
    Data에 대한 CRUD에만 집중한 레이어

Business Layer

  • 비지니스 로직을 구현하는 역할
  • Persistence Layer와의 상호작용(Data를 읽고 쓰는 행위)을 통해 비지니스 로직을 전개시킨다
  • 트랜잭션을 보장해야 한다.
    → 롤백에 대한 트랜잭션을 보장을 해주는 책임을 가지는 곳이 비지니스 레이어

Presentation Layer

  • 외부 세계의 요청을 가장 먼저 받는 계층
  • 파라미터에 대한 최소한의 검증을 수행한다.
    - 비지니스 로직이 들어가지 않고 넘어온 값들에 대한 유효성 검증이 최우선이다.

하위 레이어들을 목킹 처리함

하위에 있는 두 레이어(Business, Presentation)를 가짜 객체로 대신하여 정상 동작한다는 가정하에 테스트 하고자하는 프레젠테이션 레이어에 집중하기 위하여

⭐️ Mock

MockMvc

Mock(가짜) 객체를 사용해 스프링 MVC 동작을 재현할 수 있는 테스트 프레임워크이다. MockMvc를 사용해 컨트롤러의 엔드포인트를 HTTP 메서드(GET, POST, PUT, DELETE 등)를 사용해 실제 HTTP 요청처럼 호출하여 동작을 검증한다.

class ProductControllerTest {

    @Autowired
    private MockMvc mockMvc;

	// .. 중략
}

@WebMvcTest

컨트롤러 레이어를 독립적으로 테스트하려는 경우 MockMvc와 함께 사용한다. 컨트롤러와 관련된 빈만 로드하므로 테스트 속도가 빠르다. Service, Repository 등 다른 계층의 로직은 Mock으로 대체된다.

@WebMvcTest(controllers = ProductController.class)
class ProductControllerTest {

    @Autowired
    private MockMvc mockMvc;

	// .. 중략
}

@MockBean

Mock 객체를 주입하기 위한 어노테이션이다. Mockito라는 라이브러리 사용하며, 컨테이너에 Mockito로 만든 Mock 객체를 주입하낟.

@WebMvcTest(controllers = ProductController.class)
class ProductControllerTest {

    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private ProductService productService;

	// .. 중략
}

ObjectMapper

JSON과 자바 객체간의 직렬화, 역직렬화를 처리한다. 테스트 시 요청 본문 또는 응답 데이터를 JSON 형태로 변환하거나 파싱할 때 유용하다.

@WebMvcTest(controllers = ProductController.class)
class ProductControllerTest {

    @Autowired
    private MockMvc mockMvc;
    
    @Autowired
    private ObjectMapper objectMapper;
    
    @MockBean
    private ProductService productService;
    
	// .. 중략
}

❗️ Caused by: java.lang.IllegalArgumentException: JPA metamodel must not be empty 오류 발생

-> Auditing 이슈

  1. config 분리
@EnableJpaAuditing
@Configuration
public class JpaAuditingConfig {
}
  1. @MockBean(JpaMetamodelMappingContext.class) 어노테이션 추가하기
@WebMvcTest(controllers = ProductController.class)
@MockBean(JpaMetamodelMappingContext.class)
class ProductControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @MockBean
    private ProductService productService;

[출처]https://velog.io/@cjh8746/%EC%97%90%EB%9F%AC-JPA-metamodel-must-not-be-empty-%ED%95%B4%EA%B2%B0%EA%B8%B0

1번으로 config 분리를 했는데도 오류가 발생하여 2번 방법으로 해결했다.

입력 데이터 검증(@Valid, @NotNull, ...)

입력 데이터를 검증(Validation)하기 위해 사용된다. DTO 객체나 메서드 매개변수에 붙여 데이터 유효성을 검사한다.

build.gradle에 아래 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-validation'

@Valid - DTO 객체의 필드에 설정된 검증 어노테이션과 함께 작동한다.

Controller

public class ProductController {
    // ..중략
    public void createProduct(@Valid @RequestBody ProductCreateRequest request) {
    	// ..중략
    }

주요 검증 어노테이션

  • @NotNull: 값이 null이면 안 됨
  • @NotEmpty: 값이 null이거나 비어 있으면 안 됨
  • @NotBlank: 공백 문자만으로 구성되어 있으면 안 됨
  • @Size(min, max): 문자열 또는 컬렉션의 길이 제한
  • @Email 유효한 이메일 형식이어야 함
  • @Pattern: 정규 표현식에 맞는 값이어야 함
  • @Min: 숫자의 최소값 지정
  • @Max: 숫자의 최대값 지정

DTO

// ..중략
public class ProductCreateRequest {

    @NotNull(message = "상품 타입은 필수입니다.")
    private ProductType type;

    @NotBlank(message = "상품 이름은 필수입니다.")
    private String name;

    @Positive(message = "상품 가격은 양수여야 합니다.")
    private int price;
    
    // ..중략
}

@NotNull vs @NotEmpty vs @NotBlank

  • @NotNull:값이 null이면 안 됨. (빈 문자열이나 공백만 있는 문자열은 통과)
  • @NotEmpty:값이 null이거나 비어 있으면 안 됨. (공백만 있는 문자열은 통과, 빈 문자열은 통과X)
  • @NotBlank:공백 문자만으로 구성되어 있으면 안 됨. (빈 문자열, 공백만 있는 문자열 모두 통과 X. 문자열을 체크할 땐 @NotBlank 사용 권장)

추가 키워드 - @Transactional 어노테이션

@Transactional 어노테이션
@Transactional(readOnly = true)로 주면 읽기 전용이 되며, CRUD에서 CUD 작업이 동작하지 않고 오직 R만 가능하다.

JPA에선 @Transactional(readOnly = true)를 주면 CUD 스냇샵 저장, 변경감지 기능이 동작하지 않아 성능이 향상된다.

@Transactional을 통해 CQRS 패턴으로 동작 가능하다.
"CQRS - Command와 Query를 분리하자"
즉, CUD(쓰기)와 R의(읽기) 책임을 분리 하는 것이다.
Write DB와 Read DB의 엔드 포인트를 분리함으로써 장애 격리가 가능하다.

@Transactional 어노테이션 사용 시 클래스 상단에 @Transactional(readOnly = true)를 걸고
쓰기 작업이 필요한 메서드라면 메서드 단위로 @Transactional을 달자


출처 - 박우빈, Practical Testing: 실용적인 테스트 가이드
이 블로그에 포함된 모든 코드와 이미지는 원작자이신 박우빈 강사님의 저작권에 귀속됩니다.

0개의 댓글