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");
}
이 원칙이 중요한 이유
- 컬렉션의 동작이 코드 전반에 분산되는 것을 막고, 관련 로직을 한곳에 모읍니다.
- 컬렉션을 다룰 때 의도를 명확히 전달할 수 있습니다.
- 코드 유지보수가 쉬워지고, 컬렉션 구조를 바꾸더라도 영향을 최소화합니다.
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));
이 원칙이 중요한 이유
- 객체지향 설계를 유지하며 코드의 확장성을 높입니다.
- 예를 들어, Calculator 클래스를 상속하거나 다형성을 활용할 수 있습니다.
- 전역 상태 관리 문제를 방지합니다.
- static 메서드가 많아지면, 전역 상태를 조작하는 코드가 늘어나 예기치 않은 동작이 발생할 수 있습니다.
- 단위 테스트가 쉬워집니다.
- static 메서드에 의존하는 코드는 Mocking이 어려워 테스트하기 까다롭습니다.
적용하지 않아도 되는 경우
- 불변 상태를 다루는 순수 함수:
- 예: Math.max(), UUID.randomUUID()
- 유틸리티 클래스:
- 예: 파일 I/O, 문자열 처리, 날짜 변환 등.
이 원칙들을 따르면 객체지향 프로그래밍의 장점을 극대화할 수 있으며, 유지보수성과 확장성이 높은 코드를 작성할 수 있습니다.
728x90