프로그래밍/Java 정리

객체 지향 프로그래밍(OOP)

윤도ri 2022. 7. 21. 21:25

| 본문을 시작하기 전..

객체지향 프로그래밍이란.. 자바 개발자에게 꼭 알아야 할 중요한 개념이다. 그러나 막상 객체지향 프로그래밍이 뭘까요? 라고 물어본다면.. 추상화를 통해 객체를 만들어서 프로그래밍하는것..? 이라고 밖에 떠오르지 않는다. 객체지향 프로그래밍은 개발을 하면서 부족한 점을 개선하려보니 탄생한 프로그래밍 패러다임이다. 그래서 나는 이번 포스팅에서 이전의 프로그래밍 몇가지에 대해서 먼저 이야기 해보고자 한다.

| 순차적(비구조적) 프로그래밍

: 정의한 기능의 흐름에 따라 순서대로 동작을 추가하며 프로그램을 완성하는 방식이다.

간단한 프로그램의 경우, 이렇게 코드를 짜게 되면 흐름이 눈으로 보이기 때문에 매우 직관적일것이다. 그러나 프로그램의 규모가 커진다면 곤란해진다. A > B > C 동작을 구현하다가, C > A로 돌아가려는 상황이면 goto 를 활용해야한다.

goto문을 무분별하게 활용하게 되면, 쭉 나열된 코드 속에서 스파게티 마냥 위로 갔다 아래로 갔다 난리가 아닐것이다.
이렇게 되면 동작이 직관적이지 못하고, 유일한 장점이 사라지게 된다.

그래서 등장한 것이 절차적, 구조적 프로그래밍이다.

| 절차적 프로그래밍

: 절차적 프로그래밍에서 '절차'는 함수를 의미한다. 따라서 절차적 프로그래밍이란, 반복되는 동작을 함수 및 프로시저 형태로 모듈화하여 사용하는 방식이다.

이 패러다임은 반복동작을 모듈화하여 코드를 많이 줄일 수 있다. 하지만 프로시저라는 것 자체가 너무 추상적이라는 단점이 있다.

예를 들면, 도서관의 도서 관리 프로그램을 개발한다고 했을때

  • '책'이라는 자료형을 구현해야 함
  • 책에 대한 함수를 구현해야 함

그러나, 구조적 프로그래밍에서는 이 둘을 따로 생각할 수 밖에 없다. 책은 책이고 책에 관한 함수는 따로 있기 때문에, 같은 소스 파일 내에 있더라도 이 둘의 연관 여부는 단 번에 알아차리기 어렵다. 즉, 논리적으로 묶여있을 수 없는 구조이기 때문에 동작이 추상적인 것이다.

따라서, 이를 묶기 위한 패러다임으로 '객체지향 프로그래밍'이 등장하게 된다.

| 객체 지향 프로그래밍

어떤 개념에 대한 자료형과 함수를 '객체' 형태로 함께 묶어서 관리하기 위해 객체 지향 프로그래밍 패러다임이 등장하게 되었다. 핵심 포인트는 객체 내부에 자료형 필드와 함수가 함께 존재한다는 것이다. 가능한 모든 물리적, 논리적 요소를 객체로 만드는 것이 객체지향 프로그래밍이다.

위에 들었던 예시인 '도서 관리 프로그램'도 객체지향으로 구현하게 되면 책의 제목,저자,페이지수와 같은 자료형 필드와 대출하기,반납하기 등의 메소드를 책이라는 객체에 함께 관리하는 것이 가능해진다. 이렇게 되면 추상적이었던 동작도 훨씬 직관적으로 보이게 되어 코드 가독성이 증가한다.

결론적으로 객체 간의 독립성이 뚜렷하게 생기고, 중복되는 코드의 양이 줄어든다. 따라서 유지보수에 용이해질 것이다.


| 객체지향 프로그래밍의 4가지 특징

1. 추상화 (Abstraction)

: 객체들의 공통적인 속성과 기능을 뽑아내는 것이다.

