๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
Spring Boot/์Šคํ”„๋ง ๋ถ€ํŠธ์™€ AWS๋กœ ํ˜ผ์ž ๊ตฌํ˜„ํ•˜๋Š” ์›น ์„œ๋น„์Šค

3์žฅ: JPA๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋‹ค๋ฃจ๊ธฐ (2)

by oliviarla 2022. 6. 15.

Table of Contents

     

     

    API ๋งŒ๋“ค๊ธฐ

    API๋ฅผ ๋งŒ๋“ค์–ด๋ณด๋ฉฐ ๊ตฌ์กฐ ์ดํ•ดํ•˜๊ธฐ

    ํ•„์š”ํ•œ ํด๋ž˜์Šค

    - Request ๋ฐ์ดํ„ฐ ๋ฐ›์„ Dto

    - API ์š”์ฒญ ๋ฐ›์„ Controller

    - ํŠธ๋žœ์žญ์…˜, ๋„๋ฉ”์ธ ๊ธฐ๋Šฅ ๊ฐ„ ์ˆœ์„œ๋ฅผ ๋ณด์žฅํ•˜๋Š” Service (๋น„์ง€๋‹ˆ์Šค ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜์ง€๋Š” ์•Š์Œ)

    Spring ์›น ๊ณ„์ธต

    Web Layer

    - ์ปจํŠธ๋กค๋Ÿฌ์™€ JSP๋“ฑ์˜ ๋ทฐ ํ…œํ”Œ๋ฆฟ ์˜์—ญ

    - Filter, ์ธํ„ฐ์…‰ํŠธ, ControllerAdvice ๋“ฑ ์™ธ๋ถ€ ์š”์ฒญ๊ณผ ์‘๋‹ต์— ๋Œ€ํ•œ ์ „๋ฐ˜์ ์ธ ์˜์—ญ

     

    Service Layer

    - @Service์— ์‚ฌ์šฉ๋˜๋Š” ์˜์—ญ

    - Controller์™€ Dao ์ค‘๊ฐ„์—์„œ ์‚ฌ์šฉ๋จ

    - @Transactional์ด ์‚ฌ์šฉ๋˜์–ด์•ผ ํ•จ

     

    Repository Layer

    - ๋ฐ์ดํ„ฐ ์ €์žฅ์†Œ์— ์ ‘๊ทผํ•˜๋Š” ์˜์—ญ

     

    DTOs

    - Data Transfer Object

    - ๊ณ„์ธต ๊ฐ„ ๋ฐ์ดํ„ฐ ๊ตํ™˜์„ ์œ„ํ•œ ๊ฐ์ฒด DTO์˜ ์˜์—ญ

    - ๋ทฐ ํ…œํ”Œ๋ฆฟ ์—”์ง„์—์„œ ์‚ฌ์šฉ๋˜๊ฑฐ๋‚˜ Repository Later์—์„œ ๊ฒฐ๊ณผ๋กœ ๋„˜์–ด์˜จ ๊ฐ์ฒด๋“ค์ด ํ•ด๋‹น๋จ

     

    Domain Model

    - @Entity๊ฐ€ ์‚ฌ์šฉ๋œ ์˜์—ญ

    - VO(๊ฐ’ ๊ฐ์ฒด)๋„ ์ด ์˜์—ญ์— ํ•ด๋‹น๋˜๋ฏ€๋กœ, ๋ฌด์กฐ๊ฑด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ํ…Œ์ด๋ธ”๊ณผ ๊ด€๊ณ„์žˆ์ง€๋Š” ์•Š์Œ

     

    ๐Ÿ’ก ์„œ๋น„์Šค์™€ ๋„๋ฉ”์ธ์˜ ์—ญํ•  ๋ถ„๋‹ด์€ ์–ด๋–ป๊ฒŒ ํ•˜๋Š”๊ฐ€?

    ์„œ๋น„์Šค ํด๋ž˜์Šค ๋‚ด๋ถ€์—์„œ ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค๋Š”

    ๋„๋ฉ”์ธ ๋ชจ๋ธ ๋‚ด์—์„œ ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๊ณ  ์„œ๋น„์Šค ๋ฉ”์†Œ๋“œ๋Š” ํŠธ๋žœ์žญ์…˜๊ณผ ๋„๋ฉ”์ธ ๊ฐ„์˜ ์ˆœ์„œ๋งŒ ๋ณด์žฅํ•˜๋„๋ก ๊ตฌ์„ฑ

     

    - ์„œ๋น„์Šค ๋ฉ”์†Œ๋“œ ์˜ˆ์ œ

    @Transactional
    public Order cancelOrder(int orderId){
        Orders order = ordersRepository.findById(orderId);
        Billing billing = billingRepository.findById(orderId);
        Delivery delivery = deliveryRepository.findById(orderId);
    
        delivery.cancel();
        order.cancel();
        billing.cancel();
    
        return order;
    }

    ๊ฐ ๋„๋ฉ”์ธ ๋ชจ๋ธ์ด ์ทจ์†Œ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๊ณ  ์„œ๋น„์Šค ๋ฉ”์†Œ๋“œ๋Š” ํŠธ๋žœ์žญ์…˜๊ณผ ๋„๋ฉ”์ธ ๊ฐ„์˜ ์ˆœ์„œ๋งŒ ๋ณด์žฅ

     

    Bean์„ ์ฃผ์ž…๋ฐ›๋Š” ๋ฐฉ์‹

    1. ์ƒ์„ฑ์ž

    • @Autowired์™€ ๋™์ผํ•œ ํšจ๊ณผ๋ฅผ ๊ฐ€์ง
    • @RequiredArgsConstructor์—์„œ final์ด ์„ ์–ธ๋œ ๋ชจ๋“  ํ•„๋“œ๋ฅผ ์ธ์ž๊ฐ’์œผ๋กœ ํ•˜๋Š” ์ƒ์„ฑ์ž๋ฅผ ์ƒ์„ฑํ•ด์ฃผ๋ฏ€๋กœ, ํด๋ž˜์Šค์˜ ์˜์กด์„ฑ ๊ด€๊ณ„๊ฐ€ ๋ณ€๊ฒฝ๋˜์–ด๋„ ์ง์ ‘ ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•˜์ง€ ์•Š์•„๋„ ๋จ
    • ๊ฐ€์žฅ ๊ถŒ์žฅํ•˜๋Š” ๋ฐฉ์‹์ž„

    2. setter

    3. @Autowired

    ํด๋ž˜์Šค ์ƒ์„ฑํ•ด์„œ CRUD ๊ตฌํ˜„ํ•˜๊ธฐ

    Controller ํด๋ž˜์Šค ๊ตฌํ˜„

    @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);
        }
    
        @DeleteMapping("/api/v1/posts/{id}")
        public Long delete(@PathVariable Long id){
            postsService.delete(id);
            return id;
        }
    }

    Service ํด๋ž˜์Šค ๊ตฌํ˜„

    - update ์„œ๋น„์Šค ๋ฉ”์†Œ๋“œ

    • id์™€ ์ผ์น˜ํ•˜๋Š” Posts ๊ฐ์ฒด๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ ํ›„ ๋ถˆ๋Ÿฌ์˜ด
    • PostsUpdateRequestDto์— title๊ณผ content๋ฅผ ๋‹ด์•„์˜จ ํ›„ ๋„๋ฉ”์ธ ๊ฐ์ฒด(Posts)์˜ update ๋ฉ”์†Œ๋“œ์— PostsUpdateRequestDto ์ „๋‹ฌ
    @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);
        }
    
        @Transactional(readOnly = true)
        public List<PostsListResponseDto> findAllDesc(){
            return postsRepository.findAllDesc().stream()
                    .map(PostsListResponseDto::new)
                    .collect(Collectors.toList());
        }
    
        @Transactional
        public void delete(Long id){
            Posts posts=postsRepository.findById(id)
                    .orElseThrow(()->new IllegalArgumentException("ํ•ด๋‹น ๊ฒŒ์‹œ๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค. id= "+id));
            postsRepository.delete(posts);
        }
    
    }

    ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ

    - ์—”ํ‹ฐํ‹ฐ๋ฅผ ์˜๊ตฌ ์ €์žฅํ•˜๋Š” ํ™˜๊ฒฝ

    - Spring Data JPA๋ฅผ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ, ํŠธ๋žœ์žญ์…˜ ์•ˆ์—์„œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋ฉด ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋Š” ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ๊ฐ€ ์œ ์ง€๋œ ์ƒํƒœ์ž„

    - ๋ฐ์ดํ„ฐ ๊ฐ’์„ ๋ณ€๊ฒฝ ์‹œ ํŠธ๋žœ์žญ์…˜์ด ๋๋‚˜๋Š” ์‹œ์ ์— ํ•ด๋‹น ํ…Œ์ด๋ธ”์— ๋ณ€๊ฒฝ๋ถ„์„ ๋ฐ˜์˜ํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ฟผ๋ฆฌ๋ฅผ ๋‚ ๋ฆด ํ•„์š”๊ฐ€ ์—†์Œ(Dirty Checking)

     

    DTO ํด๋ž˜์Šค ๊ตฌํ˜„

    DTO ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ 

    Entity ํด๋ž˜์Šค๋ฅผ ๊ธฐ์ค€์œผ๋กœ ํ…Œ์ด๋ธ” ์ƒ์„ฑ, ์Šคํ‚ค๋งˆ ๋ณ€๊ฒฝ์ด ์ˆ˜ํ–‰๋จ

    ํ•˜์ง€๋งŒ Request/Response์šฉ DTO๋Š” View๋ฅผ ์œ„ํ•œ ํด๋ž˜์Šค์ด๋ฏ€๋กœ ์ž์ฃผ ๋ณ€๊ฒฝ๋˜์–ด์•ผ ํ•จ 

    ๋”ฐ๋ผ์„œ Entity ํด๋ž˜์Šค๋ฅผ ์ ˆ๋Œ€ Request/Response ํด๋ž˜์Šค๋กœ ์‚ฌ์šฉํ•˜๋ฉด ์•ˆ๋จ

    ์‹ค์ œ๋กœ Controller์—์„œ ๊ฒฐ๊ด๊ฐ’์œผ๋กœ ์—ฌ๋Ÿฌ ํ…Œ์ด๋ธ”์„ ์กฐ์ธํ•ด ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Œ

    View Layer์˜ DTOํด๋ž˜์Šค์™€ DB Layer์˜ Entity ํด๋ž˜์Šค๋กœ ๋‚˜๋ˆ„์–ด ์ฒ ์ €ํ•œ ์—ญํ•  ๋ถ„๋ฆฌ ํ•„์š”

     

    - PostsSaveRequestDto

    title, content, author๋ฅผ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ž…๋ ฅ ๋ฐ›์•„ ์—”ํ‹ฐํ‹ฐ๋กœ ๋งŒ๋“ฆ

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

     

    - PostsResponseDto

    ๋ชจ๋“  ํ•„๋“œ๋ฅผ ๊ฐ€์ง„ ์ƒ์„ฑ์ž๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์ƒ์„ฑ์ž๋กœ Entity๋ฅผ ๋ฐ›์•„ ํ•„๋“œ์— ๊ฐ’ ์ž…๋ ฅ

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

     

    ํ…Œ์ŠคํŠธ ๋ฉ”์†Œ๋“œ ๊ตฌํ˜„

    @Test
    public void edit_Posts() throws Exception{
        //given
        Posts savedPosts=postsRepository.save(Posts.builder()
                .title("title")
                .content("content")
                .author("me")
                .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);
    }

     

    JPA Auditing์œผ๋กœ ์ƒ์„ฑ์‹œ๊ฐ„/์ˆ˜์ •์‹œ๊ฐ„ ์ž๋™ํ™”

    ๋ฐ์ดํ„ฐ์˜ ์ƒ์„ฑ ์‹œ๊ฐ„๊ณผ ์ˆ˜์ • ์‹œ๊ฐ„์„ ๋ชจ๋“  ํ…Œ์ด๋ธ”๊ณผ ์„œ๋น„์Šค ๋ฉ”์†Œ๋“œ์— ํฌํ•จํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋Š” ๋ฐฉ๋ฒ•

     

    BaseTimeEntity ํด๋ž˜์Šค ๊ตฌํ˜„

    @Getter
    @MappedSuperclass
    @EntityListeners(AuditingEntityListener.class)
    public abstract class BaseTimeEntity {
    
        @CreatedDate
        private LocalDateTime createdDate;
    
        @LastModifiedDate
        private LocalDateTime modifiedDate;
    }

    ๋ชจ๋“  Entity์˜ ์ƒ์œ„ ํด๋ž˜์Šค๊ฐ€ ๋˜์–ด Entity๋“ค์˜ createdDate, modifiedDate๋ฅผ ์ž๋™์œผ๋กœ ๊ด€๋ฆฌ

    @MappedSuperclass: JPA Entity ํด๋ž˜์Šค๋“ค์ด ํ•ด๋‹น ํด๋ž˜์Šค๋ฅผ ์ƒ์†ํ•  ๊ฒฝ์šฐ BaseTimeEntity์˜ ํ•„๋“œ๋“ค๋„ ์นผ๋Ÿผ์œผ๋กœ ์ธ์‹ํ•˜๋„๋ก ํ•จ

    Entity ํด๋ž˜์Šค์— ์ƒ์† ํ‘œ์‹œ

    public class Posts extends BaseTimeEntity {
    ...
    }

    Application ํด๋ž˜์Šค์— ํ™œ์„ฑํ™” ์–ด๋…ธํ…Œ์ด์…˜ ์ถ”๊ฐ€

    @EnableJpaAuditing
    @SpringBootApplication
    public class Application {
    ...
    }

     

    ๋Œ“๊ธ€