본문 바로가기
카테고리 없음

레거시 프로젝트 DAO 에서 JPA로 변경할때는 조심하세요.

by 손너잘 2021. 8. 17.

이전 초여름에 우테코 크루들과 함께 제작했던 프로그램이 있다. [15분 회고 프로젝트]

간단히 도메인을 설명하자면, 크루들의 하루 회고를 위한 프로그램이다.

  • 사용자는 자신의 이름을 작성하고 로그인 한다. (만일 없는 아이디라면 자동으로 가입을 진행합니다.)
  • 사용자는 6시 회고, 10시 회고 중 하나를 선택한다.
  • 6시, 10시가 되면 각 시간의 회고를 선택한 사용자들을 셔플하여 3명, 또는 2명으로 묶어서 페어를 만들고 이를 반환하여 회고를 진행할 인원(페어) 보여준다.

 

당시에는 JPA를 공부한 상태가 아니었기에 DB관련 로직이 모두 JDBC를 통해 이루어져 있었다. 따라서 JPA를 공부한 지금, 이 시스템을 JPA로 옮겨보고자 한다.

 

도메인 분석

가장 먼저 할 일은 도메인 모델를 그리는 것이다. 사실 이걸 프로젝트 시작할 때 제일 먼저 했어야 하지만... 이미 늦은걸 어찌하는가

Intellij의 기능을 이용하여 핵심 도메인간의 UML을 뽑아봤다. 이제와서 보니 뭔가 어색하고 작은 도메인 구조다. 하지만 위 구조를 유지하면서 최대한 리팩토링 해 보도록 하겠다.

위 구조를 통해 도메인 모델을 도출해 보면

이런식으로 도출이 된다. 이해한 도메인대로 어그리거트를 나눠봤더니, 의도하진 않았는데 각 도메인별로 각자의 어그리거트(생명주기)를 가지게 됐다.

사실 어그리거트의 크기가 엔티티 하나정도로 작은게 이상한 것은 아니라서 괜찮다고 보여진다. 오히려 너무 큰 어그리거트를 조심해야 한다.

그럼 어그리거트가 4개로 분리되었으니 이를 기반으로 Repository를 생성 한다.


 

왼쪽의 구조에서 오른쪽의 구조처럼 Repository를 생성해 주었다. ConferenceTime이 하나의 어그리거트로 분리됨에 따라 패키지를 따로 가져가게 했다.

이제 위 도메인 모델에 따라 JPA와 도메인을 적절히 매핑해 주도록 하겠다.

먼저 Attendance다. 도메인 모델에 있는대로, 모두 ManyToOne 매핑을 진행했다.

Member와 ConferenceTime의 경우는 객체관계에서 봤을 때 Attendance를 알아야 할 필요가 없기 때문에 단방향 매핑으로 진행했다.

다음은 Pair, 혹시 추후에 Pair를 다시 만들어야 하는 요구사항이 발생할지도 모르기 때문에 REMOVE 혹은 orphanRemoval옵션을 생략하여 softDelete가 진행되도록 하였다. 또한 Pair를 객체를 사용할 때 언제나 attendances를 사용하기 때문에 fetch join이 가능하도록 EAGER 옵션을 넣었다(JPQL을 사용한다면 조심해야한다).

마지막으로, 현재 양방향 매핑을 사용하기 때문에 생성자에서 연관관계를 만들어주는것을 볼 수 있다.

이 다음은 노가다... Service 및 Test로직에 있는 Dao를 제거하고, 필요한 query들을 Repository에서 지원할 수 있도록 열심히 바꿔준다... 또한 새로 생기는 코드에 대한 test도 꼼꼼하게 작성 해준다.

기존의 data.sql을 통해 초기화 했던 데이터도CommandLineRunner를 통해 초기화 해준다.

마무리 하면 멋지게 JPA로 마이그래이션 된 프로젝트를 볼 수 있다.


좌측은 변경 전, 우측은 변경 후 ERD이다.

 

그런데 무언가 이상한것이 있다. 둘의 DDL이 다르다. JPA로 변경 전에는 PAIR테이블에서 attendance_id를 관리하고 있으나, 변경 후에는 attendance에서 pair_id를 관리하고 있다.

 

현재 프로젝트의 비즈니스 로직은 다음과 같다.

  • 사용자가 참석할 회고 시간을 누르면, 사용자 id(MEMBER 테이블)와 사용자가 누른 회고시간의 id(CONFERENCE_TIME 테이블)을 이용하여 attendance를 하나 생성하고 데이터 베이스에 저장한다.
  • 회고 시간이 되면 해당 시간과 오늘 날짜와 매핑되는 attendance들을 데이터베이스로부터 불러와 N 개의 pair를 생성한다. 이때 각 pair에는 서로 다른 group_id가 부여된다. 이때 group_id는 비즈니스 로직 상에서 부여된다. 그런 뒤 이 페어들을 데이터베이스에 저장한다.
  • 추후 특정 시간의 pair들을 다시 불러와야 한다면, pair 테이블을 통해 특정 시간대에 속한 참석자를 모두 조회한 뒤([id, grou_id, attendacne_id] 를 반환할 것이다), 이 를 group_id별로 묶어 Pair들을 만들어 리스트로 반환한다.

 

