프로젝트/크루트

AService에서는 ARepository만 사용하고 BService 사용은 지양하자

발전생 2022. 1. 30. 23:35

처음에 하나의 엔티티 타입을 위한 서비스 클래스를 만들 때 다른 엔티티 타입의 서비스 클래스도 막 가져와서 사용했었다.  이렇게 여러개가 만들어지다 보니 어디에서 썼는지 기억도 잘 안 나고 코드가 너무 지저분해 보였다. 또 단일 책임 원칙도 약간 빗겨간 느낌이 들었다. 

 

초반의 더러운 코드 - 여러 다른 서비스를 주입 받아 사용하고 있다.

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ProjectService {
    private final ProjectRepository projectRepository;
    private final PartService partService;
    private final UserPartService userPartService;

    public List<Project> findAll() {
        return projectRepository.findAll();
    }

    @Transactional
    public void saveProject(Project project) {
        projectRepository.save(project);

        Part frontendPart = new FrontendPart(project);
        Part backendPart = new BackendPart(project);
        Part designPart = new DesignPart(project);
        partService.saveParts(List.of(frontendPart, backendPart, designPart));

        switch (project.getProposer().getPosition()) {
            case FRONTEND:
                userPartService.saveUserPart(project.getProposer(), frontendPart);
                break;
            case BACKEND:
                userPartService.saveUserPart(project.getProposer(), backendPart);
                break;
            case DESIGN:
                userPartService.saveUserPart(project.getProposer(), designPart);
                break;
        }

    }
}

 

어떻게 다른 타입의 서비스를 안 쓸 수 있을 지 고민해봤다,. 그 결과 CASCADE라는 키를 떠올렸다.

다른 타입의 서비스를 호출하는 대신 엔티티 객체의 컬렉션에 직접 엔티티 객체를 넣는다. 나중에 이 엔티티가 persist될 때 컬렉션에 있는 엔티티 객체들도 같이 persist된다.  이렇게 하면 코드가 훨씬 깔끔해지고 논리적으로도 명확해진다. 논리적으로는 그저 소속 관계를 명확하게 해줄 뿐이다.

 

이런 리팩토링은 UserPart처럼 다대다 관계를 일대다, 다대일로 풀기 위해 만들어진 중간 테이블이 있을 경우 효과적이라고 본다. 처음에 UserPart 객체만 persist 했다가 Part가 persist 되지 않아 UserPart 내에 part에 null이 들어가는 오류를 겪었다.  실수를 방지해주는 효과 또한 있다. 그냥 Part의 UserPart 타입 컬렉션에 User와 Part를 가지고 UserPart를 생성해준 다음 컬렉션에 넣어주면 끝이다. 간단하다. 나머지는 jpa의 dirty checking 로직이 알아서 처리해준다.

 

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ProjectService {
    private final ProjectRepository projectRepository;

    public List<Project> findAll() {
        return projectRepository.findAll();
    }

    @Transactional
    public void saveProject(Project project) {
        projectRepository.save(project);

        Part frontendPart = new FrontendPart(project);
        Part backendPart = new BackendPart(project);
        Part designPart = new DesignPart(project);

        switch (project.getProposer().getPosition()) {
            case FRONTEND:
                frontendPart.addMember(project.getProposer());
                break;
            case BACKEND:
                backendPart.addMember(project.getProposer());
                break;
            case DESIGN:
                designPart.addMember(project.getProposer());
                break;
        }

        project.addPart(frontendPart);
        project.addPart(backendPart);
        project.addPart(designPart);
    }
}

 

리팩토링하면서 논리가 정리된 점도 있지만 코드가 훨씬 이해하기 쉬워졌다. 기대한 대로 잘 작동하는 것을 볼 수 있다.

 

Proejct.java

@OneToMany(mappedBy = "project", cascade = CascadeType.PERSIST)
private List<Part> parts = new ArrayList<>();

 

Part.java

@OneToMany(mappedBy = "part", cascade = CascadeType.ALL)
private List<UserPart> userParts = new ArrayList<>();

 

일대다 관계인 컬렉션에 cascade 옵션을 넣어줘서 컬렉션에 엔티티를 넣으면 persist할 때 자동으로 같이 해당 엔티티를persist 되게 했다. persist 하면 새로운 엔티티를 영속성 컨텍스트에 넣으면서 sql 쿼리 지연 저장소에 insert문을 생성해서 넣는다. 후에 영속성 컨텍스트의 commit이 일어나면 sql 쿼리 지연 저장소에 있던 쿼리들이 db에 날아가게 된다.

 

UserPart는 Part가 존재해야만 의미 있는 테이블이므로 part가 remove되어도 딸려있는 userPart들이 모두 remove 되게 했다. 하지만 project는 remove 시 part까지 remove 하는 건 지나친 데이터 제거라고 생각했다. 권한을 가진 프로젝트원 1명이 project를 제거하면 프로젝트원들은 본인이 참여했던 part를 이제 볼 수 없게 되는 것인데 이는 가혹하다. 힘들게 만들었는데 참여한 기록은 남아야 한다고 생각했다. 생각해보면 Proejct 자체를 제거 못 하게 하는 것 또한 나머지 팀원들을 위한 좋은 선택 같아 고민 중이다.