프로젝트 리팩터링 - OCP원칙

꾸준하게 달리기~·2023년 8월 24일
0
post-thumbnail

좋은 코드란?

해당 질문에 대한 대답은, 진짜 어려운 대답이다.
정답은 없는 문제이고,
그 아무리 좋은 코드라고 하더라도, 계속해서 나아질 여지는 충분하기 때문이다.

하지만, 계속해서 리팩터링과 관심을 가지다보면, 더 나아진다는 점은 확실하다.



어떻게 변경할것인가? - 모듈화

지난 프로젝트의 Member 엔터티의 Service클래스는
단순히 MemberService라는 클래스에서 구현되었다.

그러면, 서비스 클래스의 로직이 변경되거나
전면 수정되어야 할 일이 있다면,
MemberSerivice 클래스에서 행해져야 한다.

만약 MemberService클래스가 수정되었는데, 이전 버전의 로직이나 코드가 필요하다면 어떻게될까?

이럴때 난감한 상황이 발생하고,
이러한 문제를 해결하기 위해 모듈화가 필요하다.

우리가
어떤 MemberService 클래스를 만들더래도 필요한 로직들은
인터페이스로 기록해놓고,
각각 인터페이스의 구체 로직은 Impl 구현을 통해 저장하는 것이다.

그럼 각각의 Impl 클래스에서, 내가 원하는 service 로직을 사용하면 된다는 소리이다.

확장에 더 유연한 코드가 되도록 만들 예정이다.

백문이 불여일타, 코드를 보자.




기존의 코드

기존의 코드는 위와 같다.
Service 클래스에서 바로 @Service 어노테이션을 붙여,
바로 빈으로 등록했다.

이 방식은 유연성이 떨어진다.



변경된 코드

변경시킬 코드는 위와 같다.

그렇기 위해서는, 아래의 내용이 필요하다.

  • 필수적인 내용을 담고 있는 MemberService 인터페이스 만들기

  • MemberService 인터페이스의 구체 클래스를 컨테이너에 실어줄 MemberConfig클래스

  • MemberService 인터페이스의 구체 클래스로, MemberServiceImpl1 클래스 만들기

MemberService 인터페이스
CRUD의 U는 클래스에 따라 다르게 만들 수 있으므로 추가하지 않았다.

public interface MemberService {
    public Member createMember();
    public void deleteMember();
    public Member findMember();
}

MemberServiceImpl 클래스 (MemberService 상속)
@Service
@Slf4j
@Transactional
@RequiredArgsConstructor
public class MemberServiceImpl1 implements MemberService{

    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;
    private final CustomAuthorityUtils authorityUtils;
    private final MemberMapper memberMapper;
    private final StorageService storageService;
    private final MailService mailService;



    
    public Member createMember(Member member) {
        Member savedMember = memberRepository.save(member);
        return savedMember;
    }

    @Override
    public Member createMember(MemberDto.Post memberPostDto) { //mem 001

        //기존에 있는 회원인지 확인, 비밀번호와 비밀번호 확인이 일치하는지 확인, 닉네임 길이 괜찮은지, 닉네임 존재하는지 확인
        verifyExistEmail(memberPostDto.getEmail());
        verifyPassword(memberPostDto.getPassword(), memberPostDto.getPasswordConfirm());
        verifyExistNickName(memberPostDto.getNickname());
        verifyNickName(memberPostDto.getNickname());

        
        //패스워드 암호화 저장, PostDto에서 Member로 객체 변환
        String encryptedPassword = passwordEncoder.encode(memberPostDto.getPassword());

        Member member = memberMapper.memberPostDtoToMember(memberPostDto);
        member.setPassword(encryptedPassword);
        
        //member가 가지고 있는 이메일로 role 확인 후 저장
        List<String> roles = authorityUtils.createRoles(member.getEmail());
        member.setRoles(roles);

        mailService.sendEmail(member.getEmail(), "반갑습니다!", "정말 반갑습니다!");
        log.info("이메일 전송 완료!");

        Member savedMember = memberRepository.save(member);

        //publisher.publishEvent(new MemberRegistraionApplicationEvent(this, savedMember));
        return savedMember;
    }


