객체지향 생활 체조 원칙은 객체지향 설계(OOP)에서 코드의 유지보수성과 가독성을 높이고, 강한 응집도와 낮은 결합도를 지닌 코드를 작성하기 위해 지켜야 할 실천적인 원칙들입니다. 주로 다음 9가지로 요약됩니다.
들여쓰기 단계가 많아지면 코드가 복잡해지고 가독성이 떨어진다.
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;
}
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!");
의도: 원시값과 문자열을 직접 사용하는 대신 객체로 감싸 비즈니스 로직을 포함시키고 불변성을 유지.
이점: 의미를 명확히 하고 잘못된 값 사용을 방지.
// 나쁜 예
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 같은 상위 클래스에 비대한 책임을 주는 것을 막고 추가적인 요구사항에 대응하기가 매우 편리해진다.
(스트림 등 체이닝하는 일부를 제외)
어느 코드 한 곳에서 점이 둘 이상 있다면, 해당 부분을 다시 리팩토링 해야 함
// 나쁜 예
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();
}
}
과도한 축약은 코드 가독성을 저해한다. 무조건 짧다고 좋은 것은 아니다.
메서드의 이름이 긴 이유 중 하나는, 책임을 너무 많이 갖고 있거나, 적절한 클래스의 아래에 위치하지 않아서 일 수 있음
한 두 단어정도로 되어있는 경우엔, 축약을 하지 말 것
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 = "제이미";
}
}
클래스와 메서드의 크기를 작게 만들어 단일 책임을 부여.
클래스
50줄 이상인 경우 보통 클래스가 한 가지 일만 하지 않는다. (한 가지 일만 한다면 놔둬도 되는 듯...?)
50줄 정도면 스크롤을 내리지 않아도 된다. - 한 눈에 들어오는 효과!
패키지
하나의 목적을 달생하기 위한 연관된 클래스들의 모임
작게 유지하면 패키지가 진정한 정체성을 가지게 된다.
권장 크기: 50줄 이상 되는 클래스 또는 10개 파일 이상의 패키지는 없어야 한다.
인스턴스 변수가 많아지면 클래스의 책임이 커짐.
해결 방법: 관련된 필드를 별도의 클래스로 분리하거나 일급 컬렉션 사용.
//나쁜 예
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 {};
// ..
메서드에 인자가 많으면 이해하기 어렵고 재사용성이 낮아짐.
일급 컬렉션이란 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장 - 경계)
이부분은 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 클래스에게 필요한 메시지를 보내기만 하면 된다.
객체의 내부 상태를 외부에서 조작하지 않도록 방지.
대안: 객체에 메시지를 보내 상태를 변경하거나 값을 반환하도록 설계.
코드의 응집도가 높아지고, 객체 간 결합도가 낮아져 유지보수가 쉬워짐.
객체의 자율성과 캡슐화를 강화.
테스트 가능성이 높아져 코드 품질이 개선.
객체지향 생활 체조 원칙은 모두 지키기 어렵지만, 이 원칙을 염두에 두고 코드를 작성하면 객체지향의 본질을 살린 설계로 나아갈 수 있습니다.