티스토리 뷰

Dev

[OOP] SOLID 설계 원칙

j.y.eunseo 2023. 3. 27. 12:29

SOLID 원칙

SOLID 원칙이란 객체 지향 프로그래밍의 다섯 가지 기본 원칙의 앞글자를 따서 만든 원칙들의 집합이다.

이 원칙은 소프웨어의 유연성, 확장성, 유지보수성을 높이기 위해 적용되는 지침이다.

 

SOLID 원칙의 다섯 가지 원칙은 다음과 같다.

  • 단일 책임 원칙 (Single responsibility principle, SRP)
  • 개방-폐쇄 원칙 (Open-closed principle, OCP)
  • 리스코프 치환 원칙 (Liskov substitution principle, LSP)
  • 인터페이스 분리 원칙 (Interface segregation principle, ISP)
  • 의존 역전 원칙 (Dependency inversion principle. DIP)

 

1. 단일 책임 원칙 (Single responsibility principle, SRP)

한 클래스는 하나의 책임만 가져야 한다.

클래스가 제공하는 모든 서비스는 그 하나의 책임을 수행하는 데 집중되어야 한다.

한 클래스가 여러 개의 책임을 가지게 되면, 코드의 이해와 유지보수성이 떨어진다.

 

예를 들어, 사용자 인증 기능을 처리하는 애플리케이션에서 사용자 인증 로직을 구성해 보자.

사용자 인증에는 로그인, 회원가입, 로그아웃 등 다양한 기능을 포함하고 있다. 이때, 각각의 기능들이 서로 다른 책임을 가지고 있어야 하며, 각각의 기능이 변경되더라고 다른 기능에 영향을 미치지 않아야 한다.

// 로그인 기능을 처리하는 클래스
class LoginService {
  loginUser(username, password) {
    // 로그인 로직 구현
  }
}

// 회원가입 기능을 처리하는 클래스
class SignupService {
  signupUser(username, password, email) {
    // 회원가입 로직 구현
  }
}

// 로그아웃 기능을 처리하는 클래스
class LogoutService {
  logoutUser() {
    // 로그아웃 로직 구현
  }
}

// 사용자 인증을 관리하는 클라이언트 클래스
class AuthClient {
  constructor() {
    this.loginService = new LoginService();
    this.signupService = new SignupService();
    this.logoutService = new LogoutService();
  }

  login(username, password) {
    this.loginService.loginUser(username, password);
  }

  signup(username, password, email) {
    this.signupService.signupUser(username, password, email);
  }

  logout() {
    this.logoutService.logoutUser();
  }
}

// 사용자 인증 기능 사용 예시
const authClient = new AuthClient();

authClient.login('user123', 'password123'); // 로그인
authClient.signup('user456', 'password456', 'user456@example.com'); // 회원가입
authClient.logout(); // 로그아웃

위의 코드에서는 로그인, 회원가입, 로그아웃 각각의 기능을 처리하는 별도의 클래스를 생성하고 AuthClient 클래스에서 각각의 기능을 사용하는 메소드를 제공하여 이를 호출하도록 한다.

 

2. 개방-폐쇄 원칙 (Open-closed principle, OCP)

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

즉, 새로운 요구사항이나 기능이 추가될 때 기존의 코드를 변경하지 않고도 확장이 가능하도록 설계되어야 한다. 이를 통해 코드의 변경에 따른 파급 효과를 최소화하고, 유연한 확장이 가능해진다.

 

예를 들어,다양한 유형의 애니메이션을 처리하는 웹 애플리케이션에서 각각의 애니메이션 유형을 처리하는 로직을 각각의 클래스로 분리하고, 추상화나 인터페이스를 도입할 수 있다. 이렇게 하면 애니메이션 유형이 추가될 때  새로운 클래스를 추가하기만 하면 되므로 기존의 클래스를 변경하지 않고도 확장이 가능하게 된다.

 

// 추상화 또는 인터페이스를 활용한 애니메이션 처리 인터페이스
interface Animation {
  play(): void;
}

// 각각의 애니메이션 유형을 처리하는 클래스들
class FadeAnimation implements Animation {
  play(): void {
    // 페이드 애니메이션 로직 구현
  }
}

class ScaleAnimation implements Animation {
  play(): void {
    // 스케일 애니메이션 로직 구현
  }
}

// 애니메이션 유형을 선택하고 처리하는 클라이언트 클래스
class AnimationClient {
  private animation: Animation;

  constructor(animation: Animation) {
    this.animation = animation;
  }

