객체지향 프로그래밍(Object-Oriented Programming)이란?

Alex Moon·2022년 7월 26일
0

Java

목록 보기
2/4

🤔 객체(Object)란?

프로그램에서 필요한 데이터들을 추상화시켜 상태와 행위를 가진 객체로 나누고 이 객체들 간의 상호작용을 통해 프로그래밍하는 방법이다.

객체는 코드 상에서 속성(property)과 행동(method)의 집합인 class로 표현된다. 속성은 객체의 데이터, 특성, 상태 등을 가진다. 행동은 이런 속성들과 외부에서 넘겨주는 매개변수를 활용해 객체가 동작할 행위를 정의한다. 그리고 각 객체들은 메세지(*)을 통해 서로 소통한다.

이 객체들을 조합하여 프로그래밍하는 것을 객체지향 프로그래밍이라고 한다. 이 방식은 현실 세계를 반영하고 있기 때문에 유연하고 변경이 쉽다. 또한 현대에 가장 많이 쓰이는 프로그래밍 패러다임이다.

  • 메세지 : 객체에게 필요한 행동을 명령하는 것



📖 객체지향 이전의 코딩 패러다임(역사)

초기에는 절차지향 프로그래밍(procedural programming)을 했다.
이 방식은 간단한 알고리즘을 작성할 때에는 큰 문제가 없었다. 하지만 소프트웨어 시장이 빠르게 발전하면서 크고 복잡한 알고리즘을 작성할 일이 많아지면서 이슈가 발생했다. 순서도로 표현하기 어려울만큼 논리가 지나치게 복잡해지니 코드 작성하기가 어려워졌고, 결국 코드의 논리가 꼬이기 시작하면서 작성자조차 이해하기 힘든 "스파게티 코드"를 만들게 됐다.

이 문제를 해결하기 위해 1968년 에츠허르 다익스트라가 프로그램을 프로시저(procedure) 단위로 나누고 프로시저끼리 호출을 하는 구조적 프로그래밍(structured programming)을 제안했다.
이 방식을 통해 코드의 중복을 줄이고 재사용성을 높일 수 있었다. 하지만 데이터의 처리 방식을 구조화했을 뿐, 데이터 자체는 구조화하지 못했다. 즉 논리적 단위(함수)만 해결됐고, 물리적인 단위(데이터)는 해결하지 못한 것이다. 그러다보니 데이터의 관리를 위해 많은 제약조건들이 추가적으로 필요했고, 이런 제약조건들을 관리하기 위한 네임스페이스가 포화되는 등의 문제가 발생했다.

그래서 이런 문제들을 근본적으로 해결하기 위해 나온 방식이 객체지향 프로그래밍(Object-Oriented Programming)이다.




😎 객체지향은 과거 패러다임의 어떤 부분을 보완하기 위해 나왔나?

객체지향 프로그래밍은 큰 문제를 작게 쪼개고, 작은 문제들을 신뢰할 수 있는 객체로 만든 뒤, 이 객체들을 조합해서 큰 문제를 해결하는 상향식(Bottom-up) 프로그래밍 방식이다.

논리(함수)와 데이터를 각각 필요에 맞는 객체에서 관리하고, 이 객체들을 모두 테스트되어 신뢰할 수 있는 상태가 된다. 즉, 코드를 유연하고 확장성 있게 가져갈 수 있고, 유지보수 및 안정성 또한 좋아진다.

하지만 이 방식이 만능은 아니다. 현실 세계의 모든 것을 객체만으로 표현할 수 없기 때문이다. 상황에 따라 이렇게 행동하자고 정해진 사회적 약속이나 패턴들이 존재하기 때문이다. 그러므로 OOP로 개발을 진행해도 상황에 따라 절차지향과 객체지향을 적절하게 잘 섞어서 사용할 줄 알아야 한다.




🧐 절차지향과 객체지향 프로그래밍의 장단점

🏭 절차지향 프로그래밍

장점

  1. 컴퓨터 처리구조과 유사하여 실행속도가 빠르다.
  2. 프로그램의 흐름을 파악하기 편하다.

단점

  1. 코드 의존성이 높아서 코드를 유지보수하기 힘들다.
  2. 코드의 순서가 엄격하게 정해져 있어서 생산성이 떨어진다.

🌏 객체지향 프로그래밍

