밍쎄의 코딩공간
Service, ServiceImpl 본문
MemberService
public interface MemberService {
Member findById(MemberId id);
Member findByEmail(Email email);
void changePassword(PasswordDto.ChangeRequest dto);
Member updateName(MemberId id, Name name);
}
위 와 같은 인터페이스는 좋은 구조라고 생각지 않습니다. 우선 인터페이스를 두어서 얻는 이점은 세부 구현체를 숨기고 인터페이스를 바라보게 함으로써 클래스 간의 의존관계를 줄이는 것, 다형성을 사용 하는 것 이 핵심이라고 생각합니다.
조금 더 쉽게 정리하면 하나의 인터페이스를 구현하는 여러 구현체가 있고 기능에 따라 적절한 구현체가 들어가서 다형성을 주기 위함이 이라고 생각합니다. 또 하나의 인터페이스만 바라보니 의존관계도 줄일 수 있습니다.
하지만 인터페이스의 하나에 구현체 하나를 두면 의존관계를 줄이는 효과도 다형성을 주는 효과도 없습니다. 그렇다면 인터페이스 하나에 구현체 하나는 반드시 나쁜 구조냐의 답에는 그렇지 않다고 생각합니다. 이 부분에 대한 설명은 아래에서 진행하겠습니다.
위의 인터페이스의 문제점은 무엇일까요? 저의 생각은 인터페이스의 책임이 너무 많은 것이 문제라고 생각합니다. 저 인터페이스의 구현체가 두 개 이상이 되려면 해당 구현체가 다른 기능을 가져야 합니다. findById, findByEmail, changePassword 의 메소드들이 다른 구현 클래스가 들어갔다고 해서 다른 기능을 수행해야할까요? findById의 기능은 PK 값으로 해당 Member를 찾는 것입니다. id 값이 1번인 Member가 철수라면 무슨 구현체를 쓰든 철수가 나와야 하는 너무나도 당연하죠.
그렇다는 것은 위의 메소드들은 인터페이스에 대상이 되지 않습니다. MemberService의 기능을 전부 다른 기능으로 대체 가능한 세부 구현체는 현실적으로 존재하기 어렵습니다. 예외가 있다면 테스트를 위해서 Mock 으로 추상화시킬 수는 있습니다.
하나의 인터페이스의 하나의 구현체
그렇다면 위의 내용은 하나의 구현체를 갖는 경우에는 인터페이스를 둘 필요가 없다는 것으로 결론을 내는 것처럼 보이지만 하나의 구현체만 갖더라도 인터페이스를 사용하는 것이 바람직하다고 생각합니다. 아래와 같은 가정으로 설명하겠습니다.
요구사항
- 신한 카드결제 기능이 필요하다.
- 앞으로 결제 가능한 카드가 지속해서 추가될 예정이다.
public interface CardPaymentService {
void pay();
}
public class ShinhanCardPaymentService implements Card{
private ShinhanCard shinhanCard;
@Override
public void pay() {
shinhanCard.pay(); //신한 카드 결제 API 호출
// 결제를 위한 비즈니스 로직 실행....
}
}
위처럼 카드 인터페이스를 두고 신한카드 구현체를 하나만 갖지만 향후 추가 적으로 생길 여지가 있으니 인터페이스를 두는 것이 바람직합니다.
그렇다면 앞으로 추가될 여지가 없다고 판단된다면 어떻게 해야 할까요? 저의 생각은 정말 추가될 여지가 없나 에 대한 꽤 깊은 고민이 필요하다고 생각합니다. 개발 관점에서만 보는 것이 아니라 도메인 관점에서 해당 도메인을 가장 잘 이해하고 있는 분과 이야기를 충분히 하고 결론짓는 것을 추천합니다.
그런데도 추가될 여지가 없다고 판단하면 저 같은 경우에는 인터페이스를 두지 않습니다. 추가되더라도 그 때 인터페이스를 두는 것은 그다지 어렵지 않거니와 애초에 모든 변경에 대응할 수 있는 구조라는 건 없다고 생각합니다. 픽스 시킬 것은 픽스 시켜야 추상화가 되고 그 추상화 기반으로 다형성을 가질 수 있다고 저는 생각합니다.
인터페이스는 어떻게 두어야 하는가?
위에서 MemberService 인터페이스가 책임이 너무 크다고 했습니다. 그렇다는 건 MemberServiceImpl 또한 책임이 너무 큰 것입니다. 해당 인터페이스의 기능을 모두 구현하고 있으니 말이죠. DomainService로 두는 것은 좋은 구조라 생각하지 않습니다. 물론 그다지 중요하지 않는 도메인에는 Service로 가는 것은 크게 상관없습니다. 하지만 핵심 도메인들은 다양한 구현체를 갖게 됩니다. 위에서 설명했듯이 다양한 구현체를 같은 인터페이스를 갖게 하려면 그 책임이 작아야 합니다.
public class MemberFindService {
private MemberRepository memberRepository;
public Member findById(final MemberId id) {
final Member member = memberRepository.findOne(id);
if (member == null) throw new MemberNotFoundException(id);
return member;
}
public Member findByEmail(final Email email) {
final Member member = memberRepository.findByEmail(email);
if (member == null) throw new MemberNotFoundException(email);
return member;
}
}
위의 코드는 조회만을 위한 서비스 클래스입니다. 위의 메소드들은 일반적으로 다양한 구현체를 갖기는 어렵습니다. 그런 것들은 인터페이스에 대상이 아니라고 생각합니다.
그렇다면 비밀번호 변경 기능은 어떨까요? 비밀번호 변경 기능은 대표적으로 다음과 같습니다.
- 비밀번호 기반으로 비밀번호를 변경하는 기능
- 비밀번호를 잃어버렸을 때 다른 인증 기반으로 비밀번호를 변경하는 기능
비밀번호를 변경하는 방식이 일반적으로 2개 이상입니다. 즉 구현체를 2개 이상 갖게 되고 이럴 때 인터페이스를 두는 것이 바람직하다고 생각합니다.
public interface ChangePasswordService {
public void change(MemberId id, PasswordDto.ChangeRequest dto);
}
public class ByAuthChangePasswordService implements ChangePasswordService {
private MemberFindService memberFindService;
@Override
public void change(MemberId id, PasswordDto.ChangeRequest dto) {
if (dto.getAuthCode().equals("인증 코드가 적합한지 로직 추가...")) {
final Member member = memberFindService.findById(id);
final String newPassword = dto.getNewPassword().getValue();
member.changePassword(newPassword);
// 필요로직...
}
}
}
public class ByPasswordChangePasswordService implements ChangePasswordService {
private MemberFindService memberFindService;
@Override
public void change(MemberId id, PasswordDto.ChangeRequest dto) {
if (dto.getPassword().equals("비밀번호가 일치하는지 판단 로직...")) {
final Member member = memberFindService.findById(id);
final String newPassword = dto.getNewPassword().getValue();
member.changePassword(newPassword);
}
}
}
ChangePasswordService 책임은 비밀번호를 변경하는 것입니다. 해당 구현체들은 본인의 비밀번호 변경의 필요한 인증방식을 구현하고 최종적으로 비밀번호를 변경하게 됩니다. 이것을 인터페이스를 둘 수 있는 이유는 인터페이스의 책임이 하나이기 때문입니다.
그렇다면 하나의 인터페이스에는 하나의 메소드만 갖게 되는 것이냐? 라는 질문을 하게 됩니다. 그렇지는 않습니다. 다시 한번 카드 예제로 돌아가겠습니다.
public interface CardPaymentService {
void pay();
void cancel();
}
일반적으로 카드는 결제가 있으면 반드시 취소 기능도 함께 있습니다. 결제 취소가 되지 않는 것은 상식적으로 이해하기 어렵습니다. 그것은 신한 은행 이외의 카드사들도 당연히 결제 취소 기능이 있다는 것입니다. 그렇다면 카드 결제는 위와 같이 인터페이스를 두고 세부 구현체에 따라서 카드 결제를 진행하게 됩니다.
https://cheese10yun.github.io/spring-oop-04/#service-serviceimpl-1
Spring OOP 프로그래밍 예제(4) - Service, ServiceImpl 구조에 대한 고찰 - Yun Blog | 기술 블로그
Spring OOP 프로그래밍 예제(4) - Service, ServiceImpl 구조에 대한 고찰 - Yun Blog | 기술 블로그
cheese10yun.github.io
'개념정리' 카테고리의 다른 글
실전 배당금 프로젝트 개념 (초기단계) (0) | 2023.08.27 |
---|---|
스프링부트를 통해 계좌 만들기 (0) | 2023.08.27 |
스프링 핵심 원리 - 싱글톤 (2) | 2023.08.20 |
스프링 핵심 원리 기본편 정리 (0) | 2023.08.20 |
깊이 우선 탐색 ( DFS ) (0) | 2023.08.13 |