회원 도메인 개발

회원 리포지토리 개발

// 컴포넌트 스캔에 의해 자동으로 빈으로 관리된다.
@Repository
public class MemberRepository {

    // 스프링이 EntityManager를 주입해준다.
    @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() {
        // 리스트는 jpql을 사용해야 한다.
        // sql은 테이블을 대상으로, 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) // :name 파라미터 바인딩
                .getResultList();
    }
}

@Repository

  • 스프링 빈으로 등록된다.

  • JPA 예외를 스프링 기반 예외로 변환한다.

@PersistenceContext

  • EntityManager를 주입한다.

  • 직접 긴 코드를 쓸 필요가 없어졌다.

@PersistenceUnit

@Repository
public class MemberRepository {

    // EntityManagerFactory를 직접 주입받고 싶다면 아래를 사용한다.
    @PersistenceUnit
    private EntityManagerFactory emf;
}
  • EntityManagerFactory를 주입한다.

회원 서비스 개발

// 컴포넌트 스캔에 의해 자동으로 빈으로 등록된다.
@Service
// JPA의 모든 데이터 변경과 로직은 트랜잭션 내에서 실행되어야 한다.
@Transactional(readOnly = true)
public class MemberService {

    @Autowired
    private MemberRepository memberRepository;

    @Transactional
    public Long join(Member member) {
        // 중복 회원 검증
        validateDuplicateMember(member);
        memberRepository.save(member);

        return member.getId();
    }

    private void validateDuplicateMember(Member member) {
        // WAS가 동시에 여러 개 떠서 동시에 validate를 시도하면 문제가 생긴다.
        // 실무에서는 이런 멀티 스레드 문제를 해결해줘야 한다.
        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

  • javax와 springframework 두 가지 종류가 있다.

    • 이미 스프링에 의존이 많이 되어있기 때문에 javax보다는 springframework를 import하는 게 좋다.

    • 그래야 쓸 수 있는 옵션도 더 다양하다.

  • readOnly = true

    • 더티 체킹을 안하거나 DB 옵션에 따라 읽기 전용 모드로 읽는 등 성능상 이점이 있다.

    • 기본을 true로 두고 변경이 필요한 곳에만 false로 달아두면 된다.

조회가 아니라면 true일 때 데이터 변경이 안되므로 주의하자. 커맨드성이 강해서 조회가 거의 없다면 기본 값으로 두는 게 더 좋다.

@Autowired

  • @Autowired로 필드 주입을 하면 엑세스할 방법이 없어서 다른 의존성으로 바꿔치기 할 수가 없다.

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

    private MemberRepository memberRepository;

    public void setMemberRepository(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

}
  • setter 주입을 쓰면 직접 mock 등을 주입해줄 수 있는 장점이 있다.

  • 애플리케이션 동작 시점에 누군가가 setter에 접근해서 바꿀 수 있는 위험이 있다.

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

    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}
  • 생성자 주입을 하면 생성된 이후에는 바꿀 수 없어서 좋다.

  • 객체 생성 시 의존성을 넣어주도록 컴파일 타임에 강제한다.

    • 주입해야 한다는 사실을 안 놓치고 안전하게 개발할 수 있다.

  • 스프링 최신 버전에서는 생성자가 하나만 있는 경우 @Autowired가 없어도 알아서 생성해준다.

  • 이때 private final로 선언해주는 것이 좋다.

    • 값 세팅을 안하면 컴파일 시점에 체크해주기 때문이다.

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

    private final MemberRepository memberRepository;
}
  • 롬복 @AllArgsConstructor를 쓰면 생성자를 대체할 수 있다.

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

    private final MemberRepository memberRepository;
}
  • @RequiredArgsConstructor는 final인 필드만 생성자를 만들어준다.

  • @AllArgsConstructor보다 추천한다.

@Repository
@RequiredArgsConstructor
public class MemberRepository {

    private final EntityManager em;
}
  • 스프링 데이터 JPA 덕분에 EntityManager도 생성자 주입으로 받을 수 있다.

