객체지향 설계원칙 5가지
하나의 클래스는 하나의 책임을 가져야 한다.
// SRP를 위반한 예시
public class User {
public void createUser() { // 사용자 생성 코드 }
public void sendEmail() { // 이메일 전송 코드 }
}
// SRP를 준수한 예시
public class User {
public void createUser() { // 사용자 생성 코드 }
}
public class EmailSender {
public void sendEmail() { // 이메일 전송 코드 }
}
확장에는 열려있고 수정에는 닫혀있어야 한다.
기존의 코드를 변경하지 않으면서(Closed), 기능을 추가할 수 있도록(Open) 설계가 되어야 한다는 원칙을 말한다.
// OCP를 위반한 예시
public class DiscountCalculator {
public double calculateDiscount(int price, String customerType) {
double discount = 0.0;
if (customerType.equals("Regular")) {
discount = price * 0.1;
} else if (customerType.equals("Premium")) {
discount = price * 0.2;
}
return discount;
}
}
위 코드는 DiscountCalculator 클래스가 새로운 할인율을 추가하거나 변경할 때마다 코드를 수정해야 합니다. 이를 OCP를 준수하도록 리팩토링하면, 아래와 같이 인터페이스를 사용하여 할인율 계산을 캡슐화할 수 있습니다.
// OCP를 준수한 예시
public interface DiscountCalculator {
double calculateDiscount(int price);
}
public class RegularDiscountCalculator implements DiscountCalculator {
public double calculateDiscount(int price) {
return price * 0.1;
}
}
public class PremiumDiscountCalculator implements DiscountCalculator {
public double calculateDiscount(int price) {
return price * 0.2;
}
}
자식클래스는 부모클래스로 대체 할 수 있어야한다.
즉 부모클래스 자리에 자식클래스를 넣어도 계획대로 잘 작동해야 한다.
// LSP 위반 예시
public class Rectangle {
private int width;
private int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
public class Square extends Rectangle {
public void setWidth(int width) {
this.width = width;
this.height = width;
}
public void setHeight(int height) {
this.width = height;
this.height = height;
}
}
위 코드는 사각형(Rectangle)과 정사각형(Square) 클래스가 상속 관계에 있습니다. 그러나 정사각형은 가로와 세로가 항상 같으므로, setWidth()와 setHeight() 메서드를 오버라이딩하여 항상 두 값이 같도록 만들었습니다. 이로 인해 정사각형 클래스의 getArea() 메서드는 올바른 결과를 반환하지 않아 LSP를 위반합니다.
// LSP를 준수한 예시
public abstract class Shape {
public abstract int getArea();
}
public class Rectangle extends Shape {
private int width;
private int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
public class Square extends Shape {
private int length;
public void setLength(int length) {
this.length = length;
}
public int getArea() {
return length * length;
}
}
클라이언트는 자신이 사용하는 메소드에만 의존해야 한다
클래스는 자신이 사용하지 않는 인터페이스는 구현하지 않아야함
public interface Animal {
void eat();
void fly();
void swim();
}
public class Dog implements Animal {
@Override
public void eat() {
System.out.println("Dog is eating.");
}
@Override
public void fly() {
// This method should not exist for a dog.
}
@Override
public void swim() {
System.out.println("Dog is swimming.");
}
}
Animal 인터페이스는 eat(), fly(), swim() 메서드를 모두 포함하고 있습니다. 하지만 Dog 클래스에서는 fly() 메서드를 구현할 필요가 없으며, 오히려 이 메서드를 사용할 경우 잘못된 결과를 내게 됩니다.
public interface Animal {
void eat();
}
public interface Swimmable {
void swim();
}
public class Dog implements Animal, Swimmable {
@Override
public void eat() {
System.out.println("Dog is eating.");
}
@Override
public void swim() {
System.out.println("Dog is swimming.");
}
}
Animal 인터페이스는 eat() 메서드만을 포함하도록 변경하고, Swimmable 인터페이스를 새로 만들어 swim() 메서드를 추가합니다. 이제 Dog 클래스에서는 필요한 인터페이스만 구현하면 되므로 ISP를 지킬 수 있습니다.
변하기 쉬운 것(구체적인 것)보다는 변하기 어려운 것(추상적인 것)에 의존해야함
public class UserService {
private UserDao userDao;
public UserService() {
userDao = new UserDao();
}
public void addUser(User user) {
userDao.addUser(user);
}
}
public class UserDao {
public void addUser(User user) {
// code to add user to database
}
}
UserService 클래스는 UserDao 클래스를 직접 생성하고 사용합니다. 이는 DIP를 위반하는 코드입니다. UserService 클래스는 UserDao 클래스에 직접 의존하고 있기 때문입니다. 이러한 의존성은 코드 유지보수와 확장을 어렵게 만들 수 있습니다.
public interface IUserDao {
void addUser(User user);
}
public class UserService {
private IUserDao userDao;
public UserService(IUserDao userDao) {
this.userDao = userDao;
}
public void addUser(User user) {
userDao.addUser(user);
}
}
public class UserDao implements IUserDao {
public void addUser(User user) {
// code to add user to database
}
}
UserDao 클래스를 IUserDao 인터페이스로 추상화하고, UserService 클래스는 IUserDao 인터페이스에 의존하도록 변경했습니다. 또한, UserService 클래스는 IUserDao 인터페이스를 생성자 인자로 받아서 사용합니다. 이렇게 함으로써 UserService 클래스와 UserDao 클래스 사이의 결합도를 낮출 수 있으며, 코드 유지보수와 확장성을 높일 수 있습니다.