[JPA 활용 1편] 2. 회원, 상품 도메인 개발

HJ·2024년 2월 13일
0

JPA 활용 1편

목록 보기
2/4
post-thumbnail
post-custom-banner

김영한 님의 실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 강의를 보고 작성한 내용입니다.


1. 회원 도메인 개발

1-1. Repository 개발

@Repository
public class MemberRepository {
    @PersistenceContext
    private EntityManager em;

    public void save(Member member) {
        em.persist(member);
    }

    public Member findOne(Long id) {
        return em.find(Member.class, id);
    }

    public List<Member> findAll() {
        return em.createQuery("select m from Member m", Member.class).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();
    }
}

persist() 는 저장할 때 사용하는데 persist() 를 하면 영속성 컨텍스트에 Member Entity 를 넣고, 나중에 트랜잭션이 commit 되는 시점에 DB 에 반영됩니다.

find() 는 단건을 조회할 때 사용하는데 첫 번째 인자로 반환 타입을 지정하고, 두 번째 인자에 PK 를 넣어주면 됩니다.

여러 건을 조회할 때는 createQuery() 를 사용하는데 첫 번째 인자에는 JPQL 을 넣어주고, 두 번째는 반환 타입을 지정해줍니다.

SQL 은 테이블을 대상으로 쿼리를 하는 반면, JPQL 은 Entity 객체를 대상으로 쿼리를 합니다.

조건을 사용해서 쿼리를 하는 경우 where 절에서 :파라미터 형태로 작성한 후, setParameter() 를 통해 어떤 파라미터에 어떤 값이 들어갈 지를 지정합니다.

참고로 @Persistence 를 사용하지 않고, @RequiredArgsConstructorfinal 키워드를 이용해서 EntityManager 를 주입 받는 방법도 가능합니다.


1-2. Service 개발

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService {
    private final MemberRepository memberRepository;
    // 회원 가입
    @Transactional
    public Long join(Member member) {
        validateDuplicateMember(member);    // 중복 회원 체크
        memberRepository.save(member);
        return member.getId();
    }
    // 나머지 코드는 생략
}

JPA 의 모든 데이터 변경이나 로직들은 가급적이면 트랜잭션 안에서 실행되어야 합니다. 그래서 @Transactional 이 필요하며, 이 어노테이션이 있어야 지연로딩과 같은 기능들이 동작합니다.

readOnly = true 라는 옵션을 주면 JPA 가 조회하는 곳에서는 성능을 최적화 합니다. 읽기가 아닌 쓰기에서 적용하면 데이터 변경이 일어나지 않습니다.

위에서는 생략된 부분에 조회 관련 코드가 있기 때문에 전체적으로 readOnly = true 를 걸어주었고, 변경이 일어나는 메서드에는 @Transactional 을 사용하여 변경이 일어날 수 있도록 하였습니다.


[ member.getId() 로 ID 값을 가져올 수 있는 이유 ]

persist() 를 통해 Entity 를 영속성 컨텍스트에 올리는데 영속성 컨텍스트는 key 와 value 를 가지고 있습니다.

영속성 컨텍스트에 엔티티를 관리하려면 그 순간 바로 PK 인 ID가 필요합니다. 왜냐하면 PK인 ID를 기준으로 영속성 컨텍스트가 관리되기 때문입니다.

그래서 영속성 컨텍스트는 DB 에 들어간 시점이 아닌 persist() 를 호출하는 순간에 바로 PK인 ID를 획득하게 되고 해당 ID를 엔티티의 PK ID에 넣어둡니다.

그렇기 때문에 member.getId() 를 했을 때 항상 값이 존재한다는 것이 보장됩니다.


1-3. 회원가입 Test

@SpringBootTest
@Transactional
class MemberServiceTest {

    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;

    @Test
    void 회원가입() {
        Member member = new Member();
        member.setName("김");

        Long savedId = memberService.join(member);

        assertEquals(member, memberRepository.findOne(savedId));
    }
}

