본문 바로가기
IT(Dev) Notes

객체의 필드에 직접 접근하면 안되는 이유

by 미티치 2017. 3. 24.
이전에는 소프트웨어 개발에 있어서 오로지 문제를 해결하는 알고리즘(절차)를 중요하게 생각했다면 ( 절차지향 )
소프트웨어가 다루는 데이터가 중요해지면서 로직과 데이터를 묶어둔 하나의 단위로 클래스를 만들게 되었다. ( 객체지향 )


클래스의 필드에 직접 접근하면 안되는 이유?

1
2
3
4
5
6
7
8
9
10
11
12
13
public class VO {
    
    public String a;
 
    public String getA() {
        return a;
    }
 
    public void setA(String a) {
        this.a = a;
    }
        
}
cs


위와 같은 클래스가 있다. 이 VO에서 필드 a는 setter,getter가 있지만 public으로 선언되어있기 때문에 외부 클래스에서 접근이 가능하다. 예를 들어 다른 클래스에서 VO vo = new VO(); 를 해서 vo.a = "hello"; 라고하면 이 변수 a에는 hello가 저장된다.

내가 라이브러리를 만들어서 배포하는 사람이라고 해보자. 자바 사용자는 내가만든 VO 클래스를 이용해서 vo객체를 사용하고 싶은데, vo객체에 a 라는 값을 vo.a = "hello" 로 설정할 수도 있지만 vo.setA("hello") 로 설정할 수도 있다. 이 두가지가 어떻게보면 로직은 같은 건데 왜 객체의 필드에 직접 접근하면 안되는 걸까


다시 돌아와서 내가 라이브러리를 만들어서 배포한다고 해보자. 나는 이 클래스를 사용해서 객체를 생성하는 사용자들이 이 객체를 이용해서 현재 시간과 내 이름 그리고 그 뒤에 사용자가 입력하고자 하는 string을 출력할 수 있도록 하기 위해 설계했다고 가정해보자. RunClass는 vo 객체가 갖고있는 string a의 값을 사용자가 넘긴 매개변수 param을 이용해서 지정해준다. 그리고 현재 시간과 위처럼 string a를 갖고 있는 VO와, RunClass라는 클래스를 하나 더 만들어서 다음과 같이 구현했다


1
2
3
4
5
6
7
8
9
import java.util.Calendar;
 
public class RunClass {
    
    public void setValueA(VO vo, String param) {    
        Calendar calendar = Calendar.getInstance();
        vo.setA("mititch_ TIME : " + calendar.getTime() +" / "+ param);
    }
}
cs


이렇게 하면 클래스 RunClass와 VO를 사용해서 사용자는 코드를 작성하다가 현재 시간과 내이름(을 찍고싶은 일은 없겠지만)에, 사용자가 정의한 string을 붙여서 출력할 수 있다.


이 라이브러리를 배포했고, 사용자는 RunClass와 VO클래스를 이용하려고한다. 

( 실제로는 import해서 사용하겠지만 예제니까 같은 패키지의 main함수에서 사용 )


1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {
    public static void main(String[] args) {
        
        VO vo = new VO();
        RunClass runClass = new RunClass();
        
        runClass.setValueA(vo, "aa setting");
        
        System.out.println(vo.getA());
        vo.a = "bb";
        System.out.println(vo.getA());
    
    }
}
cs



결론부터 말하면 결과값은 다음과 같다.

 

Class RunClass와 VO가 라이브러리라고 가정했을 때, 이 클래스를 설계한 나의 의도는 이게 아니었다. 나는 string a라는 값에 사용자가 입력하고자 하는 스트링 값을 "현재날짜+mititch / 파라미터" 이렇게 사용할 수 있는 클래스를 작성한 것이고, 이렇게 사용할 경우에만 VO를 사용하면 되는 것인데, 사용자가 이 객체의 필드 a에 사용자가 직접 접근할 수 있게되버리면 위와같은 참사가 일어나는 것이다.


