Program to an interface, not an implementation.
=> 구현이 아닌 인터페이스에 맞춰 프로그래밍하라.
출처 : GoF(Gang of Four)의 《디자인 패턴 (Design Patterns: Elements of Reusable Object-Oriented Software)》
객체지향 관련 글은 2025년 9월 이후로 작성하지 않고 있었는데
정말 오랜만에 작성한다. (지난 글이 궁금하면 여기 클릭!)
그동안 Netty를 중심으로 학습이 진행되었고, AWS 자격증 취득을 목표로 하다보니 사실 흥미를 느끼지 못했다.
하지만 OOP에 준수한 코드를 작성하고, 이를 Netty에 모듈로 붙여서 작동하는 코드롤 만들 예정이다.
해당 코드의 작업들은 oop-ddd-test-project라는 곳에 생성해놓았다.
하루아침에 할 수 있는 작업은 아니기에 코드를 반영하면서 내용을 정리하려고 한다.
물론! DDD 관련해서 여러 개념이 있고, aggregate이 뭐니 하는게 있겠지만 그런건 다 재껴두고 작업하자.
사실 개념이 그렇게 중요한가? 모든 개념은 경험 후 생성되기 마련이다!
우리가 아는 개념이란 어쩌면 빛좋은 개살구다. 실제로 적용하기에는 혹은 현업에서는 이거 정말 그러한가 하는 의심이 끊임없이 양산된다는 점이다.
AI를 빠르게 사용하는 것도 그래서 위험하다고 생각한다. AI가 정말 빠르고 똑똑하지만 정말 이따금씩 한 번 실수를 한다.
결국 책임은 내가 지기 때문에, 그 나머지 1퍼센트를 위해서 공부를 해야한다 생각한다.
그리고 DDD관련해서 아주 깔쌈한 영상을 찾았는데, 비판적으로 책을 읽으면서 이것을 어떻게 적용할지 함께 고민해주는 영상이 있다. 해당 유튜브를 추천하니 관심이 있는 분들은 다음 영상을 보면 좋다.
https://www.youtube.com/watch?v=SijsYmY9Jmc&t=3627s
목표
1. OOP와 DDD의 차이점에 대해 이해한다.
2. 헥사고널에 대해 이해하고 구조를 파악한다.
3. TDD를 시행하기 전 junit 꿀 기능을 알아본다.
4. 소-중-대형 테스트와 5가지 테스트 대역에 대해 알아본다.
5. 연습을 위한 PRD(Production Required Document)를 정의한다.
1. OOP(Object-Oriented Programming)와 DDD(Domain Driven Design)
사실 두개가 헷갈린다.
기존에는 OOP면 OOP, DDD면 DDD 이렇게 따로 공부를 했는데
실제로 이를 SOLID 원칙에 따르고 추후에 어느 프레임워크에 써도 잘 작동하도록 하려다 보니
유사점이 너무 많기에 이를 확실히 하고 넘어가야겠다는 생각이 들었다.
다시 두개의 정의부터 곱씹어보자.
OOP(객체 지향 프로그래밍)
객체 지향 프로그래밍은 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러 개의 독립된 단위, 즉 '객체들의 모임'으로 파악하고자 하는 것이다.
데이터와 그 데이터를 조작하는 함수(메서드)를 하나의 객체로 묶어서 관리하는 프로그래밍 패러다임이다.
이러한 객체들은 서로 상호작용하며 시스템을 구성한다. 컴퓨터 프로그래밍에서 작게 구분하는 단위는 클래스이고, 이러한 클래스를 활용해 코딩하는 것을 객체 지향형 프로그래밍이라고 한다.
즉, 클래스는 객체를 생성하기 위한 틀(template) 또는 설계도의 역할을 한다.
...
출처: https://ko.wikipedia.org/wiki/객체_지향_프로그래밍
DDD(도메인 주도 설계)
도메인에 따라 소프트웨어를 모델링하는 데 중점을 둔 소프트웨어 설계 접근 방식이다.
도메인 주도 설계는 단일한 모델 대신, 큰 체계를 각각 정해진 맥락에 따라 각자의 모델을 갖는 부분들로 나눈다.
도메인 주도 설계에서 소프트웨어 코드의 구조와 언어(클래스 이름, 클래스 메서드, 클래스 변수)는 비즈니스 도메인과 일치되어야 한다.
예를 들어 소프트웨어가 대출 응용프로그램을 처리해야 한다면 "대출 애플리케이션(loan application)", "고객(customers)"을 클래스 이름으로, "거래 수락(accept offer)"이나 "인출(withdraw)"과 같은 이름을 메서드 이름으로 사용할 것이다.
...
출처:https://ko.wikipedia.org/wiki/도메인_주도_설계
객체지향이란 결국 객체만의 세상을 구축해서 서로가 잘 작동하게끔 한다면,
도메인 주도 설계란 OOP를 통해 설계한 객체를 비즈니스 도메인에 잘 맞춰서 개발한 것이라고 이해한다.
생각해보면 Domain중심적으로 개발을 하다보면 자연스럽게 의존성이 분리되고, 도메인에 역할이 일임되니 확장에는 열려있고 수정에는 닫힐 것이다.
개인적인 주관으로는 객체 지향 프로그래밍은 결국 객체에만 집중했다면, 이를 확장해서 실제 비즈니스 구현까지 고민한 것을 DDD라고 정리하면 되겠다! (반박시 그대말이 맞습니다)
2. 헥사고널 아키텍처란?
이전 글에서 해당 그림을 그렸었다.