  playAnimation(): void {
    this.animation.play();
  }
}

// 애니메이션 유형을 선택하고 변경하는 부분
const fadeAnimation = new FadeAnimation();
const scaleAnimation = new ScaleAnimation();

const client = new AnimationClient(fadeAnimation);
client.playAnimation(); // 페이드 애니메이션 실행

client.animation = scaleAnimation;
client.playAnimation(); // 스케일 애니메이션 실행
 

3. 리스코프 치환 원칙 (Liskov substitution principle, LSP)

하위 클래스는 상위 클래스로 대체 가능해야 한다.

프론트엔드에서의 컴포넌트나 모듈은 다른 컴포넌트나 모듈로 대체 가능해야 한다. 

즉, 인터페이스나 추상화를 통해 다형성을 보장하고, 대체 가능성을 유지해야 한다는 의미이다.

이를 통해 컴포넌트나 모듈의 교체가 용이해지고, 코드의 재사용성이 증가한다.

 

class Animal {
  makeSound() {
    console.log("동물이 소리를 냅니다.");
  }
}

class Dog extends Animal {
  makeSound() {
    console.log("강아지가 멍멍이 소리를 냅니다.");
  }
}

class Cat extends Animal {
  makeSound() {
    console.log("고양이가 야옹이 소리를 냅니다.");
  }
}

// 사용자가 동물의 소리를 출력하는 함수
function outputAnimalSound(animal: Animal) {
  animal.makeSound();
}

const animal = new Animal();
const dog = new Dog();
const cat = new Cat();

outputAnimalSound(animal); // "동물이 소리를 냅니다."
outputAnimalSound(dog); // "강아지가 멍멍이 소리를 냅니다."
outputAnimalSound(cat); // "고양이가 야옹이 소리를 냅니다."

위의 코드에서 Animal 클래스를 슈퍼 클래스로 두고, Dog와 Cat 클래스Animal 클래스의 서브 클래스로 정의하였다. 이 예시에서 Dog와 Cat 클래스가 슈퍼 클래스 Animal의 makeSound 메소드의 시그니처를 유지하고, 각각의 클래스에서 자신만의 동작을 일관성 있게 제공하고 있어 LSP 원칙을 준수하고 있음을 알 수 있다.

 

4. 인터페이스 분리 원칙 (Interface segregation principle, ISP)

특정 클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다.

커다란 인터페이스를 구체적이고 작은 단위들로 분리함으로써 클라이언트가 꼭 필요한 메서드를 이용하도록 한다. 이와 같이 작은 단위들을 역할 인터페이스라도고 부른다. 

이를 통해 컴포넌트나 모듈 간의 결합도를 낮추고 의존 관계를 명확하게 관리할 수 있으며, 리팩토링, 수정, 재배포를 쉽게 할 수 있다.

 

// ISP를 준수하지 않은 인터페이스 예시
interface Animal {
  makeSound(): void;
  fly(): void; // 동물이 날 수 있는 메소드
}

class Dog implements Animal {
  makeSound() {
    console.log("강아지가 멍멍이 소리를 냅니다.");
  }

  fly() {
    // 강아지는 날 수 없는데 빈 메소드를 구현해야 함
  }
  
  swim() {
    console.log("강아지가 헤엄치고 있습니다.");
  }
}

class Bird implements Animal {
  makeSound() {
    console.log("새가 짹짹 소리를 냅니다.");
  }

  fly() {
    console.log("새가 날아가고 있습니다.");
  }
  
  swim() {
    // 새는 수영할 수 없는데 빈 메소드를 구현해야 함
  }

}

// 사용자가 동물의 소리와 날아가는 동작을 출력하는 함수
function outputAnimalSoundAndFly(animal: Animal) {
  animal.makeSound();
  animal.fly();
}

const dog = new Dog();
const bird = new Bird();

outputAnimalSoundAndFly(dog); // "강아지가 멍멍이 소리를 냅니다." / 에러: fly 메소드가 구현되지 않았습니다.
outputAnimalSoundAndFly(bird); // "새가 짹짹 소리를 냅니다." / "새가 날아가고 있습니다."

위의 코드에서 Animal 인터페이스makeSound, flt, swim 세 개의 메소드를 포함하고 있다.

그러나 Dog 클래스Fish 클래스가 이 인터페이스를 구현할 때, 각각의 클래스에서는 날 수 있는 메소드와 수영할 수 있는 메소드가 의미 없는 빈 메소드로 구현되어 있다. 이는 ISP를 준수하지 않고 있는 예시로, ISP를 준수하기 위해서는 Animal 인터페이스를 세분화하여 각각의 동물 종류가 필요로 하는 메소드만 포함하도록 인터페이스를 분리해야 한다.

