35
레거시 DB+JPA﴾+DDD 구현 패턴﴿ 적용기 신림프로그래머 세미나, 2017‐12‐17, 최범균[email protected]﴿

레거시 DB+JPA(+DDD 구현 패턴) 적용기

Embed Size (px)

Citation preview

Page 1: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

레거시 DB+JPA﴾+DDD 구현 패턴﴿ 적용기신림프로그래머 세미나, 2017‐12‐17, 최범균﴾[email protected]﴿

Page 2: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

내용

두 개의 프로젝트레거시DB + JPA﴾+DDD﴿ 적용 예주의사항﴾시행착오﴿정리

신림프로그래머 세미나 2

Page 3: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

1년간 진행한 두 개의 프로젝트항목 상반기 프로젝트1 하반기 프로젝트2

내용 고객용 모바일 앱 직원용 모바일 업무 앱

업무 API 서버 API 서버

특징 범위가 넓음﴾다양한 업무﴿ 특정 영역에 한정

공통점

레거시 DB 직접 연동 ﴾기존 레거시에 API 없음﴿기존 개발자는 레거시 시스템 운영/유지보수 위주

레거시: 마이플랫폼, 쿼리 중심

API 서버는 발표자 포함 2명이서 개발

신림프로그래머 세미나 3

Page 4: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

상반기 프로젝트개인적으로 개발 PM과 API 서버 개발 병행

현업﴾갑﴿, 기획과의 회의, 응대에 꽤 많은 시간 씀초기에 구현 기술을 고민하고 준비할 시간 부족

업무 범위가 넓음 ‐> 업무 분석 위해 상대해야 할 레거시 개발자가 많음

레거시는 테이블과 쿼리 중심 개발 ﴾where 절에 업무 로직 많음﴿현실적인 선택

협업 개발자 분이 익숙한 기술과 구조 사용쿼리 중심 ‐> MyBatis, 연통구조 : 컨트롤러﴾API﴿ + 서비스 + DAO

기존 쿼리를 최대한 재활용하는 방식으로 진행

신림프로그래머 세미나 4

Page 5: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

연통구조

신림프로그래머 세미나 5

Page 6: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

코드를 덜 더럽게 만들려 노력했으나, 아쉬운 결과물깨지기 쉬운 코드

불필요한 모델 중복 발생불필요한 쿼리 중복 발생

같은 듯 미세하게 다른﴾조인 테이블 대상, where 조건 등﴿ 쿼리단위/통합 테스트가 깔끔하지 않음

분석이 ﴾다소﴿ 드러운 코드 발생로직이 포함된 쿼리를 재사용하면서 로직이 분산조회 기능은 그나마 괜찮은데, 수정 기능은 분석에 더 많은 시간 필요한 코드

신림프로그래머 세미나 6

Page 7: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

하반기 프로젝트속으로 결정

기술에 고민할 수 있게 개발에 집중할 수 있는 여견을 만들자

가능하다면 쿼리 중심이 아닌 도메인 중심으로 개발하자

신림프로그래머 세미나 7

Page 8: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

하반기 프로젝트기술에 고민할 수 있게 개발에 집중할 수 있는 여견을 만들자

상반기 프로젝트 오픈 준비와 하반기 프로젝트 기획이 겹침개발 PM을 다른 분이 맡게 됨

쿼리 중심에서 도메인 중심으로 개발하자

신림프로그래머 세미나 8

Page 9: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

하반기 프로젝트기술에 고민할 수 있게 개발에 집중할 수 있는 여견을 만들자

상반기 프로젝트 오픈 준비와 하반기 프로젝트 기획이 겹침

개발 PM을 다른 분이 맡게 됨쿼리 중심에서 도메인 중심으로 개발하자

유연할 것 같은 프리랜서 분 선택레거시 DB이긴 하지만, JPA+DDD 적용 결심원하는 구조의 샘플 코드를 제공해서 일정한 틀 안에서 개발하도록 유도

신림프로그래머 세미나 9

Page 10: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

하반기 프로젝트 준비프리랜서 분 출근한 후 실제 성향 분석

슬쩍 JPA 던져 봄 ‐> 반응 좋음

얼른 표준 구조 샘플 제공PK 칼럼이 1개이고 관련 테이블도 1개인 모델 사용DDD﴾Domain‐Driven Design﴿에 기반한 구조로 샘플 제공

컨트롤러‐서비스‐도메인﴾엔티티+리포지토리﴿Spring Data JPA, JPA

리포지토리 인터페이스만 만들면 되는 것: 반응 좋음조회 관련 통합 테스트 코드 포함

