티스토리 뷰

반응형

이제 서비스를 구현해볼 차례다. 먼저 서비스는 애플리케이션 레이어에 해당하는데 보통 비즈니스를 구현하는 구현부라고 생각하면 된다. 물론 이미 여러분들은 틀이 완성 되어있을 것이다. 컨트롤러를 먼저 만들었기 때문에 없는 클래스들은 alt + enter(mac. option + enter)를 이용하면 쉽게 틀을 만들 수 있다. 또한 ReadService와 Service가 따로 구현이 되어있는데 Transactional을 공통으로 사용할 수 있어 간결하게 코딩이 가능하며 나중에 Read와 Write 도메인이 분리되더라도 영향도를 줄일 수 있고 분리된 서비스를 기반으로 빠르게 리팩토링이 가능할 수 있습니다.

@Service
@Slf4j
@RequiredArgsConstructor
@Transactional
public class MemberService {
    private final MemberRepository memberRepository;
    private final PasswordEncoder encoder;
    private final MemberRedisRepository memberRedisRepository;

    public JoinResponse create(final Member member) {
        existEmail(member.getEmail());
        existNickname(member.getNickname());

        Member savedMember = memberRepository.save(member.encode(encoder));

        memberRedisRepository.saveMember(MemberRedisDto.of(savedMember));

        return JoinResponse.of(savedMember);
    }

    public void update(final Member updateMember, final Long memberId) {
        Member member = memberRepository.findById(memberId).orElseThrow(
                () -> new MemberNotFoundException(ErrorCode.USER_NOT_FOUND));

        if (member.getNickname() != updateMember.getNickname()) {
            existNickname(updateMember.getNickname());
        }

        memberRedisRepository.saveMember(MemberRedisDto.of(member));

        member.update(updateMember, encoder);
    }

    public void delete(final Long memberId) {
        memberRepository.findById(memberId)
                .orElseThrow(() -> new MemberNotFoundException(ErrorCode.USER_NOT_FOUND))
                .delete();

        memberRedisRepository.deleteBy(memberId);
    }

    private void existEmail(final Email email) {
        memberRepository.existByEmail(email).ifPresent(member -> {
            throw new DuplicateEmailException(ErrorCode.DUPLICATE_EMAIL);
        });
    }

    private void existNickname(final Nickname nickname) {
        memberRepository.existByNickname(nickname).ifPresent(member -> {
            throw new DuplicateNicknameException(ErrorCode.DUPLICATE_NICKNAME);
        });
    }
}

 

1. 회원 가입 : 우리는 Controller에서 받아온 DTO를 Entity로 이미 변환해주었다. 그렇기 때문에 Entity를 직접적으로 받아오고 있다. 그 후 유효성 검사를 한다. (사실 이것도 따로 api를 빼서 따로 작업하는 것이 더 옳다고 본다.) 그 후 저장해주면 끝! (비밀번호 인코딩 관련  로직은 Entity 구현부에서 설명)

 

회원 가입 후 다음 정보들을 리턴해주고 있다. 

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class JoinResponse {
    private Long memberId;
    private Email email;
    private Name name;

    public static JoinResponse of(final Member member) {
        return new JoinResponse(member.getId(), member.getEmail(), member.getName());
    }
}

생성자에 접근제어자가 달려있는 이유는 직렬화 시 기본 생성자가 필요하기 때문에 선언해주어야하고 팩토리 메소드를 사용하기 때문에 모든 필드에 대한 생성자도 만들어주었다. 하지만 생성전략이 많을수록 코드는 지저분해지고 나쁜냄새가 나는 코드가 되기 쉽기 때문에 접근제어자를 통해 생성을 제한해주고 있다.

 

2. 회원 수정 : 수정은 받아온 회원 PK로 회원 엔티티를 받아온다. 이후 nickname에 대한 중복체크가 이뤄지고 이후 업데이트가 이뤄진다.

                     서비스에서는 도메인 계층에 수정요청을 하고 메서드를 종료시킨다. 

 

