[OOP] 객체지향 설계의 5원칙 SOLID

황제연·2024년 3월 22일
0

CS학습

목록 보기
2/193

학습을 목적으로 여러 자료를 찾아보면서 정리했습니다. 혹여나 틀린 내용이 있다면 댓글로 말해주세요!

객체 지향 설계의 5원칙 SOLID

SOLID 원칙이란?

  • 객체 지향 설계에서 지켜야할 5개의 소프트웨어 개발 원칙을 말한다

SRP (Single Responsibility Principle)

단일 책임 원칙

OCP (Open-Closed Principle)

개방-폐쇄 원칙

LSP (Liskov Subsitution Principle)

리스코프 치환 원칙

ISP (Interface Segregation Principle)

인터페이스 분리 원칙

DIP (Dependency Inversion Principle)

의존 역전 원칙

좋은 소프트웨어란?

  • 좋은 소프트웨어는 결국 변화되는 요구에 잘 대응하는 것이라고 할 수 있다
  • SOLID 원칙은 좋은 객체지향 소프트웨어 설계를 하도록 도움을 주며,
    이 원칙을 따라 객체지향 소프트웨어를 설계한다면 좋은 소프트웨어의 모습을 갖출 것이다

SOLID 원칙을 모두 지켜야 하는가?

  • 모든 원칙을 지키는 것은 프로젝트의 상황에 따라 다르다.
  • 각 원칙은 특정 문제를 해결하기 위한 지침일 뿐이며, 해당 프로그램에 큰 문제가 없다면 원칙을 칼같이 적용할 이유는 없다
  • 하지만 이 원칙을 지킨다면 객체지향 소프트웨어 설계와 유지보수에서 더 편리함을 얻는다는 장점을 갖추게 되고, 따라서 더 좋은 객체지향 소프트웨어가 된다는 것은 명백하다

각 SOLID 원칙 세부 내용

SRP 단일 책임 원칙

개념:

하나의 클래스는 하나의 책임만 가져야한다

  • 코드 설계에 있어서 하나의 클래스가 여러가지 기능을 갖게 되면 유지보수에 있어서
    마이너스적인 측면이 많다

문제 예시상황

  • 한가지 기능을 추가하거나 삭제할때, 그와 연관된 모든 부분을 수정해야합니다
class knife(){
	int cut(int size){
    	return size;
    }
}

이런 기능을 사용한다고 했을 때..

class knife(){
	int cut(long size, LocalDateTime when){
    	return size;
    }
}

-> 이런식으로 수정을 해버리면 이 클래스 메소드를 사용하는 모든 연관 요소들은 다 수정해야한다

  • 또한 한가지 기능을 수정했을 때, 연쇄적으로 연관된 기능에서 버그가 발생할 수 있다

SRP 원칙을 지켰다면..

  • 위 예시와 같은 상황이 발생하지 않았을 것이고, 해당 기능 하나만 지닌 클래스만 수정해주면 되어서 유지보수에 있어서 큰 이점을 갖게 된다.
  • 지금은 작은 크기의 예제로 확인을 하지만 더 큰 시스템을 만나게 될때, (특히 여러 의존관계가 섞이는 스프링 프로젝트를 할때) SRP의 중요성을 크게 체감하게 될 것이다.

"책임" 개념 이해의 주의점

  • 여기서 말하는 책임이란 클래스가 존재하는 목적으로 보면 이해가 쉽다
  • 예를들어 비밀번호 처리에 대한 클래스가 있는데, 여기에서 아이디 처리에 대한 로직까지 들어가게 된다면 SRP를 어긴 것이다.
  • 하지만 다른 비밀번호 로직 방식이 단순히 추가된다면 그것은 SRP를 어긴 것이 아니라 기능의 범위가 확장되었다고 봐야한다.

정리

  • 한 클래스는 한가지 책임을 갖도록 설계하자!

참고

  • 스프링의 싱글톤 패턴과 SRP가 이어지므로, 추후에 싱글톤 패턴도 학습 후 정리하겠습니다

OCP 개방 폐쇄 원칙

개념:

하나의 클래스는 확장에는 개방적이고, 수정에 대해서는 폐쇄적이어야한다

확장에 열려있다

  • 새로운 변경 사항이 생겼을 때, 코드를 유연하게 추가하면서 큰 비용없이 기능을 확장할 수 있다는 것을 의미한다

수정에 닫혀있다

  • 객체를 직접적으로 수정하는 것을 제한해야한다는 의미이다
  • 객체를 직접적으로 수정하지 않고도 변경사항을 적용할 수 있도록 설계해야한다

자바의 추상화 개념을 떠올리면 이해하기 쉽다

