객체지향 생활체조원칙

김상진 ·2024년 11월 26일
0

CS

목록 보기
12/30

객체지향 생활 체조 원칙

객체지향 생활 체조 원칙은 객체지향 설계(OOP)에서 코드의 유지보수성과 가독성을 높이고, 강한 응집도와 낮은 결합도를 지닌 코드를 작성하기 위해 지켜야 할 실천적인 원칙들입니다. 주로 다음 9가지로 요약됩니다.


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

들여쓰기 단계가 많아지면 코드가 복잡해지고 가독성이 떨어진다.
1개 메서드 안에서 if/for/while 등을 2depth 이상 사용하지 않는다.
해당 부분만 지켜도 가독성이 향상되고 메서드가 자연스럽게 분리되는 효과가 있다.
실천 방법: 메서드가 여러 가지 일을 하고 있다면 작은 메서드로 나누어 단일 책임을 부여한다..

// 나쁜 예
public static int operate(int[] numArray, String op) {
    int result = 0;
    if(op.equuals("+")) {
        for(int i = 0; i < numArray.length; i++) {
            result += numArray[i];
        }
        return result;
    }
    if(op.equals("*")) {
        // ..생략
    }
}
// 좋은 예
public static int operate(int[] numArray, String op) {
    if(op.equals("+")) {
        return sum(numArray);
    }
    // 생략
}

private static int sum(int[] numArray) {
    int result = 0;
    for(int i = 0; i < numArray.length; i++) {
        result += numArray[i];
    }
    return result;
}

2. else 예약어를 사용하지 않는다.

if-else를 사용하는 대신 조기 반환(early return), 조건부 실행, 다형성 등을 활용.
이유: else는 불필요한 복잡성을 유발하며, 코드의 의도를 명확히 하기가 어려워짐.
ealry exit pattern을 적용해서 의도를 분명히 나타낼 수 있다.

// 나쁜 예
if (user.isLoggedIn()) {
    System.out.println("Welcome!");
} else {
    System.out.println("Please log in.");
}

// 좋은 예
if (!user.isLoggedIn()) {
    System.out.println("Please log in.");
    return;
}
System.out.println("Welcome!");

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

의도: 원시값과 문자열을 직접 사용하는 대신 객체로 감싸 비즈니스 로직을 포함시키고 불변성을 유지.
이점: 의미를 명확히 하고 잘못된 값 사용을 방지.

// 나쁜 예
private static int sum(int[] numArray) {
    int result = 0;
    for(int i = 0; i < numArray.length; i++) {
        int num = numArray[i];
        if(num < 0) {
            throw new RuntimeException();
        }
        result += num;
    }
    return result;
}

// 좋은 예
public class Positive {
    private int number;

    public Positive(int number) {
        if (number < 0) {
            throw new RuntimeException();
        }
        this.number = number;
    }

    public Poisitive add(Positive other) {
        return new Positive(this.number + other.number);
    }

    public int getNumber() {
        return number;
    }
}

private static Positive[] toPositives(int[] values) {
    Positive[] numbers = new Positive[values.length];
    for (int i = 0; i < values.length; i++) {
        numbers[i] = new Positive(values[i]);
    }
    return numbers;
}

private static int sum(Positive[] numbers) {
    Positive result = new Positive(0);
    for (Positive number : numbers) {
        result = result.add(number);
    }
    return result.getNumber();
}

이런 수정이 낯설게 느껴질 수 있다. 하지만 클래스 분리는 객체 지향적인 코드를 유도하고 SOLID의 SRP, OCP도 만족할 수 있게 돕는다.

Positive 객체는 도메인을 충분히 반영하고 스스로를 검증하는 자율적인 객체이다. 결과적으로 Calculator 같은 상위 클래스에 비대한 책임을 주는 것을 막고 추가적인 요구사항에 대응하기가 매우 편리해진다.


4. 한 줄에 점을 하나만 사용한다.

