[글의 의도]
공부할 양이 너무 많다는 걸 깨닫고 방황하며 이리저리 패스트캠퍼스 강의를 듣다가 어디선가 주워 들어본 내용의
'유지보수하기 좋은 코드 디자인 ' - '적절한 객체의 크기를 찾아가는 여정' 강의를 우연히 들었다. 강의 내용을 어디서 들어봤나 싶었는데 4개월 전 부트캠프 초반 멘토님께서 과제를 리뷰해줄 때 들었던 내용과 일치하여 블로그에 작성해보기로 했다. 강의의 주 내용은 흔히 개발을 할 때, Controller, Service, Repository 계층으로 개발을 진행한다. 이 때 MemberService를 인터페이스로 선언하고 구현체로 MemberServiceImpl을 사용했었는데 이에 관한 인터페이스 내용이다.
[가물가물한 멘토님의 피드백 내용]
내 기억속에 KakaoAPI를 활용한 도서 검색 토이프로젝트를 주제로 한 피드백 멘토링 시간이었다. 멘토님께서 우리 그룹스터디의 팀원 한 명씩 개인 과제를 리뷰하면서 이야기를 나눴던 기억이 난다. 그 때 상황은 아마 팀원중에서 BookService를 개발할 때, BookServiceImpl로 클래스 명을 지은 코드를 보고 멘토님께서 ~~Impl 명명법은 별로 좋지 않다. 정도로만 기억을 하고있다. 그 당시엔 끄덕끄덕하고 지나갔지만 지금 다시 생각해보니 뭐라 하셨는지 기억이 안난다.. 나는 그저
김영한님 강의에서 Impl을 쓰는 구나.. 어 팀원분도 이런 명명법을 쓰셨구나.. 어 멘토님이 쓰지 말라시네.. 이런 몹쓸 메모리만 남았다.
다시 기억을 되찾고자 멘토님께서 남기신 코멘트를 찾았고 멘토님의 자세한 리뷰는 다음과 같았다.
[인터페이스를 왜 쓸까?]
강의에서 예시로 드는 상황은 위 사진과 같다. PaymentController가 실제 구현체가 아닌 인터페이스 PaymentService를 참조하는 이유는 다음과 같다.
1. 세부 구현체를 숨기고 인터페이스를 바라보게 함으로써 클래스 간의 의존 관계를 줄임.
2. PaymentService를 구현하는 여러 구현체가 있고 기능에 따라 적절한 구현체가 들어가 다형성을 주기 위함
만약 PaymentController가 구현체를 참조한다면, 구현체의 개수가 늘어날 수록 맺어야하는 의존 관계가 늘어난다. 그러므로 의존성이 지나치게 많아지니깐 인터페이스를 사용해서 확장, 유지보수에 장점을 가진다. PaymentController 입장에서신한, 우리 등등 누가 어떻게 구현을 하든 pay()라는 기능만 수행할 수 있으면 장땡인 것이다.
강의는 이 부분에서 대체성이라는 단어를 소개했다. 이 구조에서 PaymentService는 책임이 명확하다. 다들 pay()라는 기능을 수행하는 책임이 있다. 책임이 명확하면 대체성이 높다. 이 말은 대체성을 띄고 있다는 말로 해석된다. 즉 쉽게 대체가 가능하다 == 맡은 책임이 명확하다 로 볼 수 있다.
[역할, 책임이 너무 많은 인터페이스]
강의에서 보여준 MemberService 예제는 위 사진과 같다. MemberService 에서는 Member 와 관련한 모든 동작을 정의하고 있다.
1. findById
2. findByEmail
3. changePassword
4. updateName
MemberService가 정의하는 위 4가지의 메서드는 아래 2개의 기준으로 분류를 할 수 있을 것 같다.
- CRUD 관점 (작성, 조회, 수정, 변경, 삭제)
- 2개 이상의 방법으로 구현이 가능한지? 유일한 방법인지.. 다른 대안이 있는지..
첫 번째 관점으로 메서드를 분류 하면 다음 표와 같다.
조회 | findById, findByEmail |
수정 | updateName |
변경 | changePassword |
수정, 변경을 같이 둘 수도 있지만 일단 이렇게 설정을 해두고 진행하자. 이렇게 두고 본다면 MemberService 인터페이스를 MemberFindService, MemberSignUpService, MemberEditService 와 같이 여러 개로 분리함으로써 모호한 MemberService의 책임 범위를 세세하게 쪼갤 수 있을 것이다.
두 번째 관점으로 다른 여지가 없는? 인터페이스를 찾아보자. 이 부분은 예제임으로 극단적으로 판단해서 진행해보자.
1번, 2번 그리고 4번 메서드의 경우 고유한 id, Email 값으로 Member를 찾는 역할을 한다. 이는 대체할 방법이 없다고 볼 수 있다. 여기서 대체할 방법이 있다 없다를 예로 들면 (집 -> 학교 가능 방법) 을 생각해 봤을 때 걸어서 가거나, 버스를 타거나, 지하철을 타거나 여러 방법을 생각할 수 있는데 1번, 2번, 4번의 경우는 살짝 극단적이지만 대체할 방법이 없다고 볼 수 있다.
즉 대체할 방법이 없는 메서드들은 인터페이스로 선언하는 것이 아닌 그냥 바로 구현체로 설정해도 괜찮을 것 같다는 의견이다.
public class MemberFindService{
public Member findById(Long id){
}
public Member findByEmail(Long id){
}
}
반면에 changePassword의 경우 대체할 경우가 2개 이상 존재한다고 볼 수 있다. 현재 비밀번호로 변경을 한다던지, SNS 연동을 통해 인증을 하고 변경을 한다던지.. 여러 가지 방법으로 비밀번호를 변경할 수 있다고 볼 수 있다. 그렇기에 changePassword는 구현체가 아닌 인터페이스로 개발을 진행할 수 있을 것 이다.
public interface ChangePasswordService{
void change(Long id, PasswordChangeRequest dto);
}
//////////////////////// 인증 방식
public class ByAuthChangePasswordService implements ChangePasswordService{
@Override
public void change(Long id, PasswordChangeRequest dto){
}
}
//////////////////////// 비밀번호 방식
public class ByPasswordChangePasswordService implements ChangePasswordService{
@Override
public void change(Long id, PasswordChangeRequest dto){
}
}
[결론]
내가 개발하는 프로젝트의 인터페이스와 예제의 MemberService 인터페이스는 너무 많은 책임을 지고있는지도 모른다.
해당 글을 쓰면서 정확하게 모두 작성하진 못했지만, 어느정도 인터페이스를 설계할 때 주의해야 할 점에 대해 알게된 것 같다.
서비스 인터페이스를 쪼개라 -> 책임을 명확하게 해라 -> 행위 기반으로 서비스를 표현해라
다른 대체제가 없다면 인터페이스로 선언하지 말고 구현체로 선언해라
인터페이스 이름으로 역할, 책임을 표현할 수 있다.