포스트

SOLID - SRP와 OCP

SOLID Principle

Single Reponsibility Principle

  • 단일 책임의 원칙 (SRP)
  • 모든 소프트웨어 컴포넌트는 단 하나, 그리고 오직 하나의 책임만 있어야 한다.
  • 소프트웨어 컴포넌트는 클래스, 메서드, 함수, 그리고 모듈일 수도 있다.
  • 스위스 군용 나이프를 예시로 들어보자. 스위스 군용 나이프는 여러 개의 도구로 구성되어있으며, 가위, 캔 오프너, 나이프, 등등 여러 개의 기능이 있다. 이렇게 스위스 군용 나이프는 실생활에서는 아주 유용한 도구이지만, 프로그래밍에서는 다르다. 스위스 군용 나이프를 소프트웨어 컴포넌트로 바라보면 단일 책임의 원칙을 위배한다.

Cohesion (통일성)

  • 소프트웨어 세상에서 ‘통일성’이란 소프트웨어 컴포넌트의 다양한 부분이 서로 어느정도 연관성을 갖는가에 대한 척도이다.
  • 예로 쓰레기장을 생각해보자. 별도의 분리수거 없이 방치된 쓰레기장은 낮은 통일성이 있다고 볼 수 있다. 만약 쓰레기들을 분리수거를 하게 되면 높은 통일성을 갖는다고 볼 수 있다.
  • 단일 책임 원칙의 한 측면은 구성 요소의 응집력이 높아야 한다.

Coupling (결합)

  • 결합도는 다양한 소프트웨어 구성 요소들 간의 상호 의존성의 정도로 정의된다.
  • 크기가 서로 다른 두 종류의 기차가 있으면, 각 기차는 규격에 맞는 레일에서만 움직일 수 있다. 이 경우 기차와 트랙 간에 높은 결합도가 있다고 할 수 있다. 기차의 경우에는 높은 결합도가 필수이지만, 소프트웨어에서의 높은 결합도는 바람직하지 않다.

