객체지향 생활체조

2023. 3. 3. 17:00ETC/OOP

SOLID 원칙을 적용하기 위한 객체지향 생활 체조 9 원칙은 코드를

readable(읽기 좋은), maintainable(유지 관리), reuseable(재사용 가능), scalable(확장 가능) 하게 디자인 할 수 있다.

 

더보기

객체지향 생활 체조 9가지 원칙

규칙 1: 한 메소드에 오직 한 단계의 들여쓰기(indent)만 한다.
규칙 2: else 예약어를 쓰지 않는다.
규칙 3: 모든 원시값과 문자열을 포장한다.
규칙 4: 한 줄에 점을 하나만 찍는다.
규칙 5: 줄여쓰지 않는다(축약 금지).
규칙 6: 모든 엔티티를 작게 유지한다.
규칙 7: 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
규칙 8: 일급 컬렉션을 쓴다.
규칙 9: 게터/세터/프로퍼티를 쓰지 않는다.

1. 한 메소드에 오직 한 단계의 들여 쓰기(indent)만 한다.

Use only one level of indentation per method

각 메소드는 간단명료하게 이해할 수 있어야 한다. 중첩 단계가 없는 코드들은 더 쉽게 읽을 수 있고 유지 보수하기도 용이하다.

 

예를 들어서 물건의 전체 가격을 계산해 주는 코드가 있다고 해보자.

아래 코드는 for 문 안에 if 문이 있다.

저 코드에서 for 문의 역할은 물건들의 전체 가격을 더 하는 것이고, if 문의 역할은 물건의 가격을 구하는 코드이다.

public double calculateTotalPrice(List<Item> items) {
    double totalPrice = 0.0;
    for (Item item : items) {
        double price = item.getQuantity() * item.getProduct().getPrice();
        if (item.getProduct().isDiscounted()) {
            price *= 0.9;
        }
        totalPrice += price;
    }
    return totalPrice;
}

위의 코드를 1원칙에 따라 분리해보자.

public double calculateTotalPrice(List<Item> items) {
    double totalPrice = 0.0;
    for (Item item : items) {
        totalPrice += calculatePrice(item);
    }
    return totalPrice;
}

private double calculatePrice(Item item) {
    double price = item.getQuantity() * item.getProduct().getPrice();
    if (item.getProduct().isDiscounted()) {
        price *= 0.9;
    }
    return price;
}

코드가 훨씬 직관적으로 읽어지며 기능분리를 함으로써 나중에 수정사항이 생길 때 유지보수하기도 좋을 것 같다.

항상 1원칙을 생각하며 개발하다 보면 수정사항이 생기더라도 기능별로 메서드를 구분해 놓았기 때문에 수정하기도 정말 편했다.

 

2. else 예약어를 쓰지 않는다.

Don't use the else keyword

else 예약어를 쓰지 않는 것은 다형성을 사용하기 위해 로직을 분리시키고 코드를 더 확장 가능하고, 유지 보수 할 수 있도록 해준다.

 

예를 들어서 제품의 종류에 따라 가격을 다르게 측정하는 메서드가 있다고 생각하자. 일반적으로 else를 이용한다면 코드는 이와 같을 것이다.

 

public double calculatePrice(Product product) {
    double price = 0.0;
    if (product.getType().equals("book")) {
        price = product.getBasePrice() * 0.9;
    } else if (product.getType().equals("video")) {
        price = product.getBasePrice() * 0.8;
    } else if (product.getType().equals("music")) {
        price = product.getBasePrice() * 0.7;
    }
    return price;
}

if-else 구조를 쓰는 이러한 접근은 지금까지는 괜찮다. 하지만 종류가 수만 가지가 되고, 자주 가격을 바꿔 줘야 할 상황이 온다면? 이런 구조로는 개발자에게 아주 부담이 될 것이다.

 

이러한 접근을 개선하여 다형성을 이용해 if-else 구조를 없애보자.

public abstract class Product {
    protected double basePrice;
    public Product(double basePrice) {
        this.basePrice = basePrice;
    }
    public abstract double calculatePrice();
}

public class Book extends Product {
    public Book(double basePrice) {
        super(basePrice);
    }
    public double calculatePrice() {
        return basePrice * 0.9;
    }
}

public class Video extends Product {
    public Video(double basePrice) {
        super(basePrice);
    }
    public double calculatePrice() {
        return basePrice * 0.8;
    }
}

public class Music extends Product {
    public Music(double basePrice) {
        super(basePrice);
    }
    public double calculatePrice() {
        return basePrice * 0.7;
    }
}

base가 되는 추상클래스 Product를 만들고 안에 calculatePrice라는 추상 메서드를 미리 명시를 해준다.

