Home 프로그램 패러다이밍 - 객체 지향 프로그래밍 (OOP)
Post
Cancel

프로그램 패러다이밍 - 객체 지향 프로그래밍 (OOP)

이번 시간에는 쿠버네티스를 학습하기 이전에 프로그램의 패러다이밍 중 하나인 OOP에 대해서 설명하고 넘어가려 한다.

객체

객체의 사전적 정의는 실제로 존재하는 것이다.

하지만 전기 신호로 이루어진 컴퓨터의 세계에서는 실존하는 것이 없는데 이를 실존하는것 처럼 정의하는 것을 추상화라고 한다.

객체 지향적 프로그래밍 관점에서 추상화는 클래스를 정의하는 것이라 할 수 있다.

클래스와 인스턴스

흔한 예시로 우리는 붕어빵을 만든다고 가정하자.

이때 필요한 반죽과 앙금은 붕어빵에 필요할 속성이라 할 수 있고, 붕어빵을 만들기 위한 틀이 class가 되며 틀에서 나온 붕어빵을 인스턴스라 한다.

자바 코드와 함께 설명해 본다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class 붕어빵 {

        public String 반죽;

        public String 앙금;

        public 붕어빵(String 반죽, String 앙금) {
            this.반죽 = 반죽;
            this.앙금 = 앙금;
        }

        public void 가격() {
            System.out.println("붕어빵은 2개에 천원입니다.");
        }

        붕어빵 슈크림붕어빵 = new 붕어빵("밀반죽", "슈크림");

        붕어빵 흑미팥붕어빵 = new 붕어빵("흑미반죽", "팥");
    }

위의 코드를 보면 붕어빵이란 클래스에 반죽과 앙금이라는 두개의 속성이 있고 반죽과 앙금을 받아 객체를 생성하는 생성자가 있다.

new를 사용하여 밀반죽의 슈크림붕어빵와 흑미반죽의 흑미팥붕어빵을 만들었다.

이때 실제로 두개의 붕어빵은 메모리에 존재하게 되는데 이를 인스턴스라고 말하며 객체라고 한다.

여기서 슈크림붕어빵(객체)의 멤버는 밀반죽과, 슈크림이 되며 흑미팥붕어빵(객체)의 멤버는 흑미반죽과, 팥이다.

만약 같은 객체를 다시 만든다고 하면???

1
붕어빵 슈크림붕어빵2 = new 붕어빵("밀반죽", "슈크림");

같은 틀에서 나온 새로운 밀반죽의 슈크림붕어빵2가 나오더라도 이는 현실세계에서 붕어빵은 모두 다르 듯이 프로그래밍 세계에서도 다른 객체로 본다.

객체 지향 프로그래밍 : OOP( Object Oriented Programing )

객체 지향 프로그래밍의 방법론은 위의 예시처럼 만들어진 각각의 인스턴스(객체)들이 서로 유기적으로 상호작용하며 프로세스가 이루어 진다는 것을 말하는데 이로 인해 4가지의 특징을 가진다.

1. 추상화(Abstration)

  • 현실에 실제로 존재하는 것을 프로그래밍 상에 존재하느 것처럼 정의하는 것
  • 클래스를 정의하는 것과 같으며 불필요한 것은 섞이면 안됨

우리는 위에서 붕어빵이란 class를 정의하고 새로운 붕어빵이 필요하면 new 라는 생성자를 통해 붕어빵을 만들었다.

새로운 붕어빵이 필요할 때 마다 이미 정의된 class에 의하여 또 다시 코드를 작성할 필요가 없고 재사용이 가능하다는 장점이 있다.

2. 상속(Inheritance)

  • 추상화된 클래스를 활용하여 새로운 클래스를 정의하는 것
  • 부모(상위)클래스로 부터 속성과 기능을 사용가능
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class 계란빵 {

        public String 반죽;

        public String 앙금;

        public 국화빵(String 반죽, String 앙금) {
            this.반죽 = 반죽;
            this.앙금 = 앙금;
        }

        public void 가격() {
            System.out.println("국화빵은 3개에 천원입니다.");
        }
    }

public class 잉어빵 {

        public String 반죽;

        public String 앙금;

        public 잉어빵(String 반죽, String 앙금) {
            this.반죽 = 반죽;
            this.앙금 = 앙금;
        }

        public void 가격() {
            System.out.println("잉어빵은 1개에 천원입니다.");
        }
    }

위의 코드는 붕어빵이 아닌 잉어빵과 국화빵을 추상화한 클래스이다.

붕어빵과 비교해보면 가격을 나타내는 가격() 메서드를 제외한 부분이 코드가 중복되는 것을 알 수 있다.

