회원 도메인 개발

woom·2023년 4월 27일

Spring Boot

목록 보기
3/6
post-thumbnail

김영한 강사님 [실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발] 강의 참조


🌼 구현 기능

  • 회원 등록
  • 회원 목록 조회

🌼 회원 레포지토리 개발

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 {

    //스프링이 entitymanager 빈으로 만들어서 주입
    @PersistenceContext
    private EntityManager em;

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

    //JPA가 제공하는 find메소드로 id값을 넘기면 member찾아서 반환
    public Member findOne(Long id){
        return em.find(Member.class, id);
    }

    //List 조회 (JPQL을 실행하기 위한 쿼리객체 생성 후 결과 반환)
    public List<Member> findAll(){
        return em.createQuery("select m from Member m", Member.class).getResultList();
    }

    //이름으로 회원을 검색하여 List 조회
    public List<Member> findByName(String name){
        return em.createQuery("select m from Member m where m.name = :name", Member.class)
                .setParameter("name", name)
                .getResultList();
    }
}


📕 레포지토리 관련 개념

@Repository : 스프링 빈으로 등록

  • @PersistenceContext : JPA의 EntityManager를 빈으로 주입할 때 사용하는 어노테이션
    • 스프링에서는 영속성 관리를 위해 EntityManager가 존재(스프링 컨테이너가 시작될 때 EntityManager를 만들어서 빈으로 등록)
    • EntityManagerFactory에서 새로운 EntityManager를 생성하거나 Transaction에 의해 기존에 생성된 EntityManager를 반환
    • @PersistenceContext
       private EntityManager em;
  • EntityManager : 내부적으로 DB connection pool을 사용해서 DB에 접근
    • Transaction 단위를 수행할 때마다 생성 (고객의 요청이 올 때마다 사용했다가 닫는다.)
    • Transaction 수행 후에는 반드시 EntityManager 를 닫는다. (내부적으로 DB Connection 을 반환)
  • 영속성 컨텍스트(Persistence Context) : Entity를 영구 저장하는 환경
    • EntityManager를 통해서 영속성 컨텍스트에 접근
    • EntityManager가 생성되면 1:1로 영속성 컨텍스트가 생성
  • EntityManager.persist(entity) : DB에 저장하는 것이 아니라 영속성 컨텍스트를 통해서 Entity를 영속화한다는 뜻 (persist() 시점에는 Entity를 영속성 컨텍스트에 저장하는 것)
    • 트랜잭션 커밋되는 시점에 DB에 반영(DB에 insert쿼리 전달)
    • entity값의 @Id가 키(영속성 콘텍스트에는 키와 값이 있음)가 됨(DB pk와 매핑) → id값이 항상 생성(보장됨)
  • EntityManager.find(type, pk) : 단건조회(pk값을 넘기면 알맞은 type조회)

📌 JPQL(Java Persistence Query Language)

  • 테이블이 아닌 엔티티 객체를 대상으로 검색하는 객체지향 쿼리
  • SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않음
  • JPA는 JPQL을 분석한 후 적절한 SQL을 만들어 데이터베이스를 조회
  • 기본문법 : String jpql = "select m from Member m where m.name = 'c'";(별칭은 필수)
  • typedQuery : JPQL을 실행하려면 쿼리 객체를 만들어야 한다. 쿼리 객체로는 TypedQuery와 Query가 있는데 반환할 타입을 명확하게 지정할 수 있으면 TypedQuery 객체를, 명확하게 지정할 수 없으면 Query 객체를 사용

    • query=em.createQuery(jpql, 반환(조회)타입)
    • return query.getResultList() : 결과를 예제로 반환
    public static void typedQuery(EntityManager em) {
        String jpql = "select m from Member m";
        TypedQuery<Member> query = em.createQuery(jpql, Member.class);
    
        List<Member> list = query.getResultList();
        for( Member member : list) {
            System.out.println("Member : " + member);
        }
    }
    

ex. 이름으로 회원을 검색하여 List를 조회하는 메소드 작성

  • :name : parameter를 바인딩한것
public List<Member> findByName(String name){
	return em.createQuery(qlString: "select m from Member m where m.name=:name",
    Member.class).setParameter(name:"name", name).getResultList();
}

🌼 회원 서비스 개발

