프로젝트를 더욱 S.O.L.I.D 하게 수정해보기
SOLID는 다음 5가지 원칙을 말합니다.
- SRP : 단일 책임 원칙(Single Responsibility Principle)
- OCP : 개방 폐쇄 원칙(Open Closed Principle)
- LSP : 리스코프 치환 원칙(Liskov Substitution Principle)
- ISP : 인터페이스 분리 원칙(Interface Segregation Principle)
- DIP : 의존관계 역전 원칙(Dependency Inversion Principle)
따라서 SOLID하게 라는 말이 맞지는 않지만견고하게 만든다는 뜻에서 한번 사용해봤습니다 😅
오늘 수정해볼 부분은 Service 클래스와 그 구현체들을
SOLID 원칙, 특히 SRP 원칙에 맞춰 리팩토링 하는 시간을 가져보려고 합니다.
✅ 단일 책임 원칙(SRP)
우선 단일 책임 원칙에 대한 명확한 정의를 하려고 합니다.
SRP는 한 클래스가 하나의 책임만 가져야 한다는 뜻입니다.
여기서 '책임'은 무엇을 뜻하는 걸까요?
예를 들어 한 클래스에 여러 개의 메소드가 있다면 복수의 책임을 지는 클래스일까요?
또는 클래스가 다중상속을 한다면 복수의 책임을 갖는 걸까요?
SRP에서 말하는 '단일 책임'은 측정할 수 있는 개념은 아닙니다.
즉 명확한 단일 책임은 존재하지 않는 것이죠.
따라서 다중 메서드가 있는 클래스여도 경우에 따라 SRP를 지킬 수도 아닐 수도 있는 것이죠.
SRP의 핵심적인 개념은 메서드의 수나 상속의 수가 아니라
'변경'입니다.
해당 클래스에 '변경'이 일어났을 때 '파급 효과'가 중요한 것이죠.
SRP를 잘 지키는 클래스라면 변경이 발생해도 다른 클래스에
일어나는 파급 효과가 매우 한정적일 것입니다.
그렇다면 다중 메서드를 구현해야 할 때,
SRP를 지키려면 어떻게 해야할까요?
바로 '액터(Actor)'를 기준으로 책임을 나누는 것입니다.
예를 들어 보겠습니다.
음식점에 있다고 가정할 때 발생할 수 있는 행위를 나열해보겠습니다.
- 음식을 주문한다.
- 음식을 만든다.
- 음식을 서빙한다.
- 음식을 먹는다.
- 음식값을 지불한다.
SRP를 지키지 않은 클래스는 위의 동작이 한 클래스에 담길 겁니다.
반면 SRP를 지킨 클래스의 경우는 어떨까요?
앞서 말한 듯이 액터의 기준으로 바라보겠습니다.
여기서 존재하는 액터는 두 명입니다.
음식을 주문하고 먹고 가격을 지불하는 '손님'
그리고 음식을 만들고 서빙하는 '주인'
따라서 SRP를 지키면 클래스를 만든다면
두 개의 클래스를 만들어 각각 다음과 같이 나누는 것이 바람직할 것입니다.
손님
- 음식을 주문한다.
- 음식을 먹는다.
- 음식값을 지불한다.
주인
- 음식을 만든다.
- 음식을 서빙한다.
✅ 프로젝트 수정하기
'액터'에 맞춰 기존 프로젝트를 리팩토링 해보겠습니다.
기존 BoardService 객체입니다.
public interface BoardService {
//전체 스터디 글 개수
Integer getCount();
//작성한 글 개수 BY 회원아이디
Integer getCountById(int id);
//모집 중인 전체 스터디 글 가져오기
List<BoardDto> getStudyBoardList(Integer id, Pagination pagination);
//회원 게시글 가져오기
List<BoardDto> getMyStudyBoardList(Integer id, Pagination pagination);
//위시리스트 스터디 글 가져오기
List<BoardDto> getWishlistBoardListAll(Integer id, List<WishlistDto> wishlist, Pagination pagination);
//게시글 정보 가져오기
BoardDto findResultById(int study_id, BoardDto boardWriteDto);
//게시글 정보 가져오기(BoardDto 값이 없을 때)
BoardDto findStudyById(int study_id);
//게시글 추가하기
Integer insertStudyBoard(BoardDto boardWriteDto) throws Exception;
//게시글 수정하기
BoardDto updateBoard(BoardDto boardDto) throws Exception;
//게시글 삭제하기
Integer deleteBoard(int study_id) throws Exception;
//검색 내용에 따른 게시글 개수
Integer getCountBySearching(SearchDto searchDto);
//검색한 스터디 글 가져오기
List<BoardDto> getSearchStudyBoardList(Integer id, SearchDto searchDto, Pagination pagination);
}
딱 봐도 하나의 클래스가 너무 많은 책임을 지고 있는 것 같죠..?
이런 기존 클래스를 조금 더 SOLID 원칙에 맞게 리팩토링 해보겠습니다.
우선 기존 BoardService 객체를 다음과 같이 나눴습니다.
하나하나 인터페이스 객체만 살펴보겠습니다.
BoardService 인터페이스는 다음과 같습니다.
public interface BoardService {
//게시글 추가하기
Integer insertStudyBoard(BoardDto boardWriteDto) throws Exception;
//게시글 수정하기
BoardDto updateBoard(BoardDto boardDto) throws Exception;
//게시글 삭제하기
Integer deleteBoard(int study_id) throws Exception;
}
GetBoardService 인터페이스는 다음과 같습니다.
public interface GetBoardService {
//전체 스터디 글 개수
Integer getCount();
//작성한 글 개수 BY 회원아이디
Integer getCountById(Integer id);
//게시글 정보 가져오기
BoardDto findResultById(Integer study_id, BoardDto boardWriteDto);
//게시글 정보 가져오기(BoardDto 값이 없을 때)
BoardDto findStudyById(Integer study_id);
}
SearchBoardService 인터페이스는 다음과 같습니다.
public interface SearchBoardService {
//검색 내용에 따른 게시글 개수
Integer getCountBySearching(SearchDto searchDto);
//검색한 스터디 글 가져오기
List<BoardDto> getSearchStudyBoardList(Integer id, SearchDto searchDto, Pagination pagination);
}
GetBoardListService 인터페이스는 다음과 같습니다.
public interface GetBoardListService {
//모집 중인 전체 스터디 글 가져오기
List<BoardDto> getBoardList(Integer id, Pagination pagination);
}
BoardService 인터페이스 부터 보겠습니다.
기존 BoardService 인터페이스에서 게시글의 수정, 삭제, 추가 기능만 남겼습니다.
액터의 기준을 '게시글을 작성하는 회원'에게 두었습니다.
GetBoardService 인터페이스에서는
게시글의 개수를 가져오고 게시글 단일 객체를 가져오는 기능을 분리했습니다.
게시글 목록을 가져오는 것도 함께 구현하려고 했는데
목록을 구현하는 부분은 GetBoardListService에 따로 구현하기로 했습니다.
이 부분은 뒤에 후술하겠습니다.
액터의 기준은 '게시글을 조회하는 회원'입니다.
SearchBoardService 인터페이스에서는 게시글 검색과 관련된 기능을 분리했습니다.
액터의 기준은 게시글을 검색하는 사람입니다.
마지막으로 GetBoardListService 인터페이스입니다.
기존 프로젝트에서 게시글 목록을 조회하는 메서드는 다음과 같이 3개 였습니다.
//모집 중인 전체 스터디 글 가져오기
List<BoardDto> getStudyBoardList(Integer id, Pagination pagination);
//회원 게시글 가져오기
List<BoardDto> getMyStudyBoardList(Integer id, Pagination pagination);
//위시리스트 스터디 글 가져오기
List<BoardDto> getWishlistBoardListAll(Integer id, List<WishlistDto> wishlist, Pagination pagination);
위와 같은 3개의 메서드는 모두 List<BoardDto> 객체를 똑같이 반환하기 때문에
기능에 맞춰 서로 다른 구현체로 구현하는 것이 자바의 다형성에 더욱 어울리는 설계 방식입니다.
따라서 List<BoardDto>를 반환하고 'Integer'객체와 'Pagination'객체를 파라미터로 전달받는
서로 다른 구현체를 구현하여 클래스를 분리하기로 결정했습니다.
수정한 Service 인터페이스는 위에서 봤던 이 코드입니다.
public interface GetBoardListService {
//모집 중인 전체 스터디 글 가져오기
List<BoardDto> getBoardList(Integer id, Pagination pagination);
}
GetBoardListService 인터페이스의 경우
액터의 기준은 굳이 따지자면 '게시글 목록을 조회하는 회원'이지만
더 정확히는 다형성을 살리기 위해 인터페이스를 따로 추출했다고 보면 될 것 같습니다.
✅ 인터페이스의 구현체가 여러 개라면(같은 타입의 빈이 여러개라면?)
인터페이스의 경우 구현체가 여러 개일 경우 빈으로 등록할 때 주의가 필요합니다.
저는 'GetAllStudyListImpl' 클래스에 @Primary 어노테이션을 붙여 기본 빈으로 등록하고
나머지 두 개에는 @Qualifier 어노테이션을 붙여 필요할 때 사용할 수 있도록 등록했습니다.
✅ 인터페이스를 리팩토링한 결과
하나의 인터페이스와 하나의 구현체로 구성되어 있던
Service 객체를 여러 개의 인터페이스와 여러 개의 구현체로 리팩토링 해봤습니다.
리팩토링을 하고 느낀 점을 정리하면 다음과 같습니다.
- 수정을 하고 나니 각 Service 인터페이스와 클래스가 역할에 따라 조금 더 명확해졌다.
- 인터페이스를 역할에 따라 분리하여 각 인터페이스의 책임이 덜어졌다.(SRP 원칙 준수)
- 필요할 때 필요한 구현체를 주입받아 사용하기 때문에 자바의 '다형성'을 활용할 수 있었다.
- 하나의 범용 인터페이스에서 역할에 따라 인터페이스를 분리했기 때문에
- ISP(인터페이스 분리 원칙)를 지키며 개발할 수 있었다.
- OCP(개방 폐쇄 원칙)를 지키면 개발할 수 있었다. ➡️ 기능의 확장이 필요할 때 코드를 수정하는 것이 아닌 구현체를 새로 만듦