객체지향 특징 - 캡슐화, 정보 은닉

2024. 1. 11. 14:41Tip

728x90

객체지향의 특징
LM2001020230_프로그래밍+언어+응용.pdf
10.06MB

 

인터넷에서는 4가지만 나와있는데, NCS에서는 5가지를 정의하고 있다.

보통 '정보 은닉'은 캡슐화의 부차적인 기능 정도로 생각했는데, 이를 직접적으로 명시한 의도는 무엇일까?

2번 항목을 보고 아래의 내용이 떠올라서 책을 다시 폈다.

로버트 마틴의 '클린 코드'에서는 이런 내용이 있다.

변수를 private로 정의하는 이유가 있다.
남들이 변수에 의존하지 않게 만들고 싶어서다.
충동이든 변덕이든, 변수 타입이나 구현을 맘대로 바꾸고 싶어서다.
그렇다면 어째서 수많은 프로그래머가 getter/setter로 비공개 변수를 외부에 노출할까?

 

만약 어떤 클래스가 생성 시에 참조하는 모든 클래스의 인스턴스를 매개변수로 넣어주어야 한다면,

또 공개된 메서드에는 매개변수도 많고, 오버로딩에 따른 차이가 극명하다면...

과연 해당 클래스를 사용하고 싶어질까?

심지어 개발 시, 유닛 테스트 작성에도 자유도가 지나치게 높아 불편할 것이다.

이 관점에서 1번 항목은 단순화와 연관이 있다고 생각한다.

위와 같은 문제를 해결하기 위해 IoC(Inversion of Control)를 적용하고,

함수가 하나의 일만 담당할 수 있도록 매개변수는 줄이고, 함수의 수를 늘리면 된다.

 

A라는 클래스는 getter와 setter를 제공하여 필드에 대한 접근/수정 기능을 제공한다.

B와 C 클래스는 A 인스턴스에 대한 getter를 참조하여 자신의 메서드를 구현한다.

B와 C 모두 동일한 A의 인스턴스를 참조한다고 했을 때,
만약 D라는 클래스가 해당 인스턴스의 setter를 호출하여 부적절한 값으로 변경하면 어떻게 될까?

또 A에서 해당 필드의 타입을 변경하거나, 필드를 삭제하고 싶다면 어떻게 될까?

이 관점에서 2번 항목은 의존성과 연관이 있다고 생각한다.

위와 같은 문제를 해결하기 위해 DIP(Dependency Inversion Principle)와 DI(Dependency Injection)를 적용하여,
상호 의존성을 분리하고, 클래스 간 결합도를 느슨하게 만들어야 한다.

 

나는 이러한 내용을 '의존성 꼬리 자르기'라 부른다.

팀 프로젝트를 진행할 때, 나의 코드에 의존하는 다른 코드에 영향 없이 나의 코드를 수정할 수 있도록 만드는 것이다.


의존성 꼬리 자르기

클래스 A와 B가 있다.

A가 가진 어떠한 값을 참조하여, B의 기능을 구현하려고 한다.

A를 수정하면, B도 수정해야 하는 경우

public class A {
    public Long value;
}

public class B {
    private final Long a;

    public B(Long a) {
        this.a = a;
    }

    public Long fooB() {
        return a;
    }
}
// main
A a = new A();
a.value = 1L;
new B(a.value).fooB();
  1. A의 클래스명을 바꿨을 때
  2. 필드 value의 접근 제어자를 바꿨을 때
  3. 필드 value의 타입을 바꿨을 때
  4. 필드 value의 필드명을 바꿨을 때

물론 1, 4 변수명과 클래스명 수정은 IDE의 도움을 받아 손쉽게 처리할 수 있다.

그러나 2, 3은 그렇지 않다.

개선
1. A를 통한 결과는 인터페이스 Foo에만 의존
2. 도메인 독립적인 형 변환 기능은 자료구조에서 제공
3. 도메인 의존적인 연산은 fooB()에서 구현

public class FooData {
    private Long value;

    public FooData(Long value) {
        this.value = value;
    }

    public Long toLong() {
        return value;
    }
}

public interface Foo {
    FooData foo();
}

public class A implements Foo {
    public FooData fooData;

    public A(FooData fooData) {
        this.fooData = fooData;
    }

    public FooData foo() {
        return fooData;
    }
}

public class B {
    public Long fooB(Foo a) {
        return dataToLong(a.foo());
    }

    private Long dataToLong(FooData fooData) {
        return fooData.toLong();
    }
}
// main
FooData fooData = new FooData(1L);
A a = new A(fooData);
new B().fooB(a);

 

이렇게 하면 코드의 길이는 길어졌지만

A가 interface Foo를 implements한다는 가정 하에
A에 대해 어떠한 변화가 발생하더라도 B를 수정할 필요는 없어진다. (DIP: Dependency Inversion Principle)

추가 요구사항에 따라 A에 로직을 추가/수정/삭제할 때, B를 수정할 필요가 없다.

public class C implements Foo { ... }

// main
C c = new C(fooData);
new B().fooB(c);

뿐만 아니라 다양한 로직에 대한 분기 처리를 수행할 때에는

인터페이스 Foo의 구현체를 생성하여 전달하면 된다. (DI: Dependency Injection)

 

public class B {
    public Long fooB(Foo a) {
        return dataToLong(a.foo());
    }

    protected Long dataToLong(FooData fooData) {
        return fooData.toLong();
    }
}

public class D extends B {
    public D() {
        super();
    }
    public Long fooB(Long value) {
        FooData fooData = new FooData(value);
        A a = new A(fooData);
        return dataToLong(a.foo());
    }
}

 

만약 이 모든 과정을 하나의 클래스로 다루고 싶다면 (IoC: Inverison of Control)를 적용하면 된다.

B의 dataToLong() 메서드의 접근 제어자를 protected로 변경하고, B를 상속하는 D를 작성한다.

결과는 다음과 같이 단순하게 변한다.

// main
new D().fooB(1L);

물론 목적에 따라 생성자 매개변수로 전달하고, 내부 필드에 Foo 타입의 필드에 인스턴스를 할당하는 것도 방법이다.


Reference

[SOLID] 의존 관계 역전 규칙(DIP), 의존성 주입(DI), 제어의 역전(IoC) · NSKG (yoojin99.github.io)

개인 노션 정리: DIP vs DI vs IoC

728x90