Java/스프링 입문을 위한 자바 객체 지향의 원리와 이해

Ch 05. 객체 지향 설계 5원칙 SOLID

계란💕 2023. 12. 1. 17:43
객체 지향 설계의 원칙,  SOLID 에 대해  각 예시와 함께 살펴보려고 한다.
평소에 자주 사용하는 Java, JDBC 도 SOLID와 관련되어 있다. 

 


SRP (Single Responsibility Principle, 단일 책임원칙)

  • 단일책임원칙이란? 하나의 클래스는 하나의 역할만 수행해야한다는 의미.
  • "어떤 클래스를 변경해야하는 이유는 오직 하나뿐이어야한다."

 

 

  Ex 1) SRP를 만족하는 설계

  • "남자" 클래스는 다음 네 가지 역할을 하고 다음 8개의 메서드로 구성된다. 
    • 남자친구 역할 - 기념일 챙기기(), 데이트하기()
    • 아들 역할 - 효도하기(), 안마하기
    • 사원 역할 - 출근하기(), 아부하기()"
    • 소대원 역할 - 사격하기(), 구보하기()
  • 이를 SRP에 맞게 바꾸려면?
    • 남자클래스 안에서의 역할과 책임이 많다. 
    • 따라서, 역할과 책임에 맞게 남자 클래스를 => [ 남자친구, 아들, 사원, 소대원]  4개의 클래스로 분리하는 것이다. 

 

 

  Ex 2) 속성이 SRP를 만족하지 않는 경우

<hide/>
class 사람{
	String 군번;
}
사람 로미오 = new 사람();
사람 줄리엣 = new 사람();
줄리엣.군번 = "232434";  // (*)

 

  • 줄리엣의 군번에 값을 넣고 있다. 
  • 이런 잘못된 부분을 제한할 수 없다. 
  • 따라서, 남자, 여자 클래스를 분리하고 남자 클래스에만 군번 속성을 만들면 된다. 
  • 이렇게 하나의 속성이 여러 의미를 갖는 것도 SRP를 지키지 못하는 것이다. 

 

 

  Ex 3) 메서드가 SRP를 지키지 않는 이유

<hide/>
class 강아지{
	final static Boolean 수컷 = true;
	final static Boolean 암컷 = false;
	Boolean 성별;
}
void 소변보다(){
	if(this.성별 = 수컷){
	 // 한쪽다리 들고 소변 본다. 
	}else{
	 // 뒷다리두개를 굽혀 앉은 자세로 본다.  
	}
}
  • 메서드 안에서 성별에 따라서 분기 처리되는 것을 알 수 있다. 
    • 그런데 여기에서 메서드가 암, 수 행위를 모두 구현하려하기 때문에 SRP를 위반하는 것이다. 
    • 따라서 아래와 같이 추상클래스를 적용하는 게 좋다. 
<hide/>
abstract class 강아지{
	abstract void 소변보다();
}

class 수컷 extends 강아지{
	void 소변보다(){
  // 	한쪽다리 들고 소변 본다. 
 }
}

class 암컷 extends 강아지{
	void 소변보다(){
  // 뒷다리두개를 굽혀 앉은 자세로 본다.
 }
}

 


OCP(Open Closed Principle, 개방 폐쇄 원칙)

  • 소프트웨어 엔티티(클래스, 모듈, 함수)는 확장에 열려있고 변경에 대해서는 닫혀있어야한다. 
    • 자신의 대화에는 열려있고 주변의 대화에는 닫혀있어야한다. 

 

 

 Ex 1) OCP를 지키지 못한 예시

  • 어떤 운전자가 마티즈를 몬다고 하자. 
  •  운전자마티즈클래스 (창문 수동조작, 기어 수동 조작)
  • 그런데 만약, 차종이 바뀌어서 소나타를 몬다면?
  •  운전자  소나타클래스 (창문 자동조작, 기어 동 조작)
  • 이 경우에 OCP를 적용해서 리팩토링할 수 있다. 
  • 운전자  →  자동차 ← ( 마티즈클래스  소나타클래스 )
  • 이렇게 바꾸면 자동차 인터페이스를 만들어서 마티즈, 소나타 클래스가 자동차 인터페이스를 구현하도록 하면 된다. 
  • 다양한 자동차가 생긴다는 것은
    • 자동차  입장에서 확장에 열려있고
    • 운전자  입장에서는 변화에 폐쇄되어있는 것이다. 

 

 Ex 2) OCP를 지키는 좋은 예시 - JDBC

                                                                        Java 애플리케이션
                                                                                     ↕
                                                                         JDBC 인터페이스
                                                                                     ↕
  JDBC 드라이버(오라클)                         JDBC 드라이버(MySQL)                          JDBC 드라이버(MS-SQL)
                  ↕                                                                 ↕                                                               ↕
            오라클DB                                              MySQL DB                                                 MS-SQL DB

 

  • JDBC를 사용하는 클라이언트는 DB의 종류가 바뀌더라도 Connection을 설정하는 부분 외에는 코드를 수정할 게 없다. 
  • DB 종류가 바뀌더라도 Java 애플리케이션에 영향이 없도록 JDBC는 완충 장치 역할을 하는 것이다. 
  • Java 애플리케이션은 DB 라는 주변의 환경에 닫혀 있다. DB 교체는 DB가 자신의 확장에 열려있는 것을 의미한다. 

 

 

 Ex 3) OCP를 지키는 좋은 예시 - Java

  • Java 코드를 작성할 때, 코드를 실행할 환경의 OS에 따라서 다르게 작성할 필요가 없다. 
  • 어떻게 가능할까?
    • (OS별) JVM 과  목적파일이라는 이 있기 때문에 개발자는 본인이 작업 중인 개발 PC에서 돌아가고 있는 JVM에서 구동되는 소스코드 만 작성하면 된다. 
  • 즉, 소스코드 는 OS의 변화에 닫혀있고 (OS별) JVM 사이에는   목적파일 이라는 완충 장치가 있다. 

 


