이제부터 리팩토링에 들어간다.
먼저 패키지 정리를 한다
기존에는 Controller, Service 등 기존에는 레이어를 두고 그 내부에 필요한 패킺 명들을 작성했다
예로들면
1. Controller 내부 post, user
2. Service 내부 post, user
이걸 이제
1. post 내부에 Controller, Service
2. user 내부에 Controller, Service
이런식으로 나눠서 사용한다.
외부연동시에는 RDB 뿐만 아니라 모든 외부 연동을 의존성 역전을 통해서 다룬다.
의존성 역전은 아는대로 하면 된다.
팁을 좀 보자면 여기서는 의존성 역전시, Interface 패키지들을 infrastructure가 아닌 Service에 두었다.
만약 infrastructure에 그대로 두었다면, Service가 infrastructure에 의존하는 그림이 만들어지기 때문에 서비스 내부에 port라는 패키지를 만들어 관리하였다.
이렇게 하면 구현체는 infrastructure에 있고, 인터페이스만 Service에 있다.
전에 봤던 사진을 가져왔다.
이메일을 보내는 테슽의 경우. 테스트시 이메일을 보내는 Sender가 필요하다.
이때 빨간색 위치(Repository Interface)에 실제 MailSender가 들어가는 것이 아니라, mock패키지 폴더에 가짜 mailSender를 만들어 준다(dummy 혹은 fake)
테스트시 fake를 사용하여 데이터가 잘 들어왔는지 확인 해 볼 수도 있고, 필요한 값을 리턴할려면 Mockito 프레임 워크가 아니라 자바 코드로 쉽게 테스트가 가능하다.
게다가 추상화에 의존을 하였기 때문에 추후 메일 시스템이 바뀌거나, DB 시스템이 바뀌더라도 쉽게 변경이 가능하다.
먼저 용어를 정리하면
영속성 객체: 데이터를 저장, 관리하기 위한 객체로 ORM기술과 함께 객체 간 매핑을 다룬다
PO: PlainOldJavaObject로 DTO, VO로도 불리며 데이터의 표현 혹은 전달을 위한 객체이다
내 코드를 생각해 보면 Entity 로 조회해서 그대~로 Service에 가져간다.
이것을 분리하는게 목표인것 같다.
Entity와 같은 필드를 가지는 DTO(난 Dto가 더 편해서)를 만들고 Repository에서 조회했을 때 Entity가 아니라 Dto를 조회하게 만들자.
강의에서는 Entity마다 toModel
, fromModel
이라는 매서드를 만들어서 이를 통해 Entity <-> Dto로 바꾸게 했다.
위 이련의 과정이 끝나면 이제 순수 자바코드로 테스트를 짤 차례이다.
Spring, h2, mockito가 없다.
리포지토리 테스트는 아래처럼 저장과 업데이트를 한다.
public class PostServiceImpl implements PostService {
private final PostRepository postRepository;
private final UserRepository userRepository;
private final ClockHolder clockHolder;
public Post getById(long id) {
return postRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Posts", id));
}
public Post create(PostCreate postCreate) {
User writer = userRepository.getById(postCreate.getWriterId());
Post post = Post.from(writer, postCreate, clockHolder);
return postRepository.save(post);
}
public Post update(long id, PostUpdate postUpdate) {
Post post = getById(id);
post = post.update(postUpdate, clockHolder);
return postRepository.save(post);
}
}
따라서 직접 저장 할 필요 없이 아래코드의 from이 잘 동작하는지만, update가 잘 동작하는지만 보면 된다. 이후 에는 save 코드가 작성되어 있어 저장되는지 확인이 되기 때문이다.
save가 되었는지 확인이 하고 싶으면 그건 중형테스트로 넘어가면 된다.
public class Post {
private final Long id;
private final String content;
private final Long createdAt;
private final Long modifiedAt;
private final User writer;
@Builder
public Post(Long id, String content, Long createdAt, Long modifiedAt, User writer) {
this.id = id;
this.content = content;
this.createdAt = createdAt;
this.modifiedAt = modifiedAt;
this.writer = writer;
}
public static Post from(User writer, PostCreate postCreate, ClockHolder clockHolder) {
return Post.builder()
.content(postCreate.getContent())
.writer(writer)
.createdAt(clockHolder.millis())
.build();
}
public Post update(PostUpdate postUpdate, ClockHolder clockHolder) {
return Post.builder()
.id(id)
.content(postUpdate.getContent())
.createdAt(createdAt)
.modifiedAt(clockHolder.millis())
.writer(writer)
.build();
}
}
서비스는 Repository 및 domain을 가져와 사용한다.
domain은 어차피 가짜로 만들면 된다. 앞에서 순수 자바코드로 작성해 두었기 때문에.
Repository는 의존성 역정을 시켜두었다. 앞에서 한것처럼 FakeRepository를 만들어서 그걸 사용해 테스트를 하면 H2를 직접 띄우거나, 테스트시 Mock프레임워크를 사용해서 데이터를 넣을 필요가 없다
ID 는 Long 내부 변수를 사용해서 받았고, 데이터는 배열에 넣어서 저장하고 가져온다.
이렇게 하면 지금 내 코드에서처럼 데이터를 DB에 넣고 빼고 할 필요 없이 데이터를 저장하고 없앨 수 있다.
이제 테스트 속도도 빨라지고 테스트도 할 수 있는데 거지같은 Mocking도 할 필요가 없어졌다.
컨트롤러도 소형 테스트가 가능하다. 어떻게? 똑같이 의존성 주입을 받아서.
이후에 MockMVC
를 사용해서 테스트가 아니라, Service를 테스트 마다 생성해 준다.
모든 Service를 추상화 해준다. 그리고 테스트에서는 Dummy, Fake, Stub등을 해준다. Mock프레임워크는 필요없다. 앞의 과정의 반복이다.
이때 만약 너무 많은 책임을 하나의 서비스가 지고 있다면, 서비스를 여러개로 분리하고, 아랫단에 Domain, Repository등은 여러 인터페이스를 상속 받으면 된다.
그리고 stub을 선호하지 않는다고한다, 그 이유는 이렇게 하면 이렇게 응답을 내려줘야 한다 이렇게 강제하기 떄문이라고 한다.
그래서 스프링의 IoC같은 친구를 만들어서 테스트를했다
public class TestContainer {
public final MailSender mailSender;
public final UserRepository userRepository;
public final PostRepository postRepository;
public final PostService postService;
public final CertificationService certificationService;
public final UserController userController;
public final MyInfoController myInfoController;
public final UserCreateController userCreateController;
public final PostController postController;
public final PostCreateController postCreateController;
@Builder
public TestContainer(ClockHolder clockHolder, UuidHolder uuidHolder) {
this.mailSender = new FakeMailSender();
this.userRepository = new FakeUserRepository();
this.postRepository = new FakePostRepository();
this.postService = PostServiceImpl.builder()
.postRepository(this.postRepository)
.userRepository(this.userRepository)
.clockHolder(clockHolder)
.build();
this.certificationService = new CertificationService(this.mailSender);
UserServiceImpl userService = UserServiceImpl.builder()
.uuidHolder(uuidHolder)
.clockHolder(clockHolder)
.userRepository(this.userRepository)
.certificationService(this.certificationService)
.build();
this.userController = UserController.builder()
.userService(userService)
.build();
this.myInfoController = MyInfoController.builder()
.userService(userService)
.build();
this.userCreateController = UserCreateController.builder()
.userService(userService)
.build();
this.postController = PostController.builder()
.postService(postService)
.build();
this.postCreateController = PostCreateController.builder()
.postService(postService)
.build();
}
}
이후 TestContainer를 이용해서 테스트를 진행한다.
만약 테스트를 짜는데 뭔가 막 귀찮고, 어디에 이상하게 의존하고 그런다? 이말은 지금 니 코드가 냄새나니까 안나게좀 해봐 이말이다.
이것은 대부분 설계 자체의 문제이거나, 잘못된책임을 지고 있거나, 책임이 너무 많거나, 아니면 의존성 역전을 하지 않아 생기는 문제이다. 다른 문제들도 있을텐데 이것들은 차차 경험 할 수 있을것 같다.
이 강의가 사실 하고싶은 말은 이론과 방향성 탐색에서 끝났다고 생각한다.
아마 더 보여줄수 있는건 약간의 기술? 정도인데 사실 내용 20~30분중 중요한건 10분? 도 안되는것 같다
사실 SOLID? 코드 스멜 등 예쁘고, 유지보수성 좋게 코드를 짜는 법은 이미 배웠다.
하지만 그걸 실제로 구현하는 일은 생각보다 힘들다.
왜인가 생각을 해보니 써봐야 아는데 대학교 수업에서의 실습 코드는 현실과 매우 동떨어져 있다고 생각한다. 게다가 학교에서는 수업 진도를 생각하면 이렇게까지 해줄 수 없다. 거기다 직접 코드를 짜면서 냄새가 나는데? 이상한데를 느끼고 직접 짜봐야 하는데 사실 나는 그럴 기회가 이제 왔다.
이제 얼른 기능개발 완료하고 내 코드를 뒤집어 엎어야겠다.