DEV ℧ Developer Diary

[Refactoring] 냄새 1. 이해하기 힘든 이름

해당 포스트는 inflearn의 백기선님의 강의인 리팩토링 을 듣고 정리한 글입니다.

냄새 1. 이해하기 힘든 이름 (Mysterious Name)

  • 깔끔한 코드에서 가장 중요한 것 중 하나가 바로 “좋은 이름”이다.
  • 함수, 변수, 클래스, 모듈의 이름 등 모두 어떤 역할을 하는지 어떻게 쓰이는지 직관적이어야 한다.
  • 처음부터 변수명이나, 함수명을 완벽하게 지을 수 없다. 코드를 다시한번 검토하면서 변수명을 신경쓰거나, 변경/수정하는 좋은 습관을 들이는것이 중요하다.
  • 사용할 수 있는 리팩토링 기술
    • 함수 선언 변경하기 (Change Function Declaration)
    • 변수 이름 바꾸기 (Rename Variable)
    • 필드 이름 바꾸기 (Rename Field)

예제 코드

public class StudyDashboard {

    private Set<String> usernames = new HashSet<>();

    private Set<String> reviews = new HashSet<>();

    /**
     * 스터디 리뷰 이슈에 작성되어 있는 리뷰어 목록과 리뷰를 읽어온다.
     * @param issue
     * @throws IOException
     */
    private void studyReviews(GHIssue issue) throws IOException {
        List<GHIssueComment> comments = issue.getComments();
        for (GHIssueComment comment : comments) {
            usernames.add(comment.getUserName());
            reviews.add(comment.getBody());
        }
    }

    public Set<String> getUsernames() {
        return usernames;
    }

    public Set<String> getReviews() {
        return reviews;
    }

    public static void main(String[] args) throws IOException {
        GitHub gitHub = GitHub.connect();
        GHRepository repository = gitHub.getRepository("dh37789/basic");
        GHIssue issue = repository.getIssue(1);

        StudyDashboard studyDashboard = new StudyDashboard();
        studyDashboard.studyReviews(issue);
        studyDashboard.getUsernames().forEach(System.out::println);
        studyDashboard.getReviews().forEach(System.out::println);
    }
}

해당 코드는, 깃허브의 repository에서 이슈에 대한 리뷰와, 리뷰어의 목록을 불러오는 코드이다. 위의 코드를 이용하여, 상황에 맞게 리팩토링을 진행해 보고자 한다.

리팩토링 1. 함수 선언 변경하기

