일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 스프링 mvc2 - 로그인 처리
- 자바의 정석 기초편 ch2
- 타임리프 - 기본기능
- 자바의 정석 기초편 ch1
- 2024 정보처리기사 수제비 실기
- 자바의 정석 기초편 ch14
- 스프링 mvc1 - 스프링 mvc
- 자바의 정석 기초편 ch7
- 자바의 정석 기초편 ch13
- 스프링 mvc1 - 서블릿
- 자바의 정석 기초편 ch4
- 자바의 정석 기초편 ch9
- 게시글 목록 api
- 스프링 입문(무료)
- 자바의 정석 기초편 ch3
- 자바의 정석 기초편 ch12
- 스프링 db1 - 스프링과 문제 해결
- 자바의 정석 기초편 ch5
- 자바의 정석 기초편 ch11
- 2024 정보처리기사 시나공 필기
- 스프링 고급 - 스프링 aop
- 스프링 db2 - 데이터 접근 기술
- 자바의 정석 기초편 ch8
- 스프링 mvc2 - 검증
- 코드로 시작하는 자바 첫걸음
- @Aspect
- jpa - 객체지향 쿼리 언어
- 자바의 정석 기초편 ch6
- 스프링 mvc2 - 타임리프
- jpa 활용2 - api 개발 고급
- Today
- Total
나구리의 개발공부기록
데이터 접근 기술 시작, 소개, 프로젝트 설정과 메모리 저장소, 프로젝트 구조 설명(기본/설정/테스트), 데이터베이스 테이블 생성 본문
데이터 접근 기술 시작, 소개, 프로젝트 설정과 메모리 저장소, 프로젝트 구조 설명(기본/설정/테스트), 데이터베이스 테이블 생성
소소한나구리 2024. 9. 18. 15:37 출처 : 인프런 - 스프링 DB 2편 데이터 접근 핵심 원리 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2
1. 데이터 접근 기술 진행방식 소개
1) 적용 데이터 접근기술들
(1) SQL Mapper
- JdbcTemplate
- MyBatis
(2) ORM 관련 기술
- JPA, Hibernate
- 스프링 데이터 JPA
- Querydsl
2) 주요 기능 설명
(1) SQL Mapper 주요 기능
- 개발자는 SQL만 작성하면 해당 SQL의 결과를 객체로 편리하게 매핑해줌
- JDBC를 직접 사용할 때 발생하는 여러가지 중복을 제거해주고 기타 개발자에게 여러가지 편리한 기능을 제공함
(2) ORM 주요기능
- 기본적인 SQL은 JPA가 대신 작성하고 처리
- 개발자는 저장하고 싶은 객체를 자바 컬렉션에 저장하고 조회하듯이 사용하면ㄴ ORM 기술이 데이터베이스에 해당 객체를 저장하고 조회해줌
- JPA는 자바 진영의 ORM 표준이고 Hibernate(하이버네이트)는 JPA에서 가장 많이 사용하는 구현체임(자바에서 ORM을 사용할 때는 JPA 인터페이스를 사용하고 그 구현체를 하이버네이트를 사용한다고 생각하면 됨)
- 스프링 데이터 JPA, Querydsl은 JPA을 더 편리하게 사용할 수 있게 도와주는 프로젝트이며 실무에서는 JPA를 사용하면 필수로 함께 사용하는 것이 좋음
2. 프로젝트 설정과 메모리 저장소
- 강의에서 제공된 itemservice-db-start을 복사 후 itemservice-db로 바꿔서 진행
** 프로젝트 수정사항
- 자바 17로 변경
- 스프링 부트 버전 3.3.3 으로 변경
- 스프링 dependency-management 버전 1.1.6 으로 변경
- gradle-wrapper.properties에서 gradle 버전 7.5로 변경
프로젝트 설정
plugins {
id 'org.springframework.boot' version '3.3.3'
id 'io.spring.dependency-management' version '1.1.6'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
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()
}
3. 프로젝트 구조 설명 - 기본
1) 도메인 분석
(1) Item
- 상품 자체를 나타내는 객체
- 이름, 가격, 수량을 속성으로 가지고 있음
package hello.itemservice.domain;
@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;
}
}
2) 리포지토리 분석
(1) ItemRepository 인터페이스
- 인터페이스로 구현되어있기 때문에 지금은 메모리로 db가 구현되어있지만 향후 다양한 데이터 접근 기술 구현체로 손쉽게 변경이 가능함
- findAll()에 검색 조건을 파라미터로 가짐
package hello.itemservice.repository;
public interface ItemRepository {
Item save(Item item);
void update(Long itemId, ItemUpdateDto updateParam);
Optional<Item> findById(Long id);
List<Item> findAll(ItemSearchCond cond);
}
(2) ItemSerchCond
- 검색조건으로 사용되며 상품명, 최대 가격이 있으며 상품명의 일부만 포함되어도 검색이 가능해야 함(like 검색 활용)
- cond -> condition을 줄여서 사용, 실제 프로젝트 시 이런 명칭은 팀원들끼리 합의하면 됨(일관성을 가져가면 됨)
package hello.itemservice.repository;
@Data
public class ItemSearchCond {
private String itemName;
private Integer maxPrice;
public ItemSearchCond() {}
public ItemSearchCond(String itemName, Integer maxPrice) {
this.itemName = itemName;
this.maxPrice = maxPrice;
}
}
(3) ItemUpdateDto
- 상품을 수정할 때 사용하는 객체
- 단순히 데이터를 전달하는 용도로 사용되므로 DTO를 뒤에 붙힘
** DTO(Data Tranfer Object)
- 데이터 전송 객체
- DTO는 보통 기능은 없고 데이터를 전달만 하는 용도로 사용되는 객체를 뜻하며, DTO에 기능이 없어야만 하는 것은 아니고 객체의 주 목적이 데이터를 전송하는 것이라면 DTO라고 할 수 있음
- 객체 이름에 DTO를 꼭 붙혀야 하는 것은 아니지만 붙혀두면 용도를 알 수 있다는 장점이 있음
- ItemSerchCond도 DTO 역할을 하지만 현재 프로젝트에서 Cond는 검색조건으로 사용한다는 규칙을 정했고 Cond라는 것만 봐도 용도를 알수있고 DTO의 범위를 포함하고 있기 때문에 DTO를 붙히지 않았음
- 이런 규칙은 정해진 것이 없으므로 프로젝트의 규모나 상황에 맞춰서 팀원과 일관성있게 규칙을 정하면 됨
package hello.itemservice.repository;
@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;
}
}
(4) memory/MemoryItemRepository
- ItemRepository 인터페이스를 구현한 메모리 저장소이며 메모리이기 때문에 자바를 다시 실행하면 기존에 저장된 데이터가 모두 사라짐
- findById는 Optional을 반환해야 하기 때문에 Optional.ofNullable을 사용
- findAll은 ItemSerchCond라는 검색 조건을 받아서 내부에서 데이터를 검색하는 기능을 함(where 구문을 사용해서 필요한 데이터를 필터링 하는 과정을 거치는 것)
- 자바 스트림을 사용해서 .filter로 ItemName이나 maxPrice가 null이거나 비었으면 해당 조건을 무시하고, 값이 있으면 해당 조건으로 필터링한 기능을 수행 후 List로 반환
- clearStroe() 는 메모리에 저장된 Item을 모두 삭제해서 초기화 -> 테스트 용도로 사용하기 위해 만듦
package hello.itemservice.repository.memory;
@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();
}
}
3)서비스 분석
(1) ItemService 인터페이스
- 서비스의 구현체를 쉽게 변경하기 위해 인터페이스 사용
- 서비스는 구현체를 변경할 일이 많지는 않기 때문에 사실 서비스에 인터페이스를 잘 도입하지는 않지만 여기서는 예제 설명 과정에서 구현체를 변경할 예정이여서 인터페이스를 도입함
package hello.itemservice.service;
public interface ItemService {
Item save(Item item);
void update(Long itemId, ItemUpdateDto updateParam);
Optional<Item> findById(Long id);
List<Item> findItems(ItemSearchCond itemSearch);
}
(2) ItemServiceV1
- V1 버전의 서비스 구현체, 기능은 저장, 수정 조회, 전체조회
package hello.itemservice.service;
@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);
}
}
4) 컨트롤러 분석
(1) HomeController
- 단순히 홈으로 요청이 왔을 때 items로 이동하는 컨트롤러
package hello.itemservice.web;
@Controller
@RequiredArgsConstructor
public class HomeController {
@RequestMapping("/")
public String home() {
return "redirect:/items";
}
}
(2) ItemController
- 상품을 CRUD하는 컨트롤러 - 자세한 내용은 MVC1편 내용을 참고해야함
package hello.itemservice.web;
@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의 위치에 대한 의견
- 지금 작성된 아키텍처 구조에서는 별도의 DTO 패키지를 만들어서 두어도 되지만 service 패키지에 두는 것은 안됨
- 계층상 ItemService는 ItemRepository를 호출하는데, ItemRepository에서 ItemSerchCond와 ItemUpdateDto를 구현하였고, 이를 사용한 결과를 service에서 사용하고 있음
- ItemRepository를 사용할 때 해당 구현체들은 무조건 필요하므로 의존관계상 지금 패키지 구조에서는 repository에 두는 것이 맞음
- 만약 ItemUpdateDto가 서비스에 있으면 리포지토리가 반대로 서비스를 참조해야하는 의존관계 순환이 꼬이는 순환참조가 일어나게 됨
- 물론 서비스에서 사용이 끝나고 리포지토리에 넘겨주지 않는다거나, 검색조건에만쓰고 리포지토리에 넘겨주지 않는경우에는 DTO가 서비스나 컨트롤러에서 가지고 있는 경우도 있음
- 전체 흐름이 컨트롤러 -> 서비스 -> 리포지토리로 흐른다고 봤을 때 DTO를 제공하는 마지막 단이 어디인가를 파악하고 해당 위치에 두면 되며, 여러 군데에서 사용하거나 애매한 경우 별도의 패키지에 DTO를 두면 됨
4. 프로젝트 구조 설명 - 설정
1) 스프링 부트 설정 분석
(1) config/MemoryConfig
- ItemServiceV1, MemoryItemRepository를 스프링 빈으로 등록하고 생성자를 통해 의존관계를 주입했는데, 여기서는 서비스와 리포지토리의 구현체를 편리하게 변경하기 위해서 수동으로 빈을 등록했음
- 컨트롤러는 컴포넌트 스캔을 사용
package hello.itemservice.config;
@Configuration
public class MemoryConfig {
@Bean
public ItemService itemService() {
return new ItemServiceV1(itemRepository());
}
@Bean
public ItemRepository itemRepository() {
return new MemoryItemRepository();
}
}
(2) TestDataInit
- 애플리케이션 실행할 때 초기 데이터를 저장하기 위한용도
- 지금은 메모리로 DB를 구현했기 때문에 해당 기능이 없으면 서버를 실행할 때마다 데이터를 입력해야 리스트가 나타나기 때문에 리스트에 데이터가 잘 나오는지 편리하게 확인하는 용도로 사용
- @EventListener(ApplicationReadyEvent.class) : 스프링 컨테이너가 완전히 초기화를 다 끝내고 실행 준비가 되었을 때(AOP를 포함한 스프링 컨테이너가 완전히 초기화된 이후)발생하는 이벤트이며, 이벤트가 발생하면 해당 애노테이션이 붙은 메서드를 호출 하기 때문에 아래와 같은 문제가 발생되지 않음
- @PostConstruct를 대신 사용할 수 있으니 AOP 같은 부분이 아직 다 처리되지 않은 시점에 호출 될 수 있기 때문에 간혹 @Transactional과 관련된 AOP가 적용되지 않는 상태로 호출 될 수 있는 등의 타이밍과 관련된 문제가 발생될 수 있음
package hello.itemservice;
@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));
}
}
(3) ItemServiceAplication
- @Import(MemoryConfig.class) : 앞서 설정한 MemoryConfig를 설정 파일로 사용함
- scanBasePackages = "hello.itemservice.web") : 여기서는 컨트롤로만 컴포넌트 스캔을 사용하고 나머지는 직접 수동 등록하기 위해서 컴포넌트스캔 경로를 ~web/ 하위 경로로 지정함
- @Profile("local"): local이라는 이름의 프로필이 사용되는 경우에만 스프링빈을 등록(특정 프로필을 스프링 빈으로 등록), 편의상 초기 데이터를 만들어서 저장하는 TestDataInit을 스프링 빈으로 등록
package hello.itemservice;
@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);
}
}
2) 프로필
(1) 프로필 사용
- 스프링은 로딩 시점에 application.properties의 spring.profiles.active 속성을 읽어서 프로필로 사용함
- 프로필은 로컬(나의 PC), 운영 환경, 테스트 실행 등의 다양한 환경에 따라 다른 설정을 해야할 때 사용되는 정보임
- 예를 들어 로컬 PC에서는 로컬 PC에 설치된 DB에 접근해야하고, 운영 환경에서는 운영 데이터베이스에서 접근해야 할 때, 환경에 따라서 다른 스프링 빈을 등록해야 할 때 프로필을 사용하면 깔끔하게 해결할 수 있음
(2) main 프로필
- ~main/resources 하위의 application.properties
spring.profiles.active=local
- main 하위의 자바 객체를 실행할 때 (주로 main()) 동작하는 스프링 설정
- spring.profiles.active=local이라고 하면 스프링은 local이라는 프로필로 동작하고, ItemServiceApplication에 작성된 @Profilce("local") 애노테이션이 동작하고, TestDataInit이 스프링 빈으로 등록되어 작성해둔 데이터들이 등록됨
- 프로필을 등록 후 실행하면 The following 1 profile is active: "local" 이라는 로그를 확인할 수 있고, 만약 프로필이 없다면 No active profile set, falling back to 1 default profile: "default" 이라는 로그를 확인할 수 있음(디폴트 프로필이 실행 됨)
(3) test 프로필
- ~test/resources 하위의 application.properties
spring.profiles.active=test
- test 하위의 자바 객체를 실행할 때 동작하는 스프링 설정이며 주로 테스트 케이스를 실행할 때 동작함
- 테스트 하위의 객체를 실행해보면 The following 1 profile is active: "test" 로그를 확인할 수 있듯이 스프링은 test라는 프로필로 동작하며, 이 경우에는 @Profile("local") 정보가 맞지 않아서 동작하지 않고 따라서 TestDataInIt도 빈으로 등록되지 않으므로 데이터도 추가하지 않음
- 초기화 데이터는 편리한 점도 있지만 테스트 케이스를 실행할 때는 문제가 될 수 있는데, 데이터를 하나 저장하고 전체 카운트를 확인하는데 테스트 데이터때문에 데이터가 추가되어서 결과가 1이아닌 3이 되는 문제가 될 수 있기 때문에 테스트에는 초기 데이터를 제외 해야 함
- 이렇게 설정을 다르게 할때 프로필을 활용하면 local 프로필이 등록된 main 애플리케이션을 실행할 때는 테스트 데이터가 등록되어 편리하게 확인할 수 있고, 테스트 때는 test 프로필이 실행되어 테스트 데이터 없이 테스트 환경을 구성하는 설정을 쉽게 해결할 수 있음
** 참고
- 프로필에 대한 스프링 부트 공식 메뉴얼
- https://docs.spring.io/spring-boot/redirect.html?page=features#features.profiles
- 프로필에 대한 더 자세한 내용은 스프링 부트 강의에서 더 자세히 다룰 예정
5. 프로젝트 구조 설명 - 테스트
(1) afterEach()
- 테스트는 서로 영향을 주면 안되므로 각각의 테스트 가 끝나고 나면 저장한 데이터를 제거하기 위해서 @AfterEach 애노테이션으로 테스트의 실행이 끝나는 시점에 afterEach()메서드가 호출되게 하여 데이터를 초기화하도록 작성
- ItemRepository 인터페이스에는 clearStore()가 없기 때문에 MemoryItemRepository인 경우에만 다운 캐스팅을 해서 데이터를 초기화하도록 설정하고, 실제 DB를 사용하는 경우에는 테스트가 끝난 후에 트랜잭션을 롤백해서 데이터 초기화를 진행
(2) save(), updateItem(), findItems()
- 상품 저장, 수정, 찾기를 검증하는 테스트 진행
- findItems()의 경우에는 검색 조건이 null, 빈문자("")인 경우에도 잘 동작하는지 검증
(3) 인터페이스로 테스트
- 테스트 코드를 보면 지금 사용하고 있는 MemoryItemRepository 구현체를 테스트하는 것이 아니라 ItemRepository 인터페이스로 테스트하고 있는데, 이렇게 하면 다른 구현체로 변경되었을 때 해당 구현체가 잘 동작하는지 같은 테스트로 편리하게 검증할 수 있음
- 가끔 구현 자체를 검증해야될 때를 빼고는 가급적 인터페이스로 테스트 하는 것이 좋음
package hello.itemservice.domain;
@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);
}
}
6. 데이터베이스 테이블 생성
- 다양한 데이터 접근 기술을 활용해서 메모리가 아닌 데이터 베이스에 데이터를 보관하는 방법으로 변경
1) H2 데이터베이스로 테이블 생성, 등록, 조회
- 맥인 경우 터미널로 ~bin에 위치한 h2.sh로 데이터베이스를 실행
(1) 테이블 생성
- gunerated by default as identity : identity 전략, 기본키 생성을 데이터베이스에 위임하는 방법으로 MySQL의 Auto Increment와 같은 방법임
- PK로 사용되는 id는 개발자가 직접 지정하는 것이 아니라 비워두고 저장하면 데이터베이스가 순서대로 증가하는 값을 사용해서 넣어줌
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)
);
(2) 등록 및 조회
- 데이터를 여러건 등록하고 조회했을 때 잘 조회되는지 테스트 실행
# 등록
insert into item(item_name, price, quantity) values ('ItemTest', 10000, 10);
# 조회
select * from item;
2) 권장하는 식별자 선택 전략
(1) 데이터베이스의 기본 키의 3가지 조건
- null 값은 허용 불가
- 유일해야 함
- 변해선 안됨
(2) 테이블의 기본키 선택 전략 2가지
- 자연키(natural key) : 비즈니스에 의미가 있는키
- 주민번호, 이메일, 전화번호 등
- 대리 키(surrogate key) : 비즈니스와 관련이 없는 임의로 만들어진 키, 대체 키로도 불림
- 오라클 시퀀스, auto_increment, identity, 키생성 테이블 사용 등
(3) 자연 키 보다는 대리 키를 권장하는 이유 - 의견이 갈릴 수는 있다고 함
- 전화번호, 이메일과 같은 것은 그 번호가 유일할 수는 있지만 전화번호가 없을 수도 있고 전화번호가 변경될 수도 있어서 기본키로 적합하지 않음
- 문제는 주민등록번호처럼 그럴듯하게 보이는 값인데, null도 아니고 유일하며 변하지도 않는 조건을 모두 만족하는 것 처럼 보이지만, 현실과 비즈니스 규칙은 생각보다 무수한 영향으로 쉽게 변할 수 있어 주민등록번호 조차도 여러 가지 이유로 변경될 수 있음
(4) 비즈니스 환경은 언젠가 변한다 - 김영한님의 경험
- 레거시 시스템을 유지보수할 일이 있었는데, 분석해보기 회원 테이블에 주민등록번호가 기본키로 잡혀있었음
- 회원과 관련되 테이블에서 조인을 위해 주민등록번호를 외래 키로 가지고 있었고 심지어 자식 테이블의 자식 테이블까지 주민등록번호가 내려가 있었음
- 정부 정책이 변경되변서 법적으로 주민등록번호를 저장할 수 없게 되면서 문제가 발생하게 되었고 데이터베이스 테이블은 물론이고 수많은 애플리케이션 로직을 수정해야 했음
- 만약, 데이터베이스를 처음 설계할 때부터 자연 키인 주민등록번호 대신에 비즈니스와 관련 없는 대리 키를 사용했더면 수정할 부분이 많지 않았을 것
- 기본 키의 조건을 현재, 미래까지 충족하는 자연 키를 찾기는 쉽지 않음
- 대리 키는 비즈니스와 무관한 임의의 값으로 요구사항이 변경되어도 기본 키가 변경되는 일은 드물어서 대리 키를 기본 키로 사용하되 주민번호나, 이메일처럼 자연 키의 후보가 되는 컬럼들은 필요에 따라 유니크 인덱스를 설정해서 사용하는 것을 권장함
- JPA는 모든 에티티에 일관된 방식으로 대리키 사용을 권장함
- 비즈니스 요구사항은 계속해서 변하는데 테이블은 한 번 정의하면 변경하기가 어렵기 때문에 외부적인 요인으로 변경되지 않는 대리 키가 일반적으로 좋은 선택이라 생각 됨