앞으로 실무에서 주로 사용하는 다음과 같은 다양한 데이터 접근 기술들을 학습해보겠습니다.
SQLMapper
ORM 관련 기술
SQL Mapper 주요기능
ORM 주요 기능
스프링 MVC 1편에서 만든 프로젝트는 단순히 메모리에 상품 데이터를 저장하도록 했었다. 이 프로젝트를 메모리가 아닌 실제 데이터 접근 기술들을 하나씩 적용해가면서 실습해보겠습니다.
도메인 분석
Item
package hello.itemservice.domain;
import lombok.Data;
@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;
}
}
ItemSearchCond
package hello.itemservice.repository;
import lombok.Data;
@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
package hello.itemservice.repository;
import lombok.Data;
@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(Data Transfer Object)
MemoryItemRepository
package hello.itemservice.repository.memory;
import hello.itemservice.domain.Item;
import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.ItemSearchCond;
import hello.itemservice.repository.ItemUpdateDto;
import org.springframework.stereotype.Repository;
import org.springframework.util.ObjectUtils;
import java.util.*;
import java.util.stream.Collectors;
@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.getItemName().contains(itemName);
}).filter(item -> {
if (maxPrice == null) {
return true;
}
return item.getPrice() <= maxPrice;
})
.collect(Collectors.toList());
}
public void clearStore() {
store.clear();
}
}
서비스 분석
ItemService 인터페이스
package hello.itemservice.service;
import hello.itemservice.domain.Item;
import hello.itemservice.repository.ItemSearchCond;
import hello.itemservice.repository.ItemUpdateDto;
import java.util.List;
import java.util.Optional;
public interface ItemService {
Item save(Item item);
void update(Long itemId, ItemUpdateDto updateParam);
Optional<Item> findById(Long id);
List<Item> findItems(ItemSearchCond itemSearch);
}
ItemServiceV1
package hello.itemservice.service;
import hello.itemservice.domain.Item;
import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.ItemSearchCond;
import hello.itemservice.repository.ItemUpdateDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@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);
}
}
컨트롤러
ItemController
package hello.itemservice.web;
import hello.itemservice.domain.Item;
import hello.itemservice.repository.ItemSearchCond;
import hello.itemservice.repository.ItemUpdateDto;
import hello.itemservice.service.ItemService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.util.List;
@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
package hello.itemservice.config;
import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.memory.MemoryItemRepository;
import hello.itemservice.service.ItemService;
import hello.itemservice.service.ItemServiceV1;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MemoryConfig {
@Bean
public ItemService itemService() {
return new ItemServiceV1(itemRepository());
}
@Bean
public ItemRepository itemRepository() {
return new MemoryItemRepository();
}
}
ItemServiceV1
, MemoryItemRepository
를 스프링 빈으로 등록하고 생성자를 통해 의존관계를 주입한다.ItemServiceApplication
package hello.itemservice;
import hello.itemservice.config.*;
import hello.itemservice.repository.ItemRepository;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Profile;
@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
하위로 지정했다.프로필
application.properties
의 spring.profiles.active
속성을 읽어서 프로필로 사용한다.application.properties
spring.profiles.active=local
스프링 프로필에 대한 내용은 나중에 더 자세히 알아보겠습니다.
ItemRepositoryTest
package hello.itemservice.domain;
import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.ItemSearchCond;
import hello.itemservice.repository.ItemUpdateDto;
import hello.itemservice.repository.memory.MemoryItemRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@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);
}
}
save()
updateItem()
findItems()
이제부터 다양한 데이터 접근 기술들을 활용해서 메모리가 아닌 데이터베이스에 데이터를 보관하는 방법을
알아보자!
create table item
(
id bigint generated by default as identity,
item_name varchar(10),
price integer,
quantity integer,
primary key (id)
);
generated by default as identity
= Mysql에 Auto Increment와 같은 방법이다. 데이터베이스가 순서대로 증가하는 값을 사용해서 넣어준다.참고
김영한: 스프링 DB 2편 - 데이터 접근 활용 기술(인프런)
Github - https://github.com/b2b2004/Spring_DB