Advance I/Spring DB

22.11.08 :: 스프링 DB 2편 - 데이터 접근 활용 기술

kinggora 2022. 11. 8. 22:15

스프링 DB 2편 - 데이터 접근 활용 기술

 

데이터 접근 기술들, 각 기술의 장단점, 테스트

스프링 트랜잭션(@Transactional) : 주의사항, 옵션, 예외처리, 커밋/롤백 원리, 전파

1. 데이터 접근 기술 - 시작 

데이터 접근 기술 진행 방식 소개

적용 데이터 접근 기술

SQLMapper

  • JdbcTemplate
  • MyBatis

 

ORM 관련 기술

  • JPA, Hibernate
  • 스프링 데이터 JPA
  • Querydsl

 

SQL Mapper 주요기능

  • 개발자는 SQL만 작성하면 해당 SQL의 결과를 객체로 편리하게 매핑해준다.
  • JDBC를 직접 사용할 때 발생하는 여러가지 중복을 제거해주고, 기타 개발자에게 여러가지 편리한 기능을 제공한다.

 

ORM 주요 기능

  • JdbcTemplate이나 MyBatis 같은 SQL 매퍼 기술은 SQL을 개발자가 직접 작성해야 하지만, JPA를 사용하면 기본적인 SQL은 JPA가 대신 작성하고 처리해준다. 개발자는 저장하고 싶은 객체를 마치 자바 컬렉션에 저장하고 조회하듯이 사용하면 ORM 기술이 데이터베이스에 해당 객체를 저장하고 조회해준다.
  • JPA는 자바 진영의 ORM 표준이고, Hibernate(하이버네이트)는 JPA에서 가장 많이 사용하는 구현체이다. 자바에서 ORM을 사용할 때는 JPA 인터페이스를 사용하고, 그 구현체로 하이버네이트를 사용한다고 생각하면 된다.
  • 스프링 데이터 JPA, Querydsl은 JPA를 더 편리하게 사용할 수 있게 도와주는 프로젝트이다. 실무에서는 JPA를 사용하면 이 프로젝트도 꼭! 함께 사용하는 것이 좋다. 개인적으로는 거의 필수라 생각한다.

강의 목표

  • 데이터 접근 기술에 대한 기본 이해와 전체 큰 그림을 그린다.
  • 각 기술들의 핵심 기능 위주로 학습한다.
  • 각 기술들을 점진적으로 도입하는 과정을 통해서 각 기술의 특징과 장단점을 자연스럽게 이해할 수 있다.

먼저 메모리 기반으로 완성되어 있는 프로젝트를 확인하고, 이 프로젝트에 데이터 접근 기술을 하나씩 추가해보자.

프로젝트 설정과 메모리 저장소

스프링 MVC 1편에서 마지막에 완성한 상품 관리 프로젝트는 단순히 메모리에 상품 데이터를 저장하도록 되어 있었다.

여기에 메모리가 아닌 실제 데이터 접근 기술들을 하나씩 적용해가면서, 각각의 데이터 접근 기술들을 어떻게 사용하는지, 장단점은 무엇인지 학습해보자.

 

MVC1 편에서 개발한 상품 관리 프로젝트를 다듬고 일부 기능을 추가해서 사용할 것이다.

데이터 접근 기술에 초점을 맞추기 위해 검증을 포함한 여러 기능이 빠져있다.

프로젝트 구조 설명1 - 기본

ItemRepository 인터페이스

메모리 구현체에서 향후 다양한 데이터 접근 기술 구현체로 손쉽게 변경하기 위해 리포지토리에 인터페이스를 도입했다.

 

ItemSearchCond

검색 조건으로 사용된다. 상품명, 최대 가격이 있다. 참고로 상품명의 일부만 포함되어도 검색이 가능해야 한다. (like 검색)

 

ItemUpdateDto

상품을 수정할 때 사용하는 객체이다.

단순히 데이터를 전달하는 용도로 사용되므로 DTO를 뒤에 붙였다.

 

참고: DTO(data transfer object)  데이터 전송 객체

기능은 없고 데이터를 전달만 하는 용도로 사용되는 객체를 뜻한다. 참고로 DTO에 기능이 있으면 안되는가? 그것은 아니다. 객체의 주 목적이 데이터를 전송하는 것이라면 DTO라 할 수 있다.

