토이 프로젝트 회고 - feat. 객체지향적이지 못한 코드

jonghyun.log·2023년 5월 7일
1
post-thumbnail
post-custom-banner

츄즈(Chooz)

츄즈는 저와 제 지인들이 만든 토이 프로젝트의 이름입니다.

특정 주제로 투표를 하고 투표한 사람들의 성별, 나이, mbti의 사람들은 어떻게 투표했는지 반응을 확인하는 프로잭트 입니다.

츄즈 사이트

작년 가을부터 시작해서 올해 연초까지 만들었던 프로젝트의 MVP 구현이 끝났습니다.
스프링을 배우고 처음으로 내가 온전히 완성했다고 할 수 있는 프로젝트라 저에게는 큰 의미가 있는 프로젝트인데요.

분명 만들때는 열심히 만들었는데 왜 만들고 나니 아쉬움만 남을까요?
아쉬움만 가지고 행동하지 않으면 아무것도 변하지 않으니..
이번에 개발하면서 생겼던 문제를 회고하고 하나씩 문제들을 고쳐보는 시간을 가져볼까 합니다.

돌이켜보니 보이는 문제들

우선 개발하면서 느꼈던 커다란 문제점으로는 객체지향적이지 못한 코드들이 문제들을 만든 것 같습니다.

개인적으로 경험한 것들에 기반해서 문제점을 적어보려고 합니다.

크게 세 가지 문제점이 있었는데

  1. 너무 비대해진 서비스 클래스 코드
  2. 서비스가 영속성 계층에 휘둘리게 된다는 점
  3. DB 테이블 구조에 의존하는 코드

하나씩 이게 무슨말인지 적어보겠습니다.

1. 너무 비대해진 서비스 클래스 코드

서버는 저 포함 3명의 팀원이 각자 매주 특정 기능을 구현하고 버그가 생기면 고치는 식으로 개발을 진행하였는데요.

저희가 만드는 서비스는 커다란 서비스가 아니고 간단한 서비스다 보니
각자가 특정 도메인을 나눠서 개발하기보다 하나의 도메인 안에서 작은 기능들을 각자 분담하여 개발하는 방식으로 하게 되는 경우가 많았습니다.

하지만, 여기에 더해 더 큰 재앙을 불러 일으킨 원인이 있었으니 바로
하나의 서비스 객체에 비즈니스 로직을 몽땅 넣어서 개발 하고 있던 것입니다.

각자 책임과 역할을 나눈 객체를 개발하고 합치는 식으로 개발하지 못하고
하나의 클래스 안에서 모든 것을 해결하려고 하니,

각자의 브렌치에서 작업을 하고 각자의 코드가 합쳐질 때 생기는 중복과 더불어
생기는 여러 문제로 인해 골머리를 앓았습니다.

여기에 비대해진 클래스 코드와 그에 따른 분석이 어려워진 점도 한 몫 했습니다.

이것이 반복되자 어느덧 저희는 한 사람이 개발하고
다른 사람들은 그 사람이 개발할 때까지 기다리게 되는 아주 불편한 상황을 겪게 되었습니다.

2. 서비스가 영속성 계층에 휘둘리게 된다.

2번과 3번은 연결되는 이야기인데요.

컨트롤러 - 서비스 - 리포지토리 패턴에서는 컨트롤러가 서비스를 의존하고 서비스가 리포지토리를 의존하는 그러니까 특정 방향으로 의존하여 개발하게 됩니다.

public class VoteService {

    private final UserRepository userRepository;
    
    public Long createVote(@Valid CreateVoteRequest createVoteRequest, Long userId) throws UserNotFoundException {
        User findUser = userRepository.findById(userId).orElseThrow(UserNotFoundException::new);
	}        
        	... // 서비스 코드에서 레포지토리 코드를 의존하고 있음
}

위의 코드는 이번에 작업한 코드를 일부 가져온 것 입니다.

의존성의 개념

우선 의존성이 무엇인지 부터 정리해보도록 하겠습니다.

의존성 이란 A를 생성하고 사용할 때 B라는 것을 알지 못하면 동작이 불가능한 것

즉, 서비스 코드에서는 영속성 계층에 존재하는 레포지토리 코드를 알아야 동작한다는 것이죠.

위 내용을 읽으면서 의구심을 가지는 분들도 계실 것 같습니다.

"아니 서비스 코드에서 영속성 계층의 코드를 사용하는게 왜 문제야?"

물론 맞는 말입니다. 서비스 코드가 있는 비즈니스 로직의 계층에서 영속성 계층의 코드를 가져다가 사용해야 DB에 접근해서 일련의 작업들을 실행할 수 있으니까요.

서비스 객체가 레포지토리 객체를 의존한다.

위의 코드에서 UserReposityfindById 메서드의 변경이 생겨서 이름이 변경되거나 혹은 메서드의 리턴타입이 변경되면 무슨 일이 일어날까요?

서비스의 createVote 메서드에 수정을 해줘야 겠죠.