Liskov Substitution Principle (LSP , 리스코프 치환법칙)

  • 하위 클래스의 인스턴스는 상위형 객체 참조 변수에 대입해서 상위클래스의 인스턴스 역할하는데 문제가 없어야한다. 
  • 서브 타입은 언제든 자신의 기반 타입으로 교체할 수 있어야한다. 
  • 객체 지향의 상속은 다음 조건을 만족해야한다. 
    • 하위 클래스 is kind of 상위 클래스
    • 구현 클래스 is able to 인터페이스
    • 계층도(ex) 가족 관계), 조직도가 아닌 분류도로 나타낼 수 있어야한다. 

 

 

 Ex) 리스코프 치환법칙의 잘못된 예시

  • 아버지 - 상위클래스, 딸 - 하위 클래스
  • 위와 같은 상황은 앞에서 살펴본 객체 지향의 상속 조건을 만족하지 않는다. 
    • '딸 is kind of 아버지' 

 


ISP(Interface Seperation Principle, 인터페이스 분리 원칙)

  • 클라이언트는 자신이 사용하는 메서드에 의존 관계를 맺으면 안된다는 의미.
  • 앞서 살펴본 SRP(단일 책임 원칙)의 대체로 사용할 수 있는 방법이다. 
    • 같은 문제에 대한 두 가지 해결책이라고 볼 수 있다. 
    • 특별한 경우가 아니면 SRP를 사용하는 게 좋다. 
  • 남자친구 클래스 =>  [남자친구, 아들, 사원, 소대원] 각 4개의 인터페이스로 나누어서 각 인터페이스를 구현하는 것이다. 
    • 아까 예제에 4개의 인터페이스가 중간에 하나씩 추가된 형태라고 볼 수 있다. 

 

 cf) 인터페이스 최소 주의 원칙

  • 인터페이스를 통해 메서드를 외부에 제공하는 경우, 최소한의 메서드만 제공해야한다. 
  • ex) 앞에서 살펴본 예시에 대해서 남자친구 클래스에서는 사격하기()를 제공할 수 없다는 의미.

 

상위클래스가 풍성해야하는 이유와 관련해서 다음 예제를 살펴보자. 

 

 

 Ex 1) 빈약한 상위클래스

  • 사람
<hide/>
public abstract class 사람 {
    public String name;
    abstract void 먹다();
    public 사람(String name) {
        this.name = name;
    }
}

 

 

  • 학생
<hide/>
public class 학생 extends 사람{
    String birthday;
     String identityNumber;
     String studentNumber;

    void 자다(){
    }

    void 소개하다(){
    }

    void 공부하다(){
    }

    @Override
    void 먹다() {
    }

    public 학생(String name, String birthday, String identityNumber, String studentNumber) {
        super(name);
        this.birthday = birthday;
        this.identityNumber = identityNumber;
        this.studentNumber = studentNumber;
    }
}

 

 

  • 군인
<hide/>
public class 군인 extends 사람{

     String birthday;
     String identityNumber;
     String armyNumber;
    void 자다(){
    }

    void 소개하다(){
    }

    void 훈련하다(){
    }

    @Override
    void 먹다() {
    }

    public 군인(String name, String birthday, String identityNumber, String armyNumber) {
        super(name);
        this.name = name;
        this.birthday = birthday;
        this.identityNumber = identityNumber;
        this.armyNumber = armyNumber;
    }
}

 

  • driver
<hide/>
사람 김학생 = new 학생("김학생", "940907", "940907-2333123", "20130646");
사람 이군인 = new 군인("김학생", "981231", "981231-2333123", "20230909");
System.out.println(김학생.name);
System.out.println(이군인.name);

//        System.out.println(김학생.birthday);
//        System.out.println(이군인.birthday);   //  군인으로 형변환하지 않으면 컴파일 에러난다.
System.out.println(((학생) 김학생).birthday);
System.out.println(((군인) 이군인).birthday);

