회원 도메인 개발
- 김영한님의 실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 강의를 기반으로 회원 엔티티, 리포지토리, 서비스 계층의 개발 과정과 테스트 작성을 정리함
개발 개요
구현 기능
- 회원 등록
- 중복 회원을 검증함
- 회원 정보를 저장함
- 회원 조회
- 전체 회원을 조회함
- 회원 ID로 단건 조회함
- 이름으로 회원을 검색함
개발 순서
- 회원 엔티티 확인
Member.java코드를 확인함
- 회원 리포지토리 개발
MemberRepository.java를 개발함- 데이터 접근 계층을 구현함
- 회원 서비스 개발
MemberService.java를 개발함- 비즈니스 로직을 구현함
- 회원 기능 테스트
MemberServiceTest.java를 작성함- 구현한 기능을 검증함
회원 리포지토리 개발
리포지토리 역할
- 데이터 저장
save메서드를 통해 엔티티를 저장함
- 데이터 조회
findOne,findAll,findByName메서드를 제공함
- EntityManager 사용
- JPA의 핵심인
EntityManager를 주입받아 사용함
- JPA의 핵심인
MemberRepository 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Repository
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);
}
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를 주입받음 - 트랜잭션과 생명주기를 스프링이 관리함
- 스프링이 생성한
@PersistenceUnitEntityManagerFactory를 직접 주입받을 때 사용함
메서드별 기능
-
save()- 회원 저장1 2 3
public void save(Member member) { em.persist(member); }
- 엔티티를 영속성 컨텍스트에 저장함
- 트랜잭션 커밋 시점에 DB에 INSERT 쿼리가 실행됨
-
findOne()- 단건 조회1 2 3
public Member findOne(Long id) { return em.find(Member.class, id); }
- PK(Primary Key)를 사용하여 엔티티를 조회함
- 1차 캐시에서 먼저 조회하여 성능을 최적화함
-
findAll()- 전체 조회1 2 3 4
public List<Member> findAll() { return em.createQuery("select m from Member m", Member.class) .getResultList(); }
- JPQL을 사용하여 엔티티 대상으로 쿼리함
- SQL이 아닌 객체 지향 쿼리를 사용함
-
findByName()- 이름으로 검색1 2 3 4 5
public List<Member> findByName(String name) { return em.createQuery("select m from Member m where m.name = :name", Member.class) .setParameter("name", name) .getResultList(); }
- 파라미터 바인딩(
:name)을 사용하여 SQL Injection을 방지함 - 중복 회원 검증 로직에서 주로 사용됨
- 파라미터 바인딩(
EntityManager 주입 방식 비교
-
전통적 방식 (@PersistenceContext)
1 2 3 4 5 6 7
@Repository public class MemberRepository { @PersistenceContext private EntityManager em; }
-
Spring Data JPA 사용 시 (권장)
1 2 3 4 5 6 7
@Repository @RequiredArgsConstructor public class MemberRepository { private final EntityManager em; }
- 스프링 부트와 Spring Data JPA를 사용하면
@PersistenceContext대신 생성자 주입을 사용할 수 있음
- 스프링 부트와 Spring Data JPA를 사용하면
회원 서비스 개발
서비스 계층 역할
- 비즈니스 로직
- 회원 가입, 중복 검증, 회원 조회 등 로직을 수행함
- 트랜잭션 관리
@Transactional을 사용하여 데이터 일관성을 보장함
- Repository 호출
- 데이터 접근 계층을 호출하여 데이터를 조작함
MemberService 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@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 List<Member> findMembers() {
return memberRepository.findAll();
}
/**
* 회원 단건 조회
*/
public Member findOne(Long memberId) {
return memberRepository.findOne(memberId);
}
}
@Service- 서비스 계층의 컴포넌트임을 명시함
@TransactionalreadOnly=true- 읽기 전용 트랜잭션으로 최적화함 (조회 메 서드에 적용)
readOnly=false- 쓰기 가능한 트랜잭션 (등록, 수정, 삭제 메서드에 적용)
-
@Transactional(readOnly = true)효과- 영속성 컨텍스트 flush 생략
- 변경 감지를 위한 스냅샷 비교를 하지 않아 성능이 향상됨
- DB 드라이버 최적화
- 읽기 전용 모드로 동작하여 리소스를 절약함
- 영속성 컨텍스트 flush 생략
생성자 주입 방식을 사용하는 이유
- 필드 주입 대신 생성자 주입을 권장함
- 필드 주입은 테스트가 어렵고 불변성을 보장할 수 없음
- 생성자 주입은
final키워드를 사용하여 불변 객체를 보장하고, 테스트 시 Mock 객체 주입이 용이함
- Lombok
@RequiredArgsConstructor적용final이 붙은 필드의 생성자를 자동으로 생성해주어 코드를 깔끔하게 유지할 수 있음
비즈니스 로직 흐름
-
회원 가입 프로세스
- join 호출
- 컨트롤러에서
join(member)를 호출함
- 컨트롤러에서
- 트랜잭션 시작
- 서비스 계층 진입 시 트랜잭션이 시작됨
- 중복 검증
validateDuplicateMember메서드로 이름 중복을 확인함- 중복 시
IllegalStateException예외가 발생함
- 저장
- 검증 통과 시
memberRepository.save(member)를 호출함
- 검증 통과 시
- 트랜잭션 커밋
- 메서드 종료 시 트랜잭션이 커밋되고 DB에 반영됨
- join 호출
중복 검증 로직 주의사항
- 멀티 스레드 환경
- 동시에 같은 이름으로 가입을 시도하면 중복 검증을 통과할 수 있음
- 해결 방법
- 데이터베이스의 회원명 컬럼에
UNIQUE제약조건을 추가해야 함
- 데이터베이스의 회원명 컬럼에
회원 기능 테스트
테스트 요구사항
- 정상 케이스
- 회원 가입이 성공하고, 저장된 회원이 조회되어야 함
- 예외 케이스
- 중복 회원 가입 시 예외가 발생해야 함
MemberServiceTest 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@SpringBootTest
@Transactional
public class MemberServiceTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
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
public void 중복_회원_예외() throws Exception {
// Given
Member member1 = new Member();
member1.setName("kim");
Member member2 = new Member();
member2.setName("kim");
// When
memberService.join(member1);
// Then
assertThrows(IllegalStateException.class, () ->
memberService.join(member2));
}
}
@SpringBootTest- 스프링 부트 통합 테스트를 실행함
- 스프링 컨테이너를 띄우고 의존성을 주입받음
@Transactional(테스트 환경)- 각 테스트 실행 후 자동으로 롤백함
- 반복적인 테스트 실행을 가능하게 함
Given-When-Then 패턴
- Given (준비)
- 테스트를 위한 데이터와 환경을 준비함
- When (실행)
- 테스트할 동작을 수행함
- Then (검증)
- 실행 결과를 검증함
JUnit 5 예외 테스트
assertThrows사용- JUnit 5에서는
assertThrows메서드를 사용하여 예외 발생을 검증함 - 람다 표현식을 통해 예외가 발생하는 로직을 감쌈
- JUnit 5에서는
연습 문제
-
JPA에서 SQL과 JPQL 쿼리의 주요 차이점은 무엇인가요?
a. 테이블 기반과 엔티티 객체 기반의 차이
- JPQL은 데이터베이스 테이블이 아닌 엔티티 객체를 대상으로 쿼리하며, SQL과 문법 차이가 있음
- JPA는 이 쿼리를 적절한 SQL로 변환하여 실행함
-
JPA를 사용하여 데이터를 수정하는 메서드에
@Transactional어노테이션이 필요한 주된 이유는 무엇인가요?a. 데이터 변경 작업의 일관성 및 영속성 컨텍스트 관리
- JPA는 트랜잭션 범위 안에서 영속성 컨텍스트를 관리하고 데이터 변경사항을 DB에 반영함
- 데이터 정합성을 유지하기 위해 트랜잭션 설정은 필수적임
-
Spring에서 서비스와 같은 클래스에서 의존성을 주입받을 때 권장되는 방식은 무엇일까요?
a. 생성자 주입 (Constructor Injection)
- 생성자 주입은 필수 의존성을 명확히 하고 객체의 불변성을 확보하여 테스트하기 용이함
- 최근 Spring에서 가장 권장하는 주입 방식임
-
Spring Boot 애플리케이션 테스트 시, In-Memory 데이터베이스(예: H2)를 사용하는 주된 이점은 무엇인가요?
a. 테스트 간 데이터 독립성 및 빠른 초기화
- 테스트 시작 시 DB를 초기화하고 종료 후 롤백하여 다른 테스트에 영향을 주지 않음
- 빠르고 독립적인 테스트 환경 구축에 유리함
-
Spring 테스트 클래스에서
@Transactional어노테이션의 기본 동작은 무엇인가요?a. 각 테스트 메서드 후 데이터베이스 롤백
- Spring 테스트의
@Transactional은 기본적으로 각 테스트 메서드가 끝날 때 변경사항을 DB에 반영하지 않고 자동으로 롤백시켜 테스트 데이터가 남는 것을 방지함
- Spring 테스트의
요약 정리
- 회원 엔티티는 이름(
name)과 임베디드 타입인 주소(Address)를 가지며,@Entity로 매핑함 - 회원 리포지토리는
EntityManager를 주입받아 회원을 저장(persist)하고 조회(find, JPQL)하는 역할을 수행함 - 회원 서비스는
@Transactional을 적용하여 데이터 변경을 관리하며, 중복 회원 검증과 같은 핵심 비즈니스 로직을 처리함 - 회원 테스트는
@SpringBootTest와@Transactional을 사용하여 실제 DB 환경과 유사하게 통합 테스트를 수행하며, 예외 상황(IllegalStateException)을 검증함