[스프링 활용] 회원 도메인 개발

atdawn·2024년 6월 3일

SPRING BOOT+JPA

목록 보기
22/49

참고 : 인프런 [ 실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 김영한 ]


회원 리파지토리

~/repository/MemberRepository.java


@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);
    }

    //jpql로 리스트 조회 대상 : 엔티티
    public List<Member> findAll(){
        return em.createQuery("select m from Member m",Member.class)
                .getResultList();
    }

    public List<Member> findName(String name){
        return em.createQuery("select m from Member m where m.name = :name",Member.class)
                .setParameter("name",name)
                .getResultList();
    }


}

@PersistenceContext
JPA에서 엔티티 매니저를 주입받기 위해 사용되는 어노테이션

  • 스프링 프레임워크의 경우 @Autowired 어노테이션과 함께 사용되어 엔티티 매니저를 주입받을 수 있음
    즉 스프링에서는
    @PersistenceContext private EntityManager em;
    @Autowired private EntityManager em;이 같음

JPQL

  • SQL 쿼리 대상 : 테이블
  • JPQL 쿼리 대상 : 엔티티 객체
    • JPQL에서는 엔티티 클래스와 필드를 직접 사용하여 쿼리를 작성
    • 파라미터 바인딩 : setParameter(name,value)
      String jpql = "SELECT p FROM Product p WHERE p.price > :price";
      List<Product> products = entityManager.createQuery(jpql, Product.class)
                                        .setParameter("price", 100)
                                        .getResultList();
    • 리스트형태로 반환 : getResultList()

회원 서비스

~/service/MemberService.java

@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("이미 존재하는 회원입니다.");
      }
  }

  //회원 전체 조회
	 ...
  //단건 조회
	 ...
}
  • em.persist(member)를 호출하면, 엔티티 매니저는 member 객체를 영속성 컨텍스트에 추가하고, 이때 member 객체의 @Id 필드를 기준으로 관리
  • @Id 필드는 엔티티의 기본 키로 사용되며, 영속성 컨텍스트에서 엔티티를 식별하는 데 사용됩니다.
  • @Id 값은 엔티티가 영속성 컨텍스트에 추가될 때 이미 설정되어 있거나, @GeneratedValue를 통해 자동 생성됩니다. 즉, @Id 값은 항상 생성 되어있는 것이 보장됨.

@Transactional
트랜잭션 관리가 필요한 코드의 시작과 끝을 명시.

  • @Transactional(readOnly = true) : 조회만 가능
  • @Transactional(readOnly = false) (기본값)

위 코드에서는 클래스 내 모든 메서드들을 @Transactional(readOnly = true)로 설정하고, 회원가입 로직인 join 메서드만 @Transactional(readOnly = false)로 설정해주었다.

IllegalStateException 런타임 예외
객체의 상태가 호출된 메서드를 실행하기에 적절하지 않을 때 사용

참고
실무에서는 검증 로직이 있어도 멀티 쓰레드 상황을 고려해서 회원 테이블의 회원명 컬럼에 유니크 제약 조 건을 추가하는 것이 안전하다


스프링 필드 주입 대신에 생성자 주입을 사용하자.

  • 스프링 필드 주입
    @Autowired //필드 인젝션
      private MyRepository myRepository;
  • 생성자 주입 : 생성자가 하나면 @Autowired 생략 가능
    private final MyRepository myRepository;
    	
    	//@Autowired
      public MyService(MyRepository myRepository) {
          this.myRepository = myRepository;
      }
  • 생성자 주입 (롬복)@RequiredArgsConstructor :final로 선언된 모든 필드와 @NonNull로 마크된 필드에 대해 생성자를 자동으로 생성
    @RequiredArgsConstructor
    public class MyService {
      private final MyRepository myRepository;
    }
  • 생성자 주입 방식을 권장
  • 변경 불가능한 안전한 객체 생성 가능
  • 생성자가 하나면, @Autowired 를 생략할 수 있다.
  • final 키워드를 추가하면 컴파일 시점에 memberRepository 를 설정하지 않는 오류를 체크할 수 있다.(보통 기본 생성자를 추가할 때 발견)

🌟 회원 리파지토리를 공부할 때 스프링 부트에서는 엔티티 매니저를 주입받을 때 @PersistenceContext@Autowired 모두 사용가능하다고 하였다.
즉, 엔티티 매니저 또한 생성자 주입을 통한 인젝션이 가능하다.

~/repository/MemberRepository.java

  @Repository
@RequiredArgsConstructor
public class MemberRepository {
    
    private final EntityManager em; //생성자 주입 (롬복)
  ...

로 코드 변경이 가능하다!


회원 기능 테스트

@RunWith(SpringRunner.class) //JUnit에게 스프링과 관련된 걸로 테스트 한다는 것을 알림
@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));

   }
 ...
  • assertEquals 검증 : 같은 트랜잭션 안에서 같은 엔티티(동일한 PK값)은 영속성 컨텍스트에서 하나로 관리가 된다.


    회원 기능 테스트를 실행하였을때, 쿼리를 살펴보면 select 문만 확인된다. 정작 Insert 쿼리는 없다.
  • em.persist(member);
    • member 객체를 영속성 컨텍스트(Persistence Context)에 저장
    • 엔티티 매니저는 member 객체를 관리하지만, 즉시 데이터베이스에 INSERT 쿼리를 보내지 X
  • 플러시(Flush) : 영속성 컨텍스트는 트랜잭션이 커밋될 때 데이터베이스와 동기화
  • em.persist(member)를 호출한 후에는 영속성 컨텍스트에 member가 저장되지만, INSERT 쿼리는 플러시될 때까지 데이터베이스로 전송되지 X

  • 스프링부트의 @Transactional 어노테이션은 트랜잭션 커밋을 하지 않고 롤백을 하기때문에 테스트가 실행되더라도 DB에 insert 되지 않는다.
  • 만약 커밋을 시켜 확인하고 싶다면 @Rollback(false) 를 삽입한다.

테스트 오류

테스트 코드 실행 중 위와 같은 오류가 발생하였다.

해결 방법 : 그레이들 프로젝트 테스트 실행을 IntelliJ로 변경한다.

참고 : https://stackoverflow.com/questions/55405441/intelij-2019-1-update-breaks-junit-tests

 @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("예외가 발생해야 한다."); //Assert 메서드

   }

중복회원예외 테스트의 경우 중복 이름을 가진 회원을 회원가입시켰을때,
예외가 발생하는 것을 확인해야 한다.

  • try-catch 문을 사용하여 IllegalStateException이 발생하였을때 ,return 으로 메서드가 종료되어야 한다.
  • 예외가 발생하지 않으면 fail("예외가 발생해야 한다.");가 실행되어 테스트는 실패

개선 방법 : @Test(expected = IllegalStateException.class)

  • 특정 테스트 메서드가 특정 예외를 던질 것을 예상하는 경우를 처리
  • 예외 발생 : 성공 / 예외 미발생 : 실패

Assert의 fail() 메서드

  • 테스트가 특정 코드 경로를 실행해서는 안 되는 경우 사용
  • 코드가 특정 지점에 도달했을 때, 테스트를 실패시키기 위해 명시적으로 fail() 메서드를 호출


테스트 성공!

profile
복습 복습 복습

0개의 댓글