Intro
안녕하세요. 환이s입니다👋
이전 포스팅에서 프로젝트에 필요한 엔티티 설계를 진행했습니다. 이어서 요구사항 구현 기능으로 회원 등록과 목록 조회 기능을 코드로 구현하고, 테스트 코드를 작성하여 기능이 제대로 작동하는지 확인해 보겠습니다🙂
회원 도메인 개발 - 리포지토리 개발
리포지토리에서는 엔티티매니저(EntityManager)를 통해 데이터베이스에 대한 CRUD 작업을 수행할 수 있습니다.
엔티티매니저는 일반적으로 개발자가 직접 인스턴스화하지 않고, 스프링 부트와 같은 프레임워크에서 DI(Dependency Injection) 방식으로 주입받아 사용하는데 크게 총 3가지 방법을 소개해드리겠습니다.
1️⃣ @Autowired
3가지 방법 중 가장 간단한 방법인 필드 주입 방법입니다. @Autowired 어노테이션을 붙여주면 자동으로 의존 관계가 주입됩니다.
@Autowired
private EntityManager em;
2️⃣ @PersistenceContext
JPA가 제공하는 표준 어노테이션입니다. 이 어노테이션으로 엔티티매니저를 스프링이 생성한 엔티티매니저에 주입합니다.
@PersistenceContext
private EntityManager em;
3️⃣ @RequiredArgsConstructor
final로 선언된 필드에 대한 생성자를 자동 생성해 주는 Lombok 어노테이션입니다.
@Repository // 컴포넌트 스캔으로 자동으로 스프링에서 스프링 빈으로 관리
@RequiredArgsConstructor
public class MemberRepository {
private final EntityManager em
위 세 가지 방법 중에 저는 @RequiredArgsConstructor 방법을 권장합니다.
그 이유는 final 키워드를 사용하기에 생성자로 인해 인스턴스가 생성될 대 1번만 할당되는데, 값이 한번 할당되면 변경할 수 없기에 객체의 불변성(Immutability)이 보장됩니다.
추가로 Spring 4.3부터는 @Autowired가 생략 가능해서 최근에는 생성자를 딱 1개 두거나 Lombok 라이브러리를 사용하면 생성자 또한 생략 가능해서 코드가 깔끔해집니다.
코드에 적용해서 회원 관리 로직을 작성해 보겠습니다.
✅MemberRepository
import jpabook.jpashop.domain.Member;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceUnit;
import java.util.List;
// 컴포넌트 스캔으로 자동으로 스프링에서 스프링 빈으로 관리
@Repository
@RequiredArgsConstructor
public class MemberRepository {
private final EntityManager em; //entity manager 생성
//저장 로직
public void save(Member member) {
em.persist(member) ; // jpa가 member 객체를 저장
} // 영속성 컨텍스트에 객체를 주입(insert 쿼리 생성)
// 단건 조회(id는 pk)
public Member findOne(Long id){
return em.find(Member.class, id);
} // member를 찾아서 반환
// 전체 조회
public List<Member> findAll(){
return em.createQuery("select m from Member m", Member.class)
.getResultList(); // jpa 쿼리 작성 = jpql (sql과는 다르지만 기능적으로는 동일)
// sql은 테이블 중심으로 처리하지만, jpql은 객체를 중심으로 처리
// 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();
}
}
위 코드를 보면 총 4개의 메서드가 생성된 걸 볼 수 있는데, 요구 사항으로는 회원 등록과 전체 조회만 있었지만 예시를 두기 위해 단건 조회와 특정 이름을 가진 객체를 조회할 수 있는 메서드도 추가했습니다.
저장 로직 쪽 코드를 보면 em.persist()를 통해 트랜잭션이 커밋되는 시점에서 디비에 입력값을 반영할 수 있게 member 객체를 추가했습니다.
그 아래 줄에 단건 조회 로직은 em.find()를 통해 member를 찾아서 반환할 수 있게 로직을 작성했는데, 전체 조회 로직이랑 특정 이름을 가진 객체 조회 로직은 em.createQuery()를 통해 쿼리를 생성해서 조회 작업을 수행했습니다.
em.createQuery()는 JPQL을 작성할 수 있는 메서드입니다.
여기서 쿼리에 작성된 이름들은 테이블이 아닌 엔티티 이름입니다.
JPQL에 대해 궁금하신 분들은 아래 포스팅을 참고하시면 도움이 될 것 같습니다.
전체 조회 메서드를 풀어서 설명하면 다음과 같습니다.
return em.createQuery("select m from Member m", Member.class)
.getResultList();
EntityManager는 JPQL 쿼리를 실행할 수 있는 쿼리 객체를 생성하고 getResultList()를 통해 쿼리를 실제로 실행해서 그 결과(Member Entity)를 리스트로 반환합니다.
특정 객체 조회 JPQL도 동일하게 쿼리를 생성했지만 다른 점으로는 where 절이 추가되고 name 값을 가진 데이터를 조회하는 부분이 추가되었습니다.
return em.createQuery("select m from Member m where m.name = :name", Member.class)
.setParameter("name", name)
.getResultList();
setParameter() 메소드를 통해 name 파라미터에 실제 값이 할당되고, 해당 이름을 가진 모든 Member 객체들을 리스트로 반환합니다.
회원 도메인 개발 - 서비스 개발
서비스는 회원 가입, 중복 검증 등 회원 관리 기능을 제공하는 로직을 추가해 보겠습니다.
✅MemberService
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.repository.MemberRepository;
import lombok.AllArgsConstructor;
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;
// 회원 가입
public Long join(Member member){
validateDuplicateMember(member); // 중복 회원 검증하는 로직 구현
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
// 문제가 있으면 예외 터뜨리기, 문제 없으면 넘어가서 정상적으로 save, id 반환
List<Member> findMembers = memberRepository.findByName(member.getName());
// 매개변수에 들어온 member 객체가 findMember에 존재하면 중복된 회원이므로 에러 메세지 출력
if (!findMembers.isEmpty()){
throw new IllegalStateException("이미 존재하는 회원입니다. ");
}
}
// 회원 전체 조회
public List<Member> findMembers(){
return memberRepository.findAll();
}
public Member findOne(Long memberId) {
return memberRepository.findOne(memberId);
}
}
리포지토리와 동일하게 생성자 주입으로 진행하고 지연 로딩(Lazy Loading) 전략 및 모든 메서드에 데이터 변경이 없는 읽기 전용 트랜잭션을 설정하기 위해 @Transactional(readOnly = true) 어노테이션을 적용시켰습니다.
여기서 (readOnly = true) 설정이 데이터의 변경이 없는 읽기 전용 메서드에 적용시키는데, 영속성 컨텍스트를 플러시 하지 않으므로 약간의 성능 향상을 볼 수 있습니다.
서비스에서 회원 관리 로직을 추가해서 Repository에 요청을 보내주는데 전체 조회, 단일 조회에서는 별 다른 로직 추가 없이 repository에 생성한 findAll(), findOne() 메서드를 return 값에 구현했지만 회원 가입 로직에는 중복 회원 검증 로직을 추가했습니다.
✔ validateDuplicateMember(중복 회원 검증)
private void validateDuplicateMember(Member member) {
List<Member> findMembers = memberRepository.findByName(member.getName());
if (!findMembers.isEmpty()){
throw new IllegalStateException("이미 존재하는 회원입니다. ");
}
}
위 코드는 회원 가입 전에 같은 이름을 가진 회원이 존재하는지 확인하고, 존재하면 예외를 발생시켜 가입을 방지합니다.
참고로 실무에서는 검증 로직이 있어도 멀티 쓰레드 상황을 고려해서 회원 테이블의 회원명 컬럼에 유니크 제약 조건을 추가하는 것이 안전합니다.
회원 도메인 개발 - TDD
지금까지 서비스와 리포지토리에 회원 관리 로직을 구현했는데, 생성된 로직이 제대로 작동하는지 테스트 검증을 해봐야 합니다.
✔ 테스트 요구사항
- 회원가입을 성공해야 한다.
- 회원가입 할 때 같은 이름이 있으면 예외가 발생해야 한다.
✅MemberServiceTest
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 javax.persistence.EntityManager;
import static org.junit.Assert.*;
@RunWith(SpringRunner.class)
@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(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);
try{
memberService.join(member2); // 여기서 예외가 발생해야 한다.
} catch (IllegalStateException e){
return ;
}
//then
fail("예외가 발생해야 합니다.");
}
}
회원 가입을 테스트하고, 중복 회원에 대해 예외처리하는 테스트 코드를 작성했습니다.
위 코드를 실행시켜서 테스트를 진행해 보시면 제대로 동작하는 걸 확인할 수 있습니다.
마치며
지금까지 회원 도메인을 만들어서 TDD를 진행해 보았습니다😊
다음 포스팅에서 뵙겠습니다👋
위 포스팅 글은 김영한님의 실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발을 참고했습니다.
'[ ORM ] > JPA' 카테고리의 다른 글
[ JPA ] E-commerce 프로젝트 - 상품 도메인 개발 (1) | 2025.01.06 |
---|---|
[ JPA ] E-commerce 프로젝트 - 도메인 분석 설계 (4) | 2024.12.23 |
[ JPA ] JPA와 DB 설정, 동작확인 (1) | 2024.12.19 |
[ JPA ] Java Persistence Query Language(JPQL , 객체 지향 쿼리 언어)(2) (2) | 2024.04.11 |
[ JPA ] Java Persistence Query Language(JPQL , 객체 지향 쿼리 언어)(1) (46) | 2024.04.03 |