springboot

[springboot] 실전! 스프링 부트와 JPA 활용 - 2. 회원 도메인(Member)

힝뿌 2023. 11. 13. 00:25
반응형

저번 Entity 생성에 이어 회원 관련 로직을 생성해 보자.

 

예제를 단순화하기 위해 다음 기능은 구현하지 않는다.

  • 로그인과 권한 관리 사용 X
  • 파라미터 검증과 예외처리 사용 X
  • 상품은 도서만 사용
  • 카테고리 사용 X
  • 배송 정보는 사용 X

 

 

 

 

애플리케이션 아키텍처

계층형 구조 사용

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

 

 

 

개발 순서 : service, repository 계층을 개발하고, 테스트 케이스를 작성해서 검증, 마지막에 웹 계층에 적용

 

 

 

회원 도메인 개발

구현 기능

  • 회원 등록
  • 회원 목록 조회

순서

  • 회원 repository 개발
  • 회원 service 개발
  • 회원 기능 테스트

 

 

회원 repository 개발

@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 where m.name = :name", 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();
    }
}
  • @PersistenceContext
    • EntityManager를 주입받기 위해 사용, EntityManager를 통해 데이터베이스와의 상호작용이 가능해진다.
  • createQuery("select m from Member m where m.name = :name", Member.class).setParameter("name", name).getResultList();
    • 괄호 안에 쓰인 쿼리문은 JPQL 쿼리문이다.
    • JPQL 뒤에 나오는 Member.class는 반환되는 결과 타입을 지정한 것이다.
    • .setParameter()는 where 절에 있는 :name에 해당되는 파라미터를 지정한다.
    • .getResultList()는 실행 결과를 List 형태로 반환하며, 조회된 엔티티들을 담고 있다.

 

 

회원 Service 코드

@Service
@Transactional(readOnly = true)
public class MemberService {
    
    @Autowired
    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);
    }
}
  • @Transactional
    • 트랜잭션, 영속성 컨텍스트
    • readOnly = true
      • 데이터의 변경이 없는 읽기 전용 메서드에 사용, 영속성 컨텍스트를 flush 하지 않으므로 약간의 성능 향상(읽기 전용에는 전부 적용)
      • 데이터베이스 드라이버가 지원하면 DB에서 성능 향상
  • @Autowired
    • 생성자 Injection 많이 사용, 생성자가 하나면 생략 가능

 

참고사항)

실무에서는 검증 로직이 있어도 멀티 쓰레드 상황을 고려해서 회원 테이블의 회원명 컬럼에 유니크 제약 조건을 추가하는 것이 안전하다.
스프링 필드 주입 대신에 생성자 주입을 사용하자!

 

 

 

 

필드 주입

  public class MemberService {
        @Autowired
        MemberRepository memberRepository;
        ...
}

 

 

생성자 주입

public class MemberService {
        private final MemberRepository memberRepository;
        public MemberService(MemberRepository memberRepository) {
            this.memberRepository = memberRepository;
	}
	... 
}

 

  • 생성자 주입 방식을 권장
  • 변경 불가능한 안전한 객체 생성 가능
  • 생성자가 하나면, @Aurowired 생략 가능하다.
  • final 키워드를 추가하면 컴파일 시점에 memberRepository를 설정하지 않는 오류를 체크할 수 있다. (보통 기본 생성자를 추가할 때 발견)

 

 

Lombok

@RequiredArgsConstructor
public class MemberService {
	
    private final MemberRepository memberRepository;
    ...
}

 

참고사항)

스프링 데이터 JPA를 사용하면 EntityManager도 주입 가능하다.

 

 

 

회원 Service 최종 코드는 lombok을 사용하여 코드를 작성했다.

 

 

 

회원 기능 테스트

테스트 요구 사항

  • 회원가입을 성공해야 한다.
  • 회원가입 할 때 같은 이름이 있으면 예외가 발생해야 한다.
@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);
        memberService.join(member2);        // 예외가 발생해야 한다.
        
        // Then
        fail("예외가 발생해야 한다.");
        
    }

}
  • @RunWith(SpringRunner.class)
    • JUnit 테스트를 실행할 때 스프링과 함께 실행하도록 지정하는 것이다. 'SpringRunner'는 스프링 프레임워크 테스트 러너를 의미한다.
    • 스프링과 테스트 통합
  • @SpringBootTest
    • 스프링부트 애플리케이션 컨텍스트를 로드하기 위해 해당 어노테이션이 사용된다.
    • 스프링부트를 띄우고 테스트(이게 없으면 @Autowired 다 실패)
  • @Transactional
    • 각 테스트를 실행할 때마다 트랜잭션을 시작하고, 테스트가 완료되면 강제 롤백한다. 이렇게 함으로써 각 테스트 간에 데이터베이스 상태를 격리시키고, 테스트 종료 시에는 영향을 주지 않게 된다. (이 어노테이션이 테스트 케이스에서 사용될 때만 롤백)
  • @Test(expected = IllegalStateException.class)
    • expected 속성은 해당 테스트 메서드가 특정 예외를 기대한다는 것을 나타낸다. 테스트가 실행될 때 예상된 예외가 발생하지 않으면 테스트가 실패한다.
    • 테스트 메서드 "중복_회원_예외"가 IllegalStateException이 발생할 것으로 기대한다는 것을 나타낸다.
  • fail("예외가 발생해야 한다.")
    • JUnit에서 테스트를 명시적으로 실패로 표시하는 메서드다. 일반적으로 예외가 발생하지 않았지만 특정 조건에서 테스트를 실패로 처리하고 싶을 때 사용한다.
    • fail() 메서드를 호출하면 테스트가 즉시 중단되고 실패로 표시된다. 

 

 

 

테스트 케이스를 위한 설정

테스트 케이스는 격리된 환경에서 실행하고, 끝나면 데이터를 초기화하는 것이 좋다. 그런 면에서 메모리 DB를 사용하는 것이 가장 이상적이다.
추가로 테스트 케이스를 위한 스프링 환경과, 일반적으로 애플리케이션을 실행하는 환경은 보통 다르므로 설정 파일을 다르게 사용하자. 

아래와 같이 test/resources에 테스트용 설정 파일(application.yml)을 추가하면 된다.

spring:

logging:
  level:
    org.hibernate.sql: debug

 

위와 같이 설정하면 테스트에서 스프링을 실행할 때 이 위치에 있는 설정 파일을 읽는다.

(만약 이 위치에 없으면 src/resources/application.yml을 읽는다.)

스프링부트는 datasource 설정이 없으면, 기본적으로 메모리 DB를 사용하고, driver-class도 현재 등록된 라이브러리를 보고 찾아준다. 추가로 ddl-auto도 create-drop 모드로 동작한다. 따라서 데이터소스나, JPA 관련된 별도의 추가 설정을 하지 않아도 된다.
반응형