SPRING기초 - SOLID란?

hanana·2023년 10월 14일

SOLID란

SOLID란 객체지향적으로 코드를 짤때 고려하면
좋은 5가지 원칙을 앞글자만 따서 만든 단어로

SOLID를 지키면서 코드를 작성하면
객체지향적인 코드가 생성되고
테스트하기 쉬운 코드가 만들어지며

이는 대게 품질이 좋은 코드로 이루어진다.


SOLID - S

SingleResponsibilityprinciple. 즉 단일책임원칙으로

'하나의 클래스는 하나의 책임만을 가져야한다.' 라는 의미이다.

아래는 실제로 단일책임을 지킨 사이드프로젝트 코드이다.

@Transactional
public String applyCourse(CourseApply requestDto) {
    Course course = courseRepository.findBYIdWithQuery(requestDto.courseId()).orElseThrow(CourseNotFoundException::new);
    //강의정원이 마감되는경우
    if (course.isFulled()) {
        throw new MaxCountException("수강인원이 가득 찼습니다.");
    }
    //더티체킹에의해 트랜잭션 종료시 Update
    course.addCurrentCount();
    return "수강신청 되었습니다.";
}

강의 정원이 마감되었는지를 확인하는 IsFulled() 메소드는
단순히 강의가 마감되었는지만 확인한다.
이후 수강신청인원을 1 증가하는 메소드는 addCurrent() 메소드로 별도로 구성하였다.

이 메소드 안에서 강의가 마감되지 않았으면 수강인원을 1 증가하는 식으로
메소드 한번의 호출로 모든 비지니스로직을 해결할 수 있으나,
이렇게 되는경우 단일책임원칙에 위배된다.

이로인해 테스트코드의 작성도 쉬워지고, 유지보수측에 도움이 많이 되게 된다.

만약 메소드 isFulled() 메서드 내에서 모든 로직을 수행한다면
테스트코드에서 검증해야 할 사안이 정말 많아지고,
이로인해 테스트 작성이 매우 어려워진다.

또한 단순히 isFull() 이라는 메소드 이름만 봐도 그냥 검증하는 로직이구나 하고 파악하기가 쉽고
서비스에서 장애 발생시 어느부분이 에러인지를 더 빠르게 확실하게 알기 쉽고

로직이 변경되는 경우에도 이로인한 사이드이펙트를 최소화 할 수 있다.


SOLID - O

Open Close Principle. 개방폐쇄원칙 이다.

개인적으로 이름이 너무 아리까리해서 '이게 뭔소리야?' 라는 반응을 했던 기억이 있는 원칙이다.

변경에는 닫혀있고 확장에는 열려있어야 한다라는 원칙이다.
조금 아리송한데 생각보다 훨씬 쉬운 개념이다.


만약 새로운 기능을 추가하기 위해서 기존 코드를 이것저것 변경해야하고
이로인한 사이드이펙트를 짐작하기 어렵다면 개방폐쇄원칙이 잘 이루어지지 않은 코드라고 볼 수 있다.

이런식으로 기능의 추가와는 관계없는 코드를 이것저것 변경해야 하는 상황을 확장에는 닫혀있다 라고 표현 한다.
또한 기능확장을 위해서 관계없는 이파일 저파일들을 다 건드려야 한다면 수정에는 열려있다. 라고 표현할 수 있습니다.
개방폐쇄원칙이 지켜지지 않는 경우는 기본적으로 추상화가 부족한 경우에 발생한다.

역시 개인 프로젝트에서 구현한 바가 있어서 공유한다.

public interface CourseService {
    List<CourseResponse> findCourses(CourseSearch courseSearch);
    CourseResponse findOne(Long id);
    Long saveCourse(CourseCreate requestDto);
    String applyCourse(CourseApply requestDto);
    Long editCourse(Long id, CourseEdit requestDto);
    void deleteCourse(Long id);
}

수강신청을 하는 상황을 가정하여 프로젝트를 작성하였고,
처음에는 단순히 RDB만을 이용하여 데이터 정합성 보장을 위해 RDB의 LOCK 기능을 이용하여 구현했다.

