Spring Boot 환경에서 설명하는 TDD 관련 책을 읽어 본다면 고전파와 런던파에 대한 설명이 자주 언급되어 관련 내용을 간단히 정리하고자 한다.


요약

구분 고전파 런던파
테스트 방식 실제 객체 많이 사용 의존 객체를 Mock으로 대체
검증 방식 상태(State) 검증 행위(Interaction) 검증
철학 결과 중심 협력 관계 중심

SpringBoot + Mock 환경에서 런던파 장점

1. 테스트 속도가 빠름 

  • DB 안 붙임
  • 외부 API 호출 안 함
  • 실제 Bean 전체 로딩 안 함
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
    @Mock OrderRepository orderRepository;
    @InjectMocks OrderService orderService;
}

→ @SpringBootTest 안 써도 됨
→ CI(= Continuous Integration (지속적 통합)) 에서 훨씬 빠름


2. 단위 테스트가 진짜 “단위”가 됨

고전파는 종종:

  • Service + Repository + DB 같이 묶임

런던파는:

  • Service만 테스트
  • Repository는 Mock

의존성 끊고 진짜 로직만 검증 가능


3. 협력 관계 설계가 좋아짐 🧠

런던파는 이런 걸 검증함:

verify(orderRepository).save(any());

즉:

  • 이 객체가
  • 이 의존성을
  • 정확히 이렇게 호출했는가?

→ 자연스럽게 의존성 역전, 인터페이스 기반 설계가 강제됨

 

결과적으로:

  • 결합도 낮아짐
  • 테스트 가능한 구조가 됨
  • SOLID 지키게 됨

4. 에러 상황 테스트가 쉬움

예:

when(paymentClient.pay(any()))
    .thenThrow(new RuntimeException());

고전파에서는:

  • 실제 API 실패 상황 만들기 어려움

런던파에서는:

  • 예외 상황 쉽게 재현 가능

→ 예외 처리 테스트가 매우 쉬움


5. TDD에 더 잘 맞음

런던파는:

  1. 인터페이스 정의
  2. Mock 기반으로 테스트 먼저 작성
  3. 구현

→ TDD 스타일에 잘 맞음


SpringBoot 환경에서 특히 좋은 경우

  • 외부 API 연동 많음 (결제, 문자, 인증 등)
  • DB 의존 로직 많음
  • 마이크로서비스 구조
  • 도메인 로직 복잡함
  • 빠른 CI/CD 필요

단점도 있음

1. 테스트가 구현에 의존함

verify(repository).save()

→ 내부 구현 바꾸면 테스트 깨짐

(고전파는 결과만 맞으면 통과)


2. 리팩토링 저항성 낮음

행위 검증 위주라
→ 메서드 호출 순서 바꾸면 깨짐


3. 과도한 Mock은 오히려 가짜 안정감

실제 통합 문제는 못 잡음
그래서 통합 테스트는 별도로 필요


결론 (SpringBoot 기준)

실무에서 가장 좋은 구조는

단위 테스트 = 런던파 (Mock 기반)
통합 테스트 = 고전파 (실제 Bean + DB)

 

이렇게 혼합하는 것


한 줄 요약

SpringBoot + Mock 환경에서 런던파 장점은:

 

- 빠름
- 진짜 단위 테스트 가능
- 설계 품질 향상
- 예외 테스트 쉬움
- TDD 친화적

반응형

'SpringBoot > TDD' 카테고리의 다른 글

TestCode Bean 객체 주입에 관련 하여  (0) 2025.02.11

Nested Loop Join(NL Join, 중첩 루프 조인)

 

동작 원리 : 외부 테이블(Outer)의 각 행마다 내부 테이블(Inner)를 순회해서 매칭되는 행을 찾는 방식

즉, 이중 for문 구조처럼 작동한다.

 

-- 예시 
SELECT *
FROM employees e
JOIN departments d ON e.dept_id = d.id;

-- 실제 실행 개념
for each row in employees
  for each row in departments
    if (e.dept_id == d.id)
      return row

 

항목 내용

사용 조건 조인 키에 인덱스가 존재할 때 매우 효율적
시간 복잡도 O(N * M) (단, 인덱스 없을 시 급격히 느려짐)
장점 즉시 결과 반환 가능, 인덱스 기반 랜덤 액세스
단점 내부 테이블 크거나 인덱스 없으면 성능 급락

 

실무 예시

1. 한 쪽 테이블이 작고, 다른 쪽에 인덱스가 잘 잡혀 있을 때

2. 예: user_id로 주문 내역 조회(주문 테이블에 인덱스 있음) 

 

Hash Join(해시 조인)

 

동작 원리: 조인키를 기준으로 작은 테이블을 해시 테이블로 만들고 큰 테이블을 스캔하여 해시 매칭으로 조인하는 방식

 

