04 회원 도메인 개발 - Member Service

shin·2023년 9월 3일
1

1. Member Service

1) Member Service 구현

package jpabook.jpashop.service;

import jpabook.jpashop.domain.Member;
import jpabook.jpashop.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
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;

    /**
     * 회원가입
     * @param member
     * @return Long
     */
    @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("이미 존재하는 회원입니다.");
        }
    }

    /**
     * 회원 전체 조회
     * @param
     * @return List<Member>
     */
    public List<Member> findMembers(){
        return memberRepository.findAll();
    }

    /**
     * 회원 단건 조회
     * @param memberId
     * @return Member
     */
    public Member findOne(Long memberId){
        return memberRepository.findOne(memberId);
    }

}


2) 코드 상세 설명

@Service

@Service
public class MemberService {

primary key - id

@Transactional
 public Long join(Member member){
        validateDuplicateMember(member); //중복회원검증
        memberRepository.save(member);

        return member.getId();
}
public class MemberRepository {
    ...
    public void save(Member member){
        em.persist(member);
    }
  • MemberRepository save 메서드의 em.persist(member)에서 영속성 컨텍스트에 멤버 객체를 올림

  • 영속성 컨텍스트는 pk인 id 생성을 보장함

    • 영속성 컨텍스트는 값을 넣을 때 값으로 key value가 들어가는데, 그 key value에 pk 값을 그대로 넣어주기 때문에
    • db에 들어가는 시점이 아니여도 생성됨

validateDuplicateMember()

private void validateDuplicateMember(Member member) {
    List<Member> findMembers = memberRepository.findByName(member.getName());
    if(!findMembers.isEmpty()){
         throw new IllegalStateException("이미 존재하는 회원입니다.");
}
  • 위 메서드에서는 회원의 이름으로 중복 체크를 수행하고 있음
  • 이름으로 중복 체크를 하는 경우 만약 같은 이름의 회원이 동시에 회원 가입을 수행하는 경우 문제가 발생할 수 있음
  • 따라서 실무에서는 검증 로직이 있어도 멀티 쓰레드 상황을 고려해서 회원 테이블의 name 컬럼에 unique 제약 조건을 추가하는 것이 안전함

💡@Transactional

@Transactional(readOnly = true)
public class MemberService {
	...
	@Transactional
    public Long join(Member member){
    ...
  • jpa의 모든 데이터 변경 로직들은 transaction 안에서 이루어져야 함

  • findMembersfindOne 메서드와 같이 조회만 수행하는 경우에는 readonly 옵션을 true로 설정

  • 위 코드처럼 Service class@Transactional(readOnly = true)을 걸어놓고 join 메서드는 @Transactional로 선언을 하면,

    • join 메서드에 달린 어노테이션이 더 우선권을 가져서 join 시에는 readonly 옵션 없이 변경을 수행하게 됨
  • 만약 해당 서비스에서 쓰기 기능만 수행을 한다면 서비스에만 readonly 옵션이 없는 @Transactional을 붙이는 것이 좋음


💡@Transactional(readOnly = true)

  • 조회 최적화, 데어터의 변경이 없는 읽기 전용 메서드에 사용

  • 영속성컨텍스트를 Dirty Checking(변경 감지) 하지 않음

    • Dirty Checking
      • 쓰기 지연 저장소에 저장되어 있는 SQL Query를 flush하고 디비의 트랜잭션을 Commit해서 update와 같은 메서드를 사용하지 않고 Entity의 수정이 이루어지는 것
    • 따라서 트랜잭션을 commit했을 때 영속성 컨텍스트가 자동으로 flush되지 않기 때문에 조회용으로 가져온 entity의 예기치 못한 수정을 방지할 수 있음
  • 영속성 컨텍스트를 플러시 하지 않기 때문에 약간의 성능이 향상됨

    • JPA는 해당 트랜잭션 내에 조회하는 Entity는 조회용임을 인식하고, 변경 감지를 위한 Snapshot을 따로 보관하지 않기 때문에 메모리가 절약되는 성능상의 이점도 존재함
    • 데이터베이스 드라이버가 지원하면 DB에서 성능 향상
  • 데이터 변경이 필요한 것에 해당 옵션을 사용하면 변경이 안되기 때문에 주의해서 사용해야 함


💡@Autowired

@Autowired

  • 스프링이 스프링 빈에 등록되어 있는 멤버 리포지토리를 인젝션 해줌
  • 생성자 Injection을 많이 사용함
  • 생성자가 하나이면 해당 어노테이션 생략이 가능함

필드 주입 방식

@Autowired
private MemberRepository memberRepository;
  • 단점 : 접근할 수 있는 방법이 없어서 주입이 까다롭기 때문에, 테스트 시 변경이 어려움

setter를 활용한 주입 방식

private MemberRepository memberRepository;

@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
    this.memberRepository = memberRepository;
}
  • 장점 : 테스트시 메서드를 통해 바로 주입이 가능함

  • 단점 : 애플리케이션 로딩 시점에 조립이 모두 이루어지고 런타임에 변경이 이루어질 일은 없음

    • 조립이 끝난 후에 애플리케이션 동작을 변경할 일은 없음
    • 따라서 setter를 활용하여 인젝션을 수행하는 것은 좋지 않은 방법

생성자 주입 방식

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

@Autowired
public MemberService(MemberRepository memberRepository) {
    this.memberRepository = memberRepository;
}
  • 생성이 될 떄 완성되기 때문에 중간에 set으로 변경이 불가능함
  • 테스트 케이스 작성 시 직접 주입을 해줄 수 있음

    public static void main(String[] args) {
        MemberService memberService = new MemberService(주입 필요);
    }
  • 주입을 하지 않으면 에러가 발생하기 때문에, 주입이 필요한 부분을 명확하게 나타내어 놓치지 않고 챙길 수 있음

private final MemberRepository memberRepository;

public MemberService(MemberRepository memberRepository) {
    this.memberRepository = memberRepository;
}
  • 생성자가 하나면 @Autowired를 생략할 수 있음

    • 최신 버전의 스프링은 @Autowired 없이도 생성자가 하나만 있는 경우에는 자동으로 인젝션 해줌
  • final 키워드를 추가하면 컴파일 시점에 memberRepository를 설정하지 않는 오류를 체크할 수 있음(보통 기본 생성자를 추가할 때 발견)

    • 멤버 리포지토리 필드는 변경될 일이 없기 때문에 final로 선언해야 함
    • 생성자에 값 세팅을 안하면 오류 발생하기 때문에, 컴파일 시점에 체크가 가능해짐
  • 또한 아래 세줄로 작성된 생성자 선언 코드는 @RequiredArgsConstructor로 대체 가능

    • 해당 어노테이션을 사용하면 final이 있는 필드만 가지고 생성자 선언이 가능해짐

리포지토리 - 생성자 주입

활용 전

@Repository
public class MemberRepository {

    @PersistenceContext
    private EntityManager em;

활용 후

@Repository
@RequiredArgsConstructor
public class MemberRepository {
    private final EntityManager em;
  • EntityManager@PersistenceContext라는 표준 어노테이션이 있어야 인젝션이 가능함

    • 최근 버전의 스프링 부트 JPA는 @Autowired로도 인젝션이 되도록 지원을 해주고 있음
  • 따라서 서비스 코드와 같이 리포지토리의 엔티티 매니저를 생성자로 인젝션하도록 코드를 구현할 수 있음

  • 일관성 있는 코드 작성이 가능해짐



강의 : 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발

profile
Backend development

0개의 댓글