본문 바로가기
내가 공부하려고 올리는/스프링

나는 ServiceImpl을 잘 활용했는가?

by 결딴력 2022. 7. 11.
반응형

 

✅ 나는 ServiceImpl을 잘 활용했는가?


스프링 프로젝트를 진행할 때

비즈니스 계층(Business Layer)에서 Service 클래스를 만들어 사용했다.

 

이때 서비스 클래스는

  1. Service Interface

  2. Service Interface 구현 객체

로 나뉜다.

 

실제 내가 진행했던 프로젝트의 패키지이다.

 

Service 패키지

모든 서비스 클래스는 인터페이스와 그 구현 객체로 나뉜다.

 

관습적으로 서비스 클래스를 '구현''역할'로 나누어 설계한 점도 있지만

나누게 된 나름의 목표는 있었다.

 

우선 자바의 다형성을 살리는 것이 하나의 목표였다.

클래스를 설계하면서 때에 따라 필요한 인터페이스만을 가져와서

컨트롤러단의 코드를 깔끔하게 하면서 

여러 부품을 필요할 때마다 갈아 끼우듯 코딩하는 것이 1차 목표였다.

 

다른 목표는 클래스 간 결합도를 낮추며 인터페이스에 의존하는 코드를 작성하는 것이었다.

실제 내 프로젝트의 로그인 컨트롤러의 모습이다.

public class LoginController {

    private final MemberService memberService;
    private final NaverService naverService;
    private final KakaoService kakaoService;
    
    
    ...중략

 

private final로 인터페이스 객체를 선언만 하면

구현 객체를 자유자재로 사용할 수 있다.

컨트롤러 입장에서는 MemberService 인터페이스의 구현 객체가

어떻게 생겼는지는 알아야 할 이유가 없이 가져다 쓰기만 하면 된다.

 

이 목표를 가지고 Service와 ServiceImpl, 즉 역할과 구현을 나누어 설계했다.

 

쓰다 보니 느끼게 된 장점은 2가지였다.

 

1. 협업할 때 좋다.

 

예를 들어 인터페이스를 다음과 같이 구현했다고 치면

public interface MemberService {

  Member findMemberById(Integer id);

}

누가 봐도 id를 이용해 Member 객체를 반환한다는 것을 알 수 있다.

 

때문에 내가 짠 코드 혹은 남의 짠 코드가 어떤 식으로 구현되어있는지 파악하여

사용성을 높이기 굉장히 좋았다.

 

2. 한눈에 파악하여 기능을 확장하기 좋다.

 

인터페이스는 위의 코드와 같이 역할만이 명시되어 있기 때문에

구현 코드에 대한 내용은 빠져서 해당 서비스 클래스를 한눈에 파악하기 좋다.

 

때문에 새로운 기능이 필요할 때

구현하려는 기능이 기존 구현한 코드와 겹치지 않는지 파악하기가 좋았다.

 

그렇다면 나는 Service 인터페이스와 ServiceImpl 구현 객체의 맛을 살려서 개발을 했을까?

 

 

 

 

 

✅ 내 코드의 문제점


내가 내린 결론은 '정말 아니오'이다.

 

아까 위에서 보여줬던 내 서비스 패키지 모습을 다시 한번 보자

Service 패키지

패키지 구성만 보더라도 많은 문제가 있어 보인다.

우선 해당 클래스가 'SRP(Single Responsibility Principle, 단일 책임 원칙)'를 만족할지 의문이다.

해당 구현 객체가 기능이 확장되고 재사용될 수 있어 보이지 않는다.

 

 

로그인 인터페이스가 있다고 생각해보자.

해당 로그인 인터페이스에서 구현해야 할 부분은 다음과 같을 것이다.

 

//로그인 인터페이스 예시
public interface LoginService {
	
    Member login(Integer id);
}

 

만약 좋은 인터페이스와 그 구현체를 구현했다고 가정하고

구현 클래스를 예시로 만들어보면 다음과 같다.(네이버/카카오/일반 로그인)

 

//네이버 로그인
public class NaverLoginService implements LoginService {
	
    @Override
    public Member login(Integer id) {
    	//.. 네이버 로그인 비즈니스 로직
        return member;
    }

}

//일반 로그인
public class PublicLoginService implements LoginService {
	
    @Override
    public Member login(Integer id) {
    	//.. 일반 로그인 비즈니스 로직
        return member;
    }

}

//카카오 로그인
public class KakaoLoginService implements LoginService {
	
