여러 JPA 기반의 프로젝트를 진행하면서 Service 계층과 Repository계층을 분리하여 관리하였지만 그때 마다 드는 의문이 하나 있었다. 어떤 방법으로 계층을 분리하여 사용하는게 관리하는 것이 좋은가? 이에 대해 오늘 나의 주관적인 말을 적어보도록하겠다.
참고로, 아직 JPA에 익숙하지 않은 사람을 위한 계층을 간략히 설명하자면
Service 계층은 서버측에서 실제적으로 비즈니스 로직을 실행하는 곳이고
Repository 계층은 실질적으로 DB에 Query를 날려 데이터의 입출력을 관리하는 계층이다.
우선 MVC 프로젝트를 진행할때 가장 관습적으로 사용되는 구조인 Service와 ServiceImpl을 각각 Interface와 Class로 나누어 사용하는 방법이다.
1.Service Interface & Impl Class
대부분의 MVC 프로젝트를 진행함에 있어서 비즈니스 로직계층을 나누어 사용하는 구조를 많이 접하게 되었다.
예를 들어 회원 비즈니스 로직을 개발한다고 친다면 Entity가 Member로 네이밍 되었다는 가정하에 MemberService와 MemberServiceImpl가 생성된다.
이론상으로 이러한 패턴으로 설계를 한다면 인터페이스와 구현체가 분리되어 구현체의 확장성이 높아지고 독립되어 응집성이 낮아진다는 장점을 얻어 추후 구현체 클래스를 변경,확장 시에 클라이언트 코드에 영향을 적게 받는다는 장점이 있다.
또한 추상화를 함으로서 Solid 5원칙 중하나인 *OCP(Open Closed Principle)원칙을 가장 잘 실현 시켜주는 방식이다.
하지만 실무에서는 인터페이스와 구현체 클래스의 관계가 1:1 관계로 구성되어 이론적으로는 가질 수 있는 이점이 많으나 실질적으로는 이득을 볼 수없는 디자인 패턴으로 평가 받으나 개발자들은 해당 패턴을 관습적으로 사용하여왔다.
또한 코드 구조가 복잡해진다. 코드를 수정하거나 확장시킬 때 반드시 인터페이스를 거쳐 구현체를 손봐야하는 구조를 가지고 있기 때문에 글쓴이인 나는 좋아하지 않는 구조중 하나이다.
예제를 통해 조금 더 쉽게 이해해보도록 하자.
public interface MemberService {
Member signUp(SignUpDTO signUpDTO);
}
@Service
@RequiredArgsConstructor
public class MemberServiceImplA implements MemberService {
private final MemberRepository memberRepository;
@Override
public Member signUp(SignUpDTO signUpDTO) {
return memberRepository.save(signUpDTO.toEntity());
}
}
@Service
@RequiredArgsConstructor
public class MemberServiceImplB implements MemberService {
private final MemberRepository memberRepository;
@Override
public Member signUp(SignUpDTO signUpDTO) {
return memberRepository.save(signUpDTO.toEntity());
}
}
위에는 회원가입 비즈니스 로직을 간단히 표현한 서비스 계층이다. 위에서 설명하였듯이 대부분 인터페이스와 구현체는 1:1 관계를 갖게된다. 위와 같이 회원가입 로직을 굳이 2개로 나누어 사용할 이유가 없으며 만약 회원조회,수정,삭제 같은 나머지 CRUD 작업들 또한 같은 서비스 내에서 처리하는 것이 유지보수를 하는 입장에서 훨씬 편하기 때문에 해당 구조 패턴은 관습적이며 사용하는 것을 추천하지 않는다.
물론 OCP 원칙을 중요시하는 여러 객체지향 프로그래밍 관련 도서나 유명한 토비의 프로그래밍에서도 인터페이스와 구현체 분리를 통해 특정 기술이나 외부환경에 독립적으로 보다 자유로운 확장이 가능하다는 이유로 이러한 패턴(비슷한 패턴으로 브릿지 패턴을 예시로 들 수 있다)을 권장하긴 하지만 개발하는 입장에서는 까다롭기 때문에 선호하는 패턴은 아니다.
* OCP: 개방,폐쇄 원칙이라고 하며 소프트웨어 개체(클래스,모듈,함수 등)는 확장에 대하여 열려있고 수정에 있어서는 닫혀있어야 한다는 객체지향 프로그래밍 원칙중 하나이다.
그럼 아예 사용하지 않아야하는가?
나는 이에 대한 답은 명확히 내릴 수 없지만 개인적인 스타일 차이라고 생각한다. 그리고 나보다 개발경력이 월등히 많은 시니어 개발자들의 말을 들어보면 서로의 의견이 갈리는 걸로 보았을때 완벽한 정답은 없는것 같다.
세상에 변하는 것과 변하지 않는 것이 있지만, 객체지향의 세계에서는 모든 것이 변한다.
여기서 변한다는 것은 오브젝트에 대한 설계와 이를 구현한 코드가 변한다는 의미이다.
소프트웨어 개발에서의 끝이란 개념은 없고 사용자의 비즈니스 프로세스와
그에 따른 요구사항은 끊임없이 바뀌고 발전한다.
그래서 개발자가 객체를 설계할 때 가장 염두에 두어야 할 사항은 미래를 어떻게 대비할 것인가이다.
-토비의 스프링 3.1 1장 중-
토비의 스프링에서 얘기하는 핵심은 위에서 보이듯 "객체는 계속해서 발전하고 개발자는 이를 대비해야 하는 입장으로서 미래에 대한 설계 또한 변화에 효과적으로 대처할 수 있는 방법이다" 라는 뜻입니다. 위에서 말했듯 인터페이스 구현체 패턴을 좋아하지 않는 이유가 1:1을 벗어나지 못해 확장성이 좋은 구조이지만 그 확장성을 거의 이용할 경우의 수가 적기 때문이었는데 얼마든지 서비스가 커짐에 따라 구현체가 확장되고 변경될 수 있기 때문에 미래 변화에 대처할 수 있는 구조가 필요하다는 것이다.
그렇다면은 반대입장을 들어보자
서비스를 인터페이스로 만들었던건 관례로 굳어지게 되었는데
개발은 Transaction Script 형식으로 진행하다 보니까 관례는 관례대로 남고
애초에 그렇게 하자고 했던 이유는 사라져버리게 된 것이다.
그래서 내린 결론은 한 메서드에서 모든 역할을 다하는 이런 절차지향적인 코드에서는
사실 서비스를 인터페이스로 할 필요는 없다는 것이다.
물론 인터페이스를 만들지 말자 보다는 애초에 인터페이스를 만들었던 이유를 잘 살리는 것이
바람직 하다는 것이다.
-LichKing 블로그 중-
위에 말이 어려워 보이지만 천천히 잘읽어 보면 핵심은 "우리가 왜 인터페이스와 구현체로 나누어 사용했는지 이유를 알고 사용해야된다" 라는 의미 이다. 대부분의 나같은 뉴비 개발자들은 혼자 공부를 하다보면 구글링을 필수로 달고 살게되는데 그럴때 "이 블로그에서는 그렇게하던데?" , "이렇게 많이들 하던데?" 같은 것이 아닌 왜 추상화를 시켜 분리하였는지 그 의도를 알고 사용하는 것이 좋다는 것이다.
이렇게 시니어개발자들 간에도 의견이 갈리듯 결국 추상화 패턴 구조를 사용하더라도 결국에는 내가 이구조를 왜?사용하는지를 잘아는게 중요하다고 생각하며 자신의 프로젝트 구조상 추상화를 시켜 얻는 이득이 더 크다면 사용하는 것이 맞고 그게 아니라면 최대한 지양 하는것이 좋다는게 나의 의견이다.(물론 개인적으로 좋아하지않는 구조이긴하다. 아직 큰 규모의 프로젝트를 해보지 않아서일지도 모른다.)
또한 네이밍에 대한 문제도 있다. 회원관련 비즈니스로직을 모두 MemberService라 칭하고 그에 따른 구현체를 모두 MemberServiceImpl로 정하는 것 보다는 이용자에 따라 네이밍 하는방법이 더 좋다고 생각한다. 예를 들어 현재 내가 진행중인 스마트쇼핑카트 프로젝트에서는 관리자와 일반 회원 두가지의 권한 종류가 존재한다. 그럴 경우에는 MemberService라는 Interface 밑에 GeneralMemberService , AdminMemberService로 나누어 구현한다면 추상화 패턴구조의 이점인 확장성 또한 지키며 객체지향적으로 사용하는 방법이 될것이다.
2. 그래서 넌 무슨 방법을 쓰는데?
사실 위에서 그렇게 확장성에 대해 떠들었지만 내가 사용하는 방법은 김영한님의 JPA 강의를 듣고 사용하는 방식이며 RestAPI 구조에서 많이 사용되는 방법중 하나이다.(왜 사용하는지 알라면서 본인은 남의 껄 곧잘 따라한다...)
스프링의 기본적인 계층 분리는 잘 알겠지만 한번 다시 설명하자면 모델 계층, 서비스 계층, 데이터 액세스 계층 총 3가지가 존재한다. 나는 이를 이용해 모델 계층에서는 API 요청을 받고 Service를 호출하여 Service 종류에 따라 데이터 액세스 계층에 접근하여 값을 찾거나,생성,수정,삭제(CRUD) 하는 방법을 사용한다. 이해가 어려우니 코드로 접해 보도록하자.
@PostMapping("/member")
public ResponseEntity<String> signup(@RequestBody MemberDTO.SignUpDTO signUpDTO){
String signUpAPiState = memberService.SignUpApi(signUpDTO);
return new ResponseEntity<>(signUpAPiState, HttpStatus.OK);
}
먼저 컨트롤러에서 API 요청을 받으면 해당 요청을 Service 계층으로 넘겨준다.
@Transactional
public String SignUpApi(MemberDTO.SignUpDTO signUpDTO) throws MemberException{
if (signUpDTO.getUsername() == null || signUpDTO.getPassword() == null){
throw new MemberException(MemberExceptionType.NULL_OF_USERNAME_OR_PASSWORD);
}else if (!signUpDTO.getPassword().matches("(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,20}")){
throw new MemberException(MemberExceptionType.WRONG_PASSWORD);
}else if(memberRepository.findByUsername(signUpDTO.getUsername()).isPresent()) {
throw new MemberException(MemberExceptionType.ALREADY_EXIST_USERNAME);
} else{
Member member = signUpDTO.toEntity();
member.addMemberAuthority();
member.encodeToPassword(passwordEncoder);
memberRepository.save(member);
return "회원가입이 정상적으로 동작하였습니다.";
}
}
(예외처리가 좀 많은것은 무시해주길 바란다)
그럼 서비스 계층에서는 해당 요청에 따라 Repository 계층 즉, DB와 관련된 쿼리를 날릴 수 있는 계층에 해당 요청을 다시한번 넘긴다. 해당 예제에서는 Spring Data JPA를 사용하여서 직접 쿼리를 작성하진 않았다.
그렇게 서비스가 작동하고 나면 반환값을 다시 컨트롤러 계층으로 전달하여주고 컨트롤러 계층에서도 클라이언트 쪽으로 전달해주게된다.
나는 해당구조를 사용하며 추상화를 별도로 사용하진 않고 기능별로 네이밍하여 Service Class를 생성하여 Service에 맞는 Repository에 의존성을 주입시켜 사용하고 있다. 해당 구조를 사용하게 되면 확장성에 대한 고려가 줄게되지만 그만큼 클래스가 많이 생성된다는 단점이 있지만 유지보수를 하는 입장에서는 기능별로 Class가 분리되어 있기 때문에 유지보수가 편하다는 장점이있다.
3. 아니 Entity에 다 넣으면 안되나?
물론 가능하다. Entity 내부에서 로직을 작성하여 컨트롤러에서 호출만하면 되긴한다. 하지만 해당 방법을 사용하게되면 Entity 자체에 응집력이 올라가게되고 DB와 직결되는 Entity내부에 비즈니스로직을 넣는다는 행위 자체가 객체 지향과는 거리가 멀며 스프링을 사용하는 의미 또한 퇴색된다. 이러한 것을 도메인 패턴 모델이라고 하며 도메인 패턴 모델을 사용할 때 대표적인 예시로 변경감지같은 수정 쿼리를 날려야 할때 사용된다.
결론
결국 자신의 프로젝트 구조에 대해 개발자 본인이 이해하고 있는 것이 가장 중요하다. 자신의 프로젝트에 맞는 디자인 패턴 구조를 선택하여 적용할 줄 알아야 좋은 개발자라고 할 수 있는 것이다.(나는 나쁜 개발자)
글쓰는데에 도움
'Spring-boot' 카테고리의 다른 글
@Getter와 @Setter는 왜 지양되어야 하는가? (0) | 2022.11.29 |
---|---|
[Spring] 패키지 구조 (0) | 2022.10.21 |
Web-Socket 공부 흔적(2) STOMP를 사용하는 채팅방[SpringBoot] (0) | 2022.07.09 |
[JWT-Token] 스프링 시큐리티 JWT 토큰을 이용한 스프링부트 프로젝트 만들기 -(1) (0) | 2022.06.28 |
Web-Socket 공부 흔적(1) (0) | 2022.02.23 |