이후 이 추상클래스를 상속받은 서브클래스들은 calculatePrice라는 메소드를 만들면 된다.

 

Product book = new Book(10.0);
double price = book.calculatePrice();

적당한 instance를 만들어서 calculatePrice 라는 함수만 호출하면 간단하게 확인할 수 있다.

이러한 접근은 기존 코드를 수정하지 않고 새로운 종류를 추가할 수 있다. 또한 코드를 더 읽기 좋고 유지보수 하기도 좋게 해 준다. 

 

다형성 측면 말고도 기능분리를 위해 if 조건절에서 return 하는 방식으로 구현하는 방법도 가능할 것 같다.

 

3. 모든 원시값과 문자열을 포장하자.

Wrap all primitives and strings

자기가 만든 클래스의 원시값(int, long, boolean, 등)과 문자열(String)들을 캡슐화하라는 것이다.

public class Rectangle {
    public int width;
    public int height;

    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }

    public int getArea() {
        return width * height;
    }
}

사각형이라는 클래스를 만들면 높이, 길이라는 변수를 만들고 getArea 함수를 만들어 넓이를 구할 수 있다. 이러한 코드는 단순해보이긴 하지만 클래스의 내부 상태가 노출된다는 점이 문제가 된다. 문제가 되는 이유는 외부에서 직접 내부 상태를 접근 하거나 수정 할 수 있기 때문이다. 이런식으로 개발이 된다면 만약 높이, 길이 라는 변수가 public이고 사각형 클래스를 이용하는 코드들이 있다고 했을 때, 내부 사각형 클래스의 코드가 변경되면 그 클래스를 이용하는 다른 클래스 들도 변경을 해주어야 한다. 이는 코드의 유지보수를 어렵게 한다. 또한 객체지향적 관점에서 캡슐화(encapsulation)가 지켜지지 않는다.

public class Rectangle {
    private final int width;
    private final int height;

    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }

    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return height;
    }

    public int getArea() {
        return new Area(width, height).calculateArea();
    }

    private class Area {
        private final int width;
        private final int height;

        public Area(int width, int height) {
            this.width = width;
            this.height = height;
        }

        public int calculateArea() {
            return width * height;
        }
    }
}

이를 개선하기 위해 클래스 멤버변수를 상수화 해주는 것이 좋다. private으로 클래스 내부에서만 접근 가능하게 하며 final로 하나의 객체에서는 수정할 수 없도록 만들어준다. getArea 함수도 return 값이 int로 반환되므로 이도 Area라는 클래스를 만들어서 사각형 클래스에서는 Area 객체를 호출하는 식으로 변경해 주었다. 이런 식으로 만들어주면 외부에서 사각형 클래스에 접근이 불가능하기 때문에 사각형 클래스 내부의 코드가 바뀌더라도 이 클래스를 이용하는 다른 클래스들은 바뀌지 않아도 된다.

 

4. 코드 한 줄에 점 하나만을 사용하자.

Use only one dot per line

