객체 지향 설계

사나이장대산·2024년 11월 13일

Spring

목록 보기
16/26

SOLID 원칙

객체 지향 설계의 5가지 기본 원칙, 소프트웨어 설계에서 유지보수성, 확장성, 유연성을 높이기 위한 지침을 제공한다.

  • SOLID 원칙의 종류
    • 단일 책임 원칙 SRP(Single Responsibility Principle)
      • 하나의 클래스는 하나의 책임만 가져야 한다.
        • 클래스는 한 가지 기능에 집중해야 하며, 그 외의 기능을 담당하지 않아야 한다
      • 예시
        • User 클래스는 사용자 정보 관리, 로그인 및 데이터베이스 저장 책임을 동시에 가지고 있다.

          public class User {
          		private String name; // 사용자 정보
              public void login() { /* 로그인 기능 */ }
              public void saveUser() { /* 데이터베이스 저장 기능 */ }
          }
          
      • 단일 책임 원칙 적용
        public class User { /* 사용자 정보 관리 */ }
        
        public class AuthService {
            public void login(User user) { /* 로그인 기능 */ }
        }
        
        public class UserRepository {
            public void saveUser(User user) { /* 데이터베이스 저장 */ }
        }
        
        • 실제로는 상황에 따라 책임의 크기가 달라진다.
        • 클래스가 변경될 때 파급 효과가 작으면 된다.
  • 개방 폐쇄 원칙 OCP(Open Closed Principle)
    • 소프트웨어 요소는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다.
      • 새로운 기능을 추가할 때 기존 코드를 수정하지 않고, 확장할 수 있도록 설계해야 한다.
    • 예시
      • 새로운 도형이 추가될 때마다 AreaCalculator 클래스를 수정해야 한다.

        public class Shape {
            public String type;
        }
        
        public class AreaCalculator {
            public double calculate(Shape shape) {
                if (shape.type.equals("circle")) {
                    return /* 원의 넓이 계산 */;
                } else if (shape.type.equals("square")) {
                    return /* 사각형의 넓이 계산 */;
                }
            }
        }
    • 개방 폐쇄 원칙 적용
      public interface Shape {
          double calculateArea();
      }
      
      public class Circle implements Shape {
          public double calculateArea() { return /* 원의 넓이 계산 */; }
      }
      
      public class Square implements Shape {
          public double calculateArea() { return /* 사각형의 넓이 계산 */; }
      }
      
      public class AreaCalculator {
          public double calculate(Shape shape) {
              return shape.calculateArea();
          }
      }
      • 새로운 도형이 추가되더라도 Shape 인터페이스만 구현하면 된다.
      • AreaCalculator는 수정할 필요가 없다.
    • 다형성을 활용하여 해결한다.
      • 인터페이스를 implements 하여 구현한 새로운 클래스를 만들어서 새로운 기능을 구현한다.
      • 역할(도형)과 구현(원, 사각형, 삼각형 등)을 분리하면 된다.
    • 문제점
      // Circle을 계산하는 경우
      public class Main {
      		public static void main(String[]) {
      		
      				AreaCalculator areaCalculator = new AreaCalculator();
      				Circle circle = new Circle();
      				
      				areaCalculator.calculate(circle);
      			
      		}
      }
      
      // Square를 계산하는 경우
      public class Main {
      		public static void main(String[]) {
      		
      				AreaCalculator areaCalculator = new AreaCalculator();
      				// Circle circle = new Circle();
      				Square square = new Square();
      				
      				areaCalculator.calculate(square);
      			
      		}
      }
      • 구현 객체를 변경하기 위해서는 해당 코드를 사용하는 클라이언트측의 코드를 변경해야 한다.
      • 객체의 생성, 사용 등을 자동으로 설정해주는 무엇인가가 필요하다.
        • Spring Container의 역할
  • 리스코프 치환 원칙 LSP(Liskov Substitution Principle)
    • 자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 한다.
      • 부모 클래스를 사용하는 곳에서 자식 클래스를 사용해도 프로그램의 동작에 문제가 없어야 한다.
    • 예시
      • ElectricCarCar 클래스를 상속 받았지만, accelerate() 를 사용할 수 없다. LSP 위반

        class Car {
            public void accelerate() {
                System.out.println("자동차가 휘발유로 가속합니다.");
            }
        }
        
        class ElectricCar extends Car {
            @Override
            public void accelerate() {
                throw new UnsupportedOperationException("전기차는 이 방식으로 가속하지 않습니다.");
            }
        }
        
        public class Main {
            public static void main(String[] args) {
                Car car = new Car();
                car.accelerate(); // "자동차가 가속합니다."
        
                Car electricCar = new ElectricCar();
                electricCar.accelerate(); // UnsupportedOperationException 발생
            }
        }
    • 리스코프 치환 원칙 적용
      // 가속 기능(역할)을 인터페이스로 분리
      interface Acceleratable {
          void accelerate();
      }
      
      class Car implements Acceleratable {
          @Override
          public void accelerate() {
              System.out.println("내연기관 자동차가 가속합니다.");
          }
      }
      
      class ElectricCar implements Acceleratable {
          @Override
          public void accelerate() {
              System.out.println("전기차가 배터리로 가속합니다.");
          }
      }
      
      public class Main {
          public static void main(String[] args) {
              Acceleratable car = new Car();
              car.accelerate(); // "내연기관 자동차가 가속합니다."
      
              Acceleratable electricCar = new ElectricCar();
              electricCar.accelerate(); // "전기차가 배터리로 가속합니다."
          }
      }
      • 인터페이스를 구현한 구현체를 믿고 사용할 수 있도록 만들어준다.
      • 엑셀은 앞으로 가는 기능이다. 만약 뒤로 간다면 LSP를 위반한다.
  • 인터페이스 분리 원칙 ISP(Interface Segregation Principle)
    • 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
      • 클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.
      • 즉, 하나의 큰 인터페이스보다는 여러 개의 작은 인터페이스로 분리해야 한다.
    • 예시
      • Dog 클래스는 fly() 메서드를 사용하지 않지만 구현해야 한다.

        public interface Animal {
            void fly();
            void run();
            void swim();
        }
        
        public class Dog implements Animal {
            public void fly() { /* 사용하지 않음 */ }
            public void run() { /* 달리기 */ }
            public void swim() { /* 수영 */ }
        }
    • 인터페이스 분리 원칙 적용
      public interface Runnable {
          void run();
      }
      
      public interface Swimmable {
          void swim();
      }
      
      public class Dog implements Runnable, Swimmable {
          public void run() { /* 달리기 */ }
          public void swim() { /* 수영 */ }
      }
      • 인터페이스가 명확해진다.
      • Spring의 기능들은 대부분 인터페이스로 분리 되어 있다.
  • 의존관계 역전 원칙 DIP(Dependency Inversion Principle)
    • 구체적인 클래스에 의존하지 말고, 인터페이스나 추상 클래스에 의존하도록 설계해야 한다.
    • 예시
      • NotificationServiceEmailNotifier 클래스를 의존한다.

        // Email 알림 클래스
        class EmailNotifier {
            public void sendEmail(String message) {
                System.out.println("Email 알림: " + message);
            }
        }
        
        // 알림 시스템
        class NotificationService {
            private EmailNotifier emailNotifier;
        
            public NotificationService() {
        		    // 구체적인 클래스인 EmailNotifier에 의존
                this.emailNotifier = new EmailNotifier();
            }
        
            public void sendNotification(String message) {
                emailNotifier.sendEmail(message);
            }
        }
        
        public class Main {
            public static void main(String[] args) {
                NotificationService service = new NotificationService();
                service.sendNotification("안녕하세요! 이메일 알림입니다.");
            }
        }
      • 이메일 알림이 아닌 SMS 알림과 같은 기능이 추가되면 NotificationService 는 수정되어야 한다. DIP 위반

    • 의존관계 역전 원칙 적용
      // 알림 인터페이스(추상화)
      interface Notifier {
          void send(String message);
      }
      
      // Email 알림 클래스
      class EmailNotifier implements Notifier {
          @Override
          public void send(String message) {
              System.out.println("Email 알림: " + message);
          }
      }
      
      // SMS 알림 클래스
      class SMSNotifier implements Notifier {
          @Override
          public void send(String message) {
              System.out.println("SMS 알림: " + message);
          }
      }
      
      // 알림 서비스 (높은 수준 모듈)
      class NotificationService {
      		// 추상화된 인터페이스에 의존
          private Notifier notifier;
      
          // 의존성 주입 (생성자를 통해 주입)
          public NotificationService(Notifier notifier) {
              this.notifier = notifier;
          }
      
          public void sendNotification(String message) {
      		    // notifier가 어떤 구현체인지 상관하지 않음
              notifier.send(message);
          }
      }
      
      public class Main {
          public static void main(String[] args) {
              // Email 알림을 사용
              Notifier emailNotifier = new EmailNotifier();
              NotificationService emailService = new NotificationService(emailNotifier);
              emailService.sendNotification("안녕하세요! 이메일 알림입니다.");
      
              // SMS 알림을 사용
              Notifier smsNotifier = new SMSNotifier();
              NotificationService smsService = new NotificationService(smsNotifier);
              smsService.sendNotification("안녕하세요! SMS 알림입니다.");
          }
      }
      • 추상화된 Notifier 인터페이스에만 의존한다.
        • 새로운 알림 방식이 추가되어도 NotificationService 는 변경되지 않아도 된다.
      • 필요한 Notifier 객체를 외부에서 주입받는다.
        • NotificationService는 어떤 알림 방식을 사용할지에 대한 세부 사항을 몰라도 되므로, 의존성이 약해진다.
      • 모듈간의 결합도를 낮추고 유연성과 확장성을 높일 수 있다.
      • 서로의 변경 사항에 독립적이어서 변경에 유연하다.

