객체 지향 프로그래밍의 가장 중요한 원칙이라 알려진 SOLID 원칙이다.
- 단일 책임의 원칙
- 개방-폐쇄의 원칙
- 리스코프 치환의 원칙
- 인터페이스 분리의 원칙
- 의존성 역전의 원칙
각 원칙에 대해 알고는 있었다.
하지만, 각 원칙을 적용했을 때 어떤 장점이 있는지 말로 설명할 수 있었지만 어떻게 구현하는지 막막했다.
이번에 기회가 있어 SOLID 원칙을 다시 공부해볼 수 있었고 이해한 내용과 책을 바탕으로 예제 코드와 함께 정리하고자 한다.
아래와 같은 예시를 개선하는 과정을 통해 SOLID 원칙을 간단하게나마 이해할 수 있도록 해보자.
class Person {
wakeUp() {
console.log('아침에 일어난다.');
}
goToBathroom() {
console.log('화장실로 간다.');
}
washFace() {
console.log('얼굴을 씻는다.');
}
brushTeeth() {
console.log('양치를 한다.');
}
washHair() {
console.log('머리를 감는다.');
}
leaveBathroom() {
console.log('화장실을 나온다.');
}
dryOff() {
console.log('물기를 닦는다.');
}
}
const person = new Person();
person.wakeUp();
person.goToBathroom();
person.washFace();
person.washHair();
person.brushTeeth();
person.leaveBathroom();
person.dryOff();
단일 책임의 원칙 (SRP)
첫 번째로 단일 책임의 원칙이다.
이 원칙에서 강조하는 내용은 "클래스는 단 하나의 변경 이유만 가져야 한다"이다.
이는 모듈의 응집도가 변경과 연관이 있다는 것을 나타낸다.
SRP를 만족하기 위해서는 어떤 점을 신경써야 할까?
캡슐화
캡슐화는 설계의 제 1원칙 이라 한다.
낮은 응집도와 높은 결합도의 문제의 근본적인 원인은 캡슐화를 제대로 하지 못했기 때문이라 할 수 있다.
캡슐화는 외부에서 알 필요가 없는 부분을 감춤으로써 대상을 단순화하는 추상화의 한 종류다.
따라서, 객체 지향 설계의 가장 중요한 원리는 불안정한 구현 세부사항을 안정적인 인터페이스 뒤로 캡슐화하는 것이다.
응집도와 결합도
응집도는 모듈에 포함된 내부 요소들이 연관되어 있는 정도를 나타낸다.
결합도는 의존성의 정도를 나타내며 다른 모듈에 대해 얼마나 많은 정보를 갖고 있는지 나타내는 정도다.
만약, 특정 모듈이 다른 모듈에 대해 너무 많은 정보를 갖고 있다면 두 모듈은 높은 결합도를 갖고 있다는 것이다.
결합과 연관지어 본다면 특정 원인에 대해 하나의 모듈만 수정해도 된다면 이는 높은 응집성을 가지고 있다고 할 수 있을 것이다.
응집도가 낮다면 특정 원인에 의해 변경해야 하는 부분이 여러 모듈에 분산되어 있기 때문에 여러 모듈을 동시에 수정해야 할 것이다.
한 모듈이 변경되기 위해 많은 모듈이 함께 수정된다면 결합도가 높다고 할 수 있다.
따라서, 설계를 하며 높은 응집도와 낮은 결합도를 추구해야 할 것이다.
이러한 응집도와 결합도에 영향을 미치는 요소가 바로 캡슐화이다.
캡슐화와 응집도는 비례하고 캡슐화와 결합도는 반비례한다.
결론적으로 높은 캡슐화를 통해 높은 응집도와 낮은 결합도를 달성할 수 있다.
캡슐화 위반
가장 흔히 하는 실수는 접근자와 수정자 메서드를 사용했기 때문에 캡슐화의 원칙을 지킨다고 생각하는 것이다.
class Test {
private variable: number;
setVariable(variable: number): void {
this.variable = variable;
}
getVariable(): number {
return this.variable;
}
}
위 두 메소드는 객체 안에 number 타입의 variable이라는 변수가 있음을 드러낸다.
이렇게 작성된 코드는 객체의 협력을 고려하지 않은 경우에 해당한다.
협력에 대해 고민하지 않으면 캡슐화를 위반하게 되고 과도한 접근자와 수정자를 갖게 될 것이다.
예를 들어 위의 예시에서 variable의 타입이 string으로 변경되는 경우 getVariable()를 호출하는 다른 객체(모듈 등)이 있는 경우, 해당 부분도 모두 수정해야 한다.
한 모듈이 수정되며 여러 모듈이 함께 수정되어야 하는 경우에 해당하므로 이는 높은 결합성을 갖는다고 할 수 있다.
또한, 위와 같이 private 변수를 접근자와 수정자 메서드를 통해 그대로 드러내게 되면 사실상 public 변수와 다름이 없을 것이다.
결론
결론적으로 SRP 원칙을 제대로 지키게 되면 다른 설계 원칙을 적용하기 위한 기초로 작용하고 유지보수에 용이한 코드를 작성할 수 있다.
객체는 하나의 책임만 가져야 한다는 것이 중요한 것이다.
여러개의 책임(역할)을 갖는다면 파급효과가 커지고 그에 따라 유지 보수가 어려워 질 것이다.
처음에 언급했던 에제를 보자.
class Person {
wakeUp() {
console.log('아침에 일어난다.');
}
goToBathroom() {
console.log('화장실로 간다.');
}
washFace() {
console.log('얼굴을 씻는다.');
}
brushTeeth() {
console.log('양치를 한다.');
}
washHair() {
console.log('머리를 감는다.');
}
leaveBathroom() {
console.log('화장실을 나온다.');
}
dryOff() {
console.log('물기를 닦는다.');
}
}
const person = new Person();
person.wakeUp();
person.goToBathroom();
person.washFace();
person.washHair();
person.brushTeeth();
person.leaveBathroom();
person.dryOff();
아침에 우리가 하는 일련의 과정을 코드로 나타낸 것이다.
(위의 에제에서는 단순히 출력만 하지만 실제로는 더 복잡한 로직이 들어갈 것이다)
Person 클래스가 많은 책임을 갖고 있음을 알 수 있다.
이를 개선하기 위해 간단하게 "사람의 동작과 관련된 것" + "화장실과 관련된 것"으로 구분해보자.
class Person {
private bathroom: Bathroom;
constructor() {
this.bathroom = new Bathroom();
}
wakeUp() {
console.log('아침에 일어난다.');
}
bathroomActivity() {
this.bathroom.goToBathroom();
this.bathroom.washFace();
this.bathroom.brushTeeth();
this.bathroom.washHair();
this.bathroom.dryOff();
this.bathroom.leaveBathroom();
}
}
class Bathroom {
goToBathroom() {
console.log('화장실로 간다.');
}
washFace() {
console.log('얼굴을 씻는다.');
}
brushTeeth() {
console.log('양치를 한다.');
}
washHair() {
console.log('머리를 감는다.');
}
dryOff() {
console.log('물기를 닦는다.');
}
leaveBathroom() {
console.log('화장실을 나온다.');
}
}
class Morning {
private person: Person;
constructor() {
this.person = new Person();
}
morningActivity(): void {
this.person.wakeUp();
this.person.bathroomActivity();
}
}
const morning = new Morning();
morning.morningActivity();
(더 세밀하게 나눌 수도 있고 다른 방법도 있겠지만 지금은 이정도로 가볍게 넘어가도록 하자)
위의 개선을 통해 객체의 책임을 분리했다.
하지만 여전히 문제점이 존재한다.
SOLID의 다른 원칙들도 적용하며 위의 코드를 개선해보자.
[객체지향] SOLID 예제(2) - 의존성 역전의 원칙(DIP)
[참고자료]
위키북스, 오브젝트, 2019
'개발 > 개념' 카테고리의 다른 글
[객체지향] SOLID 예제(3) - 개방-폐쇄의 원칙(OCP) (0) | 2023.09.26 |
---|---|
[객체지향] SOLID 예제(2) - 의존성 역전의 원칙(DIP) (0) | 2023.09.25 |
[CS: 운영체제] 컴파일러 (0) | 2023.07.15 |
[CS: 운영체제] 프로세스 메모리 구조 (0) | 2023.07.15 |
React에 TypeScript 사용하는 방법 (0) | 2022.08.31 |