상세 컨텐츠

본문 제목

[스프링 3팀] 6장. 데이터베이스 연동

24-25/Spring 3

by Igumi 2024. 11. 29. 10:00

본문

728x90

 

6. 데이터베이스 연동

 

6.6 데이터베이스 연동

5장에서와 동일한 방식으로 name이 jpa인 프로젝트를 생성합니다.

의존성 선택 단계에서는 Lombok, Spring Configuration Processor, Spring Web, Spring Data JPA, MariaDB Driver를 선택합니다.

 

* Spring Boot 3.x.x 부터는 SpringFox와 호환 되지 않으므로, springdoc을 사용합니다.

 

연동할 데이터베이스의 정보를 application.properties 파일에 작성합니다.

# 데이터베이스 연동
spring.datasource.driverClassName=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://localhost:3306/springboot
spring.datasource.username=root
spring.datasource.password=password
# username와 password는 본인이 설정한 정보 입력

# 하이버네이트 옵션
spring.jpa.hibernate.ddl-auto=create
# create: 애플리케이션이 가동되고 SessionFactory가 실행될 때 기존 테이블을 지우고 새로 생성.
# create-drop:create와 동일한 기능을 수행하나 애플리케이션을 종료하는 시점에 테이블을 지움.
# update:SessionFactory가 실행될 때 객체를 검사해서 변경된 스키마를 갱신. 기존에 저장된 데이터는 유지됨.
# validate:update처럼 객체를 검사하지만 스키마는 건드리지않음. 검사 과정에서 데이터베이스의 테이블 정보와 객체의 정보가 다르면 에러 발생.
# none:ddl-auto 기능을 사용하지 않음.
# 운영 환경에서는 대체로 validate 또는 none을 사용.

# 하이버네이트가 생성한 쿼리문 출력
spring.jpa.show-sql=true

# 쿼리문 포매팅
spring.jpa.properties.hibernate.format_sql=true

 

 

6.7 엔티티 설계

Spring Data JPA를 사용하면 데이터베이스에 테이블을 생성하기 위해 직접 쿼리를 작성할 필요가 없는데, 이 기능을 가능하게 하는 것이 바로 엔티티입니다.

 

엔티티 어노테이션을 사용하여 다음과 같이 Product 엔티티 클래스를 생성합니다.

@Entity // 클래스와 테이블 일대일 매칭.
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@ToString(exclude = "name")
@Table(name = "product")    // 클래스명과 테이블명이 다를 때 테이블명 명시.
public class Product {

    @Id // 필수 어노테이션.
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 필드의 값을 자동 생성. 일반적으로 @Id와 함께 사용.
    // AUTO: IDENTITY, SEQUENCE, TABLE 세 전략 중 자동 선택
    // IDENTITY: 데이터베이스의 AUTO_INCREMENT를 사용해 기본값을 생성.
    // SEQUENCE: @SequenceGenerator 어노테이션으로 식별자 생성기를 정의해 값을 주입받음.
    // TABLE: 모든 DBMS에서 동일하게 동작. @TableGenerator 어노테이션으로 별도의 테이블을 생성하여 값을 갱신.
    private Long number;

    @Column(nullable = false)   // 필드에 설정을 더할 때 사용. (name, nullable, length, unique)
    private String name;

    @Column(nullable = false)
    private Integer price;

    @Column(nullable = false)
    private Integer stock;

    private LocalDateTime createdAt;

    private LocalDateTime updatedAt;

    // @Transient: 데이터베이스에서 필요 없는 필드에 사용하는 어노테이션.

}

 

 

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

Spring Data JPA는 JpaRepository를 기반으로 더욱 쉽게 DB를 사용할 수 있는 아키텍처를 제공합니다.

 

JpaRepository를 상속받아 다음과 같이 ProductRepository 인터페이스를 생성합니다

// 엔티티에 대한 interface 생성, JpaRepository 상속 받음. (대상 엔티티와 ID의 타입 지정)
// JpaRepository에서 findAll() 등 많은 기능 제공.
public interface ProductRepository extends JpaRepository<Product, Long> {

}

 

리포지토리 메서드의 생성 규칙은 다음과 같습니다. (자세한 쿼리 메서드는 7장에서 다룹니다.)

- 메서드에 이름을 붙일 때는 첫 단어를 제외한 이후 단어들의 첫 글자를 대문자로 설정해야 합니다.

