Spring Boot/데이터베이스 연동

[Spring boot] DAO 설계

재윤 2025. 8. 17. 17:36
반응형

DAO(Data Access Object)는 DB에 접근하기 위한 로직을 관리하기 위한 객체

  • 비즈니스 로직의 동작 과정에서 데이터를 조작하는 기능은 DAO 객체가 수행
  • but 스프링 데이터 JPA에서 DAO의 개념은 레포지토리가 대체

규모가 작은 서비스에서는 DAO를 별도로 설계하지 않고 바로 서비스 레이어에서 DB에 접근해서 구현하기도 한다.

  • 단 실제로 업무에 필요한 비즈니스 로직을 개발하다 보면 데이터를 다루는 중간 계층을 다두는 것이 유지보수 측면에서 용이한 경우가 많다.

DAO 클래스 생성

  • DAO 클래스는 일반적으로 ‘인터페이스-구현체’ 구성으로 생성
  • DAO 클래스는 의존성 결합을 낮추기 위한 디자인 패턴
  • 서비스 레이어에 DAO 객체를 주입받을 때 인터페이스를 선언하는 방식으로 구성

밑과 같이 data

 

기본적인 CRUD를 다루기 위해 다음과 같이 인터페이스에 메서드를 정의

import entity.Product;

// 예제 6.9
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;
}
  • DB에 접근하는 메서드는 리턴 값으로 데이터 객체를 전달
    • 이때 데이터 객체를 엔티티 객체로 전달할지 DTO 객체로 전달할지에 대해서는 개발자마다 의견이 분분하다.
  • 일반적인 설계 원칙에서 엔티티 객체는 DB에 접근하는 계층에서만 사용하도록 정의한다.
  • 다른 계층으로 데이터를 전달할 때는 DTO 객체를 사용

 

이제 인터페이스의 구현체를 만들어야한다.

  • 기능 구현을 위해 다음과 같은 코드 구현체 클래스 작성
  • 빈에 등록하려면 → @Component or @Service 애너테이션 지정
import java.time.LocalDateTime;
import java.util.Optional;

import entity.Product;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import repository.ProductRepository;

@Component
public class ProductDAOImpl implements ProductDAO {

    private 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) {
        Product selectedProduct = productRepository.getById(number);

        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);
        } 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();
        }
    }
}
  • 레포지토리에서는 단건조회를 위한 기본 메서드로 두 가지를 제공하는데 getById(), findById() 메서드이다.
  • 두 메서드는 조회한다는 기능 측면에서는 동일하지만 세부 내용이 다르다.
    @Override
    public Product selectProduct(Long number) {
        Product selectedProduct = productRepository.getById(number);

        return selectedProduct;
    }
  • getById()
    • 내부적으로 EntityManager의 getRefrence() 메서드를 호출
    • getReference() 메서드를 호출하면 프락시 객체를 리턴
    • 실제 쿼리는 프락시 객체를 통해 최초로 데이터에 접근하는 시점에 실행됨
    • 이때 데이터가 존재하지 않을 경우 EntityNotFoundException이 발생
  • findById()
    • 내부적으로 EntityManager의 find() 메서드를 호출
    • 이 메서드는 컨텍스트의 캐시에서 값을 조회한 후 영속성 컨텍스트에 값이 존재하지 않는다면 실제 DB에서 데이터 조회
    @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에서 데이터의 값을 변경할 때 다른 메서드와 차이점이 있다.
  • JPA는 값을 갱신할 때 update라는 키워드 사용 X

 

영속성 컨텍스트 활용해 값을 갱신

  • find() 메서드를 통해 DB에서 값을 가져오면 가져온 객체가 영속성 컨텍스트에 추가

 

  • 영속성 컨텍스트가 유지되는 상화에서 객체의 값을 변경하고 다시 save()를 실행하면 JPA에서는 더티 체크라고 하는 변경 감지 수행

 

save 메서드 살펴보기

@Transactional
@Override
public <S extends T> S save(S entity) {

    Assert.notNull(entity, "Entity must not be null.");

    if (entityInformation.isNew(entity)) {
        em.persist(entity);
        return entity;
    } else {
        return em.merge(entity);
    }
}
  • Transactional으로 인해 작업을 마칠 경우 자동으로 flush() 메서드 실행
  • 이 과정에서 변경이 감지 되면 대상 객체에 해당하는 DB의 레코드를 업데이트하는 쿼리 실행

 

delete

  • findById해서 가져오고 객체 삭제
@Override
@Transactional
@SuppressWarnings("unchecked")
public void delete(T entity) {
    Assert.notNull(entity, "Entity must not be null!");

    if (entityInformation.isNew(entity)) {
        return;
    }

    Class<?> type = ProxyUtils.getUserClass(entity);

    T existing = (T) em.find(type, entityInformation.getId(entity));

    // if the entity to be deleted doesn't exist, delete is a NOOP
    if (existing == null) {
        return;
    }

    em.remove(em.contains(entity) ? entity : em.merge(entity));
}

  • 밑 코드에서 엔티티가 영속성 컨텍스트에 있는지 파악하고 해당 엔티티를 영속성 컨텍스트에 영속화하는 작업을 거쳐 DB의 레코드와 매핑
  • 영속 객체를 대상으로 삭제 요청을 수행하는 메서드를 실행해 작업을 마치고 커밋(Commit) 단계에서 삭제 진행
    if (entityInformation.isNew(entity)) {
        return;
    }

 

반응형