본문 바로가기
JAVA

[JAVA] - 객체 지향 설계 5원칙 - S.O.L.I.D & 특징 4가지 (추상화, 상속, 다형성, 캡슐화)

by nam_ji 2024. 3. 13.

객체 지향 설계 특징 4가지 (추상화, 상속, 다형성, 캡슐화) & 5원칙 - S.O.L.I.D

1. 객체 지향 프로그래밍 4가지 특징

1) 객체지향이란

  • 객체 지향 프로그래밍 (Object - Oriented Programming)이란 컴퓨터 프로그램을 어떤 데이터를 입력받아 순서대로 처리하고 결과를 도출하는 명령어들의 목록으로 보는 시각에서 벗어나 여러 독립적인 부품들의 조합
    즉, 객체들의 유기적인 협력과 결합으로 파악하고자 하는 컴퓨터 프로그래밍의 패러다임을 의미합니다.
  • 마치 자동차를 만든다고 했을 때, 수 많은 부품들의 결합과 연결로 하나의 완전한 자동차가 만들어지는 것과 같다고 할 수 있습니다. 객체 지향적으로 소프트웨어를 설계한다는 말의 의미는 어떤 프로그램의 일부분에 해당하는 작은 부품
    즉, 객체를 먼저 만들고 이렇게 만들어진 여러 객체들을 조립해서 하나의 완성된 프로그램을 만드는 프로그래밍 방법론을 뜻합니다.

2) 객체 지향 프로그래밍의 장점

  1. 객체 지향적 설계를 통해 프로그램을 보다 유연하고 변경이 용이하게 만들 수 있다는 점입니다.
    마치 컴퓨터 부품을 갈아 끼울 때, 해당하는 부품만 쉽게 교체하고 나머지 부품들을 건드리지 않아도 되는 것처럼 소프트웨어를 설계할 때 객체 지향적 원리를 잘 적용해 둔 프로그램은 각각의 부품들이 각자의 독립적인 역할을 가지기 때문에 코드의 변경을 최소화 하고 유지보수를 하는데 유리합니다.
  2. 코드의 재사용을 통해 반복적인 코드를 최소화 하고, 코드를 최대한 간결하게 표현할 수 있습니다.
    또한 객체 지향 프로그래밍은 실제 우리가 보고 경험하는 세계를 최대한 프로그램 설계에 반영하기 위한 지속적인 노력을 통해 발전해왔기 때문에, 보다 인간친화적이고 직관적인 코드를 작성하기에 용이합니다.

3) 객체란

  • 객체는 객체지향 프로그래밍의 가장 기본적인 단위이자 시작점이라 할 수 있습니다. 객체지향 개념의 가장 기본적인 전제는 실제 세계는 객체들로 구성되어 있으며, 보여지는 모든 현상과 발생하는 모든 사건은 이러한 객체들 간의 상호작용을 통해 발생한다는 것에서 출발합니다.
  • 즉, 객체란 책상, 의자, 시계, 전등, 책 등 우리가 주변에서 흔히 볼 수 있는 모든 실재하는 대상을 객체지향 프로그래밍 언어에서 객체라고 부릅니다.
    • 더보기
      public class ObjectOrientedProgrammingExample {
      
        public static void main(String[] args) {
          Car car = new Car();
          car.startEngine();
          car.moveForward();
          car.moveBackward();
        }
      }
      
      class Car {
        // 속성 정의
        String company;
        String model;
        String color;
        int wheel;
        boolean isConvertible;
      
        // 기능 정의
        void startEngine() {
          System.out.println("시동을 겁니다.");
        }
      
        void moveForward() {
          System.out.println("자동차가 앞으로 갑니다.");
        }
      
        void moveBackward() {
          System.out.println("자동차가 뒤로 갑니다.");
        }
      }
      
  • 객체지향 프로그래밍에서는 이와 같은 각각의 객체를 추상화시켜 속성(state)과 기능(behavior)으로 분류한 후 이것을 다시 각각 변수(variable)와 함수(function)로 정의하고 있습니다.

