본문 바로가기
Engineering WIKI/Docs

객체지향 (S: SRP / O : OCP)

by wonos 2021. 1. 17.

2023.05.27 - [Engineering WIKI/Docs] - SOLID 5가지 설계 원칙

2021.01.17 - [Engineering WIKI/Docs] - 객체지향 (LID)


  • S → SRP (단일 책임 원칙)
    • 한 클래스는 하나의 책임만 가져야 한다. → Single Responsebility Principle
    • 책임이란?
      • 객체가 할 수 있는 것과 해야 하는 것으로 나뉜다. 즉, 객체는 자신이 할 수 있는 것과 해야하는 것만 수행 할 수 있도록 설계되어야 한다는 법칙.
    • SRP를 지켜야 하는 이유?
      • 고전적 설계개념인 응집도와 결합도
      • 응집도 → 한 프로그램 요소가 얼마나 뭉쳐있는가를 나타내는 척도.
      • 결합도 → 프로그램 구성 요소들 사이가 얼마나 의존적인지를 나타내는 척도.
    • SRP가 필요한 코드
public class Student {
    public void getCourse(){	}
    public void addCourse() {	}
    public void save(){	}
    public Student load() {	}
    public void printOnReportCard() {	}
    public void printOnAttendanceBook() {	}
}
  • 위의 예제 학생이라는 클래스는 수강과목을 조회하고 추가하고 데이터베이스에 저장하고 저장된 학생들을 불러오고 기록을 출력하는 책임을 담당하고 있다. 이렇게 하나의 클래스가 다양한 책임을 갖는 경우가 되는 이유는 변경이라는 관점에서 문제를 일으키기 때문이다.

 

  • 잘 설계된 프로그램이란? → 새로운 요구사항이나 변경사항이 있을 때 가능한 영향 받는 부분을 최소화.

 

  • 뿐만 아니라 클래스 내부에서 서로 다른 역할을 수행하는 코드끼리 강하게 결합되는데, 예를 들어, 현재 수강과목을 조회하는 부분과 데이터베이스에서 학생 정보를 가져오는 코드는 어딘가에 연결될 확률이 높다.이러한 코드끼리의 결함은 하나의 변화에 대해 많은 변경사항을 발생시키고 관련된 모든 기능을 테스트 해봐야 한다는 단점이 발생. → 유지보수가 어렵게 됨. 따라서 각 객체는 하나의 책임만을 수행해야 한다.

 

  • 위의 문제를 요약하면 프로그램에서 응집도는 낮아지고 결합도는 올라간다. 객체가 다양한 기능을 하기 때문에 필요한 기능만이 모여있어야하는 응집도는 낮아지고 프로그램 내부에서 그리고 외부 프로그램간의 결합도는 올라가게 된다. (수 많은 기능과 얽혀있는 모든 클래스와 결합되기 때문)

 

  • 따라서, 위와 같은 클래스는 학생 - 학생DAO - 성적표 출석부 등의 클래스를 통해 쪼개어 관리하는것이 변경에 유연하게 대처 할 수 있다. 이렇게 단일책임에 적절하게 분리하게 되면 변경된 부분만을 수정 할 수 있고 각각 의존하고 있는 영역이 줄어들어 변경에 유연하게 대처가 가능.

 

  • AOP(Aspect Oriented Programming) 또한 SRP의 예제가 될 수 있다. 여러 개의 클래스가 로깅이나 보안 , 트랜잭션과 같은 부분을 공유하고 있을 수 있다.

 


Functions should do one thing

함수는 한 가지 작업을 수행해야 합니다.이것은 소프트웨어 엔지니어링에서 단연코 가장 중요한 규칙입니다. 함수가 한 가지 이상을 수행할 때 구성, 테스트 및 추론하기가 더 어렵습니다. 함수를 하나의 작업으로 분리할 수 있으면 쉽게 리팩토링할 수 있고 코드가 훨씬 더 깔끔하게 읽힙니다. 이 가이드에서 이것 외에 다른 것을 빼지 않는다면 당신은 많은 개발자들보다 앞서게 될 것입니다.

 

Bad

function emailClients(clients: Client[]) {
  clients.forEach((client) => {
    const clientRecord = database.lookup(client);
    if (clientRecord.isActive()) {
      email(client);
    }
  });
}

Good

function emailClients(clients: Client[]) {
  clients.filter(isActiveClient).forEach(email);
}