코드 한 줄에 점 하나를 사용하라. 이는 단순히 코드 가독성을 높일 수 있다는 장점이 있다. 또한 코드 한 줄에 점이 여러 개 있다면 이는 한 줄에서 여러 가지 기능을 수행한다는 말이다. 객체는 다른 하나의 객체와 메세지를 주고받는 것이 좋다. 점이 여러개라면 하나의 객체가 여러개의 객체와 메세지를 주고 받는 느낌으로 생각할 수 있다. 캡슐화의 관점에서도 맞지 않다.

    public String[] inputCarNames() {
        String[] carNames = sc.nextLine().split(",");
        return carNames;
    public String[] inputCarNames() {
        String userInput = sc.nextLine();
        String[] carNames = userInput.split(",");
        return carNames;

간단한 예시이다. 사용자에게 input을 받은 후, 단위로 split 하는 기능인데 1번 코드는 한 줄에. 이 두 개 들어가 있다. 이를 2번 코드처럼 나눠서 작성하라는 것이다. 저런 식으로 나누거나 메서드를 새로 분리하는 방법도 좋다.

 

5. 줄여 쓰지 않는다. (축약 금지)

Don’t abbreviate

의도가 분명하게 이름을 지으라!

 

클래스, 메서드, 변수명 등을 줄여 쓰다 보면 명확한 의미전달이 불가할 수 있다. 나 혼자 코드를 짜는 것이 아닌 협업을 하기 때문에 다른 사람들도 읽기 좋고 알아보기 좋은 코드를 쓰는 것이 좋다. 

 

public class Cust {
    private String custNm;
    private String custAddr;
    private String custPh;

    public String getCustNm() {
        return custNm;
    }

    public String getCustAddr() {
        return custAddr;
    }

    public String getCustPh() {
        return custPh;
    }

}

위의 코드는 무엇을 말하는지 생각을 해야 코드가 대충 이해가 된다. 이러한 코드를 개선해 보자.

public class Customer {
    private String name;
    private String address;
    private String phoneNumber;

    public String getName() {
        return name;
    }

    public String getAddress() {
        return address;
    }

    public String getPhoneNumber() {
        return phoneNumber;
    }

}

이런 식으로 명확하게 쓰는 것이 훨씬 알아보기가 쉽다.

 

또한 메서드명이 너무 길어진다면 이 메서드가 한가지 기능만을 수행하는지에 대해서 생각해 볼 필요가 있다. 네이밍이 너무 길다면 그 메소드가 여러 가지 기능을 수행하려고 하는 것일 수 도 있다.

 

6. 모든 엔티티를 작게 유지한다.

Keep all entities small

 

클래스, 메서드 등의 크기가 큰 것은 좋지 않다. 작게 유지하게 되면 더욱 읽기 좋고 이해하기 쉽게 만든다. 또한 엔티티 단위로 테스트나 디버깅 및 수정도 훨씬 쉽다.

 

이에 이 규칙은 50줄 이상 되는 클래스, 10개 파일 이상 되는 패키지 가 없어야 한다는 뜻을 가지고 있다. 패키지도 객체처럼 단일한 목표를 가져야 목표나 정체성이 훨씬 뚜렷해지기 때문이다.

 

하나의 클래스 혹은 패키지에서 너무 많은 기능을 수행하지 말고 분리하여 관리해 유지보수를 할 때도 용이하게 하기 위함이다.

 

7. 3개 이상의 인스턴스 변수를 가진 클래스를 사용하지 않는다.

No Classes With More Than Two Instance Variables

 

하나의 클래스는 작은 단위여야 함을 의미한다. 한 클래스가 인스턴스 변수가 너무 많다면 그 클래스의 목적이 무엇인지 이해하기 어렵다. 추후 유지보수도 쉽지가 않다. 인스턴스 변수가 더 많을 경우도 있겠지만 대부분 클래스들에서는 변수를 최대한 적게 사용하는 것이 좋다.

 

8. 일급컬렉션을 쓴다.

First class collections

 

3번 모든 원시값과 문자열을 포장하자와 같은 맥락인데 이를 collection에 적용하는 것이다. 또한 collection 외의 다른 멤버 변수가 없는 상태여야 한다. 

 

일급컬렉션을 사용하면 3번에서 말한 장점과 동일한 장점을 가질 수 있다.

일급컬렉션에 대한 글은 추후 다시 정리하여 올릴 것이다.

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class RectangleList {
    private List<Rectangle> rectangles = new ArrayList<>();

    public void addRectangle(Rectangle rectangle) {
        rectangles.add(rectangle);
    }

    public void removeRectangle(Rectangle rectangle) {
        rectangles.remove(rectangle);
    }

    public int getNumberOfRectangles() {
        return rectangles.size();
    }

    public List<Rectangle> getRectangles() {
        return Collections.unmodifiableList(rectangles);
    }
}

 

9. 게터/세터/프로퍼티를 쓰지 않는다.

No getters/setters/properties
public class Rectangle {
    private int width;
    private int height;

    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }

    public int calculateArea() {
        return width * height;
    }
}

게터/세터/프로퍼티를 사용하지 않으면 해당 객체가 어떤 행동에 대한 책임이 분명해지고 내부의 상태는 외부에 공개되지 않는다. 이는 캡슐화를 생각했을 때처럼 유지보수에 더 용이해진다.

 

객체지향에서 객체들 간에는 클래스 내부에서 어떤 일이 벌어지는지는 중요하지 않다. 객체들은 서로 메시지를 주고 받을 뿐이다. 다른 클래스에서 사각형 클래스에 width, height의 상태를 알 필요조차 없다. 우리는 사각형 클래스가 줄 수 있는 메세지 (calculateArea)만 있으면 되는 것이다.

 

 

정리하며

객체지향프로그래밍의 4가지 특징은 상속, 추상화, 다형성, 캡슐화이다. 객체지향체조설계 9원칙은 이러한 객체지향의 특징을 살리기 위해 개발자로 하여금 어떤 방법으로 코드를 설계하고 디자인해 나갈지에 대한 가이드라인을 제공해 준다. 이러한 가이드라인은 맨 앞에서 말했듯이 readable(읽기 좋은), maintainable(유지 관리), reuseable(재사용 가능), scalable(확장 가능) 하게 디자인할 수 있게 도와준다. 결국결국 개발은 협업의 가치를 중요하게 여기고 어떤 서비스를 유지하고 더 개선하기 위해 유지보수를 가장 중요하게 여기는 것 같다.