Book Study/스프링 부트 핵심 가이드

06. 데이터베이스 연동 (6)

정진킴 2023. 11. 3. 03:57

6.8 리포지토리 인터페이스 설계

  • Spring Data JPA 는 JpaRepository를 기반으로 더욱 쉽게 데이터베이스를 사용할 수 있는 아케텍처를 제공한다.

6.8.1 리포지토리 인터페이스 설정

  • 엔티티가 생성한 데이터베이스에 접근하는데 사용
  • 리포지토리를 생성하기 위해서는 접근하려는 테이블과 매핑되는 엔티티에 대한 인터페이스를 생성하고 JpaRepository를 상속받으면 된다.
package com.springboot.jpa.data.repository;

import com.springboot.jpa.data.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductRepository extends JpaRepository<Product, Long> {
}
  • JpaRepository에서 기본 메서드는 제공된다.
    • findAll(), findById() 등

6.8.2 리포지토리 메서드의 생성 규칙

  • 몇 가지 명명규칙에 따라 커스텀 메서드도 생성 가능하다.
  • 메서드에 이름을 붙일 때는 첫 단어를 제외한 이후 단어들의 첫 글자를 대문자로 설정해야 JPA에서 정상적으로 인식하고 쿼리를 자동으로 생성한다.
    • 조회 메서드
      • findBy : SQL문의 where 절 역할을 수행하는 구문, findBy 뒤에 엔티티의 필드값을 입력하여 사용
        • ex) findByName(String name)
        • SELECT * FROM product WHERE name = ?;
      • AND, OR 조건을 여러 개 설정하기 위해 사용
        • ex) findByNameAndEmail(String name, String email)
        • SELECT * FROM product WHERE name = ? and email = ?;
      • Like/NotLike: SQL문의 like와 동일한 기능을 수행하며, 특정 문자를 포함하는지 여부를 조건으로 추가한다.
        비슷한 키워드로 Containing, Contains, isContaing 이 있다.
      • StartsWith/StartingWith: 특정 키워드로 시작하는 문자열 조건을 설정
      • EndsWith/EndingWith: 특정 키워드로 끝나는 문자열 조건을 설정
      • isNull/isNotNull: 레코드 값이 Null이거나 Null이 아닌 값을 검색
      • True/False: Boolean 타입의 레코드를 검색할 때 사용
      • Before/After: 시간을 기준으로 값을 검색
      • LessThan/GreaterThan: 특정 값(숫자)을 기준으로 대소 비교를 할 때 사용
      • Between: 두 값(숫자) 사이의 데이터를 조회
      • OrderBy: SQL 문에서 orderBy 동일한 기능
        • ex) List<Product> findByNameOrderByPriceAsc(String name);
        • SELECT * FROM product WHERE name = ? ORDER BY price ASC;

6.9 DAO 설계

  • Data Access Object 의 약자로 데이터베이스에 접근하기 위한 로직을 관리하는 객체
  • 비지니스 로직의 동작 과정에서 데이터를 조작하는 기능은 DAO 객체가 수행
  • Spring Data JPA에서 DAO의 개념은 리포지토리가 대체

6.9.1 DAO 클래스 생성

  • DAO 클래스는 일반적으로 '인터페이스-구현체' 구성으로 생성한다.
  • DAO 클래스는 의존성 결합을 낮추기 위한 디자인 패턴이며, 서비스 레이어에 DAO 객체를 주입받을 때 인터페이스를 선언하는 방식으로 구성할 수 있다.
package com.springboot.jpa.dao;

import com.springboot.jpa.data.entity.Product;

public interface ProductDAO {
    Product insertProduct(Product product);

    Product selectProduct(Long number);

    Product updateProductName(Long number, String name) throws Exception;

    void deleteProduct(Long number) throws Exception;
}
  • 일반적인 설계 원칙에서 엔티티 객체는 데이터베이스에 접근하는 계층에서만 사용하도록 정의
  • 다른 계층으로 데이터를 전달할 때는 DTO 객체를 사용