함수 이름 변경하기, 메소드 이름 변경하기, 매개변수 추가하기, 매개변수 제거하기, 시그니처 변경하기

  • 좋은 이름을 가진 함수는 함수가 어떻게 구현되었는지 코드를 보지않아도 이름만 보고도 이해할 수 있다.
  • 좋은 이름을 찾아내는 방법? 함수에 주석을 작성한 다음, 주석을 함수 이름으로 만들어 본다.
  • 함수의 매개변수는
    • 함수 내부의 문맥을 결정한다 (예. 전화번호 포매팅 함수, 어떤 타입으로 받을 것인지, 함수 내부에 많은 정보를 줄것인지, 필요한 것만 줄것인지.. 등등
    • 의존성을 결정한다. (예. Payment 만기일 계산 함수, Payment에 종속된다면 Payment타입을, date속성만 필요하다면 date만 넘겨줄것인지 등등..)

1.1 역할에 맞는 함수명 변경

studyReviews 함수를 보면, GHIssue 객체를 받아, GHIssueComment에 해당 이슈에 대한 유저와 리뷰를 불러오는 역할을 한다. studyReviews의 함수명만 본다면 위에 해당하는 역할이 떠오르는가? 한번 역할에 맞게 해당 코드를 리팩토링 해보도록 하자.

private void studyReviews(GHIssue issue) throws IOException {
    List<GHIssueComment> comments = issue.getComments();
    for (GHIssueComment comment : comments) {
        usernames.add(comment.getUserName());
        reviews.add(comment.getBody());
    }
}

IntelliJ에서 함수명을 클릭한 뒤 Shift + F6을 누르면 함수명과 외부에서 사용하고있는 종속되어 호출되거나 선언된 함수명을 모두 변경해 줄 수 있다.

먼저 repository의 reivew를 호출하는 함수이기 때문에, studyReviews 에서 loadReviews라는 이름으로 변경을 해주도록 하자.

private void loadReviews(GHIssue issue) throws IOException {
    List<GHIssueComment> comments = issue.getComments();
    for (GHIssueComment comment : comments) {
        usernames.add(comment.getUserName());
        reviews.add(comment.getBody());
    }
}

1.2 함수역할에 따른 매개변수 변경

메인 함수를 보면 호출하는 리뷰는 정해져 있다. 만약 해당 함수가 특정되지 않은 repository의 특정되지 않는 issue를 가져온다면, 매개변수는 달라질 것이다.

GitHub gitHub = GitHub.connect();
GHRepository repository = gitHub.getRepository("dh37789/basic");
GHIssue issue = repository.getIssue(1);

하지만 main함수 내에 선언되어있는 repository와 issue는 dh37789/basic와, 1번으로 고정해져 있다. 이뜻은 파라미터를 따로 주지 않아도 된다는 뜻이다. 그렇다면, 매개변수로 issue의 정보를 따로 넘겨주지 않아도 될것이다. issue를 불러오는 코드를 loadReviews안에 넣어주도록 하자.

private void loadReviews() throws IOException {
    GitHub gitHub = GitHub.connect();
    GHRepository repository = gitHub.getRepository("dh37789/basic");
    GHIssue issue = repository.getIssue(1);

    List<GHIssueComment> comments = issue.getComments();
    for (GHIssueComment comment : comments) {
        usernames.add(comment.getUserName());
        reviews.add(comment.getBody());
    }
}

리팩토링 2. 변수 이름 바꾸기

  • 더 많이 사용되는 변수일수록 그 이름이 더 중요하다.
    • 람다식에서 사용하는 변수 vs 함수의 매개변수
  • 다이나믹 타입을 지원하는 언어에서는 타입을 이름에 넣기도 한다.
  • 여러 함수에 걸쳐 쓰이는 필드 이름에는 더 많이 고민하고 이름을 짓는다.

2.1 상황에 맞는 변수면 변경 (1)

main 함수를 보면 람다식을 이용하여, 리뷰어의 이름과, 리뷰를 출력하는 로직이 있다.

public static void main(String[] args) throws IOException {
    StudyDashboard studyDashboard = new StudyDashboard();
    studyDashboard.loadReviews();
    studyDashboard.getUsernames().forEach(name -> System.out.println(name));
    studyDashboard.getReviews().forEach(review -> System.out.println(review));
}

람다식 내부에서 사용하는 변수 name과, review의 변수명은 이미 studyDashboard.getUsernames()이나 studyDashboard.getReviews()로 명시가 되어있기 때문에 n, r과 같이 줄여서 사용을 해도 된다. 하지만 name과 review로 명시를 해주는것도 틀린 방법은 아니다.

studyDashboard.getUsernames().forEach(n -> System.out.println(n));
studyDashboard.getReviews().forEach(r -> System.out.println(r));

또한 내부 변수를 사용하는 것이 아닌 람다식의 메소드 참조방식을 이용하여, 코드를 줄일 수도 있을 것이다.

studyDashboard.getUsernames().forEach(System.out::println);
studyDashboard.getReviews().forEach(System.out::println);

2.2 상황에 맞는 변수명 변경 (2)

이제 loadReviews함수 내부의 변수를 살펴보도록 하자.

private void loadReviews() throws IOException {
    GitHub gitHub = GitHub.connect();
    GHRepository repository = gitHub.getRepository("dh37789/basic");
    GHIssue issue = repository.getIssue(1);

    List<GHIssueComment> comments = issue.getComments();
    for (GHIssueComment comment : comments) {
        usernames.add(comment.getUserName());
        this.reviews.add(comment.getBody());
    }
}

이슈에 달리는 리뷰들은 코멘트 형식으로, comments 라는 변수를 사용하는것도 틀리지는 않다. 하지만 함수명과 맞게 통일을 해주는것을 추천한다. loadReviews 함수안에 load를 하는 review들이 없지 않은가?

List<GHIssueComment> reviews = issue.getComments();
for (GHIssueComment review : reviews) {
    usernames.add(review.getUserName());
    this.reviews.add(review.getBody());
}

comments의 변수명을 reviews라는 변수명으로 변경해 줌으로써, loadReviews함수의 명시를 보다 정확하게 표현해 주었다.

리팩토링 3. 필드 이름 바꾸기

  • Record 자료 구조의 필드 이름은 프로그램 전반에 걸쳐 참조될 수 있기 때문에 매우 중요하다.
    • Record 자료 구조 : 특정 데이터와 관련있는 필드를 묶어놓은 자료 구조.
    • 파이썬의 Dictionay, 또눈 줄여서 dicts.
    • C#의 Record.
    • 자바 14 버전 부터 지원. (record 키워드)
    • final와 같이 immutable하다.
    • 자바에서는 Getter와 Setter 메소드 이름도 필드의 이름과 비슷하게 간주할 수 있음.

3.1 review 필드명 바꾸기

필드명은 중요하다. 클래스 전반적으로 사용 될 수 있기 때문에, 주의해서 이름을 지어야한다.

private Set<String> usernames = new HashSet<>();

먼저 가장 상단에 선언된 username의 필드를 보자. 코드를 작성할 당시에는 리뷰어의 username들을 가져오는 Set 컬렉션이기 때문에 usernames로 작성했지만, 조금더 직관적으로 reviwers라고 명시해 줄 수 있을 것이다.

private Set<String> reviewers = new HashSet<>();

이렇게 필드명에 대한 변경은 간단하다. 하지만, reviewerreview는 다른곳에서도 공통적으로도 쓰일수 있으니 객체로 빼주는것은 어떨까? 보통은 class로 빼줄 수 있겠지만, 강의에서는 record라는 필드를 사용하였기에 강의를 따라하도록 하겠다.

record에 대한 자세한 사항은 따로 포스팅을 작성하고자 한다.

/**
 * Java 14부터 지원
 * Getter, Setter, Hashcode, equals 가 자동으로 생성
 * */
public record StudyReview(String reviewer, String review) {
}

StudyReview 라는 reviewerreview의 필드를 받는 record를 생성했다. record를 간단하게 설명한다면, final과 같이 불변이고, Getter, Setter, Hashcode, equals 가 자동으로 생성 된다.

이제 Set컬렉션으로 선언된 reviewers와, review를 해당 객체로 분리해 보자.

private Set<StudyReview> studyReviews = new HashSet<>();

위와 같이 StudyReview 객체를 사용하여 reviewers와, review를 하나로 묶어 주었다.

그리고 해당 필드를 사용하던 로직들을 변경된 필드변수에 맞게 수정한다.

이제 전체적으로 리팩토링을한 코드를 살펴보면 다음과 같다.

리팩토링된 예제코드

  • StudyReview
public record StudyReview(String reviewer, String review) {
}
  • StudyDashboard
public class StudyDashboard {

    private Set<StudyReview> studyReviews = new HashSet<>();

    /**
     * 스터디 리뷰 이슈에 작성되어 있는 리뷰어 목록과 리뷰를 읽어옵니다.
     * @throws IOException
     */
    private void loadReviews() throws IOException {
        GitHub gitHub = GitHub.connect();
        GHRepository repository = gitHub.getRepository("dh37789/basic");
        GHIssue issue = repository.getIssue(1);

        List<GHIssueComment> reviews = issue.getComments();
        for (GHIssueComment review : reviews) {
            studyReviews.add(new StudyReview(review.getUserName(), review.getBody()));
        }
    }

    public Set<StudyReview> getStudyReviews() {
        return studyReviews;
    }

    public static void main(String[] args) throws IOException {
        StudyDashboard studyDashboard = new StudyDashboard();
        studyDashboard.loadReviews();
        studyDashboard.getStudyReviews().forEach(System.out::println);
    }
}