Sep 15, 2023 - 개발팀의 진행 프로세스

개발팀 진행 업무 프로세스

Sprint Life Cycle

SLC

SLC

Requirement Life Cycle

RLC

RLC

상태 설명 상태 전이 가능자 정보 추가
요청됨 개발 요청서 작성 누구나 오류내용, 기대결과
보류 개발안함, 다른방식으로 기능 제공 됨 PMO, 개발자 사유
요구사항분석 백로그, 구체적 내용 확인 (필요 시 인터뷰) PMO, 개발자 담당자 지정
개발 중 담당자가 업무를 시작함 담당 개발자 작업일정
개발완료 요구사항 개발 완료 담당 개발자 (검증 환경 및 조건)
검증 중 QA담당자가 확인하는 중 QA 담당자  
수정 중 개발된 결과에 오류가 발견됨 QA 담당자 재현 조건
검증완료 기능동작 확인. 배포대기 QA 담당자 배포 버전/일, 릴리즈 노트
배포됨 수정내용이 포함된 바이너리가 배포됨 PMO, 개발자 배포버전, 배포일

PMO 는 요구사항 관계자로 대체 가능합니다.
이러한 프로세스 표는 프로젝트 관리와 협업을 효과적으로 조직하고 추적하는 데 도움이 되며, 무조건적인 진리는 아닌, 프로젝트의 크기, 복잡성 및 요구 사항에 따라 조정이 가능한 영역입니다.

다만, 이표를 근거로, 프로젝트의 상태 및 진행을 시각적으로 추적하고, 팀 간의 의사 소통을 강화하기 위해 도입되었습니다.

Semantic Versioning

참고: https://semver.org/lang/ko/

Semantic Versioning (SemVer)를 커스터마이즈하여 프로젝트에 맞게 조정하는 것은 흔한 일입니다.

  1. 기존 버전과 호환되지 않는 대규모 업데이트는 “MAJOR” (주) 버전을 올린다.
  2. 기존 버전과 호환되면서 새로운 기능을 추가할 때는 “MINOR” (부) 버전을 올린다.
  3. 기존 버전과 호환되면서 버그를 수정한 것이라면 “PATCH” (수) 버전을 올린다.
  4. QA 를 할 수 없거나, 필요가 없는 업데이트 (ex : sre 작업, info, error 등 백엔드 단순작업, query 수행 등)라면 “NOQA” (무시주석) 버전을 올린다.

현재 배포 예정중인(비정기 배포) 사항에 긴급하게 Hotfix 처리를 통해서 배포 되어야할 경우
부득이 하게 miner(N.N.N) 버젼명을 변경하여 예정중인 버젼명을 일괄 변경 처리해야되는 부분을 고도화 하여
miner 버젼 숫자를 두자리(NN)으로 적용해서 차기 버젼명을 바꾸지 않고 처리하는것으로 정리.

Sep 8, 2023 - 인공지능 소프트웨어 품질보증을 위한 테스트 기념

simple3

AI 는 엄청난 발전을 하고 있고, 뛰어난 많은 개발자들에 의해 정형화, 일반화된 모델 및 라이브러리가 존재합니다.

그러나 AI 를 정확하고 명확하게 사용하기 위해서는 질문과 정답에 대한 많은 샘플 데이터가 필요합니다. 그렇게 했을 때 샘플만의 데이터를 가지고 테스트를 어떻게 성공할 수 있는지에 대한 설명을 합니다.

1장은 기본적인 AI 모델 기법에 대해서 나옵니다. 은닉층, 포레스트, 앙상블 등 기초적인 부분의 일반적인 내용이 그림과 같이 잘 설명되어있습니다! 회사에 QA 팀이 있어, 테스트 방법론에 관심이 있어 AI 의 자동 테스트 기법이라 판단해서 서평을 신청한 건데, 책 자체가 자신이 만든 AI 개발건에 대한 테스트 방법론에 대한 이야기였습니다.

테스트 기법의 이름은 메타모픽, 뉴런 커버리지, 최대 안전 반경, 커버리지 이렇게 4개의 기법에 대해 설명을 하고 있습니다. 개발자는 TDD 라는 개념이 있어, 테스트 주도 개발에 대해서 접근을 하려고 하는데, AI 의 경우는 학습이 완료된 모델에 대한 테스트 코드를 작성하는 방법에 대해 이야기 하고 있어 매우 재미있게 보았습니다. 실제로 저도 테스트 코드를 개발을 하면서 진행하기보다, 배포 전에 유니 테스트가 가능한 개발건이었는지를 검토하는 단계에서 테스트 코드를 작성하고 있습니다.

