본문 바로가기
개발/스프링

[혼자 구현하는 웹서비스] 3. (2) JPA 테스트 코드 및 API 작성

by 카펀 2021. 7. 9.

1. Spring Data JPA 테스트 코드 작성하기

2. 등록/수정/조회 API 만들기

3. JPA Auditing으로 생성시간/수정시간 자동화하기

 

 

1. Spring Data JPA 테스트 코드 작성하기

앞에서 작성한 코드가 잘 작동되는지 테스트하기 위한 코드를 작성해 본다.

 

test 디렉토리 아래에 domain.posts 패키지를 생성하고, 테스트 클라스는 PostsRepositoryTest라고 하자.

 

domain.posts 패키지를 만들고, 그 아래에 PostsRepositoryTest 클라스를 만든다.

 

이 테스트에서는 다음과 같이 save, findAll 기능을 테스트한다.

 

PostsRepositoryTest.java 1/2
PostsRepositoryTest.java 2/2

@After는 Junit에서 단위 테스트가 끝날 때마다 수행되는 메소드를 지정한다. 보통은 배포 전 전체 테스트를 수행할 때 테스트간 데이터 침범을 막기 위해 사용한다. 위 코드에서도 deleteAll()을 통해 남아 있는 데이터를 제거한다.

postsRepository.save는 테이블 posts에 insert/update 쿼리를 실행한다. ID 값이 존재하면 insert, 없으면 update가 실행된다.

postsRepository.findAll은 테이블 posts에 있는 모든 데이터를 조회해 온다.

 

별다른 설정 없이 @SpringBootTest를 사용할 경우 H2 데이터베이스를 자동으로 실행해 준다.

이 테스트 코드를 한번 실행해 보자.

테스트를 실행한다.
테스트가 통과되었다!

테스트를 성공적으로 통과함을 알 수 있다.

JPA를 통해 데이터베이스를 다루었다. 그렇다면 실제로 실행된 쿼리는 어떤 형태인지 알 수 있을까?

이러한 쿼리 로그를 조회할 수 있다. 스프링 부트에서는 application.properties, application.yml 등의 파일로 한 줄의 코드로 설정할 수 있도록 지원하고 권장한다.

src/main/resources 디렉토리 아래에 application.properties 파일을 생성하고, 다음과 같이 입력하자.

 

Resource Bundle을 선택하고, 이름 칸에 application으로 이름지어 주면 된다.

spring.jpa.show_sql=true

 

이렇게 하면 콘솔에서 쿼리 로그를 확인할 수 있다.

 

중간에 Hibernate: 이후에 쿼리가 표시된다.

한 가지 더 짚고 넘어가자면, 위 사진에서 create table 쿼리를 보면 id bignit generated by default as identity 라는 옵션으로 생성되었다.

이는 H2의 쿼리 문법이 적용되었기 때문인데, H2는 MySQL 쿼리를 적용해도 정상작동 되므로 출력되는 쿼리 로그를 MySQL 버전으로 할 수 있다.

 

applications.properties 파일을 다시 열고, 아래와 같은 코드를 추가하자.

 

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect

 

이후 다시 테스트 코드를 수행하면 아래와 같은 결과를 확인할 수 있다.

 

MySQL 버전으로 출력

creatte table의 옵션에 id bignit not null auto_increment 라고 표시된다.

바꾼 설정이 잘 적용되었음을 확인할 수 있다.

 

JPA와 H2에 대한 기본적인 기능과 설정을 진행했으니, 본격적으로 API를 만들어 보자.

 

4. 등록/수정/조회 API 만들기

 

API를 만들기 위해서는 총 3개의 클라스가 필요하다.

1. Request를 받을 Dto

2. API 요청을 받을 Controller

3. 트랜잭션, 도메인 기능 간의 순서를 보장하는 Service

 

오해하기 쉬운 부분으로, Service에서 비즈니스 로직을 처리한다고 생각하기 쉽다. 그렇지 않다. Service는 오로지 transaction, domain 간 순서 보장의 역할만 한다.

 

다음은 Spring 웹 계층을 나타낸 그림이다.

 

Spring Web Layer

 

간단하게 각 영역에 대해 짚고 넘어가자.

 

Web Layer

- 흔히 사용하는 컨트롤러 (@Controller)와 JSP/Freemaker 등의 뷰 템플릿 영역이다.

- 이외에도 필터 (@Filter), 인터셉터, 컨트롤러 어드바이스 (@ControllerAdvice) 등 '외부 요청과 응답' 에 대한 전반적인 영역을 이야기한다.

 

Service Layer

- @Service에 사용되는 서비스 영역이다. 일반적으로 Controller와 Dao의 중간 영역에서 사용된다.

- @Transactional이 사용되어야 하는 영역이기도 하다.

 

Repository Layer

- Database와 같이 데이터 저장소에 접근하는 영역이다. Dao (Data Access Object) 영역으로 이해할 수 있다.

 

Dtos

- Dto는 Data Transfer Object의 줄임말로, 계층 간의 데이터 교환을 위한 객체를 이야기한다. Dtos는 이들의 영역을 뜻한다.