어떻게 OCP를 지킬까?

  • 변경이 되는 부분을 추상화해서 분리하고, 특정 구현에 의존하는 것이 아닌 바로 이 추상화된 인터페이스에 의존하도록 한다

OCP를 이용한 JDBC

  • SQL언어는 DBMS마다 다르다 Oracle과 Mysql에는 비슷하지만 다른 문법이 분명 존재한다
  • 각각 DB에 맞는 언어를 모두 구현해야한다면 정말 골치아플 것이다.
  • 이러한 골치아픈 것을 해결하는 것이 바로 JDBC이다
  • JDBC에는 각 데이터베이스 드라이버가 추상화되어있으며, 사용되는 DB 설정 정보에 맞춰 해당 드라이버가 구현되어 사용된다

OCP를 지켜서 설계를 하면?

  • 수정사항이 생기더라도 크게 코드를 수정하지 않으면서 기능을 확장할 수 있다.
    -> MYSQL에서 ORACLE로 바꾼다고 해도 JDBC에는 ORACLE 구현체가 있기 때문에, 설정정보만 바꾸면 된다.
    -> 즉 코드 관리를 더 용이하게 할 수 있어 유지보수 측면에서 큰 이점을 얻게 된다

OCP 설계 주의점

  • OCP를 잘 지키려면 추상화를 잘 설계하면 된다.
  • 이때 처음 정의하는 추상화(추상 클래스 또는 인터페이스)를 잘 정의해야한다.
  • 처음 정의할 때, 추상화를 잘 정의하지 않는다면 상속관계라든지, 내용 추가에 있어서 문제가 생기게 되고, 이후 다루게 될 LSP나 ISP 위반으로 이어질 수도 있다.

LSP 리스코프 치환 원칙

개념:

자식 클래스는 부모 클래스의 행동 규약을 위반하면 안된다
즉, 자식 클래스는 부모 클래스에서 가능한 기능을 모두 수행할 수 있어야 한다는 의미이다
이것은 다형성을 위한 원칙이다

예시

  • 자바의 컬렉션 프레임워크가 바로 이 LSP을 잘 지킨 예시이다.

    내가 코딩테스트 문제를 풀 때 주로 사용하는 방식이다
List<?> lists = new ArrayList<>(); 
  • 위에서 내가 LinkedList로 바꾸더라도 탐색되는 방법에 조금 차이가 있을지라도 큰 변화는 없게 된다.
  • LSP, 즉 다형성을 잘 지켰기 때문에 이렇게 사용할 수 있는 것이다.

LSP을 위반하는 경우

다음 세가지 경우에서 LSP를 위반하게 된다.

  • 메소드 오버라이딩을 할때, 자식 메소드에서 부모 메소드에서 지정한 형식이 아닌 다른 형식으로 지정하지 않도록 해야한다

예시

class people{
	int sleep(int time){
    	return 0;
    }
}
class sleepyPeople extends people{
	void sleep(int time, int wakeCount){
    		...
    }
}

-> 부모클래스에 없는 파라미터와 반환 타입을 지정하면 안된다.

  • 부모 클래스에서 지정한 의도와 다르게 메소드 오버라이딩을 할 때 발생한다
class people{
	void getGender(){}
}
class man extends people{
	String getGender(){
    	return "남자";
    }
}

-> 정상적인 상속이다

class woman extends people{
	String getGender(){
    	return null;
    }
}

-> 부모 클래스에서 지정한 의도와 다르게 null을 반환하게 된다..

  • 잘못된 상속관계 구성으로 인한 메소드 정의에서도 발생할 수 있다
class people{
	void speak(){}
}
class man extends people{
	void speak(){
    	System.out.println("hi");
    }
}

-> 정상적인 상속 관계이다
하지만 다음과 같은 경우는 잘못된 상속관계이다

class muteMan extends people{
	void speak(){
   		try{
        	throw new Exception("벙어리는 말을 할 수 없습니다.");
        }catch (Exception e){
        	e.printStackTrace();
        }
    }
}

-> 혼자 개발한다면 상관없지만 협업하는 과정에서 이렇게 된다면....
물론 클래스 개수가 적다면 상관없겠지만 상속받은 사람의 종류가 수천개가 된다면 문제가 발생하게 된다

잘 작동하던 코드가 예외가 뻥~하고 터져 버리면 의도치 않은 결과를 낳게 된다..

다형성에 대해 잘 알고 있다면...

  • 다형성에 대해 잘 알고 있다면 위 위반 사례는 알아서 컴파일 에러가 터질 것임을 짐작할 수 있다
  • 그리고 당연히 다형성이라는 개념이 머리속에 들어와 있다면 저렇게 하면 안된다는 것도 알고 있을 것이다.

