개발이군고구마

[사면초가 TDD/OOP] 1. 구현 원칙 확립 (나는 나의 역할 만을 담당한다) 본문

SERVER/Architecture

[사면초가 TDD/OOP] 1. 구현 원칙 확립 (나는 나의 역할 만을 담당한다)

김구황 2025. 5. 19. 22:31
728x90

테스트 코드와 담 쌓고 살았던 나

 

다덜 테스트 코드 테스트 코드 하는데 회사에서도 귀찮아서 매번 시간나면 해야지 해야지 하고 미룬지 어언 2년

얕은 테스트 코드 지식이 만천하에 공개되어 망신을 당할 날이 앞으로 얼마 안남은 것 같다. *^^* 데헷 🤭

 

조금이라도 덜 수치스럽고자 2주 속성 테스트 코드 무한 흡입을 해보련다. 

(아무도 읽진 않겠지만... 누군가는 읽겠지 나같이 허덕이는 사람들....환영해요.....★)

 

 

우선 프로젝트의 설계와 구현 플로우를 살펴보자 

1. 설계 
ㄴDDD를 근간으로 함 (도메인 <- 컨텍스트) 
ㄴ클린 아키텍처 - 헥사고날 (덧붙힘)

2. 구현 1단계 (구현 전 설계) 
ㄴ의사코드  > 이벤트 스토밍 
ㄴCRC 책임 역할 

3. 구현 2단계 (TDD 실제 구현 + 설계) 
ㄴ 그렇게 나눈 클래스들을 어디에 배정할 것인가 

위 전체적인 설계 플로우에서 TDD/OOP가 다루는 영역은 2번과 3번 구현 영역이다. 

 

 

해당 시리즈는 구현 영역안에서 4개의 단계로 나누어 작성할 예정이며, 이 단계를 통해 코드가 실제 '구현' 이 된다. 

[OOP 설계 프로세스] 
1. 설계 원칙 확립 < 구현 1단계

2. TDD 구현 단위 < 구현 2단계
3. TDD 구현 통합 < 구현 최종 
4. 이터레이션과 현실

 


* 해당 시뮬레이션은 사이드프로젝트에서 훌륭한 팀원이 작성해준 '파일 업로드' 기능을 기반으로 쌓아나가본다. 

 

1. 요구사항 분석 (플로우 잡기)

Q. 어떤 시스템을 개발해야하나? 

A. 이미지를 첨부해서 추가하는 기능, 파일 업로드 기능 

 

Q. 실제 사용자는 누구인가?

A. 일반 고객, 관리자  

 

Q. 사용자가 할 수 있는 모든 행동들은? 

A. 

파일을 올리다가 기종 문제 등으로 중간에 첨부가 안될 수도 있음 

파일은 단독 기능으로 제공되지 않고, 모듈별 정보를 입력하는 것에 덧붙여 제공되는 기능임

파일을 추가해놓고 글은 저장하지 않고 다른 페이지로 이동을 할 수도 있음 

 

Q. 파일 업로드는 어떤게 필요한가? 

A. file을 첨부기능 정도로 이벤트를 발생시켜셔 파일 객체를 가져오는 방식

그 외 이미지에 대한 추가 기능은 없다. 첨부 기능이 전부

 

Q. 어떤 속성을 가져야하지? 

A. 

(기본적) 등록날짜, 수정날짜, 등록자 

(파일객체) 파일이름, 콘텐츠타입, 콘텐츠길이, 저장경로,

(설계상 특징) 어떤 모듈이 파일을 수정했는지, 현재 파일의 상태 

 

Q. 로직은 어떻게 동작해야할까? 

A. 하나의 api에 모듈정보와 파일에 대한 모든 책임을 주지 않고, 파일은 파일 / 정보는 정보로 나누어 2단계로 분리해서 진행한다. 

1. 파일이 첨부되는 순간 파일객체를 들고 파일을 저장한다.  (1 step)

2. 파일에 대한 유효성 검증을 시작하고 

3. S3 에 저장될 파일의 key 값이 만들어지고 S3에 연동되어 파일이 저장된다. 

4. S3 버킷에 접근할 수 있도록 url을 프론트에게 응답을 하고 

5. 그 응답과 최종적으로 입력된 정보가 합쳐서 정보 저장 api가 호출된다. (2 step)

 

Q. 각 단계에서 필요한 검증과 에러처리는? 

A. 

1. 프론트에서 넘겨준 파일 객체에 대한 검증 에러처리 