-- 예시
SELECT *
FROM orders o
JOIN customers c ON o.customer_id = c.id;

-- 실행 개념
Build phase: customers → 해시 테이블 생성
Probe phase: orders → 해시 테이블에서 customer_id 일치 여부 검색

 

항목 내용

사용 조건 조인 컬럼에 인덱스 없어도 가능
장점 대용량 테이블 간 조인에도 효율적
단점 해시 테이블 생성 시 메모리 사용량 많음
시간 복잡도 O(N + M)

 

실무 예시

1. 대용량 조인, 인덱스 없는 컬럼 조인

2. 예: 월별 통계 계산, 로그 + 사용자 정보 병합

 

Sort-Merge Join(SM Join, 정렬 병합 조인)

 

동작 원리: 양쪽 테이블을 조인 키 기준으로 정렬한뒤, 정렬된 상태에서 한 번에 병합 하는 방식

 

-- 예시
SELECT *
FROM sales s
JOIN region r ON s.region_id = r.id;

-- 실행 개념
1. sales, region 각각 조인 키 기준 정렬
2. 정렬된 두 집합을 병합하면서 일치하는 행 연결

 

항목 내용

사용 조건 두 테이블 모두 정렬된 상태일 때 가장 효율적
장점 정렬된 대량 데이터 처리에 적합
단점 정렬 과정 비용 큼, 인덱스 없으면 임시 정렬 필요
시간 복잡도 O(N log N + M log M)

 

실무 예시

1. 대용량 정렬 데이터 조인

2. 예: 날짜, 타임스탬프 기반 로그 조인

 

비교 요약표

조인 방식 원리 인덱스 필요 장점 단점 적합한 상황
Nested Loop Join 외부→내부 반복 검색 ✅ 필요 인덱스 있을 때 빠름 인덱스 없으면 느림 작은 테이블 + 인덱스 존재
Hash Join 해시 테이블 빌드 후 매칭 ❌ 불필요 대용량 처리 빠름 메모리 많이 사용 대용량 + 인덱스 없음
Sort-Merge Join 양쪽 정렬 후 병합 ❌ 불필요 정렬 데이터 조인 효율 정렬 비용 큼 이미 정렬된 데이터

 

정리 한 줄 요약

 

인덱스 있으면 -> Nested Loop Join

인덱스 없어도 대용량이면 -> Hash Join

둘 다 정렬되 있으면 -> Sort-Merge Join

반응형

Spring MVC에서 응답을 쓰는 방식이 크게 두가지가 있다.

1. response.getWriter() - 문자 기반 응답

2. response.getOutputStream() - 바이트 기반 응답

 

한 번 getWriter()를 호출하면 내부적으로 OutputStream이 이미 사용된 것으로 간주되며, 그 이후에는 getOutputStream()을 사용할 수 없습니다, 반대도 마찬가지입니다.

 

GenericFilterBean만 상속받고 있다면, 다음과 같은 흐름에서는 여러 번 필터가 실행될 수 있습니다.

  • 예외 처리 중 /error로 forward
  • Servlet Dispatcher가 다시 요청 처리

이와 반대로 

OncePerRequestFilter가 요청당 한 번만 필터를 실행하도록 자동으로 보장해 주는데, 내부에 그런 "중복 실행 방지 로직"이 구현되어 있기 때문이다.

 

때문에 OncePerRequestFilter는 이런 경우에 유용하다. 

  • 예외 처리로 인해 /error로 내부 forward 되는 경우
  • DispatcherType.FORWARD, DispatcherType.ERROR로 필터 체인이 재실행되는 경우
  • 보안 필터(JWT, 인증, IP 제한 등)처럼 요청당 한 번만 실행돼야 할 때

-> 참고로 OncePerRequestFilter는 GenericFilterBean을 상속받고 있다.

 

OncePerRequestFilter에는 ShouldNotFilter라는 메소드가 존재하며 이를통해 filter를 안타게끔 패스 지정을 할 수 있다.(REGEX 지정 가능)

반응형

'SpringBoot' 카테고리의 다른 글

Lombok getter  (0) 2024.10.07

기본값은 true이며, 이 경우 스프링이 @Configuration 클래스를 CGLIB 프록시로 감싸서 메서드 호출을 가로채고, 항상 싱글톤 빈을 반환합니다.

하지만 false로 설정하면 프록시를 사용하지 않으며, 각 @Bean 메서드가 직접 호출되어 빈을 생성합니다. 다음과 같이 예제를 통해 상세 내용을 갈음하고자 한다.

// helloService()와 worldService()는 서로 의존하지 않기 때문에 proxyBeanMethods = false로 해도 문제 없습니다.
// 이럴 땐 성능 면에서 약간 이득을 볼 수 있습니다. (프록시 미사용)
@Configuration(proxyBeanMethods = false)
public class SimpleConfiguration {