(스트림 등 체이닝하는 일부를 제외)
어느 코드 한 곳에서 점이 둘 이상 있다면, 해당 부분을 다시 리팩토링 해야 함

  • 어쩌면 다른 두 개의 객체를 동시 조작하고 있는 것일 수도 있음
    디미터(Demeter)의 법칙 : "친구하고만 대화하라"
    자신 소유의 객체, 자신이 생성한 객체, 그리고 누군가 준(파라미터로) 객체에만 메시지를 보낼 것
  • 그렇지 않을 경우, 다른 객체에 너무 깊숙이 관여하게 됨 : 캡슐화를 어기는 것
    메시지를 받는 객체는 자신의 속을 오픈하기보다는, 작업을 해주도록 해야 함
    이유: 메서드 체이닝이나 객체의 내부 구현에 대한 의존을 줄임.
// 나쁜 예
public class JamieObject {

    void getMoney() {
        jamieWallet.getTotalMoney().getMoney();
    }
}

class JamieWallet {
    private final JamieMoney totalMoney;
    
    JamieMoney getTotalMoney() {
        return totalMoney;
    }
}

class JamieMoney {
    
    private final int money;

    int getMoney() {
        return getMoney();
    }
}


// 좋은 예
public class JamieObject {

    void getMoney() {
        jamieWallet.getTotalMoney();
    }
}

class JamieWallet {

    private final JamieMoney totalMoney;

    int getTotalMoney() {
        return totalMoney.getMoney();
    }
}

class JamieMoney {

    private final int money;

    int getMoney() {
        return getMoney();
    }
}


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

과도한 축약은 코드 가독성을 저해한다. 무조건 짧다고 좋은 것은 아니다.

메서드의 이름이 긴 이유 중 하나는, 책임을 너무 많이 갖고 있거나, 적절한 클래스의 아래에 위치하지 않아서 일 수 있음

한 두 단어정도로 되어있는 경우엔, 축약을 하지 말 것

  • englishName이 길다고 굳이 EName으로 변경하지 말 것

    또한 문맥상 중복되는 단어는 자제할 것

  • Jamie의 printJamieName의 경우 문맥상 중복이므로 printName으로!


// 나쁜 예
public class Jamie {

    void printJamieName() {
        String EName = "Jamie";
        String KName = "제이미";
    }
}

// 좋은 예
public class Jamie {

    void printName() {
        String englishName = "Jamie";
        String koreanName = "제이미";
    }
}

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

클래스와 메서드의 크기를 작게 만들어 단일 책임을 부여.

클래스

  • 50줄 이상인 경우 보통 클래스가 한 가지 일만 하지 않는다. (한 가지 일만 한다면 놔둬도 되는 듯...?)

  • 50줄 정도면 스크롤을 내리지 않아도 된다. - 한 눈에 들어오는 효과!

패키지

  • 하나의 목적을 달생하기 위한 연관된 클래스들의 모임

  • 작게 유지하면 패키지가 진정한 정체성을 가지게 된다.
    권장 크기: 50줄 이상 되는 클래스 또는 10개 파일 이상의 패키지는 없어야 한다.


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

인스턴스 변수가 많아지면 클래스의 책임이 커짐.
해결 방법: 관련된 필드를 별도의 클래스로 분리하거나 일급 컬렉션 사용.

//나쁜 예
public class Car {
    private String brand;
    private String model;
    private int year;
    private String color;
    private int currentSpeed;
    private int maxSpeed;
    private boolean engineStarted;
    private boolean lightsOn;
    private boolean wipersOn;
    private boolean parkingBrakeEngaged;
    // ...
}
//좋은 예
public class Car {
    private String brand;
    private String model;

    public Car(String brand, String model) {
        this.brand = brand;
        this.model = model;
    }

    public String getBrand() {
        return brand;
    }

    public String getModel() {
        return model;
    }
}

