[Java] 객체지향 프로그래밍이란?

자바 객체지향

reference : 뉴렉처 객체지향 프로그래밍 을 정리한 내용입니다.

1. 객체지향 프로그래밍의 시작 캡슐화

객체화란? 실세계 일들을 객체를 사용해서 모델링 하는 것

프로그래밍을 절차적으로 만들고, 유지보수를 쉽게 하기 위해 절차적 프로그래밍 코드를 함수로 잘라 구조를 만들었다. 하지만 그렇게 구조를 만들다 보니 새로운 문제가 발생했고, 그 문제를 해결하기 위한 방안이 필요했다. 그게 바로 객체지향 프로그래밍이다.

캡슐화가 왜 객체지향의 초석이 되는가?

앞서 말했듯, 프로그램은 흐름을 갖고 있다. 그 흐름을 잘라 만든 게 구조적 프로그램이다. 프로그램은 결국 흐름 뿐 아니라 잘게 잘려진 구성품으로 프로그램이 만들어지는데, 여기서 문제가 생긴다. 잘라서 만든 함수들이 너무 많아졌다. 정리를 해야 한다. 그런데 무슨 기준으로 정리를 해야하지?

수납 공간은 파일(클래스 파일)이다.

  • 기능으로 정리? class Input, class Sort, class draw (X)
  • 데이터에 따라 정리? class Exam, class Student, class Omok (O)

정답은 데이터에 따라 정리를 해야한다. 왜? 데이터에 따라 정리하면, 구조화된 데이터를 사용하는 함수 모듈의 독립성을 침해하는 문제를 해결할 수 있기 때문이다.

함수는 독립적이다. 함수는 외부의 수정에 절대 영향을 받아선 안 된다.

함수는 데이터를 사용한다. 데이터를 구조화 시켜 바꾼 함수는 함수의 독립성이 깨져버린다. 함수 코드는 건드리지도 않았는데, 외부 수정에 의해 오류가 발생하게 된다. 아래 예를 보며 이해하자.

class Ellipse{
    int x,y,w,h;
    int color;
}
void draw(Ellipse ellipse){
    printf("...", ellipse.x);
    ellipse.x + ellipse.y / ...
}

x라는 이름을 바꿨을 때 draw 함수에 오류가 발생하게 된다(ellipse.x 때문). 이 함수에선 지역변수로만 구성 되어 있고 외부 전역변수를 사용하지 않았음에도 불구하고, 구조화를 함으로서 구조화 된 속성 변화에 의해 오류가 발생하게 되었다.

어떻게 해결하지? 이 상태로라면 변수명을 바꿀 수도 없고, 바꿨을 때 어디가 바뀌었는지도 알기가 힘들다.

그 해결안이 데이터를 중심으로 정리하는 것이다. 고쳤을 때 오류가 발생하는 건 막을 수 없다. 대신 해당 변수를 바꿨을 때 오류가 나는 함수들을 한꺼번에 모아두면 수정하기 편하지 않을까? 데이터 구조를 정의하고 있는 위치에, 그 데이터를 사용하는 함수를 전부 불러온다.
에러나는 범위가 한정되어 수정부담이 줄어들게 된다! 이게 바로 캡슐화다.

정리하면, 구조화 된 객체를 사용하는 함수는 객체의 구조 변경에 아주 취약하다. 캡슐화를 하면 데이터 구조에 따른 코드의 수정범위를 캡슐 범위로 한정시킬 수 있다.

캡슐화 : 데이터 구조와 함수를 하나의 영역에 함께 정의하는 것

2. 인스턴스 메소드

캡슐화를 해서 프로그램을 만들며 절차를 작성하다 보니 또다른 문제가 생겼다. 함수를 사용하는데 불편함이 생긴다.

void main(){
    list = new ExamList();
    ExamList.inputList(list);
    ExamList.printList(list);
    ExamList.saveList(list);
}

현재까지 작성한 함수 구조다.
캡슐화를 했기 때문에 main에서 데이터구조list를 이용해 연산하거나 알고리즘을 작성할 수 없다. 하려면 ExamList에 직접 가서 해야 한다. 지금까지 한 작업, 캡슐화의 의미를 깨지게 할 수는 없으니까.

보통 우리가 프로그램 만들 땐 구조화된 녀석의 실체를 가지고new ExamList() 객체화list를 한다. 객체란 데이터의 실체를 뜻하고 그 객체로 함수를 호출하면 된다.

그래서 호출하고 있는데…문제는 객체가 잘 안 보인다. 정확히 말하자면 print라는 행위의 주체가 잘 안 보인다. list가 매개변수로 되어 있어서 print의 주체인 list가 눈에 확 띄지 않게 되었다. 원래라면 list를 이용해서 입력받고 출력하는 거니까 list가 먼저 눈에 띄고, list를 이용한 처리라고 한 눈에 보여야 한다. 하지만 지금은 함수 이름이 범위를 많이 차지하고 함수 위주로 프로그램이 진행되다 보니 주체가 눈에 안 들어오게 되었다.

