Computer Theory/Design partterns

디자인 패턴과 SOLID 원칙

jheaon 2024. 5. 22. 12:17

최근 객체 지향 프로그래밍을 자주 하게 되면서 해당 로직이 어떻게 더 깔끔하게 작성할지, 또는 코드의 유지보수성이 잘 되었으면 하는 생각에 여러 주제를 찾아보다가 디자인 패턴을 접하게 되었다. 따라서 "헤드 퍼스트 디자인 패턴"이라는 책과 여러 웹에 뿌려져 있는 지식을 통해 디자인 패턴을 학습하고, 이를 활용해 보고자 한다.

 

디자인 패턴

디자인 패턴이란 객체 지향 프로그래밍 설계를 할 때 자주 발생되는 문제를 피하기 위해 사용하는 패턴이다. 소프트웨어 설계에 있어 공통적인 문제들에 대한 표준적인 해법과 작명법을 제안하여, 알고리즘과 같이 프로그램 코드로 바로 변환될 수 있는 형태는 아니지만, 특정한 상황에서 구조적인 문제를 해결하는 방식이다. "효율적인 코드를 만들기 위한 방법론"이라고 말하기도 한다. 

 

 

디자인 패턴의 종류 - 한빛네트워크

 

하지만 디자인 패턴은 모든 상황의 해결책이 아니며, 단지 상황에 따라 자주 쓰이는 설계 방법을 정리한 방법론이다. 따라서 모든 문제를 디자인 패턴으로 해결하기보다는 코드 베이스의 간결성을 유지하는 것에 초점을 두고 코드를 작성하여야 한다. 

 

 

SOLID 원칙

SOLID 원칙이란 객체 지향 프로그래밍을 하면서 지켜야 하는 5대 원칙을 의미한다. SOLID 원칙을 지킨다면 시간이 지나도 변경이 용이하고 유지보수와 확장이 쉬운 소프트웨어를 개발하는데 도움이 된다.  SOLID의 각 뜻을 풀이해 보면 다음과 같다. 

 

  • SRP(Single Responsibility Principle): 단일 책임 원칙
  • OCP(Open Closed Priciple): 개방 폐쇄 원칙
  • LSP(Listov Substitution Priciple): 리스코프 치환 원칙
  • ISP(Interface Segregation Principle): 인터페이스 분리 원칙
  • DIP(Dependency Inversion Principle): 의존 역전 원칙

 

 

 

 

단일 책임 원칙 - SRP

단일 책임 원칙은 객체는 단 하나의 책임을 가져야 한다는 것을 의미한다. 여기서 책임이란 기능 담당을 의미한다. 즉 하나의 클래스는 하나의 기능을 담당하여 하나의 책임을 수행하는데 집중되어 있어야 한다.

 

단일 책임 원칙 준수의 유무에 따른 가장 큰 특징 기준 척도는 '기능 변경이 일어났을 때의 파급 효과'이다. 한 객체에 책임이 많아질수록 클래스 내부에서 서로 다른 역할을 수행하는 코드끼리 강하게 결합될 가능성이 높아지게 되어 시스템이 복잡해질 수 있다. 그래서 그 객체가 하는 기능에 변경사항이 생긴다면 그 기능을 사용하는 부분의 코드를 모두 테스트해야 할 수도 있다. 즉 A을 고쳤으나 B를 수정해야 하고 또 C를 수정해야 하는 마치 책임이 순화되는 형태를 예로 들 수 있다. 

 

SRP 원칙을 잘 따르게 된다면 한 책임의 변경으로부터 다른 책임의 변경으로의 연쇄작용에서 자유로울 수 있게 되며, 코드의 가독성과 유지보수 용이라는 이점을 누릴 수 있게 된다. 

 

이번에는 원칙 위반 예제와 에제를 수정하는 것을 살펴보자 

 

 

각 메서드는 다음과 같은 역할을 지니고 있다. 

  • calculatePay() : 회계팀에서 급여를 계산하는 메서드
  • reportHours() : 인사팀에서 근무시간을 계산하는 메서드 
  • saveDababase() : 기술팀에서 변경된 정보를 DB에 저장하는 메서드
  • calculateExtraHour() : 초과 근무 시간을 계산하는 메서드 (회계팀과 인사팀에서 공유하여 사용)