4) 객체 지향 프로그래밍의 4가지 특징 - 추상화 (Abstraction)

  • 추상화의 핵심 개념은 공통성과 본질을 모아 추출한다는 것입니다.
  • 객체지향 프로그래밍에서 추상화는 객체의 공통적인 속성과 기능을 추출하여 정의하는 것을 의미합니다.
  • 위 예시를 보면, 자동차와 오토바이는 모두 이동 수단이며 모든 이동 수단은 전진과 후진을 할 수 있다는 공통점을 가집니다. 이것을 자바 문법 요소를 사용하여 표현하면, 자동차와 오토바이라는 하위 클래스들의 공통적인 기능을 추출하여 이동수단이라는 상위 클래스에 정의했습니다.
    • 더보기
      public class ObjectOrientedProgrammingExample {
      
        public static void main(String[] args) {
      	Car1 car1 = new Car1();
          car1.start();
          car1.moveForward();
          car1.moveBackward();
        }
      }
      
      interface Vehicle {
        public abstract void start();
      
        // public abstract는 생략해도 됩니다.
        void moveForward();
        void moveBackward();
      }
      
      class Car1 implements Vehicle {
        @Override
        public void start() {
          System.out.println("자동차 시동을 겁니다.");
        }
      
        @Override
        public void moveForward() {
          System.out.println("자동차가 앞으로 갑니다.");
        }
        @Override
        public void moveBackward() {
          System.out.println("자동차가 뒤로 갑니다.");
        }
      }
      
      class MotorBike implements Vehicle {
        @Override
        public void start() {
          System.out.println("오토바이 시동을 겁니다.");
        }
      
        @Override
        public void moveForward() {
          System.out.println("오토바이가 앞으로 갑니다.");
        }
      
        @Override
        public void moveBackward() {
          System.out.println("오토바이가 뒤로 갑니다.");
        }
      }

       

  • 가장 먼저 자동차와 오토바이 공통적인 기능을 추출하여 이동 수단 인터페이스에 정의합니다.
  • 객체지향적 설계에 있어서 인터페이스는 어떤 객체의 역할만을 정의하여 객체들 간의 관계를 보다 유연하게 연결하는 역할을 담당합니다.
  • 위 예시처럼 Vehicle 인터페이스를 구현한 구현체 Car, MotorBike 클래스에서 인터페이스에 정의한 역할을 각각의 클래스의 맥락에 맞게 구현하고 있습니다.
  • 이것을 객체지향 프로그래밍에서 역할과 구현의 분리라고 하며, 이 부분이 다형성과 함께 유연하고 변경이 용이한 프로그램을 설계하는 데 가장 핵심적인 부분이라 할 수 있습니다.
  • 정리하면, 객체지향 프로그래밍에서는 보다 유연하고 변경에 열려있는 프로그램을 설계하기 위해 역할과 구현을 분리하는데, 여기서 역할에 해당하는 부분이 인터페이스를 통해 추상화 될 수 있습니다.