    @Bean
    public HelloService helloService() {
        return new HelloService();
    }

    @Bean
    public WorldService worldService() {
        return new WorldService();
    }
}

// proxyBeanMethods = true 인 경우
// proxyBeanMethods = true라서 repository()가 여러 번 호출되어도 동일한 인스턴스를 리턴함
// Spring이 내부적으로 프록시를 사용해서 해당 메서드 호출을 컨테이너 호출로 변환
@Configuration
public class ServiceConfiguration {

    @Bean
    public Repository repository() {
        return new Repository();
    }

    @Bean
    public Service service() {
        return new Service(repository()); // 프록시로 가로채서 싱글톤 보장됨
    }
}

// 아래와 같이 하면 repository()가 직접 호출되어 다른 인스턴스가 생성됩니다.
// Service는 실제로 스프링 빈이 아닌, 새롭게 만들어진 Repository를 참조합니다.
@Configuration(proxyBeanMethods = false)
public class WrongConfiguration {

    @Bean
    public Repository repository() {
        return new Repository();
    }

    @Bean
    public Service service() {
        return new Service(repository()); // 직접 호출되므로 새 인스턴스!
    }
}

// 이 방식은 가장 안정적이고, 성능까지 고려한 현대적인 스프링 방식입니다.
// 생성자 주입이나 메서드 인자 주입을 적극적으로 활용하세요.
@Configuration(proxyBeanMethods = false)
public class SafeConfiguration {

    @Bean
    public Repository repository() {
        return new Repository();
    }

    @Bean
    public Service service(Repository repository) {
        return new Service(repository); // 스프링이 주입하므로 안전
    }
}
반응형

 

@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@ExtendWith(MockitoExtension.class)
@SpringBootTest
public class SampleServiceTest {

    @Autowired
    private MasterService masterService;

    @Mock
    private SlaveService slaveService;

    @Test
    void test() throws IOException {
        when(slaveService.checkPrint(any())).thenReturn(null);
        masterService.path(null);
    }
}

 

@SpringBootTest는 스프링 컨텍스트를 로드하면서 실제 빈을 주입한다. 때문에 SlaveService는 @Mock으로 선언되어있지만 실제 MasterService는 스프링컨테스트에서 실제빈을 주입받아 처리하기에 SlaveService의 Mock객체가 MasterService에 주입되지 않고, 실제 빈이 사용된다.

 

해결 방법으로는 크게 두가지 이다.

 

1. @InjectMocks 사용

@InjectMocks를 사용하면 @Mock으로 사용한 선언된 객체들이 MasterService에 자동으로 주입된다. 

 

@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@ExtendWith(MockitoExtension.class)
@SpringBootTest
public class SampleServiceTest {

    @InjectMocks
    private MasterService masterService;	// mock 객체가 주입됨

    @Mock
    private SlaveService slaveService;

    @Test
    void test() throws IOException {
        when(slaveService.checkPrint(any())).thenReturn(null);
        masterService.path(null);
    }
}

 

2. @MockBean 사용

@MockBean을 사용하면 SpringBootTest가 실행될 때 slaveService의 실제 빈 대신 Mock객체가 주입된다.

@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@ExtendWith(MockitoExtension.class)
@SpringBootTest
public class SampleServiceTest {

    @Autowired
    private MasterService masterService;

    @MockBean
    private SlaveService slaveService;

    @Test
    void test() throws IOException {
        when(slaveService.checkPrint(any())).thenReturn(null);
        masterService.path(null);
    }
}

 

차이점 요약

 

주입방식 MasterService 객체 SlaveService 객체
@InjectMocks + @Mock 실제 객체 (테스트용으로 생성) Mock 객체
@Autowired + @MockBean 스프링 빈 (실제 빈) Mock 객체 (스프링 컨텍스트 내에서 등록됨)

 

따라서,

  • 단위 테스트를 할 때는 @InjectMocks를 사용하여 빠르게 테스트
  • 스프링 컨텍스트와 함께 통합 테스트를 할 때는 @MockBean을 사용하여 주입
반응형

'SpringBoot > TDD' 카테고리의 다른 글

고전파 vs 런던파  (0) 2026.02.12

테이블을 설계하다 보면 자기 자신을 참조하는 경우가 있다. 이를 JPA로 나타내자면 아래와 같다.

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
public class Category {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id")
    private Category parent;

    @Builder.Default
    @OneToMany(mappedBy = "parent")
    private Set<Category> children = new HashSet<>();
}

 

 

이와 다른 형태로는 아래와 같은데 이와 다르게 자식 테이블이 별도로 생성되어 이 경우 불필요한 join query가 불필요하게 발생되어 아래와 같은 방식 보단 첫번째 방식으로 계층형 테이블을 설계해야 한다

 

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
public class Category {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany
    private Set<Category> children = new HashSet<>();
}

 

