객체지향 생활 체조 원칙

9 분 소요

클린 코드는 코드의 가독성, 유지 보수성 및 협업성을 향상시키는 코드를 말한다.
쉽게 말하여 단순히 읽기 쉽고, 각 역할마다 주어진 하나의 일만 담당하며 복잡하거나 모호하지 않은 코드를 말한다.

⚡클린코드 규칙

1. 한 메서드에 최소한의 들여쓰기(indent)만 허용했는가? 최대 depth : 2까지만 허용한다.

한 메서드에 들여쓰기가 여러 개 존재한다면 해당 메서드는 하나의 일만 하고 있다고 생각할 수 없다.

🛠 indent(들여쓰기) depth(깊이)를 줄이는 가장 좋은 방법은 메서드를 분리하는 방법이있다.

🔽 소스코드로 depth 확인하기.

class Main {
  public static void main(String[] args) {
    // depth: 0
    for {
      // depth: 1
      while {
        // depth: 2
      }
    }
  }
}

위의 소스코드 처럼 for문안에 while이 있으면 들여쓰기는 2이다.

🔽 메소드를 분리하여 코드의 depth 줄이기.

public class Main {
    public static void main(String[] args) {
        int result = sumNumbers(5);
        System.out.println("Result: " + result);
    }

    public static int sumNumbers(int n) {
        int sum = 0;
        // depth: 1
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return sum;
    }
}

✅ 한 메서드에서 로직을 모두 구현하는것이 아니라 메서드를 분리하여 depth를 줄일 수 있다.



2. else 예약어를 사용하지 않았는가?

else 예악어를 사용하면 분기에 대한 depth가 깊어질 수 있다.

🛠 else 예약어를 쓰지 않는다는 말은 [분기문을 최소한으로 한다]라고 해석하자.

좋은 분기문(if) 작성방법

🔽 사용자의 입력을 받아 입력된 숫자가 양수,음수,0 인지 확인하는 코드

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.print("Enter a number: ");
        int number = scanner.nextInt();

        if (number > 0) {
            System.out.println("Number is positive.");
        } else if (number < 0) {
            System.out.println("Number is negative.");
        } else {
            System.out.println("Number is zero.");
        }
    }
}

💬 코드를 수정해보자.!

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.print("Enter a number: ");
        int number = scanner.nextInt();

        if (number > 0) {
            System.out.println("Number is positive.");
        }
        if (number < 0) {
            System.out.println("Number is negative.");
        }
        if (number == 0) {
            System.out.println("Number is zero.");
        }
    }
}

두 소스코드는 동일한 결과를 출력하지만 else를 사용하지 않은 코드는 더 간결해지고 명확해진다.
또한 ealry exit pattern을 적용해서 코드의 의도를 분명히 나타낼 수 있다.



3.모든 원시값과 문자열을 포장했는가?

🛠 원시타입의 변수를 의미가 있는 객체로 포장한다.

원시타입 데이터는 아무런 의미를 갖고 있지 않다. 원시값의 의미는 변수명으로 추론할 수 밖에 없다.

🔽 변수를 선언하는 2가지 방법

int age = 24;   //원시 타입의 변수
Age age = new Age(24);  //원시 타입의 변수를 객체로 포장한 변수

🔽 멤버변수를 원시타입의 변수를 선언한 코드

public class User{
        private int age;

        public User(int age) {
            if(age < 0){
                throw new RuntimeException("나이는 0살 부터 시작합니다.");
            }
            this.age = age;
        }
    }

❌ 위 소스 코드는 User 클래스본인이 나이에 관한 유효성 검사를 하게된다.
만약 User 클래스에 멤버변수가 늘어나게 된다면 클래스의 부담감이 매우 높아질것이다.

🔽 기존 User클래스에 멤버변수를 추가한 코드

public class User {
    private int age;
    private String name;
    private String phone;

    public User(int ageValue, String nameValue, String phoneValue) {
        validateAge(ageValue);
        validateName(nameValue);
        validatePhone(phoneValue);
        this.age = ageValue;
        this.name = nameValue;
        this.phone = phoneValue;
    }

    public void validateAge(int age){
        if(age < 0){
            throw new RuntimeException("Age must be at least 0 years.");
        }
    }

    public void validateName(String name){
        if(name.length() < 2){
            throw new RuntimeException("The name must be at least 2 characters long.");
        }
    }

