[객체지향] SOLID 원칙 이해하기 ( feat. 디자인 패턴)

Woo Yong·2024년 1월 6일
1

자바

목록 보기
4/4
post-thumbnail

이번 글은 디자인 패턴에 대해서 공부하던 중, OCP(개방-폐쇄 원칙) 에 대해서 이해가 되지 않아서 정리하기 위해 작성하게 되었습니다.

추가적으로, SOLID원칙에 대해서 정리해보려고합니다.

1. 단일 책임 원칙 (SRP)

모든 클래스는 각각 하나의 책임만 가져야 한다. 클래스는 그 책임을 완전히 캡슐화해야 함을 말한다.

단일 책임 원칙은 클래스가 변경되어야 하는 이유가 단 하나여야 한다는 원칙입니다. 즉, 클래스는 하나의 책임만 가져야 합니다.

즉, 객체지향에서 책임의 기본단위 = 객체 이다.
예를 들어보자.

// 메시지를 다루는 책임을 가진 MessageHandler
public class MessageHandler{
	// 유저 인증, 인가
    public void authenticateUser(){;}
    // 송신 역할
	public void sendMessage(){
		authenticateUser();
        // 송신 로직
    }
    // 수신 역할
    public void receiveMessage(){;} 
}

위 객체는 메시지를 전달하는 책임을 가진 MessageHandler라고 가정해보자.
그러면 MessageHandler는 메시지 수신, 메시지 송신을 역할만 담당하면 될 것이다.

만약 메시지 송신하는데 있어서 사용자 인증 인가가 필요하여 sendMessage 메서드(역할) 내부 코드에 사용자 인증 인가에 대한 코드를 작성하거나, 사용자 인증 인가에 대한 역할을 호출한다면 MessageHandler는 두 가지의 책임(메시지 전달, 사용자 인증 인가)을 가지게 되는 것입니다.

두 가지의 책임을 가진다는 것은 authenticateUser코드를 변경할 때에도 MessageHandler를 수정해야하고, sendMessage코드를 변경할 때에도 MessageHandler를 수정해야한다는 것입니다.

그리고 객체가 여러 책임을 가지게 되면 서로 다른 역할을 수행하는 코드끼리 강하게 결합됩니다.
왜냐하면 객체지향 프로그래밍은 책임 간 상호 협력해야하는데 하나의 클래스 내부에 두 가지 책임을 가지고 있다는 것은 다른 책임의 역할이 또 다른 책임의 역할과 강하게 결합되기 때문입니다.
즉, 강하게 결합되어 하나의 코드가 변경되면 다른 코드의 영향을 줄 수 있다는 것입니다.

위 코드는 sendMessage()(메시지 전달을 위한 역할)메서드에서 authenticateUser()(사용자 인증 인가를 위한 역할)메서드를 호출하고 있어 authenticateUser의 코드가 변경되면 sendMessage()의 코드에도 영향을 줄 수 있습니다.

// 메시지를 다루는 책임을 가진 MessageHandler
public class MessageHandler {
    // 송신 역할
    public void sendMessage() {
        // 메시지 송신 로직
    }
    // 수신 역할
    public void receiveMessage() {
        // 메시지 수신 로직
    }
}

// 사용자 인증 및 인가를 다루는 책임을 가진 Authenticator
public class Authenticator {
    public void authenticateUser() {
        // 사용자 인증 로직
    }
    public void authorizeUser() {
        // 사용자 인가 로직
    }
}

따라서, 사용자 인증 인가에 대한 책임을 가지는 Authenticator 객체를 분리하여 객체들이 하나의 책임을 가지도록 하고, MessageHandler에서 메시지 송신/수신하는데 있어서 Authenticator의 인증 인가에 대한 역할을 호출하여 변화에 따라 파생되는 영향을 최소화 할 수 있습니다.

하나의 책임이 여러개의 클래스들로 분산되어 있는 경우라면 ?

즉, 동일한 기능(메서드, 역할)이 여러 클래스들에서 공통적으로 사용되는 경우이다.