추상적인 개념에 의존하여 설계해야 유연함을 갖출 수 있다. 즉, 세부적인 사물들의 공통적인 특징을 파악한 후, 하나의 묶음으로 만들어내는 것이 추상화다.

ex) '평생 살 수 없는 아파트'라는 추상화 집합을 만들어두고, 평생 살 수 없는 아파트들이 가진 공통적인 특징들 ( 서울에 위치, 30억 이상함, 벼락부자 또는 금수저만 살 수 있음^^..) 을 만들어서 활용하면 된다.

2. 캡슐화 (Encapsulation)

: 정보 은닉화를 통해 높은 응집도, 낮은 결합도를 유지할 수 있도록 설계하는 것

쉽게 말하면, 한 곳에서 변화가 일어나도 다른곳에 미치는 side effects 를 최소화 시키는 것을 의미한다. 즉, 객체 내부의 어떤 동작에 대한 구현이 어떻게 되어있는지 감추는 것이다. 이를 통해 외부에 세부 데이터가 노출되는 것을 방지하고 객체가 손상되는 일 또한 방지 할 수 있다.

결합도란 어떤 기능을 실행할 때 다른 클래스가 모듈에 얼마나 의존적인지를 나타내는 지표이다. 객체 간의 독립성을 강조하기 위해 객체 지향 프로그래밍이 등장했다. 그런데 결합도가 높아지면 객체지향으로 설계하는 의미가 있을까?

따라서, 독립적으로 만들어진 객체들 간의 의존도를 최대한 낮게 만드는 것이 중요한다. 이 때문에 소프트웨어 공학적으로, 객체 내의 모듈 간의 요소가 밀접한 관련이 있는 것으로 구성하여 응집도를 높이고, 결합도를 줄여야 요구사항 변경에 대처하는 좋은 설계라고 배운다.

한 줄로 정리하자면, 객체 각각은 독립적으로 작용할 수 있도록 응집도가 강해야 하고 결합도는 낮아야 한다.
높은 응집도와 낮은 결합도는, '은닉화'를 통해 이루어낼 수 있다. 외부에서 접근할 필요 없는 것들은 접근 지정자를 private 으로 두어 접근에 제한을 두는 것이다. 외부 객체는 객체 내부의 구조를 모르게 하고, 해당 객체가 노출해서 제공하는 필드와 메소드만 이용할 수 있도록 하여 의도하지 않은 동작 오류를 방지하고 유지보수 효율을 높일 수 있다.

3. 상속 (Inheritance)

동물에 인간과 애완동물이 속하고 애완동물에는 강아지와 고양이가 속하는 관계임을 볼 수 있다.  동물의 특징을 이들이 물려받을때 상속받는다라고 할 수 있다.

: 상위 개념의 특징을 하위 개념이 물려받는 것을 의미한다.

상속을 활용하면 상위 클래스의 구현을 활용함으로써, 코드 재사용이 용이해진다. 그러나 상속을 통한 재사용을 할때 나타나는 단점이 명백하다. 따라서 객체 지향 프로그래밍에서 '코드 재사용'을 목적으로 하는 상속 행위는 엄격히 금한다. 단점은 아래와 같다.

1) 부모 클래스의 변경이 불편해짐
:부모 클래스에 의존하는 자식 클래스가 많을 때 부모 클래스의 변경이 필요하다면, 이를 의존하는 자식 클래스들이 영향을 받게 된다.
2) 불필요한 클래스의 증가
: 유사 기능 확장시, 필요 이상의 불필요한 클래스를 만들어야 할 수 있다.
3) 잘못된 상속 사용
: 같은 종류가 아닌 클래스의 구현을 재사용하기 위해 상속을 받게 된다면, 문제가 발생할 수 있다.
상속받는 클래스가 부모 클래스와 IS-A 관계가 아닐 때 발생함.

그렇다면 이러한 문제는 어떻게 해결할까? 바로 구성(Composition) 을 통해 해결이 가능하다.