신림프로그래머 세미나 10

Page 11: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

@Getter // setter 없음@NoArgsConstructor @Builder @AllArgsConstructor@Entity@Table(name = "DRAINAGE")public class Drainage {    @Id    @Column(name = "FACI_NUM")    private String faciNum;

    @Column(name = "BRANCH_CD")    private String branchCd;

    @Column(name = "TEAM_CD")    private String teamCd;

    ...        @Embedded // 밸류    private CreateInfo createInfo;

public interface DrainageRepository     extends Repository<Drainage, String> {    Drainage findOne(String id);}

public class DrainageDataService {    private DrainageRepository repository;        public Drainage getDrainage(String id) {        Drainage drainage = repository.findOne(id);        if (drainage == null)            throw new DataNotFoundException();        return drainage;    }        // setter}

단순 조회 기능 위주로 기능 구현 시작 ‐> 기본 습득 진행

신림프로그래머 세미나 11

Page 12: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

공통 밸류 도입모든 모델에 공통으로 적용되는 밸류 구성

@Embeddablepublic class CreateInfo {    @Column(name = "CRT_DTM")    private LocalDateTime datetime;    @Column(name = "CRT_EMPID")    private String employeeId;    @Column(name = "CRT_IP")    private String ip;

    ...}

신림프로그래머 세미나 12

Page 13: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

도메인 영역의 엔티티/밸류에 setter 넣지 않음의도적으로 setter를 만들지 않음필요한 경우에 한해서 선택적으로 setter 추가setter 대신 기능을 의미하는 메서드를 넣는 시도

신림프로그래머 세미나 13

Page 14: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

단순 조회 기능의 통합 테스트 코드 샘플 제공@RunWith(SpringRunner.class)@SpringBootTestpublic class DrainageDataServiceIntTest {    @Autowired private DrainageDataService svc;

    @Test    public void 존재하면_구함() {        Drainage faci = svc.getDrainage("DR000024");        assertThat(faci).isNotNull();    }

    @Test(expected = DataNotFoundException.class)    public void 없으면_익셉션() {        svc.getDrainage("DRXXXXXX");    }}

테스트 코드 작성은 가능하면 작성할 것을 요청

신림프로그래머 세미나 14

Page 15: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

응용 서비스 메서드를 위한 별도 파라미터 사용@Servicepublic class BigCheckRegistService {

    @Transactional    public void regist(BigCheckRegistRequest regReq) {        ...        BigCheckHead newHead = createBigCheckHead(regReq);        ...    }

응용 서비스에 전달한 파라미터는 요청 데이터 기준으로 작성

신림프로그래머 세미나 15

Page 16: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

구현 기능 범위를 넓혀가며 필요한 샘플 제공리포지토리 샘플: 저장, 페이징, 단순 검색 조건복합키에 대한 매핑 설정 샘플스펙을 이용한 검색 조건 설정 샘플

신림프로그래머 세미나 16

Page 17: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

스펙 샘플public class DigWorkSpecs {    public static Specification<DigWork> untreated() {        return (Root<DigWork> root, CriteriaQuery<?> cq, CriteriaBuilder cb) ‐> {            ...        };    }

    public static Specification<DigWork> timePassed(LocalDateTime findTime, int[] hours) {        return (Root<DigWork> root, CriteriaQuery<?> cq, CriteriaBuilder cb) ‐> {            ...        };    }}

Specifications<DigWork> spes = Specifications                  .where(DigWorkSpecs.untreated())                  .and(DigWorkSpecs.timePassed(findTime, hours));Sort sort = new Sort("jupno");List<DigWork> works = digWorkRepository.findAll(spes, sort);

신림프로그래머 세미나 17

Page 18: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

스펙 사용 이유? QueryDSL, jooq를 사용하지 않은 이유?QueryDSL, jooq 잘 모름스펙을 사용해서 도메인 용어로 조건을 표현하려고

신림프로그래머 세미나 18

Page 19: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

협업 개발자 분의 JPA에 대한 반응JPA에 만족함스프링 데이터 JPA에 만족함DDL 기반 엔티티 생성기도 개발하심 ‐‐> 생산성 향상에 도움

신림프로그래머 세미나 19

Page 20: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

레거시 대응select에서 procedure, subselect 사용 ‐> @Formula

조회 전용 Entity ‐> @Subselect, @Immutable쿼리를 이용한 아이디 생성 ‐> @Query두 테이블로 나뉜 엔티티, 칼럼 개수가 많음 ‐> 1‐1 엔티티 매핑복합키를 사용한 1‐N ‐> 밸류 콜렉션

신림프로그래머 세미나 20

Page 21: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

@Formula 사용    @Formula("SOME_PKG.GET_CODE_VALUE('D123456', STS_CD)")    private String stsNm;

    @Formula("(select gis.ID FROM OSCHEMA.DRAINAGE gis "            + "WHERE gis.FACI_ID = FACI_ID)")    private Long gisId;

신림프로그래머 세미나 21

Page 22: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

@Subselect로 조회 전용 모델조회 전용 모델에서 쿼리를 직접 사용해야 할 때 사용

@Immutable@Subselect("SELECT TB.FACI_NUM, ...생략 "        + "from DAY_PLN PL, TEST_BOX TB, GTEST_BOX GTB "        + "WHERE PL.FACI_NUM = TB.FACI_NUM AND TB.FACI_NUM = GTB.FACI_NUM "        + "UNION "        + "SELECT GV.FACI_NUM, ...생략  "        + "FROM DAY_PLN PL, GOV GV, GOVROOM GR, GGOV GGV "        + "WHERE PL.FACI_NUM = GV.FACI_NUM ...생략 "        + "UNION "        + "SELECT VV.FACI_NUM, ...생략 "        + "FROM DAY_PLN PL, VALVE VV, GVALVE GVV, VALVEBOX VB "        + "WHERE PL.FACI_NUM = VV.FACI_NUM ....생략 "        )@Entitypublic class FacilityCheck {   ...

신림프로그래머 세미나 22

Page 23: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

쿼리를 이용한 아이디 생성 ‐> @Query기존 insert 쿼리에 포함되어 있던 것을 리포지토리의 기능으로 분리

public interface SmartMeasureRepository extends Repository<SmartMeasure, String> {

  // YYYYMM + 시퀀스(5)  @Query(value =     "SELECT to_char(sysdate, 'yyyymm') || LPAD(NVL(MAX(SUBSTR(REQ_NUM, 7, 5)), 0) + 1, 5, '0') " +    "FROM SMART_MEAS where SUBSTR(MEAS_REQ_NUM, 1, 6) = to_char(sysdate, 'yyyymm') ",     nativeQuery = true)  String generateNextReqNum();

SmartMeasure meas = SmartMeasureFactory.create(saveRequest, ...        smartMeasureRepository.generateNextReqNum());

신림프로그래머 세미나 23

Page 24: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

@Entity@Table(name = "GIS_CHG")@SecondaryTable(name = "GIS_ATTACH",    pkJoinColumns =        @PrimaryKeyJoinColumn(            name = "REQ_NUM",             referencedColumnName = "REQ_NUM"))public class GisChg {    @Id    @Column(name = "REQ_NUM")    private String reqNum;

    @Embedded    private Attach attach;

@Embeddablepublic class Attach {

    @Column(name = "ATTACH_FILE_NUM",             table = "GIS_ATTACH")    private Integer attachFileNum;

    ... // 칼럼마다 table 속성 추가}

두 테이블로 나뉜 엔티티, 칼럼이 많은 테이블칼럼이 많아서 @SeondaryTable 사용하면 다소 번잡

신림프로그래머 세미나 24

Page 25: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

@Entity@Table(name = "GIS_CHG")public class GisChg {    @Id    @Column(name = "REQ_NUM")    private String reqNum;

    @OneToOne(cascade = CascadeType.ALL,               mappedBy = "gisChg")    private Attach attach;

라이프사이클을 맞추기 위해 cascade를 ALL로 설정

@Entity@Table(name = "GIS_ATTACH")public class Attach {

    @Id    @Column(name = "REQ_NUM")    private String reqNum;

    @OneToOne    @PrimaryKeyJoinColumn    private GisChg gisChg;

    @Column(name = "ATTACH_FILE_NUM")    private Integer attachFileNum;}

주요키를 공유한 1‐1 연관으로 엔티티‐밸류 모델 설정

신림프로그래머 세미나 25

Page 26: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

CHECK_H ‐ CHECK‐D = 1:N복합키로 조인GROUP과 ITEM으로 정렬 처리

복합키를 사용한 밸류 콜렉션 매핑

신림프로그래머 세미나 26

Page 27: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

@Entity@Table(name = "CHECK_H")public class Check {

  @EmbeddedId private CheckId id;

  @Column(name = "FROM_TIME") private String fromTime;  @Column(name = "TO_TIME") private String toTime;

  @ElementCollection(fetch = FetchType.EAGER)  @CollectionTable(name = "CHECK_D", joinColumns = {      @JoinColumn(name = "JOIN_NUM", referencedColumnName = "JOIN_NUM"),      @JoinColumn(name = "PATH_FLAG", referencedColumnName = "PATH_FLAG"),      @JoinColumn(name = "JOIN_YMD", referencedColumnName = "JOIN_YMD"),      @JoinColumn(name = "RSLT_FLAG", referencedColumnName = "RSLT_FLAG")  })  @org.hibernate.annotations.OrderBy(clause = "GROUP asc, ITEM asc")  private Set<CheckDetail> details = new LinkedHashSet<>();

@Embeddablepublic class CheckId        implements Serializable {  @Column(name = "JOIN_NUM")  private String joinNum;  @Column(name = "PATH_FLAG")  private String pathFlag;  @Column(name = "JOIN_YMD")  private String joinYmd;  @Column(name = "RSLT_FLAG")  private String rsltFlag;

@Embeddablepublic class Detail {  private String group;  private String item;  private String rslt;  ...}

복합키를 사용한 밸류 콜렉션 매핑

신림프로그래머 세미나 27

Page 28: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

적용한 곳:

1‐N 콜렉션 매핑을 사용한 곳에 적용여러 테이블을 조인해서 목록을 보여주는 기능에 적용

조회 영역:

@Query, @SubselectJdbcTemplate, MyBatis

모델에 따라 CQRS 적용

신림프로그래머 세미나 28

Page 29: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

수정과 조회 서비스에서의 기술 비율

종류 수정 조회 비율

JPA 13 16 78%

Subselect ‐ 4 11%

JdbcTemplate ‐ 3 8%

MyBatis ‐ 1 3%

합 13 24 ‐

모델의 사용 기술 비율

종류 개수 비율

JPA 54 85.7%

Subselect 4 6.3%

JdbcTemplate 3 4.8%

MyBatis 2 3.2%

합 63 ‐

DB 관련 기술 비율

신림프로그래머 세미나 29

Page 30: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

// 테스트 코드의 @Transactional// 테스트 실행 후 롤백 처리@Transactionalpublic void someTest() {    someService.operation(req); // 이 시점에 커밋 없음        RowSet rs = jdbcTemplate.queryForRowSet(" ... ");    rs.next();    assertThat(rs.getString(1))        .isEqualTo(기대값); // 실패}

@Transactionalpublic void operation(SomeReq req) {  Some some = someRepository.findOne(req.getId());  some.doOp(req.getValue());      // someRepository.flush()}

통합테스트와 flush﴾﴿스프링 통합 테스트의 @Transactional에 따른 트랜잭션 범위 바뀜

신림프로그래머 세미나 30

Page 31: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

JPA를 처음 경험할만한 시행착오 1같은 테이블에 대해 조회 또는 수정할 속성 개수에 따라 엔티티, 리포지토리 생성

필요한 칼럼만 조회하거나 수정하는 쿼리 중심 사고에서 비롯예

DigWork, DigWorkRepository

DigWorkState, DigWorkStateRepository

개념적으로 하나인 모델에 대해 단일 엔티티를 사용하도록 유도

신림프로그래머 세미나 31

Page 32: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

JPA를 처음 경험할만한 시행착오 2수정 기능에서 save 사용

기존에 존재하는 모델에 대해, 새로 객체를 생성해서 머지조인 테이블에 저장한 밸류 콜렉션 데이터 유실

JPA의 엔티티 라이프 사이클

응용 서비스에서 논리적인 흐름을 처리하도록 코드 수정

먼저 엔티티를 찾고﴾findOne﴿, 존재하면 로딩한 엔티티를 변경하도록 코드 수정

신림프로그래머 세미나 32

Page 33: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

JPA를 처음 경험할만한 시행착오 3단일 모델 사용하려는 시도

신림프로그래머 세미나 33

Page 34: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

아쉬운 점과 얻은 점아쉬운 점

용어에서의 타협레거시 테이블, 칼럼 이름 그대로 사용 ‐> 유지보수 중인 개발자에 인수인계 해야 함

통합테스트 코드 작성을 유도한데 만족TDD는 실제 코드 작성자 본인의 연습이 필요

그래도

상반기보다 훨씬 깨끗한 코드 얻음향후 리팩토링 범위가 작아짐로직이 집중, 쿼리에 로직이 거의 없음

신림프로그래머 세미나 34

Page 35: 레거시 DB+JPA(+DDD 구현 패턴) 적용기

신림프로그래머 세미나 35