컴퓨터프로그래밍에서 SOLID란 로버트 마틴이 2000년대 초반에 명명한 객체지향 프로그래밍칙을 법칙을 마이클 페더스가 두문자어 기억술로 개한 것이다.
프로그래머가 시간이 지나도 유지 보수와 확장이 쉬운 시스템을 만들고자 할 때 이 원칙들을 함께 적용할 수 있다.
위키백과 SOLID
SOLID 란 프로그래밍을 할 때 지키면 좋은 객체지향적 방법론입니다.
코드는 단순히 작성하고 나서 끝이 아니죠. 한 번 작성하고 나서 추후 유지보수가 필요합니다.
하지만 리팩토링을 하거나 유지 보수를 할 때 편한 코드가 있고, 그렇지 못한 코드가 있죠...
이 때 도움을 주는 것이 SOLID 입니다! 한 번 하나 하나 알아가보죠!!
이 게시물에서 사용하는 예시는 freeCodeCamp - The SOLID Principles of Object-Oriented Programming Explained in Plain English 의 코드 및 설명을 가져온 것입니다.
책과 도서송장 클래스를 가지고 한 번 설명을 해 보도록 하겠습니다!!
Book과 Invoice 클래스는 우선 다음과 같습니다!!
Book class
public class Book {
String name;
String authorName;
int year;
int price;
String isbn;
public Book(String name, String authorName, int year, int price, String isbn) {
this.name = name;
this.authorName = authorName;
this.year = year;
this.price = price;
this.isbn = isbn;
}
}
Book 클래스입니다!
클래스 내 필드로는
이름, 저자명, 출판연도, 가격, isbn 등을 가지고 있고 생성자가 하나 있습니다.
Invoice class
public class Invoice {
private Book book;
private int quantity;
private double discountRate;
private double taxRate;
private double total;
public Invoice(Book book, int quantity, double discountRate, double taxRate) {
this.book = book;
this.quantity = quantity;
this.discountRate = discountRate;
this.taxRate = taxRate;
this.total = this.calculateTotal();
}
public double calculateTotal() {
double price = ((book.price - book.price * discountRate) * this.quantity);
double priceWithTaxes = price * (1 + taxRate);
return priceWithTaxes;
}
public void printInvoice() {
System.out.println(quantity + "x " + book.name + " " + book.price + "$");
System.out.println("Discount Rate: " + discountRate);
System.out.println("Tax Rate: " + taxRate);
System.out.println("Total: " + total);
}
public void saveToFile(String filename) {
// Creates a file with given name and writes the invoice
}
}
Invoice 클래스의 코드입니다.
책, 수량, 할인율, 세율, 총액 의 필드가 존재합니다.
그리고 이 클래스는 세 개의 메소드를 가지고 있습니다.
calculateTotal
- 책 가격의 총합을 계산해 주는 메소드printInvoice
- 콘솔에 도서 송장을 출력해주는 메소드saveToFile
- 도서 송장을 파일에 저장해 주는 메소드이렇게 3개의 메소드를 가지고 있습니다.
겉으로 보기에는 문제가 없어 보이죠?
하지만 위의 클래스는 SOLID 원칙을 준수하고 있지 않습니다!!
그러면 이제 SOLID 원칙을 하나하나 살펴보면서 클래스를 고쳐볼까요?
첫 번째로 단일 책임 원칙입니다.
위키에 따르면 단일 책임 원칙은 다음과 같습니다
모든 클래스는 하나의 책임만 가지며, 클래스는 그 책임을 완전히 캡슐화해야 함을 일컫는다. 클래스가 제공하는 모든 기능은 이 책임과 주의 깊게 부합해야 한다.
위의 설명을 보고 난 후 클래스를 다시 한 번 보면...
도서 송장 클래스인 Invoice
클래스가 단일 책임 원칙을 위반하고 있다는 사실을 알 수 있습니다!
만약 Invoice
클래스에서 도서 송장을 출력하는 방식을 변경하고 싶으면... 클래스 자체를 수정해야 합니다.
비즈리스 로직과 관련 없는 부분이 수정되는데, 비즈니스 로직이 포함된 클래스를 수정해야하죠
또한 saveToFile
메소드 또한 SRP를 위반하고 있습니다.
위반한 이유는 위와 같죠.
위와 같이 영속화 로직을 비즈니스 로직과 결부시키는 행위는 흔히 범하는 실수입니다.
그렇다면 위의 Invoice
클래스를 어떻게 수정해야 SRP를 지킬 수 있을까요??
바로 다음과 같이 새로운 클래스를 만들면 됩니다!
InvoicePrinter class
public class InvoicePrinter {
private Invoice invoice;
public InvoicePrinter(Invoice invoice) {
this.invoice = invoice;
}
public void print() {
System.out.println(invoice.quantity + "x " + invoice.book.name + " " + invoice.book.price + " $");
System.out.println("Discount Rate: " + invoice.discountRate);
System.out.println("Tax Rate: " + invoice.taxRate);
System.out.println("Total: " + invoice.total + " $");
}
}
InvoicePersistence
public class InvoicePersistence {
Invoice invoice;
public InvoicePersistence(Invoice invoice) {
this.invoice = invoice;
}
public void saveToFile(String filename) {
// Creates a file with given name and writes the invoice
}
}
이렇게 영속화 메소드와 출력 메소드를 해당 역할만을 수행하는 메소드로 분리시켜 놓으면 SRP를 지킬 수 있습니다!
소프트웨어 개체(클래스, 모듈, 함수 등등)는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다'는 프로그래밍 원칙이다.
여기서 수정 이란 기존 코드를 변경하는 것을 의미하고, 확장은 새 기능을 추가하는 것을 의미합니다!(너무 당연한가...)
그러면 위의 위키의 설명을 다시 한 번 풀어 보면
우리는 클래스의 기존 코드를 수정하지 않으면서 기능을 추가할 수 있어야 한다!
라는 의미가 되겠군요
왜 기존의 코드를 수정하면 안될까요??
이유가 다양하겠지만 제 생각은 '너무 위험하다..!' 입니다.
물론 기능을 추가하거나 수정하기 위해서 코드를 수정할 수는 있지만, 해당 코드가 다른 부붠에서 어떻게 사용되고 있을지도 모르는데 기능 하나를 추가하기 위해서 추가한다...
회사와 같은 집단에서는 해당 코드를 막 수정할 수도 없을 거고, 또 수정할 수 있는 권한이 없을 수도 있으니까요
그럼 우리의 코드는 OCP를 잘 준수하고 있을까요?
아쉽게도(?) 그렇지 않습니다!!!(??)
그러면 대표적으로 영속화 클래스를 한 번 봐볼까요?
InvoicePersistence class
public class InvoicePersistence {
Invoice invoice;
public InvoicePersistence(Invoice invoice) {
this.invoice = invoice;
}
public void saveToFile(String filename) {
// Creates a file with given name and writes the invoice
}
public void saveToDatabase() {
// Saves the invoice to database
}
}
위의 InvoicePersistence
클래스는
위의 클래스는 왜 OCP를 위반하고 있는 걸까요?
만약 현재는 DB를 MySQL만 사용하고 있는데, 조회 성능 최적화를 위해서 MongoDB를 추가하고 싶으면 어떻게 될까요??
InvoicePersistence
클래스를 수정해야할 수 밖에 없습니다
그러면 기능을 추가하되, 기존의 코드를 수정하지 않는것을 어떻게 구현해야 할까요??
바로 인터페이스와 추상 클래스를 이용하면 됩니다!
InvoicePersistence interface
interface InvoicePersistence {
public void save(Invoice invoice);
}
DataBasePersistence class
public class DatabasePersistence implements InvoicePersistence {
@Override
public void save(Invoice invoice) {
// Save to DB
}
}
FilePersistence class
public class FilePersistence implements InvoicePersistence {
@Override
public void save(Invoice invoice) {
// Save to file
}
}
위와 같이 인터페이스를 통해 속이 빈 메소드를 선언만 해 놓고, 추후에 원하는 형식으로 구현하면 되죠!
그러면 현재 클래스 구조는 다음과 같이 집니다!
그런데 여기서 다음과 같은 의문이 드시는 분들이 있을 수 있죠!!
이거 그냥 클래스 여러 개 만들어서 해결하면 안되나요??
굳이 인터페이스나 추상 클래스를 사용해야하나요?
그럼 다음 예시를 한 번 보겠습니다!!
책을 영속화 시키는 클래스인 BookPersistence
, 도서 송장을 영속화 시키는 InvoicePersistence
가 존재하고
두 클래스를 관리하는 즉, 영속성을 관리해주는 PersistenceManager.class
를 만들어 보면 다음과 같습니다.
PersistenceManager class
public class PersistenceManager {
InvoicePersistence invoicePersistence;
BookPersistence bookPersistence;
public PersistenceManager(InvoicePersistence invoicePersistence,
BookPersistence bookPersistence) {
this.invoicePersistence = invoicePersistence;
this.bookPersistence = bookPersistence;
}
}
이렇게 영속성을 관리하는 클래스가 인터페이스에 의존성을 가지면 InvoicePersistence
의 구현체들은 모두 영속성 PersistenceManager
에 주입될 수 있습니다!!
다형성 덕분이죠!
객체지향의 장점을 최대한 활용하기 위해서 구현체보다는 추상체를 구현하는 방식으로 개발을 진행하면 훨씬 더 관리하기 편하게 코딩을 할 수 있습니다!
치환성은 객체 지향 프로그래밍 원칙이다. 컴퓨터 프로그램에서 자료형 S가 자료형 T의 서브타입이라면 필요한 프로그램의 속성(정확성, 수행하는 업무 등)의 변경 없이 자료형 T의 객체를 자료형 S의 객체로 교체(치환)할 수 있어야 한다는 원칙이다.
리스코프 치환 원칙은 바바라 리스코프가 자료 추상화와 계층(Data abstraction principle)이라는 ...
엄밀히 용어로 말하자면 (강한) 행동적 하위형화라 부르는 하위형화 관계의 특정한 사례이다.
q(x)를 자료형 T의 객체 x에 대해 증명할 수 있는 속성이라 하자. 그렇다면 S가 T의 하위형이라면 q(y)는 자료형 S의 객체 y에 대해 증명할 수 있어야 한다.
갑자기 용어도 어려워 지고, 뭐 이상한 말들이 많이 나오는 군요...
간단히 말하면 LSP는 다음과 같습니다!
하위(자식) 클래스는 상위(부모) 클래스를 대체할 수 있어야 한다.
B 클래스가 A 클래스의 자식 클래스라면
A 클래스의 객체가 요구되는 곳에 대신 B 클래스의 객체를 넣어도 정상 작동을 해야 한다는 의미입니다!
너무 당연한 소리 아니냐구요??
그쵸!! 자식 클래스는 부모 클래스의 모든 메소드를 이어 받고, 추가로 어떤 메소드가 더 존재햇으면 존재 했지 작을 수는 없으니까요
하지만 이런 생각치도 못한 상황이 나오는 경우가 존재하닉깐 이런 원칙이 존재하겠죠???
예시를 한 번 살펴봅시다!
Rectangle class
class Rectangle {
protected int width, height;
public Rectangle() {
}
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
직사각형 클래스입니다
높이와 너비를 필드로 가지고 있고
생성자, Setter, Getter, 넓이 구하기 메소드 등을 가지고 있는 메소드 군요
그리고 이 직사각형 클래스를 상속받은 다른 클래스를 생각해 봅시다
바로 정사각형입니다.
Square class
class Square extends Rectangle {
public Square() {}
public Square(int size) {
width = height = size;
}
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width);
}
@Override
public void setHeight(int height) {
super.setHeight(height);
super.setWidth(height);
}
}
정사각형 클래스는 직사각형 중 너비와 높이가 같은 사각형입니다
그렇기 때문에 코드는 좀 더 축소될 수 있겠군요
정사각형 클래스의 특징이라면 Setter에 있습니다.
너비와 높이가 같다는 정사각형의 특성 때문에 각 Setter는 직사각형의 모든 setter를 각각 호출하고 있습니다.
정사각형의 특성 때문인데, 바로 여기서 LSP가 위반된다고 할 수 있습니다
확인을 위해 다음 예시를 보도록 해보죠
Test class
class Test {
static void getAreaTest(Rectangle r) {
int width = r.getWidth();
r.setHeight(10);
System.out.println("Expected area of " + (width * 10) + ", got " + r.getArea());
}
public static void main(String[] args) {
Rectangle rc = new Rectangle(2, 3);
getAreaTest(rc);
Rectangle sq = new Square();
sq.setWidth(5);
getAreaTest(sq);
}
}
천천히 한 번 보도록 하죠
getAreaTest
메소드는 직사각형을 인자로 받은 다음 너비를 측정합니다.
그리고 높이는 10으로 설정하죠
그런 다음 (너비 * 10) == getArea()
인자 확인하는 메소드입니다.
main 함수의 예시를 살펴보면, 첫 번째 직사각형은 너비, 높이가 각각 2, 3인 직사각형입니다.
하지만 두 번째 예시는 한 변의 길이가 5인 정사각형입니다.
이 녀석은 너비든, 높이든 어떤 setter를 호출 하던 간에 모든 변의 길이를 전부 다 바꿔버리는 녀석이죠
그렇기 때문에 getAreaTest
메소드는 정삭 동작하지 않습니다.
아마 main 함수를 돌리면 다음과 같이 뜨겠군요
Expected area of 20, got 20
Expected area of 50, got 100
즉 위와 같은 경우는 LSP가 위반된 경우라고 할 수 있습니다.
사실 정사각형 클래스가 직사각형 클래스의 자식 클래스인 구조는 객체 지향적으로 부적절한 구조인 것이죠....!!!
LSP가 위반된 경우에는 다른 원칙들과 다르게 알아채기 힘들고, 아주 더러운 버그를 발생시키기 때문에 감지하는데 어렵다는 특징이 있습니다!!
인터페이스 분리 원칙은 클라리언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다는 원칙이다.
큰 덩어리의 인터페이스들을 구체적이고 작은 단위들로 분리시킴으로써 클라이언트들이 꼭 필요한 메서드 들만 이용할 수 있게 한다.
ISP는 인터페이스를 가능한 찢어서 분리시키라는 의미입니다.
뭔가 클래스의 SRP와 비슷한 느낌을 주는 원칙이군요
다른 점이라면 ISP는 구현하는 구현체가 사용하지 않는 메소드(기능)은 구현하도록 강제하면 안된다는 점이 있습니다.
다음 코드는 ISP를 준수하지 않은 코드입니다.
ParkingLot interface
public interface ParkingLot {
void parkCar(); // Decrease empty spot count by 1
void unparkCar(); // Increase empty spots by 1
void getCapacity(); // Returns car capacity
double calculateFee(Car car); // Returns the price based on number of hours
void doPayment(Car car);
}
class Car {
}
딱 봐도 ParkingLot
인터페이스가 하는 일 굉장이 많아 보이죠?
차를 주차하고, 빼기도 하고, 주차된 차의 수를 세주기도 하고 요금 계산에 정산까지...
뭐 근데 주차장에서는 다 해야하는 일이니깐...
앗! 그런데 위 코드를 기반으로 무료주차장을 구현하려고 하면 어떻게 될까요??
FreeParking class
public class FreeParking implements ParkingLot {
@Override
public void parkCar() {
}
@Override
public void unparkCar() {
}
@Override
public void getCapacity() {
}
@Override
public double calculateFee(Car car) {
return 0;
}
@Override
public void doPayment(Car car) {
throw new Exception("Parking lot is free");
}
}
두 메소드가 놀게 되는군요...
명백하게 ISP를 위반해 버렸습니다.
무료 주차장의 특성상 요금을 정산할 필요가 없지만, 요금 관련 메소드가 존재하므로 억지로 구현한 모습입니다...
그러면 ISP를 준수해서 인터페이스를 분리하는 방향으로 구성해야 하겠군요!!
아마 다음과 같은 구조로 변경하면 될 것 같아요!
이런식으로 인터페이스를 분리했더니
유/무료 여부를 따지지도 않고, 다양한 형식의 요금 징수 주차장을 모두 표현할 수 있는 코드로 진화했군요!
의존관계 역전 원칙은 소프트웨어 모듈을 분리하는 특정 형식을 지칭한다. 이 원칙에 따르면 상위 계층(정책 결정)이 하위 계층(세부 사항)에 의존하는 전통적인 의존관계를 반전(역전)시킴으로써 상위 계층이 하위 계층의 구현으로부터 독립되게 할 수 있다.
첫째, 상위 모듈은 하위 모듈에 의존해서는 안된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야 한다.
둘째, 추상화는 세부 사항에 의존해서는 안된다. 세부사항이 추상화에 의존해야 한다.
클래스는 구체적인 클래스, 구현체 보다는 추상체에 의존해야한다는 원칙입니다.
Uncle Bob은 2000년에 다음과 같이 DIP를 요약합니다.
"If the OCP states the goal of OO architecture, the DIP states the primary mechanism".
원본의 출처가 삭제되었네요...ㅠㅠ
OCP와 DIP는 깊이 관련이 있으며, 위에 OCP에 대해 기술할 때 DIP를 어느 정도 염두에 두고 설명을 했습니다!!
PersistenceManager
클래스는 인터페이스인 InvoiceManager
에 의존하고 있습니다!!
구현체인 다른 클레스에 의존시키기보다는 추상체인 InvoiceManager
에 의존을 함으로써 다형성의 이점을 살릴수도 있고, DIP도 준수할 수 있게 되었습니다!
그런데 왜 의존성이 역전되었다고 하는 것일까요??
만약 PersistenceManager
가 직접 구현체를 들고 있었다면 의존 관계는 다음과 같겠죠
PersistenceManager
가 DB에 저장을 하기 위해 MySQLPersistence
라는 구현체에 직접 의존하는 모습입니다.
하지만 OCP에서 나왔던 것 처럼 사이에 DataBasePersistence
를 사이데 두게 되면 MySQLPersistence
도 DataBasePersistence
에 의존을 하기 때문에 의존 관계 가 역전된다고 하는 것입니다!!
SOLID를 준수하면서 개발을 한다면 추후 리팩토링, 코드 수정 시 보다 깔끔하게 수정할 수 있습니다!!
개발은 기능 개발 했다고 해서 끝이 아니니까요...
물론 모든 SOLID 원칙을 고수하면서 개발하기는 힘들겠지만... 머릿속에 내재시켜 놓으면 나중에 편할거에요!!(아마...?)
출처 : freeCodeCamp - The SOLID Principles of Object-Oriented Programming Explained in Plain English
쉽게 설명해주셔서 감사합니다!