김영한님의 스프링 DB 2편 - 데이터 접근 활용 기술을 듣고 정리한 내용입니다.
데이터 접근기술
SQLMapper
- JdbcTemplate
- MyBatis
개발자는 SQL만 작성하면 해당 SQL의 결과를 객체로 편리하게 매핑해준다.
JDBC를 직접 사용할 때 발생하는 여러가지 중복을 제거해주고, 기타 개발자에게 여러가지 편리한 기능을 제공한다.
ORM 관련기술
- JPA, Hibernate
- 스프링데이터 JPA
- Querydsl
JdbcTemplate이나 MyBatis 같은 SQL 매퍼 기술은 SQL을 개발자가 직접 작성해야 하지만,
JPA를 사용하면 기본적인 SQL은 JPA가 대신 작성하고 처리해준다
도메인분석
Item
@Data
@Entity
public class Item {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "item_name", length = 10)
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);
}
다양한 데이터 접근 기술 구현체로 손쉽게 변경하기 위해 리포지토리에 인터페이스를 도입
@Data
public class ItemSearchCond {
private String itemName;
private Integer maxPrice;
public ItemSearchCond() {
}
public ItemSearchCond(String itemName, Integer maxPrice) {
this.itemName = itemName;
this.maxPrice = maxPrice;
}
}
검색조거능로 사용됨.
상품명 최대가격이있다.
상품명의 일부만 포함되어도 검색되게끔
@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
@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();
}
}
ItemRepositoiry 구현한 메모리 저장소
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);
}
}
대부분의 기능 리포지토리에 위임
@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}";
}
}
상품을 CRUD하는 컨트롤러
@Configuration
public class MemoryConfig {
@Bean
public ItemService itemService() {
return new ItemServiceV1(itemRepository());
}
@Bean
public ItemRepository itemRepository() {
return new MemoryItemRepository();
}
}
ItemServiceV1, MemoryItemRepository 를 스프링 빈으로 등록하고 생성자를 통해 의존관계 주입
@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) : 스프링 컨테이너가 완전히 초기화를 다 끝내고,
실행 준비가 되었을 때 발생하는 이벤트이다
참고로 이 기능 대신 @PostConstruct 를 사용할 경우
AOP 같은 부분이 아직 다 처리되지 않은 시점에 호출될 수 있기 때문에, 간혹 문제가 발생할 수 있다
@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 를 설정 파일로 사용한다.
@Profile("local") : 특정 프로필의 경우에만 해당 스프링 빈을 등록한다. 여기서는 local 이라는
이름의 프로필이 사용되는 경우에만 testDataInit 이라는 스프링 빈을 등록한다
/src/main/resources/application.properties
spring.profiles.active=local
메인 프로필 설정
스프링은 local 이라는프로필로 동작한다
프로필을 지정하지 않으면 디폴트( default ) 프로필이 실행된다
/src/main/resources/application.properties
spring.profiles.active=test
/src/test 하위의 자바 객체를 실행할 때 동작하는 스프링 설정
테스트코드
@Slf4j
@Transactional
@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);
log.info("repository={}", itemRepository.getClass());
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 : 테스트는 서로 영향을 주면 안된다. 따라서 각각의 테스트가 끝나고 나면 저장한 데이터를 제거해야 한다
테스트의 실행이 끝나는 시점에 호출된다
MemoryItemRepository 인 경우에만 다운 케스팅을 해서 데이터를 초기화한다
DB 테이블 생성
drop table if exists item CASCADE;
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
identity 전략이고 하는데, 기본 키 생성을 데이터베이스에 위임하는 방법이다
MySQL의 Auto Increment와 같은 방법이다
권장하는 식별자 선택 전략
1. null값은 허용하지 않는다
2. 유일해야한다
3. 변해서는 안된다
기본키 선택 전략
1. 자연키
비즈니스에 의미가 있는 키
2. 대리키
비즈니스와 관련 없는 임의로 만들어진 키, 대체 키
자연키보다는 대리키를 권장한다
비즈니스 환경은 언젠가 변한다
비즈니스의 요구사항은 계속해서 변하기 떄문에 테이블은 한번 정하면 변경이 어렵기 때문에
외부 환경에 쉽게 변하지 않는 대리키가 좋은 선택이라고 한다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2/dashboard
스프링 DB 2편 - 데이터 접근 활용 기술 - 인프런 | 강의
백엔드 개발에 필요한 DB 데이터 접근 기술을 활용하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., 백엔드 개발자
www.inflearn.com
'스터디 > 2023_스프링부트' 카테고리의 다른 글
[study] 스프링 DB 2편 - 3. 데이터 접근 기술 - 테스트 (0) | 2023.08.28 |
---|---|
[study] 스프링 DB 2편 - 2. JdbcTemplate (0) | 2023.08.28 |
[study] 스프링 DB 1편 - 6. 스프링과 문제 해결 - 예외 처리, 반복 (0) | 2023.08.21 |
[study] 스프링 DB 1편 - 5. 자바 예외 이해 (0) | 2023.08.21 |
[study] 스프링 DB 1편 - 4. 스프링과 문제 해결 - 트랜잭션 (0) | 2023.08.21 |