    public Member updateNickname(Member member) { //mem 005
        
        //받아온 멤버아이디 유효한지, 받아온 닉네임 유효성에 맞는지(존재안하면서 길이까지)
        Member findMember = findVerifiedMember(member.getMemberId());
        String newNickname = member.getNickname();
        verifyExistNickName(newNickname);
        verifyNickName(newNickname);
        
        //통과했으면 이제 멤버 닉네임 바꿔주기
        findMember.setNickname(newNickname);

        return memberRepository.save(findMember);
    }

    public Member updatePassword(MemberDto.PatchPassword memberPatchPasswordDto) { //mem 006
        //받아온 비밀번호와 비밀번호 확인 일치하는지, 기존의 비밀번호는 입력된 비밀번호와 일치하는지
        Member findMember = findVerifiedMember(memberPatchPasswordDto.getMemberId());
        
        //verifyPassword2(findVerifiedPassword(findMember.getMemberId()), memberPatchPasswordDto.getNowPassword(), passwordEncoder);
        verifyPassword(memberPatchPasswordDto.getNewPassword(), memberPatchPasswordDto.getPasswordConfirm());
        
        //통과햇스면 멤버 비밀번호 암호화 후 바꿔주기
        String encodePassword = passwordEncoder.encode(memberPatchPasswordDto.getNewPassword());
        findMember.setPassword(encodePassword);

        return memberRepository.save(findMember);
    }


    public Member updateProfileImage(long memberId, MultipartFile file) throws IOException { //mem 007
        Member findMember = findMember(memberId);

        String imageUrl = storageService.uploadFile(file, memberId);

        findMember.setProfileImageUrl(imageUrl);

        return memberRepository.save(findMember);
    }

    public Member deleteProfileImage(long memberId) {
        Member findMember = findMember(memberId);
        findMember.setProfileImageUrl(null);

        return memberRepository.save(findMember);
    }


    @Override
    public void deleteMember(long memberId) { //mem 009
        Member findMember = findVerifiedMember(memberId);
        memberRepository.delete(findMember);
    }

    public String findMemberEmail(String RRNConfirm) { //mem012
        //핸들러매서드에서 반환형이 Email String, singleResponseDto 이런걸로 내보내면 될듯
        Member findMember = findVerifiedMemberByRRN(RRNConfirm);

        return findMember.getEmail();
    }



