Java와 Getter/Setter
객체지향 프로그래밍이라는 개념은 이름만으로는 상당히 추상적이라서 이해하기 영 좋지 않다. 약간 다르게 생각을 해보자. 객체지향이라는 개념이 왜 나왔는가를 통해서 대충 당시의 사람들이 무엇을 상상하고 만들었는지를 통해 이해해보는 것이다.
옛날 이야기
예전에는 절차지향 프로그래밍이라는 방식이 있었다. 프로그램이란, 기계(전자회로)의 동작을 제어하는 것으로, 이렇게, 저렇게, 의 명령 집합으로 생각되던 시절이었다. 당시에는 데이터나 명령이나 크게 구분이 없었다. 그래서 프로그램은 어떤 순서로 어떤 동작을 수행할지에 대한 나열인 방식으로 작성되었다
프로그램의 규모가 커지면서, 저러한 수행의 방식이 비슷한 곳이 많아졌다. 내가 짠 부분에서도, 남이 짠 부분에서도 최대 공약수를 구해야하는데, 매번 구하는 프로그램을 짜는 건 비효율적이다. 따라서 함수가 분리되고 라이브러리가 생겼다
처리해야할 자료의 양이 조금씩 늘어나면서, 프로그램 내에서 자료를 어떻게 관리해야할 지에 대한 고민도 시작되었다. 단순히 함수(코드의 집합)만으로는 데이터를 보관할 무언가를 만들 수가 없었다. 왜냐하면 그건 원래부터 데이터를 조작 하기 위한 것이었으니까.
그래서 자료를 담기 위한 공간인 자료구조라는 개념이 나왔고, 어떤 프로그램을 작성할 때 어떻게 담아야 프로그램의 효율이 좋아질지를 고민하게 되었다.
데이터를 담기 위해 같은 타입의 데이터 집합인 배열과, 다른 타입의 데이터 집합인 구조체가 있다. 이들을 적절히 이용해서 자료를 담을 수 있는 공간을 만들었다. 여러 라이브러리들은, 이러한 자료를 담을 수 있는 공간에 대한 정의와, 그 자료들을 조작할 수 있는 함수들을 라이브러리라는 이름으로 묶어서 만들기 시작했다.
프로그램의 규모가 보다 더 커지고, 처리해야할 데이터의 양이 꽤 증가했다. 이 때문에 프로그래밍을 하면서 데이터를 관리하는 작업이 더욱 많아졌으며, 자료구조는 더욱 빈번히 사용되었다. 그 때마다 자료를 저장하는 공간(구조체 혹은 배열)과 이를 조작하는 함수를 따로 관리하는 것은 번거롭게 느껴졌다. 그래서 이 둘을 하나로 합치면 어떨까, 라고 해서 객체지향이 나왔다. (는 뻥이다.)
절차지향 프로그래밍은 함수 중심이다. 여러 함수를 데이터가 통과해 가면서 데이터가 적절히 조작되고, 그 결과물을 만들어내는 형식이다. 객체지향 프로그래밍은 객체 스스로가 자신의 상태를 관리하고, 그 객체들의 조합을 통해 결과를 얻어내는 형식이다.
상태(데이터)와 조작(함수)를 하나로 묶는다는 개념은, 프로그래밍 관리 측면에서도 해당 세부를 알 필요 없다는 것과(추상화, 은닉) 기능을 확장하여 재사용하기 쉽다는 것(다형성) 등 여러 장점을 만들어냈다.
객체는 스스로 상태(데이터)를 관리한다. 어떻게 관리하냐하면 자신의 상태를 조작할 수 있는 함수(멤버 함수)를 외부로 노출(public) 함으로써 관리한다. 그리고 필요에 의해서, 객체는 자신의 상태를 외부로 공개해야할 수도 있다. 그래서 개념적으로, 이 둘을 상태를 변하게 하고, 접근할 수 있게 해주니까 mutator, accessor라고 부르게 되었다.
객체지향의 개념이 널리 퍼지면서, 많은 개발자들이 객체지향 세계로 뛰어들게 되었다. 하지만 C라는 괴물이 버티고 있는 개발의 세계에서 객체지향의 개념은 꿈도 꾸기 어려웠다.
마침 이 때, 비야네 아저씨가 ‘내 언어가 세계 최고의 언어!’ 시대에 이것저것 개념을 하나로 합쳐 C++ 이라는 더 무시무시한 괴물을 만들어버렸다. 많은 언어적 패러다임을 담은 C++ 이라는 언어는 객체지향도 가능하게 해주었지만, 그 외의 다른 많은 것들도 가능하게 해주었다. 덕분에 C++ 로 객체지향을 공부하는 사람들은 객체지향이 아닌 C++ 을 배우게 되면서 그것이 객체지향인 줄 잘못 오해하게 되는 일이 생기게 되었다.
이에 통탄한 고슬링 아저씨가 순수 객체지향을 지향하는? Java를 만들었다. C++ 보다 훨씬 객체지향적 개념에 근접했던 이 언어는, 덕분에 프로그램 설계를 하는 사람들의 많은 사랑을 받았다. 그러면서 나름 객체지향 하기 좋은 언어란 칭호도 얻었던 것 같다.
객체냐 자료구조냐
은근슬쩍 넘어갔는데, 객체랑 자료구조랑은 좀 다르다. 관심있게 읽어준 사람은, 초반에는 분명 자료구조를 이야기하다가 어느순간 능구렁이 담 넘어가듯 객체 이야기로 넘어갔다는 것을 감지했을 것이다.
자바 프로그래밍을 하면서 가장 많이 실수하는 것이 이 둘을 구분짓지 못하는 것인데 왜 그런지 보자. 객체지향을 자바라는 언어로 공부하면서 가장 많이 배우는 잘못된 것은 아래와 같은 코드이다.
class Person {
private int age;
private String name;
public Person() {}
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
Person이라는 객체는 상태를 갖는다. 나이와 이름이라는 상태를 갖는데 이를 제어할 수 있도록 age와 name에 대한 getter, setter를 만들어서 객체지향에 맞는 좋은 프로그래밍을 한 것일까?
- 객체의 상태가 잘 은닉화 되었나? (어차피 public 으로 그냥 다 노출되었다.)
- get, set 계열 함수를 Person 을 상속받는 자식에서 override 할건가? (이건 + operator를 overloading해서 - 연산을 하는 짓)
- 아니면 Person class 에서 name과 age를 관리하여 모종의 작업을 하는 무언가가 있는가?
(그럴거면 그 작업을 수행하는 함수를 만들어서 노출시키지 뭐하러 set 함수까지 만들었는가)
뭐 get/set 좋다. 취향이니 존중해줄 수는 있지만 아무 생각없이 get/set 코드 찍어내면서 난 정말 멋진 객체지향 프로그래머야! 라고 생각하지는 않았으면 좋겠다.
실제로, 위와 같은 class는 객체라기보다는 자료구조이다. 객체는 상태를 갖고 스스로 그걸 관리해주는, 즉 그걸 조작하는 함수를 갖는 것이다. 자료구조는? 자료를 담는 것이다.
어차피 Person class를 사용해서 사람에 대한 정보를 처리하는 코드를 작성한다면, 그 코드가 Person가 아닌 Person 자료를 관리하는 곳에 들어가기 때문이다.
class PeopleManager {
private List<Person> people = new ArrayList<Person>();
public int calcaulteAverageAge() {
int averageAge = 0;
for (Person p: people) averageAge += p.age;
return averageAge / people.size();
}
}
class Person {
public int age;
public String name;
}
Person 객체들은 List에 의해 관리되고 있고, 그걸 소유하는 PeopleManager에서 접근, 계산하고 있다. 여기서 Person class가 age와 name에 대해 getter/setter 를 가질 필요는 전혀 없다. 오히려 있으면 더럽다. (안그래도 느린데, getter 접근하면 더 느려진다! 물론 javac가 최적화해줄 수 있지만 그걸 바라고 getter를 쓰는건 좀 이상하다.)
단순히 사람에 대한 정보를 담는, 즉 자료구조라면 굳이 getter/setter를 두지 않고 public으로 멤버를 노출시키는게 더 올바른 설계라는 것이다.
왜 getter/setter를 붙이는 습관이 들었을까
Java가 가장 많이 사용되는 시장은 Web이다. J2EE가 크게 성공하면서 EJB가 대세! 인 세상이 있었다. EJB 는 Enterprise Java Bean의 약자인데, 여기서 Bean은 아래와 같은 코드를 말했다.
class PersonBean {
public int age;
public String name;
}
즉, DB와 어떻게 보여줄 지 연산하는 부분과 웹 페이지 만드는 부분에서 데이터를 공유하기 위해 정보를 담는 자료구조인 것이다. 이런 값 객체(ValueObject: VO) 들이 많아지면서 코드 여기저기를 누비게 되었다.
그런데 값이란게 로직과 떨어질 수가 없는 운명이다.
예를 들면, 저 나이에서 만 나이를 구하는 경우를 생각해보자. age에서 1을 빼면 된다. 따라서 만 나이가 사용되는 경우에는 다 - 1 연산을 사용하였다. 사실 이건 중복되는 로직이니까 Person class에서 이걸 반환하는 함수를 하나 넣어주면 되는데, 안타깝게도 저 부분은 거의 auto generation 되는 코드이거나, convention으로 강력하게 박혀있기 때문에 함부로 수정하지를 않았다. 그래서 로직 코드가 중복되고, 코드의 품질이 저하되었다.
크고 아름다운 설계에도 불구하고 EJB가 저런 많은 문제를 야기하자, 이 문제를 수정하기 위해 여러 설계자들은 대안을 모색하였다.
그 결과 자바가 VO를 남발하면서 로직 중복되는 이유는 기존의 높고 높은 객체지향의 뜻을 잃었기 때문이고, 그 이유는 사람들이 기존의 객체 설계법을 EJB라는 멋진 기법에 가려 잊어가고 있기 때문이다, 라는 결론에 도달하게 되었다.
그들은, 기존의 방법이 EJB만큼 멋진 이름이 없기 때문에 잘 사용하지 않는다는 것을 깨닫고, POJO(Plain Old Java Object)라는 멋진 이름을 통해 기존의 클래스 설계 방법을 널리 장려하였다.
그 후 EJB의 기세는 로직 중복 코드와 함께 서서히 망해가기 시작했고, 이에 환멸을 느낀 사람들이 POJO를 적극 권장하면서 public member에 대한 증오심을 표출했다. 덕분에 모든 member는 getter와 setter로 감싸졌고, 이것은 자바 프로그래밍의 큰 미덕이 되어 널리 퍼졌다. (는 뻥)
정리
웃자고 한 소리인데, 아무튼 자바에 특히 getter/setter가 많은 건 사실. 내용이 기니까 세 줄 요약하자면,
- C++은 객체지향이 영 좋지 못하니까 Java 를 통해 객체지향을 공부해보자.
- public으로 member를 노출시키는건 미개한 C 언어나 하는 짓이잖아? 난 고고하게 getter/setter를 쓰겠음
써보니까 은닉 좋네, 나도 이제 객체지향 프로그래머? getter/setter 안 쓰면 원시인ㅋ이러지 맙시다
아무튼 Getter, Setter을 은닉을 위해 쓰겠다는건 말도 안되는 이야기고, 진짜 쓰겠다면 Mutator와 Accessor의 개념으로 접근해서 설계를 하는 마음을 먹고 써야한다.
물론 Java에서는 멤버의 Readonly를 위해 Getter만 존재하는 경우도 있다. 이 경우에는 final keyword를 사용해서 수정이 불가능하게 해줘도 되는데, 이게 객체 멤버를 반환할 경우에는 좀 애매해진다. final은 객체 멤버의 reference을 못 바꾸게 하는거지 반환된 객체를 수정하지 못하게하는건 아니니까.
class Program {
private final List<Code> codes = new ArrayList<Code>();
public List<Code> getCodes() {
return codes;
}
}
// 중략
Program program = new Program();
program.getCodes().clear(); // 수정할 수 있다
이 때문에 @Readonly annotation이 추가되려 하였으나 실패했다.