본문 바로가기

Clean Code

객체와 자료구조 #5

목차

  • 자료구조 vs 객체
  • 객체 - 디미터 법칙
  • DTO
  • Active Record

자료구조 vs 객체

자료구조 객체
데이터 그 자체 비즈니스 로직과 관련
자료를 공개한다. 자료를 숨기고, 추상화 한다.
자료를 다루는 함수만 공개한다.
변수 사이에 조회 함수와 설정 함수로 
변수를 다룬다고 객체가 되지 않는다.
추상 인터페이스를 제공해 사용자가 구현을 모른채
자료의 핵심을 조작할 수 있다.

예시 1

//자료구조
public interface Vehicle {
    double getFuelTankCapacityInGallons();  // 연료탱크 용량(갤런 단위)
    double getGallonsOfGasoline();          // 가솔린 (갤런 단위)
}

public class Car implements Vehicle {
    double fuelTanckCapacityInGallons;
    double gallonsOfGasoline;

    @Override
    public double getFuelTankCapacityInGallons() {
        return fuelTanckCapacityInGallons;
    }

    @Override
    public double getGallonsOfGasoline() {
        return gallonsOfGasoline;
    }
}

 

// 객체
public interface Vehicle {
    double getPercentFuelRemain();
}

public class Car implements Vehicle {
    double fuelTanckCapacityInGallons;
    double gallonsOfGasoline;

    public Car(double fuelTanckCapacityInGallons, double gallonsOfGasoline) {
        if(fuelTanckCapacityInGallons <=0 ) {
            throw new IllegalArgumentException("fuelTankCapacityInGallons must be greater than zero");
        }
        this.fuelTanckCapacityInGallons = fuelTanckCapacityInGallons;
        this.gallonsOfGasoline = gallonsOfGasoline;
    }

    @Override
    public double getPercentFuelRemain() {
        return this.gallonsOfGasoline / this.fuelTanckCapacityInGallons * 100;
    }
}

예시 2

// 자료구조
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();
    }
}

 

절차적인 코드는 새로운 자료구조를 추가하기 어렵다. 함수를 고쳐야 한다. 만약 위의 코드에서 다른 도형이 추가된다면

else if ~ 로 계산식을 새로 추가해야 한다..

 

// 객체
public interface Shape {
    public double area();
}

public class Square implements Shape{
    public Point topLeft;
    public double side;

    @Override
    public double area() {
        return side * side;
    }
}

public class Rectangle implements Shape {
    public Point topLeft;
    public double height;
    public double width;

    @Override
    public double area() {
        return  height * width;;
    }
}

public class Circle implements Shape {
    public final double PI = 3.141592653589793;
    public Point center;
    public double radius;
    
    @Override
    public double area() {
        return PI * c.radius * c.radius;
    }
}

 

객체지향 코드는 새로운 클래스를 추가하기 쉽다. 하지만 함수를 추가해야 한다. 

상황에 맞는 선택을 하면 된다.

  • 자료구조를 사용하는 절차적인 코드는 기본 자료구조를 변경하지 않으면서 새 함수를 추가하기 쉽다.
  • 절차적인 코드는 새로운 자료구조를 추가하기 어렵다. 그러면 모든 함수를 고쳐야 한다.
  • 객체지향 코드는 기존 함수를 변경하지 않으면서 새 클래스를 추가하기 쉽다.
  • 객체지향 코드는 새로운 함수를 추가하기 어렵다. 그러려면 모든 클래스를 고쳐야 한다.

객체 - 디미터 법칙

디미터의 법칙이란?

디미터의 법칙은 “Object-Oriented Programming: An Objective Sense of Style” 에서 처음으로 소개되었다. Demeter라는 프로젝트를 진행하던 개발자들은 어떤 객체가 다른 객체에 대해 지나치게 많이 알다보니, 결합도가 높아지고 문제를 야기한다는 것을 발견하였다. 그래서 이를 개선하고자 객체에게 자료를 숨기는 대신 함수를 공개하도록 하였는데, 이것이 바로 디미터의 법칙이다.

즉, 디미터의 법칙은 다른 객체가 어떠한 자료를 갖고 있는지 속사정을 몰라야 한다는 것을 의미하며, 이러한 이유로 Don’t Talk to Strangers(낯선 이에게 말하지 마라) 또는 Principle of least knowledge(최소 지식 원칙) 으로도 알려져 있다.

또는 직관적으로 이해하기 위해 여러 개의 .(참조)을 사용하지 말라는 법칙으로도 많이 알려져 있다.

객체 지향 프로그래밍과 디미터의 법칙

객체 지향 프로그래밍에서 가장 중요한 것은 "객체가 어떤 데이터를 가지고 있는가?"가 아니라, "객체가 어떤 메세지를 주고 받는가?" 이다. 그렇기에 디미터의 법칙은 객체 지향 프로그래밍에서 상당히 중요한 개념인데, 디미터의 법칙이 위배된다는 것은 올바른 객체 지향 프로그래밍을 하지 못하고 있다는 증거이기도 하다.

