본문 바로가기
기록해보기

메서드 분리와 DRY의 원칙

by titlejjk 2023. 8. 27.

진행중인 프로젝트의 코드를 가다듬으면서 메서드의 분리와 DRY의 원칙에 대해서 생각을 해 보았다.

 

먼저 기존에 내가 작성했던 코드는 아래와 같다.

기능은 사용자의 회원가입에 대한 코드이다.

아래의 코드는 하나의 메서드 안에서 사용자 정보 검증, 반려동물 정보 조회, 토큰 생성 등을 모두 수행한다.

하나의 메서드안에 모두 집어 넣다 보니 메서드 자체가 굉장히 길어지고 복잡해져 가독성과 유지보수성이 매우 떨어지게 설계되어있다.

    @Override
    @Transactional
    public SignInResponseDto authenticateUser(String userEmail, String userPassword) {

        //사용자 정보 조회
        UserDto userDto = userMapper.findByEmail(userEmail);

        // 사용자 정보 또는 비밀번호가 없는 경우의 검증
        if (userDto == null || userDto.getUserPassword() == null) {
            throw new BadCredentialsException("사용자 정보가 없거나 비밀번호가 누락되었습니다.");
        }
        boolean isValid = false;
        isValid = BCrypt.checkpw(userPassword, userDto.getUserPassword());
        //비밀번호 검증
        if(!isValid){
            throw new BadCredentialsException("비밀번호가 불일치합니다");
        }
        //반려동물 유형 정보조회
        List<Map<String, Object>>petTypes = petMapper.findPetTypesByUserNum(userDto.getUserNum());
        System.out.println("여기는 뭐지? : " + petTypes);
        List<Integer> petTypeIds = new ArrayList<>();
        List<String> petTypeNames = new ArrayList<>();
        for(Map<String, Object> petType : petTypes){
            petTypeIds.add((Integer) petType.get("petTypeId"));
            petTypeNames.add((String) petType.get("petName"));
        }
        System.out.println("petTypeIds: " + petTypeIds);
        System.out.println("petTypeNames: " + petTypeNames);

        //반려동물 유형 정보 설정
        UserDto updatedUserDto = UserDto.builder()
                .userNum(userDto.getUserNum())
                .userEmail(userDto.getUserEmail())
                .userPassword(userDto.getUserPassword())
                .userNickname(userDto.getUserNickname())
                .userAddress(userDto.getUserAddress())
                .userGender(userDto.getUserGender())
                .userBirthday(userDto.getUserBirthday())
                .userProfile(userDto.getUserProfile())
                .userIntroduction(userDto.getUserIntroduction())
                .userStatus(userDto.getUserStatus())
                .userCreatedAt(userDto.getUserCreatedAt())
                .userUpdatedAt(userDto.getUserUpdatedAt())
                .role(userDto.getRole())
                .petTypeIds(petTypeIds) //반려동물 유형 ID설정
                .petTypes(petTypeNames) //반려동물 유형 이름 설정
                .build();


        //JWT토큰생성(UserEmail, UserNickName, UserPetType)
        String token = tokenProvider.create(updatedUserDto);
        //토큰 만료시간
        int exprTime = 3600;//1시간
        //응답 Dto생성
        SignInResponseDto response = new SignInResponseDto(token, exprTime, updatedUserDto);

        return response;
    }

이를 메서드의 분리(Method Decompositon)을 통해 나누어 보려고한다.

메서드 분리는 하나의 메서드가 여러 작업을 수행하는 경우, 이를 여러 개의 작은 메서드로 나누는 기법이다. 각 메서드는 하나의 작업만을 수행하도록 설계되며, 이는 코드의 가독성을 향상시키며 코드의 재사용성 그리고 유닛 테스트가 쉬워진다.

 

공부하면서 제시한 코드는 아래와 같다.

@Override
@Transactional
public SignInResponseDto authenticateUser(String userEmail, String userPassword) {
    UserDto userDto = validateUserCredentials(userEmail, userPassword);
    List<Map<String, Object>> petTypes = fetchPetTypes(userDto);
    UserDto updatedUserDto = updateUserDtoWithPetTypes(userDto, petTypes);

    String token = tokenProvider.create(updatedUserDto);
    int exprTime = 3600;

    return new SignInResponseDto(token, exprTime, updatedUserDto);
}

