본문으로 바로가기
728x90

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);
    }
 
 

 

테스트 결과

 

 

위와 같이 실제 시간이 잘 저장된 것을 확인할 수 있습니다.

 

728x90