각 메서드들은 각 팀에서 필요할 때마다 호출해 사용한다고 가정한다.  아래 코드는 calculatePay() 메서드에서 초과 근무 시간을 계산하기 위해 calculateExtraHour() 메서드를 공유해서 사용하고 있다. 

class Employee {
    String name;
    String positon;

    Employee(String name, String position) {
        this.name = name;
        this.positon = position;
    }

	// * 초과 근무 시간을 계산하는 메서드 (두 팀에서 공유하여 사용)
    void calculateExtraHour() {
        // ...
    }

    // * 급여를 계산하는 메서드 (회계팀에서 사용)
    void calculatePay() {
        // ...
        this.calculateExtraHour();
        // ...
    }

    // * 근무시간을 계산하는 메서드 (인사팀에서 사용)
    void reportHours() {
        // ...
        this.calculateExtraHour();
        // ...
    }

    // * 변경된 정보를 DB에 저장하는 메서드 (기술팀에서 사용)
    void saveDababase() {
        // ...
    }
}

그런데 회계팀에서 급여를 계산하는 방식을 새로 변경하여, 코드에서 초과 근무 시간을 계산하는 메서드의 알고리즘 업데이트가 필요해졌다. 따라서 calculateExtraHour() 메서드를 변경했는데 변경에 의한 파급효과가 reportHours() 메서드에도 영향을 주게 되었다. 

 

이 상황이 SRP을 위배한 상황인데, Employee 클래스에서 회계팀, 인사팀, 기술팀 즉 3개의 액터에 대한 책임을 한꺼번에 가지고 있기 때문이다. 이를 SRP 원칙을 적용해 변형한 코드는 다음과 같다. 

 

// * 통합 사용 클래스
class EmployeeFacade {
    private String name;
    private String positon;

    EmployeeFacade(String name, String position) {
        this.name = name;
        this.positon = position;
    }
    
    // * 급여를 계산하는 메서드 (회계팀 클래스를 불러와 에서 사용)
    void calculatePay() {
        // ...
        new PayCalculator().calculatePay();
        // ...
    }

    // * 근무시간을 계산하는 메서드 (인사팀 클래스를 불러와 에서 사용)
    void reportHours() {
        // ...
        new HourReporter().reportHours();
        // ...
    }

    // * 변경된 정보를 DB에 저장하는 메서드 (기술팀 클래스를 불러와 에서 사용)
    void EmployeeSaver() {
        new EmployeeSaver().saveDatabase();
    }
}

// * 회계팀에서 사용되는 전용 클래스
class PayCalculator {
    // * 초과 근무 시간을 계산하는 메서드
    void calculateExtraHour() {
        // ...
    }
    void calculatePay() {
        // ...
        this.calculateExtraHour();
        // ...
    }
}

// * 인사팀에서 사용되는 전용 클래스
class HourReporter {
    // * 초과 근무 시간을 계산하는 메서드
    void calculateExtraHour() {
        // ...
    }
    void reportHours() {
        // ...
        this.calculateExtraHour();
        // ...
    }
}

// * 기술팀에서 사용되는 전용 클래스
class EmployeeSaver {
    void saveDatabase() {
        // ...
    }
}

 

단일 책임의 적용은 각 책임에 맞게 클래스를 분리하여 구성하면 끝이다. 이렇게 정의하게 되면 EmployeeFacade 클래스는 어떠한 액터도 담당하지 않게 된다. 만약 변경사항이 생겨도, 각각의 분리된 클래스에서만 수정하기 때문에 기능을 이용하는 데 있어 아무런 문제가 생기지 않는다. 또한 앞서 말한 문제에서도 벗어날 수 있다. 

 

하지만 단일 책임 원칙의 범위 기준은 사람들 마다 생각하는 것이 다르고 상황에 따라서도 달라질 수 있기 때문이다. 따라서 단일 책임 원칙의 기준은 개발자 스스로 정해야 하고, 경험과 업무 이해가 부족하면 SPR원리에서 멀어져 버릴 수 있으니, 평소에 많은 연습과 경험이 필요하다. 

 

 

 

 

 

