6-2. 데이터베이스 연동[Spring Boot]

JuJaeng2·2023년 12월 11일

이전글에서는 이론적인 부분을 다루어 보았다. 이번에는 Spring Boot와 MariaDB를 연결해 보도록 할 것이다.

✅ 데이터베이스 연동

프로젝트 생성

라이브러리 선택

  • Lombok
  • Spring Configuration Processor
  • Spring Web
  • Spring Data JPA
  • MaraiDB Driver

application.properties 작성

spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://localhost:3306/springboot
spring.datasource.username=your username
spring.datasource.password=your password

spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

❗️마지막 세줄은 하이버네이트를 사용할 때 활성화할 수 있는 선택사항이다.

  • spring.jpa.hibernate.ddl-auto : 데이터베이스를 자동으로 조작하는 옵션
    • create : 애플리케이션이 가동되고 SesseionFactory가 실행될 때 기존 테이블을 지우고 새로 생성
    • create-drop : create와 동일한 기능을 수행하지만 애플리케이션을 종료하는 시점에 테이블을 지움
    • update : SessionFactory가 실행될 때 객체를 검사해서 변경된 스키마를 갱신, 기존의 저장된 데이터는 유지
    • validate : update처럼 객체를 검사하지만 스키마는 거드리지 않음. 검사 과정에서 데이터베이스의 테이블 정보와 객체의 정보가 다르면 에러가 발생
    • none : ddl-auto 기능을 사용하지 않음
  • spring.jpa.show-sql : 로그에 하이버네이트가 생성한 쿼리문을 출력하는 옵션
  • spring.jpa.properties.hibernate.format_sql : 로그에 출력한 쿼리문을 사람이 보기좋게 포메팅할 수 있음

✅ 엔티티 설계

data.entity 패키지에 생성해 주었다.


import jakarta.persistence.*;

import java.time.LocalDateTime;

@Entity
@Table(name = "product")
public class Product {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long number;
    
    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private Integer price;
    @Column(nullable = false)
    private Integer stock;
    
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;

    // Getter, Setter 메서드 생략
}

엔티티 관련 기본 어노테이션

@Entity

  • 해당 클래스가 엔티티임을 명시하기 위한 어노테이션

@Table

  • 엔티티 클래스는 테이블과 매핑되므로 특별한 경우가 아니면 @Table어노테이션은 필요하지 않다.
  • 클래스의 이름, 테이블의 이름을 다르게 지정해얗 나는 경우 사용

@Id

  • 테이블의 기본값 역할로 사용
  • 모든 엔티티는 @Id 어노테이션이 필요하다.

@GeneratedValue

  • 일반적으로 @Id 어노테이션과 함께 사용된다.
  • DB에서의 auto-increment를 사용할 것이기 때문에 IDENTITY를 사용했다.

@Column

  • 엔티티 클래스의 필드는 자동으로 테이블 칼럼으로 매핑되기때문에 별다른 설정을 하지 않는다면 명시하지 않아도 된다.

@Transient

  • 엔티티 클래스에는 선언돼 있는 필드지만 데이터베이스에서는 필요 없을 경우 사용

✅ 레포지토리 인터페이스 설계

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

엔티티를 데이터베이스의 테이블과 구조를 생성하는데 사용했다면 레포지토리는 엔티티가 생성한 데이터베이스에 접근하는 데 사용된다.

레포지토리 언테페이스 생성

data.repository 패키지에 생성한다. 접근하려는 테이블과 매핑되는 엔티티에 대한 인터페이스를 생성하고, JpaRepository를 상속받는다.

package com.database.databasebookstudy.data.repository;

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

public interface ProductRepository extends JpaRepository<Product, Long> {

}
  • JpaRepository를 상속받을 때 대상 엔티티와 기본값 타입을 지정해야 한다.
  • 처음에 만든 엔티티를 사용하기 위해서는 위 코드에서 처럼 대상 텐티티를 Product로 설정하고 해당 엔티티의 @Id필드 타입인 Long을 설정하면 된다.
  • JpaRepository를 상속받으면 별도의 메서드 구현 없이도 많은 기능을 제공한다.

JpaRepository 상속 구조

레퍼지토리 메서드의 생성 규칙

  • 레퍼지토리에서는 몇 가지 명명규칙에 따라 커스텀 메서드도 생성할 수 있다.
  • 레퍼지토리에서 기본적으로 제공하는 조회 메서드는 기본값으로 단일 조회하거나 전체 엔티티를 조회하는 것만 지우너하고 있기 때문에 필요에 따라 다른 조회 메서드가 필요하다.

