[CS] SOLID 원칙

하비·2024년 11월 10일

CS

목록 보기
4/5

객체 지향이라고 하면 꼭 나오는 단어가 있다.
그것은 바로 SOLID다.
좋은 객체 지향 설계의 5가지 원칙으로 꼭 나오는 SOLID는 과연 뭘까?

SOLID

클린코드로 유명한 로버트 마틴이 좋은 객체 지향 설계의 5가지 원칙을 정리했다.

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

여기서 제일 중요한건 OCP와 DIP다.

SRP

  • 한 클래스는 하나의 책임만 가져야 한다.
  • 하나의 책임이라는 것은 모호하다. 클 수도 있고, 작을 수도 있다. 문맥 상황과 다르다.
  • 중요한 기준은 변경이다. 변경이 있을 때 파급 효과가 적으면 단일 책임 원칙을 잘 따른 것이다. ex) 객체의 생성과 사용을 분리한다.

LSP

  • 프로그램 객체는 프로그램의 정확정을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
  • 다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 한다는 것, 다형성을 지원하기 위한 원칙, 인터페이스를 구현한 구현체는 믿고 사용하려면, 이 원칙이 필요하다.
  • 단순히 컴파일에 성공하는 것을 넘어서는 이야기다.
  • ex) 자동차 인터페이스의 엑셀은 앞으로 가라는 기능, 뒤로 가게 구현하면 LSP 위반, 느리더라도 앞으로 가야한다.

ISP

  • 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다
  • 자동차 인터페이스 -> 운전 인터페이스, 정비 인터페이스로 분리
  • 사용자 클라이언트 -> 운전자 클라이언트, 정비사 클라이언트로 분리
  • 분리하면 정비 인터페이스 자체가 변해도 운전자 클라이언트에 영향을 주지 않음
  • 인터페이스가 명확해지고, 대체 가능성이 높아진다.

OCP

  • 확장에는 열려있고, 변경에는 닫혀있어야 한다는 것이다.
  • 대체 이게 어떻게 그렇게 될수 있을까?
  • 예를 들면, MemberRepository를 Service에서 생성한다고 하자
class MemberService{
	MemberRepository memberRepository = new MemoryMemberRepository();   
}

여기서 MemoryMemberRepository의 구현체가 JdbcMemberRepository로 바뀔 때,

class MemberService{
	MemberRepository memberRepository = new JdbcMemberRepository();   
}

이 것은 OCP 원칙을 깨트리는 거다.
왜냐면 MemoryMemberRepository에서 JdbcMemberRepository라고 코드를 변경해야 하기 때문이다.

OCP 원칙을 지키기 위해서는 구현체를 이 코드에서 작성하지 않도록 하면 된다.

DIP

  • 프로그래머는 "추상화에 의존해야지, 구체화에 의존하면 안된다." 즉, 구현 클래스가 아니라 인터페이스(역할)에 의존해야 한다.
  • ex) 로미오와 줄리엣이라는 무대 공연을 생각해보자. 로미오 역할과 줄리엣 역할을 맡는 사람이 A와 B라고 할 때, 공연은 A와 B에 의존하면 안된다. 로이오 역할과 줄리엣 역할에 의존해야 A나 B가 C나 D로 바뀌었을 때도 공연에 타격이 없다.

앞에서 OCP에서 나왔던 코드를 가져오면,

class MemberService{
	MemberRepository memberRepository = new MemoryMemberRepository();   
}

이 코드는 MemberRepository라는 인터페이스를 의존하지만 더불어 MemoryMemberRepository라는 구현클래스도 의존하고 있다.
따라서 DIP에 위반된다.

해결 방법

OCP와 DIP 모두 다형성만으로는 지킬 수 없다. AppConfig와 같은 코드를 써서 이 문제를 해결해줄 수 있다.

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}

이렇게 하면 Appconfig에서만 레포지토리를 바꿔주면 된다.
스프링에서는 DI(의존성 주입)이 이걸 간단하게 해준다.

현재는 대부분의 경우 @Component 계열 애노테이션을 통해 스프링 DI가 자동으로 필요한 빈을 생성하고 주입하도록 할 수 있다. 예를 들어 @Service, @Repository, @Controller 애노테이션을 클래스에 붙이고, @Autowired 또는 생성자 주입을 통해 해당 빈을 주입받을 수 있다.

@Service
public class MemberService {
    
    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

위 코드에서 MemberService 클래스에 MemberRepository 생성자 주입 방식으로 주입하고 있다. 만약 MemberRepository 인터페이스의 구현 클래스(MemoryMemberRepository 등)가 @Repository로 등록되어 있다면, 스프링이 자동으로 이를 찾아 MemberService 생성자에 주입해준다.

profile
멋진 개발자가 될테야

0개의 댓글