회원 기능 테스트

회원 가입

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

    @Autowired
    MemberService memberService;

    @Autowired
    MemberRepository memberRepository;

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

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

        // then
        // 같은 트랜잭션에 묶었으므로 동일한 영속성 컨텍스트에
        // 같은 PK를 가질 경우 똑같은 데이터로 취급한다.
        assertEquals(member, memberRepository.findOne(savedId));
    }
}

예제에서는 이해를 돕기 위해 통합적으로 테스트를 할 것이다.

  • @RunWith(SpringRunner.class)

    • 스프링과 테스트를 통합한다.

  • @SpringBootTest

    • 스프링 부트를 띄워서 테스트한다.

    • 이게 없으면 @Autowired는 다 실패한다.

  • @Transactional

    • 반복 가능한 테스트를 지원한다.

      • 즉, 각 테스트 실행마다 테스트가 끝나면 트랜잭션을 롤백한다.

      • 자동 롤백은 테스트 케이스에서만 적용된다.

select만 나가고 실제 회원 가입을 하면서 생겨야 할 insert 쿼리는 나가지 않았다.

@Repository
@RequiredArgsConstructor
public class MemberRepository {

    public void save(Member member) {
        // insert 쿼리가 나가지 않은 이유
        em.persist(member);
    }
}
  • DB 전략마다 다르지만 기본적으로 persist만 하면 쿼리가 나가지 않는다.

  • 트랜잭션이 commit될 때 flush 되면서 쿼리가 나가는 것이기 때문이다.

@RunWith(SpringRunner.class)
@SpringBootTest
// 기본적으로 롤백이 동작한다.
@Transactional
public class MemberServiceTest {

    @Test
    @Rollback(value = false)
    public void 회원가입() {
    ...
    }
}
  • @Transactional이 자동으로 롤백을 하니까 당연히 insert 쿼리를 보내지 않는 것이다.

    • 정확하게는 영속성 컨텍스트가 flush를 하지 않는다.

  • @Rollback에 false 옵션을 주어야 커밋이 실행된다.

정상적으로 insert 쿼리가 나간 것을 볼 수 있다.

중복 회원 예외

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

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

        Member member2 = new Member();
        member2.setName("kim");

        // when
        memberService.join(member1);
        // 똑같은 이름을 넣었으니 여기서 예외를 던져야 한다.
        memberService.join(member2);

        // then
        // 여기까지 오지 않고 위에서 예외가 이미 발생해야 할 때 쓴다.
        fail("예외가 발생해야 한다.");
    }
}
  • expected에 터질 예정인 예외를 써준다.

테스트를 위한 설정

  • 테스트는 완전히 격리된 환경에서 실행하는 것이 좋다.

  • 끝나면 데이터를 초기화하기 위해 메모리 DB를 사용한다.

testresources를 따로 만들어두면 테스트 시에 mainresources보다 우선권을 가진다.

spring:
  datasource:
    url: jdbc:h2:mem:test # 인 메모리 방식으로 바꿔준다.
    username: sa
    password:
    driver-class-name: org.h2.Driver
  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        format_sql: true
logging.level:
  org.hibernate.SQL: debug

하이버네이트는 jvm 위에 띄워 인 메모리 방식으로도 사용할 수 있다.

spring:
logging.level:
  org.hibernate.SQL: debug

심지어 이렇게 간단하게만 설정해도 돌아간다. 스프링 부트는 따로 설정이 없다면 자동으로 메모리 모드로 돌려주기 때문이다.

spring:
  jpa:
    hibernate:
      ddl-auto: create  

스프링은 기본적으로 create-drop으로 돌아간다.

  • create

    • 먼저 drop한 후 애플리케이션을 실행한다.

  • create-drop

    • create와 똑같이 동작한 뒤에 종료 시점에 다시 drop 시킨다.

    • 인 메모리는 어차피 WAS가 내려가면 다 사라지기 때문에 중요하진 않다.

Last updated