package com.springboot.jpa.dao.impl;

import com.springboot.jpa.dao.ProductDAO;
import com.springboot.jpa.data.entity.Product;
import com.springboot.jpa.data.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;

@Component
public class ProductDAOImpl implements ProductDAO {
    private final ProductRepository productRepository;

    @Autowired
    public ProductDAOImpl(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Transactional
    @Override
    public Product insertProduct(Product product) {
        return productRepository.save(product);
    }

    @Transactional(readOnly = true)
    @Override
    public Product selectProduct(Long number) {
        return productRepository.findById(number)
            .orElseThrow(() -> new RuntimeException("select is error"));
    }

    @Transactional
    @Override
    public Product updateProductName(Long number, String name) throws Exception {
        Product selectProduct = productRepository.findById(number)
            .orElseThrow(Exception::new);

        selectProduct.setName(name);
        selectProduct.setUpdatedAt(LocalDateTime.now());

        return productRepository.save(selectProduct);
    }

    @Transactional
    @Override
    public void deleteProduct(Long number) throws Exception {
        Product selectProduct = productRepository.findById(number)
            .orElseThrow(Exception::new);

        productRepository.delete(selectProduct);
    }
}
  •  *Impl 클래스를 스프링이 관리하는 Bean 으로 등록할려면 @Component 또는 @Service 어노테이션을 지정해야 한다.
  • Bean 으로 등록된 객체는 다른 클래스가 인터페이스를 가지고 의존성을 주입받을 때 이 구현체를 찾아 주입하게 된다.

6.10 DAO 연동을 위한 컨트롤러와 서비스 설계

6.10.1 서비스 클래스 만들기

  • 서비스 레이어에서는 도메인 모델을 활용해 애플리케이션에서 제공하는 핵심 기능을 제공
  • 핵심 기능을 구현하려면 세부 기능의 정의해야 하는데 모든 로직을 서비스 레이어에서 포함하기는 쉽지 않기 때문에 한계를 극복하기 위해 도메인을 활용한 기능들은 비지니스 레이어 로직에서 구현하고 서비스 레이어에서는 기능들을 종합해서 핵심 기능을 전달하도록 구성하는 경우가 대표적이다.
package com.springboot.jpa.data.dto;

import lombok.*;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ProductDto {
    private String name;
    private int price;
    private int stock;
}
package com.springboot.jpa.data.dto;

import lombok.*;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class ProductResponseDto {
    private long number;
    private String name;
    private int price;
    private int stock;
}
  • @Builder: 빌더 패턴을 따르는 어노테이션이다.
    • 데이터 클래스를 사용할 때 생성자로 초기화할 경우 모든 필드에 값을 넣거나 null을 명시적으로 사용해야 한다.
    • 이러한 단점을 보완하기 위해 나온 패턴이며, 이 패턴을 이용하면 필요한 데이터만 설정할 수 있다.
package com.springboot.jpa.service;

import com.springboot.jpa.data.dto.ProductDto;
import com.springboot.jpa.data.dto.ProductResponseDto;

public interface ProductService {
    ProductResponseDto getProduct(Long number);
    ProductResponseDto saveProduct(ProductDto productDto);
    ProductResponseDto changeProductName(Long number, String name) throws Exception;
    void deleteProduct(Long number) throws Exception;
}
  • 위 인터페이스는 DAO에서 구현한 기능을 서비스 인터페이스에서 호출해 결과값을 가져오는 작업을 수행하도록 설계
  • 서비스에서는 클라이언트가 요청한 데이터를 적절하게 가공해서 컨트롤러에게 넘기는 역할
  • 서비스 레이어에서 리턴 타입이 DTO 객체와 엔티티 객체를 각 레이어에 반환해서 전달하는 역할 수행

▶ 서비스 인터페이스 구현체 클래스

package com.springboot.jpa.service.impl;

import com.springboot.jpa.data.dao.ProductDAO;
import com.springboot.jpa.data.dto.ProductDto;
import com.springboot.jpa.data.dto.ProductResponseDto;
import com.springboot.jpa.data.entity.Product;
import com.springboot.jpa.service.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;

@Service
@RequiredArgsConstructor
public class ProductServiceImpl implements ProductService {
    private final ProductDAO productDAO;