장점

  1. 객체만 가져다 쓰면 되므로 코드 재사용성이 높다.
  2. 필요한 객체만 확인하면 되므로 유지보수 및 관리하기 편하다.
  3. 현실 세계를 반영하기 때문에 사람이 이해하기 편하다.

단점

  1. 절차지향 방식에 비해서 느리고 무겁다.
  2. 어디까지 객체로 쪼개야하는지, 어떤 책임을 부여할 것인지 등 설계에 필요한 시간이 늘어난다.
  3. 개발 난이도가 높아진다.



✔ 객체지향의 요소 정리(특징)

💭 추상화(Abstraction)

추상화 대상들의 공통적인 요소들을 추출하여 필요로 하는 속성이나 행동을 추출하여 일반화하는 것이다.

내가 이해한 추상화란 가이드라인을 만드는 것과 같다고 생각한다.
만약 디자인 가이드라인을 만든다면

  1. 디자인의 특징이 잘 드러나는 포인트들을 추려낸다.
  2. 추려낸 포인트들의 조건을 정리한다.
  3. 각 포인트들을 어디에, 어떤 상황에 사용해야하는지 설명한다.

이렇게 만들어진 가이드라인을 통해 만들어진(구체화된) 디자인들은 공통된 포인트를 가지고 있지만 각자 다 다른 디자인들이 나올 것이다.
예를 들면 애플용 앱을 개발할 때, 앱 스토어 등록하기 위한 가이드라인을 따라야 한다. 이렇게 개발된 앱들은 누가봐도 애플 앱이지만 다 다른 앱들이 된다.

추상화도 비슷하게 생각할 수 있다.

  1. 추상화 대상들에서 공통 집합을 찾는다.
  2. 공통 집합에서 가지는 공통된 속성들을 추출한다.
  3. 공통 집합에서 가지는 공통된 행동들을 추출한다.

위의 과정을 거쳐 추상화가 진행되면 다음과 같이 된다.

  • 대상 : A 회사의 직원들
  • 추상화
    집합 : 직원
    속성 : 이름, 팀, 업무, 직급...
    행동 : 밥먹는다, 일한다...

이렇게 추상화가 된 정보를 통해 실제 필요한 클래스를 구체화하면 A회사 직원들의 공통 특징을 가지면서도 각 개인의 정보를 가진 객체를 만들 수 있게 된다. 또한 필요에 따라 클래스를 수정할 수도 있다.

정리하자면 추상화란 공통점을 가지는 집합에서 공통된 속성과 행동을 추출하고 일반화하여, 이와 관련된 클래스들을 구체화할 때 가이드라인 역할을 할 수 있게 하는 것이다.

💊 캡슐화(Encapsulation)

서로 연관이 있는 속성과 행동을 묶고, 외부 결합도를 낮추는 것이다.

우리가 일반적으로 커피를 주문할 때, 어떤 원두를 쓰고 원두를 어떻게 처리해서 어떤 과정들을 통해 커피가 만들어지는지 굳이 알 필요가 없다. 그저 원하는 커피를 주문하기만 하면 된다. 이처럼 클래스를 사용하는 사람도 내부 구현에 대해 굳이 알 필요 없이 필요한 행동만 요청하여 원하는 결과를 얻게할 수 있도록 하는 것이 캡슐화다.

이렇게 캡슐화를 하기 위해서 2가지 조건을 충족해야 한다.
첫번째는 클래스에 서로 연관이 있는 속성과 행동을 묶어두는 것이다. 그러면 코드가 구조화되고 메서드가 직접 클래스 내의 데이터에 접근할 수 있으므로 인수를 취할 필요가 없어진다.
두번째는 "아메리카노를 주문한다, 카페모카를 주문한다"와 같은 행동 외의 정보는 다 감춰야 한다. 즉, 필요한 정보 외의 모든 정보는 은닉되어야 한다는 것이다. 접근 제어자(private, protected, public)을 활용해 사용자에게 보여주고 싶은 것만 볼 수 있도록 할 수 있으며, 결과적으로 외부 결합도를 낮출 수 있게 된다. 클래스 내부 내용이 수정되어도 사용자는 추가 작업을 할 필요가 없다.
이러한 특성은 TDD를 진행할 때 매우 중요하게 작용한다.

아래 코드는 위에서 말한 2가지 조건에 맞게 작성된 예제다.