이후 맨날 수강신청할 때마다 서버가 죽어버려서
성능 개선을 위해 reids를 도입하는 시나리오를 만들고
서비스에 redis캐싱을 활용한 기능을 추가하였다.

서비스 인터페이스를 적절히 추상화하였고,
RDB서비스는 이를 구현했기 때문에
Redis서비스역시 인터페이스를 구현하여
의존성주입 코드를 변경해주는것 만으로 큰 영향없이
상황에 맞는 서비스를 선택할 수 있게끔 작성하였다.


SOLID - L

Liscov Substitution Principle. 리스코프치환원칙 이라고 한다.

이것도 개방폐쇄원칙과 마찬가지로 이름을 쫌 쉽게 지었으면 좋았을텐데 하는 원칙인데
리스코프라는 사람이 만들어서 리스코프 치환원칙이라고 한다.

자료형A가 자료형B의 하위 자료형이라면
자료형B로 작성된 코드는 자료형A로 치환이 될 수 있어야 한다는 뜻이다.

예를들면 이런 상황입니다.
사각형이라는 클래스와 정사각형이라는 클래스가 있고
당연히 정사각형 클래스는 사각형을 상속하고 있다.

@AllArgsConstructor
@Setter
class Rectangle {
    protected int width;
    protected int height;
}

class Square extends Rectangle {
    public Square(int length) {
        super(length, length);
    }
}
Rectangle square = new Square(5);
square.setHeight(6);

그런데 만약 정사각형 클래스를 가로세로 5로 설정했는데
사각형클래스를 이용해서 세로를 6으로 변경하는 상황이 나오면
정사각형의 정의가 깨져버리기 때문에

상속을 하는 의미가 없어지고, 사각형 클래스는 정사각형 클래스로 치환이 불가능해진다.

인터페이스는 다른말로 어떠한 메소드가 어떤 객체를 반환하고
파라미터로는 무엇을 받는지를 정의한다는 의미로
계약이라고도 합니다.

즉 위 코드에서 setter는 메소드는 높이를 변경한다는 계약이 있는데,
이 계약이 상속을 하면서 파기되는 상황이 발생하는데
이러한 상황을 리스코프 치환원칙이라고 한다.


너무 당연한 얘기같지만, 몰입해서 개발하다보면 실수로 놓칠수 있는 내용이고 이런경우 디버깅이 굉장히 힘들어지기 때문에 더욱 주의해서 작성해야하는 원칙 중 하나라고 한다. 그래서 이펙티브자바에 '상속보단 컴포지션을 사용하라.'라는 말이 있다. 상속은 생각보다 신경써야할게 많고, 비용이 많이들기 때문에 그냥 가급적이면 피하는것도 하나의 팁이라고 제시하는 경우도 있다.

SOLID - I

Interface Sergregation Principle. 인터페이스분리원칙
너무편안하다.
이름부터 어떤의미인지 쉽게 파악이 되는데,

요약하면 하나의 큰 인터페이스보다는 여러개의 작은 인터페이스가 낫다 라는 원칙입니다.

개발을 하면서 가장 체감이 되었던 부분은 SpringSecurity의 UserDatails를 상속하여 구현하는데 있었다.

시큐리티의 UserDetails를 구현하면서 사용하는 대부분의 메소드는
권한정보, id, pw를 가져오는 세가지 메소드 이지만,
userDetails를 상속하는순간 계정이 만료되었는지, 사용중인지, 잠기진않았는지에 대한 구현도 함께 해야하는 경우가 생긴다.
사실 개인체감으로는 사용빈도가 낮은 메소드이지만, 어쩔수없이 상속을 했으니 구현을 해야했기때문에 무조건 return true로 설정할 수밖에 없는 상황이 발생한다.

public class CustomUserDetails implements UserDetails {

    @Getter
    private MemberResponse memberResponse;