반응형

SQL과 객체지향언어가 갖는 큰 차이점 중 하나가 바로 상속의 유무이다.

객체 지향언어는 상속을 지원하지만, 데이터베이스는 상속을 지원하지 않는다. 

 

스프링에서는 JPA를 이용하여 ORM 기술을 사용하는데, 클래스 간 상속 관계에 대해서는 어떻게 정의를 하는지 알아보자.

 

물리 모델을 데이터 베이스 테이블로 구현하는 방법에는 크게 4가지 방법이 존재한다.

 

- MappedSupperclass

- Single Table 전략

- Joined Table 전략

- Table per Class 전략

 

MappedSuperclass 

 

MappedSuperclass 어노테이션은 부모클래스를 데이터베이스의 테이블로 매핑하지 않고 자식클래스에게 매핑 정보를 제공하고 싶을 때만 사용한다.

 

특징

- MappedSuperclass 어노테이션을 사용한 클래스는 엔티티가 아니다. 따라서 해당 클래스를 가지고 영속성 컨텍스트 또는 JPQL 등에서 사용할 수 없다.

- MappedSuperclass 어노테이션을 사용한 클래스는 자체적으로 의미가 없기에 추상 클래스로 만드는 것이 좋다.

 

Single Table 전략

 

Single Table 전략은 각 클래스 계층에 대해 하나의 테이블을 만들며 명시적으로 지정하지 않는 경우 JPA에서는 Default로 해당 전략을 사용한다. 

discriminatorType으로 식별자를 integer로 할 것 인지 혹은 다른 타입으로 지정할 것인지 선택할 수 있다. name을 지정하게 되면 해당 식별자의 이름이 지정되어 테이블로 들어간다. 

 

특징

- 싱글 테이블이기 때문에 조인 쿼리를 사용하지 않고 한 번에 조회가 가능하다.

- 싱글 테이블이기 때문에 null 값이 들어가는 자료가 생긴다.

- 단일 테이블로 유지하기 때문에 테이블의 크기가 커진다.

 

Joined Table 전략

 

조인 전략은 계층 구조를 띄는 각 클래스가 해당 테이블로 매핑된다. 이 때, 테이블마다 반복되는 열이 하나가 있는 데, 이는 조인할 때 반드시 필요한 식별자 이다. 

 

특징

- 테이블을 정규화 할 수 있다.

- 저장공간을 효율적으로 사용한다 이는 정규화를 실시했기 때문이다.

- 조회할 때 외래키를 이용하여 조인 쿼리를 사용하기 때문에 성능이 좋지 않다.

- 데이터 등록 시 쿼리가 한 번 더 발생한다. 

 

Table per Class

 

Table per Class 는 각각의 테이블을 모두 생성하는 전략이다. 결과적으로 MappedSuperclass를 사용한 것돠 유사하다. 그러나 Table per Class 전략에서는 상위 클래스에 대한 엔티티를 정의하기 때문에 상위 클래스를 연결 및 쿼리 사용이 가능하다.

 

특징

- 서브 타입과 구분해서 처리할 때 효과적이다.

- 상속 없이 각 엔티티를 매핑하는 것과 다르지 않아, Union을 사용하여 쿼리를 작성한다.

- Union 쿼리로 인해 성능이 저하될 수 있다.

반응형

Lombok 에서 'is' prefix가 붙은 컬럼의 boolean과 Boolean의 타입 차에 따라 직렬화시 값이 달라진다. 

 

@Getter
@Setter
public class Member {

    private boolean isDisabled;

    public void test(){
        this.setDisabled(true);  //Lombok - 자동생성된 setter
        this.isDisabled();       //Lombok - 자동생성된 getter
    }
}

@Getter
@Setter
public class Member {

    private Boolean isDisabled;

    public void test(){
        this.setIsDisabled(true);  //Lombok - 자동생성된 setter
        this.getIsDisabled();      //Lombok - 자동생성된 getter
    }
}

 

위와 같이 객체의 속성이 boolean 타입에 'is' prefix를 사용하게 될 경우, getter가 isXXXX로 생성되기 때문에 정상적인 동작이 이루어지지 않는다 때문에 아래처럼 @JsonProperty의 is를 제거한 상태로 넣거나 getIsXXXX() getter() 메서드를 작성하면 정상 동작한다.

@Getter
@Setter
public class Member {

	@JsonProperty("disabled")
    private boolean isDisabled;

    public void test(){
        this.setDisabled(true);  //Lombok - 자동생성된 setter
    }
}

 

다른 방법은 타입을 Warpper class인  Boolean으로 사용하여 getter, setter를 생성하면된다.

반응형

'SpringBoot' 카테고리의 다른 글

GenericFilterBean과 OncePerRequestFilter의 차이  (0) 2025.06.23

+ Recent posts