2. S3 에 파일이 잘 저장되었는지, api 호출에 실패하면 에러 처리 

3. 파일이 저장될 때 에러처리, 플래그 값이 있는지 확인 검증 

3. 파일이 삭제 플래그 처리될때 key 값이 유효한지, 또는 key에 파일이 있는지 에러처리

4. 파일이 영구 처리될때 key 값이 유효한지, 또는 key에 파일이 있는지 에러처리

 

Q. 각 과정에서 시스템이 가져야 할 책임은? 

A. 

1. 파일 서비스 

- 파일에 대한 정보

- 파일에 대한 상태를 변경

 

2. storage 서비스 

- 파일을 '저장'하기 위해서 필요한 서비스를 제공

- 실제 S3를 호출한다

- 저장을 위한 파일 객체 변화주는 메서드 등 

 

3. 그 외 이 서비스를 사용하는 모듈들 (+여기서 고려할 것은 아님) 

 

Q. 이 플로우에서 아직 고민이 필요한 점은? 

A. 

1. S3 버킷에 저장되는 파일들의 생명주기는 어떻게 할 것인가. 

2. DB 에 S3 버킷 생명주기를 관리할 수 있도록 파일 객체에 상태/삭제 플래그 처리를 함 

3. 배치가 삭제플래그나 상태가 여전히 permanent 인 경우 DB/S3를 연동하여 삭제처리 필요 

 

 

Q. 결론적으로 로직은 어떻게 설계해야하나? 

A. 

OOP원칙을 따르는 클린아키텍처를 설계 패턴으로 만들어본다고 했다면,

각 계층이 서로 맞다아 있는 부분에선 인터페이스를 두어서 각 단계를 분리시키고 의존성을 줄인다. (객체지향)

컨트롤러(web adapter) - 인터페이스(outport) - 서비스(usecase) - 인터페이스(outport) - 인프라 / 또는 다른 서비스 (클린 아키텍처)

 

파일 데이터 및 상태 변경 인터페이스 

ㄴ 파일 데이터 및 상태 변경 구현체 

    ㄴ 스토리지 업로드 인터페이스 

         ㄴ 스토리지 업로드 구현체 

 

 

Q. 비기능적 요구사항은?

A. 

1. 해당 기능은 모놀리식 구조에서 각 모듈(컨텍스트) 이 공유하는 부분이다. 

연결을 담당하는 인터페이스는 패키지에서 어디에 위치시킬 것인가. 

인터페이스 <- 호출하는 부분

구현체 <- 실제 담당부분 

 

2. 파일과 스토리지 저장은 명백하게 다른 개념이다. 책임을 분리해야한다. 

예) 파일 데이터 '직접적' 변경 != 스토리지 저장을 위한 파일 정보 "추출"

 

 


Event Storming: "비즈니스 플로우"를 팀이 다같이 빠르게 그려보는 것(전체 큰 그림, 소통/아이디어)
CRC 카드: "각 객체(클래스)의 역할"을 구체적으로 잡아보는 것(객체지향적 설계)
DDD: 이 모든 걸 바탕으로 소프트웨어 설계의 중심축을 "비즈니스 도메인"에 두고 코드까지 연결

2.  책임 상세화 (의사코드) 

1. 파일 등록 

1.1 파일 객체에 대한 검증

  • File 자체에 메타 데이터에 대한 검증 
  • 최종 root entity 생성시에 필요한 값이 존재하는지에 대한 검증 

 

트랜잭션 시작 

1.2 파일 상태값 저장 

  • 파일의 상태는 TEMPORARY 디폴트 값으로 저장된다. 
  • 오류가 나면 예외처리 후 롤백을 한다. 

 

1.3 파일을 S3에 업로드 한다. 

  • 외부 시스템 연동 포멧으로 file 을 가공한다. 에러가 나면 롤백된다. 
  • imageUrl을 반납한다. (미리보기 및 모듈별 파일관련 데이터 저장용)

트랜잭션 종료 

 

 

2. 모듈별 정보 저장 (파일 상태값 변경)

트랜잭션 시작 

1.1 모듈별 정보와 imageUrl이 반납되었을 때 정보를 정한다. 

 

💥이미지 url은 등록되었지만, 만약 temp -> perm 으로 변경될 때 에러가 생긴다면 이미지가 지워질 수 있는 것 아닐까? 

👋🏻 여기서 잠깐 문법 학습

❓ 컨트롤러에서 JPA 트랜잭션 시작한 후, 이후 서비스 객체에 @Transactional 걸려있으면 트랜잭션은 언제 종료되나?

