자바의 4가지 특징 - 추상화, 상속, 다형성, 캡슐화

알쓸코딩·2024년 3월 3일
0

트러블 슈팅

목록 보기
8/13
post-thumbnail

📝 공부한 것들을 끄적끄적

1. 객체

  • 자바는 class 가 무조건 필요하다.
  • class 안에는 멤버변수와 메서드가 존재한다.
  • 메서드는 class 내에 만드는 가장 작은 단위이다.
  • class 로 생성된 것이 객체이다.
  • 객체는 인스턴스와 유사한 말이다.
  • 파이썬은 함수와 메서드가 동시에 존재하므로 구분지어 부른다.

예를 들어보면
class는 포켓몬이고 class의 멤버변수와 메서드로 생성된 피카츄는 객체이다.
피카츄, 파이리, 꼬부기들은 포켓몬의 인스턴스라고 불린다.

2. 추상화 (Abstration)

  • 구현하는 방법은 추상 클래스(abstract)와 인터페이스(interface) 가 있다.
  • 인터페이스는 추상 메서드나 상수를 통해서 어떤 객체가 수행해야 하는 핵심 역할만을 규정해두고, 실제적인 구현은 해당 인터페이스를 구현하는 각각의 객체들에서 하도록 하는 것이다.

예를 들어보면

  • Vehicle 인터페이스

  • 자동차와 오토바이의 공통 기능인 전진과 후진을 추출하여 Vehicle이라는 상위 클래스에 정의한다.

  • Car 클래스

  • MotorBike 클래스

3. 상속 (Inheritance)

  • 상위 클래스로부터 확장된 여러 개의 하위 클래스들이 모두 상위 클래스의 속성과 기능들을 간편하게 사용할 수 있다.

예를 들어보면

빨간색으로 표시된 속성과 기능들은 자동차와 오토바이의 공통적인 부분들이고, 푸른색으로 표시된 부분들은 그렇지 않은 부분들이다.
코드로 표현해보면

  • Car 클래스

  • MotorBike 클래스

각각의 클래스마다 속성으로 model, color, wheels 기능으로 moveForward()와 moveBackward 가 완전히 동일한 코드임에도 불구하고 계속 반복되고 있다는 점을 확인할 수 있다. 또한 하나의 코드에서 변경 사항이 일어나면, 해당 코드의 변경 사항을 다른 클래스에서도 일일이 수정해주어야 한다.

이를 상속을 통해서 재정의해보자.

  • Vehicle 클래스

  • Car 클래스

  • MotorBike 클래스

  • Main 실행 클래스

Car 와 MotorBike 클래스의 공통적인 속성과 기능들을 추출(추상화)하여 Vehicle클래스(상위 클래스)에 정의하였고, extends 키워드를 통해 각각의 하위 클래스로 확장하여 해당 기능과 속성들을 매번 반복적으로 정의해야 하는 번거로움을 제거했다. 또한, 공통적인 코드의 변경이 있는 경우 상위 클래스에서 단 한 번의 수정으로 모든 클래스에 변경 사항이 반영될 수 있도록 만들었다.

상위 클래스의 기능과 속성들을 그대로 사용할 수도 있지만, 각각의 클래스의 맥락에 맞게 메서드 오버라이딩(method overriding)을 사용하여 내용을 재정의할 수도 있다.

사실 이 부분이 앞서 추상화에서 봤었던 인터페이스를 통한 구현과 상속을 구분하는 핵심적인 차이 중에 하나라 할 수 있다. 즉, 양자 모두 상위 클래스-하위 클래스의 관계를 전제하면서 공통적인 속성과 기능을 공유할 수 있지만, 상속의 경우 상위 클래스의 속성과 기능들을 하위 클래스에서 그대로 받아 사용하거나 오버라이딩을 통해 선택적으로 재정의하여 사용할 수 있는 반면, 인터페이스를 통한 구현은 반드시 인터페이스에 정의된 추상 메서드의 내용이 하위 클래스에서 정의되어야 한다.

결론적으로, 상속 관계의 경우 인터페이스를 사용하는 구현에 비해 추상화의 정도가 낮다고 할 수 있다. 인터페이스가 역할에 해당하는 껍데기만 정의해두고, 하위 클래스에서 구체적인 구현을 하도록 강제하는 것에 비해, 상속 관계의 경우 상황에 따라 모든 구체적인 내용들을 정의해두고 하위 클래스에서는 그것을 단순히 가져다가 재사용할 수 있다.