- 예를 들어 뷰 템플릿 엔진에서 사용될 객체나 repository layer에서 결과로 넘겨준 객체 등이 이들을 뜻한다.

 

Domain Model

- 도메인이라 불리는 개발 대상을 모든 사람이 동일한 관점에서 이해할 수 있고 공유할 수 있도록 단순화한 것을 도메인 모델이라고 한다.

- @Entity가 사용된 영역 역시 도메인 모델이라고 할 수 있다. 단, 무조건 데이터베이스의 테이블과 관계가 있어야 하는 것은 아니다.

 

위 설명을 보았을 때, 비지니스 처리를 담당할 곳은 어디일까? 바로 Domain이다.

 

기존에 서비스로 처리하던 방식을 '트랜잭션 스크립트' 라고 한다. 모든 로직이 스비스 클래스 내부에서 처리되고, 그러다 보니 서비스 계층이 무의미하며, 객체는 단순한 덩어리 역할을 하게 된다.

도메인 모델에서 처리할 경우, 예를 들어 다음과 같은 코드가 될 수 있다.

 

@Transactional
public Order cancelOrder (int orderId) {

	//1)
	Orders order = ordersRepository.findById(orderId);
    Billing billing = billingRepository.findByOrderId(orderId);
    Deilvery delivery = deliveryRepository.findByOrderId(orderId);
    
    //2-3)
    delivery.cancel();
    
    //4)
    order.cancel();
    billing.cancel();
    
    return order;
}

 

order, billing, delivery가 각자 역할을 하며, 서비스 메소드는 transaction과 domain 간의 순서만 보장해 준다. 앞으로도 이런 방법으로 코드를 다룰 것이다.

 

등록, 수정, 삭제 기능을 추가해 보겠다.

PostApiController를 web 패키지에, PostsSaveRequestDto를 web.dto 패키지에, PostsService를 service.posts 패키지에 생성하자.

 

위처럼 추가하면 된다.

이후 코드는 아래와 같이 입력하도록 하자.

 

PostsApiController.java

 

PostsService.java

위 코드에서, Controller와 Service에는 @Autowired가 없다.

스프링에서는 bean을 주입받는 방법에는 @Autowired, setter, 생성자 세 가지가 있다.

이 중 가장 권장하는 방식이 생성자로 주입받는 방식이고, @Autowired는 권장되지 않는 방식이다.

즉, 생성자로 Bean 객체를 받도록 하면 @Autowired와 동일한 효과를 볼 수 있다.

그리고 생성자는 @RequiredArgsConstructor가 해결해 준다. final이 선언된 모든 필드를 인자값으로 하는 생성자를 lombok의 @RequiredArgsConstructor가 대신 생성해 준다.

위 클라스의 의존성 관계가 변경될 때마다 lombok 어노테이션이 자동으로 수정해 준다. 매우 편리하다.

 

다음은 Controller와 Service에서 사용할 Dto 클라스다.

 

PostsSaveRequestDto.java

 

앞서 작성한 Entity 클라스와 매우 유사한 형태이다.

그러나 절대로 Entity 클라스를 Request/Response 클라스로 사용해서는 안 된다. Entity 클라스는 DB와 맞닿는 핵심 클라스이기 때문이다.

화면 변경은 이에 비하면 아주 사소한 기능 변경인데, 이를 위해 DB의 테이블과 연결된 Entity 클라스를 변경하는 것은 너무 큰 변경이다.

 

아무튼 등록 코드가 완성되었으니, 이를 테스트 해 보도록 한다.

테스트 패키지 중 web 패키지에 PostsApiControllerTest를 생성하고 다음과 같이 입력한다.

 

PostsApiControllerTest.java 1/2
PostsApiControllerTest.java 2/2

 

실행하면 테스트를 통과하는 것을 확인할 수 있다.

 

WebEnvironment.RAMDOM_PORT로 인해, Tomcat이 랜덤 포트에서 실행되었고, insert 쿼리가 실행된 것을 로그를 통해 확인할 수 있다.

 

비슷하게 수정/조회 기능도 추가해 보겠다.

PostsApiController, Posts, PostsService에는 내용을 추가하고, web/dto 아래에 PostsResponseDto, PostsUpdateRequestDto 클라스를 추가 후 작성하자.

 

PostsApiController.java
PostsResponseDto.java

PostsResponseDto는 Entity의 필드 중 일부만 사용하므로, 생성자로 Entity를 받아 필드에 값을 넣는다.

굳이 모든 필드를 가진 생성자가 필요하지는 않으므로, Dto는 Entity를 받아서 처리한다.

 

PostsUpdateRequestDto.java
Posts.java
PostsService.java

재미있는 점은, update 기능을 보면 DB에서 쿼리를 날리는 기능이 없다는 점이다.

이는 JPA의 영속성 컨텍스트 덕분에 가능하다.

영속성 컨텍스트란, Entity를 영구 저장하는 환경이다. 일종의 논리적 개념으로, JPA의 핵심 내용은 Entity가 영속성 컨텍스트에 포함되어 있는지 여부로 갈린다.

