Spring/SpringBoot&JPA

[스프링부트와 JPA 활용 1] Section4. 회원 도메인 개발

y-seo 2023. 11. 1. 07:34

 

회원 리포지토리 개발

  • jpashop/src/main/java/jpabook/jpashop/repository/MemberRepository.java 생성
package jpabook.jpashop.repository;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jpabook.jpashop.domain.Member;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public class MemberRepository {

    @PersistenceContext
    private EntityManager em;

    public void save(Member member){ //jpa가 저장하는 로직
        em.persist(member);
    }

    public Member findOne(Long id) { //Member를 반환, 단권 조회
        return em.find(Member.class, id);
    }

    public List<Member> findAll(){ //리스트 조회
        return em.createQuery("select m from Member m", Member.class) //jpql이라는 것, entity 객체에 대해 query
                .getResultList();
    }

    public List<Member> findByName(String name){ //이름으로 회원 검색
        return em.createQuery("select m from Member m where m.name = :name", Member.class) //jpql이라는 것, entity 객체에 대해 query
                .setParameter("name", name)
                .getResultList();
    }

}
  • @Repository
    • component 스캔에 의해 자동으로 spring bin으로 관리 된다
  • @PersistenceContext
    • 스프링이 EntityManager을 만들어 em에 주입한다

 

회원 서비스 개발

  • jpashop/src/main/java/jpabook/jpashop/service/MemberService.java 생성
package jpabook.jpashop.service;

import jpabook.jpashop.domain.Member;
import jpabook.jpashop.repository.MemberRepository;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service //컴포넌트 스캔의 대상이 되어 자동으로 스프링 빈 등록
@Transactional //(readOnly = false)
//@AllArgsConstructor //필드의 생성자 만들기
@RequiredArgsConstructor //final인 것들만 생성자 만들기
public class MemberService {

    private final MemberRepository memberRepository; //변경할 일 X

    //@Autowired //스프링이 스프링 빈에 등록되어 있는 MemberRepository를 주입 = field injection
    //spring에서는 생성자가 1개만 있는 경우에는 @Autowired 없이도 자동으로 injection 해줌
//    public MemberService(MemberRepository memberRepository) { //Setter Injection
//        this.memberRepository = memberRepository;
//    }

    //회원 가입
    @Transactional
    public Long join(Member member){
        validateDuplicateMember(member); //중복 회원 검증
        memberRepository.save(member);
        return member.getId(); //항상 값이 있다는 것이 보장됨
    }

    private void validateDuplicateMember(Member member) {
        //Exception
        List<Member> findMembers = memberRepository.findByName(member.getName());
        if (!findMembers.isEmpty()){
            throw new IllegalStateException("이미 존재하는 회원입니다.");
        }
    }

    //회원 전체 조회
    public List<Member> findMembers(){
        return memberRepository.findAll();
    }
    public Member findOne(Long memberId){ //단권 조회
        return memberRepository.findOne(memberId);
    }
}
  • @Service
    • component 스캔의 대상이 되어 자동으로 spring bin 등록
  • @Transactional
    • JPA의 모든 데이터 변경 or 로직들은 가급적이면 transaction 안에서 다 실행되어야 해서 사용
    • import org.springframework.transaction.annotation.Transactional; 사용이 나음
    • 쓸 수 있는 dependency가 많음
    • @Transactional(readOnly = true) 가 되면 JPA가 조회하는 곳에서는 성능을 더 최적화 함
    • 읽기에는 readOnly 넣어주기
    • 쓰기에는 readOnly넣지 말기

 

  • @Transactional을 이용해 "회원 리포지토리 개발" 에서 작성한 코드를 줄일 수 있다
package jpabook.jpashop.repository;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jpabook.jpashop.domain.Member;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
@RequiredArgsConstructor
public class MemberRepository {

    private EntityManager em;

    public void save(Member member){ //jpa가 저장하는 로직
        em.persist(member);
    }

    public Member findOne(Long id) { //Member를 반환, 단권 조회
        return em.find(Member.class, id);
    }

    public List<Member> findAll(){ //리스트 조회
        return em.createQuery("select m from Member m", Member.class) //jpql이라는 것, entity 객체에 대해 query
                .getResultList();
    }

    public List<Member> findByName(String name){ //이름으로 회원 검색
        return em.createQuery("select m from Member m where m.name = :name", Member.class) //jpql이라는 것, entity 객체에 대해 query
                .setParameter("name", name)
                .getResultList();
    }

}
  • 스프링 필드 주입 대신에 생성자 주입을 권장한다.
    • 필드 주입은 변경이 불가능하기 때문에 주로 사용하지 않는다.
    • Setter 주입은 런타임에 누군가가 바꿀 수 있기 때문에 주로 사용하지 않는다.
    • 생성자 주입은 중간에 변경될 여지가 없기 때문에 주로 사용한다. 변경 불가능한 안전한 객체를 생성할 수 있다는 것이다.
