Java

객체 지향이란?

shininghyunho 2024. 1. 17. 15:00

객체 지향?

객체란 현실 세계의 유무형의 사물들을 표현하기 위한 자료형입니다.

이때 객체는 3가지 요소를 갖습니다.

속성값, 액션(메서드), 관계.

 

자동차를 객체로 따지면 다음과 같을것입니다.

속성값 : 모델명, 색상, 연비, 마력 등

메서드 : 시동걸기, 전진하기, 후진하기, 방향 전환하기 등

관계 : 다른 브랜드 자동차, 오토바이 등

 

여기서 가장 눈여겨 보아야할점은 관계입니다.

객체는 다른 객체들과의 긴밀한 연결성을 갖고 있습니다.

 

예를들어 현대에서 여러가지 자동차, 오토바이를 생산한다고 했을때

여러 객체들관의 관계를 명확히 설계해야

고도화된 설비와 생산이 가능해질 것입니다.

그리고 우리는 이러한 객체들을 이용한 설계를 객체 지향이라고합니다.

 

객체 지향을 왜 써야할까?

간단하게 비유를 통해 객체 지향을 알아봤지만 뭔가 아쉽습니다.

그래서 다음 포스팅에서 예제와 함께 상세한 이유를 다뤄봤습니다.

객체지향을 사용하는 이유 (절차지향과 비교)

 

객체 지향의 특징은 무엇일까?

이제 객체 지향을 사용하면 어떤점에서 유리한지 알았습니다.

 

4가지 객체 지향의 특징이 무엇이 있고
어떤 장점이 있는지 살펴보겠습니다.
(클래스의 생명주기가 생기는것을 객체라고 하는데편의상 객체와 클래스를 혼합해서 부르겠습니다.)

 

캡슐화

캡슐화는 이름에서 느껴지듯이 캡슐처럼 데이터를 보호할 수 있는 특징이 있습니다.

내가 보여주고싶은 데이터는 공개하고

숨기고 싶은건 비공개로합니다.

 

프로제트를 진행하다보면 클래스 변수는 private 으로 해두고,

공개해도 되는 private 변수를 public 메서드로 접근하게 합니다.

 

반대로 비밀번호처럼 처음부터 끝까지 공개되면 안되는것들은

private으로 설정해두고 메서드로도 접근이 불가능합니다.

(심지어 비밀번호는 해쉬함수로 암호화까지합니다.)

그래서 유저가 입력한 비밀번호와 해쉬값이 같은 경우처럼

간접적으로 데이터에 접근합니다.

 

다만 캡슐화로 인해 접근자 (getter, setter) 구현이 불가피해져

코드의 길이가 장황해집니다.

상속

객체 지향에서 상속은 가장 중요한 특징입니다.
나머지 특징인 추상화 다형성은 상속에 개념으로부터 나오기 때문입니다.

상속은 간단하게 말해 객체의 성질을 다른 객체에게 물려주는 행위입니다.
구체적으로 변수와 메서드를 하위 객체가 재사용 할 수 있도록 해주는거죠.

그래서 하위 객체는 동일한 기능을 다시 구현할 필요가 없어,
재사용성이 좋다고 할 수 있죠.

 

상속 예시

고등학교때 생명을 배우면 종속군 이런걸 들어보지 않았습니까?
이때 가장 큰 카테고리는 동물입니다. (모든 생명이 동물이니까요.)

출처: http://cafe.naver.com/5825858/15742

 

동물계는최상위 부모 클래스입니다.

그리고 계문강목과속종은 각각 부모-자식의 관계를 갖게됩니다.

대표적인 상속의 예시라고 생각할 수 있죠.

 

그래서 사람, 고양이, 개는 모두 다른 종이지만 공통의 포유류 특징을 갖고있습니다.

이를 클래스로 만든다면 포유류 클래스에는 '새끼를 낳는다'같은 메서드가 있겠죠.


또한 다음에 기능을 수정한다고하면 부모 객체(클래스)만 변경하고
나머지 자식들은 변경이 필요없기에 유지보수에도 유리합니다.

다만 상속이 여러번되고 다양한 기능이 구현된다면
부모-자식(도는 자식의 또 자식까지도)간에 강한 결합이 생깁니다.
그래서 반대로 유지보수가 힘들게 될 수도 있죠.


이를 방지하기 위해, 여러 단계를 거친 복잡한 설계가 필요합니다.

그리고 객체가 관계가 깊어질수록, 가독성이 떨어져 객체의 기능 파악이 힘들어집니다.
따라서 객체의 상속은 사용 목적에 따라 적절하게 해야합니다.

다형성

다형성은(polymorphism)은 상속이 일어났을때 ,

부모의 동일한 메서드를 자식들별로 다르게 구현하는것입니다.

 

예를들어 포유류는 새끼를 낳지만 임신 기간이 다릅니다.

(정확한거 아님)

개:3달

고양이:2달

사람:10달

같이 모두 다른 구현이 될 수 있죠.

 

public class Main {
    public static void main(String[] args) {
        Mammal[] mammals = {new Dog(), new Cat(), new Human()};

        for (Mammal mammal : mammals) {
            mammal.breed();
        }
    }
}
/* 출력결과
개는 3달 임신했습니다.
고양이는 2달 임신했습니다.
사람은 10달 임신했습니다.
*/

 

다형성을 활용한다면 타입을 모두 동일한 부모의 클래스로 객체를 만들고