정리

  • LSP는 다형성 설계를 위한 원칙이다. 다형성을 잘 알고 있다면, LSP이해에 큰 무리는 없을 것이다.

ISP 인터페이스 분리 원칙

개념

인터페이스는 각각 그 사용에 맞게 잘 분리해야한다

SRP와 비교

SRP: 클래스의 단일 책임을 강조
ISP: 인터페이스의 단일 책임을 강조

하나의 거대한 인터페이스

  • 하나의 인터페이스에 추상 메소드를 다 몰아 넣는다면 이것을 사용하지 않는 구현 클래스가 생길 수 있다. 전혀 사용하지 않는데도, 이런 경우 구현해야한다..
  • 수정이 생기면 전혀 사용하지 않는 클래스도 위와 같은 이유로 수정해야한다

ISP의 목표

  • 따라서 인터페이스를 특정 기준에 따라 분리해서 각 클래스에 맞는 인터페이스로 나눠야한다
  • ISP는 이러한 클라이언트의 목적과 용도에 적합한 인터페이스 만을 제공하는 것이 목표이다

예시

스프링 부트 버전을 예시로 들어보자

interface springboot{
	void springbean()
    void usejakarta()
}

이러한 기능을 가진 스프링 부트 인터페이스가 있다고 가정을 해보자

스프링 부트 3.0에서는 위 조건을 만족한다

class springboot3 implements springboot{
	void springbean(){기능 구현...}
    void usejakarta(){기능 구현...}
}

하지만 스프링 부트 2.0에서는 위 조건을 만족하지 못한다. 2.0에서는 명칭이 jakarta가 아닌 javax였기 때문에 구현하려면 다음과 같이 쓸때없는 짓을 해야한다

class springboot2 implements springboot{
	void springbean(){기능 구현...}
    void usejakarta(){Systetm.out.println("지원하지 않는 기능입니다.")}
}

예시 해결방법

void usejakarta를 인터페이스로 분리하자

interface springboot{
	void springbean()
}
interface jakarta{
	어쩌구 저쩌구...
}
class springboot3 implements springboot, jakarta{
}
class springboot2, implements springboot{
}

이렇게 인터페이스를 분리해서 스프링부트3에만 분리한 인터페이스를 적용하였고 ISP를 지킬 수 있게 되었다.

인터페이스 분리는 여러번 말고 한번만!

  • ISP는 한번 인터페이스를 분리해서 구성해놓고나서 굳이 인터페이스를 더 잘게 분리하는 행위는 하지 말라고 한다.
  • 기껏 다 설계해놨는데, 인터페이스에서 기능을 더 분리할 수 있을 것 같아 더 잘게 쪼개는 행위를 지양하라는 것이다.
    -> 너무 과도한 분리는 거대한 크기를 만들게 된다.
  • 인터페이스란 한번 구성했으면 어지간해서는 변하면 안되는 정책같은 개념이다.
  • 따라서 변경 사항까지 고려해서 처음 인터페이스 분리 설계를 잘해야한다.

DIP 의존 역전 원칙

개념

어떤 클래스를 참조해야하는 상황이라면 그 클래스를 직접 참조하지말고, 그 상위 요소인 인터페이스나 추상 클래스를 참조해라

왜 그 상위 요소인 추상적인 요소를 참조해야할까?

  • 하위 요소를 참조하게 되면, 이 하위요소는 클라이언트에 의존하게 된다
  • 만약 클라이언트 요청에 따라 하위 요소가 변하게 된다면 상위 요소를 자주 수정해야한다
  • 따라서 상위의 인터페이스 타입의 객체로 통신해야한다

스프링에서의 DIP

JDBC 드라이버

이후 스프링 부트 학습하면서 DB 연결과정에서 보게될 그림이다

  • 앞서 OCP에서 말한 내용은 생략하겠다.
  • 애플리케이션 로직 입장에서는 DB 드라이버를 볼 이유도 없고, 수정하지 않는 편이 더 좋다.
  • 만약 DIP를 위배해서 MYSQL 드라이버를 애플리케이션 로직이 바로 참조했는데, Oracle 드라이버로 참조를 바꾼다면, 관련 코드를 모두 수정해야한다.
  • 하지만 JDBC 표준 인터페이스를 참조해서 그 추상화된 기능만 사용한다면, JDBC 표준 인터페이스에 사용하는 기능에만 의존하면 되어서 개발에 있어서 더 이점을 얻을 수 있다.

정리:

  • DIP는 앞으로 스프링 학습 및 정리를 하면서 여러 그림에서 자주 보게될 것이다.

참고자료:

profile
Software Developer

0개의 댓글