옛날에는 iBatis(현재 MyBatis)와 같은 SQL Mapper를 이용해 DB의 쿼리를 작성하였음
-> 이 경우에는 개발하는 시간보다, SQL을 다루는 시간이 더 많고, 객체 모델링 보다는 테이블 모델링에 집중하며, 객체를 단순히 테이블에 맞추어 데이터 전달 역할만 하는 개발 형태 였음
-> 관계형 Db를 이용하는 프로젝트에서 객체지향 프로그래밍을 하기 위해 나온 것이 JPA라는 자바 표준 ORM(Object Relational Mapping)
기술
객체를 관계형 DB에서 관리하는 것은 무엇보다 중요
-> 관계형 DB에서는 SQL만 인식할 수 있기 때문에 각 테이블마다 기본적인 CRUD SQL을 매번 생성해야 함
ex) User 객체를 테이블로 관리한다면
insert into user(id, name, ...) values (...);
select * from user where ...;
update user set ... where ...;
delete from user where ...;
개발자가 아무리 자바 클래스의 설계를 잘해도, SQL을 통해야만 DB에 저장하고 조회할 수 있음
-> 결국 RDB를 사용할려면 SQL을 피할수는 없음
-> 실제 현업의 수 많은 테이블을 만들때, 테이블 수의 몇배의 SQL을 만들고 유지보수해야함
-> 이러한 단순 반복 작업 외에도 패러다임 불일치 문제가 있음
관계형 DB는 어떻게 데이터를 저장할지에 초점을 맞춘 반면, 객체지향 프로그래밍 언어는 메세지(상호 작용을 위한 호출)
를 기반으로 기능과 속성을 한 곳에서 관리하는 기술
User user = findUser();
Group group = user.getGroup();
위 경우에는 User와 group은 부모-자식 관계임이 명확
여기에 DB가 추가된 경우
User user = userDao.findUser();
Group group = groupDao.findGroup(user.getGroupdId());
위 경우는 User와 Group을 따로 조회하게 됨
-> 상속, 1:N 등 다양한 객체 모델링을 DB로는 구현할 수 없음
-> 그러다보니 웹 애플리케이션 개발이 점점 DB 모델링에 집중하게 되었고 이를 해결하기 위해 JPA가 등장
즉, 개발자는 객체지향적으로 프로그래밍 하고, JPA가 이를 관계형 DB에 맞게 SQL을 대신 생성해서 실행
JPA는 인터페이스로서 자바 표준 명세서
-> 인터페이스를 사용하기 위해 구현체가 필요한데 대표적으로 Hibernate,Eclipse Link 등이 있음
-> Spring에서 JPA를 사용할때는 이 구현체를 직접 다루진 않고, 구현체들을 좀 더 쉽게 사용하고자 추상화시킨 Spring Data JPA라는 모듈을 사용해 JPA 기술을 다룸
JPA <- Hibernate <- Spring data JPA
관계
이렇게 한 단계 더 감싸 놓은 Spring data JPA 등장 이유
구현체 교체의 용이성 : Hibernate 외의 다른 구현체로 쉽게 교체하기 위함(Spring Data JPA 내부에서 구현체 매핑을 지원)
저장소 교체의 용이성 : 관계형 DB 외에 다른 저장소로 쉽게 교체
(ex : 관계형 DB로 트래픽이 감당안되어 MongoDb로 교체가 필요하다면 개발자는 Spring data JPA에서 Spring Data MongoDb로 의존성만 교체하면됨)
-> Spring data의 하위 프로젝트들은 기본적인 CRUD 인터페이스가 같기에 가능
게시판의 요구 사항
게시판 기능
: 게시글 조회, 게시글 등록, 게시글 수정, 게시글 삭제
회원 기능
: 구글 / 네이버 로그인, 로그인한 사용자 글 작성 권환, 본인 작성 글에 대한 권한 관리
우선적으로 jpa와 h2에 대한 의존성 추가
-> h2는 인메모리형 관계형 DB로, 애플리케이션 재시작시 초기화 되기 떄문에 테스트 용도로 자주 사용
우선적으로 domain 패키지 생성
-> domain이란 게시글, 댓글, 회원, 정산, 결제 등 소프트웨어에 대한 요구사항 혹은 문제 영역
이라고 생각하면 됨
domain 패키지에 posts 패키지와 posts 클래스 생성
@Getter
@NoArgsConstructor
@Entity // 보통 주요 어노테이션을 클래스 가까이 둠, 롬복은 필수가 아니기에 좀 멀리
public class Posts {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 500, nullable = false)
private String title;
@Column(columnDefinition = "TEXT", nullable = false)
private String content;
private String author;
@Builder
public Posts(String title, String content, String author){
this.title = title;
this.content = content;
this.author = author;
}
}
위 작성한 Posts클래스는 실제 DB의 테이블과 매칭될 클래스이며, 보통 Entity 클래스라고 함
-> JPA 사용 시, DB 데이터에 작업을 할 경우 실제 쿼리를 날리기 보다는 Entity 클래스의 수정을 통해 작업을 진행
@Entity
: 테이블과 링크될 클래스임을 나타냄
@Id
: 해당 테이블의 PK 필드
@GeneratedValue
: PK의 생성 규칙에 대해 나타냄
@Column
: 테이블의 칼럼을 굳이 선언하지 않아도 , 클래스의 필드는 모두 칼럼이 됨
-> 기본값 이외에 추가로 변경이 필요한 옵션이 있으면 사용
PK 생성 규칙은 왠만하면 GenerationType.IDENTITY(auto_increment)를 추천
-> 유니크 키나 여러키를 조합한 복합키로 PK를 경우 난감한 경우 종종 발생
중간 테이블을 하나 더 생성해야 하거나, 인덱스에 좋은 영향을 못끼침
유니크한 조건이 변경될 경우, PK 전체를 수정해야 하는 일 발생 하므로 주민등록번호나 복합키 등은 유니크 키로 별도로 추가하는걸 추천
위 코드를 보았을 때 Setter 메서드를 사용하지 않은 것에 주목!
(무작정 getter/setter를 사용하는 것은 바람직 하지 않음)
-> 무작정 사용시, 해당 클래스의 인스턴스 값들이 언제 어디서 변해야 하는지 코드상으로 명확하게 구분할 수 없어, 차후 기능 변경 시 복잡 해짐
따라서 Entity 클래스에서는 절대 Setter 메서드를 만들지 않음
-> 만약 해당 필드의 값 변경이 필요하면, 명확히 그 목적과 의도를 나타낼 수 있는 메소드를 추가해야 함
ex) 주문 취소 메서드를 만든다고 가정
// 잘못된 예시
public class Order{
public void setStatus(boolean status){
this.status = status
}
}
public void 주문서비스의_취소이벤트(){
order.setStatus(false);
}
// 올바른 예시
public class Order{
public void cancelOrder(){
this.status = false;
}
}
public void 주문서비스의_취소이벤트(){
order.cancelOrder();
}
하지만 Setter가 없는 상황에서 어떻게 값을 채워 DB에 삽입을 해야 할까?
기본적인 구조는 생성자를 통해 최종값을 채운 후에 DB에 삽입한 것이고, 값 변경이 필요한 경우 해당 이벤트에 맞는 public 메서드를 호출하여 변경하는 것을 전제
위 코드에서는 @Builder를 사용
-> 생성자나 빌더나 생성 시점에 값을 채워주는 역할을 같지만, 생성자의 경우 지금 채워야 할 필드가 무엇인지 명확히 지정할 수 없음
// 생성자의 경우
public Example(String a, String b){
this.a = a;
this.b = b;
}
// 위 경우에는 개발자가 new Example(b,a)처럼 a와 b의 위치를 변경해도 코드를 실행하기 전엔 문제를 찾을 수 없음
// 반면 빌더 사용 시에는 어느 필드에 어떤 값을 채울지 명확하게 인지 가능
Example.builder().
.a(a)
.b(b)
.build();
Post 클래스 생성이 끝났다면, Posts 클래스로 DB에 접근하게 해줄 JpaRepository 생성
public interface PostsRepository extends JpaRepository<Posts, Long>{
}
위 처럼 인터페이스 생성 후, JpaRepository<Entity 클래스, PK 타입>을 상속하면 기본적인 CRUD 메서드가 자동으로 생성됨
-> @Repository를 생성할 필요는 없지만, Entity 클래스와 기본 entity Repository는 함께 위치해야 함!
-> 만약 프로젝트 규모가 커져 도메인별로 프로젝트를 분리해야 한다면 Entity 클래스와, 기본 Repository는 함께 움직여야 하므로 도메인 패키지에서 함께 관리
@RunWith(SpringRunner.class)
@SpringBootTest
public class PostsRepositoryTest {
@Autowired
PostsRepository postsRepository;
@After
public void cleanup(){
postsRepository.deleteAll();
}
@Test
public void 게시글저장_불러오기(){
//given
String title = "테스트 게시글";
String content = "테스트 본문";
postsRepository.save(Posts.builder()
.title(title)
.content(content)
.author("jojoedu@gmail.com")
.build());
//when
List<Posts> postsList = postsRepository.findAll();
//then
Posts posts = postsList.get(0);
assertThat(posts.getTitle()).isEqualTo(title);
assertThat(posts.getContent()).isEqualTo(content);
}
}
After 어노테이션을 통해 단위 테스트가 끝날때마다 수행되는 메서드 지정
save 메서드는 테이블에 insert(id 값 없는 경우)/update(id 있는 경우) 쿼리를 실행
-> 실제 동작 쿼리를 보고 싶다면 application.properties 파일에 spring.jpa.show_sql = true
로 설정
Hibernate: drop table posts if exists
Hibernate: create table posts (id bigint generated by default as identity, author varchar(255), content TEXT not null, title varchar(500) not null, primary key (id))
Hibernate: insert into posts (id, author, content, title) values (null, ?, ?, ?)
Hibernate: select posts0_.id as id1_0_, posts0_.author as author2_0_, posts0_.content as content3_0_, posts0_.title as title4_0_ from posts posts0_
Hibernate: select posts0_.id as id1_0_, posts0_.author as author2_0_, posts0_.content as content3_0_, posts0_.title as title4_0_ from posts posts0_
Hibernate: delete from posts where id=?
create table 쿼리를 보면 id bigint generated by default as identity라는 옵션으로 생성(이는 H2 쿼리 문법이 적용된 것)
-> 이를 MySQL 버전으로 로그를 찍기 위해 옵션 변경
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
API를 만들기 위해선 총 3개의 클래스가 필요
Dto
Controller
Service
많은 사람들은, Service에서 비지니스 로직을 처리해야 한다고 착각
-> 그러나 Service는 트랜잭션, 도메인 간 순서 보장 역할만을 함(비즈니스 로직은 누가 처리하지...?)
spring 웹 계층
Web Layer
: 흔히 사용하는 컨트롤러와 JSP/Freemarker 등의 뷰 템플릿 영역
이 외에도 필터, 인터셉터, 컨트롤러 어드바이스 등 외부 요청과 응답에 대한 전반적인 영역을 다룸
Service Layer
: 일반적으로 Controller와 Dao의 중간 영역으로, @Service에 사용되는 서비스 영역
@Transactional이 사용되어야 하는 영역
Repository Layer
: DB와 같이 데이터 저장소에 접근하는 영역(기존의 DAO(Data access Object) 영역으로 이해하면 편함)
Dto(Data transfer Object)s
: 계층 간에 데이터 교환을 위한 객체를 다룸
ex) 뷰 템플릿에 사용될 객체나 Repository에서 결과로 넘겨준 객체 등
Domain Model
: 도메인이라고 불리는 개발 대상을 모든 사람이 동일한 관점에서 이해할 수 있고 공유하도록 단순화 시킨 것
ex) 택시 앱이라고 하면 배차,탑승,요금 등이 모두 도메인이 될 수 있음
@Entity 영역 또한 도메인 모델에 해당하지만, 무조건 DB의 테이블과 관계가 있어야 할 필요는 없음(ex: VO)
도메인 모델은 쉽게 말해 비즈니스 로직을 캡슐화하고, 데이터를 처리하는 객체
-> 이는 일반적으로 JPA나 MyBatis 등의 ORM 프레임워크를 사용하여 구현됨
위 5가지 레이어에서 비지니스 처리를 담당해야 할 곳은 Domain
기존에 서비스를 처리하던 방식을 트랜잭션 스크립트라 부름
ex) 트랜잭션 스크립트로 주문 취소 로직을 작성한다면
// 슈도 코드
@Transactional
public Order cancelOrder(int orderId){
1) 데이터베이스로 부터 (Orders), 결제 정보(Billing), 배송 정보(Delivery) 조회
2) 배송 취소를 해야 하는지 확인
3) if(배송 중이라면) { 배송 취소로 변경}
4) 각 테이블의 취소 상태 Update
}
// 실제 코드
@Transactional
public Order cancelOrder(int orderId){
// 1)
OrdersDto order = ordersDao.selectOrders(orderId);
BillingDto billing = billingDao.selectBilling(orderId);
DeliveryDto delivery = deliveryDao.selectDelivery(orderId);
// 2)
String deliveryStatus = delivery.getStatus();
// 3)
If("IN_PROGRESS".equals(deliveryStatus)){
delivery.setStatus("CANCEL");
deliveryDAo.update(delivery);
}
// 4)
order.setStatus("CANCEL");
ordersDao.update(order);
billing.setStatus("CANCEL");
deliveryDao.update(billing);
return order;
}
위 경우 모든 로직이 서비스 클래스 내부에서 처리됨
-> 그러다보니 서비스 계층이 무의미 하고, 객체란 단순히 데이터 덩어리 역할만 하게됨
-> 반면 도메인 모델에서 처리할 경우 다음과 같은 코드가 될 수 있음
@Transactional
public Order cancelOrder(int orderId){
// 1)
Orders order = ordersRepository.findById(orderId);
Billing billing = billingRepository.findByOrderId(orderId);
Delivery delivery = deliveryRepository.findByOrderId(orderId);
// 2-3)
delivery.cancel();
// 4)
order.cancel();
billing.cancel();
return order;
}
위 경우 order,billing,delivery 가 각자 본인의 취소 이벤트를 처리하며, 서비스 메서드는 트랜잭션과 도메인 간의 순서만 보장해 줌
PostsApiController
@RequiredArgsConstructor
@RestController
public class PostsApiController {
private final PostsService postsService;
@PostMapping("/api/v1/posts")
public Long save(@RequestBody PostsSaveRequestDto requestDto){
return postsService.save(requestDto);
}
}
PostsService
@RequiredArgsConstructor
@Service
public class PostsService {
private final PostsRepository postsRepository;
@Transactional
public Long save(PostsSaveRequestDto requestDto){
return postsRepository.save(requestDto.toEntity()).getId();
}
}
스프링에서 Bean을 주입하는 방식에는 @Autowired, setter, 생성자 방법이 있음
-> 가장 권장하는 방법은 생성자로 주입받는 방법
-> 즉 생성자(@RequiredArgsConstructor)로 bean 객체를 받도록 하면 @Autowire와 동일한 효과
Controller와 Service에서 사용할 Dto 클래스 생성(PostsSaveRequestDto.java)
@Getter
@NoArgsConstructor
public class PostsSaveRequestDto {
private String title;
private String content;
private String author;
@Builder
public PostsSaveRequestDto(String title, String content, String author){
this.title = title;
this.content = content;
this.author = author;
}
public Posts toEntity(){
return Posts.builder()
.title(title)
.content(content)
.author(author)
.build();
}
}
위 경우 앞서만든 Posts라는 엔티티 클래스와 거의 유사함에도, DTO 클래스를 추가로 생성하였음
-> 절대로 Entity 클래스를 Request/Response 클래스로 사용해서는 안됨!
Entity 클래스는 DB와 맞닿은 핵심 클래스
-> 이를 기준으로 테이블이 생성되고, 스키마가 변경됨
-> 또한 서비스 클래스나 비즈니스 로직들이 Entity 클래스를 기준으로 동작 하는데,이러한 Entity 클래스가 변경되면 여러 클래스에 영향을 끼치지만, Request와 Response용 Dto는 View를 위한 클래스라 정말 자주 변경됨
DTO 클래스는 보통 컨트롤러에서 뷰로 데이터를 전달하기 위한 용도로 사용 됨
즉 View Layer와 DB Layer의 역할 분리를 철저하게 하는 것이 좋음
-> 실제로 Controller에서 결과값으로 여러 테이블을 조인해서 줘야 할 경우가 빈번하므로 Entity 클래스만으로 표현하기가 어려운 경우가 많음
꼭 Entity 클래스와 Controller에서 쓸 Dto는 분리해서 사용해야 함
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PostsApiControllerTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private PostsRepository postsRepository;
@After
public void tearDown() throws Exception{
postsRepository.deleteAll();
}
@Test
public void Posts_등록된다() throws Exception{
//given
String title = "title";
String content = "content";
PostsSaveRequestDto requestDto = PostsSaveRequestDto.builder()
.title(title)
.content(content)
.author("author")
.build();
String url = "http://localhost:" + port + "/api/v1/posts";
// when
ResponseEntity<Long> responseEntity = restTemplate.postForEntity(
url, requestDto, Long.class);
// then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(responseEntity.getBody()).isGreaterThan(0L);
List<Posts> all = postsRepository.findAll();
assertThat(all.get(0).getTitle()).isEqualTo(title);
assertThat(all.get(0).getContent()).isEqualTo(content);
}
}
HelloController와 다르게 @WebMvcTest를 사용하지 않았음
-> @WebMvcTest의 경우 JPA가 작동하지 않고, Controller와 ControllerAdivce 등 외부 연동과 관련된 부분만 활성화 되기 떄문
-> 따라서 JPA 기능까지 한번에 테스트 할때는 @SpringBootTest와 RestTemplate를 사용
ResponseEntity는 HTTP 응답을 나타내는 클래스 중 하나로, 이 클래스는 본문(body)에 데이터를 포함 시킬수 있고, 상태코드 및 헤더 정보 설정 가능
예시
// HTTP 상태 코드 200 OK와 함께 "Hello, World!" 문자열을 포함하는 ResponseEntity 생성
ResponseEntity<String> responseEntity = ResponseEntity.ok("Hello, World!");
// HTTP 상태 코드 404 Not Found를 반환하는 ResponseEntity 생성
ResponseEntity<String> responseEntity = ResponseEntity.notFound().build();
// HTTP 상태 코드 500 Internal Server Error와 함께 오류 메시지를 포함하는 ResponseEntity 생성
ResponseEntity<String> responseEntity = ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred.");
controller
...
@PutMapping("/api/v1/posts/{id}")
public Long update(@PathVariable Long id,
@RequestBody PostsUpdateRequestDto requestDto){
return postsService.update(id, requestDto);
}
@GetMapping("/api/v1/posts/{id}")
public PostsResponseDto findById(@PathVariable Long id){
return postsService.findById(id);
}
...
PostsResponseDto
@Getter
public class PostsResponseDto {
private Long id;
private String title;
private String content;
private String author;
// entity의 필드 중 일부만을 사용하므로 생성자로 entity를 받아 필드 값에 대입
public PostsResponseDto(Posts entity){
this.id = entity.getId();
this.title = entity.getTitle();
this.content = entity.getContent();
this.author = entity.getAuthor();
}
}
PostsUpdateRequestDto
@Getter
@NoArgsConstructor
public class PostsUpdateRequestDto {
private String title;
private String content;
@Builder
public PostsUpdateRequestDto(String title, String content){
this.title = title;
this.content = content;
}
}
Posts 엔티티 클래스에 update 메서드 추가
public void update(String title, String content){
this.title = title;
this.content = content;
}
PostsService에 메서드 추가(수정/조회)
@Transactional
public Long update(Long id, PostsUpdateRequestDto requestDto){
Posts posts = postsRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("헤당 게시글이 없습니다. id= " + id));
posts.update(requestDto.getTitle(), requestDto.getContent());
return id;
}
public PostsResponseDto findById(Long id){
Posts entity = postsRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다, id = " + id));
return new PostsResponseDto(entity);
}
위 update 메서드를 보면 DB에 쿼리를 날리는 부분이 없음
-> JPA의 영속성 컨텍스트
때문
영속성 컨텍스트란 간단히 말해 엔티티를 영구 저장하는 환경
-> 여기서 핵심은 엔티티가 영속성 컨텍스트에 포함되어 있냐 아니냐로 갈림
JPA의 엔티티 매니저가 활성화된 상태(spring data jpa 사용시 기본 옵션)에서, 트랜잭션 안에서 DB에서 데이터를 가져오면 이 데이터는 영속성 컨텍스트가 유지
된 상태
-> 위 상태에서 해당 데이터의 값만 변경하면, 트랜잭션이 끝나는 시점에 해당 테이블에 변경 분을 반영(즉 Entity 객체의 필드만 변경하면 별도로 update 쿼리를 날릴 필요가 없음)
-> 이를 더티체킹(Dirty Checking)
이라고 함
수정 기능 테스트 코드
@Test
public void Posts_수정된다() throws Exception{
//given
Posts savedPosts = postsRepository.save(Posts.builder()
.title("title")
.content("content")
.author("author").build());
Long updatedId = savedPosts.getId();
String expectedTitle = "title2";
String expectedContent = "content2";
PostsUpdateRequestDto requestDto = PostsUpdateRequestDto.builder()
.title(expectedTitle)
.content(expectedContent)
.build();
String url = "http://localhost:" + port + "/api/v1/posts/" + updatedId;
HttpEntity<PostsUpdateRequestDto> requestEntity = new HttpEntity<>(requestDto);
// when
ResponseEntity<Long> responseEntity = restTemplate.exchange(url, HttpMethod.PUT,
requestEntity, Long.class);
// then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(responseEntity.getBody()).isGreaterThan(0L);
List<Posts> all = postsRepository.findAll();
assertThat(all.get(0).getTitle()).isEqualTo(expectedTitle);
assertThat(all.get(0).getContent()).isEqualTo(expectedContent);
}
조회 기능은 실제로 톰캣을 실행해서 확인해보기
-> 우선 application.properties에 옵션 추가(spring.h2.console.enabled=true)
-> main 메서드 실행 후 http://localhost:8080/h2-console로 접속
insert into posts (author, content, title) values('author', 'content', 'title');
보통 엔티티에는 해당 데이터의 생성시간과 수정시간을 포함함
-> 차후 유지보수에 중요한 정보
그렇다 보니 매번 DB에 insert, update 하기 전 날짜 데이터를 등록/수정하는 코드가 여기저기 들어감
public void savePost(){
...
posts.setCreateDAte(new LocalDate());
postsRepository.save(posts);
...
}
위와 같은 단순 반복적 코드가 모든 테이블과 서비스 메서드에 포함되어야 하면 지저분 해줌
-> 이를 해결하기 위해 JPA Auditing 사용
자바 8 이전에는 Date와 Calendar 클래스를 사용했지만, 현재는 LocalDate 및 LocalDateTime 사용
domain 패키지에 BaseTimeEntity 클래스 생성
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
@CreatedDate
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime modifiedDate;
}
@MappedSuperClass : JPA 엔티티 들이 BaseTimeEntity를 상속할 경우 필드들(createdDate,modifiedDate)도 칼럼으로 인식하도록 함
@EntityListneres(AuditingEntityListener.class) : BaseTimeEntity 클래스에 Auditing 기능을 포함시킴
@CreatedDate : Entity가 생성되어 저장될 때 시간이 자동으로 저장 됨
@LastModifiedDAte : 조회한 Entity의 값을 변경할 때 시간이 자동 저장 됨
위와 같이 모든 Entity의 상위 클래스가 되어 Entity들의 createdDate, modifiedDAte를 자동으로 관리하는 역할
-> 이후 Posts 클래스가 BaseTimeEntity를 상속받도록
이후 JPA Auditing 어노테이션들을 모두 활성화 할 수 있도록 application 클래스에 활성화 어노테이션 추가
@EnableJpaAuditing // 이 부분 추가
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@Test
public void BaseTimeEntity_등록(){
// given
LocalDateTime now = LocalDateTime.of(2019,6,4,0,0,0);
postsRepository.save(Posts.builder()
.title("title")
.content("content")
.author("author")
.build());
// when
List<Posts> postsList = postsRepository.findAll();
// then
Posts posts = postsList.get(0);
System.out.println(">>>>>> createdDate = "+posts.getCreatedDate() +
", modifiedDate="+posts.getModifiedDate());
assertThat(posts.getCreatedDate()).isAfter(now);
assertThat(posts.getModifiedDate()).isAfter(now);
}
위 코드는 JPA Auditing 테스트를 위함