클린코드 (5) - 객체와 자료구조

gentledot·2021년 6월 7일
0

객체와 자료구조

  • 변수를 private로 정의하는 이유 : 다른 개발자가 변수에 의존하지 않게 만들고 싶어서.
    • 조회(get), 설정(set) 함수를 당연하게 public으로 설정해 비공개 변수를 외부에 노출시키는가?
  • 객체는 동작을 공개하고 자료를 숨긴다.
    • 기존 동작을 변경하지 않으면서 새 객체 타입을 추가하기는 쉽지만
    • 기존 객체에 새 동작을 추가하는 것은 어렵다.
    • 새로운 자료 타입을 추가하는 유연성이 필요하면 객체가 더 적합
  • 자료 구조는 별다른 동작 없이 자료를 노출한다.
    • 기존 자료 구조에 새 동작을 추가하기는 쉽지만
    • 기존 함수에 새 자료 구조를 추가하기는 어렵다
    • 새로운 동작을 추가하는 유연성이 필요하면 자료 구조와 절차적인 코드가 더 적합
  • 모든 것이 객체라는 생각은 미신이다. 편견 없이 직면한 문제에 최적인 해결책을 선택해야 한다.

의무적으로 getter, setter를 생성하던 습관이 있었기 때문에 java class, interface 등으로 객체를 구현할 때 추상적이고 유연하게 생성할 수 있는 방법에 대해 더 고민이 필요하다는 생각이 들었습니다.

자료 추상화

// 추상적인 Point 
public interface Point {
		double getX();
		double getY();
		void setCartesian(double x, double y);
		double getR();
		double getTheta();
		void setPolar(double r, double theta);
}
  • Point interface는 자료 구조 이상을 표현한다.

    • class method가 접근 정책을 강제
    • 좌표를 읽을 때는 각 값을 개별적으로 읽어야 함
    • 좌표 설정 시에는 두 값을 한번에 설정해야 함
  • 변수를 private으로 선언하더라도 각 값마다 getter, setter를 제공한다면 구현을 외부로 노출하는 셈.

  • 구현을 감추려면 추상화가 필요! (getter, setter로 변수를 다룬다고 클래스가 되지 않는다.)

    • 사용자가 구현을 모른 채 자료의 핵심을 조작할 수 있어야 진정한 의미의 클래스!
  • 자료를 세세하게 공개하기 보단 추상적인 개념으로 표현하는 편이 좋다.

    • 개발자는 객체가 포함하는 자료를 표현할 가장 좋은 방법을 심각하게 고민해야 한다.
      // 자동차 연료 상태를 백분율이라는 추상적인 개념으로 알려줌
      // 정보가 어디서 오는지 전혀 드러나지 않음
      public interface Vehicle {
              double getPercentFuelRemaining();
      }

private로 선언한 field는 선언한 class에서만 접근할 수 있고
private + final 로 선언한 field는 한 번 생성된 뒤에는 변경되지 않음이 보장됩니다. (final 선언된 field는 setter 사용이 성립되지 않습니다.)
클래스를 사용할 때 로직에 구성되어 있는 변수, 필드에 대해 고려하지 않고 객체가 수행하는 책임(기능)에 대해서 이해하기 용이하도록 만드는 것이 머리론 이해되도 작업할 때마다 의도적으로 구현하여야 손에 익을 것 같습니다.