개방 폐쇄 원칙 - OCP

개방 폐쇄 원칙은 기존의 코드를 변경하지 않으면서, 기능을 추가할 수 있도록 설계가 되어야 한다는 원칙을 의미한다. 확장에 대해서는 개방적이고 수정에 대해서는 폐쇄적이라는 의미로 정의한다. 여기서 확장은 새로운 기능이 추가됨을 의미한다. 

  • 확장에 열려있다 : 모듈의 확장성을 보장하며, 새로운 변경 사항이 발생했을 때 유연하게 코드를 추가함으로써 애플리케이션의 기능을 큰 힘들이지 않고 확장할 수 있다. 
  • 변경에 닫혀있다 : 객체를 직접적으로 수정하는 건 제한해야 하며, 새로운 변경 사항이 발생했을 때, 객체를 직접적으로 수정해야 한다면 유연하게 대응할 수 없는 애플리케이션이라고 말한다.  

 

 

즉 OCP는 다형성과 확장을 가능하게 하는 객체지향의 장점을 극대화하는 설계 원칙이다. 우리는 코딩할 때 배운 객체를 추상화함으로써, 확장에 열려있고 변경에 닫혀있는 유연한 구조를 사용해 오며 객체 지향의 OCP 원칙의 효과를 이용해고 있다. 따라서 클래스를 추가해야 한다면 기존의 코드를 크게 수정할 필요 없이, 적절하게 상속 관계에 맞춰 추가만 한다면 유연하게 확장을 할 수 있었던 것이다. 

 

다음은 OCP 원칙 위반 예제와 수정하는 과정이다. 

 

다음은 동물 클래스가 있고 이 클래스의 타입을 받아 소리를 출력하는 helloAnimal 클래스가 있다. 

class Animal {
	String type;
    
    Animal(String type) {
    	this.type = type;
    }
}

// 동물 타입을 받아 각 동물에 맞춰 울음소리를 내게 하는 클래스 모듈
class HelloAnimal {
    void hello(Animal animal) {
        if(animal.type.equals("Cat")) {
            System.out.println("냐옹");
        } else if(animal.type.equals("Dog")) {
            System.out.println("멍멍");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        HelloAnimal hello = new HelloAnimal();
        
        Animal cat = new Animal("Cat");
        Animal dog = new Animal("Dog");

        hello.hello(cat); // 냐옹
        hello.hello(dog); // 멍멍
    }
}

 

동작 자체에는 문제가 없지만 기능을 추가할 때 문제가 일어난다. 고양이와 개 외에 양이나 사자를 추가하게 되면 어떻게 코드를 작성해야 할까? 아마도 각 객체의 필드 변수에 맞게 if문을 분기하여 구성해줘야 한다. 

 

public class Main {
    public static void main(String[] args) {
        HelloAnimal hello = new HelloAnimal();

        Animal cat = new Animal("Cat");
        Animal dog = new Animal("Dog");

        Animal sheep = new Animal("Sheep");
        Animal lion = new Animal("Lion");

        hello.hello(cat); // 냐옹
        hello.hello(dog); // 멍멍
        hello.hello(sheep); 
        hello.hello(lion);
    }
}

class HelloAnimal {
	// 기능을 확장하기 위해서는 클래스 내부 구성을 일일히 수정해야 하는 번거로움이 생긴다.
    void hello(Animal animal) {
        if (animal.type.equals("Cat")) {
            System.out.println("냐옹");
        } else if (animal.type.equals("Dog")) {
            System.out.println("멍멍");
        } else if (animal.type.equals("Sheep")) {
            System.out.println("메에에");
        } else if (animal.type.equals("Lion")) {
            System.out.println("어흥");
        }
        // ...
    }
}

 

이런 식으로 코드를 구성한다면, 동물이 추가될 때마다 계속 코드를 일일이 변경해하는 번거로운 작업이 생기게 된다. 이는 처음 설계부터 잘못되었기에 일어나는 현상이다. 

 

따라서 처음부터 OCP 설계 원칙에 따라 적절한 추상화 클래스를 구성하고 이를 상속하여 확장시키는 관계로 구성하면 변경에는 닫히고 추가에는 열려있는 프로그램을 작성할 수 있다. OCP 대로 추상화 설계를 할 것인가에 대해서는 다음의 규칙을 이행한다. 

