SOLID 원칙은 객체 지향 설계의 다섯 가지 기본 원칙을 나타내며, 소프트웨어 설계에서 유지보수성과 확장성을 개선하는 데 중점을 둡니다. 각 원칙은 객체 지향 프로그래밍에서 코드의 품질을 높이기 위해 고안되었습니다. 아래는 SOLID 원칙을 Java 코드 예제와 함께 설명한 내용입니다.
1. S: 단일 책임 원칙 (Single Responsibility Principle)
원칙: 하나의 클래스는 하나의 책임만 가져야 하며, 그 책임은 변경의 이유가 되어야 합니다.
예시:
// 잘못된 예시: Book 클래스가 데이터를 저장하고 출력까지 책임지고 있음.
class Book {
private String title;
private String author;
// 저장 책임
public void save() {
// 데이터베이스에 책 정보를 저장하는 코드
}
// 출력 책임
public void print() {
System.out.println(title + " by " + author);
}
}
// 개선된 예시
class Book {
private String title;
private String author;
public String getTitle() { return title; }
public String getAuthor() { return author; }
}
// 별도의 클래스가 출력 책임을 담당
class BookPrinter {
public void print(Book book) {
System.out.println(book.getTitle() + " by " + book.getAuthor());
}
}
2. O: 개방-폐쇄 원칙 (Open/Closed Principle)
원칙: 소프트웨어 엔터티(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다.
예시:
// 잘못된 예시: 기존 클래스의 코드를 수정해야만 새로운 기능을 추가할 수 있음.
class Rectangle {
public double length;
public double width;
}
class AreaCalculator {
public double calculateRectangleArea(Rectangle rectangle) {
return rectangle.length * rectangle.width;
}
}
// 개선된 예시: 새로운 도형이 추가되어도 기존 코드를 수정할 필요가 없음.
interface Shape {
double area();
}
class Rectangle implements Shape {
private double length;
private double width;
public Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
@Override
public double area() {
return length * width;
}
}
class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
class AreaCalculator {
public double calculateArea(Shape shape) {
return shape.area();
}
}
3. L: 리스코프 치환 원칙 (Liskov Substitution Principle)
원칙: 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서, 자식 클래스 객체로 치환할 수 있어야 한다.
예시:
// 잘못된 예시: Square가 Rectangle을 상속받았지만, Rectangle의 행동 규약을 깨뜨림.
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) {
this.width = width;
this.height = width; // 높이와 너비가 같아야 함
}
@Override
public void setHeight(int height) {
this.width = height;
this.height = height;
}
}
// Square는 Rectangle을 대체할 수 없으므로 LSP를 위반함
해결책: 상속 구조를 재고하거나 상속 대신 별도의 클래스로 구현합니다.
4. I: 인터페이스 분리 원칙 (Interface Segregation Principle)
원칙: 클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다. 인터페이스는 작고 구체적인 단위로 분리되어야 한다.
예시:
// 잘못된 예시: Interface가 너무 많아 모든 메서드를 구현할 필요가 없음.
interface Worker {
void work();
void eat();
}
class Developer implements Worker {
@Override
public void work() {
// 코딩 작업
}
@Override
public void eat() {
// 개발자도 먹기는 하지만, 이 메서드는 무의미할 수 있음
}
}
// 개선된 예시: 인터페이스를 분리함으로써 필요한 부분만 구현하도록 함.
interface Workable {
void work();
}
interface Eatable {
void eat();
}
class Developer implements Workable {
@Override
public void work() {
// 코딩 작업
}
}
class OfficeWorker implements Workable, Eatable {
@Override
public void work() {
// 사무 작업
}
@Override
public void eat() {
// 식사
}
}
5. D: 의존성 역전 원칙 (Dependency Inversion Principle)
원칙: 고수준 모듈은 저수준 모듈에 의존해서는 안 된다. 둘 다 추상화에 의존해야 한다. 또한, 추상화는 구체적인 것에 의존해서는 안 된다. 구체적인 것이 추상화에 의존해야 한다.
예시:
// 잘못된 예시: High-level 모듈이 Low-level 모듈에 의존하고 있음.
class LightBulb {
public void turnOn() {
System.out.println("LightBulb: Bulb turned on...");
}
public void turnOff() {
System.out.println("LightBulb: Bulb turned off...");
}
}
class Switch {
private LightBulb lightBulb;
public Switch(LightBulb lightBulb) {
this.lightBulb = lightBulb;
}
public void operate() {
lightBulb.turnOn();
}
}
// 개선된 예시: 인터페이스를 사용하여 추상화에 의존하게 함.
interface Switchable {
void turnOn();
void turnOff();
}
class LightBulb implements Switchable {
@Override
public void turnOn() {
System.out.println("LightBulb: Bulb turned on...");
}
@Override
public void turnOff() {
System.out.println("LightBulb: Bulb turned off...");
}
}
class Switch {
private Switchable device;
public Switch(Switchable device) {
this.device = device;
}
public void operate() {
device.turnOn();
}
}
요약
- SRP: 클래스는 하나의 책임만 가져야 한다.
- OCP: 기존 코드를 수정하지 않고 기능을 확장할 수 있어야 한다.
- LSP: 자식 클래스는 부모 클래스를 대체할 수 있어야 한다.
- ISP: 클라이언트는 사용하지 않는 메서드에 의존하지 않아야 한다.
- DIP: 고수준 모듈은 저수준 모듈에 의존해서는 안 된다.
이 원칙들을 준수하면 코드를 더 유연하고 유지보수하기 쉽게 만들 수 있습니다.
Share article