데이터 접근기술

이동건 (불꽃냥펀치)·2025년 2월 23일
0

데이터 접근 기술 종류

적용 데이터 접근 기술

  • Jdbc Template
  • MyBatis
  • JPa, Hibernate
  • 스프링 데이터 Jpa
  • Querydsl

SQL Mapper

  • Jdbc Template
  • MyBatis
    ORM 관련 기술
  • Jpa, Hibernate
  • 스프링 데이터 JPA
  • QueryDsl

SqL Mapper 주요 기능

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

ORM 주요기능

  • Jdbc Template이나 MyBatis 같은 SQL 을 개발자가 직접 작성해야 하지만 JPA를 사용하면 기본적인 SQL은 JPA가 대신 작성



도메인 분석

Item

 @Data
 public class Item {
     private Long id;
     private String itemName;
     private Integer price;
     private Integer quantity;
     public Item() {
     }
     public Item(String itemName, Integer price, Integer quantity) {
         this.itemName = itemName;
         this.price = price;
         this.quantity = quantity;
	} 
}

ItemRepository 인터페이스

public interface ItemRepository {
     Item save(Item item);
     void update(Long itemId, ItemUpdateDto updateParam);
     Optional<Item> findById(Long id);
     List<Item> findAll(ItemSearchCond cond);
}

ItemSearchCond

@Data
 public class ItemSearchCond {
     private String itemName;
     private Integer maxPrice;
     public ItemSearchCond() {
     }
     public ItemSearchCond(String itemName, Integer maxPrice) {
         this.itemName = itemName;
         this.maxPrice = maxPrice;
	}
}
  • 검색조건으로 활용됨, 상품명, 최대 가격이 있다. 참고로 상품명의 일부만 포함되어있더라도 검색이 가능해야 함

ItemUpdateDto


 @Data
 public class ItemUpdateDto {
     private String itemName;
     private Integer price;
     private Integer quantity;
     public ItemUpdateDto() {
     }
     public ItemUpdateDto(String itemName, Integer price, Integer quantity) {
         this.itemName = itemName;
         this.price = price;
         this.quantity = quantity;
     }
 }
  • 상품을 수정할 때 사용하는 객체
  • 단순히 데이터를 전달하는 용도로만 사용됨

DTO 전송객체

  • 데이터 전송객체
  • DTO는 기능은 없고 단순히 데이터를 전달만 하는 용도로 사용되는 객체

ItemRepository 구현체

@Repository
 public class MemoryItemRepository implements ItemRepository {
     private static final Map<Long, Item> store = new HashMap<>(); //static
     private static long sequence = 0L; //static
     @Override
     public Item save(Item item) {
         item.setId(++sequence);
         store.put(item.getId(), item);
         return item;
         }
      @Override
      public void update(Long itemId, ItemUpdateDto updateParam) {
          Item findItem = findById(itemId).orElseThrow();
          findItem.setItemName(updateParam.getItemName());
          findItem.setPrice(updateParam.getPrice());
          findItem.setQuantity(updateParam.getQuantity());
      }
      @Override
      public Optional<Item> findById(Long id) {
          return Optional.ofNullable(store.get(id));
      }
      @Override
      public List<Item> findAll(ItemSearchCond cond) {
          String itemName = cond.getItemName();
          Integer maxPrice = cond.getMaxPrice();
          return store.values().stream().filter(item->{
          if(ObjectUtils.isEmpty(itemName)){
          		return true;
          	}
            return item.getPrice()<=maxPrice;
            
          })
          .collect(Collectors.toList());
      }
    
    public void clearStore() {
        store.clear();
	}
}
  • ItemRepository 인터페이스를 구현한 메모리 저장소이다

  • 메모리이기 때문에 자바를 다시 실행하면 기존에 저장된 모든 데이터가 사라짐

  • save,update,findById는 쉽게 이해할 수 있다

  • 참고로 optional값을 반환하면 값이 존재하지 않으면 ofNullable로 오류가 안나게 할 수 있다

  • findAllItemSearchCond이라는 검색 조건을 받아서 내부에서 데이터를 검색하는 기능 수행

    • itemName이나 maxPrice가 빈값이면 해당 조건 무시
    • 값이 있을 때만 해당 조건으로 필터링 기능 수행



