현대 소프트웨어 개발에서 SOLID 원칙은 필수적인 지침으로 자리잡았습니다. 이 원칙들은 로버트 마틴(Robert C. Martin)에 의해 제안되었으며, 유지보수가 용이하고 확장 가능한 소프트웨어를 설계하는 데 중요한 역할을 합니다. SOLID 원칙을 따르면 코드의 품질이 향상되고, 버그 발생 가능성이 줄어들며, 새로운 기능 추가가 쉬워집니다.
SOLID 원칙이 현대 개발에서 필수적인 이유는 다음과 같습니다
- 유지보수성 향상: SOLID 원칙을 따르면 코드의 구조가 명확해지고 각 컴포넌트의 책임이 분명해집니다. 이는 향후 코드 수정이나 버그 수정 시 작업을 훨씬 쉽게 만듭니다.
- 확장성 증대: 새로운 기능을 추가하거나 기존 기능을 변경할 때, SOLID 원칙을 따른 코드는 최소한의 수정으로 이를 가능하게 합니다.
- 테스트 용이성: 각 컴포넌트가 단일 책임을 가지고 있어 단위 테스트를 작성하고 실행하기가 더 쉬워집니다.
- 재사용성 증가: 잘 설계된 컴포넌트는 다른 프로젝트에서도 쉽게 재사용될 수 있습니다.
- 협업 효율성: 팀 멤버들이 일관된 설계 원칙을 따르면 코드 이해와 협업이 더욱 원활해집니다.
이제 각 원칙에 대해 자세히 살펴보겠습니다.
1. 단일 책임 원칙 (Single Responsibility Principle, SRP)
단일 책임 원칙은 "한 클래스는 하나의 책임만 가져야 한다"는 원칙입니다. 이는 클래스가 변경되어야 하는 이유가 오직 하나여야 함을 의미합니다[1].
예를 들어, 사용자 관리 시스템에서 다음과 같은 클래스가 있다고 가정해봅시다
public class User {
private String name;
private String email;
public User(String name, String email) {
this.name = name;
this.email = email;
}
public void saveUser() {
// 데이터베이스에 사용자 저장
}
public void sendEmail() {
// 사용자에게 이메일 전송
}
}
이 클래스는 SRP를 위반하고 있습니다. 사용자 정보 관리와 이메일 전송이라는 두 가지 책임을 가지고 있기 때문입니다.
이를 SRP에 맞게 리팩토링하면
public class User {
private String name;
private String email;
public User(String name, String email) {
this.name = name;
this.email = email;
}
}
public class UserRepository {
public void saveUser(User user) {
// 데이터베이스에 사용자 저장
}
}
public class EmailService {
public void sendEmail(User user) {
// 사용자에게 이메일 전송
}
}
이렇게 분리함으로써 각 클래스는 단일 책임을 가지게 되며, 변경 사유도 하나로 제한됩니다[1].
2. 개방-폐쇄 원칙 (Open-Closed Principle, OCP)
개방-폐쇄 원칙은 "소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 대해서는 열려 있어야 하지만 수정에 대해서는 닫혀 있어야 한다"는 원칙입니다[2].
예를 들어, 도형의 면적을 계산하는 시스템을 생각해봅시다
public class Rectangle {
public double width;
public double height;
}
public class Circle {
public double radius;
}
public class AreaCalculator {
public double calculateArea(Object shape) {
if (shape instanceof Rectangle) {
Rectangle rectangle = (Rectangle) shape;
return rectangle.width * rectangle.height;
} else if (shape instanceof Circle) {
Circle circle = (Circle) shape;
return Math.PI * circle.radius * circle.radius;
}
return 0;
}
}
이 설계는 OCP를 위반합니다.
새로운 도형을 추가할 때마다 AreaCalculator 클래스를 수정해야 하기 때문입니다. OCP를 적용하면
public interface Shape {
double calculateArea();
}
public class Rectangle implements Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double calculateArea() {
return width * height;
}
}
public class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}
public class AreaCalculator {
public double calculateArea(Shape shape) {
return shape.calculateArea();
}
}
이제 새로운 도형을 추가하더라도 AreaCalculator 클래스를 수정할 필요가 없습니다[2].
3. 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)
리스코프 치환 원칙은 "프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다"는 원칙입니다[2].
예를 들어, 직사각형과 정사각형 관계를 생각해봅시다:
public 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;
}
}
public 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의 행동을 완전히 대체할 수 없기 때문입니다.
LSP를 지키려면
public interface Shape {
int getArea();
}
public 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;
}
}
public class Square implements Shape {
private int side;
public Square(int side) {
this.side = side;
}
@Override
public int getArea() {
return side * side;
}
}
이제 Square와 Rectangle은 독립적인 클래스로, Shape 인터페이스를 통해 다형성을 유지합니다[2].
4. 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)
인터페이스 분리 원칙은 "클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안 된다"는 원칙입니다[2].
예를 들어, 다기능 프린터 인터페이스를 생각해봅시다
public interface MultiFunctionPrinter {
void print();
void scan();
void fax();
}
public class AllInOnePrinter implements MultiFunctionPrinter {
public void print() { /* 인쇄 기능 */ }
public void scan() { /* 스캔 기능 */ }
public void fax() { /* 팩스 기능 */ }
}
public class SimplePrinter implements MultiFunctionPrinter {
public void print() { /* 인쇄 기능 */ }
public void scan() { throw new UnsupportedOperationException(); }
public void fax() { throw new UnsupportedOperationException(); }
}
이 설계는 ISP를 위반합니다. SimplePrinter가 사용하지 않는 메서드를 구현해야 하기 때문입니다.
ISP를 적용하면:
public interface Printer {
void print();
}
public interface Scanner {
void scan();
}
public interface Fax {
void fax();
}
public class AllInOnePrinter implements Printer, Scanner, Fax {
public void print() { /* 인쇄 기능 */ }
public void scan() { /* 스캔 기능 */ }
public void fax() { /* 팩스 기능 */ }
}
public class SimplePrinter implements Printer {
public void print() { /* 인쇄 기능 */ }
}
이제 각 클래스는 필요한 인터페이스만 구현하게 됩니다[2].
5. 의존관계 역전 원칙 (Dependency Inversion Principle, DIP)
의존관계 역전 원칙은 "고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 된다. 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다"는 원칙입니다[2].
예를 들어, 데이터베이스와 상호작용하는 시스템을 생각해봅시다
public class MySqlDatabase {
public void insert(String data) {
// MySQL에 데이터 삽입
}
}
public class UserService {
private MySqlDatabase database;
public UserService() {
this.database = new MySqlDatabase();
}
public void addUser(String userData) {
database.insert(userData);
}
}
이 설계는 DIP를 위반합니다. UserService가 구체적인 MySqlDatabase에 의존하고 있기 때문입니다.
DIP를 적용하면:
public interface Database {
void insert(String data);
}
public class MySqlDatabase implements Database {
public void insert(String data) {
// MySQL에 데이터 삽입
}
}
public class UserService {
private Database database;
public UserService(Database database) {
this.database = database;
}
public void addUser(String userData) {
database.insert(userData);
}
}
이제 UserService는 추상화된 Database 인터페이스에 의존하게 되어, 다양한 데이터베이스 구현체를 사용할 수 있게 됩니다[2].
마무리
SOLID 원칙은 객체지향 설계의 핵심 지침으로, 코드의 유지보수성, 확장성, 재사용성을 크게 향상시킵니다. 이 원칙들을 적용함으로써 개발자는 더 견고하고 유연한 소프트웨어를 만들 수 있습니다.
하지만 SOLID 원칙을 맹목적으로 따르는 것은 주의해야 합니다. 때로는 과도한 추상화나 복잡성 증가로 이어질 수 있기 때문입니다. 따라서 프로젝트의 규모, 요구사항, 팀의 역량 등을 고려하여 적절히 적용하는 것이 중요합니다.
또한, SOLID 원칙은 단순히 코드 작성 기술을 넘어 소프트웨어 설계 철학을 담고 있습니다. 이 원칙들을 이해하고 적용하는 과정에서 개발자는 더 나은 설계 결정을 내릴 수 있는 능력을 기르게 됩니다.
마지막으로, SOLID 원칙은 다른 디자인 패턴이나 아키텍처 원칙들과 함께 사용될 때 더욱 강력한 효과를 발휘합니다. 예를 들어, 의존성 주입(Dependency Injection)이나 팩토리 패턴(Factory Pattern) 등과 결합하여 사용하면 더욱 유연하고 테스트 가능한 코드를 작성할 수 있습니다.
결론적으로, SOLID 원칙은 현대 소프트웨어 개발에서 필수적인 도구입니다. 이를 올바르게 이해하고 적용함으로써, 개발자는 더 나은 코드를 작성하고, 더 효율적인 개발 프로세스를 구축
Citations:
[1] 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
[2] https://hongjinhyeon.tistory.com/139
[3] https://mangkyu.tistory.com/194
[4] https://woodadada16.tistory.com/17
[5] https://justkode.kr/java/solid-pattern/
[6] https://ayaan-dev.tistory.com/14
[7] https://velog.io/@dkwktm45/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-%EA%B0%9C%EB%B0%9C-5%EB%8C%80-%EC%9B%90%EB%A6%AC%EB%A5%BC-%ED%8C%8C%ED%97%A4%EC%B3%90-%EB%B3%B4%EC%9E%90
[8] https://growth-msleeffice.tistory.com/144
[9] https://www.nextree.co.kr/p6960/