ItemSearchCond 도 DTO 역할을 하지만, 이 프로젝트에서 Cond 는 검색 조건으로 사용한다는 규칙을 정했다. 이런 규칙은 정해진 것이 없기 때문에 해당 프로젝트 안에서 일관성 있게 규칙을 정하면 된다.

 

MemoryItemRepository

  • ItemRepository 인터페이스를 구현한 메모리 저장소이다.
  • 메모리(Map)이기 때문에 자바를 다시 실행하면 기존에 저장된 데이터가 모두 사라진다.
  • findById 는 Optional 을 반환해야 하기 때문에 Optional.ofNullable 을 사용했다.
  • findAll 은 ItemSearchCond 이라는 검색 조건을 받아서 내부에서 데이터를 검색하는 기능을 한다. 데이터베이스로 보면 where 구문을 사용해서 필요한 데이터를 필터링 하는 과정을 거치는 것이다.
    • 여기서 자바 스트림을 사용하여 필터링한다.
    • itemName 이나, maxPrice 가 null 이거나 비었으면 해당 조건을 무시한다.
    • itemName 이나, maxPrice 에 값이 있을 때만 해당 조건으로 필터링 기능을 수행한다.
  • clearStore() 메모리에 저장된 Item 을 모두 삭제해서 초기화한다. 테스트 용도로만 사용한다.

ItemService 인터페이스

서비스의 구현체를 쉽게 변경하기 위해 인터페이스를 사용했다.

참고로 서비스는 구현체를 변경할 일이 많지는 않기 때문에 사실 서비스에 인터페이스를 잘 도입하지는 않는다.

여기서는 예제 설명 과정에서 구현체를 변경할 예정이어서 인터페이스를 도입했다.

 

ItemServiceV1

ItemService 의 구현체로 대부분의 기능을 단순히 리포지토리에 위임한다.

 

ItemController

상품을 CRUD하는 컨트롤러이다. 자세한 내용은 MVC1편을 참고하자.

화면을 출력하기 위한 리소스( css , html , templates )는 MVC1편을 참고하자.

 

참고: DTO의 위치

실제로 해당 DTO가 마지막에 도달하는 곳에 소속된다고 생각하자.

ItemUpdateDto 의 경우 컨트롤러에서 생성되어 최종적으로 ItemRepository.update() 에서 파라미터로 받아서 사용된다.

상황마다 다르지만 이때 ItemUpdateDto 는 ItemRepository 가 있는 곳에 두는 것이 자연스럽다.

보통 애플리케이션에서 리포지토리->서비스->컨트롤러 순으로 호출되는데 서비스나 컨트롤러의 자원을 리포지토리가 참조하면 순환 참조가 일어나 흐름이 꼬이게 된다.

프로젝트 구조 설명2 - 설정

스프링 부트 설정 분석

@Configuration
public class MemoryConfig {

    @Bean
    public ItemService itemService() {
        return new ItemServiceV1(itemRepository());
    }

    @Bean
    public ItemRepository itemRepository() {
        return new MemoryItemRepository();
    }

}

 

  • ItemServiceV1 , MemoryItemRepository 를 스프링 빈으로 등록하고 생성자를 통해 의존관계를 주입한다.
  • 참고로 서비스와 리포지토리는 구현체를 편리하게 변경하기 위해, 이렇게 수동으로 빈을 등록했다.
  • 컨트롤러는 컴포넌트 스캔을 사용한다.

 

@Slf4j
@RequiredArgsConstructor
public class TestDataInit {

    private final ItemRepository itemRepository;

    /**
     * 확인용 초기 데이터 추가
     */
    @EventListener(ApplicationReadyEvent.class)
    public void initData() {
        log.info("test data init");
        itemRepository.save(new Item("itemA", 10000, 10));
        itemRepository.save(new Item("itemB", 20000, 20));
    }

}

 

  • 애플리케이션을 실행할 때 초기 데이터를 저장한다.
  • 리스트에서 데이터가 잘 나오는지 편리하게 확인할 용도로 사용한다. 이 기능이 없으면 서버를 실행할 때 마다 데이터를 입력해야 리스트에 나타난다. (메모리여서 서버를 내리면 데이터가 제거된다.)
  • @EventListener(ApplicationReadyEvent.class) : 스프링 컨테이너가 완전히 초기화를 다 끝내고, 실행 준비가 되었을 때 발생하는 이벤트이다. 스프링이 이 시점에 해당 애노테이션이 붙은 initData() 메서드를 호출해준다.
  • 참고로 @EventListener 대신 @PostConstruct 를 사용할 경우 AOP 같은 부분이 아직 다 처리되지 않은 시점에 호출될 수 있기 때문에, 간혹 문제가 발생할 수 있다. 예를 들어서 @Transactional 과 관련된 AOP가 적용되지 않은 상태로 호출될 수 있다.
  • @EventListener(ApplicationReadyEvent.class) 는 AOP를 포함한 스프링 컨테이너가 완전히 초기화 된 이후에 호출되기 때문에 이런 문제가 발생하지 않는다.

 

