
이 글은 인프런 이중석님의 👉[스프링부트-실전-상품주문-tdd] 강의를 들으며 작성하였습니다. (소중한 지식 나눔에 감사드립니다👍)
개인 공부 목적이며 상업적 용도가 아님을 밝힙니다.
강의를 들으며 추가로 궁금한 내용 및 오류를 정리한 글 입니다.
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'com.h2database:h2:2.1.210' //h2 Database 사용
JPA (Java Persistence API) 의 약자이며, 자바 객체(DTO , VO) 등을 DB에 영구적으로(Persistence) 저장하고 조회할 수 있는 ORM기술
PSA( Portable Service Abstraction , 일관된 추상 서비스) 중 하나로
최대한 자바스럽게(POJO의 객체지향적) DB의 쿼리를 이용할 수 있다.
(1) Repository를 interface로 바꾸고 JpaRepository를 상속한다
interface ProductRepository extends JpaRepository<Product , Long> {
}
타입매개변수 받은 Product는 @Entity로 사용할 클래스를 의미하고
Long은 Product 엔터티의 primary key 데이터 형식을 의미한다
(DB로 따지면 Product테이블의 Primary key)
(2) 엔터티로 사용할 Product 클래스를 변경한다
@Entity //이건 엔터티 클래스임을 의미
@Table(name = "products") //DB와 매핑할 테이블이름
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
class Product {
@Id //해당 엔터티의 primary key로써 사용 (id 필드를 key로 사용)
@GeneratedValue(strategy = GenerationType.IDENTITY)
//키 전략 : 자동증가 (mysql 의 AUTO_INCREMENT 같은거)
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;
}
}
NoArgConstructor는 매개변수가 없는 생성자를 만드는거고,
accessLevel은 해당 생성자의 접근 제어자를 의미한다.
즉 다음 코드와 동일하다
class Product {
protected Product() {} //protected 제어자인 생성자
...
}
public 으로 하면 무분별한 객체가 생성될 수 있고
private 로 하면 프록시 객체 생성이 되지 않기 때문이다.
결국 무분별한 불완전 객체 생성을 막기 위해 접근제어를 protected로 한 것
General error: "The write format 3 is larger than the supported format 2 [2.1.210/5]" [50000-210]
나는 h2 database를 JPA와 연동하기 위해 사용했는데 위와 같은 오류가 나왔다.
현재 의존성 설정으로 h2의 2.1.210 버전을 사용하고 있었는데 Stackoverflow을 확인해보니 이 버전은 H2 1.4.200 버전이나 그 이하 버전의 DB파일을 열 수 없다 라고 한다.
그래서 버전을 바꿔보았다.
기존
implementation 'com.h2database:h2:2.1.210'
변경
implementation 'com.h2database:h2:1.4.200'
이번에는 두번째 에러가 발생했다.
File corrupted while reading record: null. Possible solution: use the recovery tool [90030-200]
파일이 손상되었다고 한다. 이것도 해결방안을 찾아보았다.
(1) 본인 User 폴더 내의 test.mv.db 파일 삭제

(2) 새 파일 만들기로 test.mv.db와 파일명 일치시키기

(3) h2 재구동

이 문제는 해결 되었지만 또 에러가 발생했다.
Database may be already in use: null. Possible solutions: close all other connection(s); use the server mode [90020-200]
해결방법으로는 연결할떄 url을 jdbc:h2:tcp://localhost/~/test로 해야한다. 그래서 application.properties에 연결설정을 아래와 같이 변경했다.
(혹시 모르니 h2 콘솔도 함께 변경했다)
변경 전
jdbc:h2:~/test
변경후
jdbc:h2:tcp://localhost/~/test


이로써 정상적으로 작동이 되었다!
강의에서 테스트마다 DB를 초기화하기 위한 코드를 정리했으며 이를 분석해보았다.
<코드>
@Component
public class DatabaseCleanup implements InitializingBean {
@PersistenceContext
private EntityManager entityManager;
private List<String> tableNames;
@Override
public void afterPropertiesSet() {
final Set<EntityType<?>> entities = entityManager.getMetamodel().getEntities(); //JPA 엔터티 모두 가져옴
tableNames = entities.stream()
.filter(e -> isEntity(e) && hasTableAnnotation(e)) //엔터티 어노테이션과 테이블 어노테이션이 붙어있으면
.map(e -> e.getJavaType().getAnnotation(Table.class).name()) //해당 테이블 이름을 가져와서 리스트화
.collect(Collectors.toList());
final List<String> entityNames = entities.stream()
.filter(e -> isEntity(e) && !hasTableAnnotation(e)) //엔터티 어노테이션은 있지만 테이블 어노테이션이 없다면
.map(e-> CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE , e.getName()))//ProductItem -> product_item 으로 바꿔줌
.toList();
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 execute() {
entityManager.flush();
entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate(); //테이블 간 참조무결성을 작동하지 않도록 한다.
for(final String tableName : tableNames) {
entityManager.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate(); //테이블 데이터 모두 삭제
entityManager.createNativeQuery("ALTER TABLE " + tableName + " ALTER COLUMN ID RESTART WITH 1").executeUpdate();
//테이블 ID 의 시작 pk를 다시 1로 초기화
}
entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate(); //다시 참조무결성 세팅
}
}
빈의 주입이 완료된 후(빈 생성 및 설정) 실행되는 초기화 로직을 만들 수 있다.
즉, 순서로 따지면 @SpringBootTest로 bean 생성 및 설정 -> bean 설정 끝났으니 초기화 로직 실행 -> @Test 테스트 로직 실행 순서가 되어 테스트 실행 전마다 Database를 초기화 하는 작업을 진행해주는 것이다.
* 참고로 InitializingBean 사용은 권고하지 않고 보통 다른 방법을 사용한다
@PersistenceContext
private EntityManager entityManager;
EntityManager는 Entity들의 라이프 사이클 , 영속성 등을 관리하고 또한 DB 처리를 위한 CRUD 작업등을 메소드로 제공한다.
이런 EntityManager를 주입할 수 있는 표준이 @PersistenceContext 이다.
(이 부분은 JPA 공부할 때 자세히 다뤄볼 예정)
여기서는 그냥 DB관련 처리하려고 가져왔다 정도로만 하고 넘어가도록 하겠다.
현재 Product 클래스는 @Entity로써 products 라는 테이블과 매핑되도록 세팅되어있다.
DB로 따지면 [ products 테이블 => id (pk) , name , price , discountPolicy ]
그리고 이 Product를 JpaRepository<Product , Long>을 상속한 ProductRepository가 save()를 실행하면 insert 로 매핑된다.
https://velog.io/@qwerty1434/InitializingBean-vs-PostConstruct (InitializingBean)