해당 저장소의 README.md는 인프런 김영한님의 Spring Boot 강의 시리즈를 듣고 Spring 프레임워크의 방대한 기술들을 복기하고자 공부한 내용을 가볍게 정리한 것입니다. 문제가 될 시 삭제하겠습니다.
- 섹션 1 | 데이터 접근 기술 - 시작
실무에서 주로 사용하는 다음과 같은 다양한 데이터 접근 기술들을 학습하게 된다.
SQL Mapper 주요 기능
- 개발자는 SQL만 작성하면 해당 SQL의 결과를 개겣로 편리하게 매핑해준다.
- JDBC를 직접 사용할 때 발생하는 여러가지 중복을 제거해주고, 기타 개발자에게 여러가지 편리한 기능을 제공한다.
- 예: JdbcTemplate, MyBatis
ORM 주요 기능
- JdbcTemplate이나 MyBatis 같은 SQL 매퍼 기술은 SQL을 개발자가 직접 작성해야 하지만, JPA를 사용하면 기본적인 SQL은 JPA가 대신 작성하고 처리해준다.
- 개발자는 저장하고 싶은 객체를 마치 자바 컬렉션에 저장하고 조회하듯이 사용하면 ORM 기술이 데이터베이스에 해당 객체를 저장하고 조회해준다.
- 스프링 데이터 JPA, Querydsl은 JPA를 더 편리하게 사용할 수 있게 도와주는 프로젝트들이다.
- 예: JPA, Hibernate, 스프링 데이터 JPA, Querydsl
학습에서 나온 내용을 모두 요약하기 보다는 새로 배우는 내용에 대해서만 정리를 하려고 한다.
[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는 기능은 없고 데이터 전달을 하는 용도로 사용되는 객체를 뜻한다.
- DTO에 기능이 없으면 안되는 것은 아니지만, 객체의 주 목적이 데이터를 전송하는 것이라면 DTO라고 할 수 있다.
- 또한 DTO를 뒤에 붙여준다면 용도를 알 수 있다는 장점이 있다.
스프링 부트에 설정된 내용을 분석한다.
[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();
}
}[TestDataInit]
- 애플리케이션을 실행할 때 초기 데이터를 저장한다.
@EventListner(ApplicationReadyEvent.class): 스프링 컨테이너가 완전히 초기화를 다 끝내고 실행 준비가 되었을 때 발생하는 이벤트이다.@PostConstruct를 사용할 경우 AOP 같은 부분이 아직 다 처리되지 않은 시점에 호출될 수 있기 때문에 문제가 발생할 수 있다. 예를 들어@Transactional과 관련된 AOP가 적용되지 않은 상태로 호출될 수 있다.
package hello.itemservice;
import hello.itemservice.domain.Item;
import hello.itemservice.repository.ItemRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
/**
* 애플리케이션을 실행할 때 초기 데이터를 저장
*/
@Slf4j
@RequiredArgsConstructor
public class TestDataInit {
private final ItemRepository itemRepository;
/**
* 확인용 초기 데이터 추가
* @EventListener: 스프링 컨테이너가 완전히 초기화를 끝내고, 실행 준비가 되었을 때 발생하는 이벤트
* - 스프링이 이 시점에 해당 애노테이션이 붙은 메서드를 실행한다.
* - 참고로 @PostConstruct를 사용할 경우 AOP 같은 부분이 아직 다 처리되지 않은 시점에 호출될 수 있기 때문에, 간혹 문제가 될 수 있다.
* - 해당 애노테이션은 AOP를 포함한 스프링 컨테이너가 완전히 초기화 된 이후에 호출되기 때문에 이런 문제가 발생하지 않는다.
*/
@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));
}
}[ItemServiceApplication]
@Import(MemoryConfig.class):MemoryConfig파일을 설정 파일로 사용한다.@SpringBootApplication(scanBasePackages = "hello.itemservice.web"): 컴포넌트 스캔으로 범위를 지정한다. 나머지는 수동 빈으로 등록한다.@Profile("local"): 특정 프로필의 경우에만 해당 스프링 빈을 등록한다.- 여기서는 local로 설정되어 있으므로 스프링에서 프로필을 local로 설정했을 때만
testDataInit이라는 스프링 빈을 등록한다.
- 여기서는 local로 설정되어 있으므로 스프링에서 프로필을 local로 설정했을 때만
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) // 앞서 설정한 MemoneyConfig를 설정 파일로 사용
@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);
}
}스프링은 로딩 시점에 application.properties의 spring.profiles.active 속성을 읽어서 프로필로 사용한다.
이 프로필은 로컬, 운영 환경, 테스트 실행 등등 다양한 환경에 따라 다른 설정을 할 때 사용하는 정보이다.
- 로컬, 테스트 환경, 운영 환경에서 다른 설정 정보에 접근하거나,
- 환경에 따라 다른 스프링 빈을 등록하고 싶을 때 사용하면 된다.
프로필에는 크게 2가지로 나뉜다.
- main 프로필 -
/src/main/resources하위의application.properties- 이 위치의
application.properties는/src/main하위의 자바 객체를 실행할 때(주로main()) 동작하는 스프링 설정이다. spring.profiles.active=local이라고 설정하면 스프링은 local이라는 프로필로 동작하므로, 위의 테스트 코드에서@Profile("local")이 동작하고,testDataInit이 스프링 빈으로 등록된다.
- 이 위치의
- test 프로필 -
/src/test/resources하위의application.properties- 이 위치의
application.properties는/src/test하위의 자바 객체를 실행할 때 동작하는 스프링 설정이다. - 주로 테스트 케이스를 실행할 때 동작한다.
- 이 경우
@Profile("local")프로필 정보가 맞지 않으므로 동작하지 않는다. 따라서testDataInit이 스프링 빈으로 등록되지 않는다.
- 이 위치의
결국 프로필 덕분에 로컬에서는 초기 데이터를 자동으로 추가해서 쉽게 확인해볼 수 있고, 테스트에서는 초기 데이터를 추가하지 않아 정확한 테스트가 유연하게 가능하다.
[ItemRepositoryTest]
afterEach()- 인터페이스에는
clearStore()가 없기 때문에MemoryItemRepository인 경우에만 다운 캐스팅을 해서 데이터를 초기화한다.
- 인터페이스에는
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 lombok.extern.slf4j.Slf4j;
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 org.springframework.context.ApplicationContext;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@Slf4j
@SpringBootTest
class ItemRepositoryTest {
@Autowired
ItemRepository itemRepository;
@Autowired
ApplicationContext applicationContext;
/**
* 테스트는 서로 영향을 주면 안되므로, 각각의 테스트가 끝나고 나면 저장한 데이터를 제거해야 한다.
* 인터페이스에는 clearStore()가 없기 때문에 MemoryItemRepository인 경우에만 다운 캐스팅을 해서 데이터를 초기화 한다.
*/
@AfterEach
void afterEach() {
//MemoryItemRepository 의 경우 제한적으로 사용
if (itemRepository instanceof MemoryItemRepository) {
((MemoryItemRepository) itemRepository).clearStore();
}
}
@Test
void applicationContext() {
String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
log.info("beanDefinitionName = {}", beanDefinitionName);
}
}
@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); // 해당 아이템들의 포함 여부 뿐만 아니라 순서까지 체크한다.
}
}여기서는 MemoryItemRepository 구현체를 테스트 하는 것이 아니라 ItemRepository 인터페이스를 테스트하는 것을 확인할 수 있다. 인터페이스를 대상으로 테스트하면 향후 다른 구현체로 변경되었을 때 해당 구현체가 잘 동작하는지 같은 테스트로 편리하게 검증할 수 있다.
이제부터 다양한 데이터 접근 기술을 활용해 메모리가 아닌 데이터베이스에 데이터를 보관하는 방법을 알아보자
실습을 위해 H2 데이터베이스에 item 테이블을 생성과 데이터를 넣자
generated by default as identityidentity전략이라고 하는데, 기본 키 생성을 데이터베이스에 위임하는 방법이다.- MySQL의 Auto Increment와 같은 방법이다.
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)
);
insert into item(item_name, price, quantity) values ('ItemTest', 10000, 10);데이터베이스의 기본 키는 다음 3가지 조건을 모두 만족해야 한다.
null값은 허용하지 않는다.- 유일해야 한다.
- 변해선 안 된다.
테이블의 기본 키를 선택하는 전략은 크게 2가지가 있다.
- 자연 키
- 비즈니스에 의미가 있는 키
- 예: 주민등록번호, 이메일, 전화번호
- 대리 키
- 비즈니스와 관련 없는 임의로 만들어진 키, 대체 키로도 불린다.
- 예: 오라클 시퀀스, auto_increment, identity, 키 생성 테이블 사용
현실과 비즈니스 규칙은 생각보다 자주 변경되기 때문에 현재와 미래에도 변경이 없을 자연 키 보다는 대리 키를 권장한다.