vo.a = "hello" 와 vo.setA("hello") 가 다른 이유는 로직에 차이점이 있어서가 아니다.

  1. 객체 지향은 부품화의 정점이다. 다시 생각해보면, 메소드는 부품화의 예라고 할 수 있다. 연관되어 있는 로직들을 결합해서 메소드라는 완제품을 만들고, 이 메소드들을 부품으로 해서 하나의 완제품인 독립된 객체를 만드는 것이다. (절차 지향에서 메소드를 생각해보자) 메소드를 사용하면 코드의 양을 극적으로 줄일 수 있고(왜지?) 메소드 별로 기능이 분류되어있기 때문에 필요한 코드를 찾기도 쉽고 문제의 진단도 빨라진다. 하지만!!! 프로그램이 커지면 엄청나게 많은 메소드들이 생겨나게 된다. 메소드와 변수를 관리하는 것은 점점 어려운 일이 되기 시작한다. 급기야 메소드가 없을 때와 같은 상황에 봉착하게 된다. 메소드는 프로그래밍의 역사에서 중요한 도약이었지만 이 도약이 성숙하면서 새로운 도약지점이 보이기 시작한 것이다. 

  2. 그 도약 중의 하나가 객체 지향 프로그래밍이다. 이것의 핵심은 연관된 메소드와 그 메소드가 사용하는 변수들을 분류하고 그룹핑하는 것이다. 이렇게 그룹핑한 대상이 객체(Object)이다. 이를 통해 더 큰 단위의 부품을 만들 수 있게 되었다. 

  3. 부품화라고 하는 목표는 단순히 동일한 기능을 하는 메소드와 변수를 그룹핑한다고 달성되는 것은 아니다. 제대로된 부품이라면 그것이 어떻게 만들어졌는지 모르는 사람도 그 부품을 사용하는 방법만 알면 쓸 수 있어야 한다. 이를테면 모니터가 어떻게 동작하는지 몰라도 컴퓨터와 모니터를 연결하는 방법만 알면 화면을 표시할 수 있는 것과 같은 이치다.  즉, 내부의 동작 방법을 안으로 숨기고 사용자에게는 그 부품의 사용방법만을 노출하고 있는 것이다.

  4. 잘 만들어진 부품이라면 부품과 부품을 서로 교환 할 수 있어야 한다. 예를 들어보자. 집에 있는 컴퓨터에 A사의 모니터를 연결하다가 B사의 모니터를 연결 할 수 있다. 또 집에 있던 모니터에 A사의 컴퓨터를 연결해서 사용하다가 새로운 컴퓨터를 구입하면서 B사의 컴퓨터를 연결 할 수 있다. 모니터와 컴퓨터는 서로가 교환관계에 있는 것이다. 이것은 모니터와 컴퓨터를 연결하는 케이블의 규격이 표준화 되어 있기 때문에 가능한 일이다.  컴퓨터와 모니터를 만드는 업체들은 위와 같은 케이블의 규격을 공유한다. 모니터 입장에서는 컴퓨터가, 컴퓨터 입장에서는 모니터가 어떤 식으로 만들어졌는지는 신경쓰지 않는다. 각각의 부품은 미리 정해진 약속에 따라서 신호를 입, 출력하고, 연결점의 모양을 표준에 맞게 만들면 된다. 이러한 연결점을 인터페이스(interface)라고 한다. 만약 HDMI 케이블을 랜선을 연결하는 구멍에 연결하려고 한다면 어떻게 될까? 동작하지 않을 뿐 아니라 연결 자체가 되지 않는다. 인터페이스란 이질적인 것들이 결합하는 것을 막아주는 역할도 하는 것이다. 즉 인터페이스는 부품들 간의 약속이다. 



따라서, 객체의 데이터는 함부로 공개하지 않는다. 객체.변수 = 1000; 이런식으로 객체가 가진 데이터에 마음대로 접근해서 편하게 사용할 수 있지만, 객체지향 프로그램에서 이렇게 프로그램을 작성하면 안된다. 객체의 데이터를 마음대로 접근할 수 있다면 메소드를 통해 만들어진 데이터는 의미가 없게 되는 문제가 생긴다. 이 클래스라는 틀을 이용해서 만든 객체를 사용하는 의미가 없어지는 것이다. 또한 객체에서 데이터는 어떤 메소드의 실행 결과를 누적해서 보존하는 경우가 많기 때문에 함부로 누구나 사용할 수 있게 해주면 안된다.

객체의 데이터는 메소드를 통해 변경해야 한다. 객체지향 프로그래밍의 방식은 필요한 로직이나 기능을 수행할 수 있는 객체에게 내가 원하는 일을 부탁한다(ask)라는 형태로 이루어지기 때문이다. 따라서 객체지향에서 객체가 가진 데이터가 필요하다면 객체에 데이터를 알고 싶다고 부탁하는 방식의 프로그래밍이 가장 적합하다.




* 자바는 고수준(High Level) 언어입니다. 고수준이라는 것은 개발자들이 어떤 이론의 밑바닥까지 알 필요가 없다는 것을 의미하기도 합니다. 예를 들어 Java를 이용해서 네트워크 통신을 한다고 해도 개발자들이 패킷이나 프로토콜과 같이 전문적인 것에 대해 잘  몰라도 개발은 할 수 있다는 것입니다. 나머지는 Java가 제공하는 클래스들이 알아서 하기 때문에, 개발자들이 좀 더 자유롭게 자신이 원하는 기능을 만들 때에 제약이 줄게 됩니다. 이것은 마치 운전자들이 자동차의 원리를 몰라도 운전을 할 수 있는 것과 마찬가지라고 생각하면 됩니다.



 

 

 


 


인스턴스 변수와 getter, setter


 

인스턴스 변수는 특별하지 않은 이상 무조건 private으로 선언합니다. 객체가 인스턴스 변수를 private으로 선언하는 것은 일반적으로 '캡슐화'라는 객체지향 프로그래밍의 원리를 구현하기 우해서라고 알려졌습니다. 우리말로는 정보은닉이라고 할 수 있습니다만 이것이 어떻게 우리에게 도움을 주는지에 대해서는 아직 논란의 여지가 있습니다.

 