package jpabook.jpashop.service;

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

import java.util.List;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor//final이 있는 필드만을 가지고 생성자 만듦
public class MemberService {

    private final MemberRepository memberRepository;

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

    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);
    }
}

📙 서비스 관련 개념

  • service 클래스 : 다수의 repository를 호출하여 프로그램에 필요한 처리 기능을 모듈화
  • @Service : 스프링 빈으로 등록
  • @Autowired : 의존성 주입(스프링이 스프링빈에 등록되어있는 멤버레퍼지토리를 인젝션)

📌 @Autowired

  1. 필드 인젝션 : 필드를 변경 할 수 없다는 단점
@Autowired
private MemberRepository memberRepository;
  1. setter 인젝션 : 바로 주입하는 것이 아니라 setter로 들어와서 주입
    • test code작성 시 mock(실제 객체를 만들기엔 비용과 시간이 많이 들거나 의존성이 길게 걸쳐져 있어 제대로 구현하기 어려울 경우, 가짜 객체를 만들어 사용하는 것)을 주입가능
    • but, setRepository를 호출해서 개발 중간에 변경할 일이 없음
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
    this.memberRepository = memberRepository;
}
  1. 생성자 인젝션 : 생성시점에 생성자에서 인젝션
    • 테스트 케이스 작성시 직접 주입해야해서 놓치지 않고 작성 가능
    • 생성자가 하나만 있을 경우 자동으로 인젝션 해줌
@Autowired
public MemberService(MemberRepository memberRepository) {
    this.memberRepository = memberRepository;
}

  • @Transactional : 메서드가 포함하고 있는 작업 중에 하나라도 실패할 경우 전체 작업을 취소(데이터 변경 시 필수)
    • @Transactional(readOnly = true) : 로 설정하면 JPA가 조회시 성능 최적화된 결과 반환(조회 메소드에 넣을 것을 권장)
    • 클래스에 @Transactional을 넣으면 전체(public)메소드에 적용
    • 메소드에 따로 넣으면 우선순위로 실행.
  • @AllArgsConstructor : 필드 모든것을 가지고 생성자 만들어주는 롬북 어노테이션
  • @RequiredArgsConstructor : final이 있는 필드만을 가지고 생성자 만들어줌(권장)

💡 알아두면 좋아요

  • memberRepository.findByName(member.getName());
    에서 findByname에 커서를 두고 Ctrl+Alt+V(변수 추출)하면
    List<Member> findMembers = memberRepository.findByName(member.getName());이렇게 만들수 있습니당!
  • 아이디 중복 회원 검증처리를 했어도 멀티스레드를 고려해서 DB멤버의 이름을 unique 제약조건 해주는 것을 권장

  • 인젝션 필드 주입 시 final로 작성할 것을 권장(변경할 일이 없으므로)

    • private final MemberRepository memberRepository
    • 컴파일 시점에 memberRepository 를 설정하지 않는 오류 체크 가능

🌼 회원 기능 테스트

  • 테스트 요구사항

    1. 회원가입을 성공해야 한다.
    2. 회원가입 할 때 같은 이름이 있으면 예외가 발생해야 한다
  • 스프링과 통합해서 테스트(jpa가 실제 db까지 도는것까지)

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.assertEquals;
import static org.junit.Assert.fail;

@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
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));
    }

    @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("여기까지 에러가 오면 안된다.");
    }

}

📒 1. 회원가입 테스트

  • 회원가입 테스트 실행시 insert문은 롤백되어 실행되지 않음

  • insert문이 어떻게 실행되는지 보고싶으면

1) @Rollback(value = false)를 추가하면 롤백처리 하지 않음


정확하게 확인하고 싶을 경우 h2 콘솔을 이용해서 확인 가능

2) @Autowired EntityManager em; 으로 EntityManager를 주입 후 em.flush 호출하면 영속성 컨텍스트의 변경 내용을 DB 에 반영 후 롤백 처리 되어 확인 가능