    @Override
    public Member login(Integer id) {
    	//.. 카카오 로그인 비즈니스 로직
        return member;
    }

}

 

위의 코드는 SOLID 5원칙의 'OCP(Open-Closed Principle, 개방 폐쇄 원칙)'를 만족한다.

기능을 확장해야 할 때 기존 코드를 수정하는 것이 아니라 새로운 구현 클래스를 만들었기 때문이다.

 

'SRP(Single Responsibility Principle, 단일 책임 원칙)'를 만족한다.

물론 SPR를 만족하는 클래스가 반드시 하나의 메서드만으로 구현되어야 하는 것은 아니지만

변경이 있을 때 파급 효과가 적어야 하는 SRP 측면에서 해당 클래스 들은 SRP를 만족한다.

 

하지만 내가 사용한 Service와 ServiceImpl은 이러한 원칙을 만족하지 못한다.

 

 

내가 짠 Service 인터페이스의 한 예이다.

public interface MemberService {

    //회원가입
    MemberDto join(MemberDto memberDto) throws Exception;

    //소셜 회원 가입
    SocialDto joinSocial(SocialDto socialDto) throws Exception;
    
    
    ... 중략

 

회원가입과 소셜 회원 가입을 나누고 싶어서 하나의 인터페이스에 두 개의 메서드를 함께 구현했다.

때문에 MemberService를 상속받는 MemberServiceImpl 객체는 두 개의 메서드를 모두 구현해야 한다.

 

위의 모범 예시에 맞춰서 설계한다면

JoinService라는 하나의 인터페이스에 Join이라는 메서드를 선언하고,

JoinService를 상속받는 일반 회원가입 구현 객체와 소셜 회원가입 구현 객체를 따로 만들었어야 했다.

 

내가 작성한 Serivce 인터페이스는 필요할 때 다른 구현 객체를 선택할 수 있는

자바의 다형성을 활용하는 것이랑은 근본적으로 거리가 멀다.

 

 

 

 

 

 

✅ 모든 구현 클래스는 인터페이스를 가져야 하나?


또 하나 문제점은 '모든 구현 클래스가 인터페이스를 가져야 하나?'라는 문제다.

결국 인터페이스와 구현 객체를 나누는 목적은 역할과 구현을 나누어 기능을 확장하는 데 있다.

 

자동차가 있으면 k3를 구현하고 k5도 구현하고 제네시스도 구현하는 것처럼

어떤 역할에 따른 구현체가 다양할 때 인터페이스로써 의의를 갖는다.

 

그렇다면 '비밀번호와 아이디를 이용해 Member 객체 찾기'는 어떨까?

인터페이스를 두는 의의가 있을까?

 

내가 내린 결론은 '의의가 크지 않다.'이다.

 

의의가 아예 없지는 않을 것 같다.

 

컨트롤러에서는 인터페이스를 의존하게 된다.

즉, 컨트롤러가 '구체화'가 아닌 '추상화'에 의존한다는 것이고

SOLID 5원칙인 'DIP(Dependency Inversion Principle, 의존관계 역전 원칙)'를 만족한다는 것이다.

 

의의가 크지 않다고 생각하게 된 계기는,

예를 들어,  '비밀번호와 아이디를 이용해 Member 객체 찾기'는

다양한 구현체를 갖기 힘들기 때문이다.

 

앞서 언급한 로그인 방식은 네이버 로그인, 카카오 로그인 등

서로 다른 로직을 갖는 다양한 구현체를 구현할 수 있다.

 

반면, '비밀번호와 아이디를 이용해 Member 객체 찾기'의 경우

기능이 확장될 가능성이 낮다.

이는 추상화가 힘들다는 것으로 인터페이스를 갖지 않아도 무리는 없을 것 같다.

 

하지만 앞서 언급한 DIP의 측면에서 볼 때는

모든 구현 클래스를 인터페이스로 만들어야 할 것 같다.

 

현재 진행한 프로젝트는 계층형 아키텍처 구조로 각 계층이 나뉘어있고,

컨트롤러에서 서비스 계층을 호출하게 되는 경우 인터페이스를 통해 호출된다.

따라서 해당 클래스가 비즈니스 계층에 속할 경우,

인터페이스를 만드는 것이 DIP를 지킬 수 있는 설계 방향이 될 것 같다.

 

 

 

 

 


 

오늘 정리한 내용은

프로젝트를 진행하면서 개인적으로 생각하고 알아본 내용입니다.

 

제 생각이 많이 담겨 있는 글이기 때문에

내용적으로 오류가 있을 수 있다는 생각이 듭니다.

 

때문에 혹시 글을 읽고 내용에 오류가 있다는 생각이 드신다면

댓글로 남겨주시면 정말 정말 많은 도움이 될 것 같습니다.

 

반응형

댓글