객체 구성은 객체 내부 필드에서 다른 객체를 참조하는 방식으로 구현한다. 상속에 비해 런타임 구조가 복잡하고 구현이 어렵지만, 변경시 유연함을 확보할 수 있다는 장점이 있다. --> 같은 종류가 아닌 클래스를 상속 받고 싶을 땐, 객체 컴포지션을 먼저 적용해볼것.

상속이 반드시 이 두가지 경우에만 일어나야 한다.

  • IS-A 관계가 성립할 때
  • 재사용 관점 X, 기능의 확장 관점 O

상속을 코드 재사용의 개념으로 이해하면 안된다. 이렇게 되면 클래스간 결합도가 너무 높아져 유지보수 효율이 낮아진다. 일반적인 개념을 구체화하는 상황에서 상속을 사용하자!


(* IS-A 란 말그대로 포함 관계를 의미한다. eg. 햄스터는 동물이다. (햄스터의 부모 클래스는 동물) )

이와 반대되는 것이 HAS-A 이다.
(* HAS-A 란 구성(Composition)관계를 의미한다. 한 객체가 다른 객체에 속한다는 의미이다. eg.컴퓨터안에는 램이 있다. (컴퓨터 객체가 램 객체를 구성함))

4. 다형성 (Polymorphism)

같은 메소드를 사용했는데 각자 특성에 맞게 동작하는 것을 볼 수 있다.

: 서로 다른 클래스의 객체가 같은 동작 수행 명령을 받았을 때, 각자의 특성에 맞는 방식으로 동작하는 것이다.

객체지향 패러다임의 핵심이다. 다형성은 상속과는 시너지가 엄청나다. 다형성 구현을 통해 코드를 간결하게 해주고, 유연함을 갖추게 된다. 또한, 구체적으로 현재 어떤 클래스 객체가 참조되는 지는 무관하게 헐렁하게 프로그래밍하는 것이 가능하다.

상속 관계에 있다면, 새로운 자식 클래스가 추가되어도 부모 클래스의 함수를 참조해오면 되기 때문에 다른 클래스는 영향을 받지 않게 된다.

쉽게 생각하려면 오버라이딩, 오버로딩을 생각하면 될 것 같다.

1) 오버라이딩(Overiding)
- 같은 메서드 이름, 같은 인자 목록, 상위 클래스의 메서드를 재정의한다.
- 상위 클래스 타밉의 객체 참조 변수에서 자동으로 하위 클래스가 오버라이딩한 메소드를 호출해준다.

2) 오버로딩(Overroading)
- 같은 메서드 이름, 다른 인자 목록, 다수의 매서드 중복 정의가 가능하다.

