실전 활용1 JPA _개발(회원)

링딩·2021년 6월 21일
0

Spring

목록 보기
2/5

회원 도메인 개발

이 글은 인프런의 '김영한'님 강의를 참조하여 작성하였습니다.

_회원 리포지토리 개발

😂 MemberRepository


@RequiredArgsConstructor //여기 리포지토리에서도 사용 가능!
@Repository //컴포넌트 스캔에 의해_ 자동으로 스프링 빈에 등록되어 관리
public class MemberRepository {

    private final EntityManager em;
    /*
    //스프링이 jpa의 엔티티매니저를 자기가 주입해줌.
    @PersistenceContext //=>주입 받을 수 있다.
    private EntityManager em; //스프링이 em을 만들어서 인젝션 해줌
    */

    //트랜잭션이 commit이 되는 시점에 DB에 insert 됨.
    public void save(Member member){
        //영속성 컨테스트에 member엔티티를 저장
        em.persist(member);
    }

    //단건 조회
    public Member findOne(Long id){
        //해당 멤버를 찾아서 반환.
        //find(타입, PK)
        return em.find(Member.class, id);
    }

    //전체회원 목록 조회 등
    public List<Member> findAll(){

        //em.createQuery("JPQL", 반환타입)
        //JPQL은 from이 엔티티를 대상으로 쿼리를 한다.-> 이 엔티티를 조회해
        //SQL은 테이블을 대상으로 쿼리 ->테이블을 조회해.
        //JPQL과 SQL은 동일함_>결국 SQL로 변환된다.
        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();
    }

}

1)EntityManager

wan-blog님과 HeeJeong Kwon님의 블로그를 참조하여 작성하였습니다.
MemberRepository

👀EntityManger
엔티티에 관련된 모든 일을 처리하며 데이터베이스에 Entity를 (등록,수정,삭제, 조회) 할 수 있다.
=> 동시성 문제로 인해 스레드 간 공유를 해선 안되므로 일반적으로 위처럼 스프링에서 관리하는 EntityManager em 선언하여 사용한다.

2) @PersistenceContext (영속성 컨텍스트)

-Entity를 영구 저장하는 환경.
-EntityManager로 Entity를 저장하거나 조회하면 '영속성 컨텍스트'에 엔티티를 보관 및 관리한다.

  • EntityManager.persist(엔티티)
    즉 실제 DB에 저장하진 않고, persist() 시점에 Entity를 영속성 컨텍스트에 저장하겠다는 의미이다.
  • 결과적으로 트랜잭션의 commit 시점때 영속성 컨텍스트의 정보들이 DB에 쿼리로 날라감.

3)JPQL

전체회원 목록조회특정이름 회원 조회

⋇JPQL 정의
jpa에서 모든 DB 데이터를 객체로 변환해서 검색이 어렵기에 애플리케이션이 필요하다하는 데이터만 DB에서 가져오기 위해 SQL을 추상화한 JPQL을 쓰게 되었다.

  • SQL은 테이블을 대상으로 쿼리를 질의하는 것에 비해 JPQL은 엔티티 객체를 대상으로 쿼리를 질의한다.
  • 쿼리에서는 '별칭' 으로 바인딩 해줘야 한다.
  • 결과 조회 : 쿼리.getResultList()
    - 결과가 하나 이상, 리스트로 결과를 반환.
  • 파라미터 바인딩 : 쿼리.setParameter("조건이름",파라미터)
    (1)이름 기준
    - query에서 where 조건=":검색명" 으로 조건절 안에 ' : '을 넣어줘야 한다.
    (2)위치 기준
    - query에서 where 조건="?검색위치" 으로 조건절 안에 ' ? '을 넣어줘야 한다.


_회원 서비스 개발

😂 MemberService

//자동으로 스프링 빈 등록
@Service

//읽기인 곳은 (readOnly = true) , 아닌 곳은 @Transactional만
//이 클래스의 public메소드는 주로 읽기가 많기에 (readOnly = true)를 넣어줌
//(readOnly = true)가 있는 곳은 jpa가 '조회하는 곳에서' 좀 더 성능이 최적화한다.
@Transactional(readOnly = true)

//3)더더 발전한 사용법. - 롬복을 사용(강사님이 추천)
//테스트케이스에서도 유용.
//final 필드에 있는 애들만 가지고 생성자 만들어줌
@RequiredArgsConstructor
public class MemberService {