  • 먼저 변경(확장)될 것과 변경하지 않을 것을 엄격히 구분한다. 
  • 이 모듈을 만나는 지점에는 추상화를 정의한다. 
  • 구현체에 의존하기보다는 정의한 추상화에 의존하도록 코드를 작성한다. 

 

// 추상화
abstract class Animal {
    abstract void speak();
}

class Cat extends Animal { // 상속
    void speak() {
        System.out.println("냐옹");
    }
}

class Dog extends Animal { // 상속
    void speak() {
        System.out.println("멍멍");
    }
}

// 추상클래스를 상속만 하면 메소드 강제 구현 규칙으로 규격화만 하면 확장에 제한 없다 (opened)
class Sheep extends Animal {
    void speak() {
        System.out.println("매에에");
    }
}

class Lion extends Animal {
    void speak() {
        System.out.println("어흥");
    }
}

// 기능 확장으로 인한 클래스가 추가되어도, 더이상 수정할 필요가 없어진다 (closed)
class HelloAnimal {
    void hello(Animal animal) {
        animal.speak();
    }
}

public class Main {
    public static void main(String[] args) {
        HelloAnimal hello = new HelloAnimal();

        Animal cat = new Cat();
        Animal dog = new Dog();

        Animal sheep = new Sheep();
        Animal lion = new Lion();

        hello.hello(cat); // 냐옹
        hello.hello(dog); // 멍멍
        hello.hello(sheep); // 매에에
        hello.hello(lion); // 어흥
    }
}

위와 같이 구성하게 되면 기능이 추가되었을 때도  코드의 수정 없이 확장이 가능하게 된다. 따라서 양, 사자 클래스가 추가돼도 HelloAnimal 클래스의 코드 수정 없이 정상적으로 기능이 확장됨을 확인할 수 있다. 

 

이런 OCP 원칙을 잘 따르는 예시가 자바의 DB인터페이스인 JDBC이다.

만약 자바 애플리케이션에서 사용하고 있는 데이터베이스를 MySQL에서 Oracle로 바꾸고 싶다면, 복잡한 하드 코딩 없이 그냥 connection 객체 부분만 교체해 주면 된다. 즉 자바 애플리케이션은 DB라고 하는 주변의 변화에 닫혀 있는 것이며 데이터베이스를 쉽게 교체한다는 것은 데이터베이스가 자신의 확장에는 열려있다는 말이 된다. 

 

 

 

 

 

리스코프 치환 원칙 - LSP

리스코프 치환 원칙은 올바른 상속 관계를 특징을 정의하기 위해 발표한 것으로 서브 타입은 언제나 기반 타입으로 교체할 수 있다는 것을 뜻한다. 교체할 수 있다는 의미는 자식 클래스에서 최소 부모 클래스에서 가능한 행위는 수행이 보장되어야 한다는 의미이다. 즉 부모 클래스의 인스턴스를 사용하는 위치에 자식 클래스의 인스턴스를 대신 사용해도 코드가 원래대로 작동해야 한다는 의미이다. 

 

간단하게 말하자면 부모 클래스의 행동 규약을 자식 클래스가 위반하면 안 된다는 것인데, 행동 규약을 위반한다는 것은 자식 클래스가 오버라이딩 할 때, 잘못 재정의 하면 리스코프 치환 원칙을 위배할 수 있다는 의미이다. 

 

자식 클래스가 오버라이딩을 잘못하는 경우는 다음과 같이 크게 2가지로 나뉜다.

  • 자식 클래스가 부모 클래스의 메서드 시그니처를 자기 멋대로 변경
class Animal {
    int speed = 100;