3. 회원 삭제 : 삭제 기능도 도메인 계층에서 처리하고 있다.

 

도메인 계층에서 삭제와 수정을 처리한 이유 (개인적인 이유)

도메인 레이어는 도메인을 풀어나가기 위한 정보들을 갖고 있다. 당연히 이 정보들을 조합하여 비즈니스 규칙을 정의하는 것도 도메인 레이어의 책임이다. (오브젝트 : 알고 있는 것에 대한 책임이란 내용이 있다.) 또한 응용 계층은 이러한 비즈니스 규칙을 이용하여 비즈니스 로직을 정의한다. 

결론적으로 각각의 계층마다 책임이 존재하고 그에 맞춰 도메인 계층에서 수정과 삭제에 대한 규칙을 정의한 것이다. 그렇게 되어 각각의 Layer는 하나의 관심사에 집중하게 되고, 이는 개발자 입장에서 유지보수하기 편리한데 계층별로 관심사에 따라 서로 영향을 주지 않고 독립적으로 존재하니 변경에 닫혀있고 확장시엔 또 DI를 통해서 열려있다. 

추가적으로 이런 패턴은 테스트 코드를 간편하게 작성할 수 있게 해준다. 

 

자 그러면 ReadService 구현부를 알아보자

@Service
@RequiredArgsConstructor
public class MemberReadService {
    private final MemberRepository memberRepository;
    private final MemberRedisRepository memberRedisRepository;

    @Transactional(readOnly = true)
    public MemberResponse getDetailBy(final Long memberId) {
        MemberRedisDto member = memberRedisRepository.findDtoById(memberId).orElseGet(() -> {
            Member memberFromDB = memberRepository.findById(memberId).orElseThrow(
                    () -> new MemberNotFoundException(ErrorCode.USER_NOT_FOUND));

            return MemberRedisDto.of(memberFromDB);
        });

        return MemberResponse.of(member);
    }

    @Transactional(readOnly = true)
    public PageCustomResponse<MemberAllResponse> getMembers(final MemberSearch memberSearch, final Pageable pageable) {
        return memberRepository.findAllWithSearch(memberSearch, pageable);
    }
}

상세 조회시 우리는 DB에서 가져올게 아니라 Redis에서 바로 가져올 예정이다. 간단하게 이유를 설명하자면 캐시 메모리를 이용해 조회 성능을 끌어올리고 나아가서 DB 커넥션에 대한 부담도 줄일 수 있기 때문에 사용했다. 

또한 주목할점은 PageCustomResponse인데. 이에 대한 내용은 다음 링크를 참고해서 공부하면 좋을 것 같다.

https://golf-dev.tistory.com/38

 

Pagination 전략에 대해서 (Page 인터페이스 커스텀하기)

Page 인터페이스 Paging을 위해서 Spring boot에서는 Page 인터페이스를 제공합니다. 이 인터페이스의 기본 스펙을 살펴보면 다음과 같습니다. { "content": [ ... (data) ], "pageable": { "sort": { "unsorted":..

golf-dev.tistory.com

 

또한 모든 Transactional(readOnly = true)는 꼭 붙여주길 바란다. readOnly = true 옵션은 플러쉬를 하지 않기 때문에 예상하지 못한 개발자에 실수를 줄여줄 수 있다. JPA를 사용하다보면 예상치 못하게 select만 해야하는 비즈니스에서 더티 체킹으로 인해 update가 나가는 치명적인 실수를 막아줄 수 있다. 

또한 커넥션이 너무 많아지거나 트래픽이 감당할 수 없는 수준에서 DB Replication을 고려해볼 수가 있는데 이 때 readOnly = true 옵션이 잘 지켜지고 있다면 비교적 편하게 할 수 있다는 장점이 있다. 

JPA에도 더티체킹을 위한 따로 스냅샷을 저장하지 않으므로 메모리 손실을 방지할 수 있다.

 

마침.

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함