JPA의 엔티니 매니저가 활성화된 상태 (Spring Data JPA를 사용한다면 기본 옵션이다)로, 트랜잭선 안에서 DB 안에서 데이터를 가져오면, 이 데이터는 영속성 컨텍스트가 유지된 상태이다.

이 상태에서 해당 데이터의 값을 변경하면 트랜잭션이 끝나는 시점에 해당 테이블에 변경분을 반영한다. 즉 Entity 객체의 값만 변경하면 별도로 update 쿼리를 날릴 필요가 없다는 것이다.

이 개념을 더티 체킹 (ditry checking)이라고 하며, 더 자세한 정보는 링크를 참고하자.

 

길고 긴 코드 작성이 끝났다. 이제 이 코드가 정상적으로 update 쿼리를 수행하는지 테스트 코드를 통해 확인해 보겠다.

수정 기능의 테스트 코드 역시 PostsApiControllerTest에 추가하겠다.

 

PostsApiControllerTest.java

테스트 결과를 보면 update 쿼리가 정상적으로 동작하는 것을 확인할 수 있다.

 

update 쿼리를 확인해 보자.

마지막으로, 조회 기능은 실제로 톰캣을 실행해서 확인해 보자.

로컬 환경에서는 H2를 DB로 사용한다. 메모리에서 실행하기 때문에 직접 접근하려면 웹 콘솔을 사용해야 한다. 이를 활성화 하자.

application.properties에 다음과 같은 옵션을 추가한다.

 

spring.h2.console.enabled=true

 

추가한 뒤, Application 클라스의 main 메소드를 실행한다. 정상적으로 실행되었다면 톰캣이 8080 포트로 실행된다.

웹 브라우저에서 http://localhost:8080/h2-console 로 접속하면 h2의 웹 콘솔에 접근할 수 있다.

 

H2 Console

JDBC URL이 위 사진처럼 jdbc:h2:mem:testdb 로 되어있지 않다면 변경해 주자.

 

Connect를 클릭한다.

다음과 같이 좌측에 POSTS 테이블이 정상적으로 노출되어야 한다.

 

Posts 테이블 노출

간단한 쿼리를 통해 DB를 조회해 보았다.

 

DB 조회; 비어 있다.

현재는 등록된 데이터가 없다. INSERT 쿼리를 통해 데이터를 추가한 후, 만들었던 API를 통해 조회해 보자.

 

insert 쿼리 실행

 

브라우저에 http://localhost:8080/api/v1/posts/1 을 입력해 보자.

 

API로 조회한 DB

insert를 통해 삽입한 데이터가 조회된 것을 확인할 수 있다.

Google Chrome에서 JSON Viewer를 설치하면 아래와 같이 더욱 깔끔하게 볼 수 있다.

 

JSON Viewer로 조회한 결과

 

여기까지 기본적인 등록/수정/조회 기능을 만들고, 테스트 해 보았다.

앞서 다루었던 JUnit을 이용한 테스트 코드까지 제작하여 보호하고 있으므로, 이후에도 안전하게 변경할 수 있다.

 

 

3.5 JPA Auditing으로 생성시간/수정시간 자동화하기

보통 Entity에는 해당 데이터의 생성시간 및 수정시간을 포함한다. 이는 차후 유지보수에 굉장히 중요한 정보이기 때문인데, 이를 매번 DB에 삽입/갱신하기 전에 날짜 데이터를 등록/수정하는 코드를 넣는 것은 어마어마하게 귀찮고 코드를 지저분하게 만드는 일이다.

JPA Auditing을 이용하여 이를 자동화할 수 있다.

 

Java 8부터 LocalDate, LocalDateTime이 등장했다. 이를 활용하겠다.

domain 패키지에 BaseTimeEntity 클라스를 작성하자.

 

BaseTimeEntity.java

BaseTimeEntity 클라스는 모든 Entity의 상위 클라스가 되어, Entity들의 createdDate, modifiedDate를 자동으로 관리하는 역할이다.

 

Posts 클라스가 BaseTimeEntity를 상속받도록 변경하자.

 

Posts.java - BaseTimeEntity를 상속받는다.

마지막으로, JPA Auditing 어노테이션을 모두 활성화할 수 있도록 Application 클라스에 활성화 어노테이션을 하나 추가하자.

 

Application.java

이제 실제 코드는 완성이 되었으니, 당연하지만 테스트 코드를 작성하여 잘 작동하는지 확인해 보도록 하자.

 

PostsRepositoryTest 클라스에 테스트 메소드를 하나 더 추가하겠다.

 

PostsRepositoryTest.java

실행해 보면 다음과 같이 실제 시간이 잘 저장되었음을 확인할 수 있다.

 

createdDate, modifiedDate가 저장되었다.

 

BaseTimeEntity만 상속받으면 등록일/수정일이 자동으로 기록되는 예쁜 코드가 완성되었다.

 

 

*이 글은 '스프링 부트와 AWS로 혼자 구현하는 웹 서비스' (프리렉, 이동욱 저)를 공부하며 내용을 정리한 글입니다.

댓글