그렇다면 이렇게 바꾸는 건 어떨까?

void main(){
    list = new ExamList();
    list.inputList(); // list를 이용한 입력()
    list.printList(); // list를 이용한 출력()
    list.saveList(); // list를 이용한 저장()
}

주체를 먼저 거론하게 바꿨다. 방식 1에선 list를 이용한 함수가 list를 사용하기 위해서 파라미터로 전달되는 방식을 썼는데, 방식 2에선 list를 사용해야 하니 넘겨받긴 하지만 파라미터로 전달받는 게 아니라 . 를 통해서 호출하면 저절로 전달되게 된다.

이렇게 객체를 통해 호출되고 객체를 묵시적으로 넘겨받는 함수를 인스턴스 함수라고 한다.

인스턴스 함수

여기까지 하고 다시 봐보자. 메인함수에선 list 속성을 쓸 수 있다. 그런데 list 구조를 이용해서 main에서 알고리즘을 구현할 수 있나? 아니. 무조건 우린 list의 속성을 써야하는 일이 생기면 캡슐, 즉 객체에new ExamList() 부탁하게 되었다.

속성을 이용하거나, 데이터를 이용하거나 그 안에 뭘 이용하려면 다 new ExamList()에게 부탁해야 한다면, 일은 사실상 쟤가 다 하는거다. 따라서 우리가 list를 이용한 입력을 하는 게 아니다. 개념을 다시 재정의 해야 한다.

  • list를 이용한 입력() -> list(야).입력(해)()
  • list를 입력한 출력() -> list(야).출력(해)()
  • list를 입력한 저장() -> list(야).저장(해)()

객체에 역할을 주게 되었다.
처음엔 표현식을 편하게 하고 싶어 구조를 바꿨지만 깊게 생각해보면 내가 할 수 있는게 아무것도 없으니 네건 네가 해, 이렇게 변경될 수 있는 거다. 구조를 이용해서 뭘 하려면 객체, 네가 해야 하는 거야 하고 책임을 전가하게 되었다.

책임 전가를 하니 개념이 더 깔끔해진다. 캡슐화 할 때 캡슐 명명 개념도 더 분명해졌다. inputList() 이렇게 동사부터 쓰는 건 영어로 명령어 구조를 따르기 때문이다.

단순히 list 데이터를 이용해 입력받는 게 아니라, 책임마저도 list에게 물면서 하나의 대상, 도구였던 객체가 주체가 되었다. "네가 해!"

구조화에 캡슐화가 이루어졌다는 건, 단순하게 그 데이터를 사용하는 함수를 모아놨다는 게 아니라 객체에 역할로서의 의미도 부여하고, 그 의미를 부여함으로서 객체가 주체가 되는 것이다.

함수를 방식 1뿐 아니라 방식 2로도 가능하게 해주는 표현식을 지원해 준다면 그게 바로 객체지향언어이다. 객체가 눈에 띄고, 객체에게 일을 시키는 형태로 프로그램 코드를 만드는 게 객체지향이라고 할 수 있겠다.

객체 지향적인 함수 호출 방식으로의 변화(method)

  • 기존 함수의 인스턴스 전달
public static void main(String[] args){
    ExamList list = new ExamList();
    ExamList.inputList(list);
}
  • 새로운 함수의 인스턴스 전달
public static void main(String[] args){
    ExamList list = new ExamList();
    list.inputList();
}

위처럼 ExamList.inputList(list)list.inputList();로 바꾸면 list에게 시키는 명령이 된다. 그런데 list 입장에서 inputList()를 보면 inputList()는 서비스를 해주는 함수가 되고, 서비스 함수기 때문에 그냥 함수라 말하긴 어렵다.

서비스 함수 또는 list가 역할을 하기 위한 하나의 방법이 된다. 즉 method가 된다.

스태틱 메소드 vs 인스턴스 메소드

ExamList.inputList(list)는 함수지만 list.inputList()는 method다. 단, 자바에선 둘 다 method라고 부르는데 전자는 스태틱 메소드(고전적 함수), 후자는 인스턴스 메소드라고 한다.

스태틱 메소드는 모든 값을 parameter로 넘겨받고 일반적 함수와 똑같이 사용하는 함수다.
반면에 인스턴스 메소드는 반드시 객체를 통해 호출되는 함수고 inputList에서 설명하지 않아도 묵시적으로 list 객체를 넘겨받는다.

  • static method
class ExamList{
    public static void inputList(ExamList list){
        list.exams[list.current] = new Exam();
    }
}
  • instance method
class ExamList{
    public void inputList(){
        this.exams[this.current] = new Exam();
    }
}

