[리팩토링] 악취 2. 중복 코드

 

✍️ 악취 : 중복 코드

일반적으로 완전히 동일하거나 비슷한 코드를 중복 코드라고 한다. 중복 코드는 몇 가지 치명적인 문제를 가지고 있는데 코드를 읽는 사람 입장에서 비슷한 코드인지 유사한 코드인지 주의 깊게 읽어야 하며, 코드가 변경되면 동일한 모든 코드가 변경되어야 한다. 

 

중복 코드를 위해 세 가지 리팩토링을 활용할 수 있다.

1. 함수 추출하기

2. 코드 정리하기

3. 메서드 올리기

 

악취 가득한 코드

@AllArgsConstructor
@Getter
public class Customer {

    private int id;

    @Setter
    private String name;
    @Setter
    private String address;

}
@Getter
public class CustomerRepository {

    private List<Customer> customers = new ArrayList<>();

    private void insert(Customer insertObj) {
        Customer exist = customers.stream()
                .filter(c -> c.getId() == insertObj.getId())
                .findAny()
                .orElse(null);

        if (exist != null)
            return;

        customers.add(insertObj);
    }

    private void update(Customer updateObj) {
        Customer exist = customers.stream()
                .filter(c -> c.getId() == updateObj.getId())
                .findAny()
                .orElse(null);

        if (exist == null)
            return;

        exist.setName(updateObj.getName());
        exist.setAddress(updateObj.getAddress());
    }


    public static void main(String[] args) {
        Customer customer = new Customer(1, "Kang", "seoul");
        CustomerRepository repository = new CustomerRepository();

        repository.insert(customer);
    }
}

 

🍊 함수 추출하기

메서드의 이름은 "의도"를 표현한 것이고 메서드의 구현부는 말 그대로 "구현"을 표현한 것이다.

무슨 일을 하는 코드인지 알아내려고 노력해야 한다면 해당 코드를 메서드로 분리해서 이름으로 의도를 표현하면 구현부를 보다 쉽게 파악할 수 있다. 한 줄짜리 메서드도 의도를 잘 표현할 수 있다면 충분히 좋은 메서드라고 생각한다.

 

insert, update 메서드는 파라미터로 들어온 Customer의 존재 여부를 체크하는 공통된 코드를 가지고 있기에 함수로 추출이 가능해 보인다.

@Getter
public class CustomerRepository {

    private List<Customer> customers = new ArrayList<>();

    private void insert(Customer insertObj) {
        Optional<Customer> optionalCustomer = findCustomer(insertObj.getId());
        if (optionalCustomer.isPresent())
            return;

        customers.add(insertObj);
    }

    private void update(Customer updateObj) {
        Optional<Customer> optionalCustomer = findCustomer(updateObj.getId());
        if (!optionalCustomer.isPresent())
            return;

        Customer customer = optionalCustomer.get();
        customer.setName(updateObj.getName());
        customer.setAddress(updateObj.getAddress());
    }

    private Optional<Customer> findCustomer(int customerId){
        return customers.stream().filter(c -> c.getId() == customerId).findAny();
    }

    public static void main(String[] args) {
        Customer customer = new Customer(1, "Kang", "seoul");
        CustomerRepository repository = new CustomerRepository();

        repository.insert(customer);
    }
}

 

🌱 코드 정리하기

관련 있는 코드는 묶어서 작성해야 더 쉽게 이해할 수 있다. 가령 메서드에서 사용할 변수를 상단에 미리 정의하기보단 해당 변수를 사용할 코드 바로 위에 선언하자.

public static void main(String[] args) {
    CustomerRepository repository = new CustomerRepository();

    // 선언과 사용을 함께
    Customer customer = new Customer(1, "Kang", "seoul");
    repository.insert(customer);
}

 

🍃 메서드 올리기 

중복 코드는 당장은 잘 동작하더라도 미래에 버그를 만들 가능성이 높다. 예를 들어 한 쪽에선 코드를 고치고 다른 한쪽에선 반영하지 않은 경우가 그러하다.

 

만약 여러 하위 클래스에 동일한 코드가 있다면 메서드 올리기를, 비슷하지만 일부 값만 다르다면 함수 매개변수화를, 하위 클래스에 있는 코드가 상위 클래스가 아닌 하위 클래스에 의존한다면 필드 올리기를, 여러 메서드가 비슷한 절차를 밟고 있다면 템플릿 메서드 패턴을 적용할 수 있다.  

 

다음은 매우 심플한 메서드 올리기와 필드 올리기 예제이다.

VipCustomer, VVipCustomer 모두 discounRate를 별도의 멤버로 갖고 있으며 get 메서드도 동일하다.

@AllArgsConstructor
public class Customer {

    private int id;
    private String name;
}

public class VipCustomer extends Customer {

    private double discountRate;

    public VipCustomer(int id, String name) {
        super(id, name);
        this.discountRate = discountRate;
    }

    public double getDiscountRate() {
        return discountRate;
    }
}

public class VVipCustomer extends Customer {

    private double discountRate;

    public VVipCustomer(int id, String name, double discountRate) {
        super(id, name);
        this.discountRate = discountRate;
    }

    public double getDiscountRate() {
        return discountRate;
    }
}

 

모두 상위 클래스로 옮겨갈 수 있다.

@AllArgsConstructor
public class Customer {

    private int id;
    private String name;
    private double discountRate;

    public double getDiscountRate() {
        return discountRate;
    }
}

public class VipCustomer extends Customer {

    public VipCustomer(int id, String name, double discountRate) {
        super(id, name, discountRate);
    }
}

public class VVipCustomer extends Customer {

    public VVipCustomer(int id, String name, double discountRate) {
        super(id, name, discountRate);
    }
}