서비스 분석

ItemService 인터페이스

public interface ItemService {
     Item save(Item item);
     void update(Long itemId, ItemUpdateDto updateParam);
     Optional<Item> findById(Long id);
     List<Item> findItems(ItemSearchCond itemSearch);
 }

ItemServiceV1

@Service
@RequiredArgsConstructor
public class ItemServiceV1 implements ItemService {
    private final ItemRepository itemRepository;
    @Override
    public Item save(Item item) {
        return itemRepository.save(item);
    }
    @Override
    public void update(Long itemId, ItemUpdateDto updateParam) {
        itemRepository.update(itemId, updateParam);
    }
    @Override
    public Optional<Item> findById(Long id) {
        return itemRepository.findById(id);
    }
    @Override
    public List<Item> findItems(ItemSearchCond cond) {
        return itemRepository.findAll(cond);
    }
}
  • 이 구현체는 대부분의 기능을 단순히 저장소에 위임한다



컨트롤러 분석

HomeController

 @Controller
 @RequiredArgsConstructor
 public class HomeController {
     @RequestMapping("/")
     public String home() {
         return "redirect:/items";
     }
}
  • 단순히 홈으로 요청이 왔을 때 items로 이동하는 컨트롤러
    ItemController
 @Controller
 @RequestMapping("/items")
 @RequiredArgsConstructor
 public class ItemController {
     private final ItemService itemService;
@GetMapping
     public String items(@ModelAttribute("itemSearch") ItemSearchCond itemSearch,
 Model model) {
 		List<Item> items = itemService.findItems(itemSearch);
        model.addAttribute("items", items);
        return "items";
	}
    @GetMapping("/{itemId}")
    public String item(@PathVariable long itemId, Model model) {
        Item item = itemService.findById(itemId).get();
        model.addAttribute("item", item);
        return "item";
	}
    @GetMapping("/add")
    public String addForm() {
        return "addForm";
    }
    @PostMapping("/add")
    public String addItem(@ModelAttribute Item item, RedirectAttributes
redirectAttributes) {
        Item savedItem = itemService.save(item);
        redirectAttributes.addAttribute("itemId", savedItem.getId());
        redirectAttributes.addAttribute("status", true);
        return "redirect:/items/{itemId}";
	}
    @GetMapping("/{itemId}/edit")
    public String editForm(@PathVariable Long itemId, Model model) {
        Item item = itemService.findById(itemId).get();
        model.addAttribute("item", item);
        return "editForm";
}
    @PostMapping("/{itemId}/edit")
    public String edit(@PathVariable Long itemId, @ModelAttribute ItemUpdateDto
updateParam) {
        itemService.update(itemId, updateParam);
        return "redirect:/items/{itemId}";
    }
}



스프링부트 설정 분석

MemoryConfig

@Configuration
 public class MemoryConfig {
     @Bean
     public ItemService itemService() {
         return new ItemServiceV1(itemRepository());
     }
     @Bean
     public ItemRepository itemRepository() {
         return new MemoryItemRepository();
     }
}
  • itemServiceV1,MemoryItemRepository를 스프링 빈으로 등록하고 생성자를 통해 의존관계를 주입
  • 구현체를 편리하게 관리하기 위해 수동으로 빈 등록
  • 컨트롤러는 컴포넌트 스캔을 사용

TestDataInit

@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) : 스프링 컨테이너가 완전히 초기화를 띁내놓고 실행 준비가 되었을 때 발생하는 이벤트이다. 스프링이 이 시점에 해당 애너테이션이 붙은 메서드를 호출함