여기서 ExamList의 inputList() 함수를 방식1과 방식2로 비교하면 방식1의 static 은 큰 의미는 없고 인스턴스 메소드가 아니라 원래 형태의 메소드라고 보면 된다. static은 그냥 함수를 나타내기 위한 식별을 위한 의미가 된다.
인스턴스 메소드를 만들려면 static을 빼고 파라미터를 지워주자.

static일 경우엔 모든 데이터는 파라미터로 전달되기 때문에 함수 안에서 사용할 때 파라미터로 받아온 list를 이용해서 list.examslist.current로 사용 가능하다.

하지만 인스턴스 함수는 다르다. 똑같은 구현 로직을 갖고 있는데 파라미터로 전달받는게 없는 인스턴스 함수는 어떻게 list를 쓸까?

인스턴스 메서드에선 묵시적으로 예약된 이름이 있다. 바로 this이다. this.exams, this.current로 사용이 가능하다.

정리하자면 일반적 static method는 static, 파라미터로 객체를 넘겨받는다.
static을 빼고 만든 인스턴스에선 this라는 키워드로 객체를 넘겨받을 수 있다.

// instance method
void printList() {
	printList(current);
}

// static method
static void printList(ExamList list) {
	printList(list, list.current);
}

this는 무조건 받아오기 때문에 인스턴스 함수에 생략 가능하다. 생략할 수 있으면 생략하자!

캡슐의 은닉화

a라는 데이터를 사용하는 함수(이후 afunc 라고 적겠음) 중 하나가 b라는 데이터를 직접 사용하려고, 파라미터로 b데이터를 넘겨받는다거나 a안에서 지역적으로 new b를 사용해서 쓰려고 한다면 어떻게 되나? 캡슐화가 깨진다.
b와 가능한 기능은 b에 만들어 둬야하고 a에서 함수를 호출해서 쓰는 식으로 고쳐야 한다. 캡슐화는 b를 고쳤을 때 오류나는 것들만 모아둔 것이니까.

그런데 afunc가 b데이터를 그냥 가져와서 썼다고 하자. 어떻게 될까?

c++나 java는 객체지향을 지원하는 언어인데, 사실 캡슐화처럼 모아두는 건 객체지향이 아닌 언어도 가능하다. 객체지향 언어에서 말하는 캡슐화는 플러스 알파 기능이 있다.

객체지향을 지원하지 않는 언어에선 afunc이 b데이터를 쓴다해도 b라는 함수를 묶어놓은 클래스에선 그걸 막을 방법이 없다. 하지만 객체지향에선 막을 수 있는 방법이 있다. 다른 곳, 내가 허락하지 않은 곳에선 내 데이터를 쓸 수 없게 만드는 보호막이 있기 때문이다. 내 데이터를 쓸 수 있을지, 말지 b가 칼자루를 쥐게 되었다.

접근지시자 동일클래스 파생클래스 외부클래스
private O X X
protected O O X
public O X O

그게 바로 접근지시자, privatepublic이다. 클래스는 안쪽의 자기 멤버를 private, public을 통해 공개할지 말지 선택할 수 있다.

private는 동일클래스에 있는 요소끼린 같이 사용할 수 있지만 외부에서 접근이 불가능하다. public은 공유된 것이기 때문에 외부에서 접근이 가능하다.

진정한 캡슐화란 자신의 데이터를 공유하고 싶지 않은 건 공유할 수 없어야만 이루어지고, 그래야 캡슐을 안전하게 깨지지 않게 보관할 수 있다.

일반적으로 데이터구조는 private를 사용한다. private를 쓰면 ExamList가 아닌 곳에서 list.current같이 변수를 가져와 쓸 수 없게 된다. 하지만 모든 함수를 private로 막아버리면 다른 데서 쓸 수 없다. 그래서 서비스를 담당하는 함수는 public으로, 보호해야 하는 것들에 대해선 private를 쓴다.

정리하자면 데이터 구조에 대해선 private를, 서비스 해야 하는 것들은 public을 쓴다고 생각하자. private를 쓰지 않으면 캡슐화는 언제든 깨질 수 있다. private로 캡슐화를 깨지지 않도록, 캡슐화 의미를 더 크게 부각시킬 수 있다. 캡슐이란 자기가 숨겨야 하는 것들을 은닉시킬 수 있는 능력이 있어야 하며 그 은닉할 수 있는 성질을 반영한 게 private이다.

추가적으로 main 함수에선 public을 사용하고, 함수나 변수에 쓰는 접근지시자와 달리 class에도 public이 들어갈 수 있다. 이와 관련한 내용은 패키지에서 이어 설명하고, 지금은 함수나 메소드 앞에 들어가는 것과 변수 속성 클래스에 있는 멤버들에 국한지어 얘기한 것이다.

태그:

카테고리:

업데이트:

댓글남기기