🚧

Spring Data JPA 둜 등둝/쑰회/μˆ˜μ • API λ§Œλ“€κΈ°

purpplee 2021. 12. 29. 17:03

등둝/쑰회/μˆ˜μ • API λ§Œλ“€κΈ°

각 μ½”λ“œ 상세 μ„€λͺ…은 μ£Όμ„μœΌλ‘œ λ‹¬μ•˜λ‹€. 또 Entity ν΄λž˜μŠ€μ™€ Repository ν΄λž˜μŠ€λŠ” πŸ‘‰  Spring boot 에 Spring Data JPA μ μš©ν•˜κΈ° μ—μ„œ μž‘μ„±ν–ˆλ‹€.

 

DTO λ§Œλ“€κΈ°

DTOλŠ” 데이터λ₯Ό View Layer 와 주고받을 λ•Œ(request, response) κ°μ‹ΈλŠ” 객체이닀. 외뢀에 λ…ΈμΆœλ˜λŠ” κ°μ²΄μ΄λ―€λ‘œ μ•ˆμ— λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ 담지 μ•ŠλŠ”λ‹€. μ—¬κΈ°μ„œλŠ” Entity 와 μœ μ‚¬ν•˜λ‚˜, μ΄λ ‡κ²Œ View 와 μ£Όκ³ λ°›λŠ” 데이터듀은 자주 μˆ˜μ •μ΄ μΌμ–΄λ‚œλ‹€. Entity λŠ” DB와 λ°€μ ‘ν•œ 객체고 λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§λ„ λ‹΄κ³  μžˆμœΌλ―€λ‘œ 자주 μˆ˜μ •ν•˜κ²Œ 되면 κ΄€λ ¨λœ μ—¬λŸ¬ ν΄λž˜μŠ€λ“€μ— 영ν–₯을 끼치기 λ•Œλ¬Έμ— Entity λ₯Ό dto μš©λ„λ‘œ 쓰지 μ•ŠλŠ”λ‹€. 즉, Entity ν΄λž˜μŠ€μ™€ Controller μ—μ„œ μ“Έ DTO λŠ” 뢄리해야 ν•œλ‹€.

 

src/main/java/com.study.springbootaws/web/dto/PostsResponseDto

package com.study.springbootaws.web.dto;

import com.study.springbootaws.domain.posts.Posts;
import lombok.Getter;

@Getter
public class PostsResponseDto {
    private Long id;
    private String title;
    private String content;
    private String author;

	//repository λ₯Ό 톡해 μ‘°νšŒν•œ entity λ₯Ό dto 둜 λ³€ν™˜ μš©λ„ 
    public PostsResponseDto(Posts entity) {
        this.id = entity.getId();
        this.title = entity.getTitle();
        this.content = entity.getContent();
        this.author = entity.getAuthor();
    }
}

 

src/main/java/com.study.springbootaws/web/dto/PostsSaveRequestDto

package com.study.springbootaws.web.dto;

import com.study.springbootaws.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;
    }

	//resquest dto 둜 받은 Posts 객체λ₯Ό entity ν™”ν•˜μ—¬ μ €μž₯ν•˜λŠ” μš©λ„
    public Posts toEntity() {
        return Posts.builder().title(title).content(content).author(author).build();
    }
}

 

src/main/java/com.study.springbootaws/web/dto/PostsUpdateRequestDto

package com.study.springbootaws.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;
    }
}

 

Entity μ½”λ“œ μˆ˜μ •

Spring Data Jpa λŠ” 기본적으둜 JPA μ˜μ†μ„± μ»¨ν…μŠ€νŠΈ μœ μ§€λ₯Ό μ œκ³΅ν•œλ‹€. 이 μƒνƒœμ—μ„œ ν•΄λ‹Ή λ°μ΄ν„°μ˜ 값을 λ³€κ²½ν•˜λ©΄ μžλ™μœΌλ‘œ 변경사항이 DB에 λ°˜μ˜ν•œλ‹€. 즉, λ³„λ„λ‘œ Update 쿼리λ₯Ό 날리지 μ•Šμ•„λ„, λ°μ΄ν„°λ§Œ λ³€κ²½ν•˜λ©΄ μ•Œμ•„μ„œ λ³€κ²½λ˜λ―€λ‘œ μˆ˜μ •κΈ°λŠ₯은 μ•„λž˜μ²˜λŸΌ Entity 에 update λ©”μ†Œλ“œλ₯Ό λ§Œλ“€μ–΄μ€˜ κ΅¬ν˜„ν•œλ‹€. 

