개요
SOLID 원칙은 객체 지향 프로그래밍 및 설계의 다섯 가지 기본 원칙을 말합니다. 이 원칙들을 적용하여 리팩토링을 수행하면 더 유지보수하기 쉽고, 유연하며, 확장 가능한 소프트웨어를 만들 수 있습니다.
상세
SOLID 원칙의 각 요소와 그에 따른 리팩토링 기법을 살펴보겠습니다:
- 단일 책임 원칙 (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);
}
}- 개방-폐쇄 원칙 (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;
}
}- 리스코프 치환 원칙 (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;
}
}- 인터페이스 분리 원칙 (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() {
// ...
}
}- 의존관계 역전 원칙 (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() {
// ...
}
}예상 면접 질문/답변
-
Q: SOLID 원칙의 각 요소에 대해 간단히 설명해주세요. A: SOLID는 다음 다섯 가지 원칙의 약자입니다:
- Single Responsibility Principle: 클래스는 단 하나의 책임만 가져야 합니다.
- Open-Closed Principle: 소프트웨어 개체는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 합니다.
- Liskov Substitution Principle: 하위 타입은 상위 타입을 대체할 수 있어야 합니다.
- Interface Segregation Principle: 클라이언트는 자신이 사용하지 않는 인터페이스에 의존하지 않아야 합니다.
- Dependency Inversion Principle: 고수준 모듈은 저수준 모듈에 의존하지 않아야 하며, 둘 다 추상화에 의존해야 합니다.
-
Q: 단일 책임 원칙을 적용했을 때의 장점은 무엇인가요? A: 단일 책임 원칙을 적용하면 다음과 같은 장점이 있습니다:
- 코드의 가독성과 유지보수성이 향상됩니다.
- 각 클래스나 모듈의 역할이 명확해져 테스트가 용이해집니다.
- 변경 사항이 발생했을 때 영향 범위를 최소화할 수 있습니다.
- 코드 재사용성이 증가합니다.
-
Q: 개방-폐쇄 원칙을 위반하는 코드의 예와 이를 개선하는 방법을 설명해주세요. A: 예를 들어, 도형의 면적을 계산하는 클래스가 있고 새로운 도형을 추가할 때마다 이 클래스를 수정해야 한다면 OCP를 위반하는 것입니다. 이를 개선하려면 도형에 대한 인터페이스를 정의하고, 각 도형 클래스가 이 인터페이스를 구현하도록 하면 됩니다. 이렇게 하면 새로운 도형을 추가할 때 기존 코드를 수정하지 않고도 확장할 수 있습니다.
-
Q: 의존관계 역전 원칙을 적용할 때 주의해야 할 점은 무엇인가요? A: DIP를 적용할 때 주의해야 할 점은 다음과 같습니다:
- 추상화 수준을 적절히 선택해야 합니다. 너무 세부적이거나 너무 일반적인 추상화는 오히려 복잡성을 증가시킬 수 있습니다.
- 순환 의존성을 피해야 합니다.
- 인터페이스를 남용하지 않도록 주의해야 합니다. 불필요한 인터페이스는 코드를 복잡하게 만들 수 있습니다.
- 실제로 변경이 예상되는 부분에 대해서만 추상화를 적용해야 합니다.
-
Q: SOLID 원칙을 적용한 리팩토링의 실제 효과는 어떤 것이 있나요? A: SOLID 원칙을 적용한 리팩토링의 실제 효과는 다음과 같습니다:
- 코드의 유지보수성이 향상됩니다. 각 모듈의 책임이 명확해져 변경 사항 적용이 쉬워집니다.
- 코드의 재사용성이 증가합니다. 잘 설계된 모듈은 다른 프로젝트에서도 쉽게 사용될 수 있습니다.
- 테스트가 용이해집니다. 각 모듈의 책임이 명확하고 의존성이 잘 관리되어 단위 테스트 작성이 쉬워집니다.
- 확장성이 좋아집니다. 새로운 기능 추가 시 기존 코드 수정을 최소화할 수 있습니다.
- 버그 발생 가능성이 줄어듭니다. 코드의 구조가 개선되어 예상치 못한 부작용이 줄어듭니다.
스스로 찾아보면 좋은 연관 주제
- 디자인 패턴과 SOLID 원칙의 관계
- 애자일 개발 방법론에서의 리팩토링 적용 전략
- 레거시 코드 리팩토링 기법
- 테스트 주도 개발(TDD)과 리팩토링
- 코드 스멜(Code Smell)과 리팩토링