프로그래밍/JAVA Spring

[JAVA 자바] chat gpt와 SOLID 원칙에 대해 공부를 해보았다(2)

hectick 2023. 3. 25. 13:51

 

👇chat gpt와 SOLID 원칙에 대해 공부를 해보았다 1편👇

 

[JAVA 자바] chat gpt와 SOLID 원칙에 대해 공부를 해보았다(1)

요즘 책을 읽으면서도, 강의를 들으면서도 가끔 SOLID 원칙이 언급되길래, 한번 chat gpt와 대화하며 공부한 내용을 정리해 보았다. 🤔 SOLID 원칙이란? 객체 지향 프로그래밍에서 유지보수와 확장을

finger-ineedyourhelp.tistory.com

 

지난 포스팅에 이어, 이번 포스팅에서는 인터페이스 분리 원칙(ISP)와 의존관계 역전원칙(DIP)를 다룬다.

 

 


 

🍏 I 인터페이스 분리 원칙 (ISP, Interface segregation principle)

 

클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다. 즉, 인터페이스를 작게 분리하여 클라이언트가 필요한 메서드들만을 이용할 수 있게 해야 한다.

 

 

다음은 인터페이스 분리 원칙을 위반한 코드이다.

    public interface Printer {
        public void print();
        public void scan();
        public void fax();
    }

여기서 프린터기는 프린트 하는 기능, 스캔하는 기능, 팩스를 보내는 기능을 가진다. 하지만 예를 들어 집에서 쓰는 일반적인 프린터기를 생각해보자. 이 프린터기는 팩스 기능을 지원하지 않을 수 있다. 하지만, 위의 인터페이스를 사용하게 된다면, 불필요한 팩스 기능도 함께 구현해야 한다. 

 

이를 해결하기 위해서는 Printer 인터페이스를 좀 더 분리 해볼 수 있다. 다음처럼 인터페이스를 분리하면, 프린트를 하는 기능, 스캔을 하는 기능, 팩스를 보내는 기능 중 필요한 기능들만 골라서 구현할 수 있게 된다. 불필요한 메서드를 구현 하지 않아도 되고, 불필요한 메서드를 호출하는 일도 없어진다.

    public interface Printer {
        public void print();
    }

    public interface Scanner {
        public void scan();
    }

    public interface Fax {
        public void fax();
    }

    public class HomePrinter implements Printer, Scanner {
        public void print() {
            // 프린팅 기능 구현
        }

        public void scan() {
            // 스캔 기능 구현
        }
    }

 

이처럼 인터페이스 분리 원칙을 잘 지킨다면, 분리한 인터페이스들을 재사용하기 쉬워질 뿐더러, 꼭 필요한 기능만을 제공할 수 있게 된다.

 

 

 

🍏 D 의존관계 역전 원칙 (DIP, Dependency inversion principle)

 

추상화에 의존해야 하며, 구체화에 의존하면 안 된다.

 

 

다음은 의존관계 역전 원칙을 위반한 코드이다. 이 원칙을 위반하게 되면, 코드의 재사용성이 감소되고, 유지보수성도 떨어지게 된다.

    public class ShoppingCart {
        private PayPalService payPalService;

        public ShoppingCart() {
            payPalService = new PayPalService();
        }

        public void checkout(double amount) {
            payPalService.pay(amount);
        }
    }

    public class PayPalService {
        public void pay(double amount) {
            // PayPal로 지불하는 기능
        }
    }

여기서 ShoppingCart는 PayPalService에 의존하고 있다. 이렇게 되면, 지불방법이 PayPal이 아니라 다른 방법으로 변경되게 될 때, ShoppingCart 클래스의 코드도 변경되게 된다. 

 

이를 의존관계 역전 원칙을 지키도록 수정하기 위해서는, 결제 서비스에 대한 인터페이스를 정의하고, PayPalService가 이를 구현하도록 변경해야한다. 그리고 ShoppingCart는 구체클래스가 아니라, 지불방법에 대한 인터페이스에 의존해야 한다. 이런 식으로 변경한다면, ShoppingCar 클래스는 PayPal이 아닌 다른 결제 서비스도 코드의 변경없이 사용할 수 있게 된다.

    public interface PaymentService {
        void pay(double amount);
    }

    public class ShoppingCart {
        private PaymentService paymentService;

        public ShoppingCart(PaymentService paymentService) {
            this.paymentService = paymentService;
        }

        public void checkout(double amount) {
            paymentService.pay(amount);
        }
    }

    public class PayPalService implements PaymentService {
        public void pay(double amount) {
            // connect to PayPal API and make payment
        }
    }

 

이렇게 구체적인 구현이 아니라 추상적인 인터페이스에 의존하게 된다면 코드 변경이 쉬워진다. 즉, 구현체가 변경되더라도 인터페이스가 변경되지 않는 한 상위 수준 모듈에 영향을 주지 않는다. 따라서, 코드의 유지보수성과 확장성, 재사용성이 향상된다.

 

그렇다면, 구체 클래스를 직접 사용하는 것은 항상 원칙에 위반되는 것일까? chat gpt는 다음과 같이 말했다.

 

아니요, 구체 클래스를 직접 사용하는 것이 항상 DIP 위반이라는 것은 아닙니다. DIP는 구체 클래스보다는 추상화된 인터페이스나 추상 클래스에 의존하도록 권장하는 원칙입니다. 하지만 모든 경우에 추상화된 인터페이스나 추상 클래스를 사용할 필요는 없습니다. 예를 들어, 자주 변경되지 않고 안정적인 라이브러리나 프레임워크 같은 경우에는 구체 클래스를 직접 사용해도 괜찮습니다. 따라서, DIP는 추상화된 인터페이스나 추상 클래스를 사용할 것을 권장하지만, 모든 경우에 그것을 사용해야 한다는 것은 아닙니다. 상황에 따라서 적절한 판단이 필요합니다.

 

그럼 구체 클래스를 직접 사용하는 예시로는 무엇이 있을까?

 

예를 들어, 자바에서는 String 클래스를 직접 사용하는 것이 필요한 경우가 많습니다. 이 경우, String 클래스는 하위 수준의 모듈에 속하면서도 매우 기본적이며 널리 사용되는 클래스이기 때문에, DIP를 위반하지 않는 것으로 간주됩니다.
그러나, 가능하면 DIP를 준수하고 인터페이스를 사용하는 것이 좋습니다. 인터페이스를 사용하면 코드의 유연성과 재사용성을 높일 수 있기 때문입니다.

 


 

SOLID 원칙은 전체적으로 객체의 책임과 역할을 잘 분리하고, 인터페이스를 효과적으로 사용하는 것과 연관이 있는 것 같다. 아직까진 인터페이스를 잘 활용한 적이 없던 것 같은데, 앞으로 여러모로 써보기를 도전해봐야겠다.