SOLID 원칙이란?
객체지향 프로그래밍 설계의 다섯가지 기본 원칙으로 높은 응집도와 낮은 결합도 확장성 있는 시스템을 갖추도록 함
- SRP(Single Responsibility Principle) : 단일 책임 원칙
- OCP(Open/Close Principle) : 개방/폐쇄 원칙
- LSP(Liskov Substitute Principle) : 리스코프 치환 원칙
- ISP(Interface Segregation Principle) : 인터페이스 분리 원칙
- DIP(Dependency Inversion Principle) : 의존성 역전 원칙
1. SRP(Single Responsibility Principle) : 단일 책임 원칙
- 모듈이나 클래스는 하나의 책임을 가져야 한다
- 클래스를 변경해야하는 이유는 오직 하나 뿐이어야 한다. (하나의 변경 원인만 가져아한다.)
ex) 출력과 편집을 제공하는 Docs class
Class Docs {
Page page = new Page;
public Report printReport() {}
public Report editReport() {}
}
위 클래스는 문서편집기가 제공해야하는 기능을 제공하는 책임을 담당하고 있다.
언뜻 보면 문서편집기를 하나의 덩어리로 본다면 단일 책임 원칙을 잘 지킨 패턴이라고 볼 수 있을수 도 있을 것 같다.
위 클래스에 변경 가능한 부분을 생각해보면 편집 형식(페이지 번호, 정렬, 페이지 방향 등)이 있을 수 있고,
출력 형식(홀수/짝수 페이지만, 특정 페이지만, 출력 용지 규격, 페이지 방향 등)이 있을 수 있다.
만약, 출력창에서 가로/세로 페이지 형식을 바꿔 출력하는 기능을 추가하기 위해
Class Docs {
Page page = new Page;
public Report printReport(boolean isPageVertical) {
page.setPageDir(isPageVertical);
}
public Report editReport() {}
}
이와 같이 소스코드를 수정하고 기능을 사용하였더니 출력창에서만 페이지의 가로/세로가 변경된 것이 아니라
편집창에서도 가로/세로가 변경되는 현상이 나타날 수 있다.
Page 객체를 편집기능과 출력기능에 공유하니 출력에서만 가로/세로를 변경하는게 아니라 편집창에서까지 이어졌다.
따라서 아래와 같이
Class DocsPrinter {
Page page = new Page;
public Report printReport(boolean isPageVertical) {
page.setPageDir(isPageVertical);
}
}
Class DocsEditor {
Page page = new Page;
public Report editReport() {}
}
문서 출력의 책임과 문서 편집의 책임을 분리하여 설계한다면 기존 설계 방식보다 영향도를 줄이며 변경에 대응할 수 있을 것이다.
[SRP의 목표]
SRP는 높은 응집도를 요구하는 원칙이다.
따라서 SRP를 이루게 되면 높은 응집도를 통해 얻을 수 있는 코드 중복 제거, 코드 가독성 향상, 변경사항에 대한 처리를 한 곳에 집중화할 수 있는 장점을 얻을 수 있다.
SRP는 클래스에 적용되는 원칙이기에 클래스가 갖고 있는 책임에 대해 명확하게 정의하는 것이 중요하다.
책임에 대한 명확한 정의가 이루어져야 불필요한 구성요소들을 제거하고 꼭 필요한 내부 구성요소들만 위치 시킴으로써 높은 응집도를 얻을 수 있다.
하나의 책임을 갖는다고 해서 메서드 단위로 클래스를 분리시키거나 클래스를 작게 쪼갠다고 해서 SRP를 이룬 것은 아니다.
이는 오히려 응집도를 낮출 수 있다.
2. OCP(Open/Close Principle) : 개방/폐쇄 원칙
- 기존의 코드를 수정하지 않고 기능을 확장할 수 있어야 한다.
- 기존 기능 수정에 대해서는 폐쇄적이고 확장에 대해서는 열려있는 구조
ex) JDBC : DB의 종류가 추가되어도 기존 DB 연결에 영향을 주지 않고 새롭게 확장 시켜 나갈 수 있음
인터페이스 DatabaseConnector
interface DatabaseConnector {
Connection connect() throws SQLException;
}
클래스 MysqlConnection
class MySqlConnection implements DatabaseConnector {
private static final String URL = "jdbc:mysql://localhost:3306/your_database";
private static final String USER = "your_username";
private static final String PASSWORD = "your_password";
@Override
public Connection connect() throws SQLException {
return DriverManager.getConnection(URL, USER, PASSWORD);
}
}
클래스 OracleConnection
class OracleConnection implements DatabaseConnector {
private static final String URL = "jdbc:oracle:thin:@localhost:1521:your_sid";
private static final String USER = "your_username";
private static final String PASSWORD = "your_password";
@Override
public Connection connect() throws SQLException {
return DriverManager.getConnection(URL, USER, PASSWORD);
}
}
위와 같이 클래스를 설계하고 각 DB에 맞게 커넥터를 연결하여 아래와 같이 사용한다고 할 때
public class DbConnect {
public Connection connectDb(DatabaseConnector connector) {
//데이터베이스에 연결
Connection conn = connector.connect();
return conn;
}
}
새로운 DatabaseConnector가 추가 되어도 기존 소스인 DbConnect의 변경 발생 없이 사용이 가능하다.
[OCP의 목표]
OCP는 확장성 있는 시스템을 갖추도록 한다.
추상화, 상속, 다형성을 통해 OCP를 적용할 수 있다.
추상 자료형에 하위에 다양한 기능을 갖춘 구현체를 제공함으로써 기존 기능에 영향을 미치지 않고 새로운 기능을 확장함으로써 안정적인 시스템 운영과 확장성있는 구조를 갖추게 할 수 있다.
이를 통해 재사용성도 향상되고 변경사항에 유연하게 대응할 수 있다.
3. LSP(Liskov Substitute Principle) : 리스코프 치환 원칙
- 부모 클래스의 인스턴스는 자식 클래스의 인스턴스로 교체될 수 있어야 한다.
- 자식객체의 확장시 부모객체가 의도한 대로 오버라이딩 이루어져야함(올바른 상속)
- 상속, 다형성과 가장 깊은 관계가 있다.
ex) 직사각형과 정사각형의 상속 관계
Rectangle 클래스
public class Rectangle {
public int width;
public int 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;
}
}
Square 클래스
public class Square extends Rectangle{
@Override
public void setWidth(int Width) {
super.setWidth(width);
super.setHeight(getWidth());
}
@Override
public void setHeight(int height) {
super.setHeight(height);
super.setWidth(getHeight());
}
}
하위 클래스의 인스턴스를 상위클래스 인스턴스로 대체했을 때 너비 구하기
public class Main
{
public static void main(String[] args)
{
Rectangle rectangle = new Rectangle();
rectangle.setWidth(10);
rectangle.setHeight(5);
System.out.println(rectangle.getArea());
rectangle = new Square(); //하위 클래스 인스턴스인 정사각형으로 대체
rectangle.setWidth(10);
rectangle.setHeight(5);
System.out.println(rectangle.getArea());
}
}
output
50
25
리스코프 치환 원칙에 의하면 부모 객체를 호출하는 동작에서 자식 객체가 부모객체를 완전히 대체 할 수 있어야한다.
그런데 지금 자식 객체로 대체하니 결과가 50이 아닌 25가 나왔다.
자식 객체가 부모 객체를 완전히 대체하지 못하였다.
이는, 정사각형과 직사각형이 올바른 상속관계가 아니라는 것을 의미한다.
why?. 사각형의 성질로는 정사각형과 직사각형이 상속관계를 이루는데 왜 올바른 상속관계가 아닌가?
직사각형의 소스코드를 보면 width와 height가 각각 존재한다.
하지만 정사각형은 width와 height가 각각 별도로 존재할 필요가 없다.
하지만 상속을 받게 되면 부모 클래스의 성질을 그대로 가져오기 때문에 정사각형 클래스는 마치 직사각형처럼 width와 height가 별도로 각각 존재하는 사각형이 되어버렸다.
그렇다면 제대로된 설계는?
상위타입으로 Shape 인터페이스 정의
interface Shape {
int getArea();
}
Rectangle 클래스에서 Shape 인터페이스를 상속받아 getArea 오버라이딩
public class Rectangle implements Shape {
public int width;
public int height;
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;
}
@Override
public int getArea() {
return width * height;
}
}
Square 클래스에서 Shape 인터페이스를 상속받아 getArea 오버라이딩
public class Square implements Shape {
public int length;
public Square(int length){
this.length=length;
}
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
@Override
public int getArea() {
return length * length;
}
}
위와 같이 설계하고 도형의 넓이를 구해보면
public class Main
{
public static void main(String[] args)
{
Shape shape = new Rectangle(10, 5);
System.out.println(shape.getArea());
shape = new Square(5); //하위 클래스 인스턴스인 정사각형으로 대체
System.out.println(shape.getArea());
}
}
output
50
25
이제 Rectangle과 Square는 상속관계가 아니므로 결과가 다르다고 해서 LSP에 위반됬다고 생각할게 아니라
Shape의 getArea가 의도한 각 구현체의 넓이를 올바르게 구했으므로 LSP에 위배되지 않다고 볼 수 있다.
[LSP의 목표]
LSP는 상속을 올바르게 하자는 원칙이다.
LSP는 상속과 다형성을 통해 표현될 수 있으며 LSP를 지키면 확장성 있는 시스템 구조를 얻을 수 있다.
상위 타입으로 다양한 하위 타입 구현체의 행위를 처리함으로써 유연하고 확장성 있는 구조를 갖출 수 있다.
4. ISP(Interface Segregation Principle) : 인터페이스 분리 원칙
- 몇개의 큰 인터페이스가 있는 편 보다는 작은 인터페이스가 많은 편이 바람직하다.
- 인터페이스를 클라이언트를 기준으로 분리하여 이용하지 않는 메서드에 의존하지 않아야 한다는 원칙
ex) 자동차 인터페이스
interface Car {
public void drive() ;
public void reverse();
public Gas getFuelState();
public void fillFuel(Gas gas);
}
위의 자동차 인터페이스를 보면 전진,후진, 연료량 확인, 연료 채우기라는 자동차가 가져야하는 기능을 갖추고 있다.
그래서 아래와 같은 자동차의 상속 관계를 만들었다.
디젤, 가솔린, LPG 자동차 모두 Car 인터페이스를 통해 자동차가 갖춰야할 기능들을 모두 갖추게 되었다.
허나, 전기차의 등장 이후 getFuelState, fillFuel 기능이 필요가 없어졌다.
전기차는 내연기관 자동차와 달리 연료 주입이 아닌 배터리 충전이다.
Gas를 리턴값이나 매개변수로 갖는 연료 충전, 연료 상태를 확인하는 메서드를 전기차에서는 사용하지 못한다.
만약 위와 같은 구조에서 전기차를 구현한다면 아래와 같을 것이다.
class ElectricCar implements Car {
public void drive() {
//drive 로직...
}
public void reverse() {
//reverse 로직...
}
public Gas getFuelState() {
return null;
}
public void fillFuel(Gas gas) {}
public void chargingBattery(Battery battery) {
//배터리 충전
}
public Battery getBatteryState() {
//배터리 상태
}
public Battery getBatteryLife() {
//배터리 수명
}
public Battery changeBattery() {
//배터리 교체
}
}
위와 같이 getFuelState, fillFuel을 반드시 구현해야하므로 메서드를 비워놓고 배터리와 관련된 메서드를 추가할 것이다.
위 클래스에서 보듯이 전기차는 사용하지도 않는 getFuelState, fillFuel을 구현해야하므로 번거로움이 생긴다.
만약 더 많은 메서드가 존재했다면 더 큰 버건로움이 생겼을 것이다.
[인터페이스 분리 원칙을 준수한 설계]
interface CarOperate {
public void drive() ;
public void reverse();
}
interface GasEngine {
public Gas getFuelState();
public void fillFuel(Gas gas);
}
interface BatteryEngine {
public void chargingBattery(Battery battery);
public Battery getBatteryState();
public Battery getBatteryLife();
public Battery changeBattery();
}
위와 같이 자동차 조작 인터페이스와 연료 충전 인터페이스를 분리하여 설계한다면
내연기관 자동차와 전기 자동차가 갖는 서로 다른 속성으로 인해 불필요한 메서드 강제 오버라이딩을 막을 수 있다.
따라서 아래와 같이 getFuelState, fillFuel 없이 전기차 충전 기능만 오버라이딩 할 수 있다.
class ElectricCar implements CarOperator, BatteryEngine {
public void drive() {
//drive 로직...
}
public void reverse() {
//reverse 로직...
}
public void chargingBattery(Battery battery) {
//배터리 충전
}
public Battery getBatteryState() {
//배터리 상태
}
public Battery getBatteryLife() {
//배터리 수명
}
public Battery changeBattery() {
//배터리 교체
}
}
[ISP의 목표]
ISP는 낮을 결합도를 요구하는 원칙이다.
클라이언트가 사용하지 않는 추상 메서드에는 의존하지 않도록 하고,
작게 분리된 인터페이스 중 필요한 인터페이스만 의존하도록 하여 결합도를 느슨하게 만들 수 있다.
상속을 통해 필요한 기능을 복제 받는 것은 컴파일 타임에 코드를 묶어 버려 결합도를 상승시키는 구조를 만들지만,
인터페이스를 통해 필요한 기능만 주입 받는 방식(합성)을 취하면 코드 재사용성도 높아질 수 있다.
5. DIP(Dependency Inversion Principle) : 의존성 역전 원칙
- 상위 모듈은 하위 모듈에 의존해서는 안된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야 한다.
- 추상화는 세부 사항에 의존해서는 안된다. 세부사항이 추상화에 의존해야 한다.
ex) 본인인증 시스템
회원가입을 위해 사용자 정보를 인증하는 시스템이 있다고 하자 사용자는 신분증 정보를 입력하여 인증할 수 있다.
따라서 우리는 아래와 같은 메서드를 통해 사용자 인증을 진행할 수 있다.
class UserAuthenticate {
public boolean authenticateUser(IdCard idCard) {
boolean isAuthenticate = false;
//사용자 인증로직 ...
return isAuthenticate;
}
}
이때 사용자의 요구사항이 추가되서 여권이나 운전면허증으로도 인증이 가능해지도록 구현해야하는 상황이 생겼다.
따라서 우리는 아래와 같이 두가지의 메서드를 추가하였다.
class UserAuthenticate {
public boolean authenticateUser(IdCard idCard) {
boolean isAuthenticate = false;
//사용자 인증로직 ...
return isAuthenticate;
}
public boolean authenticateUser(Passport passport) {
boolean isAuthenticate = false;
//사용자 인증로직 ...
return isAuthenticate;
}
public boolean authenticateUser(DriverCard driverCard) {
boolean isAuthenticate = false;
//사용자 인증로직 ...
return isAuthenticate;
}
}
신분증, 여권, 운전면허증이라는 매개변수마다 동일한 로직을 가진 메서드를 갖게 되었다.
이는 유지보수 입장에서도 굉장히 비효율적이다.
만약 다른 인증 수단이 추가된다면 그때마다 메서드를 추가해야하고
메서드가 수정된다면 모든 메서드를 수정해야하는 상황이 생긴다.
따라서 아래와 같은 관계에서 메서드의 매개변수를 상위 타입인 Credntial로 사용한다면
class UserAuthenticate {
public boolean authenticateUser(Credential credential) {
boolean isAuthenticate = false;
//사용자 인증로직 ...
return isAuthenticate;
}
}
사용자 인증수단마다 메서드를 추가할 필요없이 기존의 메서드로 사용이 가능하게 된다.
[DIP의 목표]
DIP는 낮은 결합도를 요구하는 원칙이다.
DIP를 통해 의존관계를 설정하게 되면 런타임에 의존관계를 변경할 수 있는 유연한 구조를 갖추게 한다.또한 테스트 더블로 쉽게 대체할 수 있는 구조를 갖게 하여 테스트를 용이하게 만든다는 장점 또한 존재한다.
'Language > 자바&코틀린' 카테고리의 다른 글
컴파일 타임 의존성과 런타임 의존성 (0) | 2024.02.11 |
---|---|
불변(Immutable)객체 (0) | 2023.11.15 |
인터페이스와 추상클래스의 차이 (0) | 2023.11.15 |
java.lang.OutOfMemoryError: Java heap space 오류 해결 (0) | 2021.01.14 |
[자바]JVM의 동작방식과 구조 (0) | 2021.01.11 |