컴퓨터 프로그래밍에서 SOLID란 로버트 마틴이 2000년대 초반에 명명한 객체 지향 프로그래밍 및 설계의 다섯 가지 기본 원칙을 마이클 페더스가 두문자어 기억술로 소개한 것이다.
프로그래머가 시간이 지나도 유지 보수와 확장이 쉬운 시스템을 만들고자 할 때 이 원칙들을 함께 적용할 수 있다.
SOLID 원칙들은 소프트웨어 작업에서 프로그래머가 소스 코드가 읽기 쉽고 확장하기 쉽게 될 때까지 소프트웨어 소스 코드를 리팩터링하여 코드 냄새를 제거하기 위해 적용할 수 있는 지침이다.
SOLID의 다섯 가지 원칙을 살펴보자.
SOLID 원칙
- 단일 책임 원칙(SRP: Single Responsibility Principle)
- 개방-폐쇄 원칙(OCP: Open-Closed Principle)
- 리스코프 치환 원칙(LSP: Liskov Subsituation Principle)
- 의존 역전 원칙(DIP: Dependency Inversion Principle)
- 인터페이스 분리 원칙(ISP: Interface Segregation Principle)
1.단일 책임 원칙
SRP: Single Responsibility Principle
객체는 단 하나의 책임만 가져야 한다.
책임은 객체가 '해야 하는 것', '할 수 있는 것'으로 간주할 수 있다.
또한 객체는 책임에 수반되는 모든 일을 자신만이 수행할 수 있어야 한다.
책임의 의미
예를 들어보자. Student 클래스는 다음 일들을 수행한다.
public class Student {
public void getCources() {...} // 수강 과목 조회
public void addCource(Course c) {...} // 수강 과목 추가
public void save(){...} // 데이터베이스에 정보 저장
public Student load() {...} // 데이터베이스에서 정보 읽기
public void printOnReportCard() {...} // 성적표에 출력
public void printOnAttendanceBook() {...} // 출석부에 출력
}
Student 클래스는 너무 많은 책임을 수행한다.
Student 클래스가 잘 할 수 있는 수강 과목 추가/조회만 놔두고 나머지 일들은 더 잘 할 수 있는 클래스들이 하는 것이 좋을 것 같다.
이렇게 객체가 가장 잘 할 수 있는 일만 수행하도록 하는 것이 SRP를 따르는 설계다.
책임 분리
Student 클래스가 많은 책임을 수행하게 될수록 Student 클래스의 도움을 필요로 하는 코드도 많을 수 밖에 없다.
수강 과목을 추가/조회 하는 코드도 Student 클래스의 도움을 필요로 하고, 성적표/출력표에 정보를 출력하는 코드도 Student 클래스의 도움을 필요로 하는 등등..
이런 이유 때문에 Student 클래스에 변경 사항이 생기면 Student 클래스와 직접 혹는 간접적으로 연관되어 있는 코드들을 모두 다시 테스트해야 한다.
이와 같이 어떤 변화가 있을 때 해당 변화가 기존 시스템의 기능에 영향을 주는지 평가하는 테스트를 회귀(regression) 테스트라고 한다.
회귀 테스트의 범위가 커지는 문제를 해결하려면 한 클래스에 너무 많은 책임을 부여하지 말고 단 하나의 책임만 수행하도록 해 변경 사유가 될 수 있는 것을 하나로 만들어야 한다. 이를 책임 분리라고 한다.
2. 개방-폐쇄 원칙
OCP: Open-Closed Principle
확장에는 개방되고 수정에는 닫혀야 한다는 원칙
기존 코드를 변경하지 않으면서 기능을 추가할 수 있도록 설계가 되어야 한다는 뜻이다.
OCP가 위반된 코드를 예시로 작성해보았다.
학생의 정보를 기록하는 출석부(Attendance)와 성적표(GradeCard) 클래스가 있다. Printer 클래스는 출석부와 성적표 클래스를 매개변수로 받아 정보를 출력한다.
<출석부>
/**
* 출석부
*/
public class Attendance {
private String attendanceYn;
public Attendance(String attendanceYn) {
this.attendanceYn = attendanceYn;
}
public void print() {
System.out.println("attendance is " + attendanceYn);
}
}
<성적표>
/**
* 성적표
*/
public class GradeCard {
private long grade;
public GradeCard(long grade) {
this.grade = grade;
}
public void print() {
System.out.println("grade is " + grade);
}
}
<프린터>
/**
* 학생 정보를 출력하는 클래스
* 성적표, 출석부 등
*/
public class Printer {
public void printAttendance(Attendance attendance) {
attendance.print();
}
public void printGradeCard(GradeCard gradeCard) {
gradeCard.print();
}
}
<메인>
public class Main {
public static void main(String[] args) {
Printer printer = new Printer();
// 프린터로 출석부 정보를 출력
Attendance attendance = new Attendance("Y");
printer.printAttendance(attendance);
// 프린터로 성적표 정보를 출력
GradeCard gradeCard = new GradeCard(80);
printer.printGradeCard(gradeCard);
}
}
기존에 학생 정보는 출석부만 있었고 프린터에서는 출석부 정보만 출력했다고 하자.
그런데 이후 성적표 정보가 생겼고 프린터에는 성적표 클래스를 매개변수로 받아 출력하는 메소드가 추가된 것이다.
이런 상황이라면 출력해야할 다른 정보가 생길때 마다 프린터 클래스에는 새로운 메소드가 추가되면서 기존 코드가 변경될 것이다.
인터페이스를 추가하여 OCP를 지키는 코드를 살펴보자.
<학생 정보 인터페이스>
public interface StudentInfo {
void print();
}
<출석부>
/**
* 출석부
*/
public class Attendance implements StudentInfo {
private String attendanceYn;
public Attendance(String attendanceYn) {
this.attendanceYn = attendanceYn;
}
@Override
public void print() {
System.out.println("attendance is " + attendanceYn);
}
}
<성적표>
/**
* 성적표
*/
public class GradeCard implements StudentInfo{
private long grade;
public GradeCard(long grade) {
this.grade = grade;
}
@Override
public void print() {
System.out.println("grade is " + grade);
}
}
<프린터>
/**
* 학생 정보를 출력하는 클래스
* 성적표, 출석부 등
*/
public class Printer {
public void print(StudentInfo studentInfo) {
studentInfo.print();
}
}
<메인>
public class Main {
public static void main(String[] args) {
Printer printer = new Printer();
Attendance attendance = new Attendance("Y");
printer.print(attendance);
GradeCard gradeCard = new GradeCard(80);
printer.print(gradeCard);
}
}
인터페이스에서 구체적인 정보 매체는 캡슐화하였다. 이제는 새로운 학생 정보가 추가 되어도 프린터 클래스의 코드를 수정하지 않아도 된다.
3. 리스코프 치환 원칙
LSP: Liskov Subsituation Principle
부모 클래스와 자식 클래스 사이의 행위가 일관성이 있어야 한다는 원칙
즉, 부모 클래스의 인스턴스를 자식 클래스의 인스턴스로 대신할 수 있어야 한다.
LSP가 잘 지켜진 코드를 살펴보자.
가방(Bag) 클래스가 있고 이를 상속 받은 할인된 가방(DiscountedBag) 클래스가 있다고 하자.
<Bag>
가방 클래스는 가격을 설정(setPrice)하고 가격을 조회(getPrice)하는 기능이 있다.
메소드를 보면 설정된 가격 그대로 조회되는 것을 알 수 있다.
- 설정된 가격 == 조회되는 가격
public class Bag {
private int price;
public void setPrice(int price) {
this.price = price;
}
public int getPrice() {
return price;
}
}
<DiscountedBag>
Bag 클래스를 상속받아 DiscountedBag 클래스를 생성했다.
할인율 설정(setDiscountedRate)과 가격에 할인율 적용(applyDiscount) 기능이 추가되었지만 Bag의 기존 기능은 재정의 하지 않았다.
이런 경우 DiscountedBag 클래스가 Bag 클래스의 기능을 일관성 있게 수행할 수 있다.
public class DiscountedBag extends Bag {
private double discountedRate = 0;
public void setDiscountedRate(double discountedRate) {
this.discountedRate = discountedRate;
}
public void applyDiscount(int price) {
super.setPrice(price - (int) (discountedRate * price));
}
}
이번에는 LSP를 위반한 코드를 살펴보자.
DiscountedBag 클래스에서 Bag 클래스의 setPrice 메서드를 오버라이드하였다.
public class DiscountedBag extends Bag {
private double discountedRate = 0;
public void setDiscountedRate(double discountedRate) {
this.discountedRate = discountedRate;
}
public void applyDiscount(int price) {
super.setPrice(price - (int) (discountedRate * price));
}
// setPrice 재정의!
public void setPrice(int price) {
super.setPrice(price - (int)(discountedRate * price));
}
}
더이상 (설정된 가격 == 조회되는 가격)을 만족하지 않는다.
즉, DiscountedBag에서 setPrice를 재정의하여 Bag 클래스의 행위와 일치하지 않으므로 LSP를 만족하지 않는다.
LSP를 만족시키는 가장 간단한 방법은 재정의를 하지 않는 것이다.
어느 방향으로든 정상 동작하도록 코드를 수정할 수 있겠지만 중요한 개념은, 자식 클래스는 최소한 자신의 부모 클래스에서 가능한 행위는 수행할 수 있어야 한다는 것이다.
4. 의존 역전 원칙
DIP: Dependency Inversion Principle
의존 관계를 맺을 때 변화하기 쉬운 것 또는 자주 변화하는 것보다는 변화하기 어려운 것, 거의 변화가 없는 것에 의존하라는 원칙
한가지 예시를 보자.
아이가 장난감을 가지고 논다. 어떤 경우에는 로봇을, 어떤 경우에는 자동차 장난감을 가지고 논다.
아이가 실제 가지고 노는 구체적인 장난감의 종류는 변하기 쉽지만 아이가 장난감을 가지고 논다는 사실을 변하기 어려운 것이다.
객체지향 관점에서는 이와 같이 변하기 어려운 추상적인 것들을 표현하는 수단으로 추상 클래스와 인터페이스가 있다. DIP를 만족하려면 어떤 클래스가 도움을 받을 때 구체적인 클래스보다는 추상클래스나 인터페이스와 의존 관계를 맺도록 설계해야 한다. DIP를 만족하는 설계는 변화에 유현한 시스템이 된다.
5. 인터페이스 분리 원칙
ISP: Interface Segregation Principle
클라이언트 자신이 이용하지 않는 기능에는 영향을 받지 않아야 한다는 원칙
인터페이스를 클라이언트에 특화되도록 분리시키라는 설계 원칙
예시로 아래 그림와 같은 프린터, 팩스, 복사기 기능이 모두 포함된 복합기를 생각해보자.
복합기 기능을 제공하는 클래스는 매우 비대해질 가능성이 크다.
문제는 이 클래스의 모든 기능을 사용하는 클라이언트는 거의 없다라는 것이다.
클라이언트의 필요에 따라 복사 기능만 사용한다던지 팩스 기능만 사용할 수 있다.
따라서 프린트 기능만 이용하는 클라이언트가 팩스 기능 변경으로 인해 발생하는 문제에 영향을 받지 않도록 해야 한다.
클라이언트와 무관하게 발생한 변화로 클라이언트 자신이 영향을 받지 않으려면 범용의 인터페이스보다는 클라이언트에 특화된 인터페이스를 사용해야 한다.
복사기 클래스에 ISP 원칙을 적용한 그림이다.
이렇게 설계하면 클라이언트는 자신이 사용하지 않는 기능 변화에 의한 영향을 받지 않게 된다.
'개발 > ETC' 카테고리의 다른 글
IntelliJ 프로젝트 github에 올리기 (0) | 2021.07.31 |
---|---|
curl과 wget (1) | 2021.01.22 |