안녕하세요. J4J입니다.
이번 포스팅은 solid 설계 원칙 첫 번째인 srp (단일 책임 원칙)에 대해 적어보는 시간을 가져보려고 합니다.
SRP (단일 책임 원칙) 란?
solid 설계 원칙에서 srp가 의미하는 것은 모든 클래스는 한 개의 책임만을 가지고 있어야 하는 것을 말합니다.
여기서 한 개의 책임이라고 하는 것에 대한 의미를 파악해봐야 합니다.
한 개의 책임을 가진다고 하는 것은 개발을 하면서 생성되는 다양한 모듈들이 존재할 텐데 해당 모듈들이 한 개의 액터만을 위한 기능이 이루어져야 한다는 것입니다.
여기서 말하는 액터는 일부 특정 사용자가 될 수도 있고 일부 특정 시스템이 될 수도 있습니다.
즉, 이 모든 사용자들 중 한 사용자 만을 위한 기능을 생산하는 방식이 srp라고 말해볼 수 있습니다.
간단하게 예를 들어보겠습니다.
기능을 사용하는 액터가 크게 사람 / 채소 / 자동차가 있다고 가정해보겠습니다.
이 3개의 액터에 대해 모두 "씻는다"라는 기능을 정의해 볼 수 있습니다.
단순하게 씻는다는 개념 안에서만 바라본다면 1개의 wash()라는 메서드를 정의하여 사람 / 채소 / 자동차에 해당하는 모든 액터가 해당 메서드를 사용할 수 있습니다.
액터 | 메서드 |
---|---|
사람 | wash() |
채소 | |
자동차 |
하지만 srp 개념에서 바라본다면 1개의 wash() 라는 메서드는 사람 / 채소 / 자동차라는 3개의 액터가 모듈을 사용하는 관점이기 때문에 올바르지 않습니다.
올바른 경우로 변경한다면 사람에 대한 wash() / 채소에 대한 wash() / 자동차에 대한 wash()와 같이 각 액터에 대해 한 개의 책임만 가지도록 바꿔볼 수 있습니다.
액터 | 메서드 |
---|---|
사람 | 사람 wash() |
채소 | 채소 wash() |
자동차 | 자동차 wash() |
SRP (단일 책임 원칙)의 특징
그러면 위에서 말한 예시를 굳이 srp의 형태로 변경했을 때 발생될 수 있는 이점이 무엇인지에 대해 궁금할 수 있습니다.
개발을 하면서 많은 분들이 "결합도를 낮추고 응집도를 높여야 한다"라는 말을 많이 들어보셨을 겁니다.
결합도를 낮추고 응집도를 높였을 때 얻을 수 있는 이점은 다음과 같이 얘기해 볼 수 있습니다.
- 서로 다른 모듈에 영향을 적게 줄 수 있음
- 기능의 변경이나 확장이 발생할 때 유연하게 적용할 수 있음
- 재 사용성을 높일 수 있음
그리고 설계를 할 때 srp를 적용하는 이유는 다음과 같습니다.
- 관심사의 명확한 분리
- 객체 간의 결합도 낮춤
위의 예시에 대해 다시 한번 얘기해 보겠습니다.
사람 / 채소 / 자동차가 모두 동일한 wash() 메서드를 사용하게 되고, wash() 메서드 내부에는 단순하게 씻는 행위에 대해서만 정의를 했다면 현재 기능에서는 전혀 문제가 되지 않습니다.
하지만 개발을 하고 더 넘어서 운영 단계까지 간다면 wash() 메서드에 더 많은 정의가 요구될 수 있습니다.
예를 들어 어떤 장소에서 하는지 또는 어떤 도구를 이용해서 하는지 등이 있습니다.
이런 상황이 발생하게 되면 wash() 메서드를 변경해야 될 것이고 변경이 발생될 때마다 연관된 모든 액터에 문제가 없는지 검토를 해야 합니다.
결국 결합도가 높은 상태이기에 기능 변경이 힘들어지게 됩니다.
그러나 srp 기반으로 정의하여 사람에 대한 wash() / 채소에 대한 wash() / 자동차에 대한 wash()로 구분했다면 기능 변경이 발생할 경우 크게 어려움을 겪지 않게 됩니다.
각 액터가 처리하기 위한 1개의 책임만 담당하고 있었기에 해당되는 메서드만 변경한다면 이전에 비해 쉬운 작업을 하게 됩니다.
이런 이점을 가져올 수 있기 때문에 srp에 대해 항상 고려를 하여 설계를 한다면 좋은 개발자 경험을 느낄 수 있습니다.
책임의 기준
srp를 고려하기 위해 책임에 대해 항상 고민을 하게 됩니다.
사실 위의 예시에서도 굳이 사람 / 채소 / 자동차로 나누었기에 액터가 여러 개인 것으로 보입니다.
하지만 상황에 따라 액터를 "모든 사용자"로도 정의해 볼 수 있습니다.
그러면 wash() 메서드가 1개만 생성되는 것이 srp 설계 상 전혀 문제 되지 않습니다.
오히려 액터를 모든 사용자로 정의했는데 사람에 대한 wash() / 채소에 대한 wash() / 자동차에 대한 wash()의 형태로 모두 나눠버리면 응집도를 낮추며 유지 보수를 힘들게 만들기 때문에 잘못된 설계로 분류될 수 있습니다.
예시처럼 개인적인 경험 상 책임의 기준은 상황에 따라 항상 변경되었습니다.
동일한 설계에 대해서 어떤 상황에서는 올바른 설계일 수 있지만 또 다른 상황에서는 잘못된 설계로 판단될 수 있다는 것입니다.
그래서 초기 설계를 할 때 올바른 srp 설계가 나중에는 올바른 설계가 아닐 수 있습니다.
결국 상황에 따라 책임의 기준은 항상 변경될 수 있다고 생각하고 서비스 개발 및 운영을 할 때 상황에 맞게 대응 가능하도록 지속적인 리팩토링이 필요한 것으로 생각합니다.
즉, 여기서 제가 말하고자 하는 것은 "책임의 기준은 항상 동일하지 않다"입니다.
예제 코드 (1) - Bad Case
위에서 말한 예시를 코드화시켜보겠습니다.
먼저 bad case에 대해서 다음과 같이 작성해 볼 수 있습니다.
// main
package com.jforj.solidsrp.badcase;
public class Main {
public static void main(String[] args) {
Washing washing = new Washing();
// 사람
String person = "person";
washing.wash(person);
// 채소
String vegetable = "vegetable";
washing.wash(vegetable);
// 자동차
String car = "car";
washing.wash(car);
}
}
// washing
package com.jforj.solidsrp.badcase;
public class Washing {
public void wash(String actor) {
System.out.println("씻습니다.");
}
}
코드를 실행하면 다음과 같이 콘솔에 결과를 확인할 수 있습니다.
이 상황에서 사람이 씻을 땐 씻는 도구를 추가해야 하고 채소가 씻을 땐 채소 개수를 추가해야 된다고 가정했을 때 다음과 같이 코드가 변경될 수 있습니다.
// main
package com.jforj.solidsrp.badcase;
public class Main {
public static void main(String[] args) {
Washing washing = new Washing();
// 사람
String person = "person";
washing.wash(person, "타올", 0);
// 채소
String vegetable = "vegetable";
washing.wash(vegetable, null, 12);
// 자동차
String car = "car";
washing.wash(car, null, 0);
}
}
// washing
package com.jforj.solidsrp.badcase;
public class Washing {
public void wash(String actor, String tool, long count) {
switch (actor) {
case "person": {
System.out.println("사람이 씻습니다.");
System.out.println("도구는 " + tool + "을 사용한다고 하네요.");
break;
}
case "vegetable": {
System.out.println("채소를 씻습니다.");
System.out.println("씻어야 되는 채소의 총 갯수는 " + count + "개 입니다.");
if (count > 10) {
System.out.println("채소를 씻을 때 바구니를 사용해야 합니다.");
}
break;
}
default: {
System.out.println("씻습니다.");
}
}
}
}
코드 실행 결과물은 다음과 같이 확인될 수 있습니다.
지금 작성된 예제 코드만 보더라도 요구사항이 조금만 더 늘어난다면 wash()는 코드의 복잡성이 매우 늘어날 수 있다는 것을 느낄 수 있습니다.
단순하게 작성된 것도 이 정도인데 DB와의 connection이 생기고 데이터를 가공하며 상세한 비즈니스 로직을 구성하게 된다면 결국 좋지 않은 개발자 경험을 느끼게 됩니다.
예제 코드 (2) - Best Case
bad case를 srp를 활용하여 올바르게 변경하면 다음과 같이 작성할 수 있습니다.
먼저 추가 요구사항이 발생되기 전입니다.
// main
package com.jforj.solidsrp.bestcase;
public class Main {
public static void main(String[] args) {
// 사람
PersonWashing personWashing = new PersonWashing();
personWashing.wash();
// 채소
VegetableWashing vegetableWashing = new VegetableWashing();
vegetableWashing.wash();
// 자동차
CarWashing carWashing = new CarWashing();
carWashing.wash();
}
}
// person washing
package com.jforj.solidsrp.bestcase;
public class PersonWashing {
public void wash() {
System.out.println("씻습니다.");
}
}
// vegetable washing
package com.jforj.solidsrp.bestcase;
public class VegetableWashing {
public void wash() {
System.out.println("씻습니다.");
}
}
// car washing
package com.jforj.solidsrp.bestcase;
public class CarWashing {
public void wash() {
System.out.println("씻습니다.");
}
}
코드를 실행해 보면 bad case와 동일하게 결과를 확인할 수 있습니다.
이번에는 추가 요구사항이 bad case와 동일하게 생겼다면 코드를 다음과 같이 변경할 수 있습니다.
// main
package com.jforj.solidsrp.bestcase;
public class Main {
public static void main(String[] args) {
// 사람
PersonWashing personWashing = new PersonWashing();
personWashing.wash("타올");
// 채소
VegetableWashing vegetableWashing = new VegetableWashing();
vegetableWashing.wash(12);
// 자동차
CarWashing carWashing = new CarWashing();
carWashing.wash();
}
}
// person washing
package com.jforj.solidsrp.bestcase;
public class PersonWashing {
public void wash(String tool) {
System.out.println("사람이 씻습니다.");
System.out.println("도구는 " + tool + "을 사용한다고 하네요.");
}
}
// vegetable washing
package com.jforj.solidsrp.bestcase;
public class VegetableWashing {
public void wash(long count) {
System.out.println("채소를 씻습니다.");
System.out.println("씻어야 되는 채소의 총 갯수는 " + count + "개 입니다.");
if (count > 10) {
System.out.println("채소를 씻을 때 바구니를 사용해야 합니다.");
}
}
}
// car washing
package com.jforj.solidsrp.bestcase;
public class CarWashing {
public void wash() {
System.out.println("씻습니다.");
}
}
결과는 동일하게 나왔지만 코드를 살펴보면 각자의 책임을 가지고 있기 때문에 코드 변경에 대해 유연하게 대응할 수 있는 것을 확인할 수 있습니다.
또한 관심사도 명확히 분리되어 있기 때문에 코드를 이해하고 읽는 과정도 보다 수월한 것을 느끼실 수 있습니다.
이상으로 solid 설계 원칙 첫 번째인 srp (단일 책임 원칙)에 대해 간단하게 알아보는 시간이었습니다.
읽어주셔서 감사합니다.
'설계 > 객체지향설계' 카테고리의 다른 글
[객체지향설계] SOLID 설계 원칙 (5) - DIP (의존성 역전 원칙) (0) | 2024.03.22 |
---|---|
[객체지향설계] SOLID 설계 원칙 (4) - ISP (인터페이스 분리 원칙) (4) | 2024.03.12 |
[객체지향설계] SOLID 설계 원칙 (3) - LSP (리스코프 치환 원칙) (0) | 2024.03.09 |
[객체지향설계] SOLID 설계 원칙 (2) - OCP (개방 폐쇄 원칙) (0) | 2024.03.04 |
댓글