적용 데이터 접근 기술
SQL Mapper
SqL Mapper 주요 기능
ORM 주요기능
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 전송객체
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
로 오류가 안나게 할 수 있다
findAll
은 ItemSearchCond
이라는 검색 조건을 받아서 내부에서 데이터를 검색하는 기능 수행
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
로 이동하는 컨트롤러 @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.properties
의 spring.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