public class Engine {};
public class Light {};
public class Wiper {};
public class Brake {};
// ..

8. 일급 컬렉션을 쓴다.

메서드에 인자가 많으면 이해하기 어렵고 재사용성이 낮아짐.
일급 컬렉션이란 Collection을 Wrapping하면서 Collection 외 다른 멤버 변수가 없는 상태를 말한다.
EffectiveJava나 CleanCode에서도 자주 언급되는 내용이다.

public class Store {
    private Set<Brand> brands;

    public Store(List<Brand> brands) {
        validSize(brands);
        this.brands = brands;
    }

    private void validSize(List<Brand> brands) {
        if(brands.size() >= 10) {
            throw new IllegalArgumentException("브랜드는 10개 초과로 입점할 수 없습니다.");
        }
    }
}

일급 컬렉션은 필요한 도메인 로직을 담을 수 있다. 이로써 컬렉션을 사용하는 클래스에서 검증하는 것이 아니라, 일급 컬렉션에서 자율적으로 검증할 수 있다.

일급 컬렉션을 사용하면 컬렉션의 불필요한 메서드에 대한 가시성 문제도 해결할 수 있다.

만약 Map를 사용했다면 remove, removeAll과 같이 도메인에 필요하지 않은 메서드까지 오용할 수 있게 된다.

서비스에서 remove, removeAll 등을 언제든지 호출할 수 있게 된다.
일급 컬렉션을 사용하면 이러한 넓은 인터페이스/클래스 문제를 막을 수 있다.
(클린 코드 8장 - 경계)


9. Getter와 Setter를 사용하지 않는다.

이부분은 Object, DDD 등에서 모두 강하게 강조하는 부분이다. (Tell, don't ask) 원칙에 따르면 묻지 말고 객체에게 행위를 시켜라고 한다.

ShippingInfo shippingInfo = order.getShippingInfo();

ShippingStatus status = shippingInfo.getStatus();
if(state != OrderState.PATMENT_WATTING && state != OderState.WATTING) {
    throw new IllegalArguementException();
}
shippingInfo.setAddress(newAddress);

여기서 ShippingInfo는 수동적인 데이터일 뿐이다. 도메인이 의도를 표현하지 못하고 있고, 로직은 다양한 곳에서 중복으로 작성될 것이다.

public Order {
    private ShippingInfo shippingInfo;

    public void changeshippingInfo(ShippingInfo newShippingInfo) {
        verifyNotYetShipped();
        setShippingInfo(newShippingInfo);
    }

    private void setShippingInfo(ShippingInfo newShippingInfo) {
        this.shippingInfo = newShippingInfo;
    }
}

getter와 setter를 닫으면 메서드의 의도를 노출하기가 쉬워진다.
Order 클래스는 ShippingInfo 클래스에게 필요한 메시지를 보내기만 하면 된다.

객체의 내부 상태를 외부에서 조작하지 않도록 방지.
대안: 객체에 메시지를 보내 상태를 변경하거나 값을 반환하도록 설계.

이 원칙을 지키면 얻는 효과

코드의 응집도가 높아지고, 객체 간 결합도가 낮아져 유지보수가 쉬워짐.
객체의 자율성과 캡슐화를 강화.
테스트 가능성이 높아져 코드 품질이 개선.
객체지향 생활 체조 원칙은 모두 지키기 어렵지만, 이 원칙을 염두에 두고 코드를 작성하면 객체지향의 본질을 살린 설계로 나아갈 수 있습니다.


출처 및 참고자료

[Java] 객체지향 생활 체조 원칙 9가지 (from 소트웍스 앤솔러지)

객체지향 프로그래밍 (객체지향 생활체조원칙)

객체지향 생활 체조 원칙 9가지 정리!

profile
알고리즘은 백준 허브를 통해 github에 꾸준히 올리고 있습니다.🙂

0개의 댓글

관련 채용 정보