1) 영속성 컨텍스트(Persistence Context)
- JPA에서는 엔티티 매니저(EntityManager, 또는 Spring Data JPA의 경우 EntityManagerProxy)가 “영속성 컨텍스트”라는 내부 캐시를 유지
- findById() 같은 메서드를 통해 조회된 User user 객체는 곧바로 영속성 컨텍스트에 “관리되는(Managed)” 상태로 등록
- 자바 객체의 필드 값이 변경될 뿐이지만, JPA는 변경 감지(Dirty Checking) 메커니즘을 통해 이 변경 사실을 추적

2) 트랜잭션 경계(Transaction Boundary)와 Flush 시점
-Spring에서 @Transactional이 붙은 메서드는 해당 메서드 진입 시점에 트랜잭션을 시작하고, 메서드가 정상적으로 종료(예외 미발생)될 때 트랜잭션을 커밋(commit)
-커밋 직전, JPA는 영속성 컨텍스트에 “관리되고 있는 엔티티”들에 대해 변경 감지를 수행하고, 변경된 내용을 데이터베이스에 반영하기 위해 **flush(플러시)**를 자동으로 호출

3) 중첩된(내부) @Transactional 메서드의 전파 속성(Propagation)
- @Transactional의 propagation 속성은 REQUIRED로 설정
- 동일한 트랜잭션에 참여(join)하게 되고, 별도의 새로운 트랜잭션이 생성되지 않음
- 커밋 직전에 JPA가 영속성 컨텍스트를 flush하여 “모든 변경된 엔티티(User, FileEntity 등)에 대해” SQL을 DB에 발행

 

1.2 파일 객체 TEMPORARY -> PERMANENT 로 변경

  • 모듈 데이터에 저장한 imageUrl을 기준으로 imageKey를 찾는다. 
  • imageKey로 FileEntity를 찾는다. 없으면 에러 처리 후 오류 반납 
  • 파일이 있다면 상태 값을 변경한다. 

트랜잭션 종료 

 

 

 

3. 앞으로 남은 로직 

(배치)

DB 상태값 확인 

해당 key DB 삭제 

해당 key를 가진 S3 버킷 이미지 삭제  

 

 


3. Event Storming (MSA 대비 옵셔널)

  • "어떤 일이 벌어졌을 때, 시스템은 어떻게 반응해야 하는가?"
  • 주요 도메인 이벤트, 커맨드, 액터, 시스템 등 도출

- module/image 라고 호출하게 되면 module 과 image 사이는 강하게 결합되어있음

- file 따로 module 따로 구분을 하여 인터페이스로 묶어버리는 것이 더 적절함 


4. CRC   (Class-Responsibility-Collaboration 객체의 역할과 협력 고민) 

  • Event Storming에서 발견된 "이벤트"나 "액터"들을
  • 객체(클래스) 관점에서 Responsibility와 Collaboration을 잡아봄
  • "이 역할을 맡을 객체는 무엇이고, 누구와 협력해야 할까?"
  • 도메인 모델을 더 구체화

파일( Responsibility )  -> 스토리지 ( Collaboration )

모듈 (module)  -> 파일 ( Collaboration )

 

0) 각 모듈의 역할(책임)은 무엇이여야 할까?

- 파일

파일 저장 날짜, 등록인 등의 기본정보

🔯 파일 메타데이터 (객체 파일의 이름, 컨텐츠타입, 길이) 생성 

🔯 파일 상태 변경

수정이 가능할 필드에 대한 객체 생성

파일 삭제 처리 변경

 

- 모듈

각 모듈에 대한 특수한 정보와 이미지 객체 생성

수정이 가능한 필드에 대한 객체 생성 

객체 삭제 처리 변경 

 

 

[파일]

1) 파일이 스토리지 저장 책임을 가져야하는가? 

대안1. 파일안에 스토리지 까지 함께? 

-> 좋지 않다. 스토리지는 S3 라는 외부 시스템과의 연동 클래스이다. 테스트 할때 의존성 끊기가 어렵고

-> 또 스토리지가 많아질 때 확장성도 나쁘다. 

 

대안2. 스토리지와 파일을 분리한다. 

-> 이 방법이 더 바람직하다. 스토리지는 파일에서 추출한 인덱스 정보만 가져가서 key로 들고 있어도 충분하다. 

 

2) 파일과 스토리지의 협력은 어떻게 구성할까? 

