logo

서론

book.오브젝트의 캡슐화에 대해서 정리해보려고합니다.

<br/>

before. 캡슐화

대학교때 배운 캡슐화를 떠올려봅니다.

public class Human{
    private Integer key;
    
    public void setKey(Integer key){
        this.key = key;
    }
    
    public Integer getKey(){
        return key;
    }
}

Human이라는 클래스에, key라는 데이터를 숨깁니다.
오직 행위(메소드)만을 이용하여
setter()를 통해 값을 바꿀 수 있으며
getter()를 통해 값을 가져올 수 있습니다.

행위를 통하여 접근하게하여, 데이터를 은닉화합니다.

이때 배운 내용도 사실 의문이 들었습니다.
setter()를 통해 값을 바꿀 수 있다면, 무엇이 은닉되었다는것이지..?

after. 캡슐화

"캡슐화는 객체의 행동과 상태를 하나로 묶는 것이다" by object

행동과 상태를 하나로 묶어 클래스를 정의했습니다.
그럼 올바른 캡슐화를 한것일까라는 의문이 듭니다.
행동과 상태를 하나로만 묶어준다면 끝난것일까?
왜 이 묶음을 강조하는것일까?

<br/>

"상태와 행동을 하나로 모으는 이유는 객체의 내부 구현을 감추기 위합입니다" by object

객체의 내부구현을 숨긴다는 의미는 무엇일지 생각해봅니다.

내부구현은 변경가능성이 높은 곳을 말합니다.
좀 더 구체적으로 명확하게 내부 구현을 구분하면 다음과 같습니다.

public class MyClass{
    public String getSomeData();  //구현이 아닙니다!
}

public class MyClass{
    private String somedata = "data"; // 구현
    
    public String getSomeData(){
        return upper(somedata); //구현
    }
    
    // 구현
    private String upper(){
        return somedata.upperCase();
    }
}
  • public 메소드 선언 : 메소드를 선언하여 외부에 인터페이스를 제공하는것은 구현이 아닙니다.
  • 메소드 로직 구현 : 메소드 내부에 소스코드를 작성하는것은 구현입니다.
  • 멤버 변수 선언 : 객체내부 데이터로 구현입니다

다시 정리하면 캡슐화는 이렇습니다.
관련된 행동과 상태를 하나로 묶습니다.
그리고 내부 구현을 숨기며, 외부에 공개된 인터페이스로만 접근하게 합니다.
변경가능성이 높은 객체의 내부 구현을 내부로 숨기는 것입니다.
그렇다면 우리의 객체는 서로 독립적 관계를 유지 할 수 있습니다.

<br/>

다음 예시가 있습니다.

사람의 키는 넉넉히 3미터(300cm)를 넘어 자랄 수 없다는 요구사항이 있습니다.
소스코드의 여러곳에서는 Human을 참조하여 setter를 통해 사람의 키를 바꾸어주고있습니다.
다음과 같이 여러곳에서 키값을 검증 후 정의 하고있습니다.

class Aclass{
    public void 키가큰다(Human human, Integer key){
        if(human.getKey() + key> 300){
            throw new RuntimeException();
        }

        human.setKey(human.getKey() + key);
    }
}

class BClass{
    public void 키가큰다(Human human, integer key){
        if(human.getKey()  + key > 300){
            throw new RuntimeException();
        }

        human.setKey(human.getKey() + key);
    }
}

위 코드의 문제점은 무엇일지 생각해봅니다.

요구사항이 바뀌어 사람은 300이 아니라 200까지만 키가 클수 있다고 요구사항이 발생합니다.
그렇다면, Human 클래스를 참조하여 키값을 변경하는 모든코드를 바꾸어야 한다는 것입니다

Human의 변동이, 다른 클래스의 변동을 발생시키고있습니다.
Human의 내부 구현이 외부에 노출된 것입니다.

키는 100보다는 커야한다는 요구사항이 생긴다면,
키는 한 번만 커질수 있다는 요구사항이 생긴다면,
위 코드는 어떻게 바뀔지를 고민합니다.

궁극적으로 위 코드는 적절한 캡슐화일까를 생각하게 합니다.
행동과 상태를 묶었지만 객체는 독립적이지 않습니다.
강한 결합도를 낳았으며, 하나의 요구사항의 변경이 많은 코드의 변화가 발생합니다.

<br/>

그리고 코드를 수정합니다.

public class Human{
   private Integer key;
   
   public void increaseKey(Integer key){
       if(key > 200){
           throw new RuntimeException();
       }
       
       this.key += key;   
   }
}

class AClass{
   public void 키가큰다(Human human, Integer key){
       human.increaseKey(key);
   }
}

class BClass{
   public void 키가큰다(Human human, Integer key){
       human.increaseKey(key);
   }
}