//        System.out.println(김학생.identityNumber);
//        System.out.println(이군인.identityNumber);   //  군인으로 형변환하지 않으면 컴파일 에러난다.
System.out.println(((학생) 김학생).identityNumber);
System.out.println(((군인) 이군인).identityNumber);

김학생.먹다();
이군인.먹다();

//        김학생.자다();
//        이군인.자다();
((학생) 김학생).자다();
((군인) 이군인).자다();

//        김학생.소개하다();
//        이군인.소개하다();
((학생) 김학생).소개하다();
((군인) 이군인).소개하다();

((학생) 김학생).공부하다();
((군인) 이군인).훈련하다();

 

 

  Note) 

  • 위에서 생성한 인스턴스들은 상위 클래스(사람)의 속성이나 메서드 (이름, 먹다())에만 직접 접근 가능하다. 
    • (군인, 학생) 같은 하위  클래스에서 정의한 속성이나 메서드()에 접근하기 위해서는 명시적 형변환을 반드시 거쳐야한다. 
  • 결과적으로 보면 여기 저기서 형변환이 발생한다. 
  • 애초에  인스턴스 '김학생'을 학생 타입으로 선언하고 '이군인'을 군인 타입으로 선언할 수도 있으나 그러면 상속 구조를 만들 필요가 없다. 
  • 학생, 군인에 공통적인 속성, 메서드가 불필요하게 있어서 이를 묶을 필요가 있다. 

 

 

 Ex 2)  풍성한 상위클래스

  • 사람
<hide/>
public abstract class 사람 {

    String name;
    String birthday;
    String identityNumber;

    abstract void 먹다();
    abstract void 자다();
    abstract void 소개하다();

    public 사람(String name, String birthday, String identityNumber) {
        this.name = name;
        this.birthday = birthday;
        this.identityNumber = identityNumber;
    }
}

 

 

  • (학생 군인 클래스 생략)

 

  • driver
<hide/>
사람 김학생 = new 학생("김학생", "940907", "940907-2333123", "20130646");
사람 이군인 = new 군인("김학생", "981231", "981231-2333123", "20230909");
System.out.println(김학생.name);
System.out.println(이군인.name);
System.out.println(김학생.birthday);
System.out.println(이군인.birthday);
System.out.println(김학생.identityNumber);
System.out.println(이군인.identityNumber);
김학생.먹다();
이군인.먹다();
김학생.자다();
이군인.자다();
김학생.소개하다();
이군인.소개하다();

//        김학생.공부하다(); 컴파일 에러
//        이군인.훈련하다(); 컴파일 에러
((학생) 김학생).공부하다();
((군인) 이군인).훈련하다();

 

 

  Note) 

  • 불필요한 형변환이 발생하지 않는다. 
  • 하위클래스에서 정의한 메서드에 접근하기 위해서 명시적 형변환이 필요한 것은 1번과 동일하다. 

 


DIP(Dependency In Principle, 의존 관계 역전 원칙)

  • 추상화된 것은 구체화에 의존하면 안 된다는 의미. 

 

 

  Ex) 의존 관계 역전 원칙

  • 자동차 →  스노우타이어:  자동차가 스노우타이어에 의존하는 경우를 떠올리자. 
  • 자동차는 한번 사면 몇 년 타야한는데 스노우 타이어는 계절마다 일반 타이어로 바꿔야한다. 
  • 다시 말해, 자동차는 자주 변하지 않고 스노우타이어는 자주 변한다. 
  • 그러면 자동차는 자신보다 변하기 쉬운 스노우타이어에 의존하므로 자신이 부서지기 쉽다는 걸 알고있다. 
  • 위 관계를 개선하려면 아래와 같이 바꿔야한다. 
    •  자동차 → (interface) 타이어 ← implement —(스노우타이어, 일반타이어, 광폭타이어)
    • 이렇게 바꾸면 자동차가 구체적인 것(스노우타이어, 일반타이어, 광폭타이어)에 의존하지 않고 추상적인 타이어 interface에 의존하도록 함으로써 타이어 종류가 바뀌어도 자동차는 아무 영향을 받지 않도록 만든다. 
    • 결과적으로 의존의 방향이 역전됐다. 

 


관심사의 분리 - SOC(Seperation of Concerns)

  • 관심사가 같은 것끼리 하나의 객체 안으로 또는 친한 객체로 모으고 관심이 없는 것끼리는 가능한 떨어뜨려서 서로 분리하는 것을 말한다. 
  • 하나의 속성 또는 메서드 또는 클래스 또는 모듈 또는 패키지 안에는 하나의 관심사만 들어가야한다. 
  • SOC를 적용하면 자연스럽게 SRP, ISP, OCP에 도달하게 된다. 

 


Note

  • con
  • con

 


 

찾아보기

  • con
  • con

출처 - 「 스프링 입문을 위한 자바 객체 지향의 원리와 이해 - 김종민 」