728x90
Build.gradle
plugins {
id 'org.springframework.boot' version '2.6.5'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
//테스트에서 lombok 사용
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
}
tasks.named('test') {
useJUnitPlatform()
}
- 아직 메모리 기반이라, 특정 기술이 적용되어 있지 않다.
- spring-boot-starter-thymeleaf : 타임리프 사용
- spring-boot-starter-web : 스프링 웹, MVC 기능 사용
- spring-boot-starter-test : 스프링이 제공하는 테스트 기능
- lombok : lombok을 추가로 테스트에서도 사용하는 설정 주의
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;
}
}
- Item 은 상품 자체를 나타내는 객체이다. 이름, 가격, 수량을 속성으로 가지고 있다.
ItemRepository
public interface ItemRepository {
Item save(Item item);
void update(Long itemId, ItemUpdateDto updateParam);
Optional<Item> findById(Long id);
List<Item> findAll(ItemSearchCond cond);
}
- findAll() 같은 경우 검색조건이 넘어간다.
- 메모리 구현체에서 향후 다양한 데이터 접근 기술 구현체로 손쉽게 변경하기 위해 리포지토리에 인터페이스를 도입했다.
- 인페이스이므로, 다른 구현체로 구현이 가능하다(JPA, MyBatis...)
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;
}
}
- 검색 조건으로 사용된다. 상품명, 최대 가격이 있다. 참고로 상품명의 일부만 포함되어도 검색이 가능해야 한다. ( like 검색)
- 해당 프로젝트마다 네이밍 규칙을 따르면 된다 (Condition -> Cond로 쓴 거처럼)
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란?
- 데이터 전송 객체
- DTO는 기능은 없고 데이터를 전달만 하는 용도로 사용되는 객체를 뜻한다.
- 참고로 DTO에 기능이 있으면 안되는가? 그것은 아니다. 객체의 주목적이 데이터를 전송하는 것이라면 DTO라 할 수 있다.
- 객체 이름에 DTO를 꼭 붙여야 하는 것은 아니다. 대신 붙여두면 용도를 알 수 있다는 장점은 있다.
- 이전에 설명한 ItemSearchCond 도 DTO 역할을 하지만, 이 프로젝트에서 Cond는 검색 조건으로 사용한다는 규칙을 정했다. 따라서 DTO를 붙이지 않아도 된다. ItemSearchCondDto 이렇게 하면 너무 복잡해진다. 그리고 Cond라는 것만 봐도 용도를 알 수 있다.
- 참고로 이런 규칙은 정해진 것이 없기 때문에 해당 프로젝트 안에서 일관성 있게 규칙을 정하면 된다
MemoryItemRepository
@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();
}
}
- ItemRepository 인터페이스를 구현한 메모리 저장소이다.
- 메모리이기 때문에 자바를 다시 실행하면 기존에 저장된 데이터가 모두 사라진다.
- save , update , findById 는 쉽게 이해할 수 있을 것이다. 참고로 findById는 Optional을 반환해야 하기 때문에 Optional.ofNullable을 사용했다.
- findAll 은 ItemSearchCond 이라는 검색 조건을 받아서 내부에서 데이터를 검색하는 기능을 한다. 데이터베이스로 보면 where 구문을 사용해서 필요한 데이터를 필터링하는 과정을 거치는 것이다.
- 여기서 자바 스트림을 사용한다.
- itemName 이나, maxPrice 가 null 이거나 비었으면 해당 조건을 무시한다.
- itemName 이나, maxPrice에 값이 있을 때만 해당 조건으로 필터링 기능을 수행한다.
- clearStore() 메모리에 저장된 Item 을 모두 삭제해서 초기화한다. 테스트 용도로만 사용한다
ItemService, ItemServiceV1
public interface ItemService {
Item save(Item item);
void update(Long itemId, ItemUpdateDto updateParam);
Optional<Item> findById(Long id);
List<Item> findItems(ItemSearchCond itemSearch);
}
@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
@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}";
}
}
DTO 위치는 어디에 있는게 맞는 걸까?
- 별도의 패키지에 두어서 관리를 해도 되지만 서비스에 두거나 하면 안 된다.
- Why?
- Service가 Repository를 호출하고, 최종적으로 Repository에서 Dto를 사용하게 된다.
- 의존관계상 Repository에 종속적으로 두는 것이 맞다.
- 만약 서비스에 있으면, DTO를 수정 시 서비스 패키지를 수정해야 되기 때문에 의존성이 서비스 패키지에 생기므로 좋지 않은 설계이다.
728x90
'스프링 DB 2편(데이터 접근 활용 기술)' 카테고리의 다른 글
| Ch02. 스프링 JdbcTemplate - JdbcTemplate 적용 (0) | 2022.06.25 |
|---|---|
| Ch02. 스프링 JdbcTemplate - JdbcTemplate 소개와 설정 (0) | 2022.06.25 |
| Ch01. 시작 - 데이터베이스 테이블 생성 (0) | 2022.06.23 |
| Ch01. 시작 - 프로젝트 구조 설명(테스트) (0) | 2022.06.23 |
| Ch01. 시작 - 프로젝트 구조 설명(설정) (0) | 2022.06.23 |