Java

자바 객체지향 생활체조 9원칙

seoeunpapa 2025. 1. 10. 00:01
728x90

 

 

객체지향 생활체조 9원칙은 객체지향 프로그래밍(OOP)을 더 효과적으로 활용하기 위한 실천적 가이드라인입니다. 자바(Java)에서도 이 원칙들은 깔끔하고 유지보수 가능한 코드를 작성하는 데 도움이 됩니다.

 

 

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

  • 목적: 메서드가 단일 책임을 가지도록 하고, 복잡도를 줄여 가독성을 높이기 위함입니다.
  • 왜 중요한가: 메서드가 여러 단계의 중첩된 조건문과 반복문을 포함하면, 코드의 흐름을 추적하기 어려워집니다. 이를 피하려면 작은 메서드로 나누는 것이 중요합니다.

적용할 때 주의할 점

  • 너무 세분화된 메서드로 나누면 오히려 코드가 과도하게 분리되어 가독성이 떨어질 수 있습니다. 적절한 균형이 필요합니다.
  • 메서드의 이름을 직관적으로 지어야 코드의 의도가 명확해집니다.

추가 예시

위반:

public void validateAndProcess(int[] numbers) {
    for (int number : numbers) {
        if (number > 0) {
            if (number % 2 == 0) {
                System.out.println("Even: " + number);
            } else {
                System.out.println("Odd: " + number);
            }
        }
    }
}

개선:

public void validateAndProcess(int[] numbers) {
    for (int number : numbers) {
        processNumber(number);
    }
}

private void processNumber(int number) {
    if (isPositive(number)) {
        printParity(number);
    }
}

private boolean isPositive(int number) {
    return number > 0;
}

private void printParity(int number) {
    if (number % 2 == 0) {
        System.out.println("Even: " + number);
    } else {
        System.out.println("Odd: " + number);
    }
}

2. else 예약어 금지

  • 목적: 조건문을 단순화하고, 긍정적인 흐름(positive flow)을 유지합니다.
  • 왜 중요한가: else는 코드 가독성을 떨어뜨릴 수 있으며, 필요 없는 분기점을 생성할 수 있습니다.

적용할 때 주의할 점

  • else를 제거한다고 해서 항상 코드가 간결해지는 것은 아닙니다. 상황에 따라 else가 더 명확할 수도 있으므로 신중하게 판단해야 합니다.

추가 예시

위반:

public int calculateDiscount(int age) {
    if (age < 18) {
        return 10;
    } else {
        return 0;
    }
}

개선:

public int calculateDiscount(int age) {
    if (age < 18) {
        return 10;
    }
    return 0;
}

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

  • 목적: 의미 있는 타입을 정의하여 코드의 의도를 명확히 하고, 데이터에 대한 검증 로직을 캡슐화합니다.
  • 왜 중요한가: 원시값은 그 자체로 의미를 드러내지 않습니다. 포장하면 타입 시스템을 통해 오류를 예방하고, 관련 로직을 재사용할 수 있습니다.

적용할 때 주의할 점

  • 과도하게 작은 클래스를 만드는 것을 피해야 합니다. 실제로 의미가 있는 경우에만 적용하세요.
  • 불변 객체로 설계하는 것이 좋습니다.

추가 예시

위반:

public void createUser(String name, int age) {
    if (age < 0) {
        throw new IllegalArgumentException("Age cannot be negative");
    }
    System.out.println("User created: " + name + ", " + age);
}

개선:

public void createUser(Name name, Age age) {
    System.out.println("User created: " + name.getValue() + ", " + age.getValue());
}

class Name {
    private final String value;

    public Name(String value) {
        if (value == null || value.isEmpty()) {
            throw new IllegalArgumentException("Name cannot be empty");
        }
        this.value = value;
    }

    public String getValue() {
        return value;
    }
}

class Age {
    private final int value;