이제는 어떤 사람의 최대키의 변경에도
외부의 Human을 참조하는 클래스는 변경이 없을 것입니다.
300이든, 400이든 그 변동은 이제 외부에서 중요하지 않습니다.
수정된 코드는, 객체의 구현을 숨겼기 때문입니다.

한가지 더 강조하고싶은 것은 setter가 사라졌다는 것 입니다.
무분별한 setter는 객체를 불안정하게 만듭니다.
setter를 제공한다는 것은 '값을 맘대로 바꿀수 있어!' 라고 객체가 말해주는 것입니다.
어느샌가 모르게 클래스 외부에서 우리는 getter를 통해 key를 가져오고
새로운 계산후 setter를 통해 key를 주입할지 모릅니다.

무분별한 setter는 캡슐화를 해칩니다.
setter를 지양하는 코드를 연습하면 캡슐화에 더 가까운 코드를 짤 수 있을지 모릅니다.

<br/>

4가지 캡슐화

위의 예시를 보면
단순히 데이터를 private 접근자로 숨기고, 행위를 통해서만 접근하는것이 캡슐화가 아니란 것을 보여줍니다.
캡슐화는 행동과 상태를 하나로 묶음으로써 객체의 구현, 변경가능한 부분을 숨기는 것입니다.
book.오브젝트에는 4가지 종류의 캡슐화를 정의하고 있습니다.

첫번째. 데이터 캡슐화입니다.

첫번째 데이터캡슐화는 우리가 알고 있는 캡슐화입니다.
private으로 데이터를 숨기고, 행위를 통해서만 데이터를 조작하는것입니다.
setter를 지양하며, 객체의 행위(메소드)만을 이용하여 데이터를 조작하고 보호합니다.

두번째. 메소드 캡슐화입니다.

우리는 객체에 메소드를 정의합니다.
그리고 객체 외부에서 이 메소드를 사용하게끔 하기위해서, public 접근지정자를 사용합니다.
하지만 상속관계에서만 메소드를 사용한다면 protected를 사용하는것 입니다.
그렇다면 상속관계에서 메소드를 호출할 수 있지만,
외부에서는 이 메소드의 존재 자체를 모를 것입니다.
외부에 제공할 필요가 없는 메소드는 최소한의 접근지정자를 사용함으로써 숨기는 것입니다.

세번째. 객체 캡슐화입니다.

객체 캡슐화는 합성관계를 말합니다.
합성관계는 포함관계(has)를 의미합니다.

다음 코드를 보면 알 수 있습니다.

//합성관계
public class Aclass {
    private final Bclass bclass;
    
    public Aclass(){
        bclass = new Bclass();
    }
}

//집약관계
public class Cclass {
    private final Dclass dclass;
    
    public Cclass(Dclass dclass){
        dclass = dclass;
    }
}

public static void main(String[] arg){
    Aclass alcass = new Aclass();
    Cclass clcass = new Cclass(new Dclass());
}

메인 함수를 보면 Aclass 생성과 동시에,
Aclass 내부에, Bclass가 생성됩니다.

하지만 메인 함수 입장에서는 Aclass만 알면 될 뿐입니다.
Bclass의 존재 자체를 모르고 있습니다. 객체내부에 선언된 객체 존재를 숨기는 것입니다.

하지만 Cclass를 정의할때 우리는 Dclass를 생성하여 주입합니다. (이를 집약관계라 정의합니다)
메인함수는 Cclass가 Dclass에 의존하는 사실을 알고 있게 됩니다.

객체에 내부에 참조되는 객체가 라이프사이클이 동일하다면,
합성관계를 이용하여 객체를 더욱더 독립적으로 만들 수 있습니다.

네번째. 서브타입 캡슐화 입니다.

서브타입 캡슐화는 참조하는 객체의 구현체를 숨기는 것입니다.

Worker <|-- Developer
Worker <|-- Designer

interface Worker{
    + startWork()
}
public Aclass{
    private final Worker worker;
    
    public Aclass(Worker worker){
        this.worker = worker;
    }
    
    public void run(){
        worker.startWork();
    }
}

Aclass 입장에서는 worker.startWork()의 메소드 호출만을 하고있습니다.
참조되는 구현체가, developer 인지, desinger 인지 Aclass에게는 중요하지 않습니다.
구현체를 상속/구현관계를 통해서 숨겨버린것입니다.
Aclass에서 참조되는 Worker가 실제로 다른 구현체로 바뀌더라도 Aclass에 코드에는 변동이 없습니다.
객체의 구현체를 숨겼기 때문에, 코드의 변동이 발생하지 않는 것입니다.

<br/>

마무리

학부때 배운 캡슐화에 이어, 아! 하고 깨우치는 부분을 정리하였습니다.
혹여 부족하고 잘못된 부분이있다면 코멘트 해주시면 감사하겠습니다!

CommentCount 0
이전 댓글 보기
알림받기
등록
TOP