5) 객체 지향 프로그래밍의 4가지 특징 - 상속 (Inheritance)

  • 상속이란 기존 클래스를 재활용하여 새로운 클래스를 작성하는 자바의 문법 요소를 의미합니다.
  • 앞에서 다뤘던 추상화의 연속이라고 볼 수 있는데, 상속은 클래스 간 공유될 수 있는 속성과 기능들을 상위 클래스로 추상화 시켜 해당 상위 클래스로부터 확장된 여러 개의 하위 클래스들이 모두 상위 클래스의 속성과 기능들을 간편하게 사용할 수 있도록 합니다.
  • 즉, 클래스들 간 공유하는 속성과 기능들을 반복적으로 정의할 필요 없이 딱 한 번만 정의해두고 간편하게 재사용할 수 있어 반복적인 코드를 최소화하고 공유하는 속성과 기능에 간편하게 접근하여 사용할 수 있도록 합니다.
  • 위 그림을 보면 자동차와 오토바이가 있고, 각각의 기능과 속성들이 명시되어 있습니다. 이 중에서 빨간색으로 표시된 속성과 기능들은 자동차와 오토바이의 공통적인 부분들이고, 푸른색으로 표시된 부분들은 그렇지 않은 부분들입니다.
    • 더보기
      Vehicle 클래스
      package OOPTEST;
      
      public class Vehicle1 {
        String model;
        String color;
        int wheel;
      
        void moveForward() {
          System.out.println("전진합니다.");
        }
        void moveBackward() {
          System.out.println("후진합니다.");
        }
      }
      ​

       

      Car 클래스
      package OOPTEST;
      
      public class Car2 extends Vehicle1 {
        boolean isConvertible;
      
        void oepnWindow() {
          System.out.println("창문을 엽니다.");
        }
      }
      ​

      MotorBike 클래스

      package OOPTEST;
      
      public class MotorBike1 extends Vehicle1{
        boolean isRaceable;
      
        @Override
        void moveForward() {
          System.out.println("오토바이가 앞으로 갑니다.");
        }
      
        public void stunt() {
          System.out.println("오토바이로 묘기를 부립니다.");
        }
      }
      ​

      Main 클래스

      package OOPTEST;
      
      public class Main {
      
        public static void main(String[] args) {
          // 객체 생성
          Car2 car2 = new Car2();
          MotorBike1 motorBike1 = new MotorBike1();
      
          // car2 객체의 속성 정의
          car2.model = "자동차";
          car2.color = "색상";
      
          System.out.println("나의 " + car2.color + " " + car2.model + "입니다.");
      
          // 객체들의 기능 실행
          car2.moveForward();
          motorBike1.moveForward();
          motorBike1.moveBackward();
        }
      }
      
  • 위 예제를 보면, Car와 MotorBike 클래스의 공통적인 속성과 기능들을 추출(추상화) 하여 Vehicle 클래스(상위 클래스)에 정의하였고, extends 키워드를 통해 각각의 하위 클래스로 확장하여 해당 기능과 속성들을 매번 반복적으로 정의해야 하는 번거로움을 제거했습니다. 또한, 공통적인 코드의 변경이 있는 경우 상위 클래스에서 한 번의 수정으로 모든 클래스에 변경 사항이 반영될 수 있도록 만들었습니다.
  • 결론적으로 상속 관계의 경우 인터페이스를 사용하는 구현에 비해 추상화의 정도가 낮다고 할 수 있습니다.
    인터페이스가 역할에 해당하는 껍데기만 정의해두고, 하위 클래스에서 구체적인 구현을 하도록 강제하는 것에 비해, 상속 관계의 경우 상황에 따라 모든 구체적인 내용들을 정의해두고 하위 클래스에서는 그것을 단순히 가져다 재사용할 수 있습니다.