private UserDto validateUserCredentials(String userEmail, String userPassword) {
    UserDto userDto = userMapper.findByEmail(userEmail);
    if (userDto == null || userDto.getUserPassword() == null || !BCrypt.checkpw(userPassword, userDto.getUserPassword())) {
        throw new BadCredentialsException("계정 정보가 불일치합니다");
    }
    return userDto;
}

private List<Map<String, Object>> fetchPetTypes(UserDto userDto) {
    return petMapper.findPetTypesByUserNum(userDto.getUserNum());
}

private UserDto updateUserDtoWithPetTypes(UserDto userDto, List<Map<String, Object>> petTypes) {
    List<Integer> petTypeIds = new ArrayList<>();
    List<String> petTypeNames = new ArrayList<>();
    for (Map<String, Object> petType : petTypes) {
        petTypeIds.add((Integer) petType.get("petTypeId"));
        petTypeNames.add((String) petType.get("petName"));
    }
    return UserDto.builder()
            .userNum(userDto.getUserNum())
                .userEmail(userDto.getUserEmail())
                .userPassword(userDto.getUserPassword())
                .userNickname(userDto.getUserNickname())
                .userAddress(userDto.getUserAddress())
                .userGender(userDto.getUserGender())
                .userBirthday(userDto.getUserBirthday())
                .userProfile(userDto.getUserProfile())
                .userIntroduction(userDto.getUserIntroduction())
                .userStatus(userDto.getUserStatus())
                .userCreatedAt(userDto.getUserCreatedAt())
                .userUpdatedAt(userDto.getUserUpdatedAt())
                .role(userDto.getRole())
                .petTypeIds(petTypeIds) //반려동물 유형 ID설정
                .petTypes(petTypeNames) //반려동물 유형 이름 설정
                .build();
}

원래 사용하던 메서드와 비교하면 가독성이 많이 올라가며 기능을 나누어 각 메서드는 하나의 작업만 수행하므로, 코드의 가독성과 재사용성이 향상된다.

validateUserCredentials()메서드는 사용자의 이메일과 비밀번호만을 검증

 

fetchPetTypes()는 사용자의 반려동물 정보를 데이터베이스에서 가져오는 로직

 

updateUserDtoWithPetTypes()는 가져온 반려동물 정보를 UserDto객체에 설정하는 로직만을 수행한다.

 

이렇게 설계한 분리된 각각의 메서드들은 하나의 책임만을 가지고, 그 책임만을 수행한다.

개인적으로 객체지향 프로그래밍에서 가장 중요한 SOLID원칙 중 하나인 SRP즉 단일 책임 원칙을 잘 따르도록 설계해 보았다.

 

여기서 하나 더 생각해볼 것이 있었다.

맨 아래 메서드에서 return하는 

.userNum(userDto.getUserNum())
                .userEmail(userDto.getUserEmail())
                .userPassword(userDto.getUserPassword())
                .userNickname(userDto.getUserNickname())
                .userAddress(userDto.getUserAddress())
                .userGender(userDto.getUserGender())
                .userBirthday(userDto.getUserBirthday())
                .userProfile(userDto.getUserProfile())
                .userIntroduction(userDto.getUserIntroduction())
                .userStatus(userDto.getUserStatus())
                .userCreatedAt(userDto.getUserCreatedAt())
                .userUpdatedAt(userDto.getUserUpdatedAt())
                .role(userDto.getRole())
                .petTypeIds(petTypeIds) //반려동물 유형 ID설정
                .petTypes(petTypeNames) //반려동물 유형 이름 설정
                .build();

이 부분을 좀더 객체지향적으로 설계 할순 없을까? 하고 말이다.

 

이를 위해 전에 공부해본 것중에 정적 팩토리 메서드를 사용해 다시 코드를 정리 해보려고 한다.

먼저 UserDto에는