public class Cafe {
    private String chocolate;
    private String milk;
    private String water;
    public String beans;
    
    public String makeAmericano() {
        return beans + water; 
    }
    
    public String makeCafeMocha() {
        return beans + chocolate + milk;
    }
    
    private String beanRoasting() {
        return "roasting" + beans;
    }
}

public void main(String[] args) {
    Cafe cafe = new Cafe();
    String americano = cafe.makeAmericano();
    String cafeMocha = cafe.makeCafeMocha();
}

👪 상속(Inheritance)

기존 클래스에 새로운 기능을 추가하거나 재정의하여 새로운 클래스를 만드는 것이다.

프로그래밍에서의 상속은 우리가 일반적으로 알고 있는 상속과 비슷한 개념이다. 부모의 재산을 상속받을 경우에 하지만 부모가 숨겨둔 재산 외의 재산을 자식이 마음대로 할 수 있다. 이와 동일하게 기존 클래스를 상속받은 클래스는 기존 클래스의 속성과 행동을 사용하고 재정의할 수 있다. 하지만 private으로 지정된 것들은 직접 접근할 수 없다.

이처럼 상속을 활용하게 되면 부모 클래스를 재활용하여 코드의 중복을 제거할 수 있다. 또한 클래스의 종속 관계를 형성하게 되어 객체를 조직화할 수 있게 된다.

아래의 코드는 상속의 예제다.

public class Person {
    private String firstName;
    private String lastName;

    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String hello() {
        return "Hello! I'm " + firstName + lastName + ".";
    }
}

public class SomeMan extends Person {
    private int age;

    public SomeMan(String firstName, String lastName, int age) {
        super(firstName, lastName);
        this.age = age;
    }

    @Override
    public String hello() {
        return super.hello() + " I'm " + age + " years old.";
    }
}

위에서 말한 것처럼 부모 클래스의 내용들을 상속받았지만 private된 firstName, lastName에는 직접 접근할 수 없다. 그래서 public으로 된 hello() 메서드를 재정의할 때 부모의 hello 메서드를 통해 간접적으로 데이터를 받아와서, 새로 추가된 age와 합하여 hello 메서드를 재정의했다.
만약 자식 클래스에서 부모 클래스의 private 속성에 접근하고 싶다면 getter/setter 메서드를 추가해주면 된다. 혹은 상황에 따라 부모의 클래스의 private 속성을 protected로 변경해주는 방법을 사용할 수도 있다.

👥 다형성(Polymorphism)

하나의 클래스로 여러개의 타입을 가지거나 새로운 클래스를 생성할 수 있다.

다형성은 Poly(많다)와 Morphism(형태)의 합성어로 이름처럼 하나가 다양한 형태를 가질 수 있다는 뜻이다. 아래의 예제 코드처럼 부모 클래스를 상속받은 자식 클래스들이 여러 형태로 만들어질 수 있고, 부모 클래스는 자식 클래스 타입을 모두 가질 수 있다. 또한 부모 클래스가 추상 클래스가 아닐 경우, 명시적 타입 변환을 통해 자식 클래스가 부모 클래스를 받을 수도 있다.

public class Person {
    private String firstName;
    private String lastName;

    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String hello() {
        return "Hello! I'm " + firstName + lastName;
    }
}

public class Korean extends Person {
    public Korean(String firstName, String lastName) {
        super(firstName, lastName);
    }

    @Override
    public String hello() {
        return super.hello() + "\n나는 한국인이야.";
    }
}

public class American extends Person {
    public American(String firstName, String lastName) {
        super(firstName, lastName);
    }

    @Override
    public String hello() {
        return super.hello() + "\nI'm American.";
    }
}

public void main(String[] args) {
    Person korean = new Korean("김", "신");
    Person american = new American("kim", "sha");
    Korean person1 = (Korean) new Person("으", "아");
    American person2 = (American) new American("oh", "ah");
}

내가 이해한 다형성은 추상화, 캡슐화, 상속의 종합적인 특성이다. 왜냐하면 추상화된 클래스를 구체화하고 캡슐화한 클래스가 부모 클래스가 된다. 그리고 이 것을 상속하여 만들어진 자식 클래스들은 다 다른 클래스이면서도 부모 클래스에 종속되기 있기 때문이다.

profile
느리더라도 하나씩 천천히. 하지만 꾸준히

0개의 댓글