    public Age(int value) {
        if (value < 0) {
            throw new IllegalArgumentException("Age cannot be negative");
        }
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}

4. 한 줄에 점 하나만 사용

  • 목적: 메서드 체이닝을 방지하고, 코드의 의도를 명확히 합니다.
  • 왜 중요한가: 메서드 체이닝은 객체 간의 강한 결합(tight coupling)을 초래할 수 있고, 디버깅이 어려워질 수 있습니다.

적용할 때 주의할 점

  • 너무 많은 분리로 코드가 장황해질 수 있으므로 적절한 균형이 필요합니다.
  • 객체 설계에서 필요한 정보를 제공하는 메서드를 추가하는 것이 중요합니다.

추가 예시

위반:

String city = user.getAddress().getCity().toUpperCase();

개선:

Address address = user.getAddress();
City city = address.getCity();
String upperCaseCity = city.toUpperCase();

5. 컬렉션은 반드시 포장

  • 목적: 컬렉션의 상태를 캡슐화하고, 관련 로직을 포함시켜 응집도를 높입니다.
  • 왜 중요한가: 컬렉션이 코드 전반에 퍼져 있으면, 변경 사항이 여러 곳에서 발생할 수 있습니다. 포장하면 변경을 쉽게 관리할 수 있습니다.

적용할 때 주의할 점

  • 컬렉션을 포장한다고 해서 항상 유용한 것은 아닙니다. 로직이 복잡해질 때 포장을 고려하세요.
  • 불변 컬렉션을 선호하세요.

추가 예시

위반:

List<String> tags = new ArrayList<>();
tags.add("java");
tags.add("oop");

개선:

class Tags {
    private final List<String> tags;

    public Tags(List<String> tags) {
        this.tags = new ArrayList<>(tags);
    }

    public void add(String tag) {
        tags.add(tag);
    }

    public List<String> getTags() {
        return Collections.unmodifiableList(tags);
    }
}

Tags tags = new Tags(new ArrayList<>());
tags.add("java");
tags.add("oop");

6. 첫 번째 클래스 컬렉션

  • 5번과 동일한 맥락이므로 생략 가능.

7. Getter/Setter 사용 금지

  • 목적: 객체 캡슐화를 철저히 지키고, 객체의 동작 중심 설계를 촉진합니다.
  • 왜 중요한가: Getter/Setter는 객체를 단순한 데이터 구조로 만들 가능성이 있습니다. 동작 중심 설계를 통해 객체의 책임을 강화할 수 있습니다.

추가 예시

위반:

public class BankAccount {
    private double balance;

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }
}

개선:

public class BankAccount {
    private double balance;

    public BankAccount(double initialBalance) {
        this.balance = initialBalance;
    }

    public void deposit(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Amount must be positive");
        }
        balance += amount;
    }

    public void withdraw(double amount) {
        if (amount > balance) {
            throw new IllegalArgumentException("Insufficient funds");
        }
        balance -= amount;
    }
}

 


8. 일급 컬렉션 사용

  • 목적: 컬렉션(List, Set, Map 등)에 대한 책임을 단일 클래스로 위임하고, 컬렉션을 일종의 "객체"처럼 취급합니다.
  • 왜 중요한가:
    • 컬렉션의 상태와 관련된 로직이 흩어져 있으면, 코드가 혼란스러워지고 버그가 발생할 가능성이 높아집니다.
    • 컬렉션을 포장하면 관련 로직을 응집시킬 수 있습니다. 이는 단일 책임 원칙(SRP)을 따르는 코드 설계로 이어집니다.
  • 적용 사례:
    • 컬렉션을 다룰 때 특정 정렬, 필터링, 추가/삭제 등의 기능이 필요할 경우.

적용할 때 주의할 점

  • 컬렉션에 비즈니스 로직 포함: 단순히 데이터를 감싸는 게 아니라, 컬렉션과 관련된 동작을 클래스에 포함시켜야 합니다.
  • 불변성 유지: 컬렉션을 변경하려면 새로운 객체를 반환하거나 제한된 인터페이스를 제공해 예측 가능성을 높여야 합니다.

