SOLID 원칙

2025. 4. 11. 13:58·DEVELOP/CONCEPT

객체 지향 설계 5원칙

  • SRP(Single Responsibility Principle): 단일 책임 원칙
  • OCP(Open Closed Principle): 개방 폐쇄 원칙
  • LSP(Listov Substitution Principle): 리스코프 치환 원칙
  • ISP(Interface Segregation Principle): 인터페이스 분리 원칙
  • DIP(Dependency Inversion Principle): 의존 역전 원칙

 

🤔 SOLID 원칙을 지켜야 하는 이유?

시간이 지나도 변경이 용이하고, 유지보수와 확장이 쉬운 소프트웨어를 개발하는 데 도움이 되기 때문이다. SOLID의 핵심은 결국 추상화와 다형성이다. 구체 클래스에 의존하지 않고 추상 클래스(또는 인터페이스)에 의존함으로써 우리는 유연하고 확장가능한 애플리케이션을 만들 수 있는 것이다.

 

SRP: 단일 책임 원칙

모듈이 변경되는 이유가 한가지여야 한다.

 

 만약 모듈(클래스 or 클래스의 모음)이 여러 액터에 대해 책임을 가지고 있다면 여러 액터들로부터 변경에 대한 요구가 올 수 있으므로, 해당 모듈을 수정해야 하는 이유 역시 여러 개가 될 수 있다. 반면 어떤 모듈이 단 하나의 책임만을 가지고 있다면, 어떤 변경이 필요할 때 그 책임에 관련된 변경 주체를 명확히 특정할 수 있다. 즉, 수정해야할 이유와 시점이 분명해진다.

 단일 책임 원칙을 제대로 지키면 변경이 필요할 때 수정할 대상이 한정되고 명확해진다. 그리고 이러한 단일 책임의 원칙의 장점은 시스템이 커질수록 두드러진다. 시스템이 커지면 모듈 간의 의존성도 증가하게 되는데, 이때 SRP를 잘 지켜두면 변경이 발생하더라도 딱 하나의 클래스만 수정하면 되기 때문이다.

 예를 들어, 하나의 클래스가  사용자 생성, DB 저장, 이메일 전송이라는 3가지 책임을 동시에 가지고 있는다고 하자. 그러면 하나만 바꿔도 클래스 전체를 건드려야 하는 상황이 발생한다. 이러면 유지보수가 매우매우 어렵다. 따라서 SRP는 이러한 문제를 방지하기 위해, 각각의 책임을 별도의 클래스로 분리해서 구현하자는 의도인 것이다. 

 하지만 현실에서는 어떤 클래스가 SRP를 위반하고 있는지 판단하기 어려운 경우도 많다. 왜냐하면 동일한 기능이라도 '어떤 관점에서 바라보느냐'에 따라 책임의 기준이 달라질 수 있기 때문이다. 여기서의 관점이란, 시스템의 유스케이스와 요구사항에 따라 해당 클래스가 어떤 역할을 맡고 있는지를 보는 시각을 말한다.

 

OCP: 개방 폐쇄 원칙

소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.

 

이 말은 요구사항이 변경될 때 새로운 동작을 추가하여 애플리케이션의 기능을 확장할 수 있으나 기존의 코드를 수정하지 않고 애플리케이션의 동작을 추가하거나 변경할 수 있어야 한다는 원칙이다. 즉, 새로운 기능은 '추가'해서 구현하고, 기존 코드는 건들지 말라는 것이다. 개방 폐쇄 원칙을 지키기 위해서는 추상화에 의존해야 하는데, 아래 코드를 보면 이해할 수 있을 것이다.

 

// 추상화 인터페이스
public interface DiscountPolicy {
    int getDiscountPrice(int price);
}

// 구현체 만들기
@Component
public class SilverDiscountPolicy implements DiscountPolicy {
    public int getDiscountPrice(int price) {
        return price - 1000;
    }
}

@Component
public class GoldDiscountPolicy implements DiscountPolicy {
    public int getDiscountPrice(int price) {
        return price - 3000;
    }
}

 

