
출처 : 인프런 > 실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 강의를 듣고 작성한 글입니다.
강의 링크 : 실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발
구현 기능
- 회원 등록
- 회원 목록 조회
순서
- 회원 엔티티 코드 다시 보기
- 회원 리포지토리 개발
- 회원 서비스 개발
- 회원 기능 테스트
회원 리포지토리 코드
src\main\java\jpabook\jpashop\repository\MemberRepository.java
package jpabook.jpashop.repository;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jpabook.jpashop.domain.Member;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository // Spring에서 제공하는 @Repository를 사용하면 Component Scan에 의해서 자동으로 Spring 빈으로 관리가 됨
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); // 첫 번째 type, 두 번째 PK
}
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
// parameter binding
public List<Member> findByName(String name) {
return em.createQuery("select m from Member m where m.name =:name", Member.class)
.setParameter("name", name)
.getResultList();
}
}
기술 설명
기능 설명
회원 서비스 코드
src\main\java\jpabook\jpashop\service\MemberService.java
package jpabook.jpashop.service;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional(readOnly = true)
public class MemberService {
@Autowired // field injection
MemberRepository memberRepository;
/**
* 회원 가입
*/
@Transactional // 변경 @Transactional(readOnly = false)
public Long join(Member member) {
validateDuplicateMember(member); // 중복 회원 검증
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
// EXCEPTION
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);
}
}
기술 설명
기능 설명
✅ 참고: 실무에서는 검증 로직이 있어도 멀티 쓰레드 상황을 고려해서 회원 테이블의 회원명 컬럼에 유니크 제약 조건을 추가하는 것이 안전하다.
✅ 참고: 스프링 필드 주입 대신에 생성자 주입을 사용하자.
필드 주입
public class MemberService {
@Autowired
MemberRepository memberRepository;
...
}
생성자 주입
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
...
}
lombok
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
...
}
✅ 참고: 스프링 데이터 JPA를 사용하면 EntityManager 도 주입 가능
@Repository
@RequiredArgsConstructor
public class MemberRepository {
private final EntityManager em;
...
}
MemberService 최종코드
src\main\java\jpabook\jpashop\service\MemberService.java
package jpabook.jpashop.service;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
/**
* 회원 가입
*/
@Transactional // 변경 @Transactional(readOnly = false)
public Long join(Member member) {
validateDuplicateMember(member); // 중복 회원 검증
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
// EXCEPTION
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);
}
}
테스트 요구사항
회원가입 테스트 코드
src\test\java\jpabook\jpashop\service\MemberServiceTest.java
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.annotation.Rollback;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;
import static org.junit.Assert.*;
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class MemberServiceTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
// @Rollback(false)
public void 회원가입() throws Exception {
// given
Member member = new Member();
member.setName("kim");
// when
Long saveId = memberService.join(member);
// then
assertEquals(member, memberRepository.findOne(saveId));
}
@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("예외가 발생해야 한다.");
}
}
기술 설명
기능 설명
✅ 참고: 테스트 케이스 작성 고수 되는 마법: Given, When, Then (http://martinfowler.com/bliki/GivenWhenThen.html) 이 방법이 필수는 아니지만 이 방법을 기본으로 해서 다양하게 응용하는 것을 권장한다.
테스트는 케이스 격리된 환경에서 실행하고, 끝나면 데이터를 초기화하는 것이 좋다. 그런 면에서 메모리 DB를 사용하는 것이 가장 이상적이다. 추가로 테스트 케이스를 위한 스프링 환경과, 일반적으로 애플리케이션을 실행하는 환경은 보통 다르므로 설정 파일을 다르게 사용하자. 다음과 같이 간단하게 테스트용 설정 파일을 추가하면 된다.
src\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
logging.level:
org.hibernate.SQL: debug
# org.hibernate.orm.jdbc.bind: trace
# org.hibernate.type: trace #스프링 부트 2.x, hibernate5
# org.hibernate.orm.jdbc.bind: trace #스프링 부트 3.x, hibernate6
이제 테스트에서 스프링을 실행하면 이 위치에 있는 설정 파일을 읽는다.
(만약 이 위치에 없으면 src/resources/application.yml 을 읽는다.)
스프링 부트는 datasource 설정이 없으면, 기본적을 메모리 DB를 사용하고, driver-class도 현재 등록된 라이브러를 보고 찾아준다. 추가로 ddl-auto 도 create-drop 모드로 동작한다. 따라서 데이터소스나, JPA 관련된 별도의 추가 설정을 하지 않아도 된다.