디자인 패턴 & OOP
[OOP] SOLID 객체 지향 설계 5가지 원칙
cloud-grace
2024. 6. 2. 14:37
SOLID란? 객체 지향 설계 5가지 원칙
SOLID 원칙이란, 객체 지향 프로그래밍에서 소프트웨어 설계를 더 이해하기 쉽고 유지보수가 용이하며 확장 가능하게 만드는 다섯 가지 기본 원칙이다. 이 원칙들은 코드 품질을 높이고, 코드 변경 시 발생할 수 있는 오류를 최소화하는 데 도움을 준다. 그리고 디자인 패턴은 SOLID 원칙을 기반으로 만들어진 것이다.
1. SRP(Single Responsibility Principle) 단일 책임 원칙
- 클래스(객체)는 단 하나의 책임(기능)만 가져야 하며, 변경되어야 하는 이유가 오직 하나뿐이어야 한다는 원칙이다.
- 하나의 클래스 : 하나의 기능 담당
- 하나의 클래스에 여러 기능이 있다면 수정 시 변경 사항이 많아지며 가독성 및 유지보수성이 떨어진다.
- 클래스가 변경되는 이유는 오직 하나 뿐이어야 한다.
- 만약, 클래스가 여러 기능을 가지고 있다면 여러 액터에게 변경 요구를 받고 수정할 사항이 여러 개가 된다.
SRP 단일 책임 원칙 위반 예제 코드
- Employee 클래스는 직원 데이터 관리, 월급 계산, 보고서 작성 등 여러 책임을 가지고 있다.
class Employee {
private String name;
private String position;
public Employee(String name, String position) {
this.name = name;
this.position = position;
}
public void calculateSalary() {
// Salary calculation logic
}
public void generateReport() {
// Report generation logic
}
}
SRP 단일 책임 원칙에 맞춰 수정
- Employee 클래스는 직원 데이터 관리에 집중하며, 월급 계산 및 보고서 작성은 각각 클래스로 분리한다.
class Employee {
private String name;
private String position;
public Employee(String name, String position) {
this.name = name;
this.position = position;
}
}
class SalaryCalculator {
public void calculateSalary(Employee employee) {
// Salary calculation logic
}
}
class ReportGenerator {
public void generateReport(Employee employee) {
// Report generation logic
}
}
2. OCP(Open Closed Principle) 개방 폐쇄 원칙
- 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다는 원칙이다.
- 새로운 기능을 추가할 때 기존 코드를 수정하지 않도록 설계하는 것을 의미한다.
- 즉, 기능이 추가될 때 확장은 손쉽게, 수정은 최소화하도록 설계해야 한다.
- 따라서 추상화를 사용하여 관계를 구축하고 다형성과 확장을 통해 객체 지향을 극대화한다.
OCP 개방 폐쇄 원칙 위반 예제 코드
- 새로운 도형을 추가하려면 Shape 클래스를 수정해야 한다.
class Shape {
public void draw(String shapeType) {
if (shapeType.equals("Circle")) {
drawCircle();
} else if (shapeType.equals("Rectangle")) {
drawRectangle();
}
}
private void drawCircle() {
// Draw Circle
}
private void drawRectangle() {
// Draw Rectangle
}
}
OCP 개방 폐쇄 원칙에 맞춰 수정
- 이제 새로운 도형을 추가하려면 Shape 클래스를 상속 받아 새로운 도형 클래스를 구현하면 된다.
abstract class Shape {
public abstract void draw();
}
class Circle extends Shape {
@Override
public void draw() {
// Draw Circle
}
}
class Rectangle extends Shape {
@Override
public void draw() {
// Draw Rectangle
}
}
3. LSP(Liskov Substitution Principle) 리스코프 치환 원칙
- 자식 클래스는 언제나 자신의 부모 클래스를 대체할 수 있어야 한다.
- 이는 자식 클래스가 부모 클래스의 기능을 확장하더라도 기본적인 계약을 준수해야 함을 의미한다.
- 따라서 해당 객체를 사용하는 클라이언트는 상위 타입이 하위 타입으로 바뀌어도 상위 타입의 public interface를 통해 사용할 수 있어야 한다.
- 즉, 클라이언트와 객체 사이 계약이 존재하고 이를 준수해야 한다.
LSP 리스코프 치환 원칙 위반 예제 코드
- Square 클래스는 Rectangle 클래스를 상속받지만, 직사각형 Rectangle의 계약을 준수하지 않는다.
class Rectangle {
private int width;
private 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.setWidth(height);
super.setHeight(height);
}
}
LSP 리스코프 치환 원칙에 맞춰 수정
- 이제 Square와 Rectangle은 공통의 Shape 클래스를 상속 받아 각각의 면적 계산 방법을 제공한다.
abstract class Shape {
public abstract int getArea();
}
class Rectangle extends Shape {
private int width;
private int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
@Override
public int getArea() {
return width * height;
}
}
class Square extends Shape {
private int side;
public void setSide(int side) {
this.side = side;
}
@Override
public int getArea() {
return side * side;
}
}
4. ISP(Interface Segregation Principle) 인터페이스 분리 원칙
- 클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.
- 이는 큰 인터페이스를 작고 구체적인 인터페이스로 분리하는 것을 의미한다.
- 목적과 관심에 맞는 인터페이스만 클라이언트에게 제공하도록 인터페이스의 분리가 필요하다.
ISP 인터페이스 분리 원칙 위반 예제 코드
- Robot 클래스는 eat 메서드를 구현할 필요가 없다.
interface Worker {
void work();
void eat();
}
class Developer implements Worker {
@Override
public void work() {
// Work
}
@Override
public void eat() {
// Eat
}
}
class Robot implements Worker {
@Override
public void work() {
// Work
}
@Override
public void eat() {
// Do nothing (robots don't eat)
}
}
ISP 인터페이스 분리 원칙에 맞춰 수정
- 이제 Robot 클래스는 필요 없는 eat 메서드를 구현할 필요가 없다.
interface Workable {
void work();
}
interface Eatable {
void eat();
}
class Developer implements Workable, Eatable {
@Override
public void work() {
// Work
}
@Override
public void eat() {
// Eat
}
}
class Robot implements Workable {
@Override
public void work() {
// Work
}
}
5. DIP(Dependency Inversion Principle) 의존 역전 원칙
- 고수준 모듈은 저수준 모듈에 의존해서는 안 된다.
- 둘 다 추상화에 의존해야 한다.
- 이는 구체적인 구현이 아닌 추상화에 의존하도록 만드는 것을 의미한다.
- 즉, 어떤 클래스를 참조해야 한다면, 직접 참조가 아닌 그 클래스의 상위 요소(abstract class, interface)로 참조해야 한다.
DIP 의존 역전 원칙 위반 예제 코드
- Switch 클래스는 LightBulb 라는 구체적인 클래스에 의존한다.
class LightBulb {
public void turnOn() {
// Turn on the light
}
public void turnOff() {
// Turn off the light
}
}
class Switch {
private LightBulb lightBulb;
public Switch(LightBulb lightBulb) {
this.lightBulb = lightBulb;
}
public void operate() {
lightBulb.turnOn();
}
}
DIP 의존 역전 원칙에 맞춰 수정
- 이제 Switch 클래스는 Switchable 인터페이스에 의존하며, 이는 구체적인 구현이 아닌 추상화에 의존하도록 만든다.
interface Switchable {
void turnOn();
void turnOff();
}
class LightBulb implements Switchable {
@Override
public void turnOn() {
// Turn on the light
}
@Override
public void turnOff() {
// Turn off the light
}
}
class Switch {
private Switchable device;
public Switch(Switchable device) {
this.device = device;
}
public void operate() {
device.turnOn();
}
}
참고 자료
https://inpa.tistory.com/entry/OOP-%F0%9F%92%A0-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%EC%84%A4%EA%B3%84%EC%9D%98-5%EA%B0%80%EC%A7%80-%EC%9B%90%EC%B9%99-SOLID
https://mangkyu.tistory.com/194