CS

[CS] 객체지향 생활체조 9가지 원칙

SeungbeomKim 2023. 4. 15. 19:19

오늘은 객체지향 생활체조 원칙에 대해서 알아보려고 합니다.

객체지향 생활체조는 객체지향 프로그래밍을 개발하면서 생산성을 높이기 위한 9가지 원칙입니다. 이러한 원칙들은 소프트웨어 개발자들이 코드를 작성할 때, 유지보수 가능하고 재사용 가능하며 확장 가능한 코드를 작성하도록 도와줍니다. 

저는 "객체지향 생활체조 9가지 원칙"이 코드 구현을 하는 데 있어서도 굉장히 중요한 요소가 될 것이라고 생각했고, 개발자가 되어 사람들과 협업을 할 때에도 중요할 것 같아서 생활체조 9가지 원칙에 대해 알아보려고 합니다. 

소트웍스 앤솔러지 : 객체지향 생활체조 파트

1. 한 메서드에 오직 한 단계의 들여쓰기만 하라(One level of indentation per method)

<잘못된 코드 예시>

public void processOrder(Order order) {
  if (order.isValid()) {
    if (order.isInStock()) {
      if (order.canShip()) {
        order.ship();
      }
    }
  }
}

<올바른 코드 예시>

public void processOrder(Order order) {
  if (!order.isValid()) {
    return;
  }
  
  if (!order.isInStock()) {
    return;
  }
  
  if (!order.canShip()) {
    return;
  }
  
  order.ship();
}

if문을 중첩해서 썼을 때 보다 훨씬 가독성이 좋아 보이고, 들여 쓰기를 최소화하고, 가독성을 높이고, 조건문의 복잡도를 줄일 수 있습니다.

 