    public String findMemberPassword(String email, String question, String answer) { //mem013
        Optional<Member> optionalMember = memberRepository.findByEmail(email);
        Member findMember = optionalMember.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));

        verifyQuestion(question, findMember.getQuestion().getValue());
        verifyAnswer(answer, findMember.getAnswer());

        String randomPassword = generateRandomPassword(findVerifiedPassword(findMember.getMemberId()));
        String encodePassword = passwordEncoder.encode(randomPassword);
        findMember.setPassword(encodePassword);

        memberRepository.save(findMember);

        return randomPassword;

    }

    @Override
    public Member findMember(long memberId) { //get 요청시 멤버찾고, 그 멤버에 맞는 Responsedto를 보내줄 예정
        return findVerifiedMember(memberId);
    }

    public void updateGrade(Member member) { //member의 grade 업데이트해주는 로직, 리팩터링
        member.setPoint(member.getBoards().size() * 10 + member.getComments().size() * 5);
        int point = member.getPoint();

        member.setGrade(findGrade(point));
    }






    
    

    //여기 아래 둘은 여기 클래스에서만 사용할 매서드

    private String generateRandomPassword(String beforePassword) { //새로운 비밀번호 생성매서드
        Random random = new Random();
        StringBuilder sb = new StringBuilder();
        for(int i = 0 ; i < 10 ; i++) {
            int randomIndex = random.nextInt(beforePassword.length());
            sb.append(beforePassword.charAt(randomIndex));
        }
        return sb.toString();
    }

    private void verifyQuestion(String question1, String question2) {
        if(!question1.equals(question2)) throw new BusinessLogicException(ExceptionCode.QUESTION_NOT_SAME);
    }

    private void verifyAnswer(String answer1, String answer2) { //답변 두개 일치하는지
        if(!answer1.equals(answer2)) throw new BusinessLogicException(ExceptionCode.ANSWER_NOT_SAME);
    }



    private String findVerifiedPassword(long memberId) { //지금 패스워드 불러오기
        Member findMember = findVerifiedMember(memberId);
        return findMember.getPassword();
    }
    
    private void verifyPassword2(String password1, String password2, PasswordEncoder passwordEncoder) { //현재 비밀번호와 내가 작성한 비밀번호가 일치하는지
        if(!passwordEncoder.matches(password1, password2)) throw new BusinessLogicException(ExceptionCode.PASSWORD_NOT_SAME);
    }
    private void verifyPassword(String password1, String password2) { //비밀번호와 비밀번호 확인 둘이 일치하는지
        if(!password1.equals(password2)) throw new BusinessLogicException(ExceptionCode.PASSWORD_NOT_SAME);
    }

    private void verifyExistEmail(String email) { //존재하는 이메일인지
        Optional<Member> optionalMember = memberRepository.findByEmail(email);
        if(optionalMember.isPresent()) throw new BusinessLogicException(ExceptionCode.MEMBER_EXISTS);
    }

    private void verifyNickName(String nickname) { //닉네임 길이 10 이하인지
        if(nickname.length() >= 10) throw new BusinessLogicException(ExceptionCode.MEMBER_NICKNAME_LONG);
    }
    private void verifyExistNickName(String nickname) { //존재하는 닉네임인지
        Optional<Member> optionalMember = memberRepository.findByNickname(nickname);
        if(optionalMember.isPresent()) throw new BusinessLogicException(ExceptionCode.MEMBER_NICKNAME_EXISTS);
    }
        private Member findVerifiedMember(long memberId) { //존재하는 회원인지
            Optional<Member> optionalMember = memberRepository.findById(memberId);
            Member findMember = optionalMember.orElseThrow(() ->
                    new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));

            return findMember;
        }

    private Member findVerifiedMemberByRRN(String RRN) { //mem 012
        Optional<Member> optionalMember = memberRepository.findByRRN(RRN);
        Member findMember = optionalMember.orElseThrow(() ->
                new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));

        return findMember;
    }

    private static Grade findGrade(int point) { //점수따라 등급제
        if(point > 50) return GRADE5;
        if(point > 40) return GRADE4;
        if(point > 30) return GRADE3;
        if(point > 20) return GRADE2;
        else return GRADE1;

    }
    

}

이젠 해당 내용들을 조율해줄 MemberConfig 클래스
@Configuration
public class MemberConfig {

    @Bean
    public MemberService memberService (MemberRepository memberRepository,
                                        MemberMapper memberMapper,
                                        PasswordEncoder passwordEncoder,
                                        JavaMailSender javaMailSender) {

        return new MemberServiceImpl1(memberRepository, passwordEncoder, authorityUtils(), memberMapper, storageService(), mailService(javaMailSender));
    }

    //위의 MemberServiceImpl1 생성을 위해 필요한 주입 요소들



    @Bean
    public CustomAuthorityUtils authorityUtils() {
        return new CustomAuthorityUtils();
    }

    @Bean
    public StorageService storageService() {
        return new StorageService();
    }

    @Bean
    public MailService mailService(JavaMailSender javaMailSender) {
        return new MailService(javaMailSender);
    }


    
}

인터페이스들은 매개변수로,
내가 작성해준 Service 클래스들은
@Bean 어노테이션을 사용하여 빈으로 등록해줬다.

바로 위 코드에서
CustomAuthorityUtils, StorageService, MailService
와 같은 서비스 클래스들은 @Service어노테이션을 제거해
BeanDefinitionException 예외가 터지지 않도록 했다.




결과

지금은 더 유연한 코드가 되었다.
코드에 변경을 줄 일이 있으면,
MemberService 인터페이스를 상속받은 구체 클래스를 만들어 다시 생성하고
MemberConfig클래스에서 부품 갈아끼우듯 사용하면 되고,
기존의 MemberServiceImpl 클래스를 변경하지 않아도 된다.

PS.
처음엔, Config클래스를 만들때
인터페이스들은 빈으로 등록하면 해당 인터페이스를 구체화 해야 해서 헷갈렸다.
그래서 매개변수로서 사용했다.


소스코드 : https://github.com/ingeon2/webService_project-refactoring

profile
반갑습니다~! 좋은하루 보내세요 :)

0개의 댓글