이번에는 지난번에 만들었던 상품등록이라는 API요청이 왔을 때 메모리가 아닌 JPA를 통해 DB에 저장하는 것을 적용해 보자. (수정되는 소스가 많으니 깃허브에 올라가있는 소스를 참고하는게 좋을 것 같다....)
PorductRepository.java 수정
package com.example.productorderservice.product;
import org.springframework.data.jpa.repository.JpaRepository;
interface ProductRepository extends JpaRepository<Product,Long> {
}
class였던 ProductRepository를 interface로 변경해주었다. (extends로 사용 예정)
ProductService.java 수정 - 중간에 @Transactional이 추가되었다.
package com.example.productorderservice.product;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/products")
class ProductService {
private final ProductPort productPort;
ProductService(final ProductPort productPort) {
this.productPort = productPort;
}
@PostMapping
@Transactional
public ResponseEntity<Void> addProduct(@RequestBody final AddProductRequest request){
final Product product = new Product(request.name(), request.price(), request.discountPolicy());
productPort.save(product);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
}
Product.java 수정
수정된 내용이 많다. @Entity, @Table, @Getter, @NoAtgsConstuctor, @Id, @GenerateValue 등이 생겼다.
package com.example.productorderservice.product;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.util.Assert;
import javax.persistence.*;
@Entity
@Table(name = "products")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int price;
private DiscountPolicy discountPolicy;
public Product(final String name, final int price, final DiscountPolicy discountPolicy) {
Assert.hasText(name, "상품명은 필수입니다.");
Assert.isTrue(price > 0, "상품 가격은 0보다 커야 합니다.");
Assert.notNull(discountPolicy, "할인 정책은 필수입니다.");
this.name = name;
this.price = price;
this.discountPolicy = discountPolicy;
}
}
DatabaseCleanup.java 생성 - 주석에 상세한 설명을 적어놓았다.
package com.example.productorderservice;
import com.google.common.base.CaseFormat;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Table;
import javax.persistence.metamodel.EntityType;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Component
public class DatabaseCleanup implements InitializingBean {
@PersistenceContext
private EntityManager entityManager;
private List<String> tableNames;
@Override
public void afterPropertiesSet(){
// EntityManager에서 Entity들을 모두 가져온다.
final Set<EntityType<?>> entities = entityManager.getMetamodel().getEntities();
// 가져온 Entity들을 차례대로 돌면서 스트림을 통하여
tableNames = entities.stream()
.filter(e -> isEntity(e) && hasTableAnnotation(e)) // 돌고있는 Entity의 자바타입에서 Entity라는 Annotation이 있는지 확인한다.
.map(e -> e.getJavaType().getAnnotation(Table.class).name()) // 그 다음에 Table이라는 어노테이션이 있는지 확인한다.
.collect(Collectors.toList()); // 위 조건을 모두 만족하는 테이블의 이름을 List에 담는다.
// 테이블 이름을 가져온 후에
final List<String> entityNames = entities.stream()
.filter(e -> isEntity(e) && !hasTableAnnotation(e)) // Entity어노테이션은 있지만 Table어노테이션이 없는 애들
.map(e -> CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, e.getName()))
.toList();
// CaseFormat은 구글에서 제공하는 라이브러리 기능 중 하나로 예를들면면ProductItem 이라는 단어를 product_item 이라는 단어로 변경해준다.
// 인텔리제이 상단메뉴의 View-> Tool Windows -> Dependencies 를 열어서 guava를 검색하여 추가한다.
tableNames.addAll(entityNames);
}
private boolean isEntity(final EntityType<?> e) {
return null != e.getJavaType().getAnnotation(Entity.class);
}
private boolean hasTableAnnotation(final EntityType<?> e) {
return null != e.getJavaType().getAnnotation(Table.class);
}
@Transactional
public void excute(){
entityManager.flush();
// 참조무결성을 무시하는 명령, PK와 FK로 묶여 있기에 안지워지는 등의 데이터들을 삭제할 때 유용함.
entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate();
for(final String tableName : tableNames){
// 위에서 만든 테이블 목록을 TRUNCATE 한다.
entityManager.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate();
// @GeneratedValue 로 셋팅된 시퀀스를 초기화 한다.
entityManager.createNativeQuery("ALTER TABLE " + tableName + " ALTER COLUMN ID RESTART WITH 1").executeUpdate();
}
// 다시 참조무결성을 체크하도록 셋팅해준다.
entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate();
}
}
application.properties 수정
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
SQL 로그를 보기 위해 추가하였다.
ApiTest.java 수정
새롭게 생성한 DatabaseCleanup이 적용되었다.
package com.example.productorderservice;
import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ApiTest {
@Autowired
private DatabaseCleanup databaseCleanup;
@LocalServerPort
private int port;
@BeforeEach
void setPort(){
if(RestAssured.port == RestAssured.UNDEFINED_PORT){
RestAssured.port = port;
databaseCleanup.afterPropertiesSet();
}
databaseCleanup.excute();
}
}
DatabaseCleanup.class 위치
위와 같이 소스를 수정한 후 ProductApiTest.java를 실행하면 아래와 같이 테이블을 생성해주는 로그가 찍히고.
DatabaseCleanup 에서 셋팅해준 정보들이 나온다.
그리고 요청을 보내는 method 와 URI, Body등의 정보가 확인된 후에
DB에 Insert 하는 쿼리가 보여지고 201 코드를 응답한다.
Git : https://github.com/ShinHenry/product-order-service.git
Branch : Apply_JPA
출처 : 실전! 스프링부트 상품-주문 API 개발로 알아보는 TDD
'Backend > TDD' 카테고리의 다른 글
[TDD] 상품 조회 기능 구현하기 (0) | 2023.02.17 |
---|---|
[TDD] API 테스트로 전환하기 (0) | 2023.02.15 |
[TDD] POJO를 스프링부트 테스트로 전환하기 (0) | 2023.02.15 |
[TDD] POJO 상품 등록 기능 구현하기 (0) | 2023.02.14 |
[TDD] TDD 구현 실습 - 프로젝트 소개 및 생성 (0) | 2023.02.14 |