본 문서에서는 SOLID 원칙을 학습합니다. 각 원칙에 대한 이해를加深하고자 Java 코드 예제를 제공합니다.
SOLID 원칙은 객체 지향 프로그래밍에서 사용되는 다섯 가지 디자인 원칙의 집합입니다. 이 원칙을 준수하면 견고한 소프트웨어를 개발할 수 있으며, 코드를 더 효율적이고, 가독성이 있고, 유지보수하기 쉬워지게 만들어줍니다.
SOLID는 다음을 의미하는 약어입니다:
- 단일 책임 원칙
- 개방/封闭 원칙
- 리스코프 대체 원칙
- 인터페이스 분리 원칙
- 의존성 역전 원칙
단일 책임 원칙
단일 책임 원칙은 모든 클래스가 단일한, 명확한 책임을 가져야하며, 변경되는 이유가 단 하나여야 한다고 합니다.
public class Employee{
public String getDesignation(int employeeID){ // }
public void updateSalary(int employeeID){ // }
public void sendMail(){ // }
}
위 예제에서 Employee
클래스는 getDesignation
와 updateSalary
와 같은 몇몇 직원 클래스 전용 동작을 가지고 있습니다.
또한 sendMail
라는 다른 메서드도 있고, 이는 Employee
클래스의 책임을 벗어납니다.
이러한 동작은 이 클래스에 고유하게 없으며, 존재하면 단일 책임 원칙을 위반합니다. 이를 극복하려면 sendMail
메서드를 별도의 클래스로 옮기면 됩니다.
다음은 그 방법입니다:
public class Employee{
public String getDesignation(int employeeID){ // }
public void updateSalary(int employeeID){ // }
}
public class NotificationService {
public void sendMail() { // }
}
개방/關閉 원칙
개방/關閉 원칙에 따르면, 구성요소는 확장을 위해 개방되어야 하지만, 수정을 위해는關閉되어야 합니다. 이 원칙을 이해하기 위해, 도형의 면적을 계산하는 클래스의 예를 들어보겠습니다.
public class AreaCalculator(){
public double area(Shape shape){
double areaOfShape;
if(shape instanceof Square){
// 정사각형의 면적 계산
} else if(shape instanceof Circle){
// 원의 면적 계산
}
return areaOfShape;
}
위 예제의 문제는, 미래에 새로운 Shape
유형의 인스턴스가 면적을 계산해야 하는 경우, 추가적인 조건 else-if
블록을 추가하여 위 클래스를 수정해야 합니다. Shape
유형의 새 객체마다 이렇게 할 수 밖에 없습니다.
이를 극복하기 위해, 인터페이스를 생성하고 각 Shape
이 이 인터페이스를 구현하도록 할 수 있습니다. 그런 다음, 각 클래스는 자신의 면적을 계산하는 방법을 제공할 수 있습니다. 이렇게 되면 미래에 프로그램을 쉽게 확장할 수 있게 됩니다.
interface IAreaCalculator(){
double area();
}
class Square implements IAreaCalculator{
@Override
public double area(){
System.out.println("Calculating area for Square");
return 0.0;
}
}
class Circle implements IAreaCalculator{
@Override
public double area(){
System.out.println("Calculating area for Circle");
return 0.0;
}
}
리스코프 대체 원칙
리스코프 대체 원칙은 superclass 객체를 subclass 객체로 대체할 수 있어야 하며, 프로그램의 올바른 작동을影响力하지 않는다고 합니다.
abstract class Bird{
abstract void fly();
}
class Eagle extends Bird {
@Override
public void fly() { // 일부 구현 }
}
class Ostrich extends Bird {
@Override
public void fly() { // 더미 구현 }
}
상기 예제에서 Eagle
클래스와 Ostrich
클래스는 모두 Bird
클래스를 확장하고 fly()
메서드를 오버라이드합니다. 그러나 Ostrich
클래스는 날 수 없으므로 더미 구현을 제공하도록 강제되어 있으며, Bird
클래스 객체를 그것으로 교체하면 동일하게 동작하지 않습니다.
이는 Liskov 치환 원칙을 위반합니다. 이를 해결하려면 날 수 있는 새들을 위한 별도의 클래스를 생성하고 Eagle
클래스가 그것을 확장하게 하고, 다른 새들은 fly
동작을 포함하지 않는 다른 클래스를 확장하도록 합니다.
abstract class FlyingBird{
abstract void fly();
}
abstract class NonFlyingBird{
abstract void doSomething();
}
class Eagle extends FlyingBird {
@Override
public void fly() { // 일부 구현 }
}
class Ostrich extends NonFlyingBird {
@Override
public void doSomething() { // 일부 구현 }
}
인터페이스 분리 원칙
인터페이스 분리 원칙에 따르면, 클라이언트가 필요하지 않은 동작을 강제시키는 작고 초점이 명확한 인터페이스를 구축해야 합니다.
간단한 예제는 도형의 면적과 부피를 모두 계산하는 인터페이스를 가지는 것입니다.
interface IShapeAreaCalculator(){
double calculateArea();
double calculateVolume();
}
class Square implements IShapeAreaCalculator{
double calculateArea(){ // 면적 계산 }
double calculateVolume(){ // 더미 구현 }
}
이 문제는 Square
도형이 이를 구현하면 calculateVolume()
메서드를 필요 없이 구현하도록 강제되어 있습니다.
다른 한편으로, 큐브
는 둘 다 구현할 수 있습니다. 이 문제를 극복하기 위해 인터페이스를 분리하고 두 개의 별도의 인터페이스를 가진다: 넓이를 계산하는 것과 부피를 계산하는 것입니다. 이렇게 하면 개별적인 도형이 어떻게 구현할지 결정할 수 있습니다.
interface IAreaCalculator {
double calculateArea();
}
interface IVolumeCalculator {
double calculateVolume();
}
class Square implements IAreaCalculator {
@Override
public double calculateArea() { // 넓이를 계산합니다 }
}
class Cube implements IAreaCalculator, IVolumeCalculator {
@Override
public double calculateArea() { // 넓이를 계산합니다 }
@Override
public double calculateVolume() {// 부피를 계산합니다 }
}
의존성 반전 원리
의존성 반전 원리에 따르면, 고급 모듈은 저급 모듈에 의존해서는 안 됩니다. 다시 말해, 추상화를 따라가고 느슨한 결합을 보장해야 합니다.
public interface Notification {
void notify();
}
public class EmailNotification implements Notification {
public void notify() {
System.out.println("Sending notification via email");
}
}
public class Employee {
private EmailNotification emailNotification;
public Employee(EmailNotification emailNotification) {
this.emailNotification = emailNotification;
}
public void notifyUser() {
emailNotification.notify();
}
}
주어진 예제에서, Employee
클래스는 직접 EmailNotification
클래스에 의존합니다. 이는 저급 모듈입니다. 이렇게 하면 의존성 반전 원리를 위반합니다.
public interface Notification{
public void notify();
}
public class Employee{
private Notification notification;
public Employee(Notification notification){
this.notification = notification;
}
public void notifyUser(){
notification.notify();
}
}
public class EmailNotification implements Notification{
public void notify(){
// 이메일을 통한 알림 구현
}
}
public static void main(String [] args){
Notification notification = new EmailNotification();
Employee employee = new Employee(notification);
employee.notifyUser();
}
위의 예제에서는 느슨한 결합을 보장했습니다. Employee
클래스는 구체적인 구현에 의존하지 않고, 추상화(알림 인터페이스)에만 의존합니다.
알림 모드를 변경해야 한다면, 새로운 구현을 만들고 Employee
에 전달할 수 있습니다.
결론
결론으로, 이 문서에서 SOLID 원리의 본질을 간단한 예제를 통해 다루었습니다.
이러한 원리는 고체적인 확장성과 재사용성이 있는 응용 프로그램을 개발하는 데 기반을 제공합니다.
Source:
https://www.freecodecamp.org/news/introduction-to-solid-principles/