SOLID 원칙은 무엇이며, 왜 지켜야 하는가?
SOLID 원칙
--
SOLID는
객체 지향 프로그래밍에서 유지보수성과 확장성이 좋은 소프트웨어를 설계하기 위한 5가지 원칙의 약자다.
SOLID 종류
- SRP : 단일 책임 원칙
- OCP : 개방 폐쇄 원칙
- LSP : 리스코프 치환 원칙
- ISP : 인터페이스 분리 원칙
- DIP : 의존 역전 원칙
SOLID의 목적
- 소프트웨어의 품질을 높임
- 코드의 유연성과 재사용성을 증가 시킴
- 복잡성을 제거하여 리팩토링에 소요되는 시간 줄임
- 개발의 생산성 증가
SOLID 개념은
OOP의 특징인 "캡슐화", "추상화", "다형성", "상속" 등의 개념들을 다시 재정의한 원칙으로 생각하면 된다.
그래서 프로젝트에 SOLID의 원칙을 모두 적용해야 할 필요가 없으며, 상황에 따라 필요한 부분만 적용하면 된다.
--
단일 책임 원칙 (SRP, Single Responsibility Principle)
--
SRP는
클래스(객체)는 단 하나의 책임(기능)만 가져야 한다는 원칙이다.
즉, 다양한 기능들을 하나의 클래스에 정의하지 말고, 각 관련 기능들을 분리하여 여러 클래스로 설계하는 것이다.
만약 하나의 클래스에 여러 기능들이 모여있다면
특정 기능의 수정이 일어났을 경우 다른 기능에도 영향을 줄 수 있기 때문에 유지보수가 어려워질 수 있다.
그래서 SRP는 프로그램의 유지보수성을 높이기 위한 설계 기법이다.
예시 코드
class Order {
public void addProduct(Product product) {
// 제품을 주문에 추가하는 코드
}
}
class OrderPrinter {
public void printOrder(Order order) {
// 주문 정보를 출력하는 코드
}
}
Order 클래스에서는
주문 처리와 관련된 작업만 담당하고
OrderPrinter 클래스에서는
주문에 대한 출력과 관련된 작업만 담당한다.
하나의 클래스에 하나의 책임(기능)만 가져야 한다는 것이
하나의 메서드만 가져야 한다는 것이 아니고 특정 역할에 관련된 메서드들을 가져야 한다는 것이다.
--
개방-폐쇄 원칙 (OCP, Open-Closed Principle)
--
OCP는
클래스의 확장에는 열어두고, 수정에는 닫혀있어야 한다는 원칙이다.
즉, 기존 코드(클래스)를 수정하지 않고 새로운 기능을 추가하여 확장할 수 있도록 설계하는 것이다.
만약 기존 코드(클래스)를 수정할 수 있다면
해당 코드(클래스)를 사용하던 파트도 변경이 일어나므로 예상치 못한 변수가 일어날 수 있다.
그래서 이미 작성된 클래스의 수정을 막고 상속을 통해 해당 클래스를 확장하여 사용하는 방향으로
기존에 사용 중이던 클래스에 영향을 주지 않아 안정성을 유지하는 것이다.
간단하게 설명하면
OCP 원칙은 추상화를 사용하여 상속을 통해 기존 클래스에 영향을 주지 않고 확장하는 원칙이다.
즉, 상속을 통행 클래스 관계를 구축하는 것이다.
(다형성과 확장이 가능한 객체지향의 장점을 극대화하는 원칙)
예시 코드
abstract class Shape { // 추상 클래스
public abstract double area();
}
class Circle extends Shape { // 추상 클래스(Shape)를 확장(상속)하여 구현
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
class Rectangle extends Shape { // 추상 클래스(Shape)를 확장(상속)하여 구현
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
}
--
리스코프 치환 원칙 (LSP, Liskov Substitution Principle)
--
LSP는
자식 클래스(타입)은 언제나 부모 클래스(타입)를 대체(교체)할 수 있어야 한다는 원칙이다.
(부모 클래스 대신에 자식 클래스를 사용할 수 있어야 한다.)
즉, 다형성의 특징을 이용하기 위한 원칙으로
부모 클래스의 타입으로 작성된 코드에서 해당 자식 클래스로도 문제없이 동작해야 한다는 것이다.
- 자식 클래스는 부모 클래스의 모든 기능을 올바르게 상속받아 사용
- 부모 클래스 대신 자식 클래스를 사용해도 정상 동작
- 부모 클래스의 기능을 확장해면서도, 해당 기능의 일관성과 정상적인 동작은 유지
자식 클래스에서 부모 메서드의 오버라이딩을 주의하여해야 한다.
왜냐하면
LSP는 자식 클래스(타입)는 언제나 부모 클래스(타입)를 대신 사용할 수 있어야 하는데
부모 클래스에서 A메서드의 동작을 자식 클래스에서 오버라이딩하여 전혀 다른 동작으로 바꿔놓으면
부모 클래스 대신 자식 클래스를 사용할 때 예상한 A메서드와 달리 전혀 다른 동작이 수행되기 때문이다.
그래서 자식 클래스를 사용해도 부모 클래스를 사용할 때의 의도대로 실행되도록 구성해야 하는 원칙이다.
잘못된 LSP 예시 코드
class Rectangle {
protected int width, height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int area() {
return width * height;
}
}
class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // 정사각형은 가로와 세로가 같아야 함
}
@Override
public void setHeight(int height) {
this.width = height;
this.height = height;
}
}
부모 클래스인 Rectangle은 "가로", "세로"를 다르게 설명할 수 있는 사각형 클래스지만
자식 클래스는 정사각형 클래스로 "가로"와 "세로"의 길이가 동일하도록 오버라이딩을 했다.
다만 이렇게 되면
만약 부모 클래스인 Rectangle의 동작을 믿고 Square 클래스를 사용하면
의도와 다르게 동작하게 된다.
ex)
setWidth(5);
setHeight(10);
예상 : 가로 = 5, 세로 = 10
결과 : 가로 = 10, 세로 = 10
LSP 원칙을 위반하지 않은 예시 코드
// Rectangle과 Square의 관계를 변경하여 LSP 위반 방지
interface Shape {
int area();
}
class Rectangle implements Shape {
protected int width, height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public int area() {
return width * height;
}
}
class Square implements Shape {
private int side;
public Square(int side) {
this.side = side;
}
@Override
public int area() {
return side * side;
}
}
Rectangle 클래스와 Square 클래스의 공통 인터페이스 Shape를 정의하고
사로 상속 관계로 구현하지 않았다.
간단하게 LSP의 원칙에 대해 설명하면
위 그림처럼 부모 클래스의 성격과 일치하는 자식 클래스가 만들어져야 한다는 원칙이다.
즉, 부모 클래스의 범주인 큰 카테고리에 속하는 자식 카테고리만 자식 클래스로 상속받아야 한다는 것이다.
--
인터페이스 분리 원칙 (ISP, Interface Segregation Principle)
--
ISP는
인터페이스는 클라이언트가 사용하지 않는 메서드에 의존하지 않도록 작게 분리해야 한다는 원칙이다.
즉, 인터페이스를 각 사용에 맞게 작은 단위로 분리하여 정의해야 한다는 것이다.
간단하게 설명하면
SRP 원칙과 같은 목표지만 차이점이 있다면
SRP는 클래스의 단일 책임,
ISP는 인터페이스의 단일 책임인 느낌으로 이해하면 편하다.
그래서 ISP 원칙은
인터페이스를 사용하는 클라이언트를 기준으로 분리하므로,
클라이언트가 목적과 용도에 적합한 인터페이스로 분리하여 제공하는 것이다.
주의
인터페이스를 한 번 정의했으면 되도록 재정의하면 안 된다.
(인터페이스를 구성해 놓고 수정할 사항이 생겨 해당 인터페이스를 재정의 및 분리 X)
즉, 인터페이스는 클래스와 달리 다중 상속이 가능하기 때문에
분리할 수 있는 단위로 모두 분리하여
구현하고 싶은 클래스의 용도에 맞게 원하는 인터페이스들을 가져와서 조립하여 구현하는 원칙이라고 생각하면 된다.
--
의존 역전 원칙 (DIP, Dependency Inversion Principle)
--
DIP는
고수준 모듈은 저수준의 모듈에 의존하지 않고, 둘 다 추상화에 의존해야 한다는 원칙이다.
즉, 특정 클래스를 참조(상속) 해야 하는 클래스가 필요하다면
해당 클래스를 직접 참조(상속)하는 것이 아닌 해당 클래스의 상위 요소(추상 클래스, 인터페이스)를 참조(상속)하여
해당 클래스를 의존하는 형태가 아닌 그 상위의 인터페이스에 의존하는 원칙이다.
간단하게 설명하자면
의존 관계를 맺을 때에는 변화하기 쉬운(or 자주 변화하는) 클래스에 의존하는 것보다는
변화하기 어려운(or 변화가 거의 없는) 추상화된 인터페이스에 의존함으로써
코드의 유연성을 높이고 변경에 강한 구조를 만드는 것이다.
예시 코드
interface Notification {
void sendMessage(String message);
}
class EmailNotification implements Notification {
@Override
public void sendMessage(String message) {
System.out.println("이메일로 메시지 전송: " + message);
}
}
class SMSNotification implements Notification {
@Override
public void sendMessage(String message) {
System.out.println("SMS로 메시지 전송: " + message);
}
}
class User {
private Notification notification;
public User(Notification notification) {
this.notification = notification;
}
public void notifyUser(String message) {
notification.sendMessage(message);
}
}
public class Main {
public static void main(String[] args) {
User user = new User(new EmailNotification()); // EmailNotification을 주입
user.notifyUser("환영합니다!");
User user2 = new User(new SMSNotification()); // SMSNotification을 주입
user2.notifyUser("안녕하세요!");
}
}
User 클래스는 Notification 인터페이스에 의존하여,
이메일이나 SMS 같은 구체적인 구현 클래스는 주입을 통해 결정하여 사용할 수 있다.
그래서 User 클래스는 특정 구현에 의존하지 않고, 유연하게 다양한 알림 방식을 사용할 수 있게 된다.
--
정리
--
SRP (단일 책임 원칙)
- 클래스는 하나의 책임(기능)만 가져야 한다.
OCP (개방 폐쇄 원칙)
- 확장에는 열려 있고, 수정에는 닫혀 있는 구조
LSP (리스코프 치환 원칙)
- 자식 클래스는 부모 클래스를 대신 사용할 수 있어야 한다.
ISP (인터페이스 분리 원칙)
- 인터페이스는 클라이언트가 필요로 하는 기능만 제공해야 한다.
DIP (의존 역전 원칙)
- 고수준의 모듈과 저수준의 모듈은 추상화에 의존해야 한다.
--
'Terminology' 카테고리의 다른 글
디자인 패턴 (1) | 2024.10.28 |
---|---|
의존성 주입 (DI, Dependency Injection) (+ IoC, DIP) (0) | 2024.10.27 |
객체지향 프로그래밍 (OOP, Object-Oriented Programming) (0) | 2024.10.25 |
CI / CD 파이프라인 (0) | 2024.07.05 |
DevOps (Development Operations) 란? (0) | 2024.07.04 |