src/main/java/com.study.springbootaws/domain/posts/Posts

...

public class Posts {

...

    public void update(String title, String content) {
        this.title = title;
        this.content = content;
    }
}

 

Service λ§Œλ“€κΈ°

보톡은 Service 에 λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ κ΅¬ν˜„ν•œλ‹€. 그것을 νŠΈλžœμž­μ…˜ 슀크립트라고 ν•˜λŠ”λ°, κ·ΈλŸ¬λ‹€λ³΄λ‹ˆ μ„œλΉ„μŠ€ 계측이 λ¬΄μ˜λ―Έν•΄μ§„λ‹€. μ„œλΉ„μŠ€ κ³„μΈ΅μ—μ„œλŠ” νŠΈλžœμž­μ…˜κ³Ό 도메인 κ°„ μˆœμ„œλ§Œμ„ 보μž₯ν•΄μ€˜μ•Ό ν•œλ‹€. λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ€ 도메인 μ—μ„œ μ²˜λ¦¬ν•œλ‹€.

src/main/java/com.study.springbootaws/service/PostsService

package com.study.springbootaws.service;

import com.study.springbootaws.domain.posts.Posts;
import com.study.springbootaws.domain.posts.PostsRepository;
import com.study.springbootaws.web.dto.PostsResponseDto;
import com.study.springbootaws.web.dto.PostsSaveRequestDto;
import com.study.springbootaws.web.dto.PostsUpdateRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;

@RequiredArgsConstructor    //final or NonNull μ˜΅μ…˜ ν•„λ“œλ₯Ό μ „λΆ€ ν¬ν•¨ν•œ μƒμ„±μž λ§Œλ“€μ–΄μ€Œ
@Service
public class PostsService {
    //@Autowired μ‚¬μš© 지양됨 -> @RequiredArgsConstructor 둜 μƒμ„±λ˜λŠ” μƒμ„±μžλ‘œ μ£Όμž…λ°›κΈ° μœ„ν•΄ final λΆ™μž„.
    private final PostsRepository postsRepository;

    @Transactional  //db νŠΈλžœμž­μ…˜ μžλ™μœΌλ‘œ commit ν•΄μ€Œ
    public Long save(PostsSaveRequestDto requestDto) {
        //dto λ₯Ό entity ν™” ν•΄μ„œ repository 의 save λ©”μ†Œλ“œλ₯Ό 톡해 db 에 μ €μž₯.
        //μ €μž₯ ν›„ μƒμ„±ν•œ id λ°˜ν™˜ν•΄μ€Œ
        return postsRepository.save(requestDto.toEntity()).getId();
    }

    @Transactional
    public Long update(Long id, PostsUpdateRequestDto requestDto) {
        Posts posts = postsRepository.findById(id).orElseThrow(()-> new IllegalArgumentException("ν•΄λ‹Ή κ²Œμ‹œκΈ€μ΄ μ—†μŠ΅λ‹ˆλ‹€. id="+id));

        //JPA 의 μ˜μ†μ„± μ»¨ν…μŠ€νŠΈ 덕뢄에 entity 객체의 κ°’λ§Œ λ³€κ²½ν•˜λ©΄ μžλ™μœΌλ‘œ 변경사항 λ°˜μ˜ν•¨!
        //λ”°λΌμ„œ repository.update λ₯Ό 쓰지 μ•Šμ•„λ„ 됨.
        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);
    }
}

 

Controller λ§Œλ“€κΈ°