그런 경우가 로깅, 보안, 트랜잭션과 같이 횡단 관심으로 분류할 수 있는 것이다.

예를들어 특정 메서드 실행 로그를 데이터 베이스에 저장하는 기능을 가지고 있다면 로그를 저장하는 메서드 내부에는 로그 저장과 관련한 코드가 구현되어있을 것이다.

만약 이 로그를 데이터베이스에 저장하는 것이 아니라 파일에 저장한다면 로그 기능을 로그 기능이 적용된 메서드를 모두 찾아서 수정해주어야한다...

이를 해결하기 위해서는 '부가 기능'을 위한 별도의 클래스로 분리해 책임을 담당하게 하면 된다.
이것이 관점 지향 프로그래밍(AOP)이고, 곧 배우게 되는 스프링에서 경험해볼 수 있다고 들었다!! 얼른 관점 지향 프로그래밍에 대해서도 학습해보고 싶다 !

SRP 정리

따라서 객체지향은 객체들간 협력하는 것으로, 객체지향프로그래밍은 객체들이 객체들의 메서드를 호출해야한다.
그렇기 때문에 객체가 2가지의 책임을 가지게 되면 변경 이유도 2가지가 되고, 하나의 클래스 내부에 여러 책임의 역할이 정의되어 있어 역할들이 강하게 결합되어서 코드 변경으로 인한 비용이 커지고, 코드의 중복이 많아지게 됩니다.

2. 개방 폐쇄 원칙 (OCP)

기존의 코드를 변경하지 않으면서 기능을 확장할 수 있도록 설계되어야한다.

해당 정의를 처음 봤을 때, 추상적이기도하면서 이해가 되지 않았었다...
많은 예시를 보아도 기능 확장하는데 있어서 명확하게 이해가 되지 않았었다.
그리고 여러 블로그와 책을 읽다가 클래스를 변경하지 않고도 대상 클래스의 환경을 변경할수 있는 설계라는 문구를 보게 되었다.

하지만 위 문구도 이해가 되지 않아서 Chat GPT에게 물어보았다.

GPT에 답변은 너무 마음에 들었다. 😆
이해한 내용을 정리해보면, OCP 원칙을 지키기 위해서는 추상화 객체 (interface, abstract class)를 활용해야한다고 생각이 들었다.
추상화 객체 를 이용해서 다양한 구현체들을 감싸서 기능을 확장해야한다고 생각이 들었다.

OCP 정리

따라서 어떤 구현체가 하나뿐이더라도, 추상화를 통해 기능을 확장할 때를 대비하여 OCP를 지키는 것이 효율적인 설계를 이끌 수 있습니다.

3. 리스코프 치환의 법칙

부모 클래스의 자식 클래스 사이의 행위가 일관성 있어야한다.

즉, 부모 클래스의 인스턴스 대신에 자식 클래스의 인스턴스로 대체가 가능하다.
부모 클래스가 들어갈 자리에 자식 클래스를 넣어도 계획대로 잘 작동해야 한다.

예시를 들어보자.

public class Authenticator {
    public void authenticate() {
        // 일반적인 인증 로직
    }
}

public class GoogleAuthenticator extends Authenticator {
    @Override
    public void authenticate() {
        // Google 인증 로직
    }
}