//필드 주입
public class MemberService {
	@Autowired
    MemberRepository memberRepository;
    ...
}

//Setter 주입
public class MemberService{
	private MemeberRepository memberRepository;
    
    @Autowired
    public void setMemberRepository(MemberRepository memberRepository){
    	this.memberRepository = memberRepository;
    }
    ...
}

//생성자 주입
public class MemberService {
	private final MemberRepository memberRepository;
    
    @Autowired
    public MemberService(MemberRepository memberRepository) {
    	this.memberRepository = memberRepository;
    }
    ...
}

 

회원 기능 테스트

  • 테스트 요구사항
    • 회원가입을 성공해야 한다
    • 회원가입 할 때 같은 이름이 있으면 예외가 발생해야 한다
  • jpashop/src/main/java/jpabook/jpashop/service/MemberService.java 생성
package jpabook.jpashop.service;

import io.micrometer.common.util.internal.logging.InternalLogLevel;
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.annotation.Rollback;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;

import static org.junit.Assert.*;

//JUnit4
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional //롤백을 위해
public class MemberServiceTest {

    @Autowired
    MemberService memberService;
    @Autowired
    MemberRepository memberRepository;

    @Test(expected = IllegalStateException.class)
    //@Rollback(false) //insert문을 볼 수 있음, Transactional은 rollback을 하니까
    public void 회원가입() throws Exception{
        //given
        Member member = new Member();
        member.setName("cho");
        //when
        Long saveId =  memberService.join(member);
        //then
        assertEquals(member, memberRepository.findOne(saveId));
    }

    @Test
    public void 중복_회원_예외() throws Exception{
        //given
        Member member1 = new Member();
        member1.setName("cho");

        Member member2 = new Member();
        member2.setName("cho");
        //when
        memberService.join(member1);
/*        try{ //exception을 잡아주어야 함
            memberService.join(member2);
        } catch (IllegalStateException e){ //여기서 exception을 잡아 줌
            return;
        }*/
        memberService.join(member2); //예외가 발생해야 한다
        //then
        fail("예외가 발생해야 한다.");

    }
}
  • 테스트 케이스 작성 툴
    • //given
    • //when
    • //then
  •  @RunWith(SpringRunner.class)
    • 스프링과 테스트 통합
  •  @SpringBootTest
    • 스프링부트를 띄우고 테스트하게 하는 어노테이션
    • 위 어노테이션이 없으면 @Autowired 다 실패
  •  @Transactional
    • 위 어노테이션을 사용하여 영속성 컨텍스트가 플러시를 안하게 하면/rollback을 하게 하면, insert query조차 나가지 않음
    • 반복 가능한 테스트를 지원하는 어노테이션
    • 각각의 테스트를 실행할 때마다 트랜잭션을 시작하고 테스트가 끝나면 트랜잭션을 강제로 롤백 
    • @Rollback(false)를 덧붙여주면 insert query 확인이 가능함
  • rollback을 해야 하는 이유
    • 테스트는 반복해서 진행되어야 하기 때문에 DB에 데이터가 남으면 안돼서 insert 하고 다시 rollback이 된다

 

  • 위 테스트 코드는 DB를 외부에 설치해야 하는 번거로움이 있다
    • Memory DB 사용
    • Java 안에 작은 DB를 만들어 띄우는 방법
    • test 디렉토리 안에 resources 디렉토리 생성
      • 기본 운영 로직 : main의 resources 패키지에 우선권을 가짐
      • 테스트 실행 시 : test의 resources 패키지에 우선권을 가짐
  • 아래와 같은 폴더 구조를 갖도록 함
    • yml 파일은 main의 것을 복붙

spring:
  #h2 세팅
  datasource:
    url: jdbc:h2:mem:test #메모리 DB로 바꿔주기
    username: sa
    password:
    driver-class-name: org.h2.Driver

    #jpa 세팅
  jpa:
    hibernate:
      ddl-auto: create #자동으로 탭을 만들어주는, 애플리케이션 실행 시점에 가지고 있는 테이블을 지우고 다시 생성
    properties:
      hiberate:
        #show_sql: true #System.out에 출력
        format_sql: true

logging.level:
    org.hibernate.SQL: debug #JPA나 Hibernate가 생성하는 SQL이 모두 보이는, logger를 통해