    int go(int distance) {
        return speed * distance;
    }
}

class Eagle extends Animal {
    String go(int distance, boolean flying) {
        if (flying)
            return distance + "만큼 날아서 갔습니다.";
        else
            return distance + "만큼 걸어서 갔습니다.";
    }
}

public class Main {
    public static void main(String[] args) {
        Animal eagle = new Eagle();
        eagle.go(10, true);
    }

윗 코드처럼 Animal 클래스를 상속하는 Eagle 자식 클래스가 go() 메서드를 자기 멋대로 코드를 재사용한답시고 메서드의 타입과 매개변수의 개수를 바꿔버렸다. 이는 메서드 오버로딩을 부모가 아닌 자식 클래스에서 해버렸기 때문에 LSP 원칙에 어긋난다. 

 

  • 자식 클래스가 부모 클래스의 의도와 다르게 메서드를 오버라이딩 
class NaturalType {
    String type;
    NaturalType(Animal animal) {
        // 생성자로 동물 이름이 들어오면, 정규표현식으로 매칭된 동물 타입을 설정한다.
        if(animal instanceof Cat) {
            type = "포유류";
        } else {
            // ...
        }
    }

    String print() {
        return "이 동물의 종류는 " + type + " 입니다.";
    }
}

class Animal {

    NaturalType getType() {
        NaturalType n = new NaturalType(this);
        return n;
    }
}

class Cat extends Animal {
	
    @Override
    NaturalType getType() {
        return null;
    }

    String getName() {
        return "이 동물의 종류는 포유류 입니다.";
    }
}

public class Main {
    public static void main(String[] args) {
        Animal cat = new Cat();
        String result = cat.getType().print();
        System.out.println(result); // "이 동물의 종류는 포유류 입니다."
    }
}

해당 코드는 자기 멋대로 자식 클래스에 부모 메서드인 getType()의 반환 값을 null로 오버라이딩 설정하여 메서드를 사용하지 못하게 설정하고, 대신 getName이라는 메서드를 만들어 한번에 출력하도록 설정한 것이다. 해당 코드를 돌려보면 NullPointerException 예외가 발생하게 된다. 

 

즉 해당 코드는 자식 클래스로 부모 클래스의 내용을 상속하는데, 기존 코드에서 보장하던 조건을 수정하거나 적용시키지 않아서, 기존 부모 클래스를 사용하는 코드에서 예상하지 않은 오류를 발생시켰고 LSP 원칙에 어긋났다고 볼 수 있다. 

 

 

인터페이스 분리 원칙 - ISP

인터페이스 분리 법칙이란 범용적인 인터페이스보다는 사용자가 실제로 사용하는 interface를 만들어야 한다는 의미로 인터페이스를 사용에 맞게끔 각각 분리해야 한다는 설계원칙이다. 

 

인터페이스의 추상 메서드들을 범용적으로 이것저것 구현한다면, 그 인터페이스를 상속받은 클래스는 자신이 사용하지 않은 인터페이스 마저 억지로 구현해야 하는 상황이 올 수 있다. 사용하지 않는 인터페이스의 추상 메서드가 변경된다면 클래스에서도 수정이 필요하게 된다. 즉 인터페이스 분리 원칙이란 인터페이스를 잘게 분리함으로써, 클라이언트의 목적과 용도에 적합한 인터페이스를 제공하는 것에 의미가 있다.

 

 

 

다음 예제는 Pet 인터페이스를 좀 더 잘게 나눔으로써 각 애완동물 클래스의 역할과 맞게 상속시켜 줌으로써, 클래스의 기능을 쉽게 파악할 수 있다는 이점이 있으며, 유연하게 객체의 기능을 확장하거나 수정할 수 있게 된다. 

 

인터페이스 분리 원칙은 마치 SRP와 비슷하게 느껴지지만 SRP는 클래스 단일 책임을 강조한다면 ISP는 인터페이스 단일 책임을 강조한다고 할 수 있다. 

 

이번에는 ISP 원칙 위반 예제와 그 예제를 수정해 보겠다. 

 

interface ISmartPhone {
    void call(String number); // 통화 기능
    void message(String number, String text); // 문제 메세지 전송 기능
    void wirelessCharge(); // 무선 충전 기능
    void AR(); // 증강 현실(AR) 기능
    void biometrics(); // 생체 인식 기능
}

스마트폰 인터페이스에는 스마트폰이라면 가지고 있을 통화나 메시지 기능 외의 무선 충전.. 등의 기능을 포함하고 있다.  만약 갤럭시 S20, S21 클래스를 구현한다면 최신 스마트폰 기종인 만큼 객체의 동작 모두 필요함으로 ISP 원칙을 만족하게 된다. 하지만 최신 기종뿐만 아니라 구형 기종의 스마트폰 클래스를 가뤄야 하는 문제가 있다. 만약 갤럭시 S3 클래스를 구현해야 한다면 무선충전이나 생체인식 같은 기능은 포함되지 않는다.

class S20 implements ISmartPhone {
    public void call(String number) {
    }