6) 객체 지향 프로그래밍의 4가지 특징 - 다형성 (Polymorphism)

  • 다형성이란 어떤 객체의 속성이나 기능이 상황에 따라 여러가지 형태를 가질 수 있는 성질을 의미합니다.
  • 비유적으로 어떤 중년의 남성이 있다고 했을 때 그 남자의 역할이 아내에게는 남편, 자식에게는 아버지, 부모님에게는 자식, 회사에서는 회사원 등 상황에 따라 달라지는 것과 비슷하게 볼 수 있습니다.
  • 객체지향의 다형성도 이와 비슷합니다.
    즉, 어떤 객체의 속성이나 기능이 그 맥락에 따라 다른 역할을 수행할 수 있는객체지향의 특성을 의미합니다.
    대표적인 예로 오버라이딩과 오버로딩이 있습니다.
  • 객체지향 프로그래밍에서 다형성이란 한 타입의 참조변수를 통해 여러 타입의 객체를 참조할 수 있도록 만든 것을 의미합니다. 좀 더 구체적으로, 상위 클래스 타입의 참조변수로 하의 클래스의 객체를 참조할 수 있도록 하는 것입니다.
    • 상속에서 사용했던 부분에 코드를 추가해 보겠습니다.
    • 더보기
      package OOPTEST;
      
      public class Main {
      
        public static void main(String[] args) {
          // 객체 생성
          Car2 car2 = new Car2();
          MotorBike1 motorBike1 = new MotorBike1();
          
          // car2 객체의 속성 정의
          car2.model = "자동차";
          car2.color = "색상";
      
          System.out.println("나의 " + car2.color + " " + car2.model + "입니다.");
          
          // 객체들의 기능 실행
          car2.moveForward();
          motorBike1.moveForward();
          motorBike1.moveBackward();
          
          Vehicle1 car3 = new Car2();
        }
      }
      
  • 위 코드에서 보면, 상위 클래스 타입의 참조변수로 하위클래스 객체를 참조하는 것의 의미를 조금 더 구체적으로 이해할 수 있습니다. 원래 사용했던 방식은 하위 클래스의 객체를 생성학여 하위 클래스 타입의 참조 변수에 할당해주었지만, 다형성을 활용한 객체 생성 방식에서는 하위 클래스의 객체를 생성하여 상위 클래스 타입의 참조변수 car3에 할당해주고  있습니다.
  • 다형성을 활용한 방법이 유용한 이유를 알아보겠습니다.
  • 다형성을 활용하면 여러 종류의 객체를 배열로 다루는 일이 가능해집니다. 코드로 확인해 보겠습니다.
    • 더보기
      Main1
      package OOPTEST;
      
      public class Main1 {
      
        public static void main(String[] args) {
          Vehicle1[] vehicles1 = new Vehicle1[2];
          vehicles1[0] = new Car2();
          vehicles1[1] = new MotorBike1();
      
          for (Vehicle1 vehicle1 : vehicles1) {
            System.out.println(vehicle1.getClass()); // 각각의 클래스를 호출해주는 메서드
          }
        }
      /*
      출력
      class OOPTEST.Car2
      class OOPTEST.MotorBike1
      */
      }
      Vehicle1
      package OOPTEST;
      
      public class Vehicle1 {
        String model;
        String color;
        int wheel;
      
        void moveForward() {
          System.out.println("전진합니다.");
        }
        void moveBackward() {
          System.out.println("후진합니다.");
        }
      }
      ​

      Car2

      package OOPTEST;
      
      public class Car2 extends Vehicle1 {
        boolean isConvertible;
      
        void oepnWindow() {
          System.out.println("창문을 엽니다.");
        }
      }
      

      MotorBike1

      package OOPTEST;
      
      public class MotorBike1 extends Vehicle1{
        boolean isRaceable;
      
        @Override
        void moveForward() {
          System.out.println("오토바이가 앞으로 갑니다.");
        }
      
        public void stunt() {
          System.out.println("오토바이로 묘기를 부립니다.");
        }
      }
      
  • 상위 클래스 Vehicle 타입의 객체 배열을 생성해주면, 이제 해당 타입의 참조 변수는 Vehicle 클래스와 상속 관계에 있는 모든 하위 클래스들을 그 안에 담아줄 수 있습니다. 원래 자바에서 배열의 개념이 하나의 같은 타입으로 이뤄져 있는 자료구조라는 사실을 기억할 때, 이렇게 다형성을 활용하면 하나의 타입만으로 여러가지 타입의 객체를 참조할 수 있어 보다 간편하고 유연하게 코드를 작성하는 것이 가능해집니다.
  • 또 다른 예제를 보여들겠습니다.
    • 더보기
      Driver
      package OOPTEST;
      
      public class Driver {
      
        void drive (Car2 car2) {
          car2.moveForward();
          car2.moveBackward();
          car2.openWindow();
      
        }
      
        void drive (MotorBike1 motorBike1) {
          motorBike1.moveForward();
          motorBike1.moveBackward();
          motorBike1.stunt();
        }
      
      }
      ​
      Main
      package OOPTEST;
      
      public class Main2 {
      
        public static void main(String[] args) {
          Car2 car2 = new Car2();
          MotorBike1 motorBike1 = new MotorBike1();
          Driver driver = new Driver();
      
      
          driver.drive(car2);
          driver.drive(motorBike1);
        }
      }
      /*
      출력
      전진합니다.
      후진합니다.
      창문을 엽니다.
      오토바이가 앞으로 갑니다.
      후진합니다.
      오토바이로 묘기를 부립니다.
      */
      Car
      package OOPTEST;
      
      public class Car2 extends Vehicle1 {
        boolean isConvertible;
      
        void openWindow() {
          System.out.println("창문을 엽니다.");
        }
      }
      ​

      MotorBike

      package OOPTEST;
      
      public class MotorBike1 extends Vehicle1{
        boolean isRaceable;
      
        @Override
        void moveForward() {
          System.out.println("오토바이가 앞으로 갑니다.");
        }
      
        public void stunt() {
          System.out.println("오토바이로 묘기를 부립니다.");
        }
      }
      

      Vehicle

      package OOPTEST;
      
      public class Vehicle1 {
        String model;
        String color;
        int wheel;
      
        void moveForward() {
          System.out.println("전진합니다.");
        }
        void moveBackward() {
          System.out.println("후진합니다.");
        }
      }
      ​
  • Driver 클래스는 매개변수로 자동차나 오토바이 객체를 전달받아 운전하는 것입니다.
    이렇게 하나의 객체가 다른 객체의 속성과 기능에 접근하여 어떤 기능을 사용할 때, A클래스는 B클래스에 의존한다라고 표현합니다.
  • 위 예제는 Driver 클래스가 Car 클래스와 MotorBike 클래스에 의존하고 있다라고 설명할 수 있습니다.
    즉, Driver 클래스와 두 개의 클래스가 서로 직접적인 관계를 가지고 있는데, 이러한 상황을 객체들 간의 결합도가 높다라고 표현할 수 있습니다.
  • 하지만 이렇게 결합도가 높은 상태는 객체지향적인 설계를 하는데 매우 불리합니다.
  • 만약 이동 수단이 자동차와 오토바이만이 아닌 수십만개라면 그만큼 작성해야 하기 때문입니다.
    • 더보기
      public class Driver {
      
      	void drive (Car car) {
          	car.moveForward();
              car.moveBackward();
          }
          
          void drive (MotorBike motorBike) {
          	motorBike.moveForward();
              motorBike.moveBackward();
          }
          
          void drive (Bus bus) {
          	bus.moveForward();
              bus.moveBackward();
          }
          
          .
          .
          .
          
      }
  • 또 다른 예시로 MotorBike가 MotorCycle로 변경해야 하는 경우 때문입니다.
  • 그럼 Driver 안에 매개변수로 전달되는 참조변수의 타입과 참조변수를 수정할 수 밖에 없는 상황이 발생합니다. 그리고 코드가 많아질수록 이 작업은 힘든 작업이 될 수 밖에 없습니다.
  • 이런 맥락에서, 객체지향 프로그래밍은 지금까지 학습한 추상화, 상속, 다형성의 특성을 활용하여 프로그래밍을 설계할 때 역할과 구현을 구분하여 객체들 간의 직접적인 결합을 피하고, 느슨한 관계 설정을 통해 보다 유연하고 변경이 용이한 프로그램 설계를 가능하게 만들었습니다.
  • 예시 코드르 보여드리겠습니다.
    • 더보기
      vehicle
      package OOPTEST.OOPExample;
      
      public interface Vehicle {
        void moveForward();
        void moveBackward();
      
      }
      ​
      Car
      package OOPTEST.OOPExample;
      
      public class Car implements Vehicle{
      
        @Override
        public void moveForward() {
          System.out.println("자동차가 앞으로 갑니다.");
        }
      
        @Override
        public void moveBackward() {
          System.out.println("자동차가 뒤로 갑니다.");
        }
      
      }
      ​
      MotorBike
      package OOPTEST.OOPExample;
      
      public class MotorBike implements Vehicle{
      
        @Override
        public void moveForward() {
          System.out.println("오토바이가 앞으로 갑니다.");
        }
      
        @Override
        public void moveBackward() {
          System.out.println("오토바이가 뒤로 갑니다.");
        }
      }
      ​
      Driver
      package OOPTEST.OOPExample;
      
      import java.lang.invoke.VarHandle;
      
      public class Driver {
      
        void drive (Vehicle vehicle) {
          vehicle.moveForward();
          vehicle.moveBackward();
        }
      }
      ​

      Main

      package OOPTEST.OOPExample;
      
      public class Main {
        public static void main(String[] args) {
          Car car = new Car();
          MotorBike motorBike = new MotorBike();
          Driver driver = new Driver();
      
          driver.drive(car);
          driver.drive(motorBike);
        }
      }
      

       

       

  • Vehicle 인터페이스를 통해 이동 수단의 역할을 추상화하고, 각 Car 클래스와 MotorBike 클래스에서 기능들을 구현하고 있습니다.
  • 예시를 이해하기 쉽게 그림으로 표현하면
  • 이와 같인 Driver 클래스가 Vehicle을 인터페이스로 만들면서 Car와 MotorBike 클래스와의 결합도가 낮아졌습니다.
  • 따라서 Driver 클래스는 각각의 클래스 내부의 변경이나 다른 객체가 새롭게 교체되는 것을 신경 쓰지 않아도 인터페이스에만 의존하여 수정이 있을 때마다 코드 변경을 하지 않아도 됩니다.