이에 대한 설명을 더 진행하기 전에, 다음 그림을 살펴보자.

의존성 역전 원칙은 결국 경계를 만드는것이다.
무엇에 대한 경계를 만드는 것인가?
여기서 말하는 '경계'란, 핵심 비즈니스 로직(고수준 모듈)과 기술적 구현체(저수준 모듈) 사이를 완벽하게 단절시키는 '보호막'이자 '방화벽'을 의미한다.
이를 Port - Adapter패턴으로도 볼 수 있는데, 실행 객체가 Port를 통해서 Adapter를 사용하는 것이다.
여기서 좀 더 자세히 설명하자면 인터페이스를 통해 명령을 입력하는 쪽을 input port - input adapter라고 하고
인터페이스의 명령을 수행해서 출력하는 쪽을 Output port - Output Adapter라고 한다.
이는 사실 우리가 자주 들은, 몇 년 전만 하더라도 유행처럼 번진 헥사고널 아키텍처의 근간을 이룬다.
어떻게?
다시 맨 처음 그림과 비슷하게 그림을 그려봤다. 맨 저음 그림에서 일반적으로 Domain을 추가하고, 구현체 사용을 한다면 다음과 같은 그림이 된다.

이것을 이제 Adaptor 패턴의 형태로 그려보자.

이것을 한 줄로 세워보자.

이를 그 유명한 육각형 구조(헥사고널 아키텍처) 로 그려볼까?