    @Override
    public ProductResponseDto getProduct(Long number) {
        Product product = productDAO.selectProduct(number);
        return product.fromEntity(product);
    }

    @Override
    public ProductResponseDto saveProduct(ProductDto productDto) {
        Product product = Product.toEntity(productDto);
        product.setCreateAt(LocalDateTime.now());
        product.setUpdatedAt(LocalDateTime.now());

        Product saveProduct = productDAO.insertProduct(product);

        return product.fromEntity(saveProduct);
    }

    @Override
    public ProductResponseDto changeProductName(Long number, String name)
        throws Exception {
        Product changedProduct = productDAO.updateProductName(number, name);

        return changedProduct.fromEntity(changedProduct);
    }

    @Override
    public void deleteProduct(Long number) throws Exception {
        productDAO.deleteProduct(number);
    }
}
  • DTO 객체와 엔티티 객체가 공존하도록 설계가 되어 있어 변환 작업이 필요하다.
    아래와 같이 엔티티 객체를 DTO 객체로 반환하는 fromEntityDTO를 엔티티 객체로 반환하는 toEntity 변환하는 메서드 구현
public class Product {
	...

    public ProductResponseDto fromEntity(Product product) {
        return ProductResponseDto.builder()
            	.number(product.getNumber())
            	.name(product.getName())
            	.price(product.getPrice())
            	.stock(product.getStock())
            	.build();
    }

    public static Product toEntity(ProductDto productDto) {
        return Product.builder()
                .name(productDto.getName())
                .price(productDto.getPrice())
                .stock(productDto.getStock())
                .build();
    }
}

6.10.2 컨트롤러 생성

package com.springboot.jpa.controller;

import com.springboot.jpa.data.dto.ChangeProductNameDto;
import com.springboot.jpa.data.dto.ProductDto;
import com.springboot.jpa.data.dto.ProductResponseDto;
import com.springboot.jpa.service.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/product")
@RequiredArgsConstructor
public class ProductController {
    private final ProductService productService;

    @GetMapping
    public ResponseEntity<ProductResponseDto> getProduct(
        @RequestParam Long number) {
        ProductResponseDto productResponseDto = productService.getProduct(
            number);
        return ResponseEntity.status(HttpStatus.OK).body(productResponseDto);
    }

    @PostMapping
    public ResponseEntity<ProductResponseDto> createProduct(
        @RequestBody ProductDto request
    ) {
        ProductResponseDto productResponseDto = productService.saveProduct(
            request);
        return ResponseEntity.status(HttpStatus.OK).body(productResponseDto);
    }

    @PutMapping
    public ResponseEntity<ProductResponseDto> changedProductName(
        @RequestBody ChangeProductNameDto request
    ) throws Exception {
        ProductResponseDto productResponseDto = productService
            .changeProductName(request.getNumber(), request.getName());

        return ResponseEntity.status(HttpStatus.OK).body(productResponseDto);
    }

    @DeleteMapping
    public ResponseEntity<String> deleteProduct(
        @RequestParam Long number
    ) throws Exception {
        productService.deleteProduct(number);
        return ResponseEntity.status(HttpStatus.OK).body("정상적으로 삭제되었습니다.");
    }
}
  • changedProductName() 경우 요청 ChangeProductNameDto를 구현
package com.springboot.jpa.data.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ChangeProductNameDto {
    private Long number;
    private String name;
}

6.10.3 Swagger API를 통한 동작 확인

저장
조회
수정
삭제

  • 정상 삭제 캡쳐를 위해 한번 더 생성 후 삭제 시도