function isActiveClient(client: Client) {
  const clientRecord = database.lookup(client);
  return clientRecord.isActive();
}
  • SOLID를 모른다고 해도 누구나 처음 배우는 클린코드의 첫 덕목입니다. 하나의 함수는 하나의 기능만 해야한다! 하나의 함수가 많은 일을 하고 있다면 함수를 쪼개야 한다.
  • 함수를 잘게 쪼개고 명확하게 만들면 절대로 이 함수는 틀릴 수 없다! 라는 코드의 조각들이 많아지게 되며 문제가 발생했을때의 확인을 해야하는 코드의 양이 줄어 들게 됩니다.

 

One more Thing! 순수함수로 작성해보자!

  • 클래스를 쓰지 않고 함수만 사용한다고 함수형 프로그래밍이라고 할 수는 없습니다. 함수형 프로그래밍이 되기 위해서는 순수함수와 부수효과를 분리하는 구조가 되어야 합니다.

 

  • 순수함수란?
    • 1개의 반환값이 반드시 존재한다.
    • 같은 인자를 넣었을때에는 항상 같은 값을 반환한다.
    • 함수 외부의 어떠한 값을 변화시켜서는 안된다.
  • 순수함수는 너무나도 SRP의 원칙에 들어맞는 모양이 되게 됩니다. 그러니 함수형 프로그래밍의 핵심인 가급적 순수함수로 작성하는 원칙은 SOLID의 첫번째 원칙인 SRP와 함께 엮어서 생각을 해주시기 바랍니다.

 


 

  • O → OCP (개방-폐쇄 원칙 (Open/closed principle))
    • “소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.”
    • 기존 코드를 변경하지 않으면서 기능을 추가 할 수 있도록 설계 되어야 한다. → 캡슐화를 통해 여러 객체에서 사용하는 같은 기능을 인터페이스에 정의
    • OCP에서 중요한 것은 요구사항이 변경되었을 때 코드에서 변경되어야 하는 부분과 변경되지 않아야하는 부분을 명확하게 구분하여, 변경되어야 하는 부분을 유연하게 작성하게 작성하는 것을 의미한다.
public class Computer {
    	private KakaoMessenger kakaoMessenger;
    	
    	public static void main(String[] args) {
    		Computer computer = new Computer();
    		computer.boot();
    	}
    
    	private void boot() {
    		System.out.println("BOOTING.....");
    		kakaoMessenger = new KakaoMessenger();
    		kakaoMessenger.boot();
    	}
    }
package kail.study.java.solid;
    
public class KakaoMessenger {
    public void boot() {
        System.out.println("Kakao BOOTING....");
    }
}
  • 위의 코드에서 컴퓨터를 실행하면 카카오톡이 함께 실행되는 코드를 작성하였다. 하지만 카카오톡을 더이상 쓰지 않고 라인을 사용한다는 변경사항이 생기면 어떻게 될까?

 

  • 위의 코드에서 카카오를 새로 생성하는 것이 아니라 라인을 생성하고 라인에게 boot 를 실행하라는 메세지를 보내야 할 것이다.

 

  • 즉, 외부의 변경사항에 의해서 내부의 Production Code에 변경사항이 발생한다. 이러한 문제를 해결하기 위해서 아래와 같이 인터페이스를 통해 메신저를 분리하였다.
package kail.study.java.solid;
    
public class Computer {
    private Messenger messenger;

    public static void main(String[] args) {
        Computer computer = new Computer();
        computer.setMessenger(new LineMessenger());
        computer.boot();
    }

    private void setMessenger(Messenger messenger) {
        this.messenger = messenger;
    }

    private void boot() {
        System.out.println("BOOTING.....");
        messenger.boot();
    }
}
package kail.study.java.solid;
    
public class LineMessenger implements Messenger{
    
    @Override
    public void boot() {
        System.out.println("Line BOOTING....");
    }
}
package kail.study.java.solid;
    
public interface Messenger {
    void boot();
}
  • 이렇게 작성하는 경우 어떠한 메신저로 변경되어도 하나의 클래스만 추가함으로써 외부의 변경에는 유연하게 대응 할 수 있으며 내부적으로 Production Code를 변경하지 않는 OCP원칙을 지키는 코드를 완성 할 수 있다.
  • OCP를 또 하나의 관점은 클래스를 변경하지 않고도 대상 클래스의 환경을 변경할 수 있는 설계가 되어야 한다.
  • 이를 위해 Mock Stub 등의 객체들이 사용되며 특히 단위테스트에서 이러한 것들이 유용하게 사용된다.

public class Client {
    public static void main(String args[]){
        Animal cat = new Cat();
        Animal dog = new Dog();
        
        cat.crying();
        dog.crying();
    }
}
  • 이렇게 캡슐화를 하면, 동물이 추가되었을 때 cyring() 함수를 호출하는 부분은 건드릴 필요가 없으면서 쉽게 확장할 수 있게됩니다.