JPA 에서 같은 트랜잭션 안에서 같은 Entity 인, 즉 영속성 컨텍스트에서 Entity 를 하나로만 관리하기 때문에 PK( ID ) 값이 똑같으면 같은 Entity 입니다.

그래서 회원가입 테스트를 진행했을 때 저장한 회원과 repository 를 통해 조회한 회원이 동일하게 됩니다.


GeneratedValue 전략에서는 persist() 를 호출해도 DB 에 INSERT 문이 전달되지 않습니다.
Insert 쿼리는 DB 트랜잭션이 Commit 될 때 flush() 되면서 DB 에 쿼리가 나가게 됩니다.

@Transactional 이 테스트에 있을 때 트랜잭션을 Commit 하지 않고 Rollback 하기 때문에 해당 테스트의 로그를 살펴보면 INSERT 쿼리가 없는 것을 확인할 수 있습니다.

EntityManager 를 주입받고, join() 이후에 em.flush() 를 작성하면 Insert 쿼리가 수행되는 것을 확인할 수 있고, @Transactional 이 있기 때문에 Rollback 되어 실제로 저장되진 않습니다.


1-4. 테스트를 위한 설정

테스트를 진행할 때는 현재처럼 외부 DB 를 사용하는 것이 아닌 완전히 격리된 환경에서 진행하는 것이 좋은데 Java 를 띄울 때 안에 DB 를 새로 만들어서 띄우는 방법이 존재합니다.

test 아래 resources 폴더를 만들고 내부에 application.yml 을 복사해서 넣습니다. 이렇게 하면 테스트 할 때는 TEST 하위에 있는 application.yml 을 가지고 실행을 하게 됩니다.
그 후 url 을 jdbc:h2:mem:test 로 변경하게 되면 H2 DB 가 메모리 모드로 동작하게 됩니다.


참고로 스프링부트는 datasource 설정이 없으면, 기본적을 메모리 DB를 사용하고, driver-class도 현재 등록된 라이브러를 보고 찾아줍니다.

또 ddl-auto 도 create-drop 모드로 동작하기 때문에 데이터소스나, JPA 관련된 별도의 추가 설정을 하지 않아도 됩니다.




2. 상품 도메인 개발

2-1. 상품 Entity

public abstract class Item {
    ...
    private int stockQuantity;

    // 비즈니스 로직
    public void addStockQuantity(int stockQuantity) {
        this.stockQuantity += stockQuantity;
    }

    public void removeStockQuantity(int stockQuantity) {
        int restStock = this.stockQuantity - stockQuantity;
        if (restStock < 0) {
            throw new NotEnoughStockException("need more stock");
        }
        this.stockQuantity -= stockQuantity;
    }
}

데이터를 가지고 있는 쪽에 비즈니스 메서드가 있는 것이 응집력이 있고 좋은 형태입니다. 그래서 재고를 늘리고, 줄이는 로직은 Item 이 가진 stockQuantity 정보를 사용하기 때문에 Item 에 작성하였습니다.

내부 값을 변경할 때 Setter 함수를 사용하는 것이 아닌 위처럼 핵시 비즈니스 메서드를 사용해서 변경하는 것이 좋은 방법입니다.


2-2. Repository

@Repository
@RequiredArgsConstructor
public class ItemRepository {

    private final EntityManager em;

    public void save(Item item) {
        if (item.getId() == null) {
            em.persist(item);
        } else {
            em.merge(item);
        }
    }

    public Item findOne(Long id) {
        return em.find(Item.class, id);
    }

    public List<Item> findAll() {
        return em.createQuery("select i from Item i", Item.class).getResultList();
    }
}

아예 새로운 Item 이라면 저장되기 전에 Item 은 id 값이 없기 때문에 persist() 를 사용하고, id 값이 있는 경우에는 merge() 를 사용합니다.

merge() 는 JPA 에서 엔티티를 영속성 컨텍스트에 병합하는 데 사용되는 메서드이며 엔티티의 변경 내용을 데이터베이스에 동기화하기 위해 주로 사용됩니다.


2-3. Service

@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);
    }
}
post-custom-banner

0개의 댓글