[Spring] 3장 스프링 부트에서 JPA로 데이터베이스 다루기
카테고리: springstudy
3장 - 스프링 부트에서 JPA로 데이터베이스 다뤄보자
3.1 JPA 소개
- JPA의 등장 배경 : 개발자는 객체 지향적으로 프로그래밍을 하고, JPA가 이를 관계형 데이터베이스에 맞게 SQL을 대신 생성해서 실행
- SQL에 종속적인 개발을 하지 않아도 됨
Spring Data JPA
- JPA는 인터페이스! 이를 사용하기 위해선 구현체 (Hibernate가 대표적) 필요 → 이들을 좀 더 쉽게 사용하고자 사용된 기술
- 구현체 교체의 용이성 : Hibernate 외에 다른 구현체로 쉽게 교체하기 위해
- 저장소 교체의 용이성 : 관계형 데이터베이스 외에 다른 저장소로 쉽게 교체하기 위해
- 트래픽이 많아져 MongoDB로 교체 시 Spring Data JPA에서 Spring Data MongoDB로 의존성만 교체하면 됨
- 왜? Spring Data의 하위 프로젝트들은 save, findAll, findOne 같은 CRUD의 인터페이스를 모두 갖고 있기 때문!
3.2 프로젝트에 Spring Data Jpa 적용하기
- 의존성 추가
1
2
3
4
depencencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'com.h2database:h2'
}
- 도메인 패키지 생성
- 도메인이란 ? 게시글, 댓글, 회원, 정산, 결제 등 요구사항 혹은 문제 영역
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Getter
@NoArgsConstructor
@Entity
public class Posts {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // PK의 생성규칙을 나타냄
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;
}
}
[ 롬복 @ ]
- @Getter : 클래스 내 모든 필드의 Getter 메소드 자동생성
- @NoArgsConstructor : 기본 생성자 자동 추가
- @Builder : 해당 클래스의 빌더 패턴 클래스 생성, 이 생성자에 포함된 필드만 빌더에 포함
[ JPA @ ]
-
@Entity
❗이 Post 클래스는 실제 DB의 테이블과 매칭, 보통 Entity 클래스라고 함
❗JPA를 사용하며 DB 데이터에 작업할 때 실제 쿼리를 날리기 보단 이 Entity 클래스의 수정으로 작업 - @Id : 해당 테이블의 PK 필드를 나타냄
- @GeneratedValue : PK의 생성규칙
- @Column : 테이블의 column을 나타냄. 기본값 외에 추가로 변경이 필요할 때 사용 (ex. 문자열 사이즈 255에서 늘리고 싶을 때, 타입을 TEXT로 바꾸고 싶을 때)
❗Entity 클래스에서는 Setter 메소드를 만들지 X
- Setter가 없는 상황에서 어떻게 DB에 값을 insert할까?
- 기본적 구조? 생성자를 통해 채운 후 DB에 삽입, 값 변경이 필요하면? public 메소드를 통해!
- @Builder 를 통해 제공되는 빌더 클래스를 사용 → 지금 채워야 할 필드가 무엇인지 명확히 지정
Posts 클래스로 Database를 접근하게 해줄 JpaRepository를 만들자!
1
2
3
public interface PostsRepository extends JpaRepository<Posts, Long> {
}
JpaRepository<Entity클래스, PK타입>을 상속하면 기본적인 CRUD 메소드가 자동 생성
❗주의❗: Entity 클래스와 Entity Repository는 함께 위치하며 domain 패키지에서 함께 관리
3.3 Spring Data JPA 테스트 코드 작성하기
✅ save, findAll 기능 테스트 - PostsRepositoryTest
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
@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("ohsusu515@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);
}
}
- @SpringBootTest : H2 DB를 자동으로 실행해줌
- @After : 단위 테스트가 끝날 때마다 실행되며 보통 배포 전 테스트 간 데이터 침범을 막기 위해
- postsRepository.findAll : posts 테이블에 insert/update 쿼리 실행, id값이 있다면 update, 있다면 insert 실행
- 실제 실행된 쿼리가 어떤 형태인지 알아보려면?
1
2
// resources/application.properties 파일
spring.jpa.show-sql=true
id bigint generated by default as identity : H2의 쿼리 문법이 적용되었기 때문
- MySQL 버전으로 바꿔보기
1
2
3
// resources/application.properties 파일
spring.jpa.properties.hibernate.dialect=org.hibernate.
dialect.MySQL5InnoDBDialect
→ 왜 안되지…
해결! -> 처음에 그냥 두었다가 어느 순간부터 꼬여서 다시 차근차근 고쳐나갔다
1
2
3
4
5
6
7
spring.jpa.show_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
spring.datasource.hikari.jdbc-url=jdbc:h2:mem:testdb;MODE=MYSQL;
spring.h2.console.enabled=true
spring.session.store-type=jdbc // 추후
spring.profiles.include=oauth // 추후
3.4 등록/수정/조회 API 만들기
필요한 클래스
[ ] Request 데이터를 받을 Dto
[ ] API 요청을 받을 Controller
[ ] 트랜잭션, 도메인 기능 간의 순서를 보장하는 Service
❗서비스 메소드는 트랜잭션과 도메인 간의 순서만 보장
❗도메인 모델에서 비즈니스 처리를 담당
- PostsApiController
1
2
3
4
5
6
7
8
9
10
11
@RequiredArgsConstructor
@RestController
public class PostsApiController {
private final PostsService postsService;
@PostMapping("/api/v1/posts")
public Long save(@RequestBody PostsSaveRequestDto requestDto) {
return postsService.save(requestDto);
}
}
- PostsService
1
2
3
4
5
6
7
8
9
10
@RequiredArgsConstructor
@Service
public class PostsService {
private final PostsRepository postsRepository;
@Transactional
public Long save(PostsSaveRequestDto requestDto) {
return postsRepository.save(requestDto.toEntity()).getId();
}
}
- @RequiredArgsConstructor : final이 선언된 모든 필드를 인자값으로 하는 생성자 생성 → 생성자 주입으로 Bean을 주입
Controller와 Service에서 쓸 Dto 클래스를 생성하자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Getter
@NoArgsConstructor
@Entity
public class Posts {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // PK의 생성규칙을 나타냄
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;
}
}
Dto (위) Entity (아래) 비슷해 보인다…
❗Entity 클래스를 Request/Response 클래스로 사용하면 안 됨, 수많은 서비스 클래스나 비즈니스 로직들이 Entity 클래스를 기준으로 동작하는데 테이블과 연결된 클래스이기 때문에 변경 시에 너무 큰 영향
❗Request, Response 용 Dto는 View를 위한 클래스라 자주 변경
❗실제로 Controller에서 결과값으로 여러 테이블을 조인하는 경우가 많다 → Entity 클래스와 Controller에서 쓸 Dto는 분리해서 사용하자
✅ 등록 테스트 - PostsApiControllerTest
1
2
@SpringBootTest(webEnvironment = SpringBootTest.
WebEnvironment.RANDOM_PORT)
- @WebMvcTest의 경우 JPA 기능이 작동하지 X, @SpringBootTest와
TestRestTemplate
를 같이 쓰면 JPA 기능까지 한번에 테스트 가능 WebEnvironment.RANDOM_PORT
로 랜덤 포트 실행
전체코드
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
```java
@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);
}
}
```
등록은 잘 된다. 수정, 조회까지 만들어보자
h2 연결오류시
[spring boot] H2 Database “mem:testdb” not found 오류 해결법
BaseTimeEntity 등록 test 통과!
댓글남기기