    // form 로그인
    public CustomUserDetails(MemberResponse memberResponse) {
        this.memberResponse = memberResponse;
    }


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        //권한부
        return Collections.singletonList(new CustomGrantedAuthority(memberResponse.memberType().getRoleName()));
    }

    @Override
    public String getPassword() {
        return memberResponse.password();
    }

    @Override
    public String getUsername() {
        return memberResponse.username();
    }


    /**
     * == 사용하지 않지만 인터페이스를 상속하면서 구현을 할 수 밖에 없었다. ==
     */

    @Override
    public boolean isAccountNonExpired() {return true; }

    @Override
    public boolean isAccountNonLocked() {return true; }

    @Override
    public boolean isCredentialsNonExpired() {return true; }

    @Override
    public boolean isEnabled() {return true; }
}

물론 훌륭한개발자들이 어떠한 목적으로 설계를 했을테고
내가 함부로 평가해선 안될테지만,
저런 부분은 따로 분리하는게 낫지않나.. 싶은 생각이 든다.

물론 제가 함부로 평가할 수 없는 훌륭한 코드이지만
짧은 지식으로 전혀 사용하지 않는 메소드인데
저런 부분은 따로 분리하는게 낫지않을까... 싶은 생각이 들긴 합니다.


SOLID - D

Dependency Inversion Principle. 의존관계역전원칙 이다.

사실 이부분은 정말 길게 얘기가 나올 수 있는 주제라고 생각한다.

앞선 SOLI처럼 자신있게 어떤것이다 라고 말할 수 있는것과 달리
오히려 알면 알수록 더 모르겠다는 생각이 드는 원칙이다.

정말 간단하게 표현하면 객체의 생명주기를 스프링에게 넘겨서 대신 너가 적당히 관리해서 필요할때 IoC컨테이너에서 사용하는 하는것이고

아무튼 필드주입, 세터주입말고,
생성자주입이 좋더라. 정도로 대답을 할수도 있을거고
실제로 면접에서 이렇게 대답하는 사람도 제법 있을것 같다.

하지만 DI와 soild의 DIP는 전혀 다른 용어로써
용어자체가 다르고, 위의 경우는 DI를 활용한것이지
DIP를 지켰다고 보기는 다소 어려운 예제이다.

구현체에 바로 의존할 때 DIP가 제대로 지켜지지 않는 현상이 발생하는데,

예를들면
서비스가 리포지토리에 의존적이듯이

맥도날드라는 클래스는 햄버거를 만들어야 하므로
햄버거 쉐프라는 클래스를 필요로 할 수밖에 없다.

그러나
햄버거쉐프라는 클래스가 쉐프를 상속하고 있고,
맥도날드 클래스는 쉐프에 의존하고 있다면 아래와 같은 구조로 변경된다.

화살표의 방향이 반대로 되었다.
이를 의존관계가 역전되었다고 표현한다!

잘 생각해보면
맥도날드가 햄버거 쉐프에 의존하던 첫번째 경우에서
맥도날드가 쉐프를 의존하고, 햄버거 쉐프도 쉐프라는 인터페이스를 의존하게 된다..
햄버거쉐프는 의존을 받던 관계에서 의존을 하는관계로 변경이 되며 이를 의존관계역전이라고 표현한다.

이제 맥도날드 입장에선 쉐프가 어떤 아이인지 궁금해할 필요가 없어진다.
요리를 만든다. 만 알면되지 굳이 햄버거를 만든다, 피자를 만든다 까지 알 필요가 없어진다.
추상화가 제대로 이루어진 것이다.

첫째, 의존성역전은 상위모듈 하위모듈 모두 추상화에 의존해야한다.
위의 예시에서 맥도날드 클래스도 쉐프라는 추상화에 의존하고
햄버거 쉐프라는 클래스도 쉐프라는 인터페이스에 의존한다.

둘째, 세부사항에 의존하면 안되고 세부사항이 추상화에 의존해야한다.
라는 원칙이다.

profile
성숙해지려고 노력하지 않으면 성숙하기까지 매우 많은 시간이 걸린다.

0개의 댓글