그래서 메타모픽의 경우, 학습된 모델에 대해서 각도를 회전시키면서 변경하여 처리하는 것을 이야기합니다. 해당 부분까지가 4장까지 이야기인데, 대부분의 테스트 코드는 DNN 을 기준으로 작업이 되어있습니다.

– 책을 다 읽고나서 9월 15일 추가내용입니다.– AI 개발은 데이터가 매우 중요하다는 생각을 합니다.

AI 모델을 효과적으로 테스트하기 위해서는 풍부한 샘플 데이터가 필요하고, 이 데이터는 모델을 학습시키는 데 사용된 데이터와 다를 수 있어야 합니다.
모델이 학습한 데이터에 대해서는 이미 잘 수행할 것으로 예상되기 때문에, 따라서 테스트 데이터는 다양한 시나리오와 엣지 케이스를 포함하고 있어야 한다는 생각은 자주했는데, 이 책은 그러한 부분에 자세한 설명이 있는 거 같습니다.

책에는 테스트 방법론에 대해서 나왔는데, AI 모델의 개발과 테스트는 반복적인 과정에 대해, 새로운 모델 버전을 개발하고 테스트할 때마다 모델의 버전 관리 내용이 있었으면 더 좋지 않았을까 아쉬운 점이 있습니다.
이를 통해 어떤 모델 버전이 어떤 수정 사항을 포함하고 있는지 추적할 수 있으며, 문제가 발생했을 때 특정 버전으로 롤백할 수 있습니다.

완성된 모델이 잘 만들었는지가 아닌, 테스트 주도 개발(TDD)에 대한 내용도 있었으면 좋지 않았을까 싶은데, AI 에 적용하긴 아직 먼 미래같습니다.
테스트 환경 구성 자체가 실업무가 아닌, 초심자 기준인 것도 조금 아쉬웠습니다.

마지막으로 자동화 테스트에 대한 부분이라든가 실업무에 도입되기에는 이론적인 부분이 많아보입니다!

Apr 19, 2022 - 백엔드 아키텍쳐 (beta v1.0) 1차

(회사 소스를 제거하다보니, 너무 난해한 문서가 되었음. 관련 내용은 한번더 예제 샘플 생성 후, velog 쪽으로 신규 개설 후 작성해야할듯..)

관련 용어 정리

객체 폭발

  • 계층간 사용되는 데이터 구조를 다른 계층에서 사용 시 또다시 객체를 만들 때 중복되서 다시 객체가 생성되는 현상을 의미함.

응집도

  • 하나의 클래스가 하나의 기능(책임)을 순도 높게 담당 (해당 문서에서는 기능적 응집에 국한되어 이야기함.)

Entity, Domain : 엔티티 정의 (데이터베이스의 필드[컬럼])

  • 핵심 업무 규칙을 캡슐화
  • 메서드를 가지는 객체 거나 일련의 데이터 구조와 함수의 집합
  • 가장 변하지 않고, 외부로부터 영향받지 않는 영역

UseCase

  • Service (UseCase) : 비즈니스 규칙 정의 (Service → ServiceImpl)
  • ServiceImpl : 비즈니스 구체화
  • 애플리케이션에 특화된 업무 규칙을 포함
  • 시스템의 모든 유스 케이스를 캡슐화하고 구현
  • 엔티티로 들어오고 나가는 데이터 흐름을 조정하고 조작

전통적인 Controller, Service, Repository

simple3

mapstruct 를 이용한, 전통적인 웹 애플리케이션 구조입니다.
웹 계층, 도메인 계층, 영속성 계층으로 구성되어있습니다.

웹 계층은 도메인 계층에 의존하고,
도메인 계층은 영속성 계층에 의존하기 때문에 자연스레 데이터베이스에 의존하게 됩니다.

전통적인 계층형 아키텍처에서 전체적으로 적용되는 유일한 규칙은,
특정한 계층에서는 같은 계층에 있는 컴포넌트나 아래이 있는 계층에만 접근 가능하다는 것이다.

따라서 상위 계층에 위치한 컴포넌트에 접근해야한다면, 해당 컴포넌트를 계층 아래로 내려버리면 됩니다.
그리고 그런 내려지는 행위가 반복되면, 지름길을 만드는 것을 당연하게 생각하면,
하위 계층은 점점 비대하게 됩니다.