흐름1. 파일 - 스토리지를 연결하고 싶다. 

흐름2. 파일은 서비스의 기준 (상위 고수준) - 스토리지 (하위 - 저수준)

흐름3. 파일을 기준으로 인터페이스와 협력을 맺는다.

흐름4. 상위 -> 하위 에 접근할 수 있는 의존성 역전. 낮은 결집도

 

3) 파일과 스토리지의 역할 분배는 어떻게 할까? 대안은 있을까? 

파일 - 정말 파일 그 자체의 값만을 변경한다. 즉, 새로운 객체를 만들거나(예외 검증) 상태 변경메서드만 책임진다.

ㄴ 파일 메타 데이터 - 파일 정보에 대한 값. aggregate로 빼는 방법 

ㄴ 파일 상태 - 파일의 상태 값으로 상수값으로 표현됨. enum 클래스로 만들어서 aggregate로 빼는 방법 

ㄴ 파일 모듈 - root entity의 개념은 아님. 모듈별로 달라지는 부분이기 때문에 aggregate로 빼는 방법 

스토리지 - 외부 시스템과 연동하거나, 파일객체의 값을 들고와서 스토리지 저장용으로 만드는 메서드를 관리한다. rds에선 관리하지 않음 

 

4) 파일의 상태관리는 어떻게 할까?

상태 변화가 단순하기 때문에 Enum으로 관리한다. (상태는 일시적, 영구적)

또한 파일 컨텍스트 <- 모듈 컨텍스트 이기 때문에 어떤 모듈인지에 관한 정보는 상수 enum 

대안) 상태패턴 적용? (상태별 행동이 다르지 않기 때문에 굳이 필요 없을 듯) 

 

5) 협업 과정에서 예외는 어디서 처리할까? 

트랜잭션을 하나로 묶어서

먼저, 파일 객체가 저장이 되고 

연결하여 스토리지 외부 모듈을 연결하는 방식으로 잡자. 예외가 터지면 모든 트랜잭션이 취소하는 로직으로 정한다. 

 

 

[모듈]

1) 모듈이 파일의 저장 책임을 가져야하는가? 

대안1. 모듈+파일저장 코드 추상화가 안되어있다. 모듈별로 메서드를 따더라도 '분기' 로 값을 구분해야한다. 

대안2. 컨텍스트를 분리하자. 모듈은 모듈만 파일저장은 파일 저장의 컨텍스트에서 처리한다. 

 

2) 모듈과 파일의 협력은 어떻게 구성할까? 

흐름1. 반납받은 이미지 url을 함께 모듈의 정보가 저장된다. 

흐름2. 모듈과 파일 은 각각 다른 컨텍스트

흐름3. 모듈을 기준으로 인터페이스와 협력을 맺는다. 

흐름4. 상위 -> 하위 에 접근할 수 있는 의존성 역전. 낮은 결집도

 

3) 파일과 모듈의 책임 분배는 어떻게 할까? 대안은 있을까? 

모듈 - 모듈에 관한 정보만 저장한다. 모듈의 상태를 변경하거나 생성시에 예외를 검증한다. 

파일 - 파일에 대한 정보 (여기선 상태값 변경) 만 담당한다. 

 

4) 협업 과정에서 예외는 어디서 처리할까? 

트랜잭션을 하나로 묶어서

먼저, 모듈 객체가 저장이 되고 파일객체의 상태변경 순으로 잡자. 

파일객체의 상태변경이 취소되면 객체저장까지 실패하게끔 트랜잭션 범위를 정함.

 

 

[최종]

1) 최종적으로 어떤 구조를 선택할까?

파일 - 파일 관리만 (vo/파일메타데이터, enum/상태-모듈,일시영구)

모듈 - 모듈에 대한 정보 관리만

 

스토리지 - 인터페이스 기반 + 구현체 

파일 서비스 - 인터페이스 기반 + 구현체 

 

2) 이 선택이 향후 확장/변경에 어떻게 도움이 될까? 

각 책임이 분리되어 있어서,

여러 모듈들이 같은 파일 콘텍스트를 인터페이스로 받아서만 사용하면 되어서 재사용성이 높아지고 

스토리지 방법이 변경되거나 또는 상태가 추가될 때 변경이 쉽다. 

 

 

 

5. 패턴  적용 

1) 객체생성

빌더 vs 생성자 직접 호출

변수가 많지 않기 때문에 생성자 직접 호출

 

2) 전략 패턴 

context (불변) 과 strategy (변)