LSP: 리스코프 치환 원칙

프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.

 

이 말은 자식 클래스는 언제나 부모 클래스로 대체 가능해야 한다는 뜻이다. 개념이 추상적인데, 쉽게 말하면 자식 클래스는 부모 클래스의 동작 규약을 그대로 따르거나, 확장만 해야 한다는 것이다. 이는 부모 클래스의 기능을 수정해선 안된다는 원칙히며, 부모의 기능을 무효화하거나 제한한다면 LSP에 위반하게 된다. 따라서, 자식 클래스는 부모 클래스보다 더 구체적인 동작을 하게끔 설계를 할 뿐 아래 코드와 같이 부모 클래스의 기능을 무효화하면 안된다.

 

 [ LSP 위반한 코드 ]

class Bird {
    public void fly() {
        System.out.println("날아갑니다!");
    }
}

class Ostrich extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("타조는 날 수 없습니다!");
    }
}

 

 

ISP: 인터페이스 분리 원칙

특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.

 

클라이언트의 목적과 용도에 따라 적합한 인터페이스만 제공해야 한다는 원칙이다. 이 원칙을 준수하면 모든 클라이언트가 자신의 관심에 맞는 퍼블릭 인터페이스(외부에서 접근 가능한 메세지)만을 접근하여 불필요한 간섭을 최소화할 수 있으며, 기존 클라이언트에 영향을 주지 않은 채로 유연하게 객체의 기능을 확장하거나 수정할 수 있다.

 

  [ ISP 위반 코드 ]

interface Worker {
    void work();
    void eat();
}

class HumanWorker implements Worker {
    public void work() { System.out.println("일함"); }
    public void eat() { System.out.println("밥 먹음"); }
}

class RobotWorker implements Worker {
    public void work() { System.out.println("로봇 일함"); }
    public void eat() {
        throw new UnsupportedOperationException("로봇은 밥 안 먹어요 ❌");
    }
}

 

RobotWorker을 보면 eat() 기능을 사용하지 않지만 Worker 인터페이스 때문에 강제 구현이 되었다. 즉, 불필요한 의존이 발생하였고 이는 ISP를 위반한 코드라고 할 수 있다.

 

  [ ISP 적용 코드 ]

interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

class HumanWorker implements Workable, Eatable {
    public void work() { System.out.println("사람 일함"); }
    public void eat() { System.out.println("사람 밥 먹음"); }
}

class RobotWorker implements Workable {
    public void work() { System.out.println("로봇 일함"); }
}

 

대신 위와 같은 구조로 구성한다면 인터페이스를 작게 나눴기 때문에 로봇은 일만 하면 되고 사람은 일도 하고 밥도 먹을 수 있을 것이다. 각각 필요한 기능만 구현하여 유지보수성을 향상시키기 위해 ISP 원칙을 꼭 지켜야 한다.

 

DIP: 의존관계 역전 원칙

프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다.

 

일단 의존한다는게 무엇일까? 의존한다는 말은 한 서비스가 다른 서비스의 기능이나 존재를 필요로 할 때를 말한다. 즉 아래 코드를 보면 UserService는 UserRepository 없이는 못 돌아간다. 이런 걸 UserService가 UserRepository에 의존한다고 말한다. 

class UserService {
    private UserRepository userRepository;

    public UserService() {
        this.userRepository = new UserRepository(); // service는 repository에 의존한다.
    }

    public void register(User user) {
        userRepository.save(user);
    }
}

 

의존관계 역전 원칙은 쉽게 말해 구현 말고 추상화된 인터페이스에 의존해야 한다는 원칙이다. 대표적으로 의존성 주입은 이 원칙을 따르는 방법 중 하나다. 아래 예시를 통해 이해해보자.

// 추상화된 인터페이스
public interface GameConsole {
	void play();
}

// 구현 클래스 (저수준 모듈)
import org.springframework.stereotype.Component;

@Component
public class NintendoSwitch implements GameConsole {
	@Override
    public void play() {
    	System.out.println("닌텐도 게임을 합니다.");
    }
}