객체는 레퍼런스를 이용해서 접근하게 됩니다. 즉 객체의 레퍼런스를 여러 곳이 가지고 있게 되면 여러 곳에서 객체를 조정할 수 있게 됩니다. 이렇게 되면 예상하지 못한 문제가 발생하게 되는데 가장 문제가 되는 것은 정보의 변경입니다. 객체라는 것은 데이터와 기능으로 구성되는데 이것은 객체의 리모컨을 여러 곳에서 사용하면서 발생하는 문제점이라고 할 수 있습니다. 따라서 객체의 데이터를 누구나 손댈 수 없게 할 필요가 있다는 생각을하기 시작합니다. 외부에서 아예 볼 수 조차 없게하자는 방식이 바로 접근제한자 private입니다.

 

이제 사람들은 데이터를 private으로 보호하기 시작했지만, 문제는 주로 데이터성 객체(Value Object, Transfer Object, Data Transfer Object )의 경우에는 데이터를 은닉시켜 버리면 사용하기 어려워진다는 겁니다. 데이터의 초기화의 경우에는 생성자를 이용할 수 있겠지만, 객체가 가진 데이터를 조회하고, 사용하기에는 너무 어려워지게 됩니다. 인스턴스 변수가 소스에서 뻔히 눈에 보여도 호출할 방법이 없고, 접근이 안되면 무척 불편합니다. 따라서 아예 조회만 할 수 있게 하는 메소드와 데이터를 세팅할 수 있는 메소드를 만들어두고 사용하자는 임시방편을 생각해냅니다. (getter, setter)

 

 

- getter는 데이터 복사본을 던져주기 때문에 원래 객체의 데이터를 손상시키지 않습니다.

따라서 원본 데이터는 안전합니다. 간단하게 생성자를 통해서만 데이터를 넣어줄 수 있고 getter 메소드만 있다면( 물론 인스턴스 변수가 기본 자료형이나 String인 경우) 외부에서는 객체의 원본 데이터를 변경할 수 없게 됩니다. 이런 경우를 불변(Immutable)이라고 합니다. 마치 기본자료형의 객체화인 Integer 객체가 Immutable 속성인 것과 마찬가지입니다.

 

- setter  메소드를 이용하면 파라미터를 검증할 수 있다.

setter 메소드의 핵심은 인스턴스 변수에 직접 접근하는 것과 무슨 차이가 있느냐는 것입니다. 위에서도 설명했지만 부가적으로 책의 내용을 덧붙이자면, 우선 객체가 가진 인스턴스 변수에 직접 접근하게 되면 어떤 문제가 있는지부터 생각해 볼 필요가 있습니다. 인스턴스 변수에 직접 접근하게 되면 실제로 비즈니스 로직에서 허락되지 않는 데이터가 적용될 수 있습니다. 따라서 이런 작업을 허용하게 되면 객체의 데이터를 적절히 보장해 줄 수 없습니다.

 


 

결론적으로 setter, getter는 가능하면 피하는 것이 좋습니다.

사실 setter, getter는 C언어의 구조체나 C++의 흔적으로 인식되는 것이 일반적인 견해입니다. 이클립스에서 제공하는 setter를 만들게 되면 어쩔 수 없이 객체는 데이터의 수정에 대해서 무방비 상태가 되는 것을 보실 수 있습니다. 이것은 객체지향의 객체의 데이터를 보호하자는 원칙에서 위반되는 것입니다. (Allen Houb의 "Why getter and setter methods are evil", 마틴 파울러의 "ThoughtWorks Anthology" 참고)

 

getter 메소드를 생각해봅시다. getter 메소드의 경우에는 기본 자료형이나 String과 같은 불변한 객체의 getter 메소드일 경우에는 문제가 없습니다만, 객체의 리모컨자체를 리턴해주게 되면 문제가 심각합니다. 예를 들어 회원 정보라는 객체가 회원 주소라는 객체를 가지는 경우를 생각해 봅시다 . Address getAddress( ) 와 같은 getter 메소드를 만들게 될 것이고, 이렇게 되면 getAddress( ) 메소드를 호출했을 때 회원 정보 객체가 가진 Address 객체의 리모컨과 같은 리모컨이 복사되게 됩니다. 이럴 때 누군가 Address 객체의 리모컨으로 정보를 수정해버리면 회원 정보 객체가 가진 Address 객체의 정보도 같이 변경되는 문제점이 발생하게 됩니다.

 

요즘 객체지향 프로그래밍과 관련된 이론 책들을 보면 가능하면 setter, getter를 사용하지 않는 구조를 만들어 보자는 움직임이 활발한 경우가 많습니다. 메소드의 공개를 신중하게 결정해야 한다는 겁니다.

 

출처 「열혈강의 객체중심 Java」 중 "인스턴스 변수와 getter, setter"










'IT(Dev) Notes' 카테고리의 다른 글

Apache Kafka  (0) 2022.09.14
[요약] 데이터 3법 개정안  (0) 2021.08.20
C++ 개발자를 위한 Visual Studio 설치하는 방법  (0) 2020.08.08
HASH 함수  (0) 2017.03.16
예외처리  (0) 2016.10.13