객체는 자신만의 기능을 제공하며, 각 객체들은 서로 연결되어 메세지를 주고 받으며 소프트웨어를 구성한다.
객체
객체는 데이터와 데이터를 조작하는 프로시저(메소드)로 구성된다.
객체는 객체마다 자신만의 책임(Responsibility)이 있다.
객체는 서로의 책임을 하며 다른 객체와 의존(Dependency)하며 소프트웨어를 구성한다.
‘읽기 객체’ , ‘집계 객체’ , ‘정산 객체’는 각자의 책임을 가지며 서로 메시지를 주고 받으며 의존 하고 있다.
객체 책임
객체가 가지는 책임을 결정하는 것이 객체 지향 설계의 출발점이다.
매출 데이터 정산에 대한 기능 목록 정리
업주의 매출 데이터를 읽는다.
매출 데이터를 업주별로 집계한다.
매출 데이터를 업주에게 정산한다.
전체 흐름을 제어한다.
각 책임을 아래와 같이 분리할 수 있다.
객체가 갖는 책임의 크기는 작을수록 좋다.
하나의 객체가 많은 기능을 포함하면, 그 기능관 관련된 데이터들도 한객체에 포함된다.
객체의 책임이 커질수로 유지보수가 어려워진다.
객체 의존
객체지향 프로그램에서는, 객체가 다른 객체의 기능을 이용해서 자신의 기능을 완성 한다.
객체에서 다른 객체를 생성 하거나 다른 객체의 메서드를 호출할 때, 해당 객체에 의존 한다고 한다.
위 코드에서 SalesFlowService는 SalesInfoReader, SalesInfoCalculator, SettleDataWriter 에 의존한다.
의존성 전이
객체의 변화로 인해 다른 객체에 영향이 가게 된다.
변경이 많은 객체는 의존이 많이 되지 않도록 해야 한다.
변경은 의존 관계를 따라 전이 된다.
캡슐화
객체가 내부적으로 기능을 어떻게 구현 했는지 감추는 것을 말한다.
캡슐화를 통해 내부구현의 변경이 그 기능을 사용하는 코드에 영향을 받지 않게한다.
주민등록번호 첫째자리를 구하는 로직이 2000년대생 이후로 남자는 ‘3’ 여자는 ‘4’로 변경 되었다면 Customer 의 데이터를 사용하는 Bank 와 Company 코드는 코드 수정이 발생한다.
캡슐화를 통해서 Customer의 구현이 변경되어도 이를 사용하는 외부 구현에는 영향을 미치지 않는다.
객체지향 설계 과정
소프트웨어 구현에 필요한 기능을 찾고 이를 세분화한다.
세분화된 기능을 알맞은 객체에 할당한다.
객체간에 어떻게 메시지를 주고받을 지 결정한다.
2, 3번 과정을 지속적으로 반복한다.
SOLID
단일 책임 원칙 (SRP)
개방-폐쇄 원칙 (OCP)
리스코프 치환 원칙 (LSP)
인터페이스 분리 원칙 (ISP)
의존 역전 원칙 (DIP)
단일 책임 원칙
Single responsibility principle; SRP
객체가 가지는 책임을 결정하는 것이 객체 지향 설계의 출발점이다.
SRP 는 책임과 관련된 “객체는 단 한 개의 책임을 가져야 한다.” 는 원칙이다.
객체가 여러 책임을 가지게 되면, 각 책임 마다 변경되는 이유가 발생한다.
객체의 변경은 의존된 다른 객체에 변화를 유발한다.
따라서, 객체는 단 한개(최대한 적은)의 책임을 가져야 한다.
위에서 살펴본 SalesFlowService 를 통해 SRP 를 살펴본다.
개방 폐쇄 원칙
Open-close principle; OCP
확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.
기능을 변경하거나 확장할 수 있으며, 그 기능을 사용하는 코드는 수정하지 않는다.
즉, 자신의 확장에는 열려 있고, 주변의 변화에 대해서는 닫혀 있어야 한다.
위 예제코드에서 기능의 변경으로 그 기능을 사용하는 코드가 변경되었다.
추상화를 통해 아래와 같이 리펙토링이 가능하다.
OCP 를 적용한 코드는 기능의 변경과 확장(음식점의 판매 품목 변경)에는 열려 있으며, 그 기능을 사용하는 코드의 변화(판매 품목이 변경되어도 가게 객체의 변화는 없다.)에는 닫혀있다.
리스코프 치환 원칙
Liskov substitution principle; LSP
상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다.
LSP가 제대로 지켜지지 않으면, 다형성에 기반한 OCP 역시 위반되게 된다.
직사각형 정사각형 예제
직사각형 ≠ 정사각형
정사각형 = 직사각형
increaseHeight 메소드는 직사각형의 가로(width)와 세로(height)를 비교하여, 가로의 길이에 1을 더한 만큼의 세로길이를 갖게 만드는 역할을 한다.
위 메소드는 정사각형에서는 정상동작 하지 않음으로 코드를 아래와 같이 수정해야 한다.
increaseHeight 메소드는 특정 객체에서는 예외가 발생하므로 확장에는 닫혀있다.
리스코프 치환 원칙을 지키지 않으면 개방 폐쇄 원칙을 위반하게 된다. 따라서 상속 관계를 잘 정의하여 LSP 원칙이 위배되지 않도록 설계해야 한다.
인터페이스 분리 원칙
Interface segregation principle; ISP
클라이언트는 자신이 사용하는 메소드에만 의존해야 한다.
왼쪽 다이어그램에서는 Client는 각각 function1, function2 만을 사용한다.
우측 다이어그램과 같이 각 Client가 사용하는 메소드만을 의존하도록 한다.
클라이언트를 기준으로 인터페이스를 분리함으로써, 클라이언트가 사용하지 않는 인터페이스에 변경이 발생하더라고 영향을 받지 않도록 하는것이 ISP의 핵심이다.
의존성 역전 원칙
Dependency inversion principle; DIP
의존 관계를 맺을 때, 변하기 쉬운것 (구체적인 것) 보다는 변하기 어려운 것(추상적이 것)에 의존해야 한다.
즉, 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다.
고수준 모듈 : 변경이 없는 추상화된 클래스 (interface, abstract class)
저수준 모듈 : 변하기 쉬운 구체 클래스 (class)
SOLID 정리
SRP 와 ISP 는 객체가 커지는 것을 막아준다.
객체가 단일 책임을 갖도록 하고(SRP), 클라이언트 마다 특화된 인터페이스를 구현(ISP)하게 함으로써 한 기능의 변경이 다른 곳까지 미치는 영향을 최소화 한다.
LSP와 DIP는 OCP를 서포트한다.
OCP는 자주 변화되는 부분을 추상화(DIP)하고 다형성을 이용함(LSP)으로써 기능확장에는 용이하되 기존 코드의 변화에는 보수적이 되도록 만들어 준다.
// 객체
public class Product {
// 데이터
private String name;
private long price
private long quantity;
// 프로시저
public void increaseQuntity() {
this.quantity++;
};
}
// 전체 흐름제어
public class SalesFlowService {
private SalesInfoRepository salesInfoRepository;
private SettleRepsitory settleRepository;
public void calculateSalesData(LocalDate txDate) {
//1. 업주의 매출 데이터를 읽어온다.
List<SalesInfo> salesInfos = salesInfoRepository.findByTxDate(txDate);
// 2. 매출데이터를 업주별로 집계한다.
Map<Long, List<SalesInfo>> salesInfoByShopNumber = salesInfos.stream().collect(groupingBy(SalesInfo::shopNumber));
// 3. 매출데이터를 업주에게 정산한다.
for (String shopNumber : salesInfoByShopNumber.keySet()) {
List<SalesInfo> specificShopSalesInfos = salesInfoByShopNumber.get(shopNumber);
List<SettleDate> specificShopSettleDatas = specificShopSalesInfos.stream().map(SettleDate::convert).collect(toList());
settleRepository.save(specificShopSettleDatas);
}
}
}
흐름 제어 객체는 매출 데이터 읽기, 집계, 정산 객체의 기능을 이용하여 자신의 기능을 완성했다.
public class Restaruant {
private Customer customer;
private Food food;
public void sellFood() {
customer.buyFood(food);
}
}
public class Customer {
private long fund;
private Food food;
public long buyFood(Food food) {
fund = fund - food.caculateAmount();
food = food;
}
}
public class Food {
private long price;
private long quantity;
public long calculateAmount() {
return price * quantity;
}
}
Food 의 변화는 Customer 에게 영향을 미치며 Customer의 변화는 Restaruant 에 변화를 미친다.
public class Bank {
public void getFirstDigitOfAfterRegistrationNumber(Customer customer) {
if(customer.isMale) {
return 1;
}
return 2;
}
}
public class Company {
public void getFirstDigitOfAfterRegistrationNumber(Customer customer) {
if(customer.isMale) {
return 1;
}
return 2;
}
}
public class Customer {
private LocalDate birthDate;
private boolean male;
public boolean isMale() {
return male;
}
public LocalDate getBirthDate() {
return birthDate;
}
}
캡슐화가 되지 않은 코드는 내부구현이 외부로 노출된다.
public class Bank {
public int getFirstDigitOfAfterRegistrationNumber(Customer customer) {
return customer.getFirstDigitOfAfterRegistrationNumber();
}
}
public class Company {
public int getFirstDigitOfAfterRegistrationNumber(Customer customer) {
return customer.getFirstDigitOfAfterRegistrationNumber();
}
}
public class Customer {
private LocalDate birthDate;
private boolean male;
public int getFirstDigitOfAfterRegistrationNumber(Customer customer) {
if (birthDate.isAfter(LocalDate.of(2000,1, 1)) {
return after2000FirstDigitOfAfterRegistrationNumber();
} else {
return before2000FirstDigitOfAfterRegistrationNumber();
}
}
private int after2000FirstDigitOfAfterRegistrationNumber() {
if(customer.isMale) {
return 3;
}
return 4;
}
private int before2000FirstDigitOfAfterRegistrationNumber() {
if(customer.isMale) {
return 1;
}
return 2;
}
}
public class SalesFlowService {
private SalesInfoReader salesInfoReader;
private SalesInfoCalculator salesInfoCaculator;
private SettleDataWriter settleDataWriter;
// 책임 1
public void calculateSalesData(LocalDate txDate) {
// 책임을 다른객체에 위임
List<SalesInfo> salesInfos = salesInfoReader.findByTxDate(txDate);
Map<Long, List<SalesInfo>> salesInfoByShopNumber = salesInfoCaculator.calculate(salesInfos);
settleDataWriter.write(salesInfoByShopNumber);
}
SRP를 적용한 SalesFlowService는 오직 한가지 책임만 가진다.
public class Shop {
private Food food;
private long salesAmount;
private Food sell() {
this.salesAmount += food.calculateAmount();
return food;
}
}
음식 판매 가게의 코드는 위와 같다.
public class Shop {
private Car car;
private long salesAmount;
private Car sell() {
this.salesAmount += car.calculateAmount();
return car;
}
}
판매할 품목을 ‘음식’ → ‘자동차’로 변경이 되었다.
public class Shop {
private Product product;
private long salesAmount;
private Product sell() {
this.salesAmount += car.calculateAmount();
return product;
}
}
public interface Product {
public long calculateAmount();
}
public class Food implements Product {
@Override
public long calculateAmount() {
// 음식판매금액 구현
};
}
public class Car implements Product {
@Override
public long calculateAmount() {
// 자동차판매금액 구현
};
}
public int calculate(Item item) {
return item.calculate();
}
상위 타입인 Item이 있고, 그 하위 타입이 Apple이라면,
메소드 파라미터로 Item이 아닌 Apple을 넘겨도 코드가 동작해야 한다.
// 직사각형 객체
public class Rectangle {
private int width;
private int height;
public void setWidth(final int width) {
this.width = width;
}
public void setHeight(final int height) {
this.height = height;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
}
// 정사각형은 직사각형이기에 직사각형 객체를 상속받아 정사각형 객체를 정의
public class Square extends Rectangle {
// 정사각형은 가로,세로가 동일하기에 직사각형의 setWidth, setHeight 기능을 재정의한다.
@Override
public void setWidth(final int width) {
super.setWidth(width);
super.setHeight(width);
}
@Override
public void setHeight(final int height) {
super.setWidth(height);
super.setHeight(height);
}
}
public class Application() {
public void increaseHeight(Rectangle rectangle) {
if (rectangle.getHeight() <= rectangle.getWidth()) {
rectangle.setHeight(rectangle.getWidth() + 1);
}
}
}
public class Application() {
public void increaseHeight(Rectangle rectangle) {
// 정사각형일 때는 수행하지 않는다.
if (rectangle instanceof Square) {
throw new IllegalStateException();
}
if (rectangle.getHeight() <= rectangle.getWidth()) {
rectangle.setHeight(rectangle.getWidth() + 1);
}
}
}