4. 다형성

  • 다형성(多形性) : 한자 이름 그대로 어떤 객체의 속성이나 기능이 상황에 따라 여러 가지 형태를 가질 수 있는 성질

비유적으로 표현하면
어떤 중년의 남성이 있다고 했을 때 그 남자의 역할이 아내에게는 남편, 자식에게는 아버지, 부모님에게는 자식, 회사에서는 회사원, 동아리에서 총무 등 상황과 환경에 따라서 달라지는 것과 비슷하다고 할 수 있다.

  • 어떤 객체의 속성이나 기능이 그 맥락에 따라 다른 역할을 수행할수 있는 객체 지향의 특성

  • 메서드 오버라이딩과 메서드 오버로딩(method overloading)이 존재한다.

  • Vehicle 인터페이스

  • Car 클래스

메서드 오버라이딩을 사용하면 같은 이름의 moveForward() 와 moveBackward() 를 각각의 클래스의 맥락에 맞게 재정의하여 사용할 수 있다. 즉, 같은 이름의 메서드가 상황에 따라 다른 역할을 수행하는 것이다. 또한, 하나의 클래스 내에서 같은 이름의 메서드를 여러 개 중복하여 정의하는 것을 의미하는 메서드 오버로딩도 이와 같은 맥락이라 할 수 있다.

사실 앞서 언급한 메서드 오버라이딩과 메서드 오버로딩도 다형성의 한 중요한 예시지만, 객체 지향의 맥락에서 이것보다 더 중요한 다형성의 정의는 이것이다.

객체 지향 프로그래밍에서 다형성이란 한 타입의 참조변수를 통해 여러 타입의 객체를 참조할 수 있도록 만든 것을 의미합니다. 
좀 더 구체적으로, 상위 클래스 타입의 참조변수로 하위 클래스의 객체를 참조할 수 있도록 하는 것입니다.

에를 들어보면

  • Main 실행 클래스

다형성을 활용하면 여러 종류의 객체를 배열로 다루는 일이 가능해진다.

  • Driver 클래스

  • Main 클래스

Driver 클래스의 코드는 매우 간단하다. 즉, 매개변수로 자동차나 오토바이객체를 전달받아 운전하는 것이다. 이렇게 하나의 객체가 다른 객체의 속성과 기능에 접근하여 어떤 기능을 사용할 때, 우리는 A클래스는 B클래스에 의존한다 라고 표현한다.

즉 Driver 클래스와 다른 두 개의 클래스가 서로 직접적인 관계를 가지고 있는데, 이러한 상황을 조금 어려운 말로 객체들 간의 결합도가 높다고 표현한다.

하지만 이렇게 결합도가 높은 상태는 객체 지향적인 설계를 하는 데 매우 불리하다.

만약 지금처럼 이동 수단이 자동차와 오토바이 단 2개가 아니라 수 십, 수 백개라면 아마 아래와 같이 똑같은 코드를 수 십, 수 백 번 작성해야 한다.

또 만약에 어떤 새로운 상황이 발생해서, MotorBike 클래스가 다른 클래스 MotorCycle 클래스로 변경되어야 하는 경우에는 Driver 안에 매개변수로 전달되는 참조변수의 타입과 참조변수를 수정할 수밖에 없는 상황이 발생한다.

  • Driver 클래스

이런 맥락에서, 객체 지향 프로그래밍은 지금까지 학습한 추상화, 상속, 그리고 다형성의 특성을 활용하여 프로그래밍을 설계할 때 역할과 구현을 구분하여 객체들 간의 직접적인 결합을 피하고, 느슨한 관계 설정을 통해 보다 유연하고 변경이 용이한 프로그램 설계를 가능하게 만들었다.

Vehicle 인터페이스 적용 전

Vehicle 인터페이스 적용 후

  • Vehicle 인터페이스

drive() 메서드로 전달되는 매개변수의 타입을 상위 클래스인 인터페이스 타입 Vehicle 로 변경한 것이다.
이제 다형성의세례를 받은 drive() 메서드의 매개변수로 인터페이스를 구현한 객체라면 무엇이든 전달이 될 수 있게 되었다.

