일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |
- AI Mathematics
- softeer
- programmers
- 알고리즘
- 데이터사이언스
- 코테
- 자바
- Java
- string
- 오블완
- 깨끗한 코드
- 소프티어
- Coursera
- data science methodology
- 부스트캠프
- 코세라
- Python
- 프로그래머스
- 코딩테스트
- Clean Code
- 클린코드
- Boostcamp AI
- IBM
- 클린코드 파이썬
- 문자열
- 파이썬
- Data Science
- 데이터 사이언스
- 데이터과학
- 티스토리챌린지
- Today
- Total
떼닝로그
[CS] 객체 지향 설계의 SOLID 규칙 (OO 5원칙) 본문
객체 지향 설계의 SOLID 규칙 (Object-Oriented Programming/Design Principles)
작성 베이스 : https://www.freecodecamp.org/news/solid-principles-explained-in-plain-english/
The SOLID Principles of Object-Oriented Programming Explained in Plain English
By Yiğit Kemal Erinç The SOLID Principles are five principles of Object-Oriented class design. They are a set of rules and best practices to follow while designing a class structure. These five principles help us understand the need for certain desi...
www.freecodecamp.org
SOLID는 객체 지향 설계의 5원칙으로, 개발자로서는 필수적으로 알고 있어야 하는 개념이다.
배경 설명을 하자면... 클린 코드 / 클린 아키텍처 책을 쓴 Robert J. Martin(a.k.a. 엉클밥ㅋ)으로부터 이 5원칙이 처음 소개됐다.
그래서 클린 코드, 객체 지향 구조, 그리고 디자인 패턴이 다 유기적으로 연결되어있다는 것~까지 ㅎㅎ
세 개 모두 공통적인 목적을 가지고 있다면:
To create understandable, readable, and testable code that many developers can collaboratively work on.
여러 개발자들의 협력을 위해 이해하기 좋고, 가독성이 좋고, 테스트하기 좋은 코드를 만드는 것.
이라고 한다.
총 다섯 개의 원칙으로 구성되어 있다. 각 앞 글자를 따서 SOLID 원칙이라고도 한다.
- Single Responsibility Principle (단일 책임의 원칙)
- Open-Closed Principle (개방-폐쇄의 원칙)
- Liskov Subtitution Principle (리스코프 치환 법칙)
- Interface Segregation Principle (인터페이스 분리 법칙)
- Dependency Inversion Principle (의존성 역전 법칙)
이제부터 하나씩 확인해보는 걸로~~~~
Single Responsibility Principle (SRP, 단일 책임의 원칙)
단일 책임의 원칙은 "하나의 class는 한 가지의 역할만 맡아야 하고, 그에 따라 한 가지의 이유로만 변경할 수 있다"를 의미한다.
조금 더 풀어서 설명하자면... 클래스의 특성은 소프트웨어 사양 내 잠재적 변경 사항(DB/로깅 로직, ...)이 있을 때만 변경할 수 있다.
예를 들자면, 어떠한 객체가 Book이나 Student class같은 data container라고 하자.
각 class 내의 개체(entity)들은 전체적인 data model이 변경될 때만 같이 변경할 수 있다.
SRP를 따라야 하는 이유
1. 한 프로젝트에 여러 팀들이 참여할 때, 하나의 class에 대해 각기 다른 이유로 변경하는 것을 막을 수 있다.
2. 버전 관리가 쉬워진다.
3. 충돌을 피할 수 있다. (각기 다른 팀이 동일 파일을 수정하면 충돌이 일어나는데, SRP를 활용함으로써 오직 한가지 사유로만 변경 가능. 문제를 해결하기도 쉬워진다)
Bookstore Invoice Program으로 확인하는 Single Responsibility Principle
기본적으로 깔리게 될 Book에 대한 class를 먼저 만들어보겠다.
보통은 class 내 각 변수(field)는 private 처리하지만, getter/setter 없이 그냥 로직만 확인하기 위해서 만드는 것이니 건너뛰기로...
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;
}
}
다음으로는 invoice에 대한 class를 만들어보겠다~
임의로 생성하는 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) {
// 매개변수로 받은 filename 명으로 파일을 만들고 invoice와 관련된 내용 기입
}
}
정리해보자면 이 Invoice class는
- calculateTotal 함수 : 전체 가격을 계산하는 역할
- printInvoice 함수 : invoice 내역을 콘솔에 출력
- saveToFile 함수 : 파일에 invoice 내역을 작성하는 역할
으로 구성되어 있는데, 단일 책임 원칙(SRP)를 위반하는 것들은 무엇이 있는지 예측 가능한가??? (물론 난 못했음 ㅋㅎ)
해당 class에서의 SRP 원칙 위반 사항
1. printInvoice 함수
- SRP의 원리는 "하나의 class는 오직 하나의 역할만을 가지고, 한가지 이유로만 class를 변경할 수 있다." 임에 따라
- Invoice class는 invoice 계산의 역할, 그리고 계산하는 방식의 변화에서만 class를 변경 가능하다.
- 현재의 구조에서 출력 형식을 바꾸고 싶다면 class를 변경해야 하기 때문에, SRP 원칙에 위배된다.
2. saveToFile 함수
- 흔히 하는 실수로, persistence logic을 비즈니스 로직과 함께 두는 것 때문이다.
-> 여기서 persistence logic : 프로그램의 저장소로부터 데이터를 저장, 검색, 갱신, 삭제하는 로직
- file에 작성하고 save하는 역할이라고는 하지만, DB에 저장할 수도 있고, API를 호출하는 등의 역할을 할 수도 있다.
위 이슈를 해결하기 위해, InvoicePrinter와 InvoicePersistence로 신규 class를 개발하고 method들을 이동시켜야 한다.
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 + " $");
}
}
public class InvoicePersistence {
Invoice invoice;
public InvoicePersistence(Invoice invoice) {
this.invoice = invoice;
}
public void saveToFile(String filename) {
// 매개변수로 주어진 filename을 제목으로 해서 invoice 내역을 파일에 저장
}
}
이러한 구조를 통해 각 class들은 모두 Single Responsibility Principe, 단일 책임의 원칙을 지키는 application이 되었다.
Hoooray!!!!!
Open-Closed Principle (OCP, 개방-폐쇄의 원칙)
개방-폐쇄의 원칙은 "class의 확장은 가능하나, 수정은 불가하다"를 의미한다.
(직독직해하자면... class는 확장에 열려 있고 수정에 닫혀 있다...)
여기서의 "변경"은 이미 존재하는 class 내의 코드를 수정하는 것이며, "확장"은 새로운 기능을 추가하는 것이다.
말하고자 하는 것은 즉,
잠재적인 버그 생성 가능성의 risk를 피하기 위해 class 내의 코드 수정 없이 신규 기능을 추가해야 한다는 것이다.
그렇다면 어떻게 class를 건드리지 않고 새로운 기능을 추가할 수 있을까 --> interface와 abstract class를 활용하면 된다.
Bookstore Invoice Program 내 Persistence Logic으로 확인하는 Open-Closed Principle
직전에 만들었던 Invoice Program에서 상사가 검색에 쉽게 invoice들을 DB에 저장할 수 있는 기능을 요청했다고 하자.
그냥 쉽게 생각했을 때는 DB를 만들고, 연결하는 함수를 InvoicePersistence class에 넣으면 된다고 할 수 있겠지!
public class InvoicePersistence {
Invoice invoice;
public InvoicePersistence(Invoice invoice) {
this.invoice = invoice;
}
public void saveToFile(String filename) {
// 매개변수로 주어진 filename을 제목으로 해서 invoice 내역을 파일에 저장
}
public void saveToDatabase() {
// Invoice를 DB에 저장
}
}
이 구조는 확장성이 좋(extendable)지 않다. 추후 기능을 추가하기 위해서는 InvoicePersistence class 수정이 필수적이다.
그러나~ lazy하지만 clever한 개발자라면 OCP 원칙을 위해 아래처럼 수정할 수 있을 것이다.
interface InvoicePersistence {
public void save(Invoice invoice);
}
InvoicePersistence class를 Interface type으로 변경하고, save라는 method를 만듦으로써
모든 persistence class가 save method를 내장하도록 했다.
public class DatabasePersistence implements InvoicePersistence {
@Override
public void save(Invoice invoice) {
// DB에 저장하기
}
}
public class FilePersistence implements InvoicePersistence {
@Override
public void save(Invoice invoice) {
// 파일에 저장하기
}
}
완성한 구조는 아래와 같다.
이러한 형태로 구성함으로써 우리의 persistence logic은 확장성을 가지게 됐다.
만약 상사가 MySQL, MongoDB 각각에 데이테를 저장해달라고 한다면 우리는 이제 쉽게 수행할 수 있당~~
여기서 조금 더 확장해서... InvoicePersistence, BookPersistence와 같은 여러 개의 persistence class를,
PersistenceManager를 통해 모든 persistence class들을 관리하는 class를 만들 수도 있다.
public class PersistenceManager {
InvoicePersistence invoicePersistence;
BookPersistence bookPersistence;
public PersistenceManager(InvoicePersistence invoicePersistence, BookPersistence bookPersistence) {
this.invoicePersistence = invoicePersistence;
this.bookPersistence = bookPersistence;
}
}
이런 식으로 구성함으로써 다형성을 기반으로 여러 class들이 InvoicePersistence interface를 활용할 수 있게 되었다.
이것이 바로 interface가 제공하는 유연성~
Liskov Substitution Principle (LSP, 리스코프 치환 법칙)
리스코프 치환 법칙은 "자식 class는 부모 class를 대체할 수 있어야 한다"를 의미한다.
풀어서 설명하자면... A가 부모 class, B는 A를 부모로 가지는 자식 class라고 할 때,
A의 객체가 들어갈 자리에 B의 객체가 들어가더라도 동일한 결과가 나와야 한다는 것... 이라고 할 수 있다.
사실 이건 우리가 이미 예측할 수 있는 형태이다.
상속을 활용함으로써 자식 class는 그의 부모 class가 가지고 있는 모든 것들을 가지게 된다.
자식 class는 부모의 모든 것들을 유지/확장하게 되며, 자식에서 부모의 속성을 변경할 수는 없다.
따라서, 만약 어떠한 class가 해당 원칙을 위배하게 된다면... 정말 예측할 수 없는 지저분한 버그로 이어질 수 있다.
Rectangle Class로 확인하는 Liskov Substitution Principle
리스코프 법칙은 이해하기에는 쉽지만, 코드에서 어 맞아 이거 그거다! 라고 찾아보기는 조금 어렵다. 아래의 예시를 보자.
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;
}
}
간단하게 사각형(Rectangle) class를 만들었고, 그 넓이를 구하는 getArea 함수를 만들었다.
자 이제 여기서 정사각형(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);
}
}
Square class는 Rectangle class를 확장하고 있다.
생성자에서 height와 width를 같은 값으로 설정하긴 했지만, 누구도 square의 특성을 벗어나는 height/width로 수정해선 안된다.
이를 위해 우리는 setter를 override(재정의)함으로써 양쪽 특성 중 어느 한 쪽이라도 바뀔 경우 같이 변경할 수 있도록 했다.
하지만 이렇게 하게 된다면 리스코프 치환 법칙을 위배하게 된다.
수행 테스트를 위해 main class에 getArea 함수를 만들어보겠다.
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);
}
}
자~ 이렇게 작성하고 제출하면 팀 테스터가 야 너 테스트 실패했어 라는 결과를 전달해줄 겁니다~
어디서 문제였을까...
- 첫번째 테스트에선 width=2, height=3인 사각형을 만들고, getAreaTest를 통해 우리가 원하는 결과값인 20을 얻었다.
- 두번째 테스트에서의 예상 결과값은 50이었으나, square의 setHeight 함수가 width 값 또한 재설정하게 되면서 결과값은 100이 나오게 된다.
이러한 실험으로 자식은 부모의 속성(?)을 변경해선 안된다!라는 리스코프 치환 법칙을 확인할 수 있다.
Interface Segregation Principle (ISP, 인터페이스 분리 법칙)
Segregation는 분리를 의미하며, Interface Segregation Principle은 말 그대로 interface를 분리해야 한다는 법칙이다.
이 법칙은 하나의 general한 목적을 가진 interface보단 여러 개의 클라이언트 친화적인 interface가 낫다는 것이다.
이에 따라 client들은 불필요한 함수들을 필수적으로 구현해야할 필요가 줄어든다.
Parking Lot Program으로 확인하는 Single Responsibility Principle
예시를 위한 ParkingLot 프로그램이다.
public interface ParkingLot {
void parkCar(); // 주차장의 빈 공간이 줄어들 때마다 1씩 감소 (차들의 주차 후)
void unparkCar(); // 주차장의 빈 공간이 늘어날 때마다 1씩 증가 (차들이 주차 공간을 떠난 후)
void getCapacity(); // 주차장에 차를 수용 가능한 값을 반환
double calculateFee(Car car); // 주차 시간에 따른 요금 반환
void doPayment(Car car); // 요금 지불
}
class Car {
}
굉장히 간단한 형태로 시간당 요금을 부가하는 parking lot 시스템을 구성했다.
여기서 우리는 요금을 부여하지 않는, 무료 주차장을 구현해보자.
public class FreeParking implements ParkingLot {
@Override
public void parkCar() {
}
@Override
public void unparkCar() {
}
@Override
public double calculateFee(Car car) {
return 0;
}
@Override
public void doPayment(Car car) {
throw new Exception("Parking lot is free");
}
}
이 Parking Lot interface는 크게 두 가지로 구성되어 있다.
- Parking과 관련된 로직 (parkCar, unparkCar, getCapacity)
- Payment(요금 지불)과 관련된 로직
하지만 이렇게 작성하면 너무... 구체적? 독특한 구조?라고 볼 수 있다.
보다시피, FreeParking class는 공짜임에도 불구하고 요금 지불과 관련된 함수들을 구현해야 했다.
따라서 위처럼 인터페이스를 분리하면 PaidParkingLot으로 더 다양한 종류의 지불 방식을 활용할 수 있게 된다.
이렇게 구성한 모델은 더욱
- 유연하고,
- 확장성이 좋고,
- 클라이언트들이 불필요한 로직을 필수적으로 구현할 필요가 없게 된다 (ParkingLot interface로 주차장과 관련된 기능을 이미 제공하기 때문에)
Dependency Inversion Principle (DIP, 의존성 역전 법칙)
의존성 역전 법칙은 "각 class들은 concrete한 class나 함수가 아닌, interface와 추상 class에만 의존해야 한다"를 의미한다.
(여기서 concrete class는 직역하자면 구체적인 객체... 로 여러 함수들이 구현된 객체로 보면 될 것 같다)
아까 나왔던 엉클 밥이 이 법칙에 대해서 이렇게 설명했다.
"If the OCP states the goal of OO architecture, the DIP states the primary mechanism.
만약 개방-폐쇄 원칙이 객체 지향 구조의 목표라면, 의존성 역전 법칙은 그에 대한 주요 메커니즘을 의미한다."
이와 같이, 개방-폐쇄 원칙(OCP)와 의존성 역전 법칙(DIP) 간에 유기적인 관계를 맺고 있다는 것을 확인할 수 있다.
PersistenceManager class가 해당 interface를 구현한 class가 아닌 InvoicePersistence에 의존하는 것을 예시로,
우리는 class가 변경보다는 확장이 되길 원하기 때문에, concrete class보다는 interface에 의존하도록 구성해야 한다.
마치며...
전체적으로 천천히 읽다 보면 정말 SOLID 원칙 (객체지향 5원칙)에 대해 깔끔하게 정리가 될 수 있을 것 같다.
따라 적으면서 나도 공부가 많이 된듯 ㅎㅎㅎㅎㅎㅎ
크게 요약하자면(내 머리에 남은 것들 바탕으로...) 아래와 같다
- Single Responsibility Principle(단일 책임의 원칙) : 하나의 class는 오직 하나의 역할만 해야 한다. 그 역할의 변경이 아닌 다른 이유로 class를 재구성할 순 없다.
- Open-Closed Principle(개방-폐쇄의 원칙) : class는 변경보다는 확장이 되어야 한다. interface를 활용해서 해보자
- Liskov Substitution Principle(리스코프 치환 법칙) : 부모에게서 상속받은 자식 class는 부모의 모든 특성을 가지고 있으며, 자식 자체에서의 변경이 가능하다. 하지만 부모의 영역까지 넘어서 변경할 순 없다. 자식 class는 부모 class의 자리를 대체할 수 있다.
- Interface Segregation Principle(인터페이스 분리 법칙) : 하나의 general한 인터페이스보다는, 특징에 따라 여러 개의 interface로 분리하라.
- Dependency Inversion Principle(의존성 역전 법칙) : 이미 구현된 class가 아닌, interface에 의존할 수 있도록 구조를 만들어야 한다.
혹시나 이 글 이전 시리즈... 그냥 객체지향의 특징에 대해 알고 싶다면 나의 다른 글을 읽어보시길 ㅎㅎㅎㅎ
[CS] 객체 지향 프로그래밍의 특징
객체 지향 프로그래밍(Object-Oriented Programming, OOP)- 프로그래밍에서 필요한 데이터를 추상화시켜 상태와 행위를 가진 객체로 만들고, 객체들간의 상호작용을 통해 로직을 구성하는 프로그래밍 방
pseeej.tistory.com
감사합니당.
'개발로그 > 기타 이론 정리' 카테고리의 다른 글
[Python] " is None " vs " == None " (2) | 2024.11.27 |
---|---|
[C#] Virtual(가상) vs Abstract(추상) vs Interface(인터페이스) (4) | 2024.10.08 |
[CS] 객체 지향 프로그래밍의 특징 (2) | 2024.07.05 |
[Python] Python 기술 면접 대비 (2) | 2024.04.24 |