본문 바로가기
Dev/JPA

[JPA] JPA 활용 I - 회원 도메인 개발

by dev_jsk 2021. 9. 16.
728x90
반응형

회원 기능인 회원 등록, 조회 기능을 구현해보자

회원 레포지토리 개발

파일 경로

main/java/jpabook/jpashop/repository/MemberRepository.java

소스 구현

package jpabook.jpashop.repository;

@Repository
public class MemberRepository {
    
    @PersistenceContext
    private EntityManager em;

    /**
     * save Member
     * @param member
     */
    public void save(Member member) {
        em.persist(member);
    }

    /**
     * find one Member
     * @param id
     * @return Member
     */
    public Member findOne(Long id) {
        return em.find(Member.class, id);
    }

    /**
     * find all Member
     * @return List<Member>
     */
    public List<Member> findAll() {
        // JPQL은 엔티티를 대상으로 쿼리, SQL은 테이블을 대상으로 쿼리
        return em.createQuery("select m from Member m", Member.class)
                .getResultList();
    }

    /**
     * find Member by name
     * @param name
     * @return List<Member>
     */
    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
    - Spring Boot Application 실행 시 Component 스캔을 하여 스프링 Bean으로 등록하는데 @Repository@Component의 하위기 때문에 스프링 Bean으로 자동 등록된다.
    - JPA 예외를 스프링 기반 예외로 예외 변환
  • @PersistenceContext
    - 스프링이 생성한 JPA의 EntityManager를 주입하는 어노테이션
    - @PersistenceUnit은 EntityManagerFactory를 주입하는 어노테이션

회원 서비스 개발

파일 경로

main/java/jpabook/jpashop/service/MemberService.java

소스 구현

package jpabook.jpashop.service;

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

    private final MemberRepository memberRepository;

    /**
     * join Member
     * @param member
     * @return Long
     */
    @Transactional  // 클래스에 선언 후 메소드에 선언 시 메소드 선언에 우선권
    public Long join(Member member) {
        // 회원 중복 확인
        validateDuplicateMember(member);
        memberRepository.save(member);
        // 아직 DB에 저장되기 전임에도 영속성 컨텍스트에 해당 key값으로 저장되어 있기 때문에 그 key를 PK 필드에 셋팅해준다.
        // 따라서 엔티티 PK필드를 꺼낼때 항상 값이 있다는 것이 보장된다.
        return member.getId();
    }

    /**
     * check duplicate Member
     * @param member
     */
    private void validateDuplicateMember(Member member) {
        List<Member> findMembers = memberRepository.findByName(member.getName());
        // 회원 조회결과가 있다면 중복
        // 만약의 경우 여러곳에서 동시에 같은 이름으로 가입 시 중복 체크가 안될 수 있다.
        // 이러한 경우를 방지하기 위해 name에 unique 제약조건을 설정하는 것을 권장한다.
        if(!findMembers.isEmpty()) {
            throw new IllegalStateException("이미 존재하는 회원입니다.");
        }
    }

    /**
     * find all Member
     * @return List<Member>
     */
    public List<Member> findMembers() {
        return memberRepository.findAll();
    }

    /**
     * find one Member
     * @param memberId
     * @return
     */
    public Member findOne(Long memberId) {
        return memberRepository.findOne(memberId);
    }
}
  • @Service
    - 비즈니스 로직을 수행하는 클래스임을 나타내는 어노테이션
  • @Transactional
    - 트랜잭션을 처리, 관리해주는 어노테이션, JPA의 모든 데이터 변경관련 로직은 트랜잭션 내에서 실행되어야 한다.
    - javaxspringframework가 각각 제공하는데 이미 스프링 관련 의존성을 사용한 로직이 많기 때문에 springframework가 제공하는 것을 사용하면 더 많은 옵션을 사용할 수 있다.
    - readOnly=true로 설정하면 데이터를 조회만 하기 때문에 영속성 컨텍스트를 flush하지 않아 약간의 성능이 향상된다. 하지만 데이터 변경은 반영되지 않는다.
  • @Autowird
    - 스프링 Bean을 찾아 주입해주는 어노테이션
  • @RequiredArgsConstructor
    - Lombok에서 제공하는 어노테이션으로 final키워드가 선언된 변수로 생성자를 만든다.
    - @AllArgsConstructor는 선언된 모든 변수로 생성자를 만든다.

다양한 Injection

Field Injection

@Autowired
private MemberRepository memberRepository;
  • 필드를 선언하여 주입하는 방법
  • 단점 : 변경이 불가능하다.