Vehicle 인터페이스를 통해 간접적으로 연결되어 결합도가 낮아졌다. 따라서, 이제 Driver 클래스는 더 이상 각각의 클래스 내부의 변경이나 다른 객체가 새롭게 교체되는 것을 신경 쓰지 않아도 인터페이스에만 의존하여 수정이 있을 때마다 코드 변경을 하지 않아도 된다.

하지만, 여전히 new Car() 와 new MotorBike() 처럼 객체에 직접적으로 의존하고 있어서, 해당 객체를 다른 객체로 변경할 시 코드의 변경이 불가피하다. 이 문제를 해결하기 위해 등장한 것이 바로 의존관계 주입(dependency injection)이라 부르는 스프링 프레임워크의 핵심적인 개념이다.

이 내용은 스프링과 스프링부트(Spring Boot) - 정의, 특징, 사용 이유, 생성 방법 포스팅을 참고하자.

다시 돌아가서 정리하면, 객체 지향 프로그래밍은 객체 간 관계와 협력을 설계하는 것인데, 다형성은 그 관계를 보다 유연하고 확장이 용이한 설계가 가능하도록 하는데 핵심적인 역할을 한다는 사실이 중요하다. 또한, 다형성을 제대로 활용하기 위해서 앞서 배웠던 추상화와 상속에 대한 내용들이 함께 존재해야 한다는 사실도 기억해야 한다. 즉, 추상화가 있어야 각 객체들의 역할 정의가 가능하고, 인터페이스는 상위 클래스-하위 클래스를 전제하기 때문에 상속에서 배웠던 개념들이 함께 필요하다.

5. 캡슐화

캡슐화를 하는 이유

  • 데이터 보호(data protection) – 외부로부터 클래스에 정의된 속성과 기능들을 보호
  • 데이터 은닉(data hiding) – 내부의 동작을 감추고 외부에는 필요한 부분만 노출

  • 구현 방법은 접근 제어자와 Getter, Setter를 이용하는 두 가지가 있다.

Car 클래스

Driver 클래스

Main 실행 클래스

Car 클래스의 3가지 메서드들에 어떤 변경이 생겼다고 가정해보자. 그러면 해당 메서드들을 사용하고 있는 Driver 클래스의 drive() 메서드의 수정이 불가피하다. 다른 말로, Driver 클래스가 Car 클래스 의 세부적인 내부 로직을 속속히 너무 잘 알고 있고, 이것은 앞서 우리가 계속 피하고자 했던 객체 간의 결합도가 높은 상태를 의미한다.

이럴 때 우리는 캡슐화를 활용하여 객체의 자율성, 즉 하나의 객체가 해당 객체의 속성과 기능에 대한 독점적인 책임을 담당하도록 만들고, 이를 통해 객체 간의 결합도를 낮게 유지할 수 있다.

코드로 살펴보면

Car 클래스

Driver 클래스

Main 실행 클래스

아까와 출력값은 동일하지만, 기존의 Driver 클래스가 하나하나 호출해줬던 메서드들을 모두 operate() 메서드로 묶어 Car 클래스로 옮겨두었고, Driver 클래스에서는 내부 동작을 전혀 신경쓰지 않아도 단순히 operate() 메서드를 호출하여 사용하고 있다.

또한, operate() 메서드 내부의 메서드들은 외부에서 호출되어 사용할 일이 없으므로 접근 제어자를 모두 private으로 변경해주었다. 정리하면, Car 클래스와 관련된 기능들은 온전히 Car 에서만 관리되도록 하였고, 불필요한 내부 동작의 노출을 최소화하였다. 이제 Driver 클래스의 입장에서는 더 이상 Car 클래스의 내부 로직을 알지 못하고, 알 필요도 없어졌다.

이렇게 캡슐화를 활용하면, 객체 내부의 동작의 외부로의 노출을 최소화하여 각 객체의 자율성을 높이고, 이를 통해 객체 간 결합도를 낮추어 앞서 설명한 객체 지향의 핵심적인 이점을 잘 살리는 방법으로 프로그램을 설계하는 일이 가능하다.

profile
알면 쓸데있는 코딩 모음!

0개의 댓글