객체지향 SOLID 원칙

대휘·2023년 9월 25일
0

SRP : Single Responsibility Principle

"책임은 변경하려는 이유" - 로버트 C.마틴

하나의 클래스는 하나의 책임만을 가져야 한다. 즉, 하나의 클래스는 하나의 이유만으로 변경되어야한다.

너무나도 당연시 여겨지는 내용이지만, 의도치 않게 위반하는 경우가 생긴다. 예시를 들어보자.

  1. Controller - Service - Repository 구조의 프로젝트에서, Service에서 예외가 발생했을 때 Http 상태 코드를 담은 예외를 던지는 경우
    -> 이 경우, Controller가 Http 요청을 받지 않도록 수정되면 Service도 함께 수정되어야하므로 SRP가 위반된다.
  2. Service가 여러개의 Repository와 이를 각각 사용하는 메서드를 한 클래스에 가지고 있는 경우
    -> 응집도가 떨어지고, 파편화된 메서드와 필드들이 섞여 있으므로 SRP가 위반된다.

OCP : Open/Close Principle

확장에는 열려있어야 하나 변경에는 닫혀있어야 한다.
OCP를 지키는 코드는 클라이언트 코드가 추상화에 의존하고 있기 때문에 확장될 때와 변경될 때 모두 다른 코드에 영향을 주지 않는다.

2 종류의 Repository 클래스가 존재하고, Service에서 if-else 문을 통해 상황에 따라 다른 레포지토리를 참조하는 경우

public class UserService { 
	private MySQLUserRepository mySQLRepository;
	private MongoUserRepository mongoRepository; 
	
	public UserService(MySQLUserRepository mySQLRepository, MongoUserRepository mongoRepository) {
		this.mySQLRepository = mySQLRepository;
		this.mongoRepository = mongoRepository; 
	}
	
	public User getUser(int id, String dbType) { 
		if ("MySQL".equals(dbType)) { 
			return mySQLRepository.findById(id); 
		} else if ("MongoDB".equals(dbType)) { 
			return mongoRepository.findById(id); 
		} 
		throw new IllegalArgumentException("Invalid dbType"); 
	}
	...
}

위의 경우, Repository 종류가 추가/변경될 때, UserService의 코드를 수정해야하므로 OCP를 위반하는 경우이다.

public class UserService { 
	private UserRepository userRepository;
	
	public UserService(UserRepository userRepository) {
		this.userRepository = userRepository;
	}
	
	public User getUser(int id) {
		return userRepository.findById(id);
	}
	...
}

따라서 UserRepository 인터페이스를 사용하여 분기를 없애고, 사용하는 Repository의 종류는 외부에서 결정하도록 하는 것이 좋다.

참고로 OCP를 설명할 때 인터페이스를 이용한 예시가 가장 일반적이지만, 꼭 인터페이스가 필요한 것은 아니며 Enum, 디자인패턴, 이벤트 기반 프로그래밍 등을 통해 OCP를 지킬 수 있다.

LSP : Liskov Substitution Principle

파생 클래스는 기반 클래스를 대체할 수 있어야한다. 쉽게말하면, 부모 클래스가 할 수 있는 행동은 자식 클래스도 할 수 있어야 한다는 원칙이다.

리스코프 치환 원칙이 성립하지 않는 경우

  1. 부모클래스가 할 수 있는 동작을 자식 클래스가 할 수 없는 경우
    -> 매번 부모 클래스인지, 자식 클래스인지 분기를 나누어 코드를 작성해야하므로 다형성을 전혀 활용할 수 없다.
  2. 자식클래스에서 필드나 메소드의 접근제어자를 더 엄격하게 변경하는 경우 (public -> private)
    -> 자식 클래스의 접근제어자가 더 엄격해지면, 부모클래스 레퍼런스 변수에 자식 클래스의 인스턴스를 할당했을 때 문제가 발생하므로 이는 불가능하다.

따라서 리스코프 치환원칙을 지키지 않는 상속은 하면 안된다.
참고: 계약에 의한 설계

ISP : Interface Segregation Principle

특정 클라이언트를 위한 인터페이스 여러개가 범용 인터페이스 하나보다 낫다.
하나의 인터페이스에 여러 개의 구현 클래스를 위한 메소드를 각각 정의해놓으면, 어느 한 쪽에서는 불필요한 메서드가 노출되는 상황이 발생한다.
이 경우에는 인터페이스를 구현 클래스별로 세분화하면, 불필요한 메소드가 노출되는 상황을 막을 수 있고 유지보수를 편리하게 할 수 있다.

DIP : Dependency Inversion Principle

고수준 컴포넌트는 저수준 컴포넌트에 의존하지 않아야 한다.
일반적으로, 구현 클래스에 직접 의존하지 않고 인터페이스를 중간에 끼워넣어 의존 관계를 역전시킬 수 있다.

OCP와 DIP 모두 인터페이스를 사용하는 것이 대표적인 방법이지만, 그 목적에 차이가 있다.

OCP: 확장, 변경 시 다른 코드들이 영향을 받지 않도록 하기 위함
DIP: 인터페이스를 사용하여 고수준 컴포넌트가 저수준 컴포넌트에 의존하지 않도록 바꾸기 위함

간접적으로 의존 역전 원칙이 깨지는 상황

예외를 처리할 때, 고수준 컴포넌트가 처리하는 예외가 저수준 컴포넌트의 정보를 간접적으로 담고 있는 경우
-> 저수준 컴포넌트가 변경되면 고수준 컴포넌트의 예외 처리를 변경해야하므로 OCP / DIP를 모두 위반하게 된다.
따라서, 저수준 컴포넌트(구현체)들이 추상적인 예외를 던지도록 캡슐화하고 고수준 컴포넌트는 하나의 예외만을 처리하도록 변경해야 한다.

profile
학생

0개의 댓글