// Without Polymorphism
if(animalType.equals("Dog")) {
bark();
} else if(animalType.equals("Cat")) {
meow();
}
// With Polymorphism
Animal animal = AnimalFactory.create(animalType);
animal.makeSound();
🤔다형성을 사용하지 않은 상황에서는, 여러 종류의 동물의 울음소리를 만들기 위해 매번 조건문을 추가해야 하고, 이로 인해 코드의 복잡성이 증가합니다. 새로운 동물 종이 추가될 때마다 if-else 블록을 수정해야 하며, 이는 유지보수가 어려워지는 결과를 가져옵니다.
반면에 다형성을 활용하면, 각 동물의 '울음소리' 로직을 해당 동물 클래스 안에 정의할 수 있습니다. 이렇게 하면, 새로운 동물이 추가되더라도 기존 코드를 변경할 필요가 없고, 단순히 새로운 동물 클래스만 추가하면 됩니다. 이로 인해 코드는 더 유연해지고 확장성이 향상됩니다.
😑다형성은 오브젝트 지향 프로그래밍의 핵심 원칙 중 하나로, 이를 적절히 활용하면 코드의 가독성은 물론 재사용성과 유지보수성까지 크게 향상될 수 있습니다.
// Violating SRP
public class UserManager {
public void createUser() {
// ...
}
public void deleteUser() {
// ...
}
public void logError() {
// ...
}
}
// Adhering to SRP
public class UserManager {
public void createUser() {
// ...
}
public void deleteUser() {
// ...
}
}
public class ErrorLogger {
public void logError() {
// ...
}
}
🧐SRP(단일 책임 원칙)에 따라, 클래스는 하나의 책임만을 가져야 합니다. 이렇게 하면 클래스가 작아지고 가독성이 높아집니다.
여기서 '책임'에 대해 오해하면 안 됩니다. 책임이라고 해서 단순히 하나의 일만을 하는 것을 의미하는 것은 아닙니다. 만약 하나의 일만을 해야 한다고 생각하면, 모든 클래스는 2개 이상의 메소드를 가질 수 없다는 제약이 생깁니다.
여기서 말하는 '책임'은 클래스가 해야 할 일, 즉 그 클래스가 존재하는 이유를 의미합니다. 예를 들어, UserManager 클래스는 사용자를 관리하는 것이 그 책임입니다. 그래서 사용자를 생성하거나 삭제하는 메소드를 가질 수 있습니다. 반면에, 에러를 로깅하는 것은 UserManager의 책임이 아니므로, 이를 별도의 ErrorLogger 클래스로 분리하는 것이 SRP를 지키는 방법입니다.
🥳이렇게 SRP를 지키면, 각 클래스는 자신의 책임에만 집중하게 되어 코드의 가독성, 재사용성, 유지보수성이 향상됩니다.
// Violating Law of Demeter
String name = user.getProfile().getName();
// Following Law of Demeter
String name = user.getName();
😎데메테르의 법칙에 따르면 객체는 그의 이웃만을 알아야 하고 그 이상은 모르는 것이 좋습니다. 이 원칙을 따르면 코드의 결합도가 낮아져서 여러 가지 이점이 있습니다.
코드의 가독성 저하
user.getProfile().getName(); 이렇게 코드를 작성하면, user 내부의 profile, 그리고 그 profile 내부의 name이라는 사실을 모두 알고 있어야 합니다. 이는 가독성을 저하시키고 코드의 복잡성을 높입니다.
유지보수의 어려움
메서드 체이닝을 통해 값에 접근할 경우, 어떠한 값이 변경되거나 문제가 발생했을 때 원인을 찾기 어렵습니다. get().get() 형식의 메서드 체이닝은 여러 객체를 거쳐 값에 접근하므로, 어떤 객체에서 문제가 발생했는지 추적하기 어렵습니다.
결합도 증가
데메테르의 법칙을 어기면 결합도가 증가합니다. 결합도가 높은 코드는 수정이 어렵고 테스트도 힘들어집니다.
메서드 체이닝 줄이기
객체에서 다른 객체를 얻어 또 그 객체의 메서드를 호출하는 것은 데메테르의 법칙에 위배됩니다. 예를 들어, a.getB().getC().doSomething()은 이를 줄일 필요가 있습니다.
DTO(Data Transfer Object) 사용
단순히 데이터만을 전달하는 객체를 사용하면, 클라이언트 코드가 내부 구조를 알 필요가 없게 됩니다.
Delegate 메서드 사용
필요한 로직을 수행하는 메서드를 해당 객체 내부에 정의함으로써, 클라이언트 코드가 다른 객체에 직접 접근하지 않도록 할 수 있습니다. 예를 들어, a.doSomething() 메서드가 내부에서 b.getC().doSomething()을 호출한다면, 클라이언트 코드는 a만 알고 있으면 됩니다.
인터페이스 활용
특정 기능만을 노출하는 인터페이스를 만들어, 구현 객체의 내부 구조를 숨길 수 있습니다.
이벤트/리스너 패턴 사용
객체가 이벤트를 발생시키고, 이에 대한 처리를 다른 객체가 담당하도록 할 수 있습니다. 이를 통해 서로의 내부 구조를 몰라도 되게 만들 수 있습니다.
🫠데메테르의 법칙을 지키면 코드가 더욱 깔끔해지고, 유지보수와 테스트가 쉬워집니다.