코드로 예시를 들면 클래스의 저장 메서드를 생각할 수 있다. 아래와 같이 Student Entity가 있다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Student {
	private String studentId;
	private Date studentDOB;
	private String address;

	public void save(){
		String objectStr = MyUtils.serializeIntoAString(this);
		Connection connection = null;
		Statement stmt = null;
		try {
			Class.forName("com.mysql.jdbc.Driver");
			connection =
				DriverManager.getConnection(
					"jdbc:mysql://localhost:3306/MyDB",
					"root",
					"password"
				);
			stmt = connection.createStatement;
			stmt.execute("INSERT INTO STUDENT VALUES (" + objectStr + ")");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public String getStudentId(){
		return studentId;
	}

	public String setStudentId(String studentId){
		this.studentId = studentId;
	}
}

이 클래스의 저장 메서드는 MySQL 데이터베이스에 저장하는 메서드이다. 이 때 Student 클래스는 MySQL과 강한 결합도를 가지며, 다른 데이터베이스로 변경하고자 할 때 Student 클래스 코드를 수정해야 한다. Student 클래스는 단일 책임 원칙(SRP)을 따르기 위해 데이터베이스 연산이 아닌 학생의 프로필 데이터를 다루는 역할에 집중해야 하며, 데이터베이스 연산은 StudentRepository와 같은 별도 클래스로 분리되어야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class StudentRepository {
	public void save(Student student){
		String objectStr = MyUtils.serializeIntoAString(student);
		Connection connection = null;
		Statement stmt = null;
		try {
			Class.forName("com.mysql.jdbc.Driver");
			connection =
				DriverManager.getConnection(
					"jdbc:mysql://localhost:3306/MyDB",
					"root",
					"password"
				);
			stmt = connection.createStatement();
			stmt.execute("INSERT INTO STUDENT VALUES (" + objectStr + ")");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

Student 클래스에서는 new StudentRepository().save(this)만 호출하도록 분리할 수 있다. 이렇게 분리하면 Student 클래스는 학생의 프로필 데이터를 다루는 단일 책임을 가지며, StudentRepository 클래스는 학생과 관련된 데이터베이스 연산에 대한 책임을 가지게 된다. 이로 인해 두 클래스는 서로 느슨하게 결합되고, 재사용성과 유지보수성이 높아진다.

→ 높은 통일성과 느슨한 결합도를 목표로 하자.

변경할 이유(?)

  • 앞서 말한 단일 책임 원칙의 정의를 살펴보면 “모든 소프트웨어 컴포넌트는 단 하나, 그리고 오직 하나의 책임만 있어야 한다“ 라고 했다. 여기서 살짝 바꿔보면 다음과 같게도 말할 수 있다:

“모든 소프트웨어 컴포넌트는 변경될 이유(Reason for change)가 단 하나여야 한다.”

책임을 분리하기 전의 Student 코드를 살펴보자:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Student {
	private String studentId;
	private Date studentDOB;
	private String address;

	public void save(){
		String objectStr = MyUtils.serializeIntoAString(this);
		Connection connection = null;
		Statement stmt = null;
		try {
			Class.forName("com.mysql.jdbc.Driver");
			connection =
				DriverManager.getConnection(
					"jdbc:mysql://localhost:3306/MyDB",
					"root",
					"password"
				);
			stmt = connection.createStatement;
			stmt.execute("INSERT INTO STUDENT VALUES (" + objectStr + ")");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public String getStudentId(){
		return studentId;
	}

	public String setStudentId(String studentId){
		this.studentId = studentId;
	}
}

위 코드를 변경해야 할 잠재적 상황들을 가정해보면 다음이 있을 것이다:

  1. Student Id 형식의 변경
  2. Student Address 형식의 변경
  3. 데이터베이스의 교체

아까와 같이 save 메서드를 분리하게 된다면 각 클래스에 대해 변경할 상황들도 분리가 될 것이다.

Student :

  1. Student Id 형식의 변경
  2. Student Address 형식의 변경

StudentRepository :

  1. 데이터베이스의 교체

아직 Student 는 변경할 상황이 두 개나 있다고 보이지만, 이들이 서로 유사하다면 하나로 병합할 수 있다: “Student 프로필의 변경”

Open-Closed Principle

  • 개방-폐쇄의 원칙 (OCP)
  • 확장에는 개방되어있지만, 수정에는 폐쇄되어있는 형태
  • 닌텐도 Wii를 예시로 들어보자. Wii는 처음에 구매하면 콘솔과 리모컨이 주어진다. Wii를 다방면으로 즐기기 위해 악세서리를 추가로 구매할 수 있다. 리모컨을 꽂을 수 있는 총 형태의 컨트롤러나 스티어링 휠과 같은 형태의 컨트롤러를 추가로 구매하면 아주 유용한 기능들을 게임플레이에 추가할 수 있다. 이 때, 실제 Wii 콘솔이나 리모컨에 어떠한 변경이 일어났는가? 단순히 리모컨을 거치하는 형태였을 뿐이다. 이러한 악세서리를 사용하기 위해 콘솔의 메인보드에 어떠한 형태의 조작을 해야 했는가? 아니다.
  • 이러한 형태로 확장을 허용한 것이 우연일까? 닌텐도의 엔지니어들은 이런 형태로만 확장을 허용하기 위해 고민했을 것이다.
  • 수정에는 폐쇄되어있다 : 소프트웨어 컴포넌트에 추가되는 기능들은 기존의 코드를 변경하지 않아야 한다.
  • 확장에는 개방되어있다 : 소프트웨어 컴포넌트는 새로운 기능의 추가나 새로운 행동을 취할 수 있도록 확장성을 고려하여 설계해야 한다.

장점

  • 신기능 추가가 쉽다 - 개발과 테스팅에 소요되는 자원이 최소화된다
  • 개방-폐쇄의 원칙에 따르다보면 디커플링이 요구되며, 이는 무의식적으로 단일 책임 원칙에 따르게 하게 한다

주의

  • 개방-폐쇄 원칙을 따를 때는 주의해야 한다. 잘못 사용하면 전체적인 디자인이 불필요하게 복잡해진다. 때로는 단순한 코드 수정으로 해결될 수 있는 문제가 있다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.