티스토리 뷰

[프로그래밍 방법론] 객체와 자료구조 – Clean Code


이 글은 Clean Code 책의 내용을 정리한 것입니다.


자료 추상화


아래의 코드를 살펴보면, 두 클래스 모두 2차원의 점을 표현하고 있습니다. 하지만 하나의 클래스는 구현을 외부로 노출하고, 다른 하나는 구현을 완전히 숨기고 있습니다.


public class Point {

public double x;

public double y;

}


public interface Point {

double getX();

double getY();

void SetCatesian(double x, double y);

}


Point 인터페이스는 직교 좌표계인지, 극좌표계인지 알 수 있는 방법이 없습니다. 하지만 이 인터페이스는 자료 구조를 명백하게 표현하고 있습니다. 또한 이 인터페이스는 값을 읽을 때는 하나씩, 설정할 때는 두개의 값을 동시에 설정하도록 강제하고 있습니다. 


반면 Point 클래스는 직교좌표계를 사용하고 있습니다. 이는 변수를 private으로 선언하더라도 set, get 구현을 통해서 외부로 노출되고 있습니다.


이렇듯 변수에 private, protected등을 통해서 접근 범위를 제한한다 하더라고, 구현부분이 감춰지는것은 아닙니다. 구현을 감추려면 추상화가 필요합니다. 추상 인터페이스를 제공하여 사용자가 구현을 모른 채 자료의 핵심을 조작할 수 있어야 진정한 의미의 클래스입니다. 


추상 인터페이스는 자료를 세세히 공개하기 보다는 추상적인 개념으로 핵심적인 내용만 제공하는것이 더 효율적입니다.


자료/객체 비대칭

  • 객체: 추상화 뒤로 자료를 숨긴 채 자료를 다루는 함수만 공개한다.
  • 자료구조 : 자료를 그대로 공개하며 별다른 함수는 제공하지 않는다.

위의 두 정의는 본질적으로 상반되고 개념역시 정반대 입니다. 사소한 차이로 보이겠지만, 그 차이가 미치는 영향을 아주 큽니다. 


자료 구조를 사용하는 절차적 코드는 기존 자료 구조를 변경하지 않으면서 새로운 함수를 추가하기는 쉽습니다. 하지만 객체 지향 코드는 기존 함수를 변경하지 않으면서 새 클래스를 추가하기 쉽습니다. 


이를 반대로 생각하면,


절차적 코드는 새로운 자료 구조를 추가하기 어렵습니다. 이를 위해서는 모든 함수를 고쳐야 합니다. 객체 지향 코드는 새로운 함수를 추가하기는 어렵습니다. 그 이유는 모든 클래스를 고쳐야 하기 때문입니다.


즉, 객체 지향에서 어려운 변경은 절차적 코드에서 쉽습니다. 반대의 개념도 참입니다. 


복잡한 시스템을 짜다 보면 새로운 자료타입이 필요한데 이때는 클래스 개념이 편하고, 간단한 시스템에는 절차적 코드가 편리할 때가 있습니다. 


디미터 법칙


디미터 법칙은 잘 알려진 휴리스틱으로 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙입니다. 객체는 자료를 숨기고 함수를 공개해야 합니다. 즉, 객체는 조회 함수로 내부 구조를 공개해서는 안됩니다. 


디미터 법칙은 

"클래스 C의 메소드f는 다음과 같은 객체의 메소드만 호출해야 합니다."

  • 클래스 C
  • f가 생성한 객체
  • f인수로 넘어온 객체
  • C인스턴스 변수에 저장된 객체


1) 기차 충돌


final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();


위와 같은 코드를 기차 충돌 코드라고 부릅니다. 일반적으로 조잡한 방식으로 여겨지기 때문에 피하는 편이 좋습니다.

위의 코드는 아래와 같이 변경되는 것이 좋습니다.


Options opts = ctx.getOptions();                            

File scratchDir = opts.getScratchDir();                    

final string outputDir = scratchDir.getAbsolutePath();


하지만 위의 코드가 디미터 법칙을 위반하는지 여부를 알기 이해서는 ctxt, opts, scratchDir이 객체인지 자료구조인지에 달려있습니다. 객체라면 내부구조를 숨겨야 하므로 디미터 법칙을 위반하는 것이고, 자료 구조라면 내부 구조를 노출하므로 디미터 법칙이 적용이 되지 않습니다.


위의 코드는 조회 함수를 사용하기 때문에 혼란을 일으키는데, 이를 아래와 같이 사용하게 된다면 디미터 법칙을 거론할 필요가 없어집니다. 


final string outputDir = ctx.opts.scratchDir.absolutePath;


2) 잡종 구조


절반은 객체이고, 절반은 자료 구조이인 상태를 잡종 구조라 말합니다. 위의 1,2번째 코드를 잡종 코드로 볼 수 있겠네요.

잡종 구조는 새로운 함수는 물론이고 새로운 자료 구조도 추가하기 어렵습니다. (유지보수가 힘듭니다.) 그러므로 되도록이면 잡종구조를 피하는 것이 좋습니다. 


3) 구조체 감추기


ctxt, opts, scratchDir이 진짜 객체라면? 객체는 내부 구조를 감춰야 하므로 임시 디렉토리의 절대 경로를 어떻게 얻어야 할까요?

ctxt.getAbsolutePathOfScratchDirectoryOption()과 같은 함수를 사용한다면 ctxt 객체가 가져야 하는 함수가 너무나도 많아집니다.  왜 절대경로가 필요한지를 파악하고, 이에 대해서 다른 객체에 위임하는 방법을 취하도록 해야합니다. 


절대 경로를 Temp 파일을 만들기 위한 목적이라고 한다면 아래와 같이 정의해서 사용할수 있습니다. 


BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);


이는 내부 구조를 들어내지 않으며, 모듈에서 해당 함수는 자신이 몰라야 하는 여러 객체를 탐색할 필요가 없고 디미터 법칙을 위반하지도 않습니다. 


Java의 대부분의 파일 처리 클래스가 대부분 위와 같은 Stream 클래스 형태로 구현이 되어있습니다. 이 부분을 살펴보시면 좀 더 명확한 이해가 가능한것이라 판단됩니다. 


4) 활성 레코드


활성 레코드는 DTO의 특수한 형태로 데이터 베이스 테이블이나 다른 소스에서 자료를 직접 변환해야할 때 만들어지는 결과입니다. 자료 구조를 객체로 취급하여 save나 find와 같은 함수를 제공하고 있습니다. 이는 잡종구조가 나오는 원인이 되기 때문에 바람직하지 않은 코드 구현 방안입니다. 


결론


객체는 동작을 공개하고 자료를 숨깁니다. 그래서 기존 동작을 변경하지 않으면서 새 객체 타입을 추가하기 쉬운 반면, 기존 객체에 새 동작을 추가하기는 어렵습니다. 반대로 자료 구조는 별다른 동작없이 자료를 노출합니다. 그래서 기존 자료구조에서 새 동작을 추가하기는 쉽지만, 기존 함수에 새 자료구조를 추가하기는 어렵습니다. 


새로운 자료 타입 추가에 대한 유연성이 필요하다면, 

객체를 동작에 대한 유연성이 필요하다면 자료 구조와 절차적 언어를 사용하는것이 바람직합니다.


이 글이 도움이 되셨나요?

그렇다면 아래의 그림을 클릭해주세요.



댓글