객체 지향의 핵심은 다형성에 있다. 하지만 다형성 만으로는 OCP, DIP를 지킬 수 없다.

Spring과 객체 지향

Spring은 다형성 만으로는 해결하지 못했던 객체 지향 설계 원칙 중 OCP, DIP를 IOC, DI를 통해 가능하도록 만들어준다.

  • Spring의 역할

    • OCP, DIP 원칙을 지킬 수 있도록 도와준다.
    • 코드의 변경 없이 기능을 확장할 수 있도록 만들어 준다.
    • 개발자가 마치 레고 블록을 조립하듯이 원하는 구성 요소를 손쉽게 교체하고 결합할 수 있도록 만들어준다.
  • 다시보는 Spring Framework 의 등장 배경

    • OCP, DIP 원칙을 지키며 개발하면 개발자가 할일이 아주 많아진다.
    • Framework로 만들어서 개발자가 편하게 개발할 수 있도록 만들어졌다.
  • 개발자의 역할

    • 이상적으로는 모든 설계를 인터페이스로 만들어야 코드가 유연하게 변경이 가능해진다.
      • 정해진 비지니스 로직이나 사용할 기술이 없는 상황에서도 개발할 수 있는 장점이 생긴다.

        실무에서는 추상화 과정에서 비용(시간)이 발생하기 때문에 기능을 확장할 가능성이 없다면 구현 클래스를 직접 사용하고 추후 변경된다면 인터페이스로 리팩토링 하면된다.

profile
사나이 張大山 포기란 없다.

0개의 댓글