src/main/java/com.study.springbootaws/web/PostsApiController

package com.study.springbootaws.web;

import com.study.springbootaws.service.PostsService;
import com.study.springbootaws.web.dto.PostsResponseDto;
import com.study.springbootaws.web.dto.PostsSaveRequestDto;
import com.study.springbootaws.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);
    }
}

 

등둝/μˆ˜μ • api ν…ŒμŠ€νŠΈν•˜κΈ°

src/test/java/com.study.springbootaws/web/PostsApiControllerTest

이전 컨트둀러 ν…ŒμŠ€νŠΈμ™€ 달리, Jpa λ₯Ό μ‚¬μš©ν•˜κΈ° λ•Œλ¬Έμ— @SpringbootTest μ™€ TestRestTemplate μ„ μ‚¬μš©ν•œλ‹€.

package com.study.springbootaws.web;

import com.study.springbootaws.domain.posts.Posts;
import com.study.springbootaws.domain.posts.PostsRepository;
import com.study.springbootaws.web.dto.PostsSaveRequestDto;
import com.study.springbootaws.web.dto.PostsUpdateRequestDto;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
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.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.web.WebAppConfiguration;

import java.util.List;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

//webmvctest λŠ” jpaκ°€ λ™μž‘ν•˜μ§€ μ•ŠμŒ.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PostsApiControllerTest {
    @LocalServerPort
    private int port;

    //mockmvc 와 resttemplate λ‘˜ λ‹€ 컨트둀러 ν…ŒμŠ€νŠΈ λΌμ΄λΈŒλŸ¬λ¦¬μž„. testresttemplate 은 μ»¨ν…Œμ΄λ„ˆλ₯Ό 직접 μ‹€ν–‰μ‹œν‚€λ―€λ‘œ ν΄λΌμ΄μ–ΈνŠΈ μž…μž₯μ—μ„œ ν…ŒμŠ€νŠΈν•  λ•Œ μ‚¬μš©ν•¨.
    //mockmvc λŠ” μ»¨ν…Œμ΄λ„ˆ 직접 μ‹€ν–‰ μ•ˆν•˜λ―€λ‘œ λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ— 문제 μ—†λŠ”μ§€λ₯Ό 확인함.
    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private PostsRepository postsRepository;

    @AfterEach
    public void tearDown() throws Exception {
        postsRepository.deleteAll();
    }

    @Test
    public void insertPosts() throws Exception {
        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";

        ResponseEntity<Long> responseEntity = restTemplate.postForEntity(url, requestDto, Long.class);

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

    @Test
    public void updatePosts() throws Exception {
        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);

        ResponseEntity<Long> responseEntity = restTemplate.exchange(url, HttpMethod.PUT, requestEntity, Long.class);

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

 

save ν…ŒμŠ€νŠΈ

 

update ν…ŒμŠ€νŠΈ

 

쑰회 ν…ŒμŠ€νŠΈ

쑰회 ν…ŒμŠ€νŠΈλŠ” 직접 ν†°μΊ£ μ„œλ²„λ₯Ό μ‹€ν–‰ν•΄μ„œ 확인할 것이닀. H2 DBλ₯Ό μ“°κΈ° λ•Œλ¬Έμ— μ›Ή μ½˜μ†”μ—μ„œ 직접 μ ‘κ·Όν•΄μ•Ό ν•œλ‹€.

src/main/resources/application.properties

μ›Ή μ½˜μ†” μ˜΅μ…˜μ„ ν™œμ„±ν™”ν•œλ‹€.

...
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.datasource.url=jdbc:h2:mem:testdb

 

Application 클래슀의 main λ©”μ†Œλ“œλ₯Ό μ‹€ν–‰ν•˜κ³  http://localhost:8080/h2-console 에 μ ‘μ†ν•œλ‹€. μ•„λž˜μ²˜λŸΌ μˆ˜μ •ν•΄μ£Όκ³  connect λ₯Ό ν΄λ¦­ν•œλ‹€. JDBC URL 은 이전 mysql 둜 쿼리 λ³€ν™˜ properties λ³€κ²½ μ‹œ μ•„λž˜μ²˜λŸΌ μž…λ ₯ν–ˆλ˜ url 을 따라 적으면 λœλ‹€.

...
spring.datasource.hikari.jdbc-url=jdbc:h2:mem://localhost/~/testdb;MODE=MYSQL

 

connect λ₯Ό λˆ„λ₯΄λ©΄ μ•„λž˜μ²˜λŸΌ λœ¬λ‹€. posts κ°€ μƒμ„±λ˜μ–΄μžˆλŠ”μ§€ ν™•μΈν•˜κ³  insert into posts (author, content, title) values ('author', 'content', 'title'); 문을 run μ‹œν‚¨λ‹€.

 

localhost:8080/api/v1/posts/1 둜 λ“€μ–΄κ°€ μ‘°νšŒν•΄μ„œ λ‚˜μ˜€λ©΄ 성곡!!

 

JPA Auditing 으둜 생성/μˆ˜μ •μ‹œκ°„ μžλ™μœΌλ‘œ λ„£κΈ°

생성/μˆ˜μ • μ‹œκ°„μ€ μ°¨ν›„ μœ μ§€λ³΄μˆ˜μ— ꡉμž₯히 μ€‘μš”ν•œ 정보닀. DB 여기저기에 λ“€μ–΄κ°€κ²Œ λ˜λŠ”λ°, λͺ¨λ“  Entity 에 ν•˜λ‚˜ν•˜λ‚˜ λ„£μ–΄μ£ΌλŠ” μž‘μ—…μ„ JPA Auditing 으둜 ν•΄κ²°ν•  수 μžˆλ‹€.

 

src/main/java/com.study.springbootaws/domain/BaseTimeEntity

package com.study.springbootaws.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   //λΆ€λͺ¨ 클래슀(BaseTimeEntity) 의 ν•„λ“œλ“€λ„ μ „λΆ€ 컬럼으둜 μΈμ‹ν•˜κ²Œ 함.
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
    @CreatedDate    //Entity κ°€ 생성될 λ•Œ μžλ™μœΌλ‘œ 생성/μˆ˜μ •μ‹œκ°„ μ‚½μž…
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime modifiedDate;
}

 