@Import(MemoryConfig.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {

   public static void main(String[] args) {
      SpringApplication.run(ItemServiceApplication.class, args);
   }

   @Bean
   @Profile("local")
   public TestDataInit testDataInit(ItemRepository itemRepository) {
      return new TestDataInit(itemRepository);
   }

}

 

  • @Import(MemoryConfig.class) : 앞서 설정한 MemoryConfig 를 설정 파일로 사용한다.
  • scanBasePackages = "hello.itemservice.web" : 여기서는 컨트롤러만 컴포넌트 스캔을 사용하고, 나머지는 직접 수동 등록한다. 그래서 컴포넌트 스캔 경로를 hello.itemservice.web 하위로 지정했다.
  • @Profile("local") : 특정 프로필의 경우에만 해당 스프링 빈을 등록한다. 여기서는 local 이라는 이름의 프로필이 사용되는 경우에만 testDataInit 이라는 스프링 빈을 등록한다.
  • TestDataInit 에서 초기 데이터를 만들어서 저장하는 메서드를 @EventListener 로 지정했는데 이 애노테이션도 결국 스프링이 관리하므로 TestDataInit 도 스프링 빈으로 등록해야 해당 애노테이션이 작동한다.

프로필 (Profile)

스프링은 로딩 시점에 application.properties 의 spring.profiles.active 속성을 읽어서 프로필로 사용한다.

이 프로필은 로컬(나의 PC), 운영 환경, 테스트 실행 등등 다양한 환경에 따라서 다른 설정을 할 때 사용하는 정보이다.

예를 들어서 로컬PC에서는 로컬 PC에 설치된 데이터베이스에 접근해야 하고, 운영 환경에서는 운영 데이터베이스에 접근해야 한다면 서로 설정 정보가 달라야 한다. 심지어 환경에 따라서 다른 스프링 빈을 등록해야 할 수 있다.

 

main 프로필

/src/main/resources 하위의 application.properties

spring.profiles.active=local

 

이 위치의 application.properties 는 /src/main 하위의 자바 객체를 실행할 때 (주로 main() ) 동작하는 스프링 설정이다. spring.profiles.active=local 이라고 하면 스프링은 local 이라는 프로필로 동작한다.

따라서 직전에 설명한 @Profile("local") 가 동작하고, testDataInit 가 스프링 빈으로 등록된다.

 

실행 로그

The following 1 profile is active: "local"

 

참고: 프로필을 지정하지 않으면 디폴트( default ) 프로필이 실행된다.

No active profile set, falling back to 1 default profile: "default"

 

test 프로필

/src/test/resources 하위의 application.properties 

spring.profiles.active=test

 

이 위치의 application.properties 는 /src/test 하위의 자바 객체를 실행할 때 동작하는 스프링 설정이다.

주로 테스트 케이스를 실행할 때 동작한다.

 

 

  • 프로필 기능을 사용해서 스프링으로 웹 애플리케이션을 로컬( local )에서 직접 실행할 때는 testDataInit 이 스프링 빈으로 등록된다. 따라서 등록한 초기화 데이터를 편리하게 확인할 수 있다.
  • 초기화 데이터 덕분에 편리한 점도 있지만, 테스트 케이스를 실행할 때는 문제가 될 수 있다. 테스트에서 이런 데이터가 들어있다면 오류가 발생할 수 있다. 예를 들어서 데이터를 하나 저장하고 전체 카운트를 확인하는데 1이 아니라 testDataInit 때문에 데이터가 2건 추가되어서 3이 되는 것이다.
  • 프로필 기능 덕분에 테스트 케이스에서는 test 프로필이 실행된다. 따라서 TestDataInit 는 스프링 빈으로 추가되지 않고, 따라서 초기 데이터도 추가되지 않는다.

참고: 프로필에 대한 스프링 부트 공식 메뉴얼 (스프링 부트 강의 참고)

https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.profiles