위 로직과 ERD에 따르면, 리팩토링 전에는 새로운 Pair를 생성하고 데이터베이스에 저장할 때 Pair 테이블에 페어가 된 참석자들의 id와 참석자가 묶이게 될 group_id의 쌍이 저장된다. 하지만 JPA로 변경 후에는 pair테이블에는 참석자가 속하게 될 group_id만 저장되고 attendance쪽에 자신이 속하게 될 group_id가 속한 Pair.id 를 가지게 된다. 따라서 새로운 pair를 생성하고 저장하는 시점에 attendacne는 이미 생성되어있는 상태이기 때문에 attendance쪽에 update쿼리를 날려서 데이터를 처리하게 된다.

하지만 이러한 데이터 관리는 우리가 의도했던바가 아니다.

분명 기존 로직의 객체를 테이블에 그대로 매핑했는데 왜 이런 차이점이 발생했을까.

 

일단 비즈니스 로직을 다시한번 살펴보자. 사실 위 비즈니스 로직에서 어색한 부분이 있다. 3번째 문장이 그렇다. 사실 위 문장을 위의 ERD에 매핑시켜 말하면 아래와 같이 말할 수 있다.

  • 추후 특정 시간의 pair들을 다시 불러와야 한다면, pair 테이블을 통해 특정 시간대에 속한 Pair를 모두 조회한 뒤([id, grou_id, attendacne_id] 를 반환할 것이다), 이 Pair들을 group_id별로 묶어 Pair들을 만들어 리스트로 반환한다.

 

말이 안된다. 페어를 불러와서 페어를 만든다는게 도대체 무슨 의미인가... 여기서 우리는 한가지 힌트를 얻을 수 있다. 위와같은 불일치가 발생한 이유는 도메인 분석이 충분히 이루어지지 않은 상태에서, UML만 믿고 리팩토링이 이루어졌기 때문이다.

 

저자는 이전에 이 프로젝트의 멤버였기 때문에 당연히 도메인에 대한 이해도가 있다고 생각했고, 충분한 도메인 분석 없이 바로 리팩토링에 들어갔다. 따라서 객체와 테이블이 그대로 매핑된다 생각했고, 기존에 DAO로 관리하던 객체들을 Entity로 바로 매핑시켰다. 바로 이 부분이 패착이다. 사실 위 프로젝트에서 객체로서 존재하는 Pair와 데이터베이스 테이블로 존재하는 Pair는 서로 다른 의미를 가지고 있다.

무슨 소린고 하니, 객체로서 존재하는 Pair는 group_id로 관리되는 모든 Attendance들의 묶음을 의미한다(위 사진을 보면 컬렉션으로 Attendance를 관리한다). 하지만 데이터베이스의 Piar는 group_id로 관리되는 Attendance 하나 를 의미한다. 테이블과 객체가 의미하는바가 다른데 이를 바로 Entity로 매핑하려고 하니 불일치가 발생한 것이다. 사실 테이블의 Pair 네이밍이 잘못됐다. Pair보다는 GroupedAttendacne등으로 표현했어야 했다.

따라서 우리가 원하는 형태로 테이블에 매핑시키기 위해서는 group_id와 Attendance를 가지는 Entity를 새로 생성하여 관리하고, 기존의 Pair객체는 이 Entity를 관리하는 객체로서 비즈니스 로직에서만 존재해야 한다.

만일 초반에 도메인 분석을 제대로 하고, 전체적인 비즈니스 로직을 자세히 검토했다면 두 Pair가 의미하는바가 다르다는걸 인지했고, 이러한 문제를 사전에 방지할 수 있었을 것이다.

 

이렇게 알아낸 사실을 바탕으로 다시 도메인 모델을 뽑아보면

위와 같이 된다. 기존의 Pair - Attendance가 1:N 관계였던것에 비해 1:1 관계가 되었다.

위와 같이 GroupedAttendance라는 객체를 하나 만들어 OneToOne으로 Attendance와의 연관관계도 만들어 주었다. 이때 Attendance에서는 GroupedAttendance를 알 필요가 없으므로 단방향맵핑으로 하였다. 또한 기존의 테이블 구조를 유지하기 위해 @Table을 통해 Pair로 테이블 명을 명시해 주었고 변경된 구조에 맞게 비즈니스 로직도 적절히 리팩토링 하였다

리팩토링 후 데이터베이스 구조이다. 레거시때와 동일한 것을 확인할 수 있다.

 

혹자는 이전의 테이블 구조가 마음에 들수도 있다. 하지만, 레거시를 마이그레이션 하는데 있어서 테이블 구조, 이름 자체가 변경된다면, 너무 많은 비용이 발생할 것이다. 따라서 최대한 레거시의 데이터베이스 구조를 유지하도록 하였다.

 

다시한번, 프로젝트를 진행함에 있어 도메인에 대한 이해, 도메인 구조 분석의 중요성을 느꼈다. 간단하게 끝날줄 알았던 작업으로 인해 정말 좋은 경험을 했다. 레거시 프로젝트를 JPA로 변경할 때, 단순하게 객체를 테이블에 매핑하려 하면 안된다. 충분한 도메인 분석과 ERD분석이 함께 수반되어야 한다.

댓글