그래서 앙금빵에 대한 부모 클래스를 추상화하고 이를 상속받은 붕어빵, 국화빵, 잉어빵을 추상화 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    public class 앙금빵 {

        public String 반죽;

        public String 앙금;

        public 앙금빵(String 반죽, String 앙금) {
            this.반죽 = 반죽;
            this.앙금 = 앙금;
        }

        public void 가격() {
            System.out.println("앙금빵의 가격은 빵의 종류에 따라 다릅니다.");
        }
    }
1
2
3
4
5
6
7
8
9
10
    public class 붕어빵 extends 앙금빵 {
        public 붕어빵(String 반죽, String 앙금) {
            super(반죽, 앙금);
        }

        @Override
        public void 가격() {
            System.out.println("붕어빵은 2개에 천원입니다.");
        }
    }
1
2
3
4
5
6
7
8
9
10
    public class 국화빵 extends 앙금빵 {
        public 국화빵(String 반죽, String 앙금) {
            super(반죽, 앙금);
        }

        @Override
        public void 가격() {
            System.out.println("국화빵은 3개에 천원입니다.");
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
    public class 잉어빵 extends 앙금빵 {
        public 잉어빵(String 반죽, String 앙금) {
            super(반죽, 앙금);
        }

        @Override
        public void 가격() {
            System.out.println("잉어빵은 1개에 천원입니다.");
        }

        public void 서비스() {
            System.out.println("잉어빵은 특별히 3개엔 2천원입니다.");
        }
    }

부모인 앙금빵을 상속 받아 반죽과, 앙금이라는 속성을 가진 다른 빵들이 생길 수 있고 잉어빵처럼 서비스()라는 새로운 기능을 정의하거나 기존의 가격()기능을 새롭게 변경하여 사용이 가능해진다.

3. 다형성(Polymorphism)

  • 프로그램의 요소가 다양한 자료형 속할 수 있는 성질
  • 대표적인 방법으로 오버로딩과 오버라이딩이 있음

오버로딩

  • 같은 이름의 기능을 매개변수 유형에 따라 다시 사용하는 기술
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  public void 계산() {
            System.out.println("계산을 진행합니다.");
            System.out.println("무료 상품으로 지불할 금액이 없습니다.");
        }

        public void 계산(String x) {
            System.out.println("계산을 진행합니다.");
            System.out.println(x + "수단으로 지불을 하였습니다.");
        }

        public void 계산(int x) {
            System.out.println("계산을 진행합니다.");
            System.out.println(x + "원을 지불하였습니다.");
        }

        public void 계산(boolean x) {
            if(x) {
                System.out.println("계산을 진행합니다.");
                System.out.println("선불 지불이 완료되었습니다.");
            }
        }

        public void 계산(String x, int y) {
              System.out.println("계산을 진행합니다.");
              System.out.println(x + "수단으로" + y "원을 지불하였습니다.");
        }

위의 코드는 앙금빵에서 계산이라는 메소드를 매개변수에 따라 오버로딩한 것이다.

처음부터 살펴보면 매개변수를 받지 않을시에는 무료인 상황으로 계산이 완료된다.

두번째 상황은 결제 수단을 선택하여 지불하는 방법으로 String 으로 결재 수단을 받아 계산이 완료된다.

세번째 상황은 금액을 통한 계산이다.

네번째 상황은 쿠폰이나 선불권에 의해 이미 지불한 사람의 상태에 따라 계산이 완료되는 방식이다.

다섯째 상황은 결제 수단 String 과 가격 int를 받아 계산을 진행하는 방식이다.

이렇게 하면 다양항 프로퍼티에 대해 메서드를 하나의 이름으로 동적으로 처리가 가능하다.

그러나 만약 계산에 대한 전체 프로세스가 수정된다면 모든 오버로딩된 코드를 수정해야 하기 때문에 사용하기에 주의가 필요하다.

오버라이딩

  • 상속받은 부모 클래스의 기능을 자식 클래스에서 재정의 하는 것
1
2
3
4
5
  // 붕어빵
  @Override
        public void 가격() {
            System.out.println("붕어빵은 2개에 천원입니다.");
        }
1
2
3
4
5
  // 국화빵
  @Override
        public void 가격() {
            System.out.println("국화빵은 3개에 천원입니다.");
        }
1
2
3
4
5
  // 잉어빵
  @Override
        public void 가격() {
            System.out.println("잉어빵은 1개에 천원입니다.");
        }

우리는 이전에 빵의 가격에 대한 정보를 리턴하는 가격() 메서드를 부모클래스인 앙금빵에 정의하여 자식 클래스에서 재정의 하였다.

이를 오버라이딩이라고 하는데 이렇게 하면 자식 클래스 마다 가격을 명시하는 새로운 메서드명을 만들 필요가 없고 코드 변경이 용이하다는 장점이 있다.

4. 캡슐화(Encapsulation)

  • 접근제어자와 getter/setter를 활용하여 외부로의 노출을 줄이고 결합도를 낮추는 것
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class 스토어 {
        public String 판매자;

        public String 판매물품;

        public int 가격;

        public void 판매자명() {
            System.out.println("판매자명 : " + 판매자);
        }

        public void 판매물품명() {
            System.out.println("판매물품명 : " 판매물품);
        }

        public void 금액() {
            System.out.println("금액 : " + 가격);
        }
    }

판매자의 이름과 판매물품 그리고 가격을 가지고있는 스토어가 있으며 각각 판매자명과 판매물품명 가격을 출력하는 코드가 있다.

이는 모두 public으로 외부에서 접근이 가능하다.

1
2
3
4
5
6
7
8
9
10
11
    public class 고객 {
        public String 구매자;

        public void 구매(스토어 x) {
            System.out.plintln(this.구매자 + " 의 구매 물품");
            x.판매자명();
            x.판매물품명();
            x.금액();
        }
    }

고객은 구매를 하기 위해 스토어를 매개변수로 지정하고 스토어에 정의된 함수들을 호출하여 각각 판매자명, 판매물품명, 가격을 출력한다.

하지만 이런 설계방식은 구매를 하기 위해 판매자의 판매 시스탬을 모두 알아야하며 또한 가격을 외부에서 조정할 수 있다는 치명적인 약점이 있다.

판매자의 변수들은 구매자로 부터 바뀌어선 안되어야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
    // 판매금액을 0원으로 수정 가능하며 스토어의 모든 메서드를 알지 못하는 경우 물품명이 누락됨
    public class 고객 {
        public String 구매자;

        public void 구매(스토어 x) {
            System.out.plintln(this.구매자 + " 의 구매 물품");
            x.판매금액 = 0;
            x.판매자명();
            // x.판매물품명();
            x.금액();
        }
    }

그렇기 때문에 우리는 접근제어자를 활용하여 데이터를 보호하고 판매 프로세스에 대한 동작 노출을 줄여야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
  public class 스토어 {
      private String 판매자;

      private String 판매물품;

      private int 가격;

      public String get판매자() {
          return this.판매자;
      }

      public String get판매물품() {
          return this.판매물품;
      }

      public int get가격() {
          return this.가격;
      }

      private void 판매자명() {
          System.out.println("판매자명 : " + get판매자());
      }

      private void 판매물품명() {
          System.out.println("판매물품명 : " + get판매물품());
      }

      private void 금액() {
          System.out.println("금액 : " + get가격());
      }

      public void 판매() {
          판매자명();
          판매물품명();
          금액();
      }
  }

모든 변수를 private로 지정하여 직접적인 호출과 수정을 못하게 막고 getter 메서드를 활용하여 값을 표출한다.

판매() 라는 public의 메서드에 기존 판매 프로세스들을 몰아 넣고 기존 프로세스들을 privte로 하여 정의한다.

1
2
3
4
5
6
7
8
    public class 고객 {
        public String 구매자;

        public void 구매(스토어 x) {
            // x.가격 = 0;
            x.판매();
        }
    }

이제 고객은 가격을 수정하여 구매를 진행할 수 없으며 판매자의 판매 프로세스를 알 필요도 없어진다.

객체 지향 언어를 사용하면 객체 지향 프로그래밍일까??

업무를 하면서 제기된 내용이다.

흔히 JAVA를 객체 지향 언어라 하여 JAVA를 다룬다고 하면 객체 지향 프로그래밍을 하고있다 라고 생각 할 수 있는데 이는 일반화에 오류가 있다.

객체 지향의 특성을 파악하고 설계해야 객체 지향 프로그래밍을 하고있다고 말 할 수 있다. 결국 소스코드는 컴퓨터는 개발자의 의지대로 순차적으로 읽힌다.

만약 이 특성을 잘 이해하지 못한다면 객체 지향 프로그래밍 언어를 사용하였다 하더라도 객체 지향 프로그래밍을 하고 있다 말하기 어렵다.

마무리

객체 지향 프로그래밍은 잘 사용하면 객체를 사용하여 코드의 재사용율을 높이고 반복된 코드를 줄여 개발자의 생산성을 향상시킬 수 있다.

또한 추상화를 통해 실제 존재하는 것처럼 프로그래밍을 할 수 있기 때문에 자연적인 모델링이 가능하며 사람이 이해하기 쉬어 유지보수성을 높일 수 있다.

하지만 객체 지향 프로그래밍에도 단점은 존재한다.

설계 단계에 시간이 많은 시간이 소모되고 잘 구현된 객체가 아니라면 오히려 재사용성과 유지보수성이 낮아질 수 있다.

개발자의 코딩 난이도가 비교적 높고 실행속도가 느린 면이 있다.

따라서 프로그래밍을 하기 전에 정확한 이해와 분석이 필요하고 높은 수준의 설계가 중요하다.

개인적인 이해를 바탕으로 작성한 글 입니다. 피드백 언제든 환영합니다!

배포와 리눅스 컨테이너, 도커, 쿠버네티스에 대해서 알아보자-(01)

Multipart를 활용한 파일 업로드에 대해서