- FindBy: SQL문의 where 절 역할을 수행하는 구문입니다. findBy 뒤에 엔티티의 필드값을 입력해서 사용합니다.
- AND, OR: 조건을 여러 개 설정하기 위해 사용합니다.
- Like, NotLike: SQL문의 like와 동일한 기능을 수행하며, 특정 문자를 포함하는지 여부를 조건으로 추가합니다.
- StartsWith/StartingWith: 특정 키워드로 시작하는 문자열 조건을 설정합니다.
- EndsWith/EndingWith: 특정 키워드로 끝나는 문자열 조건을 설정합니다.
-  IsNull/IsNotNull: 레코드 값이 Null이거나 Null이 아닌 값을 검색합니다.
- True/False: Boolean 타입의 레코드를 검색할 때 사용합니다.
- Before/After: 시간을 기준으로 값을 검색합니다.
- LessThan/GreaterThan: 특정 값(숫자)을 기준으로 대소 비교를 할 때 사용합니다.
- Between: 두 값(숫자) 사이의 데이터를 조회합니다.
- OrderBy: SQL문에서 order by와 동일한 기능을 수행합니다.
- countBy: SQL문의 count와 동일한 기능을 수행하며, 결괏값의 개수(count)를 추출합니다.

 

6.9 DAO 설계

DAO(Data Access Object)는 데이터베이스에 접근하기 위한 로직을 관리하기 위한 객체입니다.
비즈니스 로직의 동작 과정에서 데이터를 조작하는 기능은 DAO 객체가 수행합니다.

DAO 클래스는 일반적으로 '인터페이스-구현체' 구성으로 생성합니다.

 

다음과 같이 ProductDAO 인터페이스를 생성합니다.

// DAO(Data Access Object): 데이터베이스에 접근하기 위한 로직 관리.
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;

}

 

 

다음과 같이ProductDAO 인터페이스의 구현체 클래스를 생성합니다.

@Component  // 스프링 빈 등록
public class ProductDAOImpl implements ProductDAO {

    private ProductRepository productRepository;

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

    @Override
    public Product insertProduct(Product product) {
        Product savedProduct = productRepository.save(product); // JPA에서 save() 등 메서드 기본 제공

        return savedProduct;
    }

    @Override
    public Product selectProduct(Long number) {
        Product selectedProduct = productRepository.getById(number);
        // getById(): 프락시 객체 반환. 프락시 객체를 사용할 때 실제 쿼리 실행됨. 엔티티가 존재하지 않으면 EntityNotFoundException 발생.
        // findById(): Optional 타입 반환. 영속성 컨텍스트 캐시에 데이터가 없으면 실제 데이터베이스에서 데이터 조회. 엔티티가 존재하지 않으면 Optional.empty() 반환.

        return selectedProduct;
    }

    @Override
    public Product updateProductName(Long number, String name) throws Exception {
        Optional<Product> selectedProduct = productRepository.findById(number); // 영속성 컨텍스트에 추가됨.

        Product updatedProduct;
        if (selectedProduct.isPresent()) {
            Product product = selectedProduct.get();

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

            updatedProduct = productRepository.save(product);   // Dirty Check(변경 감지) 수행
        } else {
            throw new Exception();
        }

        return updatedProduct;
    }

    @Override
    public void deleteProduct(Long number) throws Exception {
        Optional<Product> selectedProduct = productRepository.findById(number);

        if (selectedProduct.isPresent()) {
            Product product = selectedProduct.get();

            productRepository.delete(product);
        } else {
            throw new Exception();
        }
    }
}

 

 

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

앞에서 설계한 구성 요소들을 클라이언트의 요청과 연결하기 위해 컨트롤러와 서비스를 생성합니다.

 

서비스에서 필요한 DTO 클래스인 ProductDto를 다음과 같이 생성합니다.

@Data
@Builder
public class ProductDto {

    private String name;

    private int price;

    private int stock;

}

 

 

서비스에서 필요한 DTO 클래스인 ProductResponseDto를 다음과 같이 생성합니다.

@Data
public class ProductResponseDto {

    private Long number;

    private String name;

    private int price;

    private int stock;

}

 


서비스 객체는 DAO와 마찬가지로 추상화해서 구성합니다.

다음과 같이 ProductService 인터페이스를 생성합니다.

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;

}

 

다음과 같이 ProductService 인터페이스의 구현체인 ProductServiceImpl를 생성합니다.

@Service
public class ProductServiceImpl implements ProductService {

    private final ProductDAO productDAO;

    public ProductServiceImpl(ProductDAO productDAO) {
        this.productDAO = productDAO;
    }

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

        ProductResponseDto productResponseDto = new ProductResponseDto();
        productResponseDto.setNumber(product.getNumber());
        productResponseDto.setName(product.getName());
        productResponseDto.setPrice(product.getPrice());
        productResponseDto.setStock(product.getStock());

