Notice
Recent Posts
Recent Comments
Link
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
Tags more
Archives
Today
Total
관리 메뉴

밤빵's 개발일지

[WIL]20240721 팀프로젝트 시작! 그리고 SOLID 본문

개발Article

[WIL]20240721 팀프로젝트 시작! 그리고 SOLID

최밤빵 2024. 7. 21. 16:40

SOLID 원칙에 대한 이해와 중요성

오늘 개발일지에는 객체 지향 설계의 핵심 원칙 중 하나인 SOLID 원칙에 대해 정리해봤다! 정재님과 기술매니저님께 종종 듣던거라 공부해야지하면서 미루다미루다 지금 정리하게되었다. SOLID 원칙은 유지보수성과 확장성이 뛰어난 소프트웨어를 설계하기 위한 다섯 가지의 원칙을 의미하고, 초보 개발자라도 반드시 이해하고 있어야 하는 중요한 개념이다. 각 원칙의 개념과 실제 개발에 어떻게 적용할 수 있는지에 대해 예시와 함께 정리했다! 

 

▶ SOLID 원칙이란?

SOLID는 객체 지향 프로그래밍에서 소프트웨어 설계를 유연하고 확장 가능하며 유지보수하기 쉽게 만드는 다섯 가지 원칙의 약어이다. 이 원칙은 견고하고 이해하기 쉬운 코드를 작성하는 데 도움을 준다. SOLID 원칙은 다음과 같다.

 

  • S: Single Responsibility Principle (단일 책임 원칙)
  • O: Open/Closed Principle (개방-폐쇄 원칙)
  • L: Liskov Substitution Principle (리스코프 치환 원칙)
  • I: Interface Segregation Principle (인터페이스 분리 원칙)
  • D: Dependency Inversion Principle (의존 역전 원칙)

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

단일 책임 원칙은 클래스는 하나의 책임만 가져야 한다는 원칙이다. 클래스는 하나의 기능이나 역할만 담당해야 하고, 해당 책임이 변경될 이유가 한 가지뿐이어야 한다는 의미이다.

→ 예시: Book 클래스. 이 클래스가 책의 정보(제목, 저자 등)를 다루는 역할 외에 책을 파일로 저장하거나 불러오는 기능까지 담당한다면, 단일 책임 원칙을 위반하는게 된다. 이러한 경우, 파일 저장과 관련된 로직을 별도의 BookFileManager 클래스로 분리하여 책임을 명확히 할 수 있다.

→ 장점: 클래스가 변경될 이유가 명확해지고, 수정 시 다른 코드에 영향을 미치지 않아서 유지보수성이 높아진다.

// 단일 책임 원칙 위반 예시
class Book {
    private String title;
    private String author;

    // 책 정보를 다루는 메서드
    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }

    // 파일로 저장하는 메서드
    public void saveToFile() {
        // 파일 저장 로직
    }
}

// 단일 책임 원칙을 따른 예시
class Book {
    private String title;
    private String author;

    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
}

class BookFileManager {
    public void saveToFile(Book book) {
        // 파일 저장 로직
    }
}

Book 클래스는 책 정보를 다루는 역할만 하고, BookFileManager 클래스는 파일 저장과 관련된 책임만 담당하도록 분리했다. ( 오늘 예시가 길어지게 될 것 같아서 클래스를 합쳐서 예시를 넣었다.)

 

▶ 개방-폐쇄 원칙 (Open/Closed Principle, OCP)

개방-폐쇄 원칙은 소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다는 원칙이다. 기존 코드를 수정하지 않고 기능을 확장할 수 있도록 설계해야 한다는 의미이다.

→ 예시: 새로운 종류의 결제 방법을 추가하려고 할 때 기존 PaymentProcessor 클래스를 수정하지 않고, PaymentProcessor 인터페이스를 구현하는 새로운 CreditCardPaymentProcessor, PayPalPaymentProcessor 클래스를 추가하면 된다.

→ 장점: 기존 코드를 수정하지 않으면서 기능을 확장할 수 있어서, 안정성을 보장하고 버그 발생 가능성을 줄인다.

// 개방-폐쇄 원칙 위반 예시
class PaymentProcessor {
    public void processCreditCardPayment() {
        // 신용카드 결제 처리 로직
    }

    public void processPayPalPayment() {
        // PayPal 결제 처리 로직
    }
}

// 개방-폐쇄 원칙을 따른 예시
interface PaymentProcessor {
    void processPayment();
}

class CreditCardPaymentProcessor implements PaymentProcessor {
    public void processPayment() {
        // 신용카드 결제 처리 로직
    }
}

class PayPalPaymentProcessor implements PaymentProcessor {
    public void processPayment() {
        // PayPal 결제 처리 로직
    }
}

새로운 결제 방법이 필요할 때 기존 코드를 수정하지 않고, PaymentProcessor 인터페이스를 구현하는 클래스를 추가함으로써 확장할 수 있다.

 

▶ 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)

리스코프 치환 원칙은 서브타입은 언제나 자신의 기반 타입으로 치환할 수 있어야 한다는 원칙이다. 자식 클래스는 부모 클래스의 행동을 대체할 수 있어야 하고, 이를 위반하면 안 된다.

→ 예시: Rectangle 클래스가 있고 이를 상속한 Square 클래스가 있다고 한다면, Square가 Rectangle의 메서드인 setWidth()와 setHeight()를 재정의하여 두 메서드가 동일하게 동작하도록 한다면, 이는 LSP를 위반하게 된다. 이 경우, 상속 구조를 변경하거나 메서드를 올바르게 설계해야 한다.