ItemServiceApplication

@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이라는 스프링 빈을 등록

프로필
스프링은 로딩 시점에 application.propertiesspring.profiles.active 속성을 읽어서 프로필로 사용함

이 프로필은 다양한 환경에 따라서 다른 설정을 할때 사용하는 정보이다. 예를 들어서 로컬 PC에서는 로컬 PC에 저장된 데이터베이스에 접근해야하고 운영 환경에서는 운영 데이터베이스에 접근해야한다면 서로 설정 정보가 달라야한다. 심지어 환경에 따라서 다른 스프링 빈을 등록할 수 있어야한다. 프로필은 이런 문제를 해결하는데 도움을 준다

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

spring.profiles.active=local
  • 이 위치의 application.properties/src/main 하위의 자바 객체를 실행할 때 (주로 main() ) 동작 하는 스프링 설정이다. spring.profiles.active=local 이라고 하면 스프링은 local 이라는 프로필로 동작한다. 따라서 직전에 설명한 @Profile("local") 가 동작하고, testDataInit 가 스프링 빈으로 등록 된다.

테스트_ ItemRepositoryTest

@SpringBootTest
 class ItemRepositoryTest {
     @Autowired
     ItemRepository itemRepository;
     
     @AfterEach
     void afterEach() {
//MemoryItemRepository 의 경우 제한적으로 사용
		if (itemRepository instanceof MemoryItemRepository) {
          ((MemoryItemRepository) itemRepository).clearStore();
    	}
	}
    @Test
    void save() {
    //given
        Item item = new Item("itemA", 10000, 10);
        //when
        Item savedItem = itemRepository.save(item);
    //then
        Item findItem = itemRepository.findById(item.getId()).get();
        assertThat(findItem).isEqualTo(savedItem);
    }
    @Test
    void updateItem() {
    //given
        Item item = new Item("item1", 10000, 10);
        Item savedItem = itemRepository.save(item);
        Long itemId = savedItem.getId();
    //when
        ItemUpdateDto updateParam = new ItemUpdateDto("item2", 20000, 30);
        itemRepository.update(itemId, updateParam);
    //then
        Item findItem = itemRepository.findById(itemId).get();
        assertThat(findItem.getItemName()).isEqualTo(updateParam.getItemName());
        assertThat(findItem.getPrice()).isEqualTo(updateParam.getPrice());
        assertThat(findItem.getQuantity()).isEqualTo(updateParam.getQuantity());
    }
    @Test
    void findItems() {
    //given
        Item item1 = new Item("itemA-1", 10000, 10);
        Item item2 = new Item("itemA-2", 20000, 20);
        Item item3 = new Item("itemB-1", 30000, 30);
    	itemRepository.save(item1);
         itemRepository.save(item2);
        itemRepository.save(item3);
		//둘 다 없음 검증
		test(null, null, item1, item2, item3); 
        test("", null, item1, item2, item3);
		//itemName 검증
		test("itemA", null, item1, item2);
    	test("temA", null, item1, item2);
        test("itemB", null, item3);
		//maxPrice 검증
		test(null, 10000, item1);
		//둘 다 있음 검증
        test("itemA", 10000, item1);
    }
    void test(String itemName, Integer maxPrice, Item... items) {
        List<Item> result = itemRepository.findAll(new ItemSearchCond(itemName,maxPrice));
        assertThat(result).containsExactly(items);
	} 
}

afterEach() : 테스트는 서로 영향을 주면 안된다. 따라서 각각의 테스트가 끝나고 나면 저장한 데이터를 제거 해야 한다. @AfterEach 는 각각의 테스트의 실행이 끝나는 시점에 호출된다. 여기서는 메모리 저장소를 완전히 삭제해서 다음 테스트에 영향을 주지 않도록 초기화 한다.






출처:https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2

profile
자바를 사랑합니다

0개의 댓글

관련 채용 정보