기존 사용자 정보와 새로운 반려동물 정보를 합쳐 새 UserDto객체를 정적팩토리 메서드로 생성하는 방법으로 아래와 같이 코드를 작성했다.

 public static UserDto fromExistingAndUpdatedPetInfo(UserDto existingUser, List<Integer> newPetTypeIds, List<String> newPetTypeNames) {
        return UserDto.builder()
                .userNum(existingUser.getUserNum())
                .userEmail(existingUser.getUserEmail())
                .userPassword(existingUser.getUserPassword())
                .userPasswordCheck(existingUser.getUserPasswordCheck())
                .userNickname(existingUser.getUserNickname())
                .userAddress(existingUser.getUserAddress())
                .userGender(existingUser.getUserGender())
                .userBirthday(existingUser.getUserBirthday())
                .userProfile(existingUser.getUserProfile())
                .userImage(existingUser.getUserImage())
                .userIntroduction(existingUser.getUserIntroduction())
                .userStatus(existingUser.getUserStatus())
                .userCreatedAt(existingUser.getUserCreatedAt())
                .userUpdatedAt(existingUser.getUserUpdatedAt())
                .role(existingUser.getRole())
                .petTypeIds(newPetTypeIds)
                .petTypes(newPetTypeNames)
                .build();
    }

길긴긴다..

 

다음으로 UserDto쪽으로 정적 메서드가 빠졌으니 아래와 같이 코드를 리팩토링해보았다.

@Override
    public SignInResponseDto authenticateUser(String userEmail, String userPassword) {
        // 사용자 정보 검증
        UserDto existingUser = validateUser(userEmail, userPassword);
        // 반려동물 정보 조회
        List<Map<String, Object>> petTypes = getPetTypes(existingUser);
        // 새로운 반려동물 정보로 사용자 정보 갱신
        UserDto updatedUser = updateUserWithPetInfo(existingUser, petTypes);

        // 응답 생성 및 반환
        return createSignInResponse(updatedUser);
    }

    // 사용자 정보를 검증하는 메서드
    private UserDto validateUser(String userEmail, String userPassword) {
        UserDto userDto = userMapper.findByEmail(userEmail);
        if (userDto == null || !BCrypt.checkpw(userPassword, userDto.getUserPassword())) {
            throw new BadCredentialsException("Invalid credentials");
        }
        return userDto;
    }

    // 사용자의 반려동물 정보를 조회하는 메서드
    private List<Map<String, Object>> getPetTypes(UserDto user) {
        return petMapper.findPetTypesByUserNum(user.getUserNum());
    }

    // 사용자 정보와 새로운 반려동물 정보를 합치는 메서드
    private UserDto updateUserWithPetInfo(UserDto existingUser, List<Map<String, Object>> petTypes) {
        List<Integer> petTypeIds = extractPetTypeIds(petTypes);
        List<String> petTypeNames = extractPetTypeNames(petTypes);

        return UserDto.fromExistingAndUpdatedPetInfo(existingUser, petTypeIds, petTypeNames);
    }

    // 반려동물 유형 ID를 추출하는 메서드
    private List<Integer> extractPetTypeIds(List<Map<String, Object>> petTypes) {
        List<Integer> petTypeIds = new ArrayList<>();
        for(Map<String, Object> petType : petTypes) {
            petTypeIds.add((Integer) petType.get("petTypeId"));
        }
        return petTypeIds;
    }

    // 반려동물 이름을 추출하는 메서드
    private List<String> extractPetTypeNames(List<Map<String, Object>> petTypes) {
        List<String> petTypeNames = new ArrayList<>();
        for(Map<String, Object> petType : petTypes) {
            petTypeNames.add((String) petType.get("petName"));
        }
        return petTypeNames;
    }

    // 로그인 응답을 생성하는 메서드
    private SignInResponseDto createSignInResponse(UserDto updatedUser) {
        String token = tokenProvider.create(updatedUser);
        int exprTime = 3600;  // 토큰 만료 시간
        return new SignInResponseDto(token, exprTime, updatedUser);
    }

왠지 드래곤볼 모으듯이 하나 만들때마다 테스트해보면서 하니 돌아가긴한다. 먼저 POSTMAN으로 테스트를 해보았다.

서버를 재시작하고 POSTMAN으로 테스트를 해보았다.

토큰발급도 원하는 정보도 아주 잘 조회되었다.

댓글