7) 객체 지향 프로그래밍의 4가지 특징 - 캡슐화 (Encapsulation)

  • 캡슐화란 클래스 안에 서로 연관있는 속성과 기능들을 하나의 캡슐로 만들어 데이터를 외부로부터 보호하는 것을 말합니다.
    • 데이터보호 (Data Protection) : 외부로부터 클래스에 정의된 속성과 기능들을 보호합니다.
    • 데이터 은닉 (Data Hiding) : 내부의 동작을 감추고 외부에는 필요한 부분만 노출시킵니다.
  • 캡슐화를 구현하기 위한 방법은 몇가지 있습니다. 그 중 하나는 접근제어자를 이용하는 방법입니다.
    • 접근제어자 (Access Modifiers) : 클래스나 멤버들을 외부에서 접근하지 못하도록 접근을 제한하는 역할을 합니다.
    • 접근제어자 클래스 내 패키지 내 다른 패키지 하위 클래스 패키지 외 설명
      private O X X X 동일 클래스 내에서만 접근 가능
      default O O X X 동일 패키지 내에서만 접근 가능
      protected O O O X 동일 패키지 + 다른 패키지의 하위클래스에서 접근 가능
      public O O O O 접근 제한 없음
  • 위 표에서 확인할 수 있는 것처럼, 접근 제어자의 접근 범위가 각각 클래스 내, 패키지 내, 다른 패키지의 하위 클래스, 그리고 패키지 외까지 각각 다른 것을 확인할 수 있습니다.
  • 이 내용을 코드로 확인해 보겠습니다.
    • 더보기
      SuperClass
      package OOPTEST.Encapsulation; // 패키지명
      
      class Test {
        // Test 클래스의 접근 제어자는 default 입니다.
        public static void main(String[] args) {
          SuperClass superClass = new SuperClass();
      
      //    System.out.println(superClass.a); // 동일 클래스가 아니기 때문에 에러발생합니다.
          System.out.println(superClass.b);
          System.out.println(superClass.c);
          System.out.println(superClass.d);
        }
      }
      
      public class SuperClass {
        // SuperClass 클래스의 접근제어자는 public 입니다.
        private int a = 1;
        int b = 2;
        protected int c = 3;
        public int d = 4;
        // a, b, c, d에 각각 다른 접근 제어자 정의
      
        public void printEach() {
          // 동일 클래스이기 때문에 에러 발생하지 않습니다.
          System.out.println(a);
          System.out.println(b);
          System.out.println(c);
          System.out.println(d);
        }
      }
      
      /*
      출력
      2
      3
      4
       */
      ​

      Test2 클래스
      package OOPTEST.Encapsulation2;
      
      import OOPTEST.Encapsulation.SuperClass;
      
      class SubClass extends SuperClass {
        // package OOPTEST.Encapsulation2;
        public void printEach() {
      //    System.out.println(a);
      //    System.out.println(b);
          System.out.println(c);
          System.out.println(d);
        }
      }
      
      public class Test2 {
      
        public static void main(String[] args) {
          SuperClass parent = new SuperClass();
      
      //    System.out.println(parent.a);
      //    System.out.println(parent.b);
      //    System.out.println(parent.c);
          // public을 제외한 모든 호출 에러 발생합니다.
          System.out.println(parent.d);
        }
      }
      
      /*
      출력
      4
       */
      ​
  • 위 예시 코드는 접근제어자를 알아보기 위한 코드입니다.
  • 캡슐화를 구현하기 위한 두가지 방법은 getter / setter 메서드가 있습니다.
    • 더보기
      package OOPTEST.Encapsulation3;
      
      import java.awt.event.MouseWheelListener;
      
      public class Car {
        private String model;
        private String color;
        private int wheel;
      
        public String getModel () {
          return model;
        }
      
        public void setModel(String model) {
          this.model = model;
        }
      
        public String getColor () {
          return color;
        }
      
        public void setColor (String color) {
          this.color = color;
        }
      
        public int getWheel () {
          return wheel;
        }
      
        public void setWheel (int wheel) {
          this.wheel = wheel;
        }
      
      }
      
  • 위 예제를 보면 모든 속성 값들이 private 접근제어자로 선언되어 있고, getter / setter 메서드의 접근제어자만이 public으로 열려 있습니다. 따라서 선택적으로 외부에 접근을 허용할 속성과 그렇지 않을 속성을 getter / setter 메서드를 통해 설정해 줄 수 있습니다.
  • 구체적인 캡슐화를 알 수 있는 코드를 작성해 보겠습니다.
    • 더보기
      Car
      package OOPTEST.Encapsulation4;
      
      public class Car {
      
        private String model;
        private String color;
      
        public Car (String model, String color) {
          this.model = model;
          this.color = color;
        }
      
        public void startEngine () {
          System.out.println("시동을 겁니다.");
        }
      
        public void moveForward () {
          System.out.println(color + " " + model + "가 앞으로 갑니다.");
        }
      
        public void openWindow () {
          System.out.println("창문을 엽니다.");
        }
      
      }
      ​

      Driver

      package OOPTEST.Encapsulation4;
      
      public class Driver {
      
        private String name;
        private Car car;
      
        public Driver (String name, Car car) {
          this.name = name;
          this.car = car;
        }
      
        public void drive () {
          car.startEngine();
          System.out.println(name + "의 ");
          car.moveForward();
          car.openWindow();
        }
      
      }
      

      Main

      package OOPTEST.Encapsulation4;
      
      public class Main {
      
        public static void main(String[] args) {
          Car car = new Car("자동차", "Black");
          Driver driver = new Driver("남지", car);
      
          driver.drive();
        }
      }
      