추가 예시

위반:

List<String> tags = new ArrayList<>();
tags.add("java");
tags.add("oop");

if (tags.contains("java")) {
    System.out.println("Java tag is present");
}

개선:

public class Tags {
    private final List<String> tags;

    public Tags(List<String> tags) {
        this.tags = new ArrayList<>(tags); // 복사본 생성
    }

    public boolean contains(String tag) {
        return tags.contains(tag);
    }

    public void add(String tag) {
        tags.add(tag);
    }

    public List<String> getTags() {
        return Collections.unmodifiableList(tags); // 불변 리스트 반환
    }
}

// 사용
Tags tags = new Tags(new ArrayList<>());
tags.add("java");
tags.add("oop");

if (tags.contains("java")) {
    System.out.println("Java tag is present");
}

이 원칙이 중요한 이유

  1. 컬렉션의 동작이 코드 전반에 분산되는 것을 막고, 관련 로직을 한곳에 모읍니다.
  2. 컬렉션을 다룰 때 의도를 명확히 전달할 수 있습니다.
  3. 코드 유지보수가 쉬워지고, 컬렉션 구조를 바꾸더라도 영향을 최소화합니다.

9. 무분별한 static 메서드 사용 금지

  • 목적: 객체지향 설계에서 객체의 상태와 행위를 분리하지 않도록 하는 데 있습니다.
  • 왜 중요한가:
    • static 메서드는 전역적으로 호출 가능하며, 이를 남용하면 객체지향적 설계가 아닌 절차지향적인 코드가 될 가능성이 높습니다.
    • 객체가 아닌 클래스 자체에 로직을 두면, 테스트나 확장이 어려워질 수 있습니다.
  • 예외적 상황:
    • 순수 함수(pure function)처럼 객체의 상태와 무관한 유틸리티 함수는 static으로 선언해도 괜찮습니다. (예: Math.sqrt())

적용할 때 주의할 점

  • 객체의 상태나 행위가 필요하면 인스턴스 메서드를 사용하세요.
  • static 메서드는 필요한 경우 최소화하여 사용하고, 공통된 동작이나 상수를 관리하는 용도로 제한하세요.
  • 의존성이 필요한 경우 의존성 주입(Dependency Injection)을 고려하세요.

추가 예시

위반:

public class MathUtils {
    public static int add(int a, int b) {
        return a + b;
    }

    public static int multiply(int a, int b) {
        return a * b;
    }
}

개선:

public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }

    public int multiply(int a, int b) {
        return a * b;
    }
}

// 사용
Calculator calculator = new Calculator();
System.out.println(calculator.add(3, 5));
System.out.println(calculator.multiply(3, 5));

이 원칙이 중요한 이유

  1. 객체지향 설계를 유지하며 코드의 확장성을 높입니다.
    • 예를 들어, Calculator 클래스를 상속하거나 다형성을 활용할 수 있습니다.
  2. 전역 상태 관리 문제를 방지합니다.
    • static 메서드가 많아지면, 전역 상태를 조작하는 코드가 늘어나 예기치 않은 동작이 발생할 수 있습니다.
  3. 단위 테스트가 쉬워집니다.
    • static 메서드에 의존하는 코드는 Mocking이 어려워 테스트하기 까다롭습니다.

적용하지 않아도 되는 경우

  • 불변 상태를 다루는 순수 함수:
    • 예: Math.max(), UUID.randomUUID()
  • 유틸리티 클래스:
    • 예: 파일 I/O, 문자열 처리, 날짜 변환 등.

 

이 원칙들을 따르면 객체지향 프로그래밍의 장점을 극대화할 수 있으며, 유지보수성과 확장성이 높은 코드를 작성할 수 있습니다.

728x90