        return productResponseDto;
    }

    @Override
    public ProductResponseDto saveProduct(ProductDto productDto) {  // 저장 메소드의 리턴 타입은 void 또는 boolean 등 고려.
        Product product = new Product();
        product.setName(productDto.getName());
        product.setPrice(productDto.getPrice());
        product.setStock(productDto.getStock());
        product.setCreatedAt(LocalDateTime.now());
        product.setUpdatedAt(LocalDateTime.now());

        Product savedProduct = productDAO.insertProduct(product);

        ProductResponseDto productResponseDto = new ProductResponseDto();
        productResponseDto.setNumber(savedProduct.getNumber());
        productResponseDto.setName(savedProduct.getName());
        productResponseDto.setPrice(savedProduct.getPrice());
        productResponseDto.setStock(savedProduct.getStock());

        return productResponseDto;
    }

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

        ProductResponseDto productResponseDto = new ProductResponseDto();
        productResponseDto.setNumber(changedProduct.getNumber());
        productResponseDto.setName(changedProduct.getName());
        productResponseDto.setPrice(changedProduct.getPrice());
        productResponseDto.setStock(changedProduct.getStock());

        return productResponseDto;
    }

    @Override
    public void deleteProduct(Long number) throws Exception {
        productDAO.deleteProduct(number);
    }
}



다음과 같이 ProductController 컨트롤러를 생성합니다.

@RestController
@RequestMapping("api/v1/product")
public class ProductController {

    private final ProductService productService;

    public ProductController(ProductService productService) {
        this.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 productDto) {
        ProductResponseDto productResponseDto = productService.saveProduct(productDto);

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

    @PutMapping()
    public ResponseEntity<ProductResponseDto> changeProductName(
            @RequestBody ChangeProductNameDto changeProductNameDto) throws Exception {
        ProductResponseDto productResponseDto = productService.changeProductName(
                changeProductNameDto.getNumber(),
                changeProductNameDto.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.NO_CONTENT).body("정상적으로 삭제되었습니다.");
    }

}

 

컨트롤러에서 사용되는 ChangeProductNameDto는 다음과 같이 생성합니다.

@Data
public class ChangeProductNameDto {

    private Long number;

    private String name;

}

 

 

Swagger API를 사용하여 다음과 같이 작성한 API 기능을 요청해볼 수 있습니다

(http://localhost:8080/swagger-ui/index.html)

 

 

 


 

QUIZ

 

  1. Spring Data JPA를 사용하면 데이터베이스에 테이블을 생성하기 위해 직접 쿼리를 작성할 필요가 없는데, 이 기능을 가능하게 하는 것은 _________ 이다.
  2. Spring Data JPA는 _________ 를 기반으로 더욱 쉽게 DB를 사용할 수 있는 아키텍처를 제공한다.
  3. _________ 어노테이션은 모든 엔티티에 필수인 어노테이션이다.
  4. _________는 DB에 접근하기 위한 로직을 관리하는 객체이다.
  5. 데이터(모델) 클래스를 생성할 때 반복적으로 사용하는메서드를 어노테이션으로 대체하는 기능을 제공하는 라이브러리는 _________이다.
  6. findById()는 Optional 타입을 반환하며, _________ 캐시에 데이터가 없으면 실제 데이터베이스에서 데이터 조회한다.
  7. _________ 어노테이션은 필드의 값을 자동 생성하며, 일반적으로 @Id와 함께 사용한다.

 

PROGRAMMING QUIZ

1. Product 클래스의 각 필드에 알맞은 어노테이션을 추가하세요.

public class Product {

    // 알맞은 어노테이션 추가
    private Long number;

    // 알맞은 어노테이션 추가
    private String name;
    
}

 

2. 알맞은 인터페이스를 상속 받아 ProductRepository를 생성하세요. (대상 엔티티: Long 타입의 id를 가진 Product 엔티티)

public interface ProductRepository /* 알맞은 인터페이스를 상속 */ {

}

 


정답

더보기

QUIZ 정답 :
엔티티, JpaRepository, @Id, DAO, 롬복(Lombok), 영속성 컨텍스트, @GeneratedValue


PROGRAMING QUIZ 정답:

1번

public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long number;

    @Column(nullable = false)
    private String name;
    
}


2번:

public interface ProductRepository extends JpaRepository<Product, Long> {

}

[출처] 장정우, 「스프링부트 핵심 가이드」 6.6장 ~ 6.11.3

 

Corner Spring 3

ⓒ Pumpkin

728x90

관련글 더보기