2 분 소요


엔티티 설계 과정은 최근에 올린 포스트와 거의 일치하기 때문에 생략…
[엔티티 설계 포스트]


애플리케이션 아키텍처

1

계층형 구조 사용

  • controller, web: 웹 계층
  • service: 비즈니스 로직, 트랜잭션 처리
  • repository: JPA를 직접 사용하는 계층, 엔티티 매니저 사용
  • domain: 엔티티가 모여 있는 계층, 모든 계층에서 사용

개발 순서: service, repository -> test case -> web


MemberRepository

@Repository
public class MemberRepository {

    @PersistenceContext
    private EntityManager em;

    public void save(Member member) {
        em.persist(member);
    }

    public void findOne(Long id) {
        return em.find(Member.class, id);
    }

    public List<Member> findAll() {
        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) 주입
    • EntityManagerFactory를 생성하지 않아도 알아서 다 만들어준다.
  • @PersistenceUnit: 엔티티 매니저 팩터리(EntityManagerFactory)주입
    • @PersistenceContext가 있기 때문에 잘 사용하지 않는다.


MemberService

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

    private final 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 Member findOne(Long memberId) {
        return memberRepository.findOne(memberId);
    }

    public List<Member> findMembers() {
        return memberRepository.findAll();
    }
}
  • @Service
  • @Transactional: 트랜잭션, 영속성 컨텍스트
    • readOnly=true: 데이터의 변경이 없는 읽기 전용 메서드에 사용.
      • 영속성 컨텍스트를 플러시 하지 않으므로 약간의 성능 향상
      • 데이터베이스 드라이버가 지원하면 DB에서 성능 향상

[참고]: 스프링 데이터 JPA를 사용하면 EntityManager도 주입이 가능

@Repositroy
@RequiredArgsConstructor
public class MemberRepository {

    private final EntityManager em;
    ...
}


회원 기능 테스트

요구사항

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

테스트 코드

@SpringBootTest
@Transactional
public class MemberServiceTest {

    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;

    @Test
    public void 회원가입() {

        // given
        Member member = new Member();
        member.setName("kim");

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

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

    @Test
    public void 중복_회원_예외() {

        // 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("예외가 발생해야 한다.");
    }
}
  • @SpringBootTest: 스프링 부트 띄우고 테스트(이 애노테이션이 없으면 @Autowired 실패)
  • @Transactional: 반복 가능한 테스트 지원, 각각의 테스트를 진행할 때마다 트랜잭션을 시작하고 테스트가 끝나면 트랜잭션을 강제로 롤백(테스트 케이스에서 사용될 때만 롤백)

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

테스트 케이스 설정

test/resources/application.yml

spring:
    datasource:
      url: jdbc:h2:mem:test
      username: sa
      password:
      driver-class-name: org.h2.Driver

    jpa:
      hibernate:
        ddl-auto: create
      properties:
        hibernate:
#          (system.out)
#          show_sql: true
          format_sql: true

logging:
  level:
#    log
    org.hibernate.SQL: debug
    org.hibernate.type: trace


<출처 : 인프런 - 실전! 스프링 부트와 JPA 활용1 (김영한)>

댓글남기기