스프링 부트와 AWS로 혼자 구현하는 웹 서비스 3장
Chapter 03
스프링 부트에서 JPA로 데이터베이스 다뤄보자
JPA의 역할?
- 서로 지향하는 바가 다른 2개 영역(객체지향 프로그래밍 언어와 관계형 데이터베이스)을 중간에서 패러다임 일치를 시켜주기 위한 기술
- 개발자는 객체지향적으로 프로그래밍을 하고, JPA가 이를 관계형 데이터베이스에 맞게 SQL을 대신 생성해서 실행
Spring Data JPA 적용하기
-build.gradle에 아래와 같은 의존성 추가
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('com.h2database:h2')
-src/main/java 아래의 com.qwon.springboot 패키지 -> domain 패키지 ->posts 패키지
-> Posts 클래스 생성
@Entity의 선언을 통해 Posts 클래스는 실제 DB의 테이블과 매칭될 클래스임을 나타냄
여기서 @Builder를 통해 Setter의 역할을 대신할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
package com.qwon.springboot.domain.posts;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@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;
}
|
-src/main/java 아래의 com.qwon.springboot 패키지 -> domain 패키지 ->posts 패키지
-> PostsRepository 인터페이스 생성
Posts 클래스로 DB를 접근하게 하는 인터페이스
ibatis나 MyBatis 등에서 Dao라고 불리는 DB Layer 접근자를 JPA에선 Repository라 부르며 인터페이스로 생성
JpaRepository<Entity 클래스, PK타입>를 상속하면 기본적인 CRUD 메소드 자동 추가
Spring Data JPA 테스트 코드 작성
test 디렉토리에 domain.posts 패키지 -> PostRepositoryTest 클래스 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
|
package com.qwon.springboot.domain.posts;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@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("rbdnjs1224@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);
}
}
|
이제 테스트 코드를 실행시켜 보면
그림과 같이 테스트가 통과하는 것을 확인할 수 있다.
쿼리 로그 확인법
src/main/resources에 application.properties 파일 생성 -> 아래와 같이 코드 작성
spring.jpa.show_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
등록/수정/조회 API 만들기
PostsApiController를 web 패키지
PostsSaveRequestDto를 web.dto 패키지
PostsService를 service.posts 패키지에 생성
- PostsApiController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package com.qwon.springboot.web;
import com.qwon.springboot.service.posts.PostsService;
import com.qwon.springboot.web.dto.PostsSaveRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RequiredArgsConstructor
@RestController
public class PostsApiController {
private final PostsService postsService;
@PostMapping("/api/v1/posts")
public Long save(@RequestBody PostsSaveRequestDto requestDto){
return postsService.save(requestDto);
}
}
|
- PostsSaveRequestDto
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
package com.qwon.springboot.web.dto;
import com.qwon.springboot.domain.posts.Posts;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@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();
}
}
|
- PostsService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package com.qwon.springboot.service.posts;
import com.qwon.springboot.domain.posts.PostsRepository;
import com.qwon.springboot.web.dto.PostsSaveRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
@RequiredArgsConstructor
@Service
public class PostsService {
private final PostsRepository postsRepository;
@Transactional
public Long save(PostsSaveRequestDto requestDto){
return postsRepository.save(requestDto.toEntity()).getId();
}
}
|
테스트 코드 검증하기
- PostsApiControllerTest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
package com.qwon.springboot.web;
import com.qwon.springboot.domain.posts.Posts;
import com.qwon.springboot.domain.posts.PostsRepository;
import com.qwon.springboot.web.dto.PostsSaveRequestDto;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@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);
}
}
|
테스트 결과
- WebEnvironment.RANDOM_PORT로 인한 랜덤 포트 실행과 insert 쿼리 실행
-> 등록 기능 완성
수정/조회 기능
- PostsApiController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
package com.qwon.springboot.web;
import com.qwon.springboot.service.posts.PostsService;
import com.qwon.springboot.web.dto.PostsResponseDto;
import com.qwon.springboot.web.dto.PostsSaveRequestDto;
import com.qwon.springboot.web.dto.PostsUpdateRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RequiredArgsConstructor
@RestController
public class PostsApiController {
private final PostsService postsService;
@PostMapping("/api/v1/posts")
public Long save(@RequestBody PostsSaveRequestDto requestDto){
return postsService.save(requestDto);
}
@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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package com.qwon.springboot.web.dto;
import com.qwon.springboot.domain.posts.Posts;
import lombok.Getter;
@Getter
public class PostsResponseDto {
private Long id;
private String title;
private String content;
private String author;
public PostsResponseDto(Posts entity){
this.id=entity.getId();
this.title=entity.getTitle();
this.content=entity.getContent();
this.author=entity.getAuthor();
}
}
|
- PostsUpdateRequestDto
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package com.qwon.springboot.web.dto;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
package com.qwon.springboot.domain.posts;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@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;
}
public void update(String title,String content){
this.title=title;
this.content=content;
}
}
|
- PostsService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
package com.qwon.springboot.service.posts;
import com.qwon.springboot.domain.posts.Posts;
import com.qwon.springboot.domain.posts.PostsRepository;
import com.qwon.springboot.web.dto.PostsResponseDto;
import com.qwon.springboot.web.dto.PostsSaveRequestDto;
import com.qwon.springboot.web.dto.PostsUpdateRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
@RequiredArgsConstructor
@Service
public class PostsService {
private final PostsRepository postsRepository;
@Transactional
public Long save(PostsSaveRequestDto requestDto){
return postsRepository.save(requestDto.toEntity()).getId();
}
@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);
}
}
|
테스트 코드 검증하기
- PostsApiControllerTest에 아래 코드 추가
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
@Test
public void Posts_수정된다() throws Exception{
//given
Posts savedPosts=postsRepository.save(Posts.builder()
.title("title")
.content("content")
.author("author")
.build());
Long updateId=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/"+updateId;
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);
}
}
|
테스트 결과
- update 쿼리 수행
-> 수정 기능 완성
조회 기능은 실제로 톰캣을 실행해서 확인해보기
웹 콘솔 사용
- application.properties에서 아래 코드 추가
spring.h2.console.enabled=true
- http://localhost:8080/h2-console로 접속 이후 JDBC URL을 아래와 같이 변경
이후 Connect 하여 아래와 같이 데이터 추가
테스트 결과
이제, http://localhost:8080/api/v1/posts/1로 이동하면 해당 데이터를 조회할 수 있다.
-> 조회 기능 완성
JPA Auditing으로 생성시간/수정시간 자동화하기
- domain 패키지에 BaseTimeEntity 클래스 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
package com.qwon.springboot.domain;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseTimeEntity {
@CreatedDate
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime modifiedDate;
}
|
- Posts 클래스가 BaseTimeEntity를 상속하도록 변경
- Application 클래스의 어노테이션 변경
JPA Auditing 테스트 코드 작성
- PostsRepositoryTest 클래스에 아래의 테스트 메소드 추가
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@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(">>>>>>>>>> createDate="+posts.
getCreatedDate()+",modifiedDate="+posts.getModifiedDate());
assertThat(posts.getCreatedDate()).isAfter(now);
assertThat(posts.getModifiedDate()).isAfter(now);
}
|
테스트 결과
위와 같이 실제 시간이 잘 저장된 것을 확인할 수 있습니다.