[클린코드 적용기] 3. 추상화 수준

전종원·2024년 8월 8일
0
post-custom-banner

클린코드 3장 함수에서 다룬 추상화 수준을 적용해 한 가지 일을 하는 함수로 가꿉니다.

추상화 수준?

책에서는 한 가지 일을 하는 함수에 대한 기준으로 추상화 수준을 언급했습니다.

  • 함수 이름 아래에서 동일한 추상화 수준을 갖는다면 한 가지 일을 하는 함수라고 주장합니다.
  • 또한, 추상화 수준을 지켜야 하는 이유를 다음과 같이 말합니다.
    1. 추상화 수준이 섞여있으면 코드를 읽는 사람이 의도를 파악하기 어려워집니다.
      • 핵심 내용과 부수적인 내용을 구분하기 어렵다는 의미입니다.
    2. 내려가기 규칙(추상화 수준이 높은 함수 → 낮은 함수)으로 이해하기 쉬운 코드가 됩니다.

적용

저는 위 이유 중 1번이 특히 공감이 되었습니다. 다음은 회원가입을 구현한 메서드입니다.

public void signup(String uid, String nickname) {
    // 이미 존재하는 유저인 경우 예외처리
    if (userRepository.existsByUid(uid)) {
        throw new CustomException(CommonCode.ALREADY_EXIST_USER);
    }

    // 이미 존재하는 닉네임인 경우 예외처리
    if (userRepository.existsByNickname(nickname)) {
        throw new CustomException(CommonCode.ALREADY_EXIST_NICKNAME);
    }

    User savedUser = userRepository.save(new User(uid, nickname));
    authorityRepository.save(new Authority(savedUser, ROLE_USER));
}
  • 다른 사람들이 제 코드를 보게 된다면, 본질적인 부분이 아닌, 부수적인 부분까지 읽어야 합니다.
    • 부수적인 부분: 어떤 예외를 처리하며, 어떤 exception을 throw하는지
  • 추상화 수준을 동일하게 유지하도록 리팩토링 한다면?
    public void signup(String uid, String nickname) {
        validateUserForSignup(uid, nickname);
        User savedUser = userRepository.save(new User(uid, nickname));
        authorityRepository.save(new Authority(savedUser, ROLE_USER));
    }
    
    private void validateUserForSignup(String uid, String nickname) {
        checkAndThrowIfUserAlreadyExists(uid);
        checkAndThrowIfNicknameAlreadyExists(nickname);
    }
    
    private void checkAndThrowIfUserAlreadyExists(String uid) {
        // 이미 존재하는 유저인 경우 예외처리
        if (userRepository.existsByUid(uid)) {
            throw new CustomException(CommonCode.ALREADY_EXIST_USER);
        }
    }
    
    private void checkAndThrowIfNicknameAlreadyExists(String nickname) {
        // 이미 존재하는 닉네임인 경우 예외처리
        if (userRepository.existsByNickname(nickname)) {
            throw new CustomException(CommonCode.ALREADY_EXIST_NICKNAME);
        }
    }
    • 회원가입을 하려면, 올바른 유저인지 확인하고, 유저를 저장한 뒤, 권한을 저장한다.
      • 책에서 언급하듯, 일련의 TO 문단을 읽듯이 메서드를 읽을 수 있습니다.

⇒ 확실히 추상화 수준을 신경씀으로써 회원가입 로직의 흐름을 명확히 전달할 수 있었고, 이에 따라 가독성이 눈에 띄게 개선되었다고 느꼈습니다.

하지만, 하기의 경우는 추상화 수준을 올바르게 적용한 것인지 의문이 들었습니다.

public ReadUserTestingStats readAnotherTestingStats(Long targetUserId, Long myUserId) {
    User targetUser = userRepository.findById(targetUserId)
            .orElseThrow(() -> new CustomException(CommonCode.NONEXISTENT_USER));
    
    long interactionCounts = userInteractionStatusRepository.countUserInteractionStatusByUserId(myUserId,
            targetUserId);

    return readUserTestingStats(targetUser, interactionCounts);
}
  • 위와 같은 메서드를 리팩토링 한다면?
    public ReadUserTestingStats readAnotherTestingStats(Long targetUserId, Long myUserId) {
        User targetUser = findUserByIdOrThrowIfNonexistentUser(targetUserId);
        long interactionCounts = getInteractionCountBy(targetUserId, myUserId);
    
        return readUserTestingStats(targetUser, interactionCounts);
    }
    
    private User findUserByIdOrThrowIfNonexistentUser(Long userId) {
        return userRepository.findById(userId)
                .orElseThrow(() -> new CustomException(CommonCode.NONEXISTENT_USER));
    }
    
    private long getInteractionCountBy(Long targetUserId, Long myUserId) {
        return userInteractionStatusRepository.countUserInteractionStatusByUserId(myUserId, targetUserId);
    }
    • Spring Data JPA를 활용한 쿼리 문장을 모두 메서드로 분리하게 된다면, 구체적으로 어떤 레포지토리에 쿼리를 날려서 데이터를 받아오는 지 정도의 로우 레벨까지 알 필요가 없기에 가독성 측면에서 이점을 지닌다고 생각합니다. 또한, 쿼리문이 복잡해질수록 메서드 명이 길어져 의미를 파악하기 어려울 때가 있는데, 이런 경우 메서드 분리는 유의미한 행위라고 느껴집니다.
    • 그러나, 이렇게 될 경우 메서드가 너무 많이 쪼개지게 되어 오히려 관리 및 개발 측면에서 부정적인 부분이 파생될 수 있을 것 같다는 생각이 들었습니다.
      • 부정적인 측면: 개발 부담(매번 쿼리를 날릴 때 구현된 메서드가 있는지 확인 → 있다면 재사용, 없다면 생성), 일관성

회고

리팩토링을 진행해보니, 추상화 수준을 고려하며 코드를 생산하는 과정의 필요성을 체감할 수 있었습니다.
이를 지키며 개발을 하게되면 명확하게 핵심만 전달할 수 있기에 가독성 측면에서 많은 이점을 지닌다는 것을 느꼈습니다.
그렇지만 어느정도 깊이까지 이를 엄격히 지켜야 하는지 판단하는 것은 아직까진 어렵고, 미숙하다는 것을 느낍니다.

피드백은 언제나 환영입니다! 🙇🏻‍♂️

post-custom-banner

0개의 댓글