객체 지향 프로그래밍(OOP) 을 하는 이유는 뭘까?

프로그램을 유연하고 변경이 용이하게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용됩니다.

 

 


객체 지향 프로그램을 설계하는 핵심적인 4가지 [ 다형성 ] [ 캡슐화 ] [ 상속 ] [ 추상화 ]

 

다형성

ㅇ 운전자와 자동차 예시

 

 운전자는 BMW를 타다가 아우디로 차를 바꾼다.

 차를 바꾼다고 해서 운전자가 운전을 못하게 되는 것은 아니다.

 여기서 요점은 자동차가 바뀌어도 운전자에게 영향을 안준다는 것이다. 

 이러한 것을 보고 "유연하고 변경이 용이하다" 라고 한다.

 

 이러한 다형성은 "클라이언트를 위해" 이뤄진다는 것이다.

 프로그램을 확장시키고, 새로운 기능을 추가를 해도 클라이언트는 무언가를 변경할 필요가 전혀 없다는 것입니다.

 이런게 가능 한 이유는 역할과 구현으로 세상을 구분했기 때문에 가능한 것입니다.

 여기서 중요한 요점은 새로운 기능을 추가한다는게 아니라 [ 새로운 기능이 추가되어도 클라이언트에게 전혀 영향을 미치지 않는다 ]

 라는 것입니다.

 

 

ㅇ 역할과 구현을 분리

  • 장점
  1. 클라이언트는 대상의 역할(인터페이스)만 알면 된다.
  2. 클라이언트는 내부 구조를 몰라도 된다.
  3. 클라이언트는 내부 구조가 변경되어도 전혀 영향을 받지 않는다.
  4. 클라이언트는 구현 대상 자체를 변경해도 영향을 받지 않는다. 
  • 자바언어 활용
  1. 자바 언어의 다형성을 활용 [ 역할=인터페이스 | 구현=인터페이스를 구현한 클래스, 구현 객체 ]
  2. 객체를 설계할 때 역할과 구현을 명확히 분리
  3. 객체 설계시 역할을 먼저 부여하고, 그 역할을 수행하는 구현 객체 만들기
  • 자바 언어의 다형성
  1. 오버라이딩 된 메서드 실행
  2. 다형성으로 인터페이스를 구현한 객체를 실행 시점에 유연하게 변경할 수 있다.
  3. 클래스 상속 관계도 다형성이고, 오버라이딩 적용 가능하다.
  • 다형성의 본질
  1. 인터페이스를 구현한 객체 인스턴스 "실행시점"에 유연하게 변경할 수 있다.
  2. 협력이라는 객체사이의 관계에서 시작해야 한다.
  3. 클라이언트를 변경하지 않고, 서버의 구현 기능을 유연하게 변경할 수 있다
* 스프링과 객체 지향
스프링에서 제어의 역전(IoC), 의존 관계 주입(DI) 는 다형성을 활용해 역할과 구현을 편리하게 다룰 수 있도록 지원한다.

 

상속

기존의 클래스를 재사용하여 새로운 클래스를 작성하는 자바의 문법 요소

상위 클래스와 하위 클래소르 나누어 상위 클래스의 멤버를 하위 클래스에게 내려주는 것을 의미합니다.

 

ㅇ 클래스의 트리구조로 이해하기

위의 그림을 보게되면 세개의 클래스에 공통적인 속성과 기능이 정의되어 있다는 것을 알 수 있습니다.

즉, 사람이라면 누구나 먹고 배우고 걷는 특성이 있습니다. 

 

  • 왜 상속을 사용할까?

상속을 통해 앞선 예제처럼 코드를 재사용하여 보다 적은 양의 코드로 새로운 클래스를 작성할 수 있습니다.

더 나아가 다형적 표현이 가능하다는 장점이 있습니다.

 

 * 다형적 표현이 가능하다 ?
프로그래머는 프로그래머다 = 참
프로그래머는 사람이다 = 참
이와 같이 하나의 객체가 여러 모양으로 표현될 수 있는것을 의미합니다.

 

  • 상속에서 주의할 점 !!

자바의 객체지향 프로그래밍에서는 다중 상속을 허용하지 않고, 단일 상속만을 허용한다는 것입니다. 

하지만 인터페이스를 통해 다중상속과 비슷한 효과를 낼 수 있습니다.

 

  • 상속과 함께 알아두면 좋을 포함관계

상속처럼 클래스를 재사용할 수 있도록 클래스의 멤버로 다른 클래스 타입의 참조변수를 선언하는 것

public class Employee {
    int id;
    String name;
    Address address;  // 참조 변수 !!

    public Employee(int id, String name, Address address) {
        this.id = id;
        this.name = name;
        this.address = address;
    }

    void showInfo() {
        System.out.println(id + " " + name);
        System.out.println(address.city+ " " + address.country);
    }

    public static void main(String[] args) {
        Address address1 = new Address("서울", "한국"); //Address 할당 후
        Address address2 = new Address("도쿄", "일본");

        Employee e = new Employee(1, "김코딩", address1); // Employee와 함께 재할당
        Employee e2 = new Employee(2, "박해커", address2);

        e.showInfo();
        e2.showInfo();
    }
}

class Address {
    String city, country;

    public Address(String city, String country) {
        this.city = city;
        this.country = country;
    }
}

 

  • 상속과 포함관계를 어떻게 구분해서 설정할 수 있을까 ?
  1. ~은 ~이다 ( IS-A 관계 ) : "Employee 는 Address이다" 는 성립하지 않는다.
  2. ~은 ~을 가지고 있다. ( HAS-A 관계 ) : "Employee는 Address를 가지고 있다" 는 성립

 이렇게 HAS-A 관계에서는 포함관계를 사용해주는게 적합합니다.

 

 

  • super 키워드