    //final 권장- 생성자에 값을 세팅했는지 확인 가능.
    private final MemberRepository memberRepository;

    /*
    //2) 발전한 요새 사용법- '생성자 주입식(인젝션)'
    //장) 테스트 케이스 작성시, 생성시점을 명확히 알 수 잇음.
    //생성자가 하나일시 -> @Autowired 생략 가능.
    @Autowired
    public MemberService(MemberRepository memberRepository){
        this.memberRepository = memberRepository;
    }
    */

    /*
    //1) 스프링이 빈에서 memberRepository을 인젝션함 => 필드 인젝션
    단점: 테스트할때 변경을 할 수가 없음.
    @Autowired
    private MemberRepository memberRepository;
    */


    /**
     * 회원 가입
     */
    @Transactional //쓰기가 주인 경우 (readOnly = true) 필요x.
    public  Long join(Member member){

       validateDuplicateMember(member); //중복 회원 검증
       memberRepository.save(member); //문제가 없다면 회원 저장.
        return member.getId();
    }

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

    //회원 전체 조회
    public List<Member> findMembers(){
        return memberRepository.findAll();
    }

    //Id로 회원 단건 조회
    public Member findOne(Long memberId){
        return memberRepository.findOne(memberId);
    }


}

◼ 주 메서드
¹회원가입 ²회원가입 내에 중복회원 검증 ³전체회원 조회 ⁴Id로 단건 회원 조회

1)@Transactional

[kdhyo님의 블로그를 참조하였습니다.]
데이터변경을 위해 되도록 @Transactional 안에서 실행 되어야 한다.
그래서 public 메소드가 쓰기가 '주'인 곳만 따로 @Transactional을 지정해주고 전체적으로는 읽기전용으로 지정해줬다.

  • 조회가 주인 곳은 @Transactional(readOnly = true)을 넣어줌.
  • 쓰기가 주인 곳은 @Transactional만!

2) 인젝션

_1. 필드 주입


(단)테스트시 액세스 되어 있어 바꾸기 어려움

_2. 생성자 주입 (권장)

생성자 주입
(장) ¹테스트 케이스때 유용 ²어떤 것이 누구에게 의존하는지 확인이 가능
³변경이 불가한 안전한 객체 생성 ⁴생성자가 하나일시, @Autowired 생략가능

final로 선언해주면, 컴파일 시점에 memberRepository를 설정했는지 오류를 체크해줄 수 있다.
-> 보통이라면 기본 생성자를 추가할때나 확인이 가능

_2. 더 나아가 lombok 까지...

나아가 2번 생성자 주입에서 lombok을 사용하면 더 깔끔하다.
@RequiredArgsConstructor을 클래스에 달아주면 final이 있는 필드만 가지고 생성자를 만들어 준다.
-> 기본적으로 인젝션(주입) 하면서 생성자에 세팅하고 끝! 인 애들만 final 필드로 잡아주면 좋다.


📣또한 Repository에서도..
본래라면 @PersistenceContextEntityManager을 의존성 주입 해줬는데 최근 스프링 데이터 JPA에서는 @Autowired 로도 의존성 주입을 가능하게 해줬다.



기능 테스트

MemberServiceTest

//메모리모드로 DB까지 엮어서 테스트하고자
@RunWith(SpringRunner.class)
//스프링 부트를 띄우고 테스트 하는 것 -> 없으면 @AutoWired 못함
@SpringBootTest

//데이터 변경을 위해 -rollback등...
//테스트케이스에서의 @Transactional은 기본적으로 테스트가 끝나면 Rollback 기능을 함
//왜 롤백을 하는가? 테스트이므로 DB에 이 데이터가 남으면 안되기 때문
//Rollback을 한단 것 자체가 jpa입장에선 DB를 다 버린다는 것..
@Transactional
public class MemberServiceTest {

    //참조를 위해
    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;

    //기본적으로 persist()만으로 db에 insert문은 안나감.
    //보통 트랜잭션이 커밋될때 db에 insert가 나간다.

    @Test
    //@Rollback(false) -> 커밋함 -> insert쿼리 볼 수 잇음.
    public void 회원가입() throws Exception {
        //given ~이 주어졌을때
        Member member = new Member();
        member.setName("kim");

        //when(~을 실행하면)
        Long saveId = memberService.join(member);

        //then (~의 결과가 나옴)
        //em.flush(); //영속성 컨텍스트에 있는 변경 등을 DB에 반영함(굳이 롤백이지만 insert보고 싶다면)
        assertEquals(member, memberRepository.findOne(saveId));
    }