(오버라이딩과 오버로딩에 대해 더 자세히 알고 싶다면, 이 게시글을 참고 하면 이해가 쉬울것이다.
https://turtle8760.tistory.com/39?category=1059080 )

| 객체 지향 설계 원칙 (SOLID)

: 객체 지향적으로 설계하기 위해 SOLID 라 불리는 다섯 가지 원칙이 있다.

1. 단일 책임 원칙(SRP, Single Responsibility Principle)

  • 하나의 클래스는 단 하나의 책임만 가져야 한다.
  • 단일 책임 원칙을 지키지 않을 경우 한 책임의 변경에 의해 다른 책임과 관련된 코드에 영향이 갈 수 있다. ->이렇게 되면 유지보수가 매우 비효율적이다.

책임이라는 기준이 모호하기 때문에 변경을 책임의 기준으로 삼으면 설계에 용이 할 수 있다.어떠한 역할에 대해 변경사항이 발생했을때, 애플리케이션의 파급 효과가 적으면 SRP 원칙을 잘 따른것으로 볼 수 있다.

2. 개방-폐쇄 원칙(OCP, Open/Closed Principle)

  • 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
  • 기능을 변경하거나 확장할 수 있으면서 기능을 사용하는 코드는 수정하지 않는다.

확장에 열려있다라는 것은 모듈의 확장성을 보장하는 것을 의미한다. 새로운 변경사항이 발생했을 때 유연하게 코드 추가 또는 수정을 할 수 있기 때문이다. 그러나 객체를 직접적으로 수정하는것은 제한해야 한다. 기능이 추가되거나 수정할 때 객체를 직접적으로 수정해야 한다면 새로운 변경사항에 대해 유연하게 대응할 수 없는 애플리케이션이다.
이는 유지보수의 비용증가가 될 수 있으며, 객체 지향적인 설계로 볼 수 없다.

따라서 객체를 직접 수정하지 않고도 변경사항을 적용할 수 있도록 설계해야 한다. 결과적으로 OCP는 추상화를 의미하는것으로 해석된다. 자주 변화하는 부분을 추상화함으로써 기존 코드를 수정하지 않고도 기능을 확장할 수 있도록 함으로써 유연함을 높이는것이 핵심이다.

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

  • 프로그램 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
  • 상위 타입의 객체를 하위 타입의 객체로 치환해도, 상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다.

즉, 상속관계에서는 꼭 일반화 관계(IS-A)가 성립해야 하고 상속관계가 아닌 클래스들을 상속관계로 설정하면, 이 원칙에 위배될 것이다. 예를들어 정사각형과 직사각형을 생각해보자. 정사각형도 넓게 보면 직사각형의 한 종류이니, 직사각형을 상속하여 정사각형 객체를 빠르게 만들 수 있을 것이라 생각할 수 있다. 그러나 실제로 자식 클래스인 정사각형에서 함수를 실행하면 객체의 높이와 너비를 같게 만들기 때문에 원하는 값을 얻기 어렵다.

곰곰히 생각해보면 사각형의 특징을 서로 갖고 있긴 하지만, 두 사각형 모두 사각형의 한 종류일 뿐이므로, 하나가 다른 하나를 완전히 포함하지 못하는 구조다. 즉, 이 경우 리스코프 치환 원칙에 위배된다.

따라서, 상속 관계를 잘 정의하여 LSP 원칙이 위배되지 않도록 설계해야 한다.

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

  • 클라이언트는 자신이 사용하는 메소드에만 의존해야 한다.

구현할 객체에게 무의미한 메소드의 구현을 방지하기 위해 반드시 필요한 메소드만을 상속/구현하도록 권고한다. 만약 상속할 객체의 규모가 너무 크다면, 해당 객체의 메소드를 작은 인터페이스로 나누는것이 좋다.

  • 한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 않아야 한다. ->하나의 통상적인 인터페이스보다는 차라리 여러 개의 세부적인 인터페이스가 낫다
  • 인터페이스는 해당 인터페이스를 사용하는 클라이언트를 기준으로 잘게 분리되어야 한다.

위 그림은 규모가 너무 큰 객체를 상속했을 때 발생하는 문제와 이를 인터페이스로 분리하여 해결하는 방법을 도식화 한 것이다. 두번째 객체는 아무런 문제가 없지만 세번째 객체를 보면 상속 했기 때문에 좋든 싫든 해당 메소드를 가지고 있어야 한다.

하지만 상속 대상인 객체의 메소드를 각 동작별로 구분해 인터페이스로 만든다면 각 객체가 인터페이스만을 상속하여 구현하면 되므로 각자가 필요한 메소드만을 가지게 된다. 이것이 인터페이스 분리 원칙을 지향하는 바이다.

각 클라이언트가 필요로 하는 인터페이스들을 분리함으로써, 클라이언트가 사용하지 않는 인터페이스에 변경이 발생하더라도 영향을 받지 않도록 만들어야 하는것이 핵심이다.

5. 의존 관계 역전 원칙(DIP, Dependency Inversion Principle)

 

  • 추상화에 의존해야지 구체화에 의존하면 안된다.
  • 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안되고 저수준 모듈은 고수준 모듈에서 정의한 추상 타입에 의존해야 한다.( 저수준 모듈이 변경되어도 고수준 모듈은 변경이 필요없는 형태가 이상적이다)

고수준 모듈이란 인터페이스와 같은 객체의 형태나 추상적인 개념이고 저수준 모듈이란 구현된 객체를 의미한다. 즉, 객체는 객체보다 인터페이스에 의존해야한다로 치환할 수 있다. 가급적 객체의 상속은 인터페이스를 통해 이루어져야 한다라는 의미로 해석될 수 있다.


 

 

| 면접 대비 질문 답변 정리 ⭐

 

1. 객체 지향 프로그래밍이란?

객체 지향 프로그래밍은 컴퓨터 프로그래밍 패러다임 중 하나로, 프로그래밍에서 필요한 데이터를 추상화시켜 상태와 행위를 가진 객체를 만들고 그 객체들 간의 유기적인 상호작용을 통해 로직을 구성하는 프로그래밍 방법입니다.

2. 객체 지향 프로그래밍의 특징 4가지를 설명해보세요

객체 지향 프로그래밍은 추상화, 캡슐화, 상속, 다형성 이 네가지의 특징을 가집니다. 첫번째로 추상화란 객체의 공통적인 기능과 속성을 뽑아내는 것입니다. 이를 통해 유연하게 개발할 수 있습니다. 두번째로는 캡슐화란 정보를 은닉하여 응집도를 높이고 결합도를 낮추는 것입니다. 보통 접근 제어자를 활용하여 은닉할 수 있습니다. 세번째로 상속이란 말그대로 부모클래스의 속성을 자식 클래스가 물려받는 것을 의미합니다. IS-A 관계가 성립하고 기능을 확장할때 사용하게 되고 이를 사용하게 되면 코드 재사용성이 증가합니다. 마지막으로 다형성이란 서로 다른 클래스의 객체가 같은 동작 수행 명령을 받았을 때, 각자의 특성에 맞는 방식으로 동작하는 것입니다. 오버라이딩이나 오버로딩을 생각하면 쉽게 이해하실 수 있습니다.

3. 객체 지향 프로그래밍의 장단점을 설명해보세요

일단 장점으로 세가지가 있는데요 첫번째로는 클래스 단위로 모듈화 시켜서 개발하기 때문에 업무 분담이 편하고 대규모 소프트웨어 개발에 적합합니다. 두번째론 클래스 단위로 수정이 가능하기 때문에 유지 보수가 편리합니다. 세번째로는 클래스를 재사용하거나 상속을 통해 확장함으로써 코드 재사용이 용이합니다.
단점으로는, 캡슐화와 격리구조 때문에 절차지향 프로그래밍과 비교하면 상대적으로 느립니다. 또한, 모든 것을 객체로 생각하기 때문에 추가적인 메모리와 연산에 대한 비용이 들어가게 됩니다.

4. 객체 지향 5대원칙에 대해 설명해보세요

SRP(단일책임원칙)은 한 클래스의 하나의 책임만 가져야 합니다.
OCP(개방-폐쇄 원칙)은 확장에는 열려 있으나 변경에는 닫혀 있어야 합니다.
LSP(리스코프 치환 원칙)은 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야하는 원칙으로 상위 타입을 상속해서 재정의 했을 때 프로그램이 깨지지 않아야 합니다.
ISP(인터페이스 분리 원칙)은 클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안되는 원칙입니다. 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 더 낫습니다. 즉, 비대한 인터페이스보단 더 작고 구체적인 인터페이스로 분리해야합니다.
DIP(의존관계 역전 원칙)은 추상적인 것은 자신보다 구체적인 것에 의존하지 않고, 변화하기 쉬운 것에 의존해서는 안된다는 원칙입니다. 구체적으론 구현 클래스에 의존하지 말고, 인터페이스에 의존해야 하는 원칙입니다.




출처:
https://blog.itcode.dev/posts/2021/08/17/dependency-inversion-principle
https://github.com/ksundong/backend-interview-question
https://debugdaldal.tistory.com/152
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=wisebrain84&logNo=100103435083
https://velog.io/@raed123456/13%EC%9E%A5.-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-1
https://velog.io/@haero_kim/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0
https://m.blog.naver.com/atalanta16/220249264429