2. else 예약어를 쓰지 마라(Don't use the else keyword)

<잘못된 예시 코드>

public void doSomething(int value) {
    if (value == 0) {
        doSomethingA();
    } else {
        doSomethingB();
    }
}

<올바른 예시 코드>

public void doSomething(int value) {
    if (value == 0) {
        doSomethingA();
        return;
    }
    
    doSomethingB();
}

 

3. 모든 원시값과 문자열을 포장하라(Wrap all primitives and string)

public class Human {

    private string name;
    private int age; 
    
    public User(string name, int age) {
    	validateName(name);
        validateAge(age);
        this.name = name;
        this.age = age;
     }
     
    private void validateName(String name) {
        if (name.length() < 2) {
            throw new IllegalArgumentException("이름은 두 글자 이상이어야 합니다.");
        }
    }

    private void validateAge(int age) {
        if (age < 0) {
            throw new IllegalArgumentException("나이는 0살부터 시작합니다.");
        }
    }
}

Human의 멤버 변수가 name, age 2개 뿐인데, 이 멤버 변수들에 대한 유효성 검사를 Human 객체에서 하고 있습니다. 그래서 원시값 포장(원시 타입의 값을 객체로 포장)을 통해  자신의 상태를 객체 스스로 관리할 수 있도록 수정해 보겠습니다.(name, age 객체를 Human 객체가 아니라 name, age 객체에서 이를 판단할 수 있도록 로직 수정)

<올바른 코드 예시>

public class Human {

    private Name name;
    private Age age;

    public User(String name, String age) {
        this.name = new Name(name);
        this.age = new Age(age);
    }
}

public class Name {

    private String name;

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

public class Age {

    private int age;

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

다음과 같이 이름, 나이에 대한 유효성 검사를 Name, Age 객체가 직접 담당하도록 로직을 수정하였습니다. 객체에 해당하는 값을 Human객체가 처리하지않고, Name, Age 객체가 직접적으로 담당함으로써 객체 스스로가 관리할 수 있게 됐습니다. 책임이 분리되었습니다. 

원시값 포장을 통해 코드의 유지 보수를 최적화시킬 수 있고, 책임 관계가 명확해질 뿐만 아니라 해당 변수가 의미하는 바를 정확히 알 수 있게 됩니다. 

 

4. 한 줄에 점을 하나만 찍어라(Use only one dot per line)

<잘못된 코드 예시>

public String getAddress() {
    return this.customer.getAddress().getStreet();
}

<올바른 코드 예시>

public String getAddress() {
    Address address = customer.getAddress();
    return address.getStreet();
}

5. 줄여쓰지 마라(Don't abbreviate)

<잘못된 코드 예시>

public void calc(int a, int b) {
    int res = a + b;
    System.out.println("Result: " + res);
}

<올바른 코드 예시>

public void calculate(int number1, int number2) {
    int result = number1 + number2;
    System.out.println("Result: " + result);
}

6. 모든 엔티티를 작게 유지하라(Keep all entities small)

클래스는 50줄, 패키지는 파일 10개를 넘기지 않아야 합니다.

긴 파일은 가독성이 떨어지고, 재사용하기가 어렵기 때문에, 엔티티들을 잘게 쪼개 세분화시키는 게 중요합니다. 

 

7. 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다(No classed with more than two instance variables)

 

한 클래스에 인스턴스 변수를 많이 두면 클래스가 많은 책임을 가지게 되어 SRP(Single Responsibility Principle)를 위배하게 됩니다. 또한 인스턴스 변수가 많을수록 클래스의 복잡성이 증가하고, 유지보수가 어려워집니다. 따라서 인스턴스 변수는 최소한으로 유지하는 것이 좋습니다. 이 원칙은 3번 원시값 포장과도 일맥상 통합니다. 의미를 갖는 상태를 따로 관리해줘야 한다는 의미입니다.

<잘못된 예시 코드>

public class Employee {

    private String firstName;
    private String lastName;
    private String department;
    private double salary;
    
    public Employee(String firstName, String lastName, String department, double salary) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.department = department;
        this.salary = salary;
    }
    // getter, setter..
}

<올바른 예시 코드>

public class Employee {

    private String name;
    private Department department;
    private Salary salary;
    
    public Employee(String name, Department department, Salary salary) {
        this.name = name;
        this.department = department;
        this.salary = salary;
    }
    
    // 각 변수에 대한 getter/setter 메서드 대신, 해당 객체를 직접 반환하는 메서드를 제공
    public String getName() {
        return name;
    }
    
    public Department getDepartment() {
        return department;
    }
    
    public Salary getSalary() {
        return salary;
    }
}

각 변수에 대한 getter/setter 메서드 대신, 해당 객체를 직접 반환하는 메서드를 작성해서 구현하였습니다. 이렇게 하면 캡슐화 원칙이 지켜지며, 객체의 데이터 무결성과 유지 보수성이 향상됩니다.

 

8. 일급 컬렉션을 쓴다.

일급 컬렉션이란 ?

프로그래밍 언어에서 컬렉션 자료형을 일급 객체로 취급하는 것을 의미합니다. 이는 컬렉션 자체가 변수에 할당되거나 함수의 인자로 전달될 수 있으며, 함수의 반환 값으로 사용될 수 있음을 의미합니다.

 

객체지향 설계에서 굉장히 중요한 역할을 합니다. 객체지향 프로그래밍에서는 데이터와 데이터를 다루는 메서드를 함께 묶어서 객체로 만듭니다. 이때 객체가 단순히 데이터의 집합체가 되도록 하지 않게 하기 위해 일급 컬렉션을 사용합니다.

 

일급 컬렉션을 사용하면 컬렉션을 포함하는 객체가 단순히 데이터의 집합체가 되지 않고, 컬렉션을 다루는 메서드와 데이터를 함께 묶을 수 있습니다. 또한 컬렉션을 다루는 메서드를 별도의 클래스로 추출하여 SRP를 준수할 수 있게 됩니다.

 

또한, 일급 컬렉션을 사용하면 컬렉션을 다루는 로직을 컬렉션 자체에 캡슐화할 수 있게 됩니다. 이러한 과정을 통해 객체의 역할과 책임을 명확하게 분리하고 SRP를 준수하여 유지보수성을 높일 수 있게 됩니다.

<잘못된 코드 예시>

public class Order {

   private List<Item> items;
   
   public Order(List<Item> items) {
       this.items = items;
   }
   
   public List<Item> getItems() {
       return items;
   }
   
   // other methods omitted
}

 

위의 Order 클래스에서 items이라는 일반적인 리스트를 사용하고 있습니다. items 리스트의 동작을 처리하는 메서드가 Order 클래스 내부에 없기에 일관성이 떨어집니다.

<올바른 코드 예시>

public class Order {

   private OrderItems items;
   
   public Order(OrderItems items) {
       this.items = items;
   }
   
   public OrderItems getItems() {
       return items;
   }
   
   public int getTotalPrice() {
       return items.getTotalPrice();
   }
   
   // other methods omitted
}

public class OrderItems {

   private List<Item> items;
   
   public OrderItems(List<Item> items) {
       this.items = items;
   }
   
   public int getTotalPrice() {
       return items.stream()
                   .mapToInt(Item::getPrice)
                   .sum();
   }
   
   // other methods omitted
}

Order 클래스에 items라는 일급 컬렉션을 적용하였습니다. items 리스트에 대한 동작을 일관성 있게 처리할 수 있고, Order 클래스에서 items 리스트의 동작을 처리하는 메서드를 적용할 수 있게 됩니다. 

 

9. getter/setter 지양(No Getters/Setters/Properties)

 

getter/setter는 객체의 상태를 외부에서 직접 접근하거나 변경할 수 있기에, 캡슐화를 약화시킬 수 있게 됩니다. 그래서 객체의 내부 구현에 대한 세부사항이 외부에 노출될 수 있으며, 객체의 유지보수성과 확장성을 저해할 수 있습니다. 또한 이를 사용하지 않고, 객체의 상태를 변경하도록 하는 방식은 객체의 상태를 제어할 수 있는 장점이 있습니다. 예를 들어서, 특정 상태에서만 특정 메서드를 실행할 수 있도록 제한하는 등의 로직을 구현할 수 있습니다. 또한 객체의 인터페이스를 명확하게 한다는 장점이 있습니다. 

<잘못된 코드 예시>

public class Person {

  private String name;
  
  public void setName(String name) {
    this.name = name;
  }
  
  public String getName() {
    return name;
  }
}

<올바른 코드 예시>

public class Person {

  private String name;
  
  public Person(String name) {
    this.name = name;
  }
  
  public String getName() {
    return name;
  }
  
  public void changeName(String newName) {
    name = newName;
  }
}

인터페이스란 객체의 사용 방법을 정의하는 것입니다. 인터페이스를 사용하면 코드를 추상화하여 객체 간의 결합도를 낮추고, 유지보수성과 확장성을 향상시킬 수 있습니다. 코드의 가독성 또한 높아지게 됩니다. 위의 코드에서 setName보다 changeName 메서드를 사용하여 가독성 측면에서도 더욱 뛰어나기에 객체의 인터페이스를 명확하게 정의할 수 있습니다. 

<참고 자료>

https://tecoble.techcourse.co.kr/post/2020-05-29-wrap-primitive-type/

 

원시 타입을 포장해야 하는 이유

변수를 선언하는 방법에는 두 가지가 있다. 원시 타입의 변수를 선언하는 방법과, 원시 타입의 변수를 객체로 포장한 변수를 선언하는 방법이 있다. (Collection…

tecoble.techcourse.co.kr