호출할 수 있어 확장성유지보수에 좋습니다.

만약에 여기에 다른 포유류가 추가된다면 기존 메인 메서드는 크게 건드릴게 없죠.

 

또한 자식들별로 다른 메서드를 호출하지 않아도 되서 코드도 간결해집니다.

 

추상화

추상화는 구체적인걸 숨겨둔 껍데기를 의미합니다.

마치 스마트폰의 홀드 버튼, 음향 버튼 같은걸 생각하면 편합니다.

우리는 홀드를 누르면 휴대폰이 켜지는걸 알지만 실제로 어떻게 동작하는지는 모르죠.

(그리고 알려주지도 않고요.)

 

그럼 추상화를 왜 사용할까요?

위의 예시처럼 추상화를 사용하면 구체적인 내용을 알 수가 없습니다.

갤럭시의 홀드 버튼 기능의 구체적인 동작이 사용자에게 알려진다면

갤럭시 스마트폰 자체에 핵심기능들이 드러나겠죠.

그러면 자연스럽게 보안성이 떨어집니다.

 

만약에 결제와 관련된 기능이라고하면 그 위험성은 더욱 커지겠죠.

서비스를 제공하는 입장에서는 핵심 기능은 알려주고싶지 않습니다.

 

만약 다음과 같은 스마트폰 예제가 있다고 해보죠.

abstract class SmartPhone {
    abstract void holdButton();
}
class Galaxy extends SmartPhone {
    @Override
    void holdButton() {
        System.out.println("갤럭시 홀드 버튼");
        // 정말 민감한 동작
    }
}
class Apple extends SmartPhone {
    @Override
    void holdButton() {
        System.out.println("애플 홀드 버튼");
        // 보안에 중요한 동작
    }
}
public class Main {
    public static void main(String[] args) {
        SmartPhone[] phones=new SmartPhone[]{new Galaxy(),new Apple()};
        for(SmartPhone phone:phones) {
            phone.holdButton();
        }
    }
}
/*출력결과
갤럭시 홀드 버튼
애플 홀드 버튼
*/

 

그러면 사용자에게는 핵심 코드는 전달하지 않고 추상화된 코드만 전달하면 됩니다.

 

// 사용자에게 공개할 코드
abstract class SmartPhone {
    abstract void holdButton();
}

 

이제 사용자는 민감한 기능을 알 수 없지만 어떤 동작을 하는지는 알 수 있게되죠.

 

또한 추상화는 개발자 입장에서도 굉장히 편리합니다.

내가 스마트폰을 개발하고 싶다?

그러면 스마트폰 추상 클래스를 상속하면 됩니다.

이제 추상화된 클래스의 메서드를 구현하면 되죠.

 

다른 개발자가 봤을때도 스마트폰 추상 클래스를 상속했다?

'아 그러면 이 클래스는 스마트폰의 기능들이 구현된 클래스구나' 라고 바로 알 수 있죠.

(추상화된 메서드는 반드시 구현해야하기 때문이죠.)

 

// 엄청난 카메라 기능
interface SuperCamera {
    void zoom100();
}
// 스마트폰
abstract class SmartPhone {
    abstract void holdButton();
}
class Galaxy extends SmartPhone implements SuperCamera {
    @Override
    void holdButton() {
        System.out.println("갤럭시 홀드 버튼");
        // 정말 중요한 동작
    }

    @Override
    public void zoom100() {
        System.out.println("갤럭시의 엄청난 100배 줌입니다.");
    }
}
class Apple extends SmartPhone {
    @Override
    void holdButton() {
        System.out.println("애플 홀드 버튼");
        // 보안에 중요한 동작
    }
}
public class Main {
    public static void main(String[] args) {
        System.out.println("--공통기능--");
        SmartPhone[] phones=new SmartPhone[]{new Galaxy(),new Apple()};
        for(SmartPhone phone:phones) {
            phone.holdButton();
        }
        System.out.println("--개별기능--");
        SuperCamera camera=new Galaxy();
        camera.zoom100();
    }
}
/*출력결과
--공통기능--
갤럭시 홀드 버튼
애플 홀드 버튼
--개별기능--
갤럭시의 엄청난 100배 줌입니다.
*/

 

위 예제를 보면 갤럭시에만 수퍼카메라 기능이 있죠.

그래서 갤럭시 개발자들은 해당 클래스가 수퍼카메라 인터페이스를 구현하는걸 보고

해당 기능이 있음을 알 수 있습니다.

기능의 강제화와 규격

이렇게 추상 메서드를 통해 기능 구현을 강제화해 해당 클래스의 특징을 나타내게 되죠.

 

이를 다르게 생각하면 해당 클래스를 상속하는 하위 클래스들은

모두 동일한 특징을 갖게 된다는 점입니다.

그래서 여러개의 제품을 만들때의 규격을 만든다고 생각할 수 있습니다.

 

위 예제에서 스마트폰 클래스에 홀드버튼은 어떤 스마트폰이건 반드시 갖고있는 특징입니다.

여기에 c-type, 음향버튼 메서드를 추가하고,

이를 구현한 다양한 클래스가 생긴다면 (구글 픽셀폰, 샤오미 등)

서로 호환이 되는 스마트폰이 될것입니다.

동일한 규격을 갖는다는 말입니다.

 

추상화 클래스들이 헷갈리면 다음 포스팅을 확인해보세요.

추상클래스, 인터페이스의 문법적,의미적 차이