그렇다. 결국에는 DDD에서 한 단계 더 발전하여 우리가 아는 헥사고널 아키텍처가 되었다!
Interface를 적극적으로 활용하여 코드간의 경계를 생성해서 최대한 큰 틀에서 클린한 코드를 작성할 수 있게 구성했다.
하지만 정말 그것이 SOLID한, 우리가 원하는 OOP한 코드를 작성한 수 있는거에 대한 확답은 주지 못한다.
미시적인 코드레벨에서는 결국 이를 이 틀에 맞춰서 직접 코딩을 해야하는건 사람이기 때문이다.
그러면 헥사고널 아키텍처를 도입함으로 얻을 수 있는 장점에 대해 Gemini 에게 정리를 부탁했다.
궁금하면 다음을 열어보면 된다.
나:
그러면 헥사고널 아키텍처를 도입함으로 얻을 수 잇는 장점이 뭐니?
tistory에 붙여넣을 수 있게 간단하게 정리해줄래?
Gemini:
티스토리 기술 블로그에 바로 활용하실 수 있도록, 서론과 결론을 포함하여 마크다운(Markdown) 포맷으로 깔끔하게 정리해 드립니다.
헥사고날 아키텍처(포트 앤 어댑터) 도입의 핵심 장점
헥사고날 아키텍처는 의존성 역전 원칙(DIP)을 시스템 전체로 확장하여, '비즈니스 로직(도메인)'과 '기술적 구현(인프라)' 사이에 완벽한 방화벽을 치는 아키텍처입니다. 이를 통해 시스템은 다음과 같은 구조적 이점을 얻게 됩니다.
1. 핵심 비즈니스 로직(도메인)의 철저한 보호
외부 기술로부터의 격리:
도메인 계층 내부에는 @Entity, @RestController, HttpServletRequest 같은 프레임워크나 외부 라이브러리 의존성이 전혀 존재하지 않게 됩니다.
비즈니스 자산의 영속성: 데이터베이스가 RDBMS에서 Redis로 변경되거나, 통신 방식이 HTTP에서 gRPC로 바뀌더라도 중심부에 있는 핵심 비즈니스 로직은 단 한 줄도 수정할 필요가 없습니다.
2. 압도적인 테스트 용이성 (TDD 최적화)
프레임워크 독립적인 순수 테스트:
핵심 로직을 검증하기 위해 무거운 DB를 띄우거나 Spring Boot 컨텍스트를 실행할 필요가 없습니다.
Test Double의 적극적 활용: 외부 인프라와 통신하는 아웃바운드 포트(인터페이스)에 가짜 객체(Fake/Mock Adapter)를 주입하여, 수백 개의 단위 테스트를 수 초 만에 실행할 수 있는 독립적인 테스트 환경이 구축됩니다.
3. 유연한 확장과 결합도 최소화
플러그인 아키텍처: 포트를 통해 들어오고 나가는 모든 외부 요소(UI, DB, 외부 API, 메시지 큐 등)는 '어댑터'라는 플러그인 형태로 동작합니다. 언제든 뺐다 꽂을 수 있습니다.
점진적 마이그레이션 및 확장: 레거시 시스템을 개편하거나 대용량 처리를 위해 인프라를 분리할 때, 도메인 코드의 타격 없이 새로운 어댑터를 추가하여 유연하게 대응할 수 있습니다.
4. 의도와 책임이 명확해지는 코드 구조
Use Case 중심 설계: 시스템이 제공하는 기능이 무엇인지(What)가 인바운드 포트를 통해 명확하게 드러나며, 비즈니스 흐름을 파악하기 쉽습니다.
개발 병렬화: 포트(인터페이스) 규격만 사전에 합의하면, 도메인 핵심 로직을 개발하는 인원과 데이터베이스 쿼리나 웹 컨트롤러를 작성하는 인원이 서로를 기다리지 않고 동시에 작업을 진행할 수 있습니다.
결론
헥사고날 아키텍처는 인터페이스와 매핑 클래스를 다수 생성해야 하는 초기 설계 비용(Boilerplate)이 존재합니다. 하지만 시스템의 트래픽이 거대해지고 복잡도가 증가하는 환경에서, **'소프트웨어의 부패를 막고 기술 부채를 통제하는 가장 확실한 구조적 방어막'**을 제공합니다.
정말 그런것 같은가? 사실 이것은 직접 해가면서 느껴야 하는 것이지, 외워서 체감할 수 있는 장점은 아니긴 하다.
그러면 정말 코드를 작성해보도록 하겠다. 그 전에! PRD에 대해 정의하고 가겠다.
3. 편리한 TDD를 위한 꿀기능 정리
TDD를 실행하면서 정말 감명(?) 깊게 사용한 기능이 있다. 이에 대해 좀 간단히 정리하려고 한다.
바로 예시를 통해서 보자.
@ParameterizedTest
@ValueSource(ints = {-1, -10})
@DisplayName("초기 생성 수량이 음수이면 예외가 발생한다.")
void createInitial_Fail_NegativeCount(int invalidCount) {
assertThatThrownBy(() -> Inventory.createInitial(invalidCount))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("초기 생성 수량은 0보다 작을 수 없습니다.");
}
@DisplayName