→ 장점: 프로그램이 더 직관적이고 예상 가능한 방식으로 동작하도록 보장하며, 코드의 가독성과 유지보수성을 높인다.

// 리스코프 치환 원칙 위반 예시
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 {
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(width); // 위반: 정사각형은 가로와 세로가 같아야 함
    }

    public void setHeight(int height) {
        super.setHeight(height);
        super.setWidth(height); // 위반: 정사각형은 가로와 세로가 같아야 함
    }
}

위 예시코드에서는 Square 클래스가 Rectangle의 동작을 변경하여 LSP를 위반하고 있다. 이를 해결하기 위해서는 상속 구조를 변경하거나 별도의 클래스를 사용하는 것이 좋다.

 

▶인터페이스 분리 원칙 (Interface Segregation Principle, ISP)

인터페이스 분리 원칙은 클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다는 원칙이다. 하나의 일반적인 인터페이스보다 여러 개의 구체적인 인터페이스를 설계해야 한다는 의미이다.

→ 예시: Worker 인터페이스에 work()와 eat() 메서드가 있다면, 이 인터페이스를 구현하는 RobotWorker 클래스는 eat() 메서드를 필요로 하지 않는다. 따라서 Worker 인터페이스를 Workable과 Eatable로 분리하여 각각의 인터페이스가 자신의 역할만 담당하도록 설계할 수 있다.

→ 장점: 인터페이스가 작고 구체적이어서 변경에 유연하고, 필요한 부분만 구현하도록 강제함으로써 코드의 명확성과 유지보수성을 높인다.

// 인터페이스 분리 원칙 위반 예시
interface Worker {
    void work();
    void eat();
}

class RobotWorker implements Worker {
    public void work() {
        // 작업 로직
    }

    public void eat() {
        // 로봇은 먹지 않음, 빈 메서드
    }
}

// 인터페이스 분리 원칙을 따른 예시
interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

class HumanWorker implements Workable, Eatable {
    public void work() {
        // 작업 로직
    }

    public void eat() {
        // 식사 로직
    }
}

class RobotWorker implements Workable {
    public void work() {
        // 작업 로직
    }
}

인터페이스를 Workable과 Eatable로 분리하여 각 클래스가 필요한 기능만 구현하도록 했다.

 

▶ 의존 역전 원칙 (Dependency Inversion Principle, DIP)

의존 역전 원칙은 고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다는 원칙이다. 또한, 추상화는 구체적인 것에 의존해서는 안 된다.

→ 예시: OrderService가 EmailService와 SmsService와 같은 구체적인 클래스에 의존하지 않고, NotificationService라는 인터페이스에 의존하게 한다면, EmailService나 SmsService를 쉽게 교체하거나 확장할 수 있다.

→ 장점: 코드의 결합도를 낮추고, 의존성을 쉽게 대체할 수 있으며, 확장 가능하고 유연한 설계를 가능하게 한다.

// 의존 역전 원칙 위반 예시
class EmailService {
    public void sendEmail(String message) {
        // 이메일 발송 로직
    }
}

class NotificationService {
    private EmailService emailService;

    public NotificationService() {
        this.emailService = new EmailService(); // 구체적인 클래스에 의존
    }

    public void send(String message) {
        emailService.sendEmail(message);
    }
}

// 의존 역전 원칙을 따른 예시
interface MessageService {
    void sendMessage(String message);
}

class EmailService implements MessageService {
    public void sendMessage(String message) {
        // 이메일 발송 로직
    }
}

class SmsService implements MessageService {
    public void sendMessage(String message) {
        // SMS 발송 로직
    }
}

class NotificationService {
    private MessageService messageService;

    public NotificationService(MessageService messageService) {
        this.messageService = messageService; // 추상화에 의존
    }

    public void send(String message) {
        messageService.sendMessage(message);
    }
}

NotificationService는 MessageService 인터페이스에 의존하며, 구체적인 구현체가 아닌 추상화에 의존하게 설계되었다.

 

▶ SOLID 원칙을 따를 때의 이점

→ 유지보수성: 코드가 명확하고 역할이 분리되어 있어 유지보수가 용이하다.

→ 확장성: 새로운 기능을 추가하거나 변경할 때 기존 코드를 수정하지 않고도 쉽게 확장할 수 있다.

→ 재사용성: 잘 설계된 모듈과 클래스는 재사용하기 쉬우며, 다른 프로젝트나 모듈에서도 활용할 수 있다.

→ 안정성: 코드의 변화가 제한적이므로, 버그 발생 가능성이 줄어든다.

 

▶정리 

SOLID 원칙의 개념과 중요성, 그리고 각 원칙이 실제 개발에서 어떻게 적용될 수 있는지에 대해서 여러번 보는데도 어려웠다. SOLID 원칙을 잘 준수하면 유지보수성과 확장성이 뛰어난 소프트웨어를 개발할 수 있다. 코드를 작성하면서 이 원칙들을 지켜야하는데, 원칙 이름들도 너무 어려워서 이해하려면 더 공부를 해야 할 것 같다. 

'개발Article' 카테고리의 다른 글

[TIL]20240723 Hibernate  (0) 2024.07.23
[TIL]20240722 DI & IOC  (0) 2024.07.22
[TIL]20240720 ORM...?  (0) 2024.07.21
[TIL]20240719 Stream()을 쓴 이유 & 주의사항  (0) 2024.07.19
[TIL]20240718 /api를 두는 이유  (0) 2024.07.19