    public void validatePhone(String phone){
      ...
    }
}

만약 User 클래스의 멤버변수가 여러개라면 멤버변수의 상태에 대한 책임은 모두 User 클래스가 갖게된다.
그러므로 자연스럽게 코드는 복잡해지고 가독성과 유지보수가 힘들어지게된다.

💬 원시타입의 변수를 의미있는 객체로 포장해주자

public class User{
  private Age age;
  private Name name;

    public User(int ageValue, String nameValue, Phone phoneValue) {
        this.age = new Age(ageValue);
        this.name = new Name(nameValue);
    }
}
  
public class Age{
    private int age;

    public Age(int age) {
        if(age < 0){
            throw new RuntimeException("나이는 0 살 부터 시작 합니다.");
        }this.age = age;
    }
}

public static class Name{
    private String name;

    public Name(String name) {
        if(name.length() < 2){
            throw new RuntimeException("이름은 두 글자 이상 이어야 합니다.");
        }
        this.name = name;
    }
}

원시타입의 변수를 의미있는 객체로 포장해주면서 멤버변수의 대해 유효하지 않은 값이 들어왔을때,
User클래스가 아닌 각각의 객체들에서 유효성 검사를 하게된다.

User클래스가 가지고 있던 멤버변수들이 의미가 있는 객체가 되면서 스스로의 상태를 관리하고 유지보수가 원활할 수 있게 되었다.

이러한 형태를 값 객체 VO(Value Object)라고 한다.


4. 콜렉션에 대해 일급 컬렉션을 적용했는가?

🛠 일급컬렉션이란? Collection을 Wrapping하면서 그 외 다른 변수가 없는 클래스의 상태

🔽 일급컬렉션을 적용하지 않은 소스코드

public class Person {
    private String name;
    private List<Car> cars;
    // ...
}

public class Car {
    private String name;
    private String oil;
    // ...
}

위 코드에서 List<Car> cars를 다음과 같이 의미있는 객체로 포장한다는 개념이다.

🔽 cars를 일급컬렉션으로 적용하여 변경한 소스코드

public class Person {
    private String name;
    private Cars cars;
    // ...
}

public class Cars {
    // 멤버변수가 하나 밖에 없다
    private List<Car> cars;
    // ...
}

public class Car {
    private String name;
    private String oil;
    // ...
}

Cars 클래스를 보면 컬렉션을 포장하였고 그 외 다른 멤버변수가 없다.
바로 일급컬렉션으로 적용하였다는 의미이다.


💬 다른 소스코드로 예를 들어보자.

public class FruitBasket {

    //멤버변수가 하나밖에 없다.
    private List<Fruit> fruits = new ArrayList<>();

    public FruitBasket(List<Fruit> fruit){
        validateSize(fruit)
        this.fruits = fruit;
    }

    public void validateSize(List<Fruit> fruit){
        if (fruit.size() >= 10){
            new throw IllegalArgumentException("과일은 10개이상 담을 수 없습니다");
        }
    }

    public double calculateTotalWeight() {
        double totalWeight = 0.0;
        for (Fruit fruit : fruits) {
            totalWeight += fruit.getWeight();
        }
        return totalWeight;
    }

    public int countFruits() {
        return fruits.size();
    }
}

public class Fruit {
    private String name;
    private double weight;

    public Fruit(String name, double weight) {
        this.name = name;
        this.weight = weight;
    }

    public double getWeight() {
        return weight;
    }
}

FruitBasket은 일급 컬렉션으로 상태와 로직을 따로 관리할 수 있게된다.
때문에 FruitBasket 로직이 사용되는 클래스의 부담을 줄일 수 있고 코드의 중복을 줄일 수 있게된다.



5. 3개 이상의 인스턴스 변수를 가진 클래스를 구현하지 않았는가?

🛠 인스턴스 변수가 많은 수록 클래스의 응집도가 낮아진다.

💬 클래스의 응집도는 클래스 내부의 속성이나 메서드들이 얼마나 밀접하게 관련되어 있는지를 나타내는 개념이다

높은 응집도를 가진 클래스는 그 내부의 요소들이 서로 관련된 작업을 수행하며, 하나의 목적을 가지고 있는 것을 의미한다.

예를 들어보면 주방 도구와 서랍은 높은 응집도를 가진다고 볼수 있다. 왜냐하면 주방 도구 서랍 안의 모든 항목은 주방 관련 작업을 수행하는 데 사용되는 도구들 이기 때문이다.