객체 지향 설계의 5원칙 S.O.L.I.D

  • SOLID 원칙이란 객체지향 설계에서 지켜줘야 할 5개의 소프트웨어 개발 원칙 (SRP, OCP, LSP, ISP, DIP)을 말합니다.
    • SRP (Single Responsibility Principle) : 단일 책임 원칙
    • OCP (Open - Closed Principle) : 개방 폐쇄 원칙
    • LSP (Liskov Substitution Principle) : 리스코프 치환 원칙
    • ISP (Interface Segregation Principle) : 인터페이스 분리 원칙
    • DIP (Dependency Inversion Principle) : 의존 역전 원칙
  • SOLID 설계 원칙은 OOP의 4가지 특징 (추상화, 상속, 다형성, 캡슐화)와 더불어, 객체 지향 프로그래밍의 단골 면접 질문 중 하나입니다. 또한 앞으로 배우게 될 여러 디자인 패턴 (Design Pattern)들이 SOLID 설계 원칙에 입각해서 만들어진 것이기 때문에, 표준화 작업에서부터 아키텍처 설계에 이르기까지 다양하게 적용되는 이의 근간이 되는 SOLID 원칙에 대해 알아보겠습니다.
  • 좋은 설계란 시스템에 새로운 요구사항이나 변경사항이 있을 때, 영향을 받는범위가 적은 구조를 말합니다. 그래서 시스템에 예상하지 못한 변경사항이 발생하더라도, 유연하게 대처하고 이후 확장성이 있는 시스템 구조를 만들 수 있습니다.
  • 즉, SOLID 객체 지향 원칙을 적용하면 코드를 확장하고 유지 보수 관리가 더 쉬워지며, 불필요한 복잡성을 제거해 리팩토링에 소요되는 시간을 줄임으로써 프로젝트 개발의 생산성을 높일 수 있습니다.

