[Spring] 3장 스프링 부트에서 JPA로 데이터베이스 다루기

Date:

카테고리:

3장 - 스프링 부트에서 JPA로 데이터베이스 다뤄보자

3.1 JPA 소개

  • JPA의 등장 배경 : 개발자는 객체 지향적으로 프로그래밍을 하고, JPA가 이를 관계형 데이터베이스에 맞게 SQL을 대신 생성해서 실행
  • SQL에 종속적인 개발을 하지 않아도 됨

Spring Data JPA

  • JPA는 인터페이스! 이를 사용하기 위해선 구현체 (Hibernate가 대표적) 필요 → 이들을 좀 더 쉽게 사용하고자 사용된 기술
    1. 구현체 교체의 용이성 : Hibernate 외에 다른 구현체로 쉽게 교체하기 위해
    2. 저장소 교체의 용이성 : 관계형 데이터베이스 외에 다른 저장소로 쉽게 교체하기 위해
    1. 트래픽이 많아져 MongoDB로 교체 시 Spring Data JPA에서 Spring Data MongoDB로 의존성만 교체하면 됨
    2. 왜? 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;
    }
}

[ 롬복 @ ]

  1. @Getter : 클래스 내 모든 필드의 Getter 메소드 자동생성
  2. @NoArgsConstructor : 기본 생성자 자동 추가
  3. @Builder : 해당 클래스의 빌더 패턴 클래스 생성, 이 생성자에 포함된 필드만 빌더에 포함

[ JPA @ ]

  1. @Entity
    ❗이 Post 클래스는 실제 DB의 테이블과 매칭, 보통 Entity 클래스라고 함
    ❗JPA를 사용하며 DB 데이터에 작업할 때 실제 쿼리를 날리기 보단 이 Entity 클래스의 수정으로 작업

  2. @Id : 해당 테이블의 PK 필드를 나타냄
  3. @GeneratedValue : PK의 생성규칙
  4. @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);

    }
}

  1. @SpringBootTest : H2 DB를 자동으로 실행해줌
  2. @After : 단위 테스트가 끝날 때마다 실행되며 보통 배포 전 테스트 간 데이터 침범을 막기 위해
  3. postsRepository.findAll : posts 테이블에 insert/update 쿼리 실행, id값이 있다면 update, 있다면 insert 실행


  • 실제 실행된 쿼리가 어떤 형태인지 알아보려면?
1
2
// resources/application.properties 파일
spring.jpa.show-sql=true

스크린샷 2022-11-18 오후 12 43 41

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

❗서비스 메소드는 트랜잭션과 도메인 간의 순서만 보장
❗도메인 모델에서 비즈니스 처리를 담당

  1. 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);
    }
}
  1. 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, @SpringBootTestTestRestTemplate를 같이 쓰면 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 통과!

스크린샷 2023-01-02 오후 2 20 30

Date:

댓글남기기