📌 flush

  • 영속성 컨텍스트의 변경 내용을 DB 에 반영하는 것(DB에 동기화)

  • Transaction commit 이 일어날 때 flush가 동작하는데, 이때 쓰기 지연 저장소에 쌓아 놨던 INSERT, UPDATE, DELETE SQL들이 DB에 날라간다. (영속성 컨텍스트를 비우는 것이 아니라 영속성 컨텍스트의 변경 사항들과 DB의 상태를 맞추는 작업)

  • 플러시의 동작 과정

    1. 변경을 감지한다. (Dirty Checking)
    2. 수정된 Entity를 쓰기 지연 SQL 저장소에 등록한다.
    3. 쓰기 지연 SQL 저장소의 Query를 DB에 전송한다. (등록, 수정, 삭제 Query)
  • flush가 발생한다고 해서 commit이 이루어지는 것이 아니고 flush 다음에 실제 commit이 일어난다.

    • 플러시가 동작할 수 있는 이유는 데이터베이스 트랜잭션(작업 단위)이라는 개념이 있기 때문. 트랜잭션이 시작되고 해당 트랜잭션이 commit 되는 시점 직전에만 동기화 (변경 내용을 날림) 해주면 되기 때문에, 그 사이에서 플러시 매커니즘의 동작이 가능한 것이다.

[참고] https://gmlwjd9405.github.io/2019/08/07/what-is-flush.html


📗 2. 중복회원 예외 테스트

  • assert.fail("message") : 무조건 실패로 AssertionError가 발생하고 다음 라인부터 실행하지 않음
    • 여기까지 에러가 온 것은 중복으로 인한 예외를 잡아주지 못한 것 (test code 잘못 작성한 것)
  • 예외 발생시 try~catch로 잡아주지 않아서 예외가 튕겨 나가 발생
  • try~catch문 작성하여 에러 발생시 return하면 테스트 성공
    @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);//중복이름으로 예외발생
        } catch (IllegalStateException e) {
            return;
        }
        //then
        fail("여기까지 에러가 오면 안된다.");
    }


📘 테스트 생성 관련 개념

  • Ctrl + Shift + T를 통해서 test 생성 가능 (service class test)
  • @RunWith(SpringRunner.class) : 스프링과 테스트 통합 (junit 실행할 때 스프링이랑 엮어서 실행할래)
  • @SpringBootTest : 스프링 부트 띄우고 테스트(없으면 @Autowired 다 실패-container안에서 test실행)
  • @Transactional : 반복 가능한 테스트 지원, 각각의 테스트를 실행할 때마다 트랜잭션을 시작하고 테스트가 끝나면 트랜잭션을 강제로 롤백 (이 어노테이션이 테스트 케이스에서 사용될 때만 롤백)
  • test는 given(이렇게 주어졌을 때) when(이렇게하면) then(이렇게 된다)형식으로 작성

  • assertEquals(expected, actual) : junit4에서 예상값이 실제값과 같은지 확인

  • @Test(expected = IllegalStateException.class) : 실행결과 IllegalStateException예외 발생시 예외 처리


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

  • 테스트는 케이스 격리된 환경에서 실행하고, 끝나면 데이터를 초기화하는 것이 좋다. 그런 면에서 메모리DB를 사용하는 것이 가장 이상적

  • 테스트 케이스를 위한 스프링 환경과, 일반적으로 애플리케이션을 실행하는 환경은 보통 다르므로 설정 파일을 다르게 사용하자

  1. test안에 resources dir생성 후 main에 있는 application.yml복사해서 붙여넣기
    • test실행 시에는 test디렉토리안의 파일이 우선권을 가지므로 main에 있는 yml파일은 무시됨
  1. https://www.h2database.com → Cheat Sheet → In-Memory → jdbc:h2:mem:test복사
  1. application.yml파일의 url에 붙여넣기 후 실행하면 test가능

🎀 spring boot는 별도의 설정이 없으면 메모리 모드로 돌리기 때문에 설정을 없애도 test 실행 가능

  • 추가로 ddl-auto 도 create-drop 모드로 동작한다. 따라서 데이터소스나, JPA 관련된 별도의 추가 설정을 하지 않아도 된다

💡 ddlAuto: create와 create-drop 차이

  • create : 내가 가진 entity를 다 drop 후 create한 다음 application실행

  • create-drop : 내가 가진 entity를 다 drop 후 create한 다음 application실행 후 마지막에 application 종료 시점에 drop query재실행(완전한초기화)


profile
Study Log 📂

0개의 댓글