메서드에 이름을 붙일 때는 첫 단어를 제외한 이후 단어들의 첫 글자를 대무자로 설정해야 JPA에서 정상적으로 인식하고 쿼리를 자동으로 만들어 준다.

  • FindBy : SQL문의 where절 역할을 수행
    ex) findByName(String name) -> name값을 통해 select
  • AND, OR : 조건을 여러개 설정하기 위해 사용
    ex) findByNameAndEmail(String name, String email)
  • Like/NotLike : SQL문의 like와 동일한 기능을 수행/특정 문자를 포함하는지 여부를 조건으로 추가
  • StartsWith/StartingWith : 특정 키워드로 시작하는 문자열 조건 설정
  • EndsWith/EndingWith : 특정 키워드로 끝나는 문자열 조건 설정
  • IsNull/IsNotNull : 레코드 값이 Null 이거나 Null이 아닌 값을 검색
  • True/False : Boolean타입의 레코드를 검색할 때 사용
  • Before/After : 시간을 기준으로 값을 검색
  • Between : 두 값(숫자) 사이의 데이터를 조회
  • OrderBy : SQL문에서 order by와 동일한 기능을 수행
    ex) List< Product > findByNameOrderByPriceAsc(String name);
  • countBy : SQL문의 count와 동일한 기능을 수행/결과값의 개수(count)를 추출

DAO 클래스 생성

  • DAO 클래스는 일반저긍로 '인터페이스-구현체' 구성으로 생성한다.
  • DAO 클래스는 의존성 결합을 낮추기 위한 디자인 패턴이다.

DAO 인터페이스

package com.database.databasebookstudy.data.dao;

import com.database.databasebookstudy.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객체로 전달할지에 대해서는 개발자마다 다르고 회사나 부서마다 다르다고 한다. 이 부분은 내 소속에 따라 유동적으로 적용하면 될 것 같다.

DAO 구현체(ProductImpl 클래스)

  • ProductImpl 클래스를 스프링이 관리하는 빈으로 등록하려면 @Component 또는 @Service 어노테이션을 지정해야 한다.
package com.database.databasebookstudy.data.dao.impl;

// import 길이상 생략

@Component
public class ProductDAOImpl implements ProductDAO {

    private final ProductRepository productRepository;

	// 생성자를 통한 의존성 주입
    @Autowired
    public ProductDAOImpl(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Override
    public Product insertProduct(Product product) {

        Product savedProduct = productRepository.save(product);

        return savedProduct;
    }

    @Override
    public Product selectProduct(Long number) {
        Optional<Product> selectedProduct = productRepository.findById(number);

        if (selectedProduct.isPresent()){
            return selectedProduct.get();
        }else {
            return null;
        }
    }

레포지토리 단건 조회

위 코드에서는 .findById()를 사용했지만 .getById()를 사용해도 된다.

  • .getById()
    • EntityManager의 getReference() 메서드를 호출하고 데이터가 존재하지 않는다면 EntityNotFoundException이 발생한다.
  • .findById()
    • EntitManager의 find()메서드를 호출하고 영속성 컨텍스트의 캐시에서 값을 조회한 후 영속성 컨텍스트에 값이 존재하지 않는다면 실제 데이터베이스에서 데이터를 조회한다. 리턴값으로 Optional객체를 전달한다.

    @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);
        }else {
            throw new Exception();
        }

        return updatedProduct;
    }

JPA는 값을 갱신할 때 update라는 키워드를 사용하지 않는다.
영속성 커택스트를 활용해 값을 갱신하는데, 데이터베이스에서 값을 가져오면 가져온 객체가 영속성 컨텍스트에 추가되고 유지되는 상황에서 객체의 값을 변경하고 다시 save()를 실행하면 JPA에서는 더티체크(Dirty Check)라고 하는 변경감지를 수행한다.
변경이 감지되면 레코드를 업데이트하는 쿼리가 실해된다.

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

레코드를 삭제하려고 할때는 레코드와 매핑된 영속 객체를 여속성 컨텍스트에 가져와야 한다. findById() 메서드를 통해 객체를 가져오고 delete()메서드를 통해 해당 객체를 삭제한다.

마무리 😁

우선 양이 많아 이번 글은 DAO 인터페이스와 구현체를 작성까지만 진행해보았다. 자세한 내용을 담고 싶어서 코드 전체를 담고 설명을 하고있다. 너무 양이 방대해지는것 같기도 하고 좋은 글인지 모르겠어서 일단 이번 챕터까지만 이런식으로 작성하고 방법을 바꿔봐야 할 것 같다.
이번 글에서는 DAO의 필요성에 대해서 알게되었다. 평소 항상 DTO만을 사용해서 객체를 전달했었는데 의존성을 낮추기위해 DAO를 사용해야 한다는 점은 오늘 처음 알게 되었다. 또한 바로 구현을 하기 보다는 인터페이스-구현체 구조를 사용해서 구현한다는 점도 신기했다. 앞으로 진행할 작은 프로젝트들은 DAO를 사용하여 구현해야겠다는 점도 느꼈다. 앞에서 공부했던 영속성 컨텍스트 개념이 update, delete 메서드를 구현할 때 필요했던 점도 신기했다.
다음 글에서는 컨트롤러와 비즈니스 로직을 짜보는 시간을 가져보려한다.

profile
다 잘하고 싶은 개발자

0개의 댓글