반면에, 주방 도구와 욕실 용품이 섞여있는경우는 낮은 응집도를 가지고 있다라고 말할 수 있다.

💬 최대한 클래스를 많이 분리하여 높은 응집도를 유지할 수 있다.

예를 들어, 주문 처리 시스템을 개발한다고 가정하면 이시스템에서 주문,결제,배송,재고 관리등의 작업을 수행해야한다. 이러한 작업을 모두 하나의 클래스에 집중시키면 해당 클래스가 복잡해지고 응집도가 낮아질 수 있다.

자판기를 만든다고 가정하자. 이 자판기에는 음료수와 과자를 판매하는 두 가지 기능이 있다.

public class VendingMachine {
    public void dispenseSoda() {
        // 음료수를 내보내는 코드
    }

    public void dispenseSnack() {
        // 과자를 내보내는 코드
    }
}

💬 위 코드에서 VendingMachine 클래스는 두 가지 서로 다른 작업을 수행한다. 이 클래스는 음료수를 내보내는 메서드와 과자를 내보내는 메서드를 모두 포함하고 있으므로 응집도가 낮다.

public class SodaMachine {
    public void dispenseSoda() {
        // 음료수를 내보내는 코드
    }
}

public class SnackMachine {
    public void dispenseSnack() {
        // 과자를 내보내는 코드
    }
}

💬 음료수를 판매하는 클래스와 과자를 판매하는 클래스로 작업을 분리하여 각 클래스는 하나의 작업을 집중하고, 그에따라 높은 응집도를 가질 수 있게 되었다.

🔽 클래스를 분리하는 다른 예시코드

public class Customer{
    private Name name;
    private CustomerId customerId;
}

public class Name{
    private String firstName;
    private String lastName;
}


public class CustomerId{
    private int id;
}



6. 핵심 로직을 구현하는 도메인 객체에 getter/setter를 사용하지 않고 구현했는가? 단 DTO 허용

🛠 상태를 가지는 객체를 추가했다면 객체가 제대로 된 역할을 하도록 구현해야 한다.
객체가 로직을 구현하도록 해야한다. 상태 데이터를 꺼내 로직을 처리하도록 구현하지 말고 객체에 메시지를 보내 일을 하도록 리팩토링한다.

💬 객체는 다른 객체와 메세지를 주고 받으면서 협력한다.
객체는 메시지를 받으면 객체 그에 따른 로직(행동)을 수행하게 되고, 필요하다면 객체 스스로 내부의 상태값을 변경하게된다. 즉 객체 스스로 일을 하도록 한다.

💬 모든 멤버변수에 getter를 생성해 놓고 상태값을 꺼내 그 값으로 객체 외부에서 로직을 수행한다면, 객체가 로직(행동)을 갖고 있는 형태가 아니고 메시지를 주고 받는 형태도 아니게 된다. 또한 객체 스스로 상태값을 변경하는것도 아니고 객체 스스로 일하는 것이 아니게 된다.

🔽 예제 코드

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public static void main(String[] args) {
        Person person1 = new Person("Alice", 30);

        int personAge = person1.getAge();

        // 객체 외부에서 로직을 수행
        performLogic(personAge);
    }

    public static void performLogic(int age) {
        // 상태값을 받아 로직 수행
        if (age >= 18) {
            System.out.println("성인입니다.");
        } else {
            System.out.println("미성년자입니다.");
        }
    }
}

🔽 변경후 소스코드

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public boolean isAdult(int age) {
        if(age >= 18){
            return true;
        }
        return false;
    }

    public static void main(String[] args) {
        Person person1 = new Person("Alice", 30);
        isAdult(30);
    }
}

✅ 객체에게 메세지를 보내 스스로 단순히 값을 꺼내와 외부에서 로직을 실행하는 것이아니라 객체에게 메세지를 보내어 객체가 스스로 일을 할 수 있도록 수정하였다.



7. 코드 한 줄에 점(.)을 하나만 허용했는가

🛠 디미터의 법칙 : 친구하고만 대화하라

💬 디미터의 법칙(Law of Demeter) 줄여서 LoD 라고도 불린다.

객체는 자신의 직접적인 관계 객체와만 상호 작용하고,
다른 객체의 내부 구조에 직접 적근해서는 안된다는 의미이다.

🔽 디미터법칙을 위배하는 코드

