[백엔드] SOLID 원칙 정리

유승선 ·2022년 11월 9일
0

Spring 강의

목록 보기
10/12
post-thumbnail

최근에 포스트가 좀 뜸했는데 면접 준비도 있었고 코딩 테스트도 보고 바빴다. 이 과정을 거치면서 느꼈던게 지금 당장은 코딩 테스트에 집중 하는것보다 백엔드 역량을 더 키우고 싶었고 실제로 스프링 개인 프로젝트도 꾸준히 진행하면서 스프링과 관련된 지식도 많이 성장한게 느껴졌다.

그런데 문득 내가 기본기가 탄탄한 사람일까 라고 생각해 봤는데 아니였다. 유투브에서 조코딩 비디오를 보는데 댓글에 중소 기업 다니는 별거 아닌 개발자들은 SOLID 가 뭔지도 모를걸? 이라는 댓글을 봤다. SOLID 는 객체 지향적인 설계를 위해 꼭 필요한 역량인거는 "스프링 핵심 원리 - 기본편" 에서 알았지만 문득 그 댓글을 보고 난 설명할 수 있나? 라고 물어보니깐 정확히 설명 못하겠는 내 자신이 화났다.

그래서 이틀동안 스프링 핵심 원리 - 기본편을 더 집중적으로 봤고 스프링을 사용한 경험 덕에 2회차에서 이해가 훨씬 많이 갔다. 이번 포스트에는 기본기에만 집중한 글을 작성할 것이다.


SOLID 란?

SOLID 란?

SOLID 원칙이란 객체 지향 설계가 올바르게 되어있는지를 확인하는 하나의 기준과 가이드라인 이다. 자신의 설계가 SOLID를 따라가고 있는지를 확인 할 수 있는 좋은 예시이다.


S :단일 책임 원칙 (SRP)

단일 책임 원칙 : 하나의 객체는 반드시 하나의 동작만의 책임을 갖는다

/**
 * 자동차 객체
 *
 * @author RWB
 * @since 2021.08.13 Fri 00:14:14
 */
public class Car
{
    private final String WD;
    
    private final int[] WHEEL = { 0, 0, 0, 0 };
    
    /**
     * Car 생성자 함수
     *
     * @param wd: [String] 휠 구동 방식
     */
    public Car(String wd)
    {
        WD = wd;
    }
    
    /**
     * 주행 함수
     *
     * @param power: [int] 동력
     */
    public void run(int power)
    {
        switch (WD.toUpperCase())
        {
            case "FWD" -> {
                WHEEL[0] = power;
                WHEEL[1] = power;
            }
            
            case "RWD" -> {
                WHEEL[3] = power;
                WHEEL[4] = power;
            }
            
            case "AWD" -> {
                WHEEL[0] = power;
                WHEEL[1] = power;
                WHEEL[3] = power;
                WHEEL[4] = power;
            }
        }
        
        System.out.println("휠 동력 상태: " + WHEEL[0] + ", " + WHEEL[1] + ", " + WHEEL[2] + ", " + WHEEL[3]);
    }
}

위에서 보는것 처럼 CAR 이라는 객체는 현재 휠 동력 상태를 변경 할 수 있다. 다만 지금 CAR 안에 있는 run() 메서드는 전륜(FWD), 후륜(RWD), 사륜(AWD) 의 특성을 가지고 휠에 동력을 채워주는 역활을 하고 있다.

지금 CAR 객체는 3가지의 특성을 전부 가지고 있고 3가지의 책임을 가진 상태다. 이런 형태는 코드의 규모가 커지고 복잡해진다면 많은 오류가 발생할 것이라 문제가 많다. 그런 이유로 1객체 = 1책임 의 원칙을 지키고 위와 같은 상황을 방지해야한다.

/**
 * 자동차 추상 객체
 *
 * @author RWB
 * @since 2021.08.13 Fri 00:14:14
 */
abstract public class Car
{
    protected final String WD;
    
    protected final int[] WHEEL = { 0, 0, 0, 0 };
    
    /**
     * Car 생성자 함수
     *
     * @param wd: [String] 휠 구동 방식
     */
    public Car(String wd)
    {
        WD = wd;
    }
    
    /**
     * 주행 함수
     *
     * @param power: [int] 동력
     */
    abstract public void run(int power);
}

CAR 이라는 구현체 자체를 추상화 시키고

/**
 * 전륜차 객체
 *
 * @author RWB
 * @since 2021.08.13 Fri 01:03:13
 */
class FrontWheelCar extends Car
{
    /**
     * FrontWheelCar 생성자 함수
     *
     * @param wd: [String] 휠 구동 방식
     */
    public FrontWheelCar(String wd)
    {
        super(wd);
    }
    
    /**
     * 주행 함수
     *
     * @param power: [int] 동력
     */
    @Override
    public void run(int power)
    {
        WHEEL[0] = power;
        WHEEL[1] = power;
        
        System.out.println("휠 동력 상태: " + WHEEL[0] + ", " + WHEEL[1] + ", " + WHEEL[2] + ", " + WHEEL[3]);
    }
}
/**
 * 후륜차 객체
 *
 * @author RWB
 * @since 2021.08.13 Fri 01:05:57
 */
class RearWheelCar extends Car
{
    /**
     * RearWheelCar 생성자 함수
     *
     * @param wd: [String] 휠 구동 방식
     */
    public RearWheelCar(String wd)
    {
        super(wd);
    }
    