// ISP를 준수하는 인터페이스 예시
interface Animal {
  makeSound(): void;
}

interface Flyable {
  fly(): void;
}

interface Swimmable {
  swim(): void;
}

class Dog implements Animal, Swimmable  {
  makeSound() {
    console.log("강아지가 멍멍이 소리를 냅니다.");
  }
  
  swim() {
    console.log("강아지가 헤엄치고 있습니다.");
  }
}

class Bird implements Animal, Flyable {
  makeSound() {
    console.log("새가 짹짹 소리를 냅니다.");
  }

  fly() {
    console.log("새가 날아가고 있습니다.");
  }
}

// 사용자가 동물의 소리와 수영 동작을 출력하는 함수
function outputAnimalSoundAndSwim(animal: Animal & Swimmable) {
  animal.makeSound();
  animal.swim();
}

// 사용자가 동물의 소리와 날아가는 동작을 출력하는 함수
function outputAnimalSoundAndFly(animal: Animal & Flyable) {
  animal.makeSound();
  animal.fly();
}


const dog = new Dog();
const bird = new Bird();

outputAnimalSoundAndSwim(dog); // "강아지가 멍멍이 소리를 냅니다." / "강아지가 헤엄치고 있습니다."
outputAnimalSoundAndFly(bird); // "새가 짹짹 소리를 냅니다." / "새가 날아가고 있습니다."

Animal 인터페이스는 동물의 전체 공통적인 속성인 makeSound 메소드만을 포함하고 있으며 Flyable 인터페이스Swimmable 인터페이스는 각각 날 수 있는 동물과 수영할 수 있는 동물의 메소드만을 포함하고 있다.

이를 통해 DogFish 클래스는 각각 필요로 하는 인터페이스만을 구현할 수 있다.

 

5. 의존 역전 원칙 (Dependency inversion principle. DIP)

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

상위 수준의 모듈은 하위 수준의 모듈에 의존하면 안되고, 모두 추상화에 의존해야 한다.

이를 통해 모듈 간의 결합도를 낮추고, 유연한 코드 재사용과 변경에 대한 대응력을 향상할 수 있다.

 
// DIP를 준수하지 않은 예시
class UserService {
  constructor() {
    // UserService가 UserRepository에 직접 의존
    this.userRepository = new UserRepository();
  }

  getUser(userId) {
    // UserRepository를 사용하여 사용자 정보 조회
    return this.userRepository.getUser(userId);
  }

  createUser(userData) {
    // UserRepository를 사용하여 사용자 생성
    return this.userRepository.createUser(userData);
  }
}

class UserRepository {
  getUser(userId) {
    // 사용자 정보 조회 로직
  }

  createUser(userData) {
    // 사용자 생성 로직
  }
}

UserServieUserRepository에 직접적으로 의존하고 있다. 이는 고수준 모듈이 저수준 모듈에 의존하고 있어 DIP 원칙을 준수하고 있지 않다.

다음과 같이 DIP 원칙을 준수하는 코드를 작성해보자.

// DIP를 준수하는 예시
interface IUserRepository {
  getUser(userId): any;
  createUser(userData): any;
}

class UserService {
  constructor(userRepository) {
    // UserService가 IUserRepository 인터페이스에 의존
    this.userRepository = userRepository;
  }

  getUser(userId) {
    // IUserRepository를 사용하여 사용자 정보 조회
    return this.userRepository.getUser(userId);
  }

  createUser(userData) {
    // IUserRepository를 사용하여 사용자 생성
    return this.userRepository.createUser(userData);
  }
}

class UserRepository implements IUserRepository {
  getUser(userId) {
    // 사용자 정보 조회 로직
  }

  createUser(userData) {
    // 사용자 생성 로직
  }
}

위의 코드에서 DIP를 준수하기 위해 UserServiceUserRepository 사이에 인터페이스(IUserRepository)를 도입하였다.

UserServiceUserRepository 대신에 IUserRepository 인터페이스에 의존하고 있으며, UserRepositoryIUserRepository 인터페이스를 구현하고 있다. 이렇게 함으로써 UserServiceUserRepository 대신에 다른 UserRepository 구현체를 주입받아 사용할 수 있으며, 두 모듈 간의 결합도를 낮추고 확장성을 높일 수 있다.

댓글
«   2025/05   »
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
최근에 올라온 글