src/main/java/com.study.springbootaws/domain/posts/Post

post Entity ν΄λž˜μŠ€μ— BaseTimeEntity 클래슀λ₯Ό μƒμ†μ‹œμΌœμ€€λ‹€.

...
public class Posts extends BaseTimeEntity { 
...

 

src/main/java/com.study.springbootaws/Application

Application μ½”λ“œμ— jpa auditing을 ν™œμ„±ν™”ν•˜λŠ” μ–΄λ…Έν…Œμ΄μ…˜μ„ λΆ™μ—¬μ€€λ‹€.

...
@EnableJpaAuditing  //jpa auditing ν™œμ„±ν™”
public class Application {
...

 

ν…ŒμŠ€νŠΈν•˜κΈ°

src/test/java/com.study.springbootaws/domain/posts/PostsRepositoryTest

PostsRepositoryTest 에 BaseTimeEntityTest μ½”λ“œλ₯Ό μΆ”κ°€ν•΄μ£Όκ³  μ‹€ν–‰ν•˜λ©΄ λœλ‹€.

...

@SpringBootTest
public class PostRepositoryTest {
 
 	...

    @Test
    public void insertBaseTileEntity() {
        LocalDateTime now = LocalDateTime.now();
        postsRepository.save(Posts.builder().title("title").content("content").author("author").build());

        List<Posts> postsList = postsRepository.findAll();
        Posts posts = postsList.get(0);

        System.out.println(">>>>>>>>> createDate = " + posts.getCreatedDate() + ", modifiedDate = " + posts.getModifiedDate());

        assertThat(posts.getCreatedDate()).isAfter(now);
        assertThat(posts.getModifiedDate()).isAfter(now);
    }
}

 

성곡!!

λ°˜μ‘ν˜•