    /**
     * 주행 함수
     *
     * @param power: [int] 동력
     */
    @Override
    public void run(int power)
    {
        WHEEL[2] = power;
        WHEEL[3] = power;
        
        System.out.println("휠 동력 상태: " + WHEEL[0] + ", " + WHEEL[1] + ", " + WHEEL[2] + ", " + WHEEL[3]);
    }
}
/**
 * 사륜차 객체
 *
 * @author RWB
 * @since 2021.08.13 Fri 01:05:57
 */
public class AllWheelCar extends Car
{
    /**
     * AllWheelCar 생성자 함수
     *
     * @param wd: [String] 휠 구동 방식
     */
    public AllWheelCar(String wd)
    {
        super(wd);
    }
    
    /**
     * 주행 함수
     *
     * @param power: [int] 동력
     */
    @Override
    public void run(int power)
    {
        WHEEL[0] = power;
        WHEEL[1] = power;
        WHEEL[2] = power;
        WHEEL[3] = power;
        
        System.out.println("휠 동력 상태: " + WHEEL[0] + ", " + WHEEL[1] + ", " + WHEEL[2] + ", " + WHEEL[3]);
    }
}

각 책임들을 담당한 객체들을 나눠서 SOLID 에 S 원칙에 맞는 단일 책임 원칙을 지켜주었다. 이로서 코드가 확장되고 커지더라도 각자 역활을 맡은 부분만 고쳐주면은 객체 지향 설계가 더 깔끔해졌다.


O : 개방 폐쇄 원칙 (OCP)

결국 SOLID 원칙에서 중요하게 생각하는 부분은 다형성을 이용하는거 같다. 객체 지향 프로그래밍에서 어느정도 배우는 부분이지만 인터페이스를 적극 사용하자.

스프링 기본편에서 가장 이해가 안됐었던 부분이다. 이게 무슨 의미이지? 하고 몇번을 스스로한테 물어봤다가 해답을 못찾았는데 이제야 보이기 시작한다.

위와 같은 코드는 OCP 를 위반하고 있다. 왜냐? 만약에 확장성을 고려해서 MemoryMemberRepository 에서 JdbcMemberRepository 로 바꾼다고 생각해보자. 이제 우리는 확장성에 따른 룰을 따라가야 하기 때문에 MemoryMemberRepository 를 쓰는 기존에 모든 서비스 클라스들을 바꾼다고 생각했을때, 전부 JdbcMemberRepository 로 바꿔주어야 한다.

OCP 와 이후에 소개할 DIP 는 굉장히 유사하다고 생각하는데 일단 OCP 는,

"구현 객체를 변경 하려면 클라이언트 코드를 변경해야 한다"

이 부분이 가장 큰 특이점이라고 생각한다.


L : 리스코프 치환 원칙 (LSP)

굉장히 어려운 말이다. 솔직히 모든 원칙 중에 가장 이해가 안돼서 계속 따로 찾아봤고 조금 더 쉬운 말로 풀자면 아래의 뜻이다.

"자식 클래스는 최소한 자신의 부모 클래스에서 가능한 행위는 수행할 수 있어야한다"

결국에 상속되는 객체는 반드시 부모 객체를 완전히 대체해도 아무런 문제가 없어야한다. 조금 더 쉬운 예를 들자면 자동차 인터페이스가 앞으로 가는 기능을 가지고 있는데 그걸 상속한 다른 클래스가 뒤로 가는 기능을 가지고 있다면 그것은 LSP 를 위반한 행위다.

그 외에도, 만약 상속한 자식 클래스가 부모 클래스의 기능을 온전히 쓸 수 없다면 그것또한 LSP 를 위반하는 행위이기 때문에 상속을 할때도 충분한 고려를 해봐야한다.

리스코프 예시

위에 예시에서는 사각형이 직사각형을 부모로 가지게 되면 가지는 문제를 보여주었다. 사각형의 넓이는 직사각형의 넓이를 구하는 공식과 완전히 다르기 때문에 온전히 상속이 될 수 없다.


I : 인터페이스 분리 원칙(ISP)

SOLID 원칙 중에서 그나마 말로 가장 이해가 되는 부분이다. 인터페이스는 모든 기능을 가진 하나의 객체 보다 분할 해가지고 특정 클라이언트를 위한 인터페이스를 여러개 가지는게 낫다. 위에 예시에서는 자동차 인터페이스 뿐만 아니라, 운전 인터페이스, 정리 인터페이스로 분리하는 예를 보여주고 있다.


D : 의존관계 역전 원칙 (DIP)

모든 SOLID 원칙 중에 가장 중요하다고 강조 됐던 부분이다. 결국 클래스는 추상화에 의존해야지 구체화에 의존하면 안된다. 즉, 클래스는 자신의 배우를 선택하는게 아니고 알아서도 안된다. 오직 감독이 배우를 선택하는 상황을 연출해야 한다.

MemberRepository 를 코드안에서 직접 지정해주는것은 클래스가 배우를 선택하는거와 같다. 배우를 지정해주는것은 감독이 해야하고 배우를 인터페이스에 비교하고 감독을 DI 해주는 스프링 컨테이너로 비유하고 싶다.


참고

스프링 핵심 원리 - 기본편
SOLID 설명 블로그
SOLID 설명 블로그2

profile
성장하는 사람

0개의 댓글