

실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발
JPA
2022.10.13. (수정됨)
Series
테이블, 엔티티 설계 시 주의사항
- 다대다 관계 사용x -> 일대다, 다대일로 풀어서 사용
- 외래키가 있는 곳을 연관관계의 주인으로 설정
- getter는 가급적 열어두고 setter는 비즈니스 메서드를 별도로 작성해 변경지점이 명확히 설계
- 값 타입 사용시 Immutable 하게 설계
- 연관관계는 지연로딩 설정
계층형 구조
- Controller : 웹 계층
- Service : 비즈니스 로직, 트랜잭션 처리
- Repository : JPA 사용 계층
- Domain : 엔티티가 모여있는 계층
서비스, 리포지토리 계층 개발 -> 테스트 케이스 작성 후 검증 -> 웹 계층 적용
어노테이션
Repository 관련
@Repository // 빈으로 등록, JPA 예외를 스프링 기반 예외로 변환
@PersistenceUnit // 엔티티 매니저 팩토리(EntityManagerFactory) 주입
@PersistenceContext // 엔티티 매니저(EntityManager) 주입
// -> SpringBoot 사용 시 @Autowird지원
// -> @RequiredArgsConstruct 사용해 생성자 주입 가능
Service 관련
@Service
// 트랜잭션, 영속성 컨텍스트
// 데이터의 변경이 없는 읽기 전용 메서드에 사용
// 영속성 컨텍스트를 플러시 않아 약간의 성능 향상(읽기 전용에는 다 적용)
@Transactional(readOnly=true)
Test 관련
@RunWith(SpringRunner.class) // Junit4 스프링과 테스트 통합
@ExtendWith(SpringExtension.class) // Junit5
@SpringBootTest // 스프링 부트 띄우고 테스트 - @Autowired시 필요
@Transactional // 테스트를 실행할 때마다 트랜잭션 시작 - 테스트 종료 시 롤백
Pattern
비즈니스 로직에 따른 패턴
1. 도메인 모델 패턴
- 엔티티가 비즈니스 로직을 가지고 객체지향 특성을 적극 활용
- 서비스 계층은 단순히 엔티티에 필요한 요청만 위임
- 엔티티에 대해 테스트 코드 작성 가능 (단위 테스트)
2. 트랜잭션 스크립트 패턴
- 일반적으로 사용하던 패턴
- 서비스 계층에서 대부분의 비즈니스 로직 처리
Form 객체 vs 엔티티 직접 사용
엔티티는 핵심 비즈니스 로직만 소유
-> 화면이나 API에 맞는 폼 객체나 DTO를 사용
-> API는 절대 엔티티 반환x -> 엔티티가 변경되면 스펙이 변경되기 때문-> DTO 사용
변경 감지(dirty check)와 병합(Merge)
JPA는 플러시 할 때 변경 감지가 일어나 업데이트 해준다 -> 준영속 상태의 엔티티 변경시 문제 발생
준영속 엔티티란?
영속성 컨텍스트가 관리하지 않는 엔티티 -> 기존 식별자를 가지고 있는 엔티티
해결 방법
-
변경 감지 기능 사용
-> 영속성 컨텍스트에서 엔티티를 다시 조회한 후에 데이터 수정하는 방법 -
병합 사용
-> 준영속 상태의 엔티티를 영속 상태로 변경할 때 사용하는 기능
em.merge(item);
병합 동작 방식
- 준영속 엔티티의 식별자 값으로 영속 엔티티 조회
- 영속 엔티티의 값을 준영속 엔티티로 병합
- 트랜잭션 커밋 시점에 변경 감지 기능이 동작해 DB에 UPDATE
=> 병합 사용 시 모든 필드를 변경, 데이터가 없으면 null로 업데이트
결론
엔티티 변경 시 변경 감지를 사용하는 것이 좋다
-> 트랜잭션이 있는 서비스 계층에서 영속 상태의 엔티티를 조회 -> 변경
(트랜잭션 커밋 시점 변경 감지 실행)
@PostMapping(value = "/items/{itemId}/edit")
public String updateItem(@ModelAttribute("form") BookForm form) {
// 변경해야 하는 엔티티 값을 넘겨준다.
itemService.updateItem(form.getId(), form.getName(), form.getPrice());
return "redirect:/items";
}
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ItemService {
...
@Transactional
public void updateItem(Long itemId, String name, int price, int stockQuantity) {
Item item = itemRepository.findOne(itemId);
item.setName(name);
item.setPrice(price);
item.setStockQuantity(stockQuantity);
}
...
}
Tip!!
- 테스트 코드 작성 시 스프링, DB없이 순수한 메서드만 단위테스트 하는게 좋다
- @Test(expected = …) -> junit5에서 사용법 변경 Migrating from JUnit 4 to JUnit 5 참고
- 동적 쿼리를 작성 할 때(ex - 검색 기능) JPQL로 구현하면 String 붙여줘서 문자열로 만들어줘야 함
-> QueryDSL 사용
값 검증
- 값 검증은 implementation ‘org.springframework.boot:spring-boot-starter-validation’ 추가
// @Valid 어노테이션 사용해 값 검증
// @Valid 어노테이션 사용한 부분에 BindingResult 사용해주면 에러를 result값으로 생성
// -> 화면에 노출 가능
public String create(@Valid MemberForm form, BindingResult result) {
,,,
}