Service, ServiceImpl

Spring 프로젝트를 하면 종종 관례적으로 Service를 interface로 기능 명세를 한 뒤 ServiceImpl에 기능을 구현하게 되는 Factory Pattern을 사용하게 됩니다.

interface는 기능을 추상화하여 클래스간 결합도를 낮추어 주고, 협업 시 업무분담도 용이합니다.
게임으로 예를 들면 스타크래프트에서 모든 유닛의 기본적인 특성 HP, 이동하기를 interface로 기능만 명시하고 각각 분업하여 유닛에 대한 HP나 이동속도를 구현할 수 있습니다.

하지만 일반적인 Spring 웹프로젝트에서는 Service interface는 1:1 구조인 경우가 많습니다. 만약 확장성을 고려한 1:N의 경우에는 interface로 가는 것이 좋지만 너무 막연한 경우에는 그냥 class로 생성 후 추후 시나리오 변경 또는 로직상 확장성이 필요한 경우 interface로 변경하는 것이 좋다고 생각합니다.

그렇다면 interface를 사용하는 경우는 어떤 경우에 사용해야할까요??

보통 하나의 기능에서 여러 곳으로 파생되는 것을 interface로 나누는게 좋을 것 같습니다.
예를 들어 소셜로그인, 패스워드 변경(개인정보수정, 패스워드찾기), 아이디 찾기(휴대폰 인증, 이메일 인증, 기타 등등), 카드 결제(카드사 별 결제 취소), 게임(게임별 플레이, 종료)등이 있습니다.
공통적으로 쓰이는 기능을하나의 기능에서 충분히 확장될 수 있는 경우 interface를 사용하는 것이 좋습니다.

로그인의 기능을 만들 때 Spring Security의 OAuth2를 이용하여 보통 기능 구현을 합니다.

그렇지만 oauth2를 사용하지 않고 기능을 구현하는 경우를 샘플 코드를 이용하여 설명해보겠습니다.

먼저 로그인 유형에 대한 정의를 합니다.

1
2
3
4
5
6
7
public enum AuthProvider {
  local,
  google,
  kakao,
  github,
  naver
}

그런 뒤 LoginService로 인터페이스를 생성합니다.

1
2
3
public interface LoginService {
  LoginDto.Response login(LoginDto.Request dto);
}

그리고 소셜에 따라 로그인 서비스에 맞게 구현 로직을 추가합니다. 이제 로그인 요청이 들어오면 해당 소셜 로그인이 동작하도록 LoginFactory 클래스를 생성합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public LoginService getLoginType(LoginDto.Request dto) {
  if (dto == null) {
    return null;
  }

  if (dto.getAuthProvider().equals(AuthProvider.github)) {
    return new LoginGithubService();

  } else if (dto.getAuthProvider().equals(AuthProvider.google)) {
    return new LoginGoogleService();

  } else if (dto.getAuthProvider().equals(AuthProvider.kakao)) {
    return new LoginKakaoService();

  } else if (dto.getAuthProvider().equals(AuthProvider.naver)) {
    return new LoginGoogleService();

  } else if (dto.getAuthProvider().equals(AuthProvider.local)) {
    return new LoginLocalService();
  }

  return null;
}

안의 내용들은 구현하지 않고 해당 로직을 타는지 print만 했습니다.
위와 같이 구현하면 dto 요청에서 소셜 로그인 정보와 일치하는 Service의 로직이 동작합니다.

Test코드로 확인하기 위해 정상적으로 하는지 확인해봅니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Test
void 로그인_타입_테스트() {
  LoginFactory loginFactory = new LoginFactory();
  
  LoginDto.Request request = new LoginDto.Request();
  
  LoginService loginService = new LoginLocalService();
  
  request.setAuthProvider(AuthProvider.github);
  loginService = loginFactory.getLoginType(request);
  loginService.login(request);
  
  request.setAuthProvider(AuthProvider.google);
  loginService = loginFactory.getLoginType(request);
  loginService.login(request);
  
  request.setAuthProvider(AuthProvider.kakao);
  loginService = loginFactory.getLoginType(request);
  loginService.login(request);
  
  request.setAuthProvider(AuthProvider.naver);
  loginService = loginFactory.getLoginType(request);
  loginService.login(request);
  
  request.setAuthProvider(AuthProvider.local);
  loginService = loginFactory.getLoginType(request);
  loginService.login(request);
}

테스트 코드를 통해 service가 정상적으로 동작하는지 확인 할 수 있습니다.

Reference

  1. https://www.manty.co.kr/bbs/detail/develop?id=13
  2. https://itzjamie96.github.io/2021/01/24/spring-service-and-serviceimpl/
  3. https://cheese10yun.github.io/spring-oop-04/
  4. https://blog.jiniworld.me/55
  5. https://www.tutorialspoint.com/design_pattern/factory_pattern.htm