public class NaverAuthenticator extends Authenticator {
    @Override
    public void authenticate() {
        // Naver 인증 로직
    }


public class MessageHandler() extends Authenticator {}

위 코드는 사용자 인증 인가 책임을 가진 Authenticator 객체입니다. 그리고 구글 인증 객체와 네이버 인증 객체를 생성하여 Authenticator가 가진 책임과 역할을 상속받았습니다.

그런데 만약 메시지 송수신하는 책임을 가진 MessageHandler 객체가 인증 메서드(역할)를 사용하기 상속한다면 LSP 원칙을 위배하는 것입니다.

MessageHandler와 Authenticator의 행위에는 일관성이 없고 LSP는 서브 타입이 언제나 기반 타입으로 대체될 수 있어야 한다는 원칙을 제시하는데, 이는 서브 클래스가 기반 클래스의 기능을 유지하고 확장해야 한다는 것을 의미합니다.

그리고 MessageHandler가 Authenticator를 상속받는다면, MessageHandler는 사용자 인증과 무관한 메시지 송수신 기능까지 갖게 됩니다. 이는 두 클래스 간의 명확한 계층 구조와 의미적인 일관성이 떨어질 수 있습니다.

4. 인터페이스 분리 원칙

인터페이스를 클라이언트에 특화되도록 분리시키는 설계 원칙

인터페이스 분리 원칙은 인터페이스를 설계할 때 명확하게 설계하라라고 다가왔지만 클라이언트에 특화되도록이라는 점이 추상적으로 다가와서 Chat GPT에게 물어봤습니다.

GPT 설명을 읽으면서 생각을 정리해보면 인터페이스를 정의 할때 필요한 기능만을 표현하라고 이해가 되었다. 즉, 해당 인터페이스를 사용하는 쪽 코드에 대한 사용 패턴에 따라 설계하고 분리하라는 의미이다.

예시를 들어보자.

클라이언트 코드를 고려한 분리 Case 1

public interface Auth {
	// return 값 : boolean값
	public boolean authenticate(){}
}

public class UserManager {
	// 경우 1
	public void login(){
    	if(auth.authenticate()) {
        	System.out.println(auth.getName() + "인증 성공");
        };
    }
}

클라이언트 코드를 고려한 분리 Case 2

public interface Auth {
	// return 값 : User객체
	public User authenticate(){}
}

public class UserManager {
	// 경우 2
	public void login(){
    	User user = auth.authenticate();
        System.out.println(user.getName() + "인증 성공");
    }
}

Case1의 경우는 클라이언트 코드에서 if문을 통해 유효성 검증 후 인증 성공 메시지를 출력해주기 때문에 boolean 값으로 return하도록 설계하였고,
Case2의 경우는 클라이언트 코드에서 바로 User객체를 받아 인증 성공 메시지를 출력해주기 때문에 User객체를 return하도록 설계하였습니다.

이처럼 Auth 인터페이스를 사용하는 UserManager(클라이언트 코드)코드에 대해 사용패턴을고려하여 인터페이스를 명확하게 설계해야한다는 것이다.

그리고 클라이언트에 특화되도록 분리한다는 것은 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않아야 한다는 것입니다.

예시를 들어보겠습니다.

클라이언트가 필요한 기능 추출 Before

public interface Auth {
	// 인가 메서드
	public void authorize();
    // 인증 메서드
    public void authenticate();
}

클라이언트가 필요한 기능 추출 After

// 인가 인터페이스
public interface AuthorizeAble {
	public void authorize();
}

// 인증 인터페이스
public interface AuthenticateAble {
	public void authenticate();
}

만약에 어떤 클래스가 Auth 인터페이스를 구현한다면 인증 메서드와 인가 메서드를 모두 구현해야합니다. 하지만 만약 인증 기능만 필요하여 인가 메서드는 필요 없을 경우라면 ISP 원칙을 위반하게 되는 것입니다.

따라서 클라이언트 코드에서 사용되지 않는 메서드에 의존하지 않도록 인터페이스를 세분화해야합니다.
이처럼 인터페이스는 작고 명확한 역할을 수행 하며 다중 제어가 가능하도록 할 수 있습니다. 그리고 클라이언트 코드는 필요한 기능에만 의존하게 됩니다. 이는 코드의 유연성을 높이고, 변경이 쉽게 이루어질 수 있도록 합니다.

5. 의존 역전 원칙

의존 관계를 맺을 때 변화하기 쉬운 것 보다는 거의 변화가 없는 것에 의존하라

객체지향프로그래밍에서는 객체들간 협력하기 때문에 객체가 자주 변하면 결국 클라이언트 코드의 수정이 잦아지게 된다.

그렇기 때문에 의존 관계를 맺을 때 구체적인 객체가 아닌 인터페이스와 추상클래스와 같은 추상적인 객체에 의존해야한다는 것이다.

profile
Back-End Developer

0개의 댓글