테스트를 실행한 후 이 메소드가 무엇을 했는지 설명해준다.
@ParameterizedTest 와 @ValueSource
이름 그대로 parameter를 받는데, 그 파라미터를 @ValueSource를 통해서 어떻게 넘겨줄건지 설정한다.
위의 코드에서 보면
ints = {-1, -10}
다음과 같이 설정이 되어 있는데, -1, -10을 method 의 매개변수로 받아서 이 갯수만큼 테스트를 실행하겠다는 것이다.

AssertAll
이것은 Annotation 은 아니고 코드이다
IssuedCoupon 의 쿠폰 발급 테스트를 위해서 다음과 같은 코드를 작성했는데, Assertions.assertAll ~ 코드 부분을 확인할 수 있다.
이렇게 되면
@Test
@DisplayName("쿠폰 발급 정책에 따라 IssuedCoupon 엔티티가 정상적으로 조립되어 반환된다.")
void 쿠폰을_멤버에게_발급하면_성공() {
// Given (테스트를 위한 환경 구성)
CouponCreate couponCreate = CouponCreate.builder()
.description("테스트 생성입니다.")
.totalCount(1000)
.originalPrice(Money.of(10000))
.expireDate(LocalDate.of(2027, 12, 31))
.build();
long currentTime = timeGenerator.millis();
Coupon coupon = Coupon.from(couponCreate, currentTime);
MembersCreate membersCreate = MembersCreate.builder()
.name("이름")
.address("주소")
.age(11)
.build();
Members members = Members.create(membersCreate, timeGenerator.millis());
// When (쿠폰 발급 도메인 서비스 실행)
IssuedCoupon issuedCoupon = couponIssueDomainService.issueToMember(coupon, members.getId());
// Then (조립된 도메인 객체의 상태 및 정책 검증)
Assertions.assertAll("IssuedCoupon 조립 검증",
() -> Assertions.assertNotNull(issuedCoupon, "발급된 쿠폰 객체는 null이 아니어야 합니다."),
// 1. 참조 데이터 매핑 검증
() -> Assertions.assertEquals(coupon.getId(), issuedCoupon.getCouponId(), "원본 쿠폰의 ID가 일치해야 합니다."),
() -> Assertions.assertEquals(members.getId(), issuedCoupon.getMemberId(), "발급 대상 멤버의 ID가 일치해야 합니다."),
() -> Assertions.assertEquals(coupon.getOriginalPrice(), issuedCoupon.getAppliedPrice(), "적용된 가격은 원본 쿠폰의 가격과 일치해야 합니다."),
// 2. 외부 의존성(Generator) 반환값 조립 검증
() -> Assertions.assertEquals("123123", issuedCoupon.getCouponNumber(), "쿠폰 번호는 FakeNumberGenerator 정책을 따라야 합니다."),
() -> Assertions.assertEquals(1000L, issuedCoupon.getIssuedAt(), "발급 시간은 FakeTimeGenerator 정책을 따라야 합니다."),
// 3. 도메인 초기 상태 정책 검증
() -> Assertions.assertEquals(IssuedCouponStatus.UNUSED, issuedCoupon.getStatus(), "발급 직후의 쿠폰 상태는 UNUSED여야 합니다.")
);
}
가장 큰 장점은 중간에 검증이 실패해도 테스트를 중단하지 않고,
끝까지 모든 항목을 검사한 뒤 '종합 에러 리포트'를 한 번에 보여준다는 점이다.
4. 소-중-대형 테스트와 테스트를 위한 5가지 테스트 대역 알아보기
필자는 학습용 코드를 작성하는 단계를 소형 테스트 코드 단계까지 할 것이다.
소,중,대형 테스트가 뭔지 모르겠다고? 필자도 몰랐고 관심도 없었다.
이걸 찾아보고 해본 결과 사실 이 부분은 Gemini에게 그냥 요청해서 보는것이 낫다고 생각했다.
4.1) 소형 테스트 (Small Test)
가장 작고, 빠르며, 독립적인 테스트입니다. 우리가 흔히 말하는 엄격한 의미의 단위 테스트(Unit Test)에 해당합니다.
- 제약 조건 (금지 사항): * 네트워크 통신 절대 불가
- 데이터베이스 접근 절대 불가
- 파일 시스템 접근(I/O) 절대 불가
- sleep() 등 의도적인 지연 불가
- 실행 환경: 단일 프로세스 및 스레드 내부
- 실행 속도: 매우 빠름 (수천 개를 실행해도 수 초 내에 완료)
- 목적 및 특징:
- 순수하게 코드가 가진 비즈니스 로직(도메인 규칙)과 알고리즘을 검증합니다.
- 외부 의존성이 필요한 경우 철저하게 **테스트 대역(Test Double - Mock, Fake, Stub 등)**으로 격리하여 테스트의 결정성(항상 같은 결과를 보장)을 유지해야 합니다.
4.2) 중형 테스트 (Medium Test)
두 개 이상의 모듈이 결합되어 정상적으로 상호작용하는지 확인하는 통합 테스트(Integration Test)에 해당합니다.
- 허용 사항:
- 단일 기기(Localhost) 내에서의 자원 사용 허용
- 로컬 데이터베이스(H2, 인메모리 DB 등) 연동 허용
- 프레임워크 컨텍스트(Spring Boot 로딩) 및 로컬 파일 시스템 사용 허용
- 제약 조건: 외부망 통신(Third-party API 등)은 여전히 금지됩니다.
- 실행 속도: 보통 (DB 커넥션이나 스프링 컨텍스트 로딩으로 인해 수 초 ~ 수십 초 소요)
- 목적 및 특징:
- 애플리케이션 서비스 계층과 데이터베이스 간의 영속성(JPA 연동, 쿼리 동작) 검증에 주로 사용됩니다.
- 웹 컨트롤러(API)의 요청/응답 스펙이 잘 맞는지 프레임워크 위에서 검증합니다.
4.3) 대형 테스트 (Large Test)
실제 사용자가 시스템을 사용하는 흐름과 동일한 환경을 구성하여 전체 파이프라인을 검증하는 엔드투엔드(E2E) 테스트에 해당합니다.
- 허용 사항: 시스템을 구성하는 모든 인프라 자원 사용 허용
- 실행 환경: 다중 서버 환경 (실제 데이터베이스, 외부 API, 메시지 큐 등 운영 환경과 가장 유사하게 구성)
- 실행 속도: 매우 느림 (네트워크 레이턴시 및 인프라 통신 대기 시간으로 인해 수 분 ~ 수십 분 소요)
- 목적 및 특징:
- 브라우저나 클라이언트 앱에서 시작하여 백엔드를 거쳐 데이터베이스에 값이 쓰이고 응답이 돌아오는 전체 사용자 시나리오를 검증합니다.
- 가장 넓은 범위를 커버하지만, 실패 시 정확히 어느 지점에서 문제가 발생했는지 파악(디버깅)하기가 가장 까다롭습니다.
필자의 경우에는 domain 단위에서 로직이 잘 작동하는지 확인하는 입장이라서 중, 대형은 가뿐하게 넘기도록 했다.
4.4) 단위 테스트와 소형 테스트의 차이
그런데 필자는 소형 테스트와 단위 테스트가 헷갈렸다. 두 개는 무엇이 다른가?
단위 테스트 : "무엇(What)을 테스트하는가?" (목적 중심)
- 가장 작은 코드 단위(주로 1개의 클래스나 메서드)인지에 초점
- 내가 짠 코드의 특정 유닛(단위)이 의도대로 동작하는지 확인하는 데 목적
- 이 관점에서는 DB를 연결했더라도 클래스 하나만 떼어서 검증했다면 단위 테스트라고 부르는 경향 있음
소형 테스트 : "어떻게(How) 동작하는가?" (제약 중심)
구글(Google)은 모호한 '단위'라는 말을 버리고, 물리적인 자원과 속도를 기준으로 테스트 크기를 소/중/대로 엄격하게 재정의함
- 네트워크 통신 절대 불가
- 데이터베이스 접근 절대 불가
- 파일 시스템 접근(I/O) 절대 불가
구글에서는 기존의 단위 테스트의 모호한 의미에서 벗어나도 좀 더 자세한 의미로 재정의 하였고, 필자도 구글에서 말하는 소형 테스트를 사용할 예정이다.
4.5) 5가지 테스트 대역(Test Double) 정리
DB도, 외부 API도 없이 어떻게 비즈니스 로직을 검증할 수 있도록 고안된 테스트 대역이 5개가 있다.
외부 의존성을 대체하기 위해 투입되는 가짜 객체들을 테스트 대역(Test Double)이라고 부르는데,
영화 촬영 시 위험한 장면을 대신하는 '스턴트맨'과 같은 역할입니다.
제라드 메스자로스(Gerard Meszaros)가 분류한 5가지 패턴은 다음과 같다.
① Dummy (더미)
- 원리: 인스턴스화된 객체가 필요하긴 하지만, 테스트하는 비즈니스 로직에는 전혀 영향을 주지 않는 '빈 껍데기' 객체입니다.
- 구조적 특징: 객체 내부의 메서드가 호출될 일이 없습니다. 주로 파라미터 규격을 맞추어 컴파일 에러를 피하기 위한 용도로만 전달됩니다. (예: new Address(null, null))
더미 테스트 예시
@Test
void issueCoupon_Dummy_Example() {
// 1. Dummy 생성: 쿠폰 발급 로직에서는 회원의 '주소'나 '등급' 정보가 전혀 필요하지 않음
Address dummyAddress = new Address(null, null, null);
// 파라미터 규격을 맞추기 위해 더미를 끼워 넣음
Member member = new Member(1L, "홍길동", dummyAddress);
Coupon coupon = CouponFixture.createDefaultCoupon();
// 2. 실행
IssuedCoupon issuedCoupon = couponIssueDomainService.issueToMember(coupon, member.getId());
// 3. 검증: 더미 객체의 상태는 검증하지 않음
assertThat(issuedCoupon.getMemberId()).isEqualTo(1L);
}
② Stub (스텁)
- 원리: 테스트에서 호출될 요청에 대해 **미리 준비된 결과값(상태)**을 무조건 반환하도록 세팅된 객체입니다.
- 구조적 특징: 실제 내부 로직을 수행하지 않고 "A를 부르면 B를 반환해라"라고 하드코딩되어 있습니다. DB 조회 결과를 모방할 때 주로 사용하며, 상태 검증(State Verification)의 핵심 도구입니다.
스텁 테스트 예시
@Test
void findMember_Stub_Example() {
// 1. Stubbing (상태 고정)
MemberRepository stubRepository = mock(MemberRepository.class);
// findById(1L)이 호출되면, 무조건 미리 만들어둔 회원 객체를 반환하라고 하드코딩
given(stubRepository.findById(1L))
.willReturn(Optional.of(new Member(1L, "홍길동")));
// 2. 로직 실행 (Service 계층 등)
Member foundMember = memberService.getMember(1L, stubRepository);
// 3. 상태 검증 (State Verification)
assertThat(foundMember.getName()).isEqualTo("홍길동");
}
③ Spy (스파이)
- 원리: 실제 객체처럼 동작하거나 Stub의 역할을 하면서, 동시에 자신에게 어떤 메서드가 몇 번 호출되었고 어떤 파라미터가 전달되었는지 내부적으로 기록(Record)하는 객체입니다.
- 구조적 특징: 호출된 내역을 몰래 숨겨두었다가, 테스트의 마지막 검증(Then) 단계에서 정보를 제공합니다. (예: 메일 발송 서비스의 send() 메서드가 정확히 1번 호출되었는지 카운트)
스파이 테스트 예시
// 테스트 디렉토리에 생성한 가짜 스파이 객체
public class SpyNotificationService implements NotificationService {
// 호출 횟수와 파라미터를 기록할 변수 (스파이의 핵심)
public int callCount = 0;
public String lastMessage = "";
@Override
public void send(Long memberId, String message) {
this.callCount++;
this.lastMessage = message;
}
}
@Test
void issueCoupon_Spy_Example() {
// 1. Spy 객체 주입
SpyNotificationService spyService = new SpyNotificationService();
CouponIssueApplicationService appService = new CouponIssueApplicationService(spyService);
// 2. 쿠폰 발급 실행 (내부에서 알림 발송 로직이 돌아감)
appService.issue(1L, 100L);
// 3. 행위 검증 (Behavior Verification): 스파이가 기록해둔 내부 장부를 열어봄
assertThat(spyService.callCount).isEqualTo(1);
assertThat(spyService.lastMessage).contains("발급 완료");
}
④ Mock (목)
- 원리: 호출에 대한 '기대(Expectations)'를 사전에 명세하고, 그 기대한 대로 상호작용이 일어났는지 행위 검증(Behavior Verification)을 하기 위해 만들어진 객체입니다.
- 구조적 특징: 다른 대역들이 주로 반환되는 '값(상태)'을 검증하는 데 반해, Mock은 특정 파라미터와 함께 의도한 프로세스가 정확히 실행되었는지를 검증합니다. (현대 테스트 프레임워크인 Mockito는 Stub과 Mock의 기능을 혼용해서 제공합니다.)
목 테스트 예시
@Test
void payment_Mock_Example() {
// 1. Mock 객체 생성
ExternalPaymentGateway mockGateway = mock(ExternalPaymentGateway.class);
PaymentService paymentService = new PaymentService(mockGateway);
// 2. 실행
paymentService.payForCoupon(1L, 10000);
// 3. 행위 검증 (Behavior Verification)
// mockGateway의 pay() 메서드가 10000이라는 숫자와 함께 '정확히 1번(times(1))' 호출되었는가?
verify(mockGateway, times(1)).pay(10000);
}
⑤ Fake (페이크)
- 원리: 실제 비즈니스 로직이 완벽하게 구현되어 있지만, 외부 I/O나 프레임워크를 타지 않아 운영 환경에서는 사용할 수 없는 단순화된 형태의 진짜 객체입니다.
- 구조적 특징: 매우 빠르면서도 실제 도메인 로직과 동일하게 동작합니다. 인메모리 컬렉션(Map, List)을 활용하여 DB 기능을 완벽히 흉내 내는 가짜 리포지토리가 대표적입니다. 순수 자바로만 구성되므로 도메인 주도 설계(DDD)에서 가장 강력하게 권장되는 패턴입니다.
페이크 테스트 예
// 프레임워크에 의존하지 않는 순수 자바 Fake Repository
public class FakeIssuedCouponRepository implements IssuedCouponRepository {
// DB의 역할을 대신할 HashMap
private final Map<Long, IssuedCoupon> store = new ConcurrentHashMap<>();
private long sequence = 0L;
@Override
public IssuedCoupon save(IssuedCoupon issuedCoupon) {
long id = ++sequence;
// 빌더나 리플렉션을 통해 ID를 할당하고 저장하는 로직
store.put(id, issuedCoupon);
return issuedCoupon;
}
@Override
public Optional<IssuedCoupon> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
}
@Test
void issueCoupon_Fake_Example() {
// DB 띄울 필요 없이 Fake 레포지토리 주입하여 0.01초 만에 테스트 완료
FakeIssuedCouponRepository fakeRepository = new FakeIssuedCouponRepository();
// ... 실행 및 검증
}
필자는 사실 코드를 작성하면서 Fake밖에 사용할 일이 없었다.
그걸로 충분한 초창기 레벨이어서 그랬는데,
이에 대해 어느정도 알아는 놓고 나중에 코드를 작성해 나갈 때 물어가면서 하면 될 듯 하다.
이걸 외우는게 중요한가? 물론... 어느정도는 안다면 기계처럼 외우는건 별로.. 쓰다보면 기억하겠지 뭐!
5. PRD(Product Requirements Document) 정의 - 상황부여
사실 뭐 학문적으로 어쩌니 저쩌니 고민하는것보다는 실제로 상황 부여를 통해 좀 더 복잡한 예시를 통해 내가 체득하는것이 가장 빠를 것이다.
이를 위해서 Gemini에게 상황 부여를 위한 시나리오를 추천해주고 PRD를 작성해달라고 했다.
해당 내용은 github의 md 파일로 작성해놨으니 관심이 있으면 여기를 보길 바란다. (여기 클릭!)
굳이 이렇게까지 자세하게 작성해야 하는 생각이 좀 들 수 있다.
필자가 맛보고(?) 싶은 것은 이러한 확장을 진행할 수 있을 만큼 객체가 제 기능을 잘 하게끔 어떻게 해야할까 하는 점이다.
당장 PRD 에서 현재 단계에서 고민해야 할 부분은 보편적 언어와 기능적 요구사항을 잘 받아들인 코드를 작성하는 것이다.
누구든 그렇겠지만 사실 필자는 흥미가 생기지 않으면 잘 하지 않는 스타일이다.
그동안은 맨날 해야지 뭐 해야지 어쩌구 했는데, 실제로 하려면 이를 실행할 실행력이 과거에는 많지가 않았고
최근에 들어서야 적극적으로 직접 학습하고 적용하기 시작했다.
현 시점에서 후회를 하는 정도는 아니지만 정말 아쉬움이 남는 점이 이 점이었다.
직접 만들어가며 고민하고, 최근에는 최고의 선생님과 늘 함께하고 있으니 학습이 놀랄만치 재미있다! (Gemini 선생님!)
우리는 정말 꿈만같은 세상에 살고 있다. 걱정없이 원하는 것을 선택하고 할 수 있는 현 상황이 어쩌면 기적이 아닐까 생각도 한다.
객체지향은 객체로 새로운 세상을 만들어가는 과정이다.
작은 글들이 모여 내 학습들이 모여 하나의 세상이 되는것 같다.
덕분에 이 지식들은 죽은 지식이 아닌 내가 활용할 수 있는 지식이 되가는것 같고, 앞으로도 그러길 바란다.
다음 시간에는! 프로젝트를 구성하고 코드를 어떻게 작성했고, 어느 부분이 문제였고 어떻게 하는게 좋다고 판단했는지 적도록 하겠다.
'programming language > Java' 카테고리의 다른 글
| [Java] 객체지향 연습 6 - 프로젝트 구성 및 기본 코드 학습 그리고 데코레이터, 책임연쇄 패턴 복기 (feat. 템플릿 메서드 패턴) (2) | 2026.03.04 |
|---|---|
| [Java] 객체지향 연습 4 - 서비스에 대한 이해와 Layered Architecture 적용(feat. Spring 공식문서) (0) | 2025.10.04 |
| [Java] 객체지향 연습 3 - SOLID 원칙과 디자인패턴 (2) | 2025.09.30 |
| [Java] 객체지향 연습 2 - 행위로 코드 설계하기 (0) | 2025.09.22 |
| [Java] 객체지향 연습 1 - 상황 부여와 객체지향 예시 (0) | 2025.09.21 |