// 비즈니스 로직 (고수준 모듈)
import org.springframework.stereotype.Service;

@Service
public class PlayerService {

	private final GameConsole gameConsole;
    
    public PlayerService(GameConsole gameConsole) {
    	this.gameConsole = gameConsole;
    }
    
    public void playGame() {
    	gameConsole.play(); // 추상화에 의존한다.
    }
}


// 컨트롤러
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class PlayerController {

    private final PlayerService playerService;

    public PlayerController(PlayerService playerService) {
        this.playerService = playerService;
    }

    @GetMapping("/play")
    public String playGame() {
        playerService.playGame();
        return "게임 시작!";
    }
}

 

위 코드를 실행하면 콘솔에 닌텐도 게임을 합니다. 가 출력될 것이다. Service에서 단지 추상화 인터페이스인 gameConsole을 의존했는데도, 자동으로 NintendoSwitch를 참고하였다. 이는 Spring이 @Component에 대해 자동으로 Bean으로 등록해주어 객체를 생성해주기 때문인데, 이걸 얘기하면 주제에 벗어나기 때문에 따로 찾아보는 걸 추천한다.

아무튼 위 코드처럼 구상하면 DIP 원칙을 아주 잘 지켰다고 말할 수 있다. 이렇게 구현하면 구현체를 만약 닌텐도 스위치에서 플레이 스테이션으로 바꾸고 싶다고 하면 금방 바꿔끼우기 쉬우니까 변화에 강해지고, 새로운 기능을 추가할 때 기존 코드를 손댈 필요가 없어 확장성과 유지보수성도 좋아진다.

 

REFERENCE

https://mangkyu.tistory.com/194

 

[OOP] 객체지향 프로그래밍의 5가지 설계 원칙, 실무 코드로 살펴보는 SOLID

이번에는 객체 지향 프로그래밍의 5가지 핵심 원칙인 SOLID에 대해 알아보고자 합니다. 실제로 애플리케이션을 개발할 때 어떻게 적용할 수 있을지 구체적인 예시를 들어 살펴보고자 합니다. 아

mangkyu.tistory.com

https://velog.io/@haero_kim/SOLID-%EC%9B%90%EC%B9%99-%EC%96%B4%EB%A0%B5%EC%A7%80-%EC%95%8A%EB%8B%A4

 

SOLID 원칙, 어렵지 않다!

객체지향 프로그래밍 설계 원칙에 대해 알아보기

velog.io

https://tech.kakaobank.com/posts/2411-solid-truth-or-myths-for-developers/

 

모든 개발자가 알아야 할 SOLID의 진실 혹은 거짓

기술 면접 자리에서 SOLID 5대 원칙에 대한 질문을 받아보신 분이라면 주목! 이 글에서는 SOLID 원칙의 역사와 장점, 그리고 각각의 원칙에서 중요한 점을 면접 상황 예시를 통해 가볍게 풀어보았습

tech.kakaobank.com

 

'DEVELOP > CONCEPT' 카테고리의 다른 글

protected 기본 생성자  (0) 2025.05.05
record class  (1) 2025.04.24
primitive type vs wrapper class  (1) 2025.04.11
final, static, static final 차이  (0) 2025.04.11
'DEVELOP/CONCEPT' 카테고리의 다른 글
  • protected 기본 생성자
  • record class
  • primitive type vs wrapper class
  • final, static, static final 차이
콘순이
콘순이
개발 보안 관련 스터디 기록장
  • 콘순이
    SECURITY DEVELOPER
    콘순이
  • 글쓰기 관리
  • 전체
    오늘
    어제
    • 분류 전체보기 (70) N
      • BAEKJOON (45)
      • ALGORITHM (4)
      • QUALIFICATIONS (0)
      • PYTHON (1)
      • PROGRAMMERS (6)
      • DEVELOP (11) N
        • SPRING (4)
        • ERROR (0)
        • CONCEPT (5)
        • AWS (2) N
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    solid
    Python
    비트 조작
    문자열
    비트 마스킹
    알고리즘
  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.3
콘순이
SOLID 원칙
상단으로

티스토리툴바