1) 단일 책임의 원칙 : SRP (Single Responsibility Principle)

  • 단일 책임 원칙은 클래스(객체)는 단 하나의 책임만 가져야 한다는 원칙입니다.
  • 여기서 책임의 의미는 하나의 기능을 담당한다고 보면 됩니다.
  • 즉, 하나의 클래스는 하나의 기능을 담당하며 하나의 책임을 수행하는데 집중되도록 클래스를 따로따로 여러 개 설계하는 원칙입니다.
  • 최종적으로 단일 책임 원칙의 목적은 프로그램의 유지보수의 편리성을 높이기 위한 설계 기법입니다.
  • 이때, 책임의 범위는 딱 정해져 있는 것이 아니고, 어떤 프로그램을 개발하느냐에 따라 개발자마다 생각 기준이 달라질 수 있습니다. 따라서 단일 책임 원칙에 100% 정답은 없습니다.

2) 개방 폐쇄 원칙 : OCP (Open Closed Principle)

  • 개방 폐쇄 원칙은 클래스는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다는 의미를 갖고 있습니다.
  • 기능 추가 요청이 오면 클래스 확장을 통해 손쉽게 구현하면서, 확장에 따른 클래스 수정은 최소화 하도록 프로그램을 작성해야 하는 설계 기법입니다.
    • 확장에는 열려있다 - 새로운 변경 사항이 발생했을 때 유연하게 코드를 추가함으로써 큰 힘을 들이지 않고 애플리케이션의 기능을 확장할 수 있어야 합니다.
    • 변경에는 닫혀있다 - 새로운 변경 사항이 발생했을 때 객체는 직접적으로 변경을 제한합니다.
  • 개방 폐쇄 원칙은 추사화 사용을 통해 관계 구축을 권장한다는 의미입니다.
  • 즉, 다형성과 확장을 가능케 하는 객체지향의 장점을 극대화하는 기본적인 설계 원칙입니다.

