Ce diaporama a bien été signalé.
Nous utilisons votre profil LinkedIn et vos données d’activité pour vous proposer des publicités personnalisées et pertinentes. Vous pouvez changer vos préférences de publicités à tout moment.

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

10 172 vues

Publié le

신림프로그래머 발표회 2017-12-17 자료

Publié dans : Technologie
  • Identifiez-vous pour voir les commentaires

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

  1. 1. 레거시 DB+JPA﴾+DDD 구현 패턴﴿ 적용기 신림프로그래머 세미나, 2017‐12‐17, 최범균﴾madvirus@madvirus.net﴿
  2. 2. 내용 두 개의 프로젝트 레거시DB + JPA﴾+DDD﴿ 적용 예 주의사항﴾시행착오﴿ 정리 신림프로그래머 세미나 2
  3. 3. 1년간 진행한 두 개의 프로젝트 항목 상반기 프로젝트1 하반기 프로젝트2 내용 고객용 모바일 앱 직원용 모바일 업무 앱 업무 API 서버 API 서버 특징 범위가 넓음﴾다양한 업무﴿ 특정 영역에 한정 공통점 레거시 DB 직접 연동 ﴾기존 레거시에 API 없음﴿ 기존 개발자는 레거시 시스템 운영/유지보수 위주 레거시: 마이플랫폼, 쿼리 중심 API 서버는 발표자 포함 2명이서 개발 신림프로그래머 세미나 3
  4. 4. 상반기 프로젝트 개인적으로 개발 PM과 API 서버 개발 병행 현업﴾갑﴿, 기획과의 회의, 응대에 꽤 많은 시간 씀 초기에 구현 기술을 고민하고 준비할 시간 부족 업무 범위가 넓음 ‐> 업무 분석 위해 상대해야 할 레거시 개발자가 많음 레거시는 테이블과 쿼리 중심 개발 ﴾where 절에 업무 로직 많음﴿ 현실적인 선택 협업 개발자 분이 익숙한 기술과 구조 사용 쿼리 중심 ‐> MyBatis, 연통구조 : 컨트롤러﴾API﴿ + 서비스 + DAO 기존 쿼리를 최대한 재활용하는 방식으로 진행 신림프로그래머 세미나 4
  5. 5. 연통구조 신림프로그래머 세미나 5
  6. 6. 코드를 덜 더럽게 만들려 노력했으나, 아쉬운 결과물 깨지기 쉬운 코드 불필요한 모델 중복 발생 불필요한 쿼리 중복 발생 같은 듯 미세하게 다른﴾조인 테이블 대상, where 조건 등﴿ 쿼리 단위/통합 테스트가 깔끔하지 않음 분석이 ﴾다소﴿ 드러운 코드 발생 로직이 포함된 쿼리를 재사용하면서 로직이 분산 조회 기능은 그나마 괜찮은데, 수정 기능은 분석에 더 많은 시간 필요한 코드 신림프로그래머 세미나 6
  7. 7. 하반기 프로젝트 속으로 결정 기술에 고민할 수 있게 개발에 집중할 수 있는 여견을 만들자 가능하다면 쿼리 중심이 아닌 도메인 중심으로 개발하자 신림프로그래머 세미나 7
  8. 8. 하반기 프로젝트 기술에 고민할 수 있게 개발에 집중할 수 있는 여견을 만들자 상반기 프로젝트 오픈 준비와 하반기 프로젝트 기획이 겹침 개발 PM을 다른 분이 맡게 됨 쿼리 중심에서 도메인 중심으로 개발하자 신림프로그래머 세미나 8
  9. 9. 하반기 프로젝트 기술에 고민할 수 있게 개발에 집중할 수 있는 여견을 만들자 상반기 프로젝트 오픈 준비와 하반기 프로젝트 기획이 겹침 개발 PM을 다른 분이 맡게 됨 쿼리 중심에서 도메인 중심으로 개발하자 유연할 것 같은 프리랜서 분 선택 레거시 DB이긴 하지만, JPA+DDD 적용 결심 원하는 구조의 샘플 코드를 제공해서 일정한 틀 안에서 개발하도록 유도 신림프로그래머 세미나 9
  10. 10. 하반기 프로젝트 준비 프리랜서 분 출근한 후 실제 성향 분석 슬쩍 JPA 던져 봄 ‐> 반응 좋음 얼른 표준 구조 샘플 제공 PK 칼럼이 1개이고 관련 테이블도 1개인 모델 사용 DDD﴾Domain‐Driven Design﴿에 기반한 구조로 샘플 제공 컨트롤러‐서비스‐도메인﴾엔티티+리포지토리﴿ Spring Data JPA, JPA 리포지토리 인터페이스만 만들면 되는 것: 반응 좋음 조회 관련 통합 테스트 코드 포함 신림프로그래머 세미나 10
  11. 11. @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
  12. 12. 공통 밸류 도입 모든 모델에 공통으로 적용되는 밸류 구성 @Embeddable public class CreateInfo {     @Column(name = "CRT_DTM")     private LocalDateTime datetime;     @Column(name = "CRT_EMPID")     private String employeeId;     @Column(name = "CRT_IP")     private String ip;     ... } 신림프로그래머 세미나 12
  13. 13. 도메인 영역의 엔티티/밸류에 setter 넣지 않음 의도적으로 setter를 만들지 않음 필요한 경우에 한해서 선택적으로 setter 추가 setter 대신 기능을 의미하는 메서드를 넣는 시도 신림프로그래머 세미나 13
  14. 14. 단순 조회 기능의 통합 테스트 코드 샘플 제공 @RunWith(SpringRunner.class) @SpringBootTest public 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
  15. 15. 응용 서비스 메서드를 위한 별도 파라미터 사용 @Service public class BigCheckRegistService {     @Transactional     public void regist(BigCheckRegistRequest regReq) {         ...         BigCheckHead newHead = createBigCheckHead(regReq);         ...     } 응용 서비스에 전달한 파라미터는 요청 데이터 기준으로 작성 신림프로그래머 세미나 15
  16. 16. 구현 기능 범위를 넓혀가며 필요한 샘플 제공 리포지토리 샘플: 저장, 페이징, 단순 검색 조건 복합키에 대한 매핑 설정 샘플 스펙을 이용한 검색 조건 설정 샘플 신림프로그래머 세미나 16
  17. 17. 스펙 샘플 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
  18. 18. 스펙 사용 이유? QueryDSL, jooq를 사용하지 않은 이유? QueryDSL, jooq 잘 모름 스펙을 사용해서 도메인 용어로 조건을 표현하려고 신림프로그래머 세미나 18
  19. 19. 협업 개발자 분의 JPA에 대한 반응 JPA에 만족함 스프링 데이터 JPA에 만족함 DDL 기반 엔티티 생성기도 개발하심 ‐‐> 생산성 향상에 도움 신림프로그래머 세미나 19
  20. 20. 레거시 대응 select에서 procedure, subselect 사용 ‐> @Formula 조회 전용 Entity ‐> @Subselect, @Immutable 쿼리를 이용한 아이디 생성 ‐> @Query 두 테이블로 나뉜 엔티티, 칼럼 개수가 많음 ‐> 1‐1 엔티티 매핑 복합키를 사용한 1‐N ‐> 밸류 콜렉션 신림프로그래머 세미나 20
  21. 21. @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
  22. 22. @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 ....생략 "         ) @Entity public class FacilityCheck {    ... 신림프로그래머 세미나 22
  23. 23. 쿼리를 이용한 아이디 생성 ‐> @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
  24. 24. @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; @Embeddable public class Attach {     @Column(name = "ATTACH_FILE_NUM",              table = "GIS_ATTACH")     private Integer attachFileNum;     ... // 칼럼마다 table 속성 추가 } 두 테이블로 나뉜 엔티티, 칼럼이 많은 테이블 칼럼이 많아서 @SeondaryTable 사용하면 다소 번잡 신림프로그래머 세미나 24
  25. 25. @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
  26. 26. CHECK_H ‐ CHECK‐D = 1:N 복합키로 조인 GROUP과 ITEM으로 정렬 처리 복합키를 사용한 밸류 콜렉션 매핑 신림프로그래머 세미나 26
  27. 27. @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<>(); @Embeddable public 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; @Embeddable public class Detail {   private String group;   private String item;   private String rslt;   ... } 복합키를 사용한 밸류 콜렉션 매핑 신림프로그래머 세미나 27
  28. 28. 적용한 곳: 1‐N 콜렉션 매핑을 사용한 곳 에 적용 여러 테이블을 조인해서 목록 을 보여주는 기능에 적용 조회 영역: @Query, @Subselect JdbcTemplate, MyBatis 모델에 따라 CQRS 적용 신림프로그래머 세미나 28
  29. 29. 수정과 조회 서비스에서의 기술 비율 종류 수정 조회 비율 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
  30. 30. // 테스트 코드의 @Transactional // 테스트 실행 후 롤백 처리 @Transactional public void someTest() {     someService.operation(req); // 이 시점에 커밋 없음          RowSet rs = jdbcTemplate.queryForRowSet(" ... ");     rs.next();     assertThat(rs.getString(1))         .isEqualTo(기대값); // 실패 } @Transactional public void operation(SomeReq req) {   Some some = someRepository.findOne(req.getId());   some.doOp(req.getValue());        // someRepository.flush() } 통합테스트와 flush﴾﴿ 스프링 통합 테스트의 @Transactional에 따른 트랜잭션 범위 바뀜 신림프로그래머 세미나 30
  31. 31. JPA를 처음 경험할만한 시행착오 1 같은 테이블에 대해 조회 또는 수정할 속성 개수에 따라 엔티티, 리포지토리 생성 필요한 칼럼만 조회하거나 수정하는 쿼리 중심 사고에서 비롯 예 DigWork, DigWorkRepository DigWorkState, DigWorkStateRepository 개념적으로 하나인 모델에 대해 단일 엔티티를 사용하도록 유도 신림프로그래머 세미나 31
  32. 32. JPA를 처음 경험할만한 시행착오 2 수정 기능에서 save 사용 기존에 존재하는 모델에 대해, 새로 객체를 생성해서 머지 조인 테이블에 저장한 밸류 콜렉션 데이터 유실 JPA의 엔티티 라이프 사이클 응용 서비스에서 논리적인 흐름을 처리하도록 코드 수정 먼저 엔티티를 찾고﴾findOne﴿, 존재하면 로딩한 엔티티를 변경하도록 코드 수정 신림프로그래머 세미나 32
  33. 33. JPA를 처음 경험할만한 시행착오 3 단일 모델 사용하려는 시도 신림프로그래머 세미나 33
  34. 34. 아쉬운 점과 얻은 점 아쉬운 점 용어에서의 타협 레거시 테이블, 칼럼 이름 그대로 사용 ‐> 유지보수 중인 개발자에 인수인계 해야 함 통합테스트 코드 작성을 유도한데 만족 TDD는 실제 코드 작성자 본인의 연습이 필요 그래도 상반기보다 훨씬 깨끗한 코드 얻음 향후 리팩토링 범위가 작아짐 로직이 집중, 쿼리에 로직이 거의 없음 신림프로그래머 세미나 34
  35. 35. 끝 신림프로그래머 세미나 35

×