상속 관계에서 상위 클래스의 객체, super() 은 상위 클래스의 생성자를 의미합니다.

public class Super {
    public static void main(String[] args) {
        Lower l = new Lower();
        l.callNum();
    }
}

class Upper {
    int count = 20; // super.count
}

class Lower extends Upper {
    int count = 15; // this.count

    void callNum() {
        System.out.println("count = " + count);
        System.out.println("this.count = " + this.count);
        System.out.println("super.count = " + super.count);
    }
}
  • 위의 예제에서 super을 붙치지 않는다면?

 

자바 컴파일러가 해당 객체의 자신이 속한 인스턴스 객체의 멤버를 먼저 참조합니다.

즉, 자동적으로 this 역할을 하게 되는것입니다.

  • 상위클래스의 멤버 = super 
  • 자신의 멤버 = this

 

캡슐화

특정 객체 안에 관련된 속성과 기능을 하나의 캡슐로 만들어 데이터를 외부로부터 보호하는 것

 

  • 캡슐화를 왜 하는가?
  1. 외부로부터 객체의 속성과 기능이 함부로 변경되지 못하게 데이터를 보호
  2. 데이터가 변경되어도 다른 객체에 영향을 주지 않게 독립성을 확보
  3. 결론적으로 유지보수와 코드 확장시에 오류의 범위를 최소화 할 수 있음

 

  • 접근 제어자

데이터 노출을 방지할 수 있는 핵심적인 방법

  1. private : 동일 클래스에서만 접근 가능
  2. default : 동일 패키지내에서만 접근 가능
  3. protected : 동일 패키지 + 다른 패키지의 하위 클래스에서만 접근 가능
  4. public : 접근 제한 없음

 여기서 사실 private / default / public 은 이해가 되는데 protected가 잘 이해되지 않았습니다.

예제로 살펴보겠습니다.

package package1; // 패키지명 package1 

//파일명: Parent.java
class Test { // Test 클래스의 접근 제어자는 default
    public static void main(String[] args) {
        Parent p = new Parent();

        System.out.println(p.c);
    }
}

public class Parent { // Parent 클래스의 접근 제어자는 public
    protected int c = 3;

    public void printEach() { // 동일 클래스이기 때문에 에러발생하지 않음
        System.out.println(c);
    }
}

package package2; // package2 

//파일명 Test2.java
import package1.Parent;

class Child extends package1.Parent { // package1으로부터 Parent 클래스를 상속
    public void printEach() {
        System.out.println(c); // 다른 패키지의 하위 클래스
    }
}

public class Test2 {
    public static void main(String[] args) {
        Parent p = new Parent();
        
//        System.out.println(p.c); // 호출 에러!
    }
}

다른 패키지에서 prarent를 상속받은 클래스에서는 protect 멤버를 호출이 가능

단, 다른 클래스에서는 상속받지 않은 클래스의 경우 호출 에러가 나옵니다.

 

 * getter, setter메서드  :  private 객체의 데이터 값을 추가, 수정하고 싶은 경우에 사용

 * final 키워드
 - 클래스 : 변경/확장/상속 불가능
 - 메서드 : 오버라이딩 불가능
 - 변수 : 값 변경 불가능

 

 

 

추상화

객체의 공통적인 속성과 기능을 추출하여 정의하는 것

 

  • abstract, interface

- abstract : 추상메서드를 만드는 미완성 설계도입니다. 

- interface : 기본 설계도

 

  • 추상화를 왜 사용하는가?

메서의 내용이 상속 받는 클래스에 따라 종종 달라지기 때문에 상위 클래스에서 선언부만을 작성하고

실제 구현 내용은 상속을 받는 하위 클래스에서 구현하도록 비워둔다면 설계하는 상황이 변하더라도

유연하게 대응할 수 있습니다.

 이때 사용되는 것이 "오버라이딩" 입니다.

 

  • Interface 의 특징

1) class 대신 inteface 키워드를 사용하며 모든 필드가 public static final로 정의됩니다.

2) extends 키워드 대신 implements 키워드를 사용합니다.

3) 구현 클래스에서 는 해당 인터페이스에 정의된 모든 추상메서드를 반드시 구현해야합니다.

4) 하나의 클래스가 여러 인터페이스를 구현할 수 있는 다중 구현이 가능합니다.

    단, 인터페이스는 인터페이스로부터만 상속이 가능합니다.

 

 

* 인터페이스는 왜 다중 구현이 가능할까?
: 클래스에서 다중 상속이 불가능했던 이유는 부모 클래스에 동일한 이름의 필드,메서드가 있는 경우 충돌할 수 있기 때문입니다.
하지만 인터페이스는 애초에 미완성 멤버를 가지고 있기 때문에 충돌할 여지가 없습니다.

 

  • 추상클래스와 인터페이스의 사용의도 차이
  1. 추상클래스 : IS-A "~이다" 의 개념
  2. 인터페이스 : HAS-A "~을 할 수 있는" 의 개념

 

 

 

 

'JAVA' 카테고리의 다른 글

람다식  (0) 2022.07.18
좋은 객체 지향 설계의 5가지 원칙  (0) 2022.07.08
String 중간 공백기준으로 배열 만들기  (0) 2022.07.06
Hamcrest, Matcher란?  (0) 2022.07.03
Optional 이란?  (0) 2022.07.02

+ Recent posts