3) 리스코프 치환 원칙 : LSP (Liskov Substitution Principle)

  • 리스코프 치환 원칙은 서브 타입은언제나 부모 타입으로 교체할 수 있어야 한다는 원칙입니다.
  • 쉽게 말해 리스코프 치환 원칙은 다형성 원리를 이용하기 위한 원칙 개념으로 생각하면 됩니다.
  • 간단히 말하면 리스코프 치환 원칙이란, 다형성의 특징을 이용하기 위해 상위 클래스 타입으로 객체를 선언하여 하위 클래스의 인스턴스를 받으면, 업캐스팅된 상태에서 부모의 메서드를 사용해도 동작이 의도대로 흘러가야 하는 것을 의미합니다.
  • 따라서 기본적으로 리스코프 치환 원칙은 부모 메서드의 오버라이딩을 조심스럽게 따져가며 해야 한다는 의미입니다.
    이유는 부모 클래스와 동일한 수준의 선행 조건을 기대하고 사용하는 프로그램 코드에서 예상치 못한 문제를 일으킬 수 있기 때문입니다.

4) 인터페이스 분리 원칙 : ISP (Interface Segregation Principle)

  • 인터페이스 분리 원칙은 인터페이스를 각각 사용에 맞게 잘게 분리해야 한다는 설계 원칙입니다.
  • 단일 책임원칙이 클래스의 단일 책임을 강조한다면, 인터페이스 분리 원칙은 인터페이스의 단일 책임을 강조하는 것이라 생각하면 됩니다.
  • 즉, 단일 책임 원칙의 목표는 클래스 분리를 통하여 이루어진다면, 인터페이스 분리 원칙은 인터페이스 분리를 통해 설계하는 원칙입니다.
  • 인터페이스 분리 원칙은 인터페이스를 사용하는 클라이언트를 기준으로 분리함으로써, 클라이언트의 목적과 용도에 적합한 인터페이스만을 제공하는 것이 목표입니다.
  • 다만 인터페이스 분리 원칙의 주의해야 할 점은 한번 인터페이스를 분리하여 구성해두고 나중에 무언가 수정 사항이 생겨서 또 인터페이스들을 분리하는 행위를 가하지 말아야 합니다.

5) 의존 역전 원칙 : DIP (Dependency Inversion Principle)

  • 의존 역전 원칙은 어떤 클래스를 참조해서 사용해야 하는 상황이 생긴다면, 그 클래스를 직접 참조하는 것이 아니라 그 대상의 상위 요소로 참조하라는 원칙입니다.
  • 쉽게 이야기 해서 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 의미입니다.
  • 의존 관계를 맺을 때 변화하기 쉬운 것 또는 자주 변화하는 것보다, 변화하기 어려운 것 거의 변화가 없는 것에 의존하라는 의미입니다.