    public void message(String number, String text) {
    }

    public void wirelessCharge() {
    }

    public void AR() {
    }

    public void biometrics() {
    }
}

class S21 implements ISmartPhone {
    public void call(String number) {
    }

    public void message(String number, String text) {
    }

    public void wirelessCharge() {
    }

    public void AR() {
    }

    public void biometrics() {
    }
}

 

이렇게 되면 추상 메서드 구현 규칙상 오버라이딩 하되, 메서드 내부는 빈 공간으로 두거나 혹은 예외가 발생하도록 구성하여야 한다. 즉 필요하지도 않은 기능을 어쩔 수 없이 구현해야 하는 낭비가 발생된다. 

 

class S3 implements ISmartPhone {
    public void call(String number) {
    }

    public void message(String number, String text) {
    }

    public void wirelessCharge() {
        System.out.println("지원 하지 않는 기능 입니다.");
    }

    public void AR() {
        System.out.println("지원 하지 않는 기능 입니다.");
    }

    public void biometrics() {
        System.out.println("지원 하지 않는 기능 입니다.");
    }
}

 

따라서 각각의 기능에 맞게 인터페이스를 잘게 분리하도록 구성한다. 잘게 분리된 인터페이스를 클래스가 지원되는 기능만을 선별하여 implements 하면 ISP원칙이 지켜지게 된다. 

 

interface IPhone {
    void call(String number); // 통화 기능
    void message(String number, String text); // 문제 메세지 전송 기능
}

interface WirelessChargable {
    void wirelessCharge(); // 무선 충전 기능
}

interface ARable {
    void AR(); // 증강 현실(AR) 기능
}

interface Biometricsable {
    void biometrics(); // 생체 인식 기능
}
class S21 implements IPhone, WirelessChargable, ARable, Biometricsable {
    public void call(String number) {
    }

    public void message(String number, String text) {
    }

    public void wirelessCharge() {
    }

    public void AR() {
    }

    public void biometrics() {
    }
}

class S3 implements IPhone {
    public void call(String number) {
    }

    public void message(String number, String text) {
    }
}

 

ISP원칙에서 중요한 점은 한번 인터페이스를 분리하여 구성해 놓고 나중에 수정사항이 생겨서 또 인터페이스들을 분리하는 행위는 삼가여야 한다는 점이다. 이미 구현되어 있는 프로젝트에 또 인터페이스를 분리한다면 해당 인터페이스를 구현하고 있는 클래스들과 클라이언트에서 문제가 일어날 수 있기 때문이다. 

 

인터페이스란 건 한번 구성하면 웬만하면 변하면 안 되는 정책 같은 개념이므로 꼭 기억해 두자. 

 

 

 

 

 

의존 역전 원칙 - DIY

의존 역전 원칙은 객체에서 어떤 class을 참조해서 사용해야 하는 상황이 생긴다면 그 class을 직접 참조하는 것이 아니라 그 대상의 상위 요소(추상 클래스 혹은 인터페이스)로 참조하라는 원칙이다. 

 

객체들이 서로 정보를 주고받을 때에는 의존 관계가 형성되는데, 이때 객체들은 나름대로의 원칙을 갖고 정보를 주고받고 있다. 나름대로의 원칙이란 추상성이 낮은 클래스보다 추상성이 높은 클래스와 통신한다는 것을 의미하는데 이것이 DIP 원칙이다. 

 

쉽게 말하자면 클라이언트가 상속 관계로 이루어진 모듈을 가져다 사용할 때, 하위 모듈을 직접 인스턴스 해서 가져다 쓰지 말라는 의미이다. 왜냐하면 그렇게 할 경우 하위 모듈의 구체적인 내용에 클 아이언트가 의존하게 되어 하위 모듈에 변화가 있을 때마다 클라이언트나 상위 모듈의 코드를 자주 수정해야 하기 때문이다. 

 

다음은 DIP원칙 위반 예제와 이를 수정하는 것에 대해 살펴보자 

 

class OneHandSword {
    final String NAME;
    final int DAMAGE;