자료/객체 비대칭

  • 객체와 자료 구조 사이의 차이

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

    public class Square {
    		public Point topLeft;
    		public double side;
    }
    
    public class Rectangle {
    		public Point topLeft;
    		public double height;
    		public double width;
    }
    
    public class Circle {
    		public Point center;
    		public double radius;
    }
    
    public class Geometry {
    		public final double PI = 3.141592653589793;
    		public double area(Object shape) throws NoSuchShapeException
    		{
    		if (shape instanceof Square) {
    				Square s = (Square)shape;
    				return s.side * s.side;
    		}
    		else if (shape instanceof Rectangle) {
    				Rectangle r = (Rectangle)shape;
    				return r.height * r.width;
    		}
    		else if (shape instanceof Circle) {
    				Circle c = (Circle)shape;
    				return PI * c.radius * c.radius;
    		}
    		throw new NoSuchShapeException();
    		}
    }
    • Geometry class에 둘레의 길이를 구하는 perimeter() 함수를 추가하고 싶다면
      • 도형 클래스들은 아무 영향을 받지 않는다.
      • Geometry 클래스에 속한 함수를 모두 고쳐야 한다.
  • 다형적인 도형

    public class Square implements Shape {
    		private Point topLeft;
    		private double side;
    
    		public double area() {
    				return side * side;
    		}
    }
    
    public class Rectangle implements Shape {
    		private Point topLeft;
    		private double height;
    		private double width;
    
    		public double area() {
    				return height * width;
    		}
    }
    
    public class Circle implements Shape {
    		private Point center;
    		private double radius;
    		public final double PI = 3.141592653589793;
    
    		public double area() {
    				return radius * radius * PI;
    		}
    }
    
    • area() : 다형(polymorphic) method
      • 새 도형을 추가해도 기존 함수에 영향을 미치지 않는다.
      • 새 함수를 추가하고 싶다면 도형 클래스 전부 고쳐야한다.
    • 객체와 자료 구조는 근본적으로 양분된다. (객체 지향에서 어려운 변경은 절차적인 코드에서 쉽고, 절차적인 코드에서 어려운 변경은 객체 지향 코드에서 쉽다.)
  • (자료 구조를 사용하는) 절차적인 코드는 기존 자료 구조를 변경하지 않으면서 새 함수를 추가하기 쉽다.

  • 반면, 객체 지향 코드는 기존 함수를 변경하지 않으면서 새 클래스를 추가하기 쉽다

  • 절차적인 코드는 새로운 자료 구조를 추가하기 어렵다. (모든 함수를 고쳐야 하기 때문에)
    • 자료 구조에 대한 처리를 parameter로 받아올 수 있다면?
  • 객체 지향 코드는 새로운 함수를 추가하기 어렵다 (모든 클래스를 고쳐야 하기 때문)
    • interface의 default를 활용한다면?
  • 반드시 객체로 구현할 수 있는 것은 아니기에 정답은 없는 것 같지만 개인적으로는 좀 더 고치기 편한 형태로 구현하는 것이 낫다고 생각합니다.

디미터 법칙

  • 디미터 법칙은 잘 알려진 휴리스틱(heuristic)으로, 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙이다.
    • 즉, 객체는 조회 함수로 내부 구조를 공개하면 안된다는 의미이다.

기차 충돌 (train wreck)

final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
  • 이어진 기차처럼 보이는 조잡한 방식으로 피하는 편이 좋다.

    • method chaining과는 다른 개념(객체의 method를 수행하고 자신(this)을 리턴).
    • ctxt가 option 객체를 불러오고 option에서 directory를 가져오고 dir 정보에서 path를 가져오는 것으로 각각 주체가 달라진다.
  • 다음과 같이 나누는 편이 좋다.

    Options opts = ctxt.getOptions();
    File scratchDir = opts.getScratchDir();
    final String outputDir = scratchDir.getAbsolutePath();
  • 위의 예제가 디미터 법칙을 위반하는지 여부는 ctxt, Options, ScratchDir이 객체인지 아니면 자료 구조인지에 달렸다.

    • 객체라면 내부 구조를 숨겨야 하므로 확실히 디미터 법칙을 위반한다.
    • 자료 구조라면 당연히 내부 구조를 노출하므로 디미터 법칙이 적용되지 않는다.
  • 자료 구조는 무조건 함수 없이 공개 변수만 포함하고 객체는 비공개 변수와 공개 함수를 포함한다는 이분법적 접근은 불가능

    • 단순한 자료 구조에도 getter, setter를 정의하라 요구하는 framework, JavaBeans 규약 등이 존재한다.

잡종 구조

  • 절반은 객체, 절반은 자료 구조
    • 주요한 기능을 수행하는 함수도 있고, 공개 변수나 공개 조회/설정 함수도 있다.
    • 공개 조회/설정 함수는 비공개 변수를 그대로 노출한다.
      • 다른 함수가 절차적인 프로그래밍의 자료 구조 접근 방식처럼 비공개 변수를 사용하고픈 유혹에 빠지기 십상... (기능 욕심, Feature Envy)
  • 잡종 구조는 새로운 함수는 물론이고 새로운 자료 구조도 추가하기 어렵다. (되도록 생성을 피하자)
    • 프로그래머가 함수나 타입을 보호할지 공개할지 확신하지 못해 (혹은 무지해서) 어중간하게 내놓은 설계에 불과하다.

