토이프로젝트 다시한번 살펴보기!
이전 글과 이어지는 내용입니다.
안 읽어도 본문의 내용과는 상관없지만!
궁금하시면 읽어주세요!
오래된 토이 프로젝트를 리펙토링을 진행해보자.
어떻게 진행할까?!
솔직히 어느 부분부터 고쳐야 할지 막막했지만 저는 다음과 같이 기준 세우고 리펙토링을 진행했습니다.
- 도메인을 선택!
- 도메인 package 구조 파악하기!
- 도메인 Entity 파악!
- Controller -> Service -> Repoistory 순서로 로직을 파악하자!
- 로직을 파악 후 기능 별로 문제점 파악하고 TODO 리스트 작성!
- 리펙토링 진행하기
가장 중요한 건 해당 과정을 하기 전에 테스트 코드가 반드시 존재해야 합니다.
따라서 사소한 변경이 발생할 때마다 Test 코드 실행은 필수입니다.
또한 위 과정보다 본인에게 맞는 과정을 찾는 것도 중요할 것 같습니다.
1. 도메인 선택하기!
저의 도메인 종류는 다음과 같습니다.
Designer, Member, MemberShip, Menu, Order 가 존재하는데
해당 본문에서는 본문 길이상 Member 도메인을 리펙토링 하는 과정만 소개하겠습니다.
(실제로는 모든 도메인 리펙토링을 다 진행했습니다.)
2. 패키지 구조 파악하기
MemberShip은 제외하고 일단 패키지 구조를 보면
문제점 (분류가 되어있지 않음)
- Member라는 하위 경로에 폴더 구분 없이 존재하는 Service , Controller, Repository 가 존재합니다.
-> Layer 별 구분 폴더가 존재해야 할 것 같습니다. - form이라는 폴더는 아마 dto를 모아 논거 같은데 네이밍이 별로입니다.
-> dto로 이름으로 변경 후 dto 폴더 내에 request, response 폴더를 만들어 각 dto를 구분해주겠습니다.
우선 패키지 구조 파악 및 변경은 리펙토링을 진행하면서 계속 일어나기 때문에 한 번에 다 진행할 필요는 없습니다.
리펙토링이 끝난다면 최종적으로는 다음과 같은 패키지 구조를 가지게 됩니다.
3. Entity 살펴보기
사실 Entity를 살펴보는 이유는 해당 도메인이 어떠한 필드를 사용하고 어떠한 연관관계를 가지고 있는지 파악하는 게 더욱 큽니다.
자기가 개발한 프로젝트여도 정확히 기억나기는 어렵기 때문이죠!
우선 Entity를 파악하여 변경할 수 있는 부분들을 체크했습니다.
문제점
- 롬복을 이용하여 기본 생성자를 생성
-> 굳이 public으로 제공할 필요 없을 것 같습니다.
- GeneratedValue의 전략을 지정하지 않아서 Auto 전략을 사용하고 있습니다.
-> IDENTITY 으로 변경하여 기본 키 생성을 데이터베이에 위임
- 롬복 빌더를 사용하고 있는데 모든 필드를 세팅할 수 있습니다.
-> Member를 생성하는데 필요한 필드를 제외한 나머지 필드는 제외
- 마지막 방문일자, 가입일자인데 굳이
LocalDateTime으로
줄 필요 없을 것 같습니다.
-> 또한 가입일자는@CreationTimestamp으로
member 저장 시 자동으로 값을 세팅 - member 가 생성될 때 orderList 가 null을 참조하게 됩니다.
-> 빈 컬렉션을 바로 세팅하도록 변경
변경 후 의 모습입니다.
Entity 변경에 따른 사이드 이펙트가 많이 발생할 수 있습니다.
따라서 해당 과정에서 굳이 Entity를 변경하지 않아도 됩니다.
중요한 것은 Entity의 필드와 연관관계들을 한번 살펴보는 것이 중요한 과정입니다.
4. Controller -> Service -> Repoistory 순서로 로직을 파악하자!
해당 과정도 앞선 과정과 같이 머릿속으로 그림을 그려보는 과정입니다. 한 번에 모든 로직을 파악하는 것은 불가능 함으로
기능 별로 나눠서 Layer 별로 파악하는 것이 중요합니다.
MemberController의 멤버 생성
@PostMapping
public ResponseEntity<Object> saveMember(@RequestBody @Validated MemberForm memberForm , Errors errors){
if(errors.hasErrors()){
return ApiResponseMessage.error(errors);
}
memberService.saveMember(memberForm);
return ApiResponseMessage.success("성공적으로 저장됨");
}
다음과 같이 있을 때 아 컨트롤러에서는 Member를 생성할 때 memberSerivce.saveMember()를 호출하는구나! 를 파악하고
Service 계층으로 가시면 됩니다.
MemberSerivce의 saveMember()
public void saveMember(MemberForm memberForm) {
Member member = modelMapper.map(memberForm, Member.class);
member.setJoinedAt(LocalDateTime.now());
memberRepository.save(member);
}
Service 계층에서는 dto를 받아서 member를 생성한 뒤 기본적으로 제공하는 JPQL을 이용해서 저장을 하는구나!
이런 식으로 그냥 흐름만 파악하시면 됩니다!
5. 로직을 파악 후 기능 별로 문제점 파악하고 TODO 리스트 작성!
Member 가 저장되기 위해서 어떠한 과정을 가지고 어떠한 값을 리턴하는지 확인했으니
아마도 예전의 내가 왜 이렇게 코딩을 했는지 이해하지 못하는 부분들이 눈에 보이고 문제점이 하나씩 눈에 보이게 됩니다.
저는 다음과 같이 TODO 리스트를 작성했습니다.
//TODO
//1. 반환 DTO가 존재하지않음
//2. Validator 를 이용하여 회원 중복검사를 하고있음
//3. 예외발생시 처리를 따로 진행하지 않음
//4. Controller 에서는 request 와 response 를 반환하는 역할만!
//해결방안
// 예외를 만들어서 GlobalExceptionController 로 처리
// DTO 생성
// Validator 로 검증하는것은 vo 값 자체만
// 회원 중복 검사는 Service 계층에서
@PostMapping
public ResponseEntity<Object> saveMember(@RequestBody @Validated MemberForm memberForm , Errors errors){
if(errors.hasErrors()){
return ApiResponseMessage.error(errors);
}
memberService.saveMember(memberForm);
return ApiResponseMessage.success("성공적으로 저장됨");
}
이런 식으로 TODO 를 작성하고 리펙토링을 진행했습니다.
6. 리펙토링 진행하기
Controller 문제점을 해결하다 보면 대부분 동시에 Service도 같이 변경하게 됩니다.
한 번에 변경이 많이 일어나면 안 좋습니다. 리펙토링을 진행할 때는 Controller -> Service -> Repoistory
순서로 진행하는 것보다.
Repoistory -> Service -> Controller
순으로 진행하는 것이 좋습니다.
(사실 Layer 별 격리가 잘 되어있다면 해당 문제는 발생하지 않습니다..)
모든 리펙토링 자세한 과정은 생략하겠습니다..
과정중 하나인 예외처리 부분만 짧게 소개하고 넘어 가겠습니다.
GlobalExceptionController으로 예외 처리
기존에는 일일이 String 형태의 Message를 만들어서 반환하고 있었습니다.
따라서 예외 계층을 따로 만들어서 해당 Application에서 전체적으로 사용할 수 있도록 변경하였습니다.
패키지 구조는 다음과 같습니다.
Enum 타입으로 Status와 메시지를 관리하도록 변경하였습니다.
나머지 에러 계층 자세한 구조는 소스코드를 확인해주세요!
이 외 나머지 리펙토링 과정은 제 프로젝트의 커밋을 봐주시면 감사하겠습니다.
최종 결과
앞서 살펴본 과정을 반복하여 최종적으로 Member 도메인을 리펙토링 한 결과는 다음과 같습니다.
Member Controller (클릭하면 크게 보입니다)
Member Service(클릭하면 크게 보입니다)