이번에는 지난번에 만들었던 상품등록이라는 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

+ Recent posts