YAGNI와 OCP를 지키는 것으로 유연성과 확장성을 유지하는 심플한 설계
-
2025년 03월 31일
안녕하세요.
저는 백엔드 엔지니어로 주로 Java, Kotlin, 그리고 Spring Boot를 이용한 시스템 개발을 하고 있는 O입니다.
이 글에서는 YAGNI와 OCP의 원칙을 준수하여 간단하고 확장성이 높은 설계를 구현하는 방법을 설명합니다.
소프트웨어 설계에서 YAGNI(You Aren’t Gonna Need It)와 SOLID 원칙 중 하나인 OCP(Open-Closed Principle)는 단순하고 효과적인 아키텍처를 만들기 위한 중요한 원칙입니다.
이 원칙들은 각각 독립적이지만, 함께 준수하면 단순하고 확장성이 높은 설계가 가능하며, 시스템이 향후 수정에 대해 유연하게 대응할 수 있도록 도와줍니다.
소개
YAGNI(You Aren’t Gonna Need It)란?
‘현재 필요하지 않은 기능은 만들지 않는다’는 원칙입니다.
흔히 나중에 필요할지도 모른다는 이유로 기능을 미리 만들어 놓는 경우가 많은데, 이는 과도한 설계로 이어져 코드가 복잡해지고 유지보수가 어려워지는 원인이 됩니다.
OCP(Open-Closed Principle)란?
‘모듈은 확장에 대해서는 열려 있어야 하고(Open), 수정에 대해서는 닫혀 있어야 한다(Closed)’는 원칙입니다.
코드의 확장은 가능하지만 기존 동작에 영향을 미치지 않도록 하여 시스템의 안정성과 유연성을 유지하는 것입니다.
즉, 기존 코드를 수정하지 않고도 새로운 기능이나 변경 사항을 추가할 수 있도록 설계하는 것을 의미합니다.
안티 패턴
그렇다면 이제 실제 코드에서 안티패턴을 살펴보겠습니다.
이 코드는 에어컨의 켜기/끄기 기능을 나타냅니다.
다음 기능을 모두 하나의 클래스에 넣으면 어떤 문제가 발생할까요?
- 확정 기능
- 리모컨으로 에어컨 켜기/끄기
- 스케줄에 따른 에어컨 켜기/끄기
- 향후 예상되는 추가 기능
- 설정된 온도에 도달하면 에어컨을 켜고 끕니다.
@Service
@RequiredArgsConstructor
public class AirConditionerService {
private final RemoteControlService remoteControlService;
private final ScheduleControlService scheduleControlService;
private final TemperatureControlService temperatureControlService; // 나중에 사용할 것 같은 기능이며, 현재는 구현되지 않은 클래스입니다.
// 리모컨으로 에어컨 켜기/끄기
public void controlWithRemote(boolean isTurnOn) {
if (isTurnOn) {
remoteControlService.turnOn();
} else {
remoteControlService.turnOff();
}
}
// 스케줄에 따른 에어컨 켜기/끄기
public void controlWithSchedule(boolean isTurnOn) {
if (isTurnOn) {
scheduleControlService.turnOn();
} else {
scheduleControlService.turnOff();
}
}
// 온도에 따른 에어컨 켜기/끄기 (나중에 사용할 것 같은 과도한 구현)
public void controlWithTemperature(boolean isTurnOn) {
// TODO: 기능 개발이 결정되면 처리를 작성. 현재로서는 임시 구현.
if (isTurnOn) {
temperatureControlService.turnOn();
} else {
temperatureControlService.turnOff();
}
}
}
YAGNI 위반
현재로서는 ‘온도에 따른 에어컨 켜기/끄기’가 필요 없음에도 불구하고, 처음부터 임시로 구현되어 있습니다.
나중에 필요한 기능을 미리 예측하여 먼저 탑재하게 되면 과잉 설계가 되어 시스템이 복잡해지고 유지보수성이 떨어지게 됩니다.
OCP 위반 위험 있음
만약 ‘온도에 따른 에어컨 켜기/끄기’가 불필요해지고 ‘AI에 의한 계절 및 사용량에 따른 에어컨 켜기/끄기’를 추가하고 싶다면 AirConditionerService 클래스를 수정해야만 합니다.
예를 들어, AIControlService를 필드에 추가하고 controlWithAI() 메서드를 추가해야 합니다.
즉, 코드를 수정하여 기능을 추가할 수밖에 없는 것입니다.
이런 경우, OCP 위반이 됩니다.
베스트 프랙티스
그럼 개선된 코드를 보기 전에 다시 한 번 각 원칙의 요점만 짚고 넘어가겠습니다.
- YAGNI 원칙을 지키기 위해 현재 필요한 기능만 구현하고 향후 기능이 필요할 때 추가한다.
- OCP를 보호하기 위해서는 새로운 기능을 추가할 때 기존 코드를 변경하지 않고 확장할 수 있도록 설계해야 한다.
이러한 원칙을 바탕으로 코드를 생각해 봅시다.
디자인 패턴 중 하나인 Strategy 패턴을 사용하여 에어컨 제어 기능을 확장 가능하게 만든 코드 예시입니다.
이 글에서는 Strategy 패턴에 대한 자세한 설명은 생략하지만, Strategy 패턴과 다른 S 동작(전략)을 동적으로 전환하는 디자인 패턴으로 시스템에 유연성과 확장성을 제공한다.
이 설계는 향후 새로운 기능(온도, AI 및 기타 기능)을 추가할 때, ControlStrategy 인터페이스에 따라 새로운 클래스를 추가하면 되기 때문에 AirConditionerService 클래스를 수정하지 않고도 기능 추가가 가능합니다.
// ControlStrategy 인터페이스 정의하기
public interface ControlStrategy {
void turnOn();
void turnOff();
}
// 리모컨으로 에어컨 켜기/끄기
@Component
public class RemoteControlService implements ControlStrategy {
@Override
public void turnOn() {
// 에어컨 켜기 처리
}
@Override
public void turnOff() {
// 에어컨 끄기 처리
}
}
// 스케줄에 따른 에어컨 켜기/끄기
@Component
public class ScheduleControlService implements ControlStrategy {
@Override
public void turnOn() {
// 에어컨 켜기 처리
}
@Override
public void turnOff() {
// 에어컨 끄기 처리
}
}
// 설정된 온도에 도달하면 에어컨을 켜고 끕니다.
// 필요할 때 만들면 된다
@Component
public class TemperatureControlService implements ControlStrategy {
@Override
public void turnOn() {
// 에어컨 켜기 처리
}
@Override
public void turnOff() {
// 에어컨 끄기 처리
}
}
다음으로 AirConditionerService 클래스에서는 기능을 나타내는 처리를 인자로 받아 이를 기반으로 에어컨을 제어합니다.
서로 다른 기능(예: 리모컨 조작 및 스케줄)을 외부에서 전달하는 것으로 에어컨 제어 방법을 유연하게 전환할 수 있으며, AirConditionerService 자체는 기능에 의존하지 않아 확장성이 높습니다.
@Service
public class AirConditionerService {
// 기능(Strategy)을 인수로 받습니다.
public void turnOnAirConditioner(ControlStrategy controlStrategy) {
controlStrategy.turnOn();
}
// 기능(Strategy)을 인수로 받습니다.
public void turnOffAirConditioner(ControlStrategy controlStrategy) {
controlStrategy.turnOff();
}
}
사용 예시
@Service
@RequiredArgsConstructor
public class RemoteControlAirConditioner {
private final AirConditionerService airConditionerService; airConditionerService
private final RemoteControlService remoteControlService;
// 리모컨으로 에어컨 켜기
public void turnOnWithRemote() {
airConditionerService.turnOnAirConditioner(remoteControlService);
}
// 리모컨으로 에어컨 끄기
public void turnOffWithRemote() {
airConditionerService.turnOffAirConditioner(remoteControlService);
}
}
만약 ‘AI가 계절이나 사용량에 따라 에어컨을 켜고 끄는 기능’을 추가하고 싶다면 ControlStrategy에 따라 AIControlService 클래스를 생성하면 된다.
// AI가 계절과 사용량에 따라 에어컨을 켜고 끌 수 있습니다.
@Component
public class AIControlService implements ControlStrategy {
@Override
public void turnOn() {
// 에어컨 켜기 처리
}
@Override
public void turnOff() {
// 에어컨 끄기 처리
}
}
끝으로
YAGNI와 OCP라는 원칙을 지킴으로써 간단하고 확장성이 높은 시스템을 만드는 방법을 설명했습니다.
과도한 기능을 구현하거나 수을 할 때마다 기존 코드를 수정해야 하는 설계는 나중에 시스템의 유연성과 유지보수성에 큰 영향을 미친다.
일상적인 개발에서 이러한 원칙을 의식하고 실천하는 것이 더 나은 소프트웨어를 만드는 데 도움이 될 것입니다.
부디 이 원칙들을 잘 활용해서 다음 프로젝트에 도전해보세요.
끝까지 읽어주셔서 감사합니다.