[Spring Boot2][2] 4. 회원 도메인 개발

📌 구현 기능

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

📌 순서
1️⃣ 회원 엔티티 코드 다시 보기
2️⃣ 회원 리포지토리 개발
3️⃣ 회원 서비스 개발
4️⃣ 회원 기능 테스트



🏷 회원 리포지토리 개발

✔️ MemberRepository

package jpabook.jpashop.repository;

import jpabook.jpashop.domain.Member;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import javax.persistence.EntityManager;
import java.util.List;

// @Repository : 컴포넌트 스캔의 대상이 되어 자동으로 스프링 빈에 등록됨
@Repository
// @RequiredArgsConstructor : final이 있는 필드에만 생성자를 만들어 줌
@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() {
        // 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 : 스프링 빈으로 등록, JPA 예외를 스프링 기반 예외로 예외 변환
  • @PersistenceContext : 엔티티 메니저(EntityManager) 주입
  • @PersistenceUnit : 엔티티 메니터 팩토리(EntityManagerFactory) 주입


🏷 회원 서비스 개발

✔️ MemberService

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 : 컴포넌트 스캔의 대상이 되어 자동으로 스프링 빈에 등록됨
@Service
// @Transactional : JPA의 모든 데이터 변경이나 로직들은 가급적 트랜잭션 안에서 실행되어야 함
@Transactional(readOnly = true)
// @RequiredArgsConstructor : final이 있는 필드에만 생성자를 만들어 줌
@RequiredArgsConstructor
public class MemberService {
    // 멤버 리포지토리 가져오기
    private final MemberRepository memberRepository;

    /**
     * 회원 가입
     */
    // reaonly=false 로 우선권을 가짐
    @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);
    }

}
  • @Transactional : 트랜잭션, 영속성 컨텍스트
    • readOnly=true : 데이터의 변경이 없는 읽기 전용 메서드에 사용, 영속성 컨텍스트를 플러시 하지 않 으므로 약간의 성능 향상(읽기 전용에는 다 적용)
    • 데이터베이스 드라이버가 지원하면 DB에서 성능 향상
  • @Autowired : 생성자 Injection 많이 사용, 생성자가 하나면 생략 가능

📌 참고
실무에서는 검증 로직이 있어도 멀티 쓰레드 상황을 고려해서 회원 테이블의 회원명 컬럼에 유니크 제약 조건을 추가하는 것이 안전하다!

📌 필드 주입 대신 생성자 주입을 사용하자!

@Repository
    @RequiredArgsConstructor
    public class MemberRepository {
        private final EntityManager em;
... 
}
  • 변경 불가능한 안전한 객체 생성 가능
  • 스프링 데이터 JPA를 사용하면 EntityManager 도 주입 가능


🏷 회원 기능 테스트

📌 테스트 요구사항

  • 회원가입을 성공해야 한다.
  • 회원가입 할 때 같은 이름이 있으면 예외가 발생해야 한다(중복 예외 처리)

✔️ MemberServiceTest

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 javax.persistence.EntityManager;

import static org.junit.Assert.*;

// @RunWith(SpringRunner.class), @SpringBootTest : 이 두가지가 있어야 스프링과 통합해서 테스트를 진행할 수 있음
@RunWith(SpringRunner.class)
@SpringBootTest
// 각 JPA 같은 트랜잭션 안에서 같은 엔티티라면(id값이 똑같다면), 같은 영속성 컨텍스트에서 하나로 관리가 됨
@Transactional
public class MemberServiceTest {

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

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

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

        // then
        // 현재 member와 리포지토리에서 가져온 member가 같은 지 확인
        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("예외가 발생해야 한다.");

    }
}
  • @RunWith(SpringRunner.class) : 스프링과 테스트 통합
  • @SpringBootTest : 스프링 부트 띄우고 테스트(이게 없으면 @Autowired 다 실패)
  • @Transactional : 반복 가능한 테스트 지원, 각각의 테스트를 실행할 때마다 트랜잭션을 시작하고 테스트가 끝나면 트랜잭션을 강제로 롤백(이 어노테이션이 테스트 케이스에서 사용될 때만 롤백)


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

  • 테스트는 케이스 격리된 환경에서 실행하고, 끝나면 데이터를 초기화하는 것이 좋다.
    ➡️ 그런면에서 메모리 DB를 사용하는 것이 가장 이상적!
    ➡️ 추가로 테스트 케이스를 위한 스프링 환경과, 일반적으로 애플리케이션을 실행하는 환경은 보통 다르므로 설정 파일을 다르게 사용하자.

다음과 같이 간단하게 테스트용 설정 파일을 추가하면 된다😉


✔️ test/resources/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
  • 이제 테스트에서 스프링을 실행하면 이 위치에 있는 설정 파일을 읽는다!
    (만약 이 위치에 없으면 src/resources/application.yml 을 읽는다.)

좋은 웹페이지 즐겨찾기