이 내용은 udemy의 SOLID Design Principles를 정리한 내용입니다.
https://www.udemy.com/course/design-patterns-in-java-concepts-hands-on-projects/
이 원칙의 일반적인 정의를 한번 알아 보자.
Open closed principle에서 엔티티는 기본적으로 클래스, 모듈, 메소드를 나타내며, 소프트웨어 엔티티는 확장에는 열려있어야 하고 수정에는 닫혀있어야 한다.
이 의미는 헷갈리게 들릴 수 있지만 실제로는 그렇지 않다. 여러분이 Java를 알고 객체지향을 알고 있다면 이해하기 매우 쉬운 원칙이다.
확장에 열려있다는 것(open for extension)은 기존 동작을 확장할 수있어야 한다는 것을 의미하고 수정에 닫혀있다는 것(closed for modification)은 이미 작성된 것은 변경이 되어서는 안된다는 것을 의미한다.
Java에서 이것이 의미하는 것은 이미 작성이 완료되고 테스트가 끝난 클래스가 있다는 것을 나타낸다.
그리고 이 클래스의 메소드 중 하나를 확장하거나 수정이 필요하다면 그렇게 할 수 있어야 한다.
이러한 것들을 상속을 사용해서 할 수 있다. 기반 클래스를 확장하고 메소드를 override 할 수 있다.
확장에 열려있고 수정에 닫혀있다는 것은 코드가 작성되고 테스트가 완료되었기 때문에 기반 클래스에 작성된 코드를 수정하지 않아야 한다는 것이다.
이것이 Open Closed Principle가 말하는 의미이다.
기반 클래스를 수정하지 말고 상속을 사용하여 기존 기능을 확장하는데 override를 사용하라는 의미이다.
예제
시작하기 전에 알아야 할 내용이 있다.
가상의 전화 회사에 대한 코드를 작성하고 있다고 해보자. 이 회사는 전화 서비스, 인터넷 서비스 같은 서비스들을 고객에게 제공하고 있다. 우리가 해야 할 일은 Java를 사용해서 고객 혹은 구독자의 사용현황을 알아내는 것이다.
@Getter
@Setter
public class PhoneSubscriber {
private Long subscriberId;
private String address;
private Long phoneNumber;
private int baseRate;
public double calculateBill() {
List<CallHistory.Call> sessions = CallHistory.getCurrentCalls(subscriberId);
long totalDuration = sessions.stream().mapToLong(CallHistory.Call::getDuration).sum();
return totalDuration * baseRate / 100;
}
}
우선 PhoneSubscriber라는 클래스가 있다.
이 클래스는 전화 서비스를 사용하는 구독자를 나타낸다. subscriberId, address, phoneNumber, baseRate가 있고 baseRate는 요금을 계산할 때 사용된다.
calculateBill은 여기서 데모용으로만 사용되고 있다는 것을 명심해야 한다.
ISPSubscriber는 또 다른 클래스이고 인터넷 서비스 구독자를 나타낸다.
@Getter
@Setter
@NoArgsConstructor
public class ISPSubscriber {
private Long subscriberId;
private String address;
private Long phoneNumber;
private int baseRate;
private long freeUsage;
public double calculateBill() {
List<InternetSessionHistory.InternetSession> sessions = InternetSessionHistory.getCurrentSessions(subscriberId);
long totalData = sessions.stream().mapToLong(InternetSessionHistory.InternetSession::getDataUsed).sum();
long chargeableData = totalData - freeUsage;
return chargeableData * baseRate / 100;
}
}
freeUsage는 추가적인 속성이다. 여유 제한이 있으면 추가 금액이 부과되지는 않을 것이다.
그리고 두 개의 추가 클래스가 있다. InternetSession, History
이 클래스들은 인터넷 서비스와 전화 서비스 사용량을 추적하는 데 사용된다.
public class InternetSessionHistory {
@Getter
@Setter
public static class InternetSession {
private LocalDateTime begin;
private Long subscriberId;
private Long dataUsed;
public InternetSession(Long subscriberId, LocalDateTime begin, long dataUsed) {
this.begin = begin;
this.dataUsed = dataUsed;
this.subscriberId = subscriberId;
}
}
private static final Map<Long, List<InternetSession>> SESSIONS = new HashMap<>();
public synchronized static List<InternetSession> getCurrentSessions(Long subscriberId) {
if(!SESSIONS.containsKey(subscriberId)) {
return Collections.emptyList();
}
return SESSIONS.get(subscriberId);
}
public synchronized static void addSession(Long subscriberId, LocalDateTime begin, long dataUsed) {
List<InternetSession> sessions;
if(!SESSIONS.containsKey(subscriberId)) {
sessions = new LinkedList<>();
SESSIONS.put(subscriberId, sessions);
} else {
sessions = SESSIONS.get(subscriberId);
}
sessions.add(new InternetSession(subscriberId, begin, dataUsed));
}
}
그럼, 여기서 알 수 있는 문제는 무엇인가? 몇 가지 정보가 중복이 되고 중복을 피할 수 있다는 것을 알 수 있다.
예를 들면, ISPSubscriber와 PhoneSubscriber를 보면 두 개 클래스에 공통인 속성이 있다. 이것들은 피해야 할 중복인 것이다.
여기 또 다른 문제가 있다. 나중에 회사에서 새 서비스를 추가한다고 해보자. 예를 들면 VoIP 서비스를 추가할 수 있다. 그러면 새로운 Subscriber를 만들 것이고 이 정보는 VoIPSubscriber에 중복되고 들어갈 것이다.
요금이 어떻게 계산되고 각 Subscriber에 추가 정보가 어떤 것이 있는지 알아보자.
freeUsage는 인터넷 서비스에만 있다. 전화 서비스는 freeDuration 같은게 있을 수 있고 VoIP 또한 다른 속성을 가질 수 있다.
하지만 이러한 공통 데이터를 기반 클래스에 두면 각 개별 클래스를 변경할 수 있고 요금에 변경사항이 있을 때 전체를 다시 테스트 할 필요가 없다는 것을 알 수 있다.
이것이 Open Closed Principle에서 말하고 싶은 것이다.
새로운 서비스가 추가될 때, 예를 들어 VoIP서비스라고 해보자. 새로운 클래스를 추가하고 calculate 메소드를 override하여 요금을 구현할 것이다.
우선 Subscriber라는 추상 클래스를 추가하고 전화서비스와 인터넷 서비스가 사용하게 하자.
@Getter
@Setter
// closed for modification
public abstract class Subscriber {
protected Long subscriberId;
protected String address;
protected Long phoneNumber;
protected int baseRate;
public abstract double calculateBill(); // open for extension
}
public class PhoneSubscriber extends Subscriber {
@Override
public double calculateBill() {
List<CallHistory.Call> sessions = CallHistory.getCurrentCalls(subscriberId);
long totalDuration = sessions.stream().mapToLong(CallHistory.Call::getDuration).sum();
return totalDuration * baseRate / 100;
}
}
public class ISPSubscriber extends Subscriber {
private long freeUsage;
@Override
public double calculateBill() {
List<InternetSessionHistory.InternetSession> sessions = InternetSessionHistory.getCurrentSessions(subscriberId);
long totalData = sessions.stream().mapToLong(InternetSessionHistory.InternetSession::getDataUsed).sum();
long chargeableData = totalData - freeUsage;
return chargeableData * baseRate / 100;
}
}
Subscriber는 수정에 닫혀져 있고(closed for modification), calculateBill은 확장에 열려져 있는 것(open for extension)이다.
이것이 Open Closed Principle를 사용하는 방식이다.
만일 VoIP서비스가 추가된다면 어떻게 하면 될까?
새 클래스를 추가하고 calculateBill을 구현하기만 하면 된다.