    //너무 복잡하여 사용(expected = Ille)
    //member2에서 예외가 발생해 터져서 나간애가 (expected = 비교대상)의
    //비교대상과 같으면 테스트가 된다.
    @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); //예외가 발생해야 함
       /* 코드가 너무 복잡.
        try{
            memberService.join(member2); //예외가 발생해야 함
        }catch (IllegalStateException e) {
            return;
        }
        */

        //then
        //fail() -> 코드가 여기에 오면 잘못된 것이므로 fail()을 띄움.
        fail("예외가 발생해야 한다.0");
    }

}

멤버에서 테스트가 필요할 기능 _ 1) 회원가입 2)중복 회원인지
👀 jUnit은 독립된 단위테스트를 지원해주는 프레임워크.

1) @RunWith(SpringRunner.class)

백기선님의 블로그를 참조하였습니다.

4.xx 버전에서 테스트 실행방법을 확장하는 방법 중 하나로 테스트를 지원하는 어노테이션이다.
그래서 @RunWith(SpringRunner.class)은 스프링부트를 테스트로 작성할때 주로 쓰였다.

but..현재는

  • 스프링부트 JUnit5 부터는 @RunWith를 쓸 필요가 없어짐 ->쓴다면 @ExtendWith(SpringExtension.class) 를 이용해 Extension 구현체를 지정해줘야 함.
    - 📣 하지만 이마저도 이미 스프링 부트가 (모든 테스트용 애노테이션) @SpringBootTest에 적용해서 @ExtendWith(SpringExtension.class)생략이 가능하다

2) @SpringBootTest

Namjun Kim님의 블로그를 참조하였습니다.

  • 통합 테스트를 제공하는 '스프링 부트 테스트 어노테이션' 이다.
  • 스프링 부트는 @SpringBootTest를 이용해 테스트에 필요한 모든 의존성을 제공한다. ->테스트를 위한 빈을 생성.
  • @MockBean으로 정의된 빈을 찾아 교체해준다.

3) @Transactional

* em.flush()

영속성 컨텍스트에 있는 변경사항 등을 DB에 반영해준다.

😥왜 테스트를 할때 insert 쿼리가 안나올까?

물론 이처럼 @Rollback(false)로 롤백이 아닌 커밋으로 바꿔버릴 수도 있다...

스프링에서는 @Transactional테스트 케이스에 있으면 기본적으로 RollBack을 한다.
►RollBack을 하면 jpa 입장에서는 일정시점으로 다시 되돌리는 기능으로 DB에 일정 부분을 다시 원래 상태로 되돌린다는 것이다.

🤷‍♀️왜 테스트에서 RollBack이 필요한 것인가?

그 이유는 테스트를 한다는 것은 정상적으로 작동을 하는지 시험을 하는 것으로 DB에 데이터가 남아선 안되기 때문이다.

4) @Test(expected = IllegalStateException.class)


위의 사진에선 member2의 중복회원으로 인해 예외 발생시 try catch문으로 작성하기엔 코드가 복잡하다.
=> 이것을 해결하기 위해 @Test(expected= 예외 클래스) 로 예외 클래스를 지정해주면 테스트에서 지정한 예외가 발생하여야 테스트가 정상적으로 작동한다.



2 테스트 케이스를 위한 설정

테스트 케이스는 격리된 환경에서 실행되고, 끝나면 데이터를 초기화 하는 것을 권장한다.=> '메모리 DB'의 사용이 이상적.

1) 테스트 케이스를 위한 스프링 환경과 일반적인 애플리케이션 실행 환경이 다르므로 application.yml 파일 개별 생성.

spring:
# datasource:
# url: jdbc:h2:mem:testdb
# username: sa
# password:
# driver-class-name: org.h2.Driver
# jpa:
# hibernate:# ddl-auto: create-drop
# properties:
# hibernate:
 # show_sql: true
# format_sql: true
# open-in-view: false
logging.level:
 org.hibernate.SQL: debug
# org.hibernate.type: trace

2) 스프링 부트는 이 처럼 datasource 설정이 따로 없다면, 기본적으로 '메모리 DB'를 사용.
추추가)create-drop 마지막 종료시점에 drop 쿼리를 날려줌 (꼭 필요치는 뭐)

profile
초짜 백엔드 개린이

0개의 댓글