BackEnd/Refactoring

[Refactoring] 이해하기 힘든 이름 (Mysterius Name)

샤아이인 2022. 2. 19.

백기선 님의 리팩터링 강의를 들으며 요약한 내용입니다.

 

1. 이해하기 힘든 이름

깔끔한 코드에서 가장 중요한 것 중 하나가 바로 “좋은 이름”이다.

함수, 변수, 클래스, 모듈의 이름 등 모두 어떤 역할을 하는지 어떻게 쓰이는지 직관적으로 이해할 수 있어야 한다.

 

다음과 같은 방식으로 이름을 리팩터링 할 수 있다.

  • 함수 선언 변경하기 (Change Function Declaration)
  • 변수 이름 바꾸기 (Rename Variable)
  • 필드 이름 바꾸기 (Rename Field)

 

1. 함수 선언 변경하기

함수의 선언 변경에는 함수 이름 변경하기, 메서드 이름 변경하기, 매개변수 추가하기, 매개변수 제거하기, 시그니처 변경하기 모두 포함 가능하다.

 

좋은 함수 이름이란, 함수의 이름만 봐도 어떤 일을 할 수 있는지 알 수 있어야 한다.

이를 위해 이름을 만들 때, 바로 작명하는 것 이 아니라 주석으로 먼저 설명을 해 본 후에 이름을 만들기를 권장한다.

 

함수에게 전달되는 매개변수의 경우

 

1) 함수 내의 문맥을 결정한다.

파라미터로 단순하게 String을 받으면 01012345678을 넘겨주면 010-1234-5678로 변환시켜 주는 포맷팅 기능을 하게 되는 경우

파라미터로 단순 String이 아닌, Person이라는 객체(내부에 전화번호 String을 갖고 있는)를 넘겨받는 겨우

각 경우 어떤 타입을 함수의 인자로 넘겨받는가에 따라서 문맥 정보다 달라진다.

 

단순 String으로 번호만 넘기면, 그 함수가 사용할 수 있는 정보는 딱 String 뿐이다. => 함수는 딱 전화번호라는 정보만 가지고 있음

전화번호가 있는 Person을 전달하면 => Person이 공개해 둔 모든 정보를 참조할 수 있게 된다.

 

가령, Person에게 지역번호라는 정보가 있으면 자동으로 서울이면 02를 번호 앞에 추가해주는 기능 등이 함수에서 가능하다.

 

2) 의존성을 결정한다.

위 Person에서의 경우와 비슷하다.

Person 내부의 전화번호 외의 다른 것들이 필요하면 Person을 인자로 넘겨주면 된다.

다만 이렇게 되면 Person내부의 여러 부분에 의존성이 생겨버린다.

 

반대로 단순하게 번호만 넘긴다면 의존성은 약해질 것이다.


예시로 살펴보자.

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 static void main(String[] args) throws IOException {
    GitHub gitHub = GitHub.connect();
    GHRepository repository = gitHub.getRepository("whiteship/live-study");
    GHIssue issue = repository.getIssue(30);

    ...
    studyDashboard.loadReviews(issue);
	...
}

studyReviews는 깃허브의 issue를 읽어와서 사용자의 이름과 리뷰를 추가해주는 함수이다.

 

이를 사용하는 Client의 입장에서 studyReviews를 호출할 때 의미 전달이 명확하지가 않다.

"스터디를 리뷰 한다는 건가? 아니면 리뷰를 읽어 오겠다는 건가?" 의미가 모호하다.

 

이름을 loadReviews로 변경하니 의미 전달이 명확해졌다.

 

다음으로 매개변수를 생각해 보자. loadReviews(issue)는 인자로 issue를 전달받고 있다. 꼭 전달받아야 할까??

하지만 잘 생각해보면, 사실 로딩할 issue는 이미 정해져 있다. => 꼭 인자로 전달받을 필요가 없다!

다음과 같이 변경되었다.

private void studyReviews() throws IOException {
    GitHub gitHub = GitHub.connect();
    GHRepository repository = gitHub.getRepository("whiteship/live-study");
    GHIssue issue = repository.getIssue(30);
    
    List<GHIssueComment> comments = issue.getComments();
    for (GHIssueComment comment : comments) {
        usernames.add(comment.getUserName());
        reviews.add(comment.getBody());
    }
}

// 생략

public static void main(String[] args) throws IOException {

    ...
    studyDashboard.loadReviews();
	...
}

 

2. 변수 이름 바꾸기

1. 더 많이 사용되는 변수일 수록 그 이름이 더 중요하다.

=> 람다식에서 사용하는 변수 vs 함수의 매개변수

public static void main(String[] args) throws IOException {
	// 생략

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

위 코드에서 getUsernames().forEach문 안의 람다식은 scope가 매우 작다.

이런 경우 대부분 컬렉션 내부에 어떤 값이 저장되어 있는지 이미 알고 있다.

따라서 간략하게 n 과같이 사용하기도 하고, 아니면 메서드 참조를 사용하면 더 편하다.

 

다른 지역 변수의 이름을 변경하는 예시를 하나 더 보자.

private void loadReviews() throws IOException {
    GitHub gitHub = GitHub.connect();
    GHRepository repository = gitHub.getRepository("whiteship/live-study");
    GHIssue issue = repository.getIssue(30);

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

이미 위에서 봤었던 함수이다.

loadReviews()는 리뷰를 불러오는 함수인데, 내부에 review가 없다???

comments라는 변수는 사실 본문에 달려 있는 댓글이지만, 본문에 대한 review로써 reviews라는 이름이 좀 더 적절하다 생각한다.

 

2. 다이나믹 타입을 지원하는 언어에서는 타입을 이름에 넣기도 한다.

대표적으로 Python은 동적 타입 언어이기 때문에 변수의 이름들이 메모리에 함께 저장됩니다. 런타임에서 생성되기 때문이죠.

 

3. 여러 함수에 걸쳐 쓰이는 필드 이름에는 더 많이 고민하고 이름을 짓는다.

 

3. 필드 이름 바꾸기

Record의 필드 이름 같은 경우 어떤 한 Class를 넘어선 범위에서 사용될 수 있기 때문에 Scope가 넓어 매우 중요하다.

 

Record 자료 구조: 특정 데이터와 관련 있는 필드를 묶어놓은 자료 구조

 

Java 14부터는 아예 Record라는 키워드를 지원한다. 사용해 보자!

public record StudyReview(String reviewer, String review) {
}

record 키워드는 파리미터를 갖고 있다.

이렇게 만 만들면, getter와 비슷한 기능을 하는 함수, toString, equals를 자동으로 지원해 준다.

 

Java에서의 Records 정의

Records는 자바 언어적으로 새롭게 소개되는 클래스의 일종이다. 클래스, 인터페이스, 열거형과 동일한 레벨로 Records가 취급된다. 

Records의 목적은 클래스 내부에 선언되어 있는 소규모의 변수들을 정의하기 위한 새로운 방법을 제시하는 것이며, 개발자의 반복 작업을 언어적으로 줄여주기 위해 사용한다.

Records에서 정의한 데이터는 불변성을 가진다. 즉, 처음 값을 설정한 이후 조회만 할 수 있고 변경은 불가능하며 이를 통해 멀티스레드 환경에서도 안전하게 사용할 수 있도록 하였다. 

 

이를 사용하는 코드로 기존의 코드를 변경해 봅시다.

public class StudyDashboard {

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

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

        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);
    }
}

 

댓글