#oop, #clean-code

SOLID 1편 SRP와 OCP

이번 포스팅에서는 객체지향에서 대표적인 원칙이라고 할 수 있는 SOLID원칙에 대해서 알아보고자 한다. 하나의 포스팅에 모든 것을 작성하면 길어질 것 같아, SRPOCP를 먼저 포스팅하고 2편에서 LSP, ISP, DIP를 설명할 것이다.

1 . SRP : 단일 책임 원칙

SRP란 Single Responsibility 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의 예제가 될 수 있다. 여러 개의 클래스가 로깅이나 보안, 트랜잭션과 같은 부분은 공유하고 있을 수 있다. 이런 부분을 모듈화를 통해 각각 필요한 곳에 위빙해주는 방식을 위해 도입된 AOP또한 로깅, 보안, 트랜잭션과 같은 부분을 하나의 모듈에 단일책임으로 부여하여 이를 사용하게 할 수 있도록 함으로써 SRP를 지키는 방법이다.

2 . 개방-폐쇄 원칙 (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원칙을 지키는 코드를 완성 할 수 있다.

클래스를 추가하는 것 또한 변경을 의미하고 Production Code의 일부를 말하는 것 아닐까? 라는 의문이 들었지만, 이 부분은 아래와 같이 생각한다면 우문이었다는 것을 알게 된다.

  • Production Code란 기존에 짜여져 있는 코드를 의미하며 위의 코드에서 메신저가 어떤 메신저인지 판별하는 if 문을 통해 해결할 수도 있지만, 이는 기존 코드의 전체 작동방식을 이해해야 할 수 있다. 즉 현업에서 관련 로직이 어떻게 작성되었고 작동하는지를 명확히 알고 있어야 추가적인 작업이 가능해진다.
  • 하지만 위와 같이 인터페이스를 통해 분리하면 기존의 코드에는 변경이 없으며 단순히 하나의 클래스를 추가하고 오버라이딩만 해준다면 내부 동작 원리를 알지 못해도 사용 할 수 있다.

OCP를 또 하나의 관점은 클래스를 변경하지 않고도 대상 클래스의 환경을 변경할 수 있는 설계가 되어야 한다. 이를 위해 Mock Stub 등의 객체들이 사용되며 특히 단위테스트에서 이러한 것들이 유용하게 사용된다.

다음 편에서는 나머지 3가지 원칙에 대해서 포스팅할 예정이다.