오늘은 자바를 사용하는 개발자가 알아야 하는 객체 지향 설계의 5가지 원칙인 SOLID 원칙에 대해서 포스팅 해보려고 한다.
SOLID 원칙은 로버트 C.마틴이 제시한 객체지향 설계의 원칙이다. SOLID는 다섯가지 원칙의 앞글자를 따서 만들어졌으먀, 다음과 같은 의미를 가진다.
이제 각각의 원칙에 대해서 알아보도록 하겠다 !
"하나의 클래스는 하나의 책임만 가져야 한다"
한 클래스는 단일 기능 또는 하나의 목적만 수행해야 한다. 만약 여러가지 일을 처리하려고 하면, 코드가 복잡해지고 수정하기가 어려워지게 된다.
예시: 나쁜 예
public class ReportManager {
public void generateReport() {
// 보고서 생성 로직
}
public void sendEmail() {
// 이메일 전송 로직
}
}
위의 코드에서 ReportManager라는 클래스는 보고서를 생성하는 것과 이메일을 전송하는 두 가지 일을 처리하게 된다. 이는 클래스의 책임이 분산된 상태이다.
수정 : 좋은 예시
public class ReportGenerator {
public void generateReport() {
// 보고서 생성 로직
}
}
public class EmailSender {
public void sendEmail() {
// 이메일 전송 로직
}
}
이렇게 역할별로 분리를 하게 되면 각각 클래스는 하나의 책임만을 가지므로 유지보수가 쉬워진다.
"확장에는 열려 있고, 수정에는 닫혀 있어야 한다."
코드를 수정하지 않고도 기능을 확장할 수 있도록 설계해야 한다. 즉, 기존 코드를 변경하지 않고 새로운 기능을 추가할 수 있어야 한다.
예시 : 나쁜 예시
public class PaymentProcessor {
public void processPayment(String type) {
if (type.equals("creditCard")) {
System.out.println("Processing credit card payment");
} else if (type.equals("paypal")) {
System.out.println("Processing PayPal payment");
}
}
}
위의 코드에서 creditCard, paypal 이 아닌 새로운 결제 방식을 추가하려면 기존 코드를 수정해야 한다. 이를 아래와 같이 인터페이스 형태로 개선을 하게 되면 아래와 같은 코드로 작성할 수 있다.
public interface Payment {
void processPayment();
}
public class CreditCardPayment implements Payment {
public void processPayment() {
System.out.println("Processing credit card payment");
}
}
public class PayPalPayment implements Payment {
public void processPayment() {
System.out.println("Processing PayPal payment");
}
}
public class PaymentProcessor {
public void processPayment(Payment payment) {
payment.processPayment();
}
}
새로운 결제방식이 추가된다면 Payment 인터페이스를 구현하기만 하면 되므로 기존 코드를 수정할 필요가 없게 된다.
"하위 클래스는 반드시 상위 클래스의 행동을 대체할 수 있어야 한다."
하위 클래스가 상위 클래스의 역할을 완전히 대신할 수 있어야 하며, 프로그램의 동작은 변하지 않아야 한다.
예시 : 나쁜 예시
public class Rectangle {
protected int width, 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 {
@Override
public void setWidth(int width) {
this.width = this.height = width;
}
@Override
public void setHeight(int height) {
this.width = this.height = height;
}
}
Square는 Rectangle을 상속했지만, Rectangle의 동작을 완전히 대체할 수 없다.
예를 들어, Square에 setWidth(5)를 호출한 후 setHeight(10)을 호출하면 정사각형의 특성이 깨집니다.
public class Rectangle implements Shape {
private int width, height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
public int getArea() {
return width * height;
}
}
public class Square implements Shape {
private int side;
public Square(int side) {
this.side = side;
}
public int getArea() {
return side * side;
}
}
이제 `Rectangle`과 `Square`는 독립적인 클래스이며, 서로의 동작을 침해하지 않는다.
<br><br>
## 인터페이스 분리 원칙(ISP)
**"클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다."**
큰 인터페이스를 여러 개의 작은 인터페이스로 나누어, 사용하지 않는 기능을 구현하지 않아도 되게 만든다.
예시 : 나쁜 예시
```java
public interface Worker {
void work();
void eat();
}
public class Robot implements Worker {
@Override
public void work() {
System.out.println("Robot is working");
}
@Override
public void eat() {
throw new UnsupportedOperationException("Robot doesn't eat");
}
}
Robot은 eat 메서드를 사용할 필요가 없지만, 인터페이스를 구현하느라 어쩔 수 없이 작성을 하게 됩니다.
개선
public interface Worker {
void work();
}
public interface Eater {
void eat();
}
public class Robot implements Worker {
@Override
public void work() {
System.out.println("Robot is working");
}
}
public class Human implements Worker, Eater {
@Override
public void work() {
System.out.println("Human is working");
}
@Override
public void eat() {
System.out.println("Human is eating");
}
}
이렇게 작성하면 각각의 클래스는 필요한 인터페이스만 구현하면 되므로 불필요한 메서드 작성이 줄어들게 된다.
"고수준 모듈은 저수준 모듈에 의존하면 안 된다. 둘 다 추상화에 의존해야 한다."
구체적인 구현이 아닌, 추상화(인터페이스나 추상 클래스)에 의존하도록 설계해야 한다.
예시 : 나쁜 예시
public class Keyboard {
}
public class Computer {
private Keyboard keyboard;
public Computer() {
this.keyboard = new Keyboard();
}
}
Computer는 Keyboard라는 구체적인 구현에 의존하므로, 다른 입력 장치(ex. 마우스 등등,,)를 추가하기 어렵다.
개선
public interface InputDevice {
}
public class Keyboard implements InputDevice {
}
public class Computer {
private InputDevice inputDevice;
public Computer(InputDevice inputDevice) {
this.inputDevice = inputDevice;
}
}
이제 InputDevice 인터페이스를 통해 입력 장치를 추상화 하였으므로, 키보드 외에도 다른 입력 장치를 쉽게 추가할 수 있다.
새로운 프로젝트의 설계단계에 이 SOLID 원칙을 적용한다면 깔끔한 코드를 작성할 수 있을 것 같다.