즉, 레포지토리의 코드에 변경이 생기면 서비스에도 변경이 생기게 됩니다.

서비스에서 리포지토리의 코드를 의존하고 사용하다 보니
리포지토리의 메서드나 로직을 수정하게 되면 어느새 서비스의 코드도 수정하는 일이 비일비재 했습니다.

이것이 크게 와닿지 않으실수도 있습니다.

하지만 만약 서비스가 엄청 커져서 서비스의 createVote() 메서드 안의 로직이 복잡해지고 그 복잡한 로직이 findById() 메서드를 사용하고 있었다면 복잡한 로직에도 수정을 해줘야겠죠.
시간이 흐를수록 수정에 필요한 자원이 많이 들어가게 됩니다. (요것이 핵심!!)

3. DB 테이블 구조에 의존하는 코드

이거는 사실 계층형 아키텍쳐를 오용한 문제라기 보단 JPA에 의존하여 생긴 문제입니다.

웹 개발을 시작한 지 얼마 안 된 저한테 있어 JPA는 테이블의 데이터를 객체로 바꿔주는 일련의 작업이 마치 개발의 마스터키 처럼 느껴진 툴입니다.

개발을 시작할 때 저는 먼저 테이블을 먼저 짜고 그 이후 테이블 구조를 기반으로 JPA의 엔티티를 만들고 그 엔티티를 서비스 계층에서 사용하는 식으로 개발했습니다.

나름 테이블 구조도 열심히 짰었다.

하지만, 이렇게 하다 보니 디비의 구조가 제가 만든 서비스 구조를 지배하는 듯한 느낌을 많이 받았습니다.
데이터베이스 주도 개발을 했다고 할까요.

처음에는 개발하기 무척이나 편했습니다.
테이블 구조만 만들면 엔티티 구조가 결정되고 그대로 사용하기만 하면 바로 개발이 가능했습니다.

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PUBLIC)
public class Vote extends BaseTimeEntity {

    @Id
    @GeneratedValue
    @Column(name = "VOTE_ID")
    private Long id;

    /**
     * User 와의 연관관계 주인
     */
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "USER_ID")
    private User postedUser;

  
    @BatchSize(size = 1000)
    @OneToMany(mappedBy = "vote", fetch = FetchType.LAZY)
    private List<VoteResult> voteResultList = new ArrayList<>();
    
    ...
}

그때 만들었던 엔티티 테이블 구조를 그대로 엔티티 구조에 반영하는 바람에 끔찍한 연관관계 구조가 만들어졌다... 그런짓은 하지 말아야 했는데..

하지만, 서비스에서 사용하는 엔티티의 구조가 디비의 테이블 구조를 따라서 만들어진 탓에
테이블의 pk fk 구조가 엔티티들의 연관관계가 되고...
이런 구조로 개발을 하다 보니 불필요한 연관관계가 주렁주렁 매달려있는 구조가 생겨
간단한 crud 쿼리를 작성할 때도 복잡한 fetch join을 고민하는 상황에 이르게 되었습니다.

삽질의 흔적..

지금 생각해보면 잘못된 설계로 인해 생긴 문제인데 어떻게든 쿼리로 고쳐볼라고 용을 썼던 기억이...

3번 문제는 서비스의 코드를 추상화하고 외부와 단절(캡슐화) 했어야 했는데
서비스 외부의 개념이 서비스 내부으로 오염되어 생기는 문제네요.

추가) 엔티티란?

이 문제는 또한 저의 엔티티의 대한 낮은 이해도로 인해 생긴 문제라고도 볼 수 있는데요.
엔티티는 맥락에 따라 두가지 의미로 사용됩니다.

1. Entity in DTO

1.실제 DB의 테이블과 매핑되는 객체. identifier로 구분된다.(DB관련 영속성 엔티티)
2. 비지니스 로직을 포함하는 도메인 엔티티

DTO는 Entity의 변경을 최소화하기 위해 탄생했습니다. 따라서 DTO에서 Entity를 사용할 때, 도메인 엔티티가 될 수도, DB의 영속성 엔티티가 될수도 있습니다.

2. Entity in 클린 아키텍처

비즈니스 로직을 캡슐화한 객체라는 의미로 사용됩니다.

출처: 도메인과 엔티티의 개념을 정리한 블로그 글

배운점

전반적으로 보면 객체지향적 코드의 특징인 역할, 책임을 적절하게 분리하지 못하고 특정 기능이나 구조에 의존성을 가진 채 설계를 했기 때문에 생긴 문제들이네요.

이번 프로젝트를 하면서 좋은 설계란 무엇인가? 에 대해 관심을 갖게 되었습니다.
나쁜 설계를 가지고 개발을 하면 얼마나 고생하는지 느끼게 되었으니까요.

앞으로는 설계 관련한 책도 읽고 위의 문제들의 해결책을 찾아보고 단계적으로 제 프로젝트에 리팩토링을 해보는 시간을 가져보겠습니다.

post-custom-banner

0개의 댓글