2022. 5. 23. 13:28ㆍJava/JPA
애플리케이션 아키텍처
계층형 구조 사용, 단방향
- controller, web: 웹 계층
- Controller에서 Service 뿐만 아니라 Repositoty도 접근 가능 (단방향이므로)
- service: 핵심 비즈니스 로직, 트랜잭션 처리
- repository: JPA를 직접 사용하는 계층, 엔티티 매니저 사용, DB 접근
- domain: 엔티티가 모여 있는 계층, 모든 계층에서 사용가능
패키지 구조
- jpabook.jpashop
- domain
- exception
- repository
- service
- web
개발 순서:
서비스, 리포지토리, 도메인 계층 개발 (웹 관련 없는 핵심 비즈니스 로직)
테스트 케이스를 작성해서 검증
마지막에 컨트롤러, 웹(타임리프) 계층 적용
API 개발 및 성능 최적화
회원 도메인 개발
구현 기능
- 회원 등록
- 회원 목록 조회
개발 순서
- 회원 엔티티 코드 다시 보기
- 회원 리포지토리 개발
- 회원 서비스 개발
- 회원 기능 테스트
회원 리포지토리 개발
package jpabook.jpashop.repository;
import jpabook.jpashop.domain.Member;
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;
@Repository //컴포넌트 스캔대상이므로 스프링 빈에 등록되어짐
public class MemberRepository {
@PersistenceContext // Entity를 영속성 컨텍스트에 저장
private EntityManager em;
public void save(Member member) {
em.persist(member); //트랜잭션 커밋시점에 DB에 반영
}
public Member findOne(Long id) { //단건 조회(타입, PK)
return em.find(Member.class, id);
}
public List<Member> findAll() { //List 조회는 JPQL사용(객체를 대상으로한 쿼리)
return em.createQuery("select m from Member m", Member.class) // (JPQL, 반환타입)
.getResultList();
}
public List<Member> findByName(String name) { //이름으로 조회
return em.createQuery("select m from Member m where m.name = :name", Member.class)
.setParameter("name", name)
.getResultList();
}
}
회원 서비스 개발
@Transactional //JPA에서 모든 데이터변경이나 로직은 @Transactional안에서 해야함
class위에@Transactional은 public메서드에 접근 가능
@Transactional은 두 개 존재
import javax.transaction.Transactional; 보다
import org.springframework.transaction.annotation.Transactional; 권장
조회하는 곳에서
@Transactional(readOnly = ture)
설정 시 성능 최적화 가능
기본값은 false
필드 주입
@Autowired 필드 주입 방법 단점 : 테스트할 때 바꿔야 할 경우가 생기는데 바꿀 수 없음
@Autowired //스프링빈에 등록된 MemberRepository 주입
private MemberRepository memberRepository;
그래서
생성자 주입을 권장합니다.
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
...
}
- 변경 불가능한 안전한 객체 생성 가능
- 생성자가 하나면, @Autowired를 생략할 수 있다.(위 코드)
- final 키워드를 추가하면 컴파일 시점에 memberRepository를 설정하지 않는 오류를 체크할 수 있다.
한 단계 더 나아가 롬복을 적용
@RequiredArgsConstructor // final필드에 대해 생성자를 만들어주는 lombok의 annotation.
package jpabook.jpashop.repository;
import jpabook.jpashop.domain.Member;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;
@Repository //@Component 스캔대상이므로 자동으로 스프링 빈에 등록되어짐
@RequiredArgsConstructor // final필드에 대해 생성자를 만들어주는 lombok의 annotation.
public class MemberRepository {
private final EntityManager em;
public void save(Member member) {
em.persist(member); //트랜잭션 커밋시점에 DB에 반영
}
public Member findOne(Long id) { //단건 조회(타입, PK)
return em.find(Member.class, id);
}
public List<Member> findAll() { //List 조회는 JPQL사용(객체를 대상으로한 쿼리)
return em.createQuery("select m from Member m", Member.class) // (JPQL, 반환타입)
.getResultList();
}
public List<Member> findByName(String name) { //이름으로 조회
return em.createQuery("select m from Member m where m.name = :name", Member.class)
.setParameter("name", name)
.getResultList();
}
}
❗❗ 정리 ❗❗
@RequiredArgsConstructor를 사용하고, MemberRepository에 final 키워드를 붙이면
MemberService가 Spring Container에 Bean 등록이 될 때 MemberRepository를 주입시켜준다.
@RequiredArgsConstructor
이 어노테이션은 NotNull이거나 final이 붙은 변수들에 대해 생성자를 만들어주는 기능을 제공하고 있다.
final 키워드
MemberRepository 같은 빈들은 Spring 컨테이너가 관리하는 싱글톤 객체이기 때문에 변하지 않으므로
해당 빈(Bean) 들이 생성자를 통해 주입되는 시점에 불변성을 보장하도록 final 키워드를 붙여주는 것이 좋다.
회원 기능 테스트
테스트 요구사항
- 회원가입을 성공해야 한다.
- 회원가입할 때 같은 이름이 있으면 예외가 발생해야 한다
MemberService 클릭 후 Ctrl + Shift + T 눌러서 테스트 클래스 생성
JUnit4
JUnit을 위한 Live Template 생성(IntelliJ) - tdd
MemberServiceTest
package jpabook.jpashop.service;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.repository.MemberRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;
import static org.junit.Assert.*;
@RunWith(SpringRunner.class) //스프링과 테스트 통합
@SpringBootTest
@Transactional //데이터변경,(커밋 안하고)롤백(영속성컨텍스트 flush 안 함) -> 커밋하고 싶으면 @Rollback(false)하면 insert문을 DB로 날림
public class MemberServiceTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
public void 회원가입() throws Exception {
//given ~주어졌을 때
Member member = new Member();
member.setName("kim");
//when ~실행 하면
Long savedId = memberService.join(member);
//then ~된다
assertEquals(member, memberRepository.findOne(savedId)); //JPA - Transactional 안에서 id값 같으면 같은 영속성컨텍스트에서 하나로 관리
}
@Test
public void 중복_회원_예외() throws Exception {
//given
Member member1 = new Member();
member1.setName("kim");
Member member2 = new Member();
member2.setName("kim");
//when
memberService.join(member1);
try{
memberService.join(member2); //예외가 발생해야 한다 -> Exception나서 밖으로 나가야한다(return). 밑으로 내려가면 안된다.
}catch (IllegalStateException e) {
return;
}
//then
fail("예외가 발생해야 한다."); //여기로 오면 안되므로 fail문 작성
}
}
try ~ catch문 수정
@Test(expected = IllegalStateException.class)
public void 중복_회원_예외() throws Exception {
//given
Member member1 = new Member();
member1.setName("kim");
Member member2 = new Member();
member2.setName("kim");
//when
memberService.join(member1);
memberService.join(member2);
//then
fail("예외가 발생해야 한다."); //여기로 오면 안되므로 fail문 작성
}
상품 도메인 개발
구현 기능
- 상품 등록
- 상품 목록 조회
- 상품 수정
순서
- 상품 엔티티 개발(비즈니스 로직 추가) : 재고+-
- 상품 리포지토리 개발
- 상품 서비스 개발, 상품 기능 테스트
상품 엔티티 개발(비즈니스 로직 추가)
사용할 데이터(stockQuantity)를 가지고 있는 [엔티티 안에 비즈니스 로직 추가]하는 것이 응집도가 있다.
package jpabook.jpashop.domain.item;
import jpabook.jpashop.domain.Category;
import jpabook.jpashop.exception.NotEnoughStockException;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) // 상속관계전략 - SINGLE_TABLE : 한 테이블에 다 넣음
@DiscriminatorColumn(name = "dtype") // 싱글테이블전략이므로 구분자 지정
@Getter
@Setter
public abstract class Item { // 추상 클래스
@Id
@GeneratedValue
@Column(name = "item_id")
private Long id;
private String name;
private int price;
private int stockQuantity;
@ManyToMany(mappedBy = "items")
private List<Category> categories = new ArrayList<>();
/**
* 사용할 데이터(stockQuantity)를 가지고 있는 [엔티티 안에 비즈니스 로직 추가]하는 것이 응집도가 있다.
* Service에서 비즈니스 로직을 짜는 것 보다 객체 지향적이다. 관리용이.
* Setter를 이용해서 stockQuantity를 변경하는 것이 아닌 비즈니스 로직을 만들어서 수행
*/
public void addStock(int quantity) { //재고 수량 증가
this.stockQuantity += quantity;
}
public void removeStock(int quantity) { //재고 수량 감소
int restStock = this.stockQuantity - quantity;
if(restStock < 0) {
throw new NotEnoughStockException("need more stock"); //예외 만들어야함
}
this.stockQuantity = restStock;
}
}
package jpabook.jpashop.exception;
public class NotEnoughStockException extends RuntimeException {
//메세지를 넘겨줘야 하므로 오버라이드 해줌
public NotEnoughStockException() {
super();
}
public NotEnoughStockException(String message) {
super(message);
}
public NotEnoughStockException(String message, Throwable cause) {
super(message, cause);
}
public NotEnoughStockException(Throwable cause) {
super(cause);
}
}
상품 리포지토리 개발
package jpabook.jpashop.repository;
import jpabook.jpashop.domain.item.Item;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
import java.util.List;
@Repository
@RequiredArgsConstructor // private final EntityManager em;
public class ItemRepository {
private final EntityManager em;
public void save(Item item) {
if(item.getId() == null) { //새로 생성한 객체
em.persist(item); //신규 등록
}else {
em.merge(item); //이미 JPA통해 DB들어간것, 업데이트와 비슷
}
}
public Item findOne(Long id) { //단건 조회는 find()
return em.find(Item.class, id);
}
public List<Item> findAll() { //전체 조회는 JPQL
return em.createQuery("select i from Item i", Item.class )
.getResultList();
}
}
상품 서비스 개발
package jpabook.jpashop.service;
import jpabook.jpashop.domain.item.Item;
import jpabook.jpashop.repository.ItemRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ItemService {
private final ItemRepository itemRepository;
@Transactional
public void saveItem(Item item) {
itemRepository.save(item);
}
public List<Item> findItems() {
return itemRepository.findAll();
}
public Item findOne(Long itemId) {
return itemRepository.findOne(itemId);
}
}
롬복 관련 참고 블로그
https://mangkyu.tistory.com/78
@RequiredArgsConstructor : https://mangkyu.tistory.com/155?category=761302
'Java > JPA' 카테고리의 다른 글
[API 개발 고급] 조회용 샘플 데이터 입력 (0) | 2022.05.24 |
---|---|
[API 개발] 회원 등록, 수정, 조회 API (0) | 2022.05.24 |
❗주문 도메인 개발❗, 웹 계층 개발 (0) | 2022.05.23 |
도메인 분석 설계 - 간단 쇼핑몰 예제 (0) | 2022.05.23 |
프로젝트 생성 및 View 환경 설정(Thymeleaf) (0) | 2022.05.17 |