    OneHandSword(String name, int damage) {
        NAME = name;
        DAMAGE = damage;
    }

    int attack() {
        return DAMAGE;
    }
}

class TwoHandSword {
    // ...
}

class BatteAxe {
    // ...
}

class WarHammer {
    // ...
}
class Character {
    final String NAME;
    int health;
    OneHandSword weapon; // 의존 저수준 객체

    Character(String name, int health, OneHandSword weapon) {
        this.NAME = name;
        this.health = health;
        this.weapon = weapon;
    }

    int attack() {
        return weapon.attack(); // 의존 객체에서 메서드를 실행
    }

    void chageWeapon(OneHandSword weapon) {
        this.weapon = weapon;
    }

    void getInfo() {
        System.out.println("이름: " + NAME);
        System.out.println("체력: " + health);
        System.out.println("무기: " + weapon);
    }
}

위에는 RPG게임에서 자주 나오는 무기들을 클래스 화한 코드이다. 그리고 이러한 무기들을 장착할 Character 클래스가 있다. 이 캐릭터 클래스는 인스턴스화될 때, 캐릭터의 이름과 체력 그리고 장착하고 있는 무기를 입력값으로 받아 초기화한다. 한손검도 한 가지만 있는 게 아니라 강철검, 미스릴검 같이 다양한 타임의 검이 올 수 있기 때문에 클래스 내에 필드 변수로써 OneHandSword클래스 변수 타입을 저장해 놓고, attack() 메서드를 수행하면 OneHandSword 클래스의 메서드가 실행되어 대미지가 가하는 형태이다. 

 

즉 인스턴스가 생성 시 OneHandSword에 의존성을 가지고 되어 공격동작을 하게 되면 attack() 메서드 역시 OneHandSword에 의존성을 가지게 된다. 

 

하지만 무기엔 한손검만 있는 것이 아니다. 위에 보이듯 양손검, 도끼, 망치 등의 여러 무기를 장착하게 되려면 클래스 필드 변수 타입을 교체해줘야 한다. 해당 코드의 문제는 이미 구현된 하위 모듈을 의존하고 있다는 것이다. 이것을 DIP 원칙을 통해 하위 모듈이 아닌 상위 모듈에 의존하도록 리팩터링 한다. 

 

// 고수준 모듈
interface Weaponable {
    int attack();
}

class OneHandSword implements Weaponable {
    final String NAME;
    final int DAMAGE;

    OneHandSword(String name, int damage) {
        NAME = name;
        DAMAGE = damage;
    }

    public int attack() {
        return DAMAGE;
    }
}

class TwoHandSword implements Weaponable {
	// ...
}


class BatteAxe implements Weaponable {
	// ...
}

class WarHammer implements Weaponable {
	// ...
}

class Character {
    final String NAME;
    int health;
    Weaponable weapon; // 의존을 고수준의 모듈로

    Character(String name, int health, Weaponable weapon) {
        this.NAME = name;
        this.health = health;
        this.weapon = weapon;
    }

    int attack() {
        return weapon.attack();
    }

    void chageWeapon(Weaponable weapon) {
        this.weapon = weapon;
    }