구조체 감추기

// ctxt 객체에 공개해야 하는 메서드가 너무 많아진다.
ctxt.getAbsolutePathOfScratchDirectoryOption();

// getScratchDirectoryOption()이 객체가 아닌 자료 구조를 반환한다고 가정한다.
ctx.getScratchDirectoryOption().getAbsolutePath();
  • ctxt가 객체라면 뭔가를 하라고 말해야 한다.
    • 속을 드러내라고 말하면 안된다.

field 값을 직접 조회하거나 조작하도로 하면 안된다는 것으로 이해했습니다.

  • 임시 디렉터리의 절대 경로를 얻으려는 이유가 임시 파일을 생성하기 위한 목적이라면

    • ctxt 객체에 임시 파일을 생성하라 시키면 되지 않을까?

      BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);
    • ctxt는 내부 구조를 드러내지 않고, 모듈에서 해당 함수는 자신이 몰라야 하는 여러 객체를 탐색할 필요가 없다.

    • 따라서 디미터 법칙을 위반하지 않는다.

자료 전달 객체

  • 자료 구조체의 전형적인 형태는 DTO(자료 전달 객체, Data Transfer Object)
    • 공개 변수만 있고 함수가 없는 클래스
    • 데이터베이스에 저장된 가공되지 않은 정보를 application code에서 사용할 객체로 변환하는 일련의 단계에서 가장 처음으로 사용하는 구조체
  • 일반적인 형태는 bean 구조
    • bean은 private 변수를 조회/설정 함수로 조작한다. (사이비 캡슐화)
    • getter, setter는 별다른 이익을 제공하지 않는다.
// bean 구조의 예 - address.java
         public class Address {
		private String street;
		private String streetExtra;
		private String city;
		private String state;
		private String zip;

		public Address(String street, String streetExtra,
									String city, String state, String zip) {
				this.street = street;
				this.streetExtra = streetExtra;
				this.city = city;
				this.state = state;
				this.zip = zip;
		}

		public String getStreet() {
				return street;
		}

		public String getStreetExtra() {
				return streetExtra;
		}
		public String getCity() {
				return city;
		}
		public String getState() {
				return state;
		}
		public String getZip() {
				return zip;
		}
      }
  • 특히 setter는 field의 변경이 언제든 가능하게 만들기 때문에 로직 구현에 혼란이 된다고 생각합니다.
  • 다른 개발자분이 작성했던 방식을 차용하여 일단 private final 로 field를 구성하고 변경이나 설정이 필요한 경우에만 해당 field의 final를 제거하고 값 세팅 setter를 설정하는 방식을 익히고 있습니다.
  • field의 값 조회가 필요하지 않다면 getter도 제외해도 괜찮을 것입니다. (필요 값만 조회하는 getter만 생성)
  • class에 대한 확인이 용이하도록 hashCode, equals, toString은 기본적으로 구현되어 있으면 유용하다고 생각합니다.

활성 레코드

  • 활성 레코드는 DTO는 특수한 형태이다.
    • 공개 변수가 있거나 비공개 변수에 조회/설정 함수가 있는 자료 구조지만
    • save(), find() 와 같은 탐색 함수도 제공
    • 활성 레코드는 데이터베이스 테이블이나 다른 소스에서 자료를 직접 변환한 결과다.
  • 불행히도 활성 레코드에 비즈니스 규칙 메서드를 추가해 이런 자료 구조를 객체로 취급하는 개발자가 흔하다. → 잡종 구조
  • 활성 레코드는 자료 구조로 취급한다!
    • 비즈니스 규칙을 담으면서 내부 자료를 숨기는 객체는 따로 생성한다. (내부 자료는 활성 레코드의 인스턴스일 가능성이 높다.)

활성 레코드에 대하여 정확히 이해하지는 못했지만 필요한 데이터 형태를 제공하는 method 외에 값을 변경하거나, 계산하거나, 부차적인 처리를 제공하는 함수는 해당 자료구조를 param으로 받아와 책임이 부여된 객체에서 수행하도록 구분을 명확히 해봐야겠습니다.

profile
그동안 마신 커피와 개발 지식, 경험을 기록하는 공간

0개의 댓글