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

Ch01. 시작 - 프로젝트 구조 설명(기본)

webmaster 2022. 6. 23. 10:54
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