public class Person{
    private Wallet wallet;

    public Person(Wallet wallet){
        this.wallet = wallet;
    }

    public boolean hasEnoughMoney(double amount){
        //Person 클래스가 Wallet 클래스의 내부 변수에 직접 접근하였다.
        return wallet.balance >= amount;
    }
}

public class Wallet{
    public double balance;

    public Wallet(double balance){
        this.balance = balance;
    }
}

🔽 디미터법칙을 따르는 코드

public class Person {
    private Wallet wallet;

    public Person(Wallet wallet) {
        this.wallet = wallet;
    }

    public boolean hasEnoughMoney(double amount) {
        // Person 클래스가 Wallet 클래스의 메서드를 통해 작업을 수행
        return wallet.hasSufficientFunds(amount);
    }
}

public class Wallet {
    private double balance;

    public Wallet(double balance) {
        this.balance = balance;
    }

    public boolean hasSufficientFunds(double amount) {
        return balance >= amount;
    }
}

💬 Person 클래스가 Wallet 클래스 내부 변수에 직접접으로 접근하지 않고 Wallet 클래스의 메서드를 통해 작업을 수행한다.

객체는 점 표기법을 사용하여 다른 객체의 메서드를 호출할 수 있지만, 그것을 넘어 객체의 내부에 접근하여서는 안된다. 객체는 자신의 직접적인 관계 객체에만 접근하여야됌.

✅ 디미터의 법칙을 따르면 객체 간의 결합도가 낮아시며 유지보수성이 높아진다.



8. 메소드의 인자 수를 3개 이하로 제한했는가?(3개를 초과하는 인자는 허용하지 않는다. 3개도 가능하면 줄이기위해노력한다)

💬 메서드에 인자가 많다는 것은 메서드가 수행해야 하는 작업이 복잡하다는 신호 일 수 있다.

또한 복잡하다는 의미는 곧 메서드가 한가지 일만 담당하지 않다는 의미로도 해석될 수 있다



9. 메서드가 한가지 일만 담당하도록 구현했는가?

💬 메서드가 한 가지 일만 담당하도록 구현해야하는 이유는 코드의 가독성, 유지보수성 등을 향상시키기 위함이다.
이를 단일 책임 원칙 또는 SRP 라고 한다.

메서드가 한 가지 일만 담당하면 그 메서드의 이름과 내용이 명확해진다. 코드를 읽는 사람은 메서드의 이름을 보고 어떤 일을 하는지 쉽계 이해할 수 있다.

단일 책임 원칙을 따르는 메서드는 특정 작업 또는 기능을 수행하는데 집중하므로 수정 또는 확장이 필요한 경우 해당 메서드만 수정하면된다. 다른 메서드에 영향을 미치지 않으므로 버그 발생성이 줄어든다.



10. 클래스를 작게 유지하기 위해 노력했는가 (메서드당 line을 10까지만 허용하며 길이가 길어지면 메서드로 분리)

✅ 길이를 엄격하게 제한하지 않고 더 중요한것은 클래스와 메서드가 “한가지 책임”을 가지도록 설계하는것이 주 목적이다.



11. 매직 리터럴 / 매직 넘버 사용을 자제하였는가

💬 매직리터럴이란 코드에 하드 코딩된 문자열이나 숫자를 의미한다. 쉽게 말해 상수로 선언되있지 않은 숫자를 매직 넘버, 문자열을 매직 리터럴이라고 한다

이런 값들은 코드 중간에 사용하면 코드의 의미를 파악하기 어렵게 하고 나중에 변경하기도 어려울 수 있다.

int result = calculate(5, 3.14);

위 코드에서 숫자 5와 3.14는 매직 넘버로 사용되고있다.
다른 사림이 위코드를 보면 어떤 뜻으로 사용되었는지 모를것이다.
이러한 값을 상수로 선언하여 코드를 개선할 수 있다.

public class Calculator {
    private static final int DEFAULT_VALUE = 5;
    private static final double PI = 3.14;

    public int calculate(int value, double multiplier) {
        return value * multiplier;
    }

    public static void main(String[] args) {
        Calculator calculator = new Calculator();
        int result = calculator.calculate(DEFAULT_VALUE, PI);
        System.out.println("Result: " + result);
    }
}

✅ 가독성이 향상되고 의미가 더 명확해진다.

태그:

카테고리:

업데이트:

댓글남기기