본문 바로가기
BackEnd/JPA

[JPA] 회원 도메인 개발

by 개발 Blog 2024. 10. 2.

공부 내용을 정리하고 앞으로의 학습에 이해를 돕기 위해 작성합니다.

 

회원 리포지토리 

package jpabook.jpashop.repository;

import jakarta.persistence.EntityManager;
import jpabook.jpashop.domain.Member;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
@RequiredArgsConstructor
public class MemberRepository {

    private final 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 예외를 스프링 기반 예외로 변환한다.
  • @RequiredArgsConstructor: final 필드를 대상으로 생성자를 자동으로 생성해주는 롬복 어노테이션이다.
  • EntityManager: JPA의 핵심 인터페이스로, 엔티티를 관리하고 데이터베이스와 상호작용한다.

기능 설명

  • save(): 주어진 회원 객체를 영속성 컨텍스트에 저장하고 데이터베이스에 반영한다.
  • findOne(): 주어진 ID로 회원을 검색해 반환한다.
  • findAll(): 모든 회원을 리스트로 반환한다.
  • findByName(): 주어진 이름으로 회원을 검색해 리스트로 반환한다.

회원 서비스

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
    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: 서비스 계층을 나타내며 스프링 빈으로 등록된다.
  • @Transactional: 트랜잭션 처리를 위한 어노테이션으로, 영속성 컨텍스트와 트랜잭션을 관리한다. readOnly=true는 읽기 전용 트랜잭션을 설정하여 성능을 최적화한다.
  • @Autowired: 스프링에서 의존성 주입을 처리해주는 어노테이션으로, 생성자가 하나인 경우 생략 가능하다.
  • final: 변경 불가능한 객체를 생성해 코드의 안정성을 높인다.

기능 설명

  • join(): 회원 가입을 처리하며 중복 회원을 검증한 후 저장한다.
  • findMembers(): 모든 회원을 조회한다.
  • findOne(): 주어진 회원 ID로 특정 회원을 조회한다.

필드 주입 vs 생성자 주입

1. 필드 주입

public class MemberService {
    @Autowired
    MemberRepository memberRepository;
    ...
 }
  • 필드에 직접 @Autowired를 사용하여 의존성을 주입한다.
  • 테스트나 유지보수 시에 유연성이 떨어지며, 변경 가능성이 있는 코드가 될 수 있다.

2. 생성자 주입

public class MemberService {
    private final MemberRepository memberRepository;
     
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
   }
   ...
}
  • 생성자 주입은 의존성을 주입하는 권장 방식으로, final 키워드를 통해 주입받은 의존성을 변경할 수 없도록 하여 안정성을 높인다.
  • 생성자가 하나뿐인 경우, @Autowired를 생략할 수 있으며, 롬복의 @RequiredArgsConstructor를 사용하면 자동으로 생성자를 생성해준다.
  • 이렇게 하면 코드가 더 간결해지고, 컴파일 시점에 의존성 주입 오류를 잡을 수 있어 안전한 객체를 생성할 수 있다.

3. Lombok 활용

 @RequiredArgsConstructor
 public class MemberService {
     private final MemberRepository memberRepository;
     ...
}
  • @RequiredArgsConstructor를 사용하면 final이 붙은 필드에 대해 자동으로 생성자를 만들어주므로, 코드 작성량이 줄어들고 가독성이 향상된다.

참고

 @Repository
 @RequiredArgsConstructor
 public class MemberRepository {
 
    private final EntityManager em;
	... 
}
  • 스프링 데이터 JPA에서는 EntityManager도 주입받을 수 있다. @Repository와 @RequiredArgsConstructor를 사용하면 EntityManager도 간편하게 주입된다.

회원 기능 테스트

설정 파일

테스트는 격리된 환경에서 실행되므로 메모리 DB(H2)를 사용하는 것이 가장 이상적이다.

이를 위해 test/resources/application.yaml 파일에 테스트 환경 설정을 추가한다.

spring:
#  datasource:
#    url: jdbc:h2:mem:test
#    username: sa
#    password:
#    driver-class-name: org.h2.Driver
#  jpa:
#    hibernate:
#      ddl-auto: create
#    properties:
#      hibernate:
#        #        show_sql: true
#        format_sql: true
logging.level:
  org.hibernate.SQL: debug
  org.hibernate.orm.jdbc.bind: trace
  • 이 설정은 스프링 부트에서 기본적으로 메모리 DB를 사용하게 하며, 테스트가 끝나면 데이터를 초기화한다.
  • Hibernate의 ddl-auto: create 설정을 통해 매번 스키마를 생성하고 테스트가 끝나면 삭제한다.

테스트 요구사항

  1. 회원가입 성공: 회원을 정상적으로 가입시키고, 데이터베이스에 저장된 회원을 확인한다.
  2. 중복 회원 예외 처리: 동일한 이름의 회원이 중복으로 가입하려고 할 때 예외가 발생해야 한다.

테스트 코드

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.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

@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 savedId = memberService.join(member);

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

    @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("예외가 발생해야 한다.");
    }
}

  • 테스트 결과, @Transactional 덕분에 테스트가 끝난 후 트랜잭션이 롤백되어 DB에 insert 쿼리가 반영되지 않았다. 따라서 테스트 중 데이터 변경이 실제로 DB에 저장되지 않고, 안전하게 초기화된다.

기술 설명

  • @RunWith(SpringRunner.class): 스프링과 JUnit을 통합해 주는 어노테이션으로, 스프링 컨텍스트에서 테스트를 실행할 수 있게 한다.
  • @SpringBootTest: 스프링 부트를 실제로 실행하여 테스트 환경을 제공한다.
  • @Transactional: 테스트가 끝나면 트랜잭션을 자동으로 롤백하여 데이터베이스에 영향을 주지 않는다.

기능 설명

  • 회원가입 테스트: 회원을 가입시키고 정상적으로 데이터베이스에 저장된 회원 객체가 일치하는지 검증한다.
  • 중복 회원 예외 처리 테스트: 동일한 이름의 회원이 가입되면 예외가 발생하는지 검증한다.

참고

  • 트랜잭션이 적용되어 테스트 중 발생한 데이터 변경은 모두 롤백된다.
  • 테스트 작성 시 Given, When, Then 구조를 사용하면 테스트의 의도가 명확해지고 가독성이 향상된다.

'BackEnd > JPA' 카테고리의 다른 글

[JPA] 주문 도메인 개발  (3) 2024.10.03
[JPA] 상품 도메인 개발  (0) 2024.10.03
[JPA] 도메인 분석 설계(3)  (2) 2024.10.01
[JPA] 도메인 분석 설계(2)  (0) 2024.09.26
[JPA] 도메인 분석 설계(1)  (2) 2024.09.23