Spring

[Spring boot & JPA 1] 회원 도메인 개발

gogi masidda 2024. 5. 1. 16:02

회원 리포지토리 개발

@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() { //여러건 검색은 em.createQuery로 JPQL 작성
        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();
    }
}
  • @Repository 애노테이션을 붙임으로써 컴포넌트 스캔 대상이 되어서 스프링 빈에 등록이 된다. 
  • @PersistenceContext: 엔티티 매니저를 스프링 빈으로 주입

회원 서비스 개발

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;

//    public MemberService(MemberRepository memberRepository) {
//        this.memberRepository = memberRepository;
//    }

    /**
     * 회원 가입
     */
    @Transactional
    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();
    }
    public Member findOne(Long memberId) {
        return memberRepository.findOne(memberId);
    }
}
  • @Service: 컴포넌트 스캔 대상이 되어 스프링 빈에 등록되게함.
  • 읽기는 @Transactional(readOnly = true)를 해두면 jpa가 최적화한다. 그래서 읽기가 더 많아서 큰곳에 readOnly = true로 설정하고, 쓰기에 따로 @Transactional을 붙인 것이다. 
  • 멀티 쓰레드 환경에서 같은 이름으로 동시에 가입하려는 사람이 있어서 둘다 검증을 통과할 수 있으므로, 데이터베이스에서 unique 제약조건을 걸어두면 좋다.
  • MemberRepository 주입은 필드 주입보다, 또 setter 주입보다, 생성자 주입으로 하는 것이 더 좋다. 
    • 생성자가 하나면, @Autowired를 생략할 수 있다.
    • 생성자 주입으로는 변경 불가능한 안전한 객체 생성이 가능하다.
    • 실행 중에 변경할 일이 없으므로 final로 하는 것이 좋다.
  • @RequiredArgsConstructor를 사용하면 final이 붙은 것은 자동으로 생성자를 만들어준다.
    • 그래서 주석으로 작성한 부분인 생성자가 필요없게 된다.

또한 앞에서 작성한 MemberRepository도 생성자 주입을 할 수 있다. 스프링 데이터 JPA를 사용하면 @PersistenceContext를 @Autowired로 쓸 수 있어서 MemberService에 적용한 것과 같다.

@Repository
@RequiredArgsConstructor
public class MemberRepository {
    
    private final 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() { //여러건 검색은 em.createQuery로 JPQL 작성
        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();
    }
}

그래서 이렇게 @RequiredArgsContructor를 사용하면 생성자 주입 간단하게 작성할 수 있게 된다.


회원 기능 테스트

  • 회원가입을 성공해야 한다.
  • 회원가입을 할 때 같은 이름이 있으면 예외가 발생한다.
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
class MemberServiceTest {

    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;
    @Autowired EntityManager em;

    @Test
    public void 회원가입() throws Exception {
        //given
        Member member = new Member();
        member.setName("memberA");

        //when
        Long savedId = memberService.join(member);

        //then
        Assert.assertEquals(member, memberRepository.findOne(savedId));
    }

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

        Member member2 = new Member();
        member2.setName("kim");
        //when
        memberService.join(member1);
        Assertions.assertThrows(IllegalStateException.class, ()-> {memberService.join(member2);});
    }
}
  • @RunWith(SpringRunner.class) : 스프링과 테스트 통합
  • @SpringBootTest : 스프링 부트 띄우고 테스트(이게 없으면 @Autowired 다 실패)
  • @Transactional : 반복 가능한 테스트 지원, 각각의 테스트를 실행할 때마다 트랜잭션을 시작하고 테스트가
    끝나면 트랜잭션을 강제로 롤백
728x90