Setter Injection

private MemberRepository memberRepository;

@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
    this.memberRepository = memberRepository;
}
  • Setter를 생성하여 주입하는 방법
  • 장점 : 테스트 코드 작성 시 mock 객체를 주입하여 사용할 수 있다.
  • 단점 : 애플리케이션 실행 시 임의로 변경이 가능하여 위험성이 존재하고 애플리케이션 동작 중 변경 가능성이 적다.

Constructor Injection

private MemberRepository memberRepository;

@Autowired
public MemberService(MemberRepository memberRepository) {
   this.memberRepository = memberRepository;
}
  • 생성자를 사용하여 주입하는 방법
  • 장점
    - 임의로 변경 가능성 없다.
    - 생성자 사용 주입 시 주입 할 변수에 final키워드를 추가하면 컴파일 시점에 해당 변수 미사용 오류를 체크할 수 있다.
  • 생성자가 하나면 @Autowired를 생략할 수 있다.

Lombok

@Repository
@RequiredArgsConstructor
public class MemberRepository {
    private final EntityManager em;
}
  • 스프링 데이터 JPA를 사용하면 @PersistenceContext@Autowired로 변경하여 사용할 수 있도록 제공한다.

회원 기능 테스트

테스트 요구사항

  • 회원가입을 성공해야 한다.
  • 회원가입 시 중복된 이름이 있으면 예외가 발생해야 한다.

테스트 구조

given ~ when ~ then 방식으로 '어떤 것이 주어졌을때(=given) 이렇게 하면(=when) 이렇게 된다.(=then)' 라는 의미다.

테스트 시 특이사항

테스트 진행 시 자동으로 롤백되기 때문에 진행 로그에서 insert SQL이 보이지 않는다. insert SQL을 확인하고 싶으면 메소드에 @Rollback(value = false)를 선언하여 롤백을 진행하지 않거나 EntityManager를 주입받아 EntityManager.flush()를 호출하여 영속성 컨텍스트를 비워 표시할 수 있다. 이때 트랜잭션 롤백은 false로 지정하지 않으면 발생한다.

테스트 파일 경로

test/java/jpabook/jpashop/service/MemberServiceTest.java

테스트 구현

package jpabook.jpashop.service;

@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class MemberServiceTest {

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

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

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

        // then
        // em.flush();
        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
    - JUnit 실행 시 스프링과 엮어서 실행
  • @SpringBootTest
    - 스프링 부트 컨테이너에서 테스트 실행, 해당 어노테이션 없을 경우 @Autowired 실패
  • @Transactional
    - 테스트를 실행할 때마다 트랜잭션을 시작하고 테스트가 끝나면 트랜잭션을 강제로 롤백한다. (테스트 케이스에서 사용시)

테스트 결과

회원가입 테스트

회원가입 메소드에 테스트 성공 마커가 표시됐다.

중복회원 예외 테스트

중복회원 예외 메소드에 테스트 성공 마커가 표시됐다.

중복회원 예외 테스트 실패

@Test(expected) 미설정 및 별도의 try-catch 처리를 하지 않았을 경우 테스트 실패 결과

중복회원 예외 테스트 내 fail()

fail() 함수 동작 결과

테스트 케이스를 위한 메모리 데이터베이스 설정

  • 테스트 케이스는 격리된 환경에서 실행하고 끝나면 데이터를 초기화하는 것이 좋다. 그런면에서 메모리 DB를 사용하는 것이 가장 이상적이다.
  • 테스트 케이스를 위한 환경과 일반 애플리케이션 실행 환경은 보통 다르므로 설정파일을 각각 다르게 사용한다.
    - 일반 애플리케이션 설정 파일 경로 : main/resources/application.yml
    - 테스트 케이스 설정 파일 경로 : test/resources/application.yml
  • 스프링 부트는 기본적으로 메모리DB를 사용하고 driver-class도 현재 라이브러리를 보고 찾아준다. 또한 ddl-auto옵션도 create-drop모드로 동작한다.
# 테스트용 설정 파일
# spring:
  # datasource:
  #   url: jdbc:h2:tcp://localhost/~/jpashop
  #   username: sa
  #   password:
  #   driver-class-name: org.h2.Driver

  # jpa:
  #   hibernate:
  #     ddl-auto: create
  #   properties:
  #     hibernate:
  #       # '[show_sql]': auto
  #       '[format_sql]': true

logging:
  level:
    '[org.hibernate.SQL]': debug
    '[org.hibernate.type]': trace
728x90
반응형

댓글