    void getInfo() {
        System.out.println("이름: " + NAME);
        System.out.println("체력: " + health);
        System.out.println("무기: " + weapon);
    }
}

 

DIP원칙을 통해 무기 변경에 따라 Character의 코드 변경이 할 필요가 없고, 또 다른 타입의 무기 확장에도 무리가 없으니 OCP 원칙에도 준수한 것을 알 수 있다. 

 


 

참조 :

https://inpa.tistory.com/entry/OOP-%F0%9F%92%A0-%EC%95%84%EC%A3%BC-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EB%8A%94-SRP-%EB%8B%A8%EC%9D%BC-%EC%B1%85%EC%9E%84-%EC%9B%90%EC%B9%99

 

💠 완벽하게 이해하는 SRP (단일 책임 원칙)

단일 책임 원칙 - SRP (Single Responsibility Principle) 단일 책임 원칙(SRP)는 객체는 단 하나의 책임만 가져야 한다는 원칙을 말한다. 여기서 '책임' 이라는 의미는 하나의 '기능 담당'으로 보면 된다. 즉,

inpa.tistory.com

https://inpa.tistory.com/entry/OOP-%F0%9F%92%A0-%EC%95%84%EC%A3%BC-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EB%8A%94-OCP-%EA%B0%9C%EB%B0%A9-%ED%8F%90%EC%87%84-%EC%9B%90%EC%B9%99

 

💠 완벽하게 이해하는 OCP (개방 폐쇄 원칙)

개방 폐쇄 원칙 - OCP (Open Closed Principle) 개방 폐쇄의 원칙(OCP)이란 기존의 코드를 변경하지 않으면서, 기능을 추가할 수 있도록 설계가 되어야 한다는 원칙을 말한다. 보통 OCP를 확장에 대해서는

inpa.tistory.com

https://inpa.tistory.com/entry/OOP-%F0%9F%92%A0-%EC%95%84%EC%A3%BC-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EB%8A%94-LSP-%EB%A6%AC%EC%8A%A4%EC%BD%94%ED%94%84-%EC%B9%98%ED%99%98-%EC%9B%90%EC%B9%99#thankYou

 

💠 완벽하게 이해하는 LSP (리스코프 치환 원칙)

리스코프 치환 원칙 - LSP (Liskov Substitution Principle) 리스코프 치환 원칙은 1988년 바바라 리스코프(Barbara Liskov)가 올바른 상속 관계의 특징을 정의하기 위해 발표한 것으로, 서브 타입은 언제나 기반

inpa.tistory.com

https://inpa.tistory.com/entry/OOP-%F0%9F%92%A0-%EC%95%84%EC%A3%BC-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EB%8A%94-ISP-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4-%EB%B6%84%EB%A6%AC-%EC%9B%90%EC%B9%99

 

💠 완벽하게 이해하는 ISP (인터페이스 분리 원칙)

인터페이스 분리 원칙 - ISP (Interface Segregation Principle) ISP 원칙이란 범용적인 인터페이스 보다는 클라이언트(사용자)가 실제로 사용하는 Interface를 만들어야 한다는 의미로, 인터페이스를 사용에

inpa.tistory.com

https://inpa.tistory.com/entry/OOP-%F0%9F%92%A0-%EC%95%84%EC%A3%BC-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EB%8A%94-DIP-%EC%9D%98%EC%A1%B4-%EC%97%AD%EC%A0%84-%EC%9B%90%EC%B9%99

 

💠 완벽하게 이해하는 DIP (의존 역전 원칙)

의존 역전 원칙 - DIP (Dependency Inversion Principle) DIP 원칙이란 객체에서 어떤 Class를 참조해서 사용해야하는 상황이 생긴다면, 그 Class를 직접 참조하는 것이 아니라 그 대상의 상위 요소(추상 클래스

inpa.tistory.com

 

'Computer Theory > Design partterns' 카테고리의 다른 글

팩토리 패턴  (0) 2023.09.20
데코레이터 패턴  (0) 2023.09.18
옵저버 패턴  (0) 2023.09.04
전략 패턴  (0) 2023.08.30

'Computer Theory/Design partterns'의 다른글

  • 현재글 디자인 패턴과 SOLID 원칙

관련글