개요

SOLID 원칙은 객체 지향 프로그래밍 및 설계의 다섯 가지 기본 원칙을 말합니다. 이 원칙들을 적용하여 리팩토링을 수행하면 더 유지보수하기 쉽고, 유연하며, 확장 가능한 소프트웨어를 만들 수 있습니다.

상세

SOLID 원칙의 각 요소와 그에 따른 리팩토링 기법을 살펴보겠습니다:

  1. 단일 책임 원칙 (Single Responsibility Principle, SRP)

클래스는 단 하나의 책임만 가져야 합니다.

예제:

// Before
class Employee {
    public void calculatePay() { /* ... */ }
    public void save() { /* ... */ }
    public void reportHours() { /* ... */ }
}
 
// After
class Employee {
    private PayCalculator payCalculator;
    private EmployeeRepository repository;
    private HourReporter hourReporter;
 
    public Money calculatePay() {
        return payCalculator.calculatePay(this);
    }
 
    public void save() {
        repository.save(this);
    }
 
    public void reportHours() {
        hourReporter.reportHours(this);
    }
}
  1. 개방-폐쇄 원칙 (Open-Closed Principle, OCP)

소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 합니다.

예제:

// Before
class Rectangle {
    public double width;
    public double height;
}
 
class AreaCalculator {
    public double calculateArea(Rectangle rectangle) {
        return rectangle.width * rectangle.height;
    }
}
 
// After
interface Shape {
    double calculateArea();
}
 
class Rectangle implements Shape {
    private double width;
    private double height;
 
    @Override
    public double calculateArea() {
        return width * height;
    }
}
 
class Circle implements Shape {
    private double radius;
 
    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }
}
  1. 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)

프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 합니다.

예제:

// 위반 예제
class Rectangle {
    protected int width;
    protected int height;
 
    public void setWidth(int width) {
        this.width = width;
    }
 
    public void setHeight(int height) {
        this.height = height;
    }
 
    public int getArea() {
        return width * height;
    }
}
 
class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(width);
    }
 
    @Override
    public void setHeight(int height) {
        super.setHeight(height);
        super.setWidth(height);
    }
}
 
// 개선된 예제
interface Shape {
    int getArea();
}
 
class Rectangle implements Shape {
    private int width;
    private int height;
 
    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }
 
    @Override
    public int getArea() {
        return width * height;
    }
}
 
class Square implements Shape {
    private int side;
 
    public Square(int side) {
        this.side = side;
    }
 
    @Override
    public int getArea() {
        return side * side;
    }
}
  1. 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)

클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 합니다.

예제:

// Before
interface Worker {
    void work();
    void eat();
}
 
// After
interface Workable {
    void work();
}
 
interface Eatable {
    void eat();
}
 
class Human implements Workable, Eatable {
    @Override
    public void work() {
        // ...
    }
 
    @Override
    public void eat() {
        // ...
    }
}
 
class Robot implements Workable {
    @Override
    public void work() {
        // ...
    }
}
  1. 의존관계 역전 원칙 (Dependency Inversion Principle, DIP)

고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 합니다.

예제:

// Before
class LightBulb {
    public void turnOn() {
        // ...
    }
 
    public void turnOff() {
        // ...
    }
}
 
class Switch {
    private LightBulb bulb;
 
    public Switch() {
        this.bulb = new LightBulb();
    }
 
    public void operate() {
        // ...
    }
}
 
// After
interface Switchable {
    void turnOn();
    void turnOff();
}
 
class LightBulb implements Switchable {
    @Override
    public void turnOn() {
        // ...
    }
 
    @Override
    public void turnOff() {
        // ...
    }
}
 
class Switch {
    private Switchable device;
 
    public Switch(Switchable device) {
        this.device = device;
    }
 
    public void operate() {
        // ...
    }
}

예상 면접 질문/답변

  1. Q: SOLID 원칙의 각 요소에 대해 간단히 설명해주세요. A: SOLID는 다음 다섯 가지 원칙의 약자입니다:

    • Single Responsibility Principle: 클래스는 단 하나의 책임만 가져야 합니다.
    • Open-Closed Principle: 소프트웨어 개체는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 합니다.
    • Liskov Substitution Principle: 하위 타입은 상위 타입을 대체할 수 있어야 합니다.
    • Interface Segregation Principle: 클라이언트는 자신이 사용하지 않는 인터페이스에 의존하지 않아야 합니다.
    • Dependency Inversion Principle: 고수준 모듈은 저수준 모듈에 의존하지 않아야 하며, 둘 다 추상화에 의존해야 합니다.
  2. Q: 단일 책임 원칙을 적용했을 때의 장점은 무엇인가요? A: 단일 책임 원칙을 적용하면 다음과 같은 장점이 있습니다:

    • 코드의 가독성과 유지보수성이 향상됩니다.
    • 각 클래스나 모듈의 역할이 명확해져 테스트가 용이해집니다.
    • 변경 사항이 발생했을 때 영향 범위를 최소화할 수 있습니다.
    • 코드 재사용성이 증가합니다.
  3. Q: 개방-폐쇄 원칙을 위반하는 코드의 예와 이를 개선하는 방법을 설명해주세요. A: 예를 들어, 도형의 면적을 계산하는 클래스가 있고 새로운 도형을 추가할 때마다 이 클래스를 수정해야 한다면 OCP를 위반하는 것입니다. 이를 개선하려면 도형에 대한 인터페이스를 정의하고, 각 도형 클래스가 이 인터페이스를 구현하도록 하면 됩니다. 이렇게 하면 새로운 도형을 추가할 때 기존 코드를 수정하지 않고도 확장할 수 있습니다.

  4. Q: 의존관계 역전 원칙을 적용할 때 주의해야 할 점은 무엇인가요? A: DIP를 적용할 때 주의해야 할 점은 다음과 같습니다:

    • 추상화 수준을 적절히 선택해야 합니다. 너무 세부적이거나 너무 일반적인 추상화는 오히려 복잡성을 증가시킬 수 있습니다.
    • 순환 의존성을 피해야 합니다.
    • 인터페이스를 남용하지 않도록 주의해야 합니다. 불필요한 인터페이스는 코드를 복잡하게 만들 수 있습니다.
    • 실제로 변경이 예상되는 부분에 대해서만 추상화를 적용해야 합니다.
  5. Q: SOLID 원칙을 적용한 리팩토링의 실제 효과는 어떤 것이 있나요? A: SOLID 원칙을 적용한 리팩토링의 실제 효과는 다음과 같습니다:

    • 코드의 유지보수성이 향상됩니다. 각 모듈의 책임이 명확해져 변경 사항 적용이 쉬워집니다.
    • 코드의 재사용성이 증가합니다. 잘 설계된 모듈은 다른 프로젝트에서도 쉽게 사용될 수 있습니다.
    • 테스트가 용이해집니다. 각 모듈의 책임이 명확하고 의존성이 잘 관리되어 단위 테스트 작성이 쉬워집니다.
    • 확장성이 좋아집니다. 새로운 기능 추가 시 기존 코드 수정을 최소화할 수 있습니다.
    • 버그 발생 가능성이 줄어듭니다. 코드의 구조가 개선되어 예상치 못한 부작용이 줄어듭니다.

스스로 찾아보면 좋은 연관 주제

  1. 디자인 패턴과 SOLID 원칙의 관계
  2. 애자일 개발 방법론에서의 리팩토링 적용 전략
  3. 레거시 코드 리팩토링 기법
  4. 테스트 주도 개발(TDD)과 리팩토링
  5. 코드 스멜(Code Smell)과 리팩토링