계층(Layer)를 구분하는 이유는 다음과 같습니다. 프로젝트의 이해도가 낮아도 전체적인 구조를 빠르게 이해가능합니다. (메소드명의 목적이 명확할 수록 더욱 빠르게 이해가능합니다.) 작성하고자하는 계층이 명확할 수록 더 빠르게 개발가능합니다.

다만, 각 레이어별로 수집개의 클래스들이 존재하게 되면서 코드 파악이 더욱 어려워질 수도 있으며, Layer 기준으로 분리했기에 코드의 응집도가 떨어질 수 있습니다.

그래서 1차 개발 시, 그 계층을 중지시키기 위한 Reader, Writer 를 만들었습니다.
추후, Master, Slave 의 구성을 위해서 분리하는 작업을 진행했습니다.

Request 객체를 전달하게 되면 의존성이 생기므로 전송 데이터 객체(DTO)와 같은 형태로 Wrapping하여 전달.
Return type의 경우 최종 응답 데이터 뿐만 아니라 다양한 정보(Meta data)들을 담아야 하는 상황이 발생할 수 있으므로 애그리거트 객체를 생성하여 사용

이 두가지를 편하게 사용하기 위해 mapstruct 를 도입하게 되었습니다. (https://mapstruct.org/)

그외의 규칙

  1. 파라미터는 단 한개만 사용 (강제 사항은 아님)
  2. 모든 프론트엔드의 return 하는 데이터는 테이블 기반으로 합니다. (필드명 등 편의성 사용)
  3. 중복은 허용되지 못합니다. 우발적 중복은 허용가능합니다.
    진짜 중복
    - 한 인스턴스가 변경되면, 동일한 변경을 그 인스턴스의 모든 복사본에 반드시 적용해야한다.
    우발적 중복(거짓된 중복)
    - 중복으로 보이는 두 코드의 영역이 각자의 경로로 발전한다면, 즉 서로 다른 속도와 다른 이유로 변경된다면 이 두 코드는 진짜 중복이 아니다.
    

와 같은 다양한 규칙으로 만들어진 계층 구조는 다음과 같습니다.

simple3

컨트롤러 → Service → Reader(or Writer) → Repository 형태로 호출됩니다. Service는 Use Cases 영역에 속하며, 각 계층간 분리를 위해, 서비스 영역은 Model 객체만 가지며, 컨트롤러와 Reader에서 Entity , Payload 등의 데이터를 Model 객체로 가공하여 전달합니다. (이때 MapStruct 가 사용됩니다.)

Reader와 Writer를 도입한 이유는 persistence 영역의 JDBC 가 JPA로 변경되었을 때나, DB 이중화 작업 시,
Service(Use Cases)의 최소한의 변경을 위하여 도입하였으나,
결국 불필요한 복제가 필요한 로직이 되었습니다.

불필요한 Reader 와 Writer 를 지우고 경량화가 가능하자는 의견들이 나오게 되었습니다.
그리고 Service 또한 모듈로 분리하는 것입니다.
추후 Master, Slave 시 Replication 대응을 위해 필요할 수도 있지만, 어노테이션으로 슬레이브, 마스터를 가리킬 수 있으므로 큰 강제성을 하지 않아도 된다고 생각합니다.

신규입사자분의 경우 대부분의 기존 Controller, Service, Repogitory 환경에서 작업했을 테니, 이해하거나 적응하기 어렵고, 우회할 수 있는 여러 여지가 있기 때문에, 강제성을 가지고 개발을 할 수 있는 구조가 무엇인지 다시금 고민하게 되었습니다.
(ex : 주입이 상관없으므로 interface 없는 @Service 생성, Service에서 바로 Repogitory 호출을 막을 방법이 없는 등.)

격리벽을 높여 강제성을 만드는 여러 방법 중에는 public > protected > default > private 에 default 만 정의하여 ,
구현체는 스프링부트의 어노테이션에서 주입하는 형태로만 허용 등이 있을 수 있었습니다.

결국 클린 아키텍쳐로 구성된 최종 서비스 구성도 입니다.

simple3

input 과 output 으로 만들어진 구조를 간단하게 포트 & 어댑터 디자인 패턴이 적용된 모듈입니다.
잘못된 의존성을 막기 위해 아키텍쳐를 여러 개의 빌드 아티택트로 만드는 여러가지 방법을 “만들면서 배우는 클린 아키텍쳐”란 책에서 영감을 받았습니다.

simple3

해당 구조에서 왼쪽에서 2번째 항목으로 구성을 구현하게 되었습니다.

어댑터 패턴 (adapter pattern)

한 클래스의 인터페이스를 클라이언트에서 사용하고자하는 다른 인터페이스로 변환합니다.
어댑터를 이용하면 인터페이스 호환성 문제 때문에 같이 쓸 수 없는 클래스들을 연결해서 사용할 수 있으며, 호환되지 않는 인터페이스를 사용하는 클라이언트를 그대로 활용할수도 있습니다.
이렇게 함으로써 클라이언트와 구현된 인터페이스를 분리시킬수 있으며, 향후 인터페이스가 바뀌더라도 그 변경 내역은 어댑터에 캡슐화 되기 때문에 클라이언트를 바꿀 필요가 없어집니다.

여기서 Port 개념이 추가되면서, 보통 포트와 어댑터 패턴이라고 합니다.
이미 api-admin 란 서비스를 구현한다고 했다면, 실제 구조는 다음과 같습니다.

simple3

컨트롤러 영역인 presenters 모듈
서비스 영역인 usecases 모듈 그리고 jdbc 를 직접구현하거나 이미 만들어져있는 Repogitory 모듈을 dependence하는 persistence 모듈로 분리합니다.

presenters-xxxx

  1. dependencies
dependencies {
    implementation(project(":modules:common-util"))
    implementation(project(":modules:common-payload"))
    implementation(project(":modules:modules-usecases:usecases-xxxx"))

    // mapstruct
    implementation("org.mapstruct:mapstruct:1.4.2.Final")
    annotationProcessor("org.mapstruct:mapstruct-processor:1.4.2.Final")

    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-validation'

    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
}

공통 payload

공통 utils

usecases

mapstruct

springboot-web

  1. package 구조

동일 class 는 구현체 오류가 나는 걸 방지하기 위해 앞에 Presentation 의 약어 Pre를 붙여서 만듬.

persistence-xxxx

  1. dependencies
dependencies {

    implementation(project(":modules:repository"))
    compile(project(":modules:db"))
    implementation(project(":modules:common-util"))
    implementation(project(":modules:modules-usecases:usecases-xxxxx"))
    implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
    runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'

    implementation 'org.springframework.boot:spring-boot-starter-web'

    // mapstruct
    implementation("org.mapstruct:mapstruct:1.4.2.Final")
    annotationProcessor("org.mapstruct:mapstruct-processor:1.4.2.Final")

    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
}

modules:repository
modules:db
modules:common-util
modules:modules-usecases:usecases-xxxxx

  1. 패키지 구조

usecases-xxxx

  1. dependencies
dependencies {
    implementation(project(":modules:common-util"))
    implementation(project(":modules:message"))
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'

    // https://mvnrepository.com/artifact/org.mariuszgromada.math/MathParser.org-mXparser
    implementation group: 'org.mariuszgromada.math', name: 'MathParser.org-mXparser', version: '4.4.2'

    implementation 'org.springframework.boot:spring-boot-starter-web'
}
  1. 패키지

port.input →
port.output →

트랜젝션 위치?

@Transactional

implementation("org.springframework:spring-tx:5.3.6")

원하는 위치나 로직이 있을 경우 dependencies 에 추가함.

에 추가함.

모든 맵핑을 처리할 것인가?

디코드 3.0의 경우 객체 폭발이라고 할 정도로, entity → model → payload(request,response) 형태의 객체를 만들었습니다.

  • 완전 맵핑 전략으로 갈 것인가?
    결합을 낮추는 게 안정적이라는 기준을 가진다면, 완전 맵핑이 맞습니다.
    • entity 의 필드 변경이 request, response에 영향을 주면 안된다.
    • 격리벽 → entity
  • 불완전 맵핑 전략으로 갈 것인가?
    단순 CRUD 인 api-admin 에서는 비즈니스 모델의 변경이 entity, payload에까지 영향을 주었습니다.
  • trade-off 에 맞추어 각자 생각하여 진행할 것인가? : 개발자 본인의 판단의 선택을 존중합니다. 도메인의 복잡도가 높을 수록, 도메인모델(Entity)를 전체 노출 시킬일이 거의 없을 겁니다. 그러나 복잡도가 낮을 경우에는 도메인모듈에 곧 테이블(DB)며, request Json 이 되므로, 객체폭발을 시킬 이유가 없다고 보여집니다.