올바른 객체 지향 프로그래밍을 하지 못하는 코드를 살펴보고, 이에 디미터의 법칙을 적용해보도록 하자.

[ 디미터의 법칙을 위반하는 코드 ]

예를 들어 서울에 살고 있는 어떤 사용자에게 알림을 보내주는 함수를 구현한다고 하자. 이를 구현하기 위해 우리는 다음과 같은 User 객체와 Address 객체를 필요로 할 것이고, User객체는 Address라는 주소 객체를 가지고 있을 것이다.

 

@Getter 
public class User { 
    private String email; 
    private String name; 
    private Address address; 
} 

@Getter 
public class Address { 
    private String region; 
    private String details; 
}

 

이제 어떤 사용자가 서울에 살고 있으면 알림을 보내주는 함수를 다음과 같이 구현하였다고 하자.

@Service 
public class NotificationService { 
	public void sendMessageForSeoulUser(final User user) { 
    	if("서울".equals(user.getAddress().getRegion())) { 
        	sendNotification(user); 
        } 
    } 
}

 

위와 같이 구현된 코드는 정말 흔하게 볼 수 있는 코드이지만, 디미터의 법칙을 위반하고 있는 코드이며 객체지향 스럽지 못하다. 왜냐하면 우리는 객체에게 메세지를 보내는 것이 아니라 객체가 가지는 자료를 확인하고 있으며, 다른 객체가 어떠한 자료를 갖고 있는지 지나치게 잘 알기 때문이다.

(이러한 이유로 @Setter는 물론이거니와 무분별한 @Getter 역시 지양하고자 하는 개발자도 많이 있다.)

그렇기에 우리는 위의 코드를 객체 지향스러우며 디미터의 법칙을 준수하도록 수정할 필요가 있다.

[ 디미터의 법칙을 준수하는 코드  ]

이를 위해 우리는 데이터로 사용자의 지역을 파악하는 것이 아니라, 메세지를 보내 다음과 같이 서울 지역에 사는지 파악하도록 구현할 수 있다.

 

public class Address {

    private String region;
    private String details;

    public boolean isSeoulRegion() {
        return "서울".equals(region);
    }
    
}

@Getter
public class User {

    private String email;
    private String name;
    private Address address;

    public boolean isSeoulUser() {
        return address.isSeoulRegion();
    }
    
}

 

위와 같이 객체에게 보내는 메세지를 구현하면 현재로써는 Address객체의 @Getter 역시 불필요하여 지워줄 수 있다. (추후에 필요할 수도 있지만 말이다.) 그리고 기존의 알림을 보내는 로직을 다음과 같이 수정할 수 있다. 

 

@Service public class NotificationService { 
	public void sendMessageForSeoulUser(final User user) { 
    	if(user.isSeoulUser()) { 
        	sendNotification(user); 
        } 
    } 
}

 

기존의 user.getAddress().getRegion() 처럼 여러 개의 .을 사용하지 않기 때문이 디미터의 법칙을 잘 준수하고 있다.

참조

https://mangkyu.tistory.com/147

 

DTO(Data Transfer Object) = 자료구조

다른 계층 간 데이터를 교환할 때 사용

  • 로직 없이 필드만 갖는다.
  • 일반적으로 클래스명이 Dto(or DTO)로 끝난다.
  • getter/setter를 갖기도 한다. => private 으로 선언하지 않고, public 으로 인스턴스 변수를 오픈하여 사용하는 경우도 있음..

Active Record

Database row 를 객체에 맵핑하는 패턴(자료구조)

  • 비즈니스 로직 매서드를 추가해 객체로 취급하는 건 바람직하지 않다.
  • 비즈니스 로직을 담으면서 내부 자료를 숨기는 객체는 따로 생성한다.
  • 하지만 객체가 많아지면 복잡하고,  가까운 곳에 관련 로직이 있는 것이 좋으므로 현업에서는 Entity 에 간단한 메서드를 추가해 사용한다. 많은 비즈니스 로직의 경우 별로 클래스로 구분한다.
public class Employee extends ActiveRecord {
    private String name;
    private String address;
}

// -----

Employee bob = Employee.findByName("Bob Martin");

bob.setName("Rovert C. Martin");
bob.save();

 

Entity와 다르게 생겼다..! Entity와 Repository 기능을 합쳐놓음;

Active Record vs DataMapper

 

'Clean Code' 카테고리의 다른 글

모호한 경계를 구분짓기 #7  (0) 2021.08.05
예외 처리하기 #6  (0) 2021.08.04
형식 맞추기 #4  (0) 2021.08.03
코드를 보조하는 주석 #3  (0) 2021.08.03
함수를 안전하고 간결하게 작성하기 #2  (0) 2021.08.02