본문 바로가기
설계/객체지향설계

[객체지향설계] SOLID 설계 원칙 (1) - SRP (단일 책임 원칙)

by J4J 2024. 3. 2.
300x250
반응형

안녕하세요. 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("씻습니다.");
    }
}

 

 

 

코드를 실행하면 다음과 같이 콘솔에 결과를 확인할 수 있습니다.

 

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, "타올", 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("씻습니다.");
            }
        }
    }
}

 

 

 

코드 실행 결과물은 다음과 같이 확인될 수 있습니다.

 

bad case 결과

 

 

 

 

지금 작성된 예제 코드만 보더라도 요구사항이 조금만 더 늘어난다면 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와 동일하게 결과를 확인할 수 있습니다.

 

best 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("씻습니다.");
    }
}

 

best case 결과

 

 

 

 

결과는 동일하게 나왔지만 코드를 살펴보면 각자의 책임을 가지고 있기 때문에 코드 변경에 대해 유연하게 대응할 수 있는 것을 확인할 수 있습니다.

 

또한 관심사도 명확히 분리되어 있기 때문에 코드를 이해하고 읽는 과정도 보다 수월한 것을 느끼실 수 있습니다.

 

 

 

 

 

 

 

 

 

이상으로 solid 설계 원칙 첫 번째인 srp (단일 책임 원칙)에 대해 간단하게 알아보는 시간이었습니다.

 

읽어주셔서 감사합니다.

 

 

 

728x90
반응형

댓글