728x90

1. 개념

1.1 Spring Batch 란?

  • 대용량 데이터반복적인 업무를 효율적으로 처리하기 위한 스프링 기반 배치 프레임워크

1.2 언제 사용하는가?

1.2.1 주기적으로 실행되는 작업이 있는 경우

  • ex) 정산

1.2.2 대용량 데이터를 처리해야 하는 경우

  • ex) 로그 분석

1.2.3 멀티 스레드, 분산 처리, 병렬 처리가 필요한 경우

  • ex) 매출 데이터 분석

1.2.4 재시도 혹은 재실행이 필요한 경우

  • ex) 외부 API에서 데이터를 가져와서 처리하는 경우

1.2.5 트랜잭션 관리가 필요한 경우

  • ex) 포인트 적립

 

1.3 왜 사용하는가?

1.3.1 일관된 아키텍처

  • 배치 작업에 필요한 공통 패턴과 템플릿을 미리 제공해 중복 코드 최소화

1.3.2 안정성 & 트랜잭션 관리

  • 실패 시 재시도, 재실행을 지원하며 트랜잭션 경계를 쉽게 설정 가능

1.3.3 손쉬운 모니터링 & 로깅

  • Job, Step 단위로 상태를 관리하여 문제 원인 파악이 쉬움

1.4 특징

1.4.1 잡(Job)과 스텝(Step)을 통한 구조적 설계

  • 작업 단계를 명확히 구분해 유지보수가 쉽고, 실패/재실행 시에도 어디서 문제가 발생했는지 확인이 용이

1.4.2 Chunk(청크) 지향 처리

  • 대량 데이터를 일정 덩어리(Chunk)로 나눠 읽고, 가공 후 한 번에 쓰는 방식

1.4.3 다양한 인터페이스 및 구현체 지원

  • ItemReader, ItemProcessor, ItemWriter 등 표준화된 인터페이스 제공
    • CSV, JDBC 등… 제공

1.4.4 에러 처리 및 재시도(Retry) 정책

  • 작업 도중 오류가 발생해도 재시도 횟수, 건너뛰기(Skip) 등을 설정 가능

2. 아키텍처

2.1 JobLauncher

  • 외부 (스케줄러 등)로 부터 배치를 실행 하라는 요청을 받아 시작
  • ❗햇갈리면 안되는 내용
    • Spring Batch VS Scheduler(or Quartz) 어떤걸 사용하는게 좋을까요??
      • 서로 다른 개념이니 비교하면 안된다!
      • Spring Batch는 대용량 처리!
      • Spring Scheduler는 특정한 시간에 등록된 작업을 자동으로 실행시키는 것!
  • Job과 파라미터 정보를 받아 Job을 실행하고, 그 결과를 반환

2.2 JobRepository

  • 배치 작업의 메타 데이터를 저장/관리하는 장소
  • JobStep의 실행 결과를 DB에 기록하여 재실행, 모니터링, 통계 분석 등 활용할 수 있도록 함

2.3 Job

  • 하나의 배치 프로세스 전체를 나타내는 상위 개념
  • 내부에 여러 Step을 가지고 있으며 각 Step을 순서대로 실행

2.4 Step

  • 배치 처리를 위한 실질적인 실행 단위

2.4.1 ItemReader

  • DB, 파일, 메시지 큐 등에서 데이터를 한 건씩(또는 여러 건 단위) 읽어 옴

2.4.2 ItemProcessor

  • 읽어온 데이터를 가공, 변환, 검증, 필터링 등 비즈니스 로직 수행

2.4.3 ItemWriter

  • 가공된 데이터를 최종적으로 저장(파일, DB 등)

2.5 ExecutionContext & JobExecution

  • ExecutionContext
    • Step에서의 중간 상태나 필요한 데이터를 임시로 저장하는 컨텍스트
  • JobExecution / StepExecution
    • 실행 시간, 처리 건수, 오류 발생 시점 등 세부 정보를 기록해 JobRepository에 보관
    • 재 시작 시 참조되어 어디까지 진행되었는지 확인 가능
반응형

'Study > Spring' 카테고리의 다른 글

[Spring] Spring Security + OAuth 2.0 소셜 로그인  (1) 2025.01.16
[Spring Boot] DB Lock  (0) 2025.01.10
[Spring] QueryDSL-JPA  (0) 2024.12.30
[Spring] @InitBinder  (0) 2021.08.08
[Spring] @Data 어노테이션  (0) 2021.06.30
728x90

1. Spring Security

1.1 Spring Security 란?

  • 보안을 손쉽게 적용하기 위한 Spring Framework의 하위 프로젝트
  • 인증(Authentication)과 인가(Authorization)에 대한 다양한 기능을 제공
  • Filter 기반의 아키텍처로 구성되어 있어, 다양한 커스터마이징이 가능

1.2 Spring Security 인증 및 인가 흐름

2. OAuth 2.0

2.1 OAuth2.0 이란?

  • Open + Authorization (개방형 인가)
  • 웹 및 애플리케이션 인증 및 권한 부여를 위한 개방형 표준 프로토콜
  • 외부 Resoure에 접근하기 위해 사용

2.2 기본 용어

2.2.1 서버 및 클라이언트

  • Resource Owner(사용자)
  • Client(애플리케이션)
  • Authorization Server(인증 서버, 토큰 발급)
  • Resource Server(API 서버, 자원을 실제로 소유)

2.2.2 기타

  • Authorization Code (인증 코드)
  • Scope (범위)

2.3 인증 방식

2.3.1 Authrozation Code Grant

  • 서버 기반 애플리케이션(Server-side Application)에서 가장 많이 사용되는 인증 방식
  • 사용자에게서 직접 자격 증명 정보를 받지 않고, “인가 코드(Authorization Code)”를 발급받아 이를 이용해 액세스 토큰(Access Token)을 얻는 구조

2.3.2 Implicit Grant

  • 브라우저에서 동작하는 자바스크립트 기반 애플리케이션과 같이 클라이언트 비밀(Client Secret)을 안전하게 보관하기 어려운 환경에서 사용하던 방식

2.3.3 Password Credentials Grant

  • 사용자가 클라이언트에 직접 아이디/비밀번호 등의 자격 증명을 입력하고, 클라이언트가 이를 권한 서버에 전달하여 인증 및 액세스 토큰을 발급받는 방식

2.3.4 Client Credentials Grant

  • 서버 대 서버 혹은 백엔드 서비스 간 통신과 같이, 사용자 대신 애플리케이션 자체가 리소스에 접근해야 할 때 사용하는 방식

2.4 통합 흐름

반응형

'Study > Spring' 카테고리의 다른 글

[Spring] Spring Batch  (0) 2025.01.23
[Spring Boot] DB Lock  (0) 2025.01.10
[Spring] QueryDSL-JPA  (0) 2024.12.30
[Spring] @InitBinder  (0) 2021.08.08
[Spring] @Data 어노테이션  (0) 2021.06.30
728x90

1. 개념

1.1 DB Lock 이란?

  • DB Lock은 여러 트랜잭션이 동시에 같은 자원에 접근할 때 데이터 무결성(정합성)을 보장하기 위해 사용되는 메커니즘
  • 쉽게 말해, 어떤 사용자가 데이터를 사용하고 있는 동안에는 다른 사용자가 그 데이터를 동시에 수정하면 안 되기 때문에, ‘잠금’을 걸어서 충돌(Conflict)을 방지

1.2 왜 Lock이 필요한가?

  • 여러 사용자가 동시에 DB를 사용하는 경우 같은 데이터를 동시에 수정하려고 시도하는 경우 충돌 제어 및 데이터의 일관성을 보장하기 위해

1.3 언제 Lock을 사용하는가?

  • 사용해야 하는 경우!
    • 동시에 읽기/쓰기가 빈번하게 일어나는 중요한 테이블에 대해, 데이터 무결성을 엄격히 보장해야 할 때.
    • 은행 이체, 재고 관리, 주문 처리와 같이 동시에 발생하면 안 되는 시나리오를 제어할 때.
  • 주의해야할 점!
    • 불필요한 락은 성능 저하를 유발할 수 있으므로, 적절한 수준에서 사용하는 것이 중요
    • DeadLock!!
      • DeadLock발생 여부를 확인해야함!

1.4 Lock의 생명주기

1.4.1 트랜잭션 시작!

  • DB와의 트랜잭션을 시작
  • 아직 특정 자원에 대한 락이 획득된 상태는 🙅

1.4.2 Lock 획득

  1. 쿼리 요청
  2. Lock 체크
  3. 대기 혹은 Lock 획득

1.4.3 트랜잭션 종료

  1. 트랜잭션 커밋 혹은 롤백
  2. 락 해제

2. Lock 종류

2.1 낙관적 락 (충돌이 발생하지 않을 것이라고 가정)

  • 데이터베이스의 락을 사용하는 것이 아닌 application 레벨에서 버전 관리
  • 특정 작업을 수행하기 전에 별도의 락(lock)을 걸지 않고, 작업 완료 시점에서 데이터의 변경 여부를 확인하여 충돌 여부를 판단 (@Version 활용)
  • 데이터 충돌이 거의 없을것이라고 가정한 경우 사용
    • LockModeType.OPTIMISTIC로 적용
  • 충돌 시 ObjectOptimisticLockingFailureException 발생

2.2 비관적 락 (충돌이 자주 발생할 것이라고 가정)

2.2.1 공유락(Shared Lock, S Lock)

  • 여러 트랜잭션이 동시에 데이터를 읽기할 수 있지만, 쓰기 하려면 공유락을 해제하고 배타락으로 변경
  • 다른 트랜잭션이 쓰기하려 하면 대기 상태
    • LockModeType.PESSIMISTIC_READ로 적용

2.2.2 배타락(Exclusive Lock, X Lock)

  • 오직 한 트랜잭션만 해당 데이터를 읽거나 쓸 수 있음
  • 다른 트랜잭션이 접근하려 하면 대기 상태
    • LockModeType.PESSIMISTIC_WRITE로 적용

2.3 분산 락

  • 여러 인스턴스나 분산 환경에서 락을 설정
  • 데이터베이스에 직접 Lock을 걸지 않고, 외부에서 권한을 받아 처리
  • Redis, **Zookeeper 등... 을 활용하여 적용**

 

반응형

'Study > Spring' 카테고리의 다른 글

[Spring] Spring Batch  (0) 2025.01.23
[Spring] Spring Security + OAuth 2.0 소셜 로그인  (1) 2025.01.16
[Spring] QueryDSL-JPA  (0) 2024.12.30
[Spring] @InitBinder  (0) 2021.08.08
[Spring] @Data 어노테이션  (0) 2021.06.30
728x90

1. 개념

1.1 아키텍처란?

  • 시스템을 효율적이고 안정적으로 만들기 위해, 각 구성 요소를 큰 그림으로 설계하는 설계도이다.

1.2 소프웨어 아키텍처란?

  • 소프트웨어 아키텍처는 소프트웨어가 제공하는 가치인 기능과 구조 중 구조에 속한다.

1.3 왜 아키텍처를 고민을 해야할까?

  1. 확장성(Scalability) 확보
    • 사용자 수가 늘어날 때, 시스템이 문제 없이 확장 가능
  2. 유지보수(Maintainability) 용이
    • 아키텍처가 잘 정리되어 있으면, 버그 수정이나 새로운 기능 추가 시 어떤 부분을 건드려야 할지 명확
    • 모듈별 역할과 의존성을 분리하면, 유지보수 비용과 시간이 크게 감소

1.4 아키텍처를 잘 구현하기 위한 패턴들

1.4.1 레이어드 아키텍처 (Layered Architecture)

  • 애플리케이션을 계층(Layer)으로 구분하여, 각 계층이 맡은 역할을 분리하는 전통적인 방식
  • 장점
    • 역할이 명확해지면서, 코드를 이해하기 쉽고 유지보수가 용이
    • 초기에 구조를 잡기 쉬움
  • 단점
    • 계층이 늘어날수록 호출이 복잡해질 수 있음
    • 변화가 많은 대규모 프로젝트에서 유연성이 떨어질 수 있음

1.4.2 헥사고날 아키텍처 (Hexagonal Architecture)

  • 도메인 로직을 ‘중심(코어)’에 두고, 외부와의 연결(어댑터)을 육각형(포트와 어댑터) 형태로 구분하는 패턴
  • 장점
    • 도메인(핵심 비즈니스 로직)과 인프라(데이터베이스, UI, 외부 시스템)를 느슨하게 결합
    • 테스트와 교체가 용이
  • 단점
    • 초기 진입장벽이 다소 높음 (개념 이해와 구조 설계가 복잡)

1.4.3 클린 아키텍처 (Clean Architecture)

  • 핵심 비즈니스 로직이 프레임워크나 외부 라이브러리에 의존하지 않도록 계층을 구성
  • 장점
    • 변경에 강하고, 테스트하기 쉬운 구조
    • 의존성 역전 원칙(DIP)을 철저히 적용하여 유지보수성 극대화
  • 단점
    • 설계 단계가 복잡하고, 팀 내 이해 합의가 필요
    • 과도하게 적용 시 오히려 개발 속도 저하

1.4.4 마이크로서비스 아키텍처 (Microservices Architecture)

  • 시스템을 작고 독립적인 서비스들로 나누어 개발, 배포, 확장을 각각 독립적으로 진행하는 방식
  • 장점
    • 서비스별로 독립된 스케일링 가능 (효율적 자원 활용)
    • 한 서비스 장애가 전체 시스템에 영향을 덜 줌
    • 기능별로 다른 기술 스택을 자유롭게 도입할 수 있음
  • 단점
    • 서비스 간 통신, 배포 파이프라인 등 복잡도 상승
    • 운영, 모니터링, 트랜잭션 관리 측면에서 추가적인 관리 부담

1.4.5 이벤트 드리븐 아키텍처 (Event-Driven Architecture)

  • 이벤트(메시지)를 통해 각 컴포넌트가 느슨하게 연결되도록 만드는 아키텍처
  • 장점
    • 비동기 처리가 가능해, 높은 확장성과 성능을 기대할 수 있음
    • 각 컴포넌트가 독립적으로 메시지를 발행/구독하기 때문에 결합도가 낮음
  • 단점
    • 트랜잭션을 다루기가 어렵고, 이벤트 플로우가 복잡해질 수 있음
    • 이벤트 중복 처리, 메시지 정합성(consistency) 관리 필요

2. 클린 아키텍처

2.1 클린 아키텍처란

  • 소프트웨어 구조가 유연하고 확장 가능하며, 유지보수가 쉽도록 설계하는 방법론 중 하나로, 로버트 C.마틴(Robert C. Martin, Uncle Bob)의 제안

2.2 클린 아키텍처 필수 요건

  1. 의존성 규칙(Dependency Rule)
    • 의존성은 바깥 레이어에서 안쪽 레이어로만 흐릅니다.
    • 핵심 비즈니스 로직(도메인)은 프레임워크, UI, DB를 전혀 몰라야 합니다.
  2. 도메인 로직(핵심 비즈니스 로직)의 독립성
    • 도메인 모델(Entity 등)은 순수 자바 코드로 작성해, 프레임워크나 DB 관련 코드를 넣지 않습니다.
    • 도메인 정책(Validation, Calculation 등)은 도메인 레벨에서 처리하고, 바깥 레이어(프레임워크/인프라) 의존 로직을 섞지 않습니다.
  3. 인터페이스를 통한 의존 역전(DIP)
    • 도메인이나 Use Case 측에서 필요한 기능(DB 저장, 메시지 전송 등)은 인터페이스로 정의합니다.
    • 실제 구현은 Adapter(구현체) 가 담당하고, 도메인/Use Case는 구현체를 몰라야 합니다.
  4. 테스트 용이성
    • 도메인/Use Case가 외부 환경(DB, 네트워크 등)에 의존하지 않도록 설계합니다.
    • Mock/Stub을 이용해 유닛 테스트가 쉽도록 구조를 마련합니다.
반응형

'Study > etc...' 카테고리의 다른 글

TDD(Test-Driven Development)  (1) 2024.12.18
클린 코드  (1) 2024.12.13
728x90

1. QueryDSL(Query Domain-Specific Language)

1.1 💡QueryDSL 이란?

  • SQL, JPQL 같은 쿼리를 Java 코드로 작성할 수 있게 해주는 타입 안전한 ORM 기반 쿼리 빌더
  • SQLJava 코드로 작성하면서, 가독성과 유지보수성을 대폭 개선할 수 있는 도구

1.2 ☝️ 왜 사용해야할까

1.2.1 SQL/JPQL의 한계 해결

  • 문법 오류 - 쿼리의 오류를 런타임에서만 확인 가능
  • 필드 이름 변경 - 엔티티 필드가 변경되면 문자열 기반 쿼리에서 이를 수동으로 수정해야 함.
  • 동적 쿼리 작성의 어려움 - 조건에 따라 쿼리를 유연하게 작성하려면 코드 복잡도 향상
  • 코드 비교
// JPQL
// 필터가 많아질수록 코드가 복잡
public List<User> findUsersDynamicJPQL(EntityManager em, Integer age, String name) {
    StringBuilder jpql = new StringBuilder("SELECT u FROM User u WHERE 1=1");

    if (age != null) {
        jpql.append(" AND u.age > :age");
    }
    if (name != null) {
        jpql.append(" AND u.name = :name");
    }

    TypedQuery<User> query = em.createQuery(jpql.toString(), User.class);
    if (age != null) query.setParameter("age", age);
    if (name != null) query.setParameter("name", name);

    return query.getResultList();
}


// QueryDSL
// BooleanBuilder 사용
public List<User> findUsersDynamicQueryDSL(JPAQueryFactory queryFactory, Integer age, String name) {
    QUser user = QUser.user;

    BooleanBuilder builder = new BooleanBuilder(); // 동적 조건 빌더
    if (age != null) {
        builder.and(user.age.gt(age));
    }
    if (name != null) {
        builder.and(user.name.eq(name));
    }

    return queryFactory
            .selectFrom(user)
            .where(builder) // 동적 조건 적용
            .fetch();
}

1.2.2 복잡한 쿼리 요구사항 처리

  • 조인 - 여러 테이블 데이터를 조합해서 처리.
  • 서브쿼리 - 쿼리 안에 또 다른 쿼리를 포함.
  • 그룹핑 및 집계 - 데이터를 그룹화하거나 통계 처리.

1.2.3 장/단점

  • 장점
    • 타입 안전한 쿼리 작성
    • 가독성 UP, 유지보수성 UP
    • 동적 쿼리 작성의 유연함
    • 다양한 쿼리 지원
  • 단점
    • 초기 세팅의 복잡성, 의존성 관리
    • Q 클래스 생성 필요
    • 러닝 커브

1.3 💁 언제 사용해야할까?

1.3.1 🙆 사용해야하는 경우

  • 동적 쿼리가 많은 프로젝트
  • 복잡한 쿼리를 자주 작성해야 하는 대규모 애플리케이션
  • 타입 안전성과 코드 가독성을 중시하는 팀 환경

1.3.2 🙅 사용하지 말아야 하는 경우

  • 프로젝트가 간단한 CRUD 중심인 경우
  • 네이티브 SQL을 주로 사용하는 환경

1.4 ✈️ 대안

1.4.1 JPA Criteria API

  • JPA 표준 스펙에 포함된 쿼리 빌더 API

✅ 장점

  • 표준 API: JPA 구현체(Hibernate 등)와 상관없이 동작.
  • 별도의 외부 라이브러리 없이 사용 가능.

❌ 단점

  • 가독성 부족: 코드가 복잡하고 장황해지기 쉬움.
  • 유지보수가 어려움.
  • 예제 코드
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> query = cb.createQuery(User.class);
Root<User> user = query.from(User.class);

Predicate agePredicate = cb.gt(user.get("age"), 18);
Predicate statusPredicate = cb.equal(user.get("status"), "ACTIVE");

query.select(user).where(cb.and(agePredicate, statusPredicate));
List<User> result = em.createQuery(query).getResultList();

1.4.2 Spring Data JPA Specification

  • Spring Data JPA에서 제공하는 Specification 인터페이스를 활용하면 동적 쿼리 구현

✅ 장점

  • Spring Data JPA와 통합: Spring 프로젝트와 자연스럽게 연동.
  • 동적 조건 작성이 비교적 간단.

❌ 단점

  • QueryDSL에 비해 표현력이 제한적.
  • 복잡한 쿼리 작성에는 적합하지 않음.
  • 예제코드
public class UserSpecifications {
    public static Specification<User> hasAgeGreaterThan(int age) {
        return (root, query, cb) -> cb.gt(root.get("age"), age);
    }

    public static Specification<User> hasStatus(String status) {
        return (root, query, cb) -> cb.equal(root.get("status"), status);
    }
}

// 사용
Specification<User> spec = Specification.where(UserSpecifications.hasAgeGreaterThan(18))
                                         .and(UserSpecifications.hasStatus("ACTIVE"));

List<User> users = userRepository.findAll(spec);

1.4.3 네이티브 SQL

✅ 장점

  • 유연성: 데이터베이스에 특화된 쿼리를 작성 가능
  • 성능 최적화에 유리

❌ 단점

  • 타입 안전성 부족: 문자열 기반으로 작성
  • 데이터베이스 종속적 → 이식성이 낮음
  • 유지보수가 어려움
  • 예제 코드
@Query(value = "SELECT * FROM users WHERE age > :age AND status = :status", nativeQuery = true)
List<User> findUsersNative(@Param("age") int age, @Param("status") String status);

1.4.4 jOOQ (Java Object Oriented Querying)

  • jOOQ는 SQL을 Java 코드로 작성하면서도, 데이터베이스와 밀접하게 통합하여 고급 기능을 사용할 수 있는 강력한 도구

✅ 장점

  • SQL 친화적 설계: SQL의 고급 기능(윈도우 함수, CTE 등)을 Java 코드로 직접 활용 가능.
  • 타입 안전성: SQL 컬럼과 Java 필드 간의 타입을 컴파일 타임에 검증.
  • DBMS 특화 기능 지원: 데이터베이스에 따라 최적화된 SQL 생성 가능.
  • 코드 자동 생성: 테이블 스키마 기반으로 Java 코드를 생성하여, 변경 사항을 자동 반영.

❌ 단점

  • SQL 종속성: 데이터베이스 변경 시 쿼리 코드 수정이 필요.
  • 학습 난이도: SQL에 익숙하지 않은 개발자에게는 진입 장벽이 높음.
  • ORM 부족: 객체 지향적 추상화가 부족하고, SQL 중심의 설계에 초점.
  • 예제 코드
// 동적 조건 추가
public List<User> findUsersDynamicJooq(DSLContext context, Integer age, String name) {
    Condition condition = DSL.trueCondition();

    if (age != null) {
        condition = condition.and(USER.AGE.gt(age));
    }
    if (name != null) {
        condition = condition.and(USER.NAME.eq(name));
    }

    return context.selectFrom(USER)
                  .where(condition)
                  .fetchInto(User.class);
}

 

 

라이브러리 특징 장점 단점
QueryDSL - 타입 안전한 동적 쿼리 작성을 지원
- Q클래스를 통해 메서드 체인 방식으로 쿼리를 작성
- 타입 안전성(Type Safety) 보장
- 가독성 높은 코드 작성 가능
- IDE 자동완성 지원
- 초기 설정 복잡
- Q클래스 생성 필요
Spring Data JPA Specifications - JPA의 Criteria API를 기반으로 동적 쿼리를 작성
- Specification 인터페이스를 사용
- 추가 라이브러리 없이 Spring Data JPA와 자연스러운 통합
- 동적 쿼리 작성 가능
- 코드가 장황하고 가독성이 떨어질 수 있음
JOOQ - SQL 빌더 라이브러리로, SQL 문법을 코드로 표현
- 고급 SQL 기능을 지원
- SQL 문법을 코드로 직관적으로 표현 가능
- 복잡한 쿼리와 고급 SQL 기능 활용 가능
- JPA와 통합하려면 추가 설정 필요
Criteria API (JPA 기본) - JPA 표준 사양
- 메서드 체인 방식을 통해 동적 쿼리를 작성
- 모든 JPA 구현체에서 사용 가능
- 추가 의존성 필요 없음
- 코드가 복잡하고 가독성이 떨어짐
Native QSL - SQL 문법그대로 쿼리 작성 - 데이터베이스에 특화된 쿼리를 작성 가능
- 성능 최적화에 유리
- 데이터베이스 종속적 → 이식성이 낮음
- 유지보수가 어려움

 

반응형

'Study > Spring' 카테고리의 다른 글

[Spring] Spring Security + OAuth 2.0 소셜 로그인  (1) 2025.01.16
[Spring Boot] DB Lock  (0) 2025.01.10
[Spring] @InitBinder  (0) 2021.08.08
[Spring] @Data 어노테이션  (0) 2021.06.30
[Spring] 영속성 컨텍스트 (Persistence Context)  (0) 2021.06.30
728x90

2.1 네 개의 영역

  • 표현 영역
    • Controller 부분
    • 사용자와 상호 작용하는 영역
    • HTTP 요청 ↔ 응용 영역이 서로 통신할 수 있도록 요청과 응답을 변환
  • 응용 영역
    • Service 부분
    • 시스템이 사용자에게 제공해야 할 기능을 구현
    • 기능을 구현하기 위해 도메인 영역의 도메인 모델을 사용
  • 도메인 영역
    • Entity, Value 부분
    • 도메인의 핵심 로직을 구현
      • ex) Order 도메인의 핵심 기능은 ‘배송지 변경’, ‘결제 완료’ … 등이 있으며 해당 로직을 구현한다.
  • 인프라스트럭처 영역
    • Repository, Utils 부분
    • 도메인 로직을 지원하고 구현을 외부 시스템과 연결하는 기술적 구조
      • ex) DB 연결, STMP 메일발송, 메세지큐 … 등

2.2 계층 구조 아키텍처

  • 상위 계층에서 하위 계층으로 의존만 존재하고 하위 계층에서 상위 계층에 의존하지 않는다.

2.3 DIP

  • DIP 란?
    • DIP(Dependency Inversion Principle, 의존성 역전 원칙)은 객체 지향 설계 원칙 중 하나로, 고수준 모듈이 저수준 모듈에 의존해서는 안 되고, 추상화를 통해 저수준 모듈이 고수준 모듈을 의존
  • 장점
    • 저수준 모듈을 교체 하기가 용이
    • 테스트코드를 구현하기에 용이
      • 저수준 모듈의 구현체가 없는 추상화(Interface)만 존재하는 경우에도 테스트 코드 작성이 가능하다 (Mock 이용)

2.3.1 DIP 주의사항

  • 단순히 인터페이스와 구현 클래스를 분리하는 것은 잘못된 사용
  • 결과 구조만 보는것이 아닌 모듈의 관점에서 생각하여 추상화를 만들어야 한다.
    • ex)
      1. cacluateDiscountService → RuleEngine ← DroolsRuleEngine (잘못된 사용)
      2. cacluateDiscountService → RuleDiscounter ← DroolsRuleDiscounter (good)

2.3.2 DIP와 아키텍처

  • DIP를 적용하게 되면, 응용 영역과 도메인 영역의 코드 수정 없이 인프라스트럭처부분만 코드를 추가/수정 및 변경하여 요구사항을 충족시킬 수 있다.

2.4 도메인 영역의 주요 구성요소

구성요소 설명 예시
엔티티(Entity) 고유 식별자를 가지며, 상태와 동작을 포함하는 객체 User, Order, Product
값 객체(Value Object) 고유 식별자가 없으며, 불변 객체로 주로 여러 속성을 하나의 개념으로 묶을 때 사용 Address, Money, DateRange
애그리게잇(Aggregate) 관련된 엔티티와 값 객체의 그룹으로, 일관된 변경을 보장하기 위한 경계를 정의하고 루트 엔티티를 통해 접근 Order (루트: Order, 구성 요소: OrderLine)
리포지토리(Repository) 애그리게잇을 영구 저장소에서 조회하고 저장하는 메커니즘을 제공, 인터페이스와 구현체로 구성 OrderRepository, UserRepository
도메인 서비스(Domain Service) 특정 엔티티에 속하지 않는 도메인 로직을 캡슐화하는 서비스 PricingService, PaymentService

2.4.1 엔티티와 벨류

  • 도메인 모델의 엔티티 vs DB 테이블의 엔티티
    • 도메인 모델의 엔티티는 단순 데이터만 담고 있는 구조가 아닌 데이터와 함께 도메인 기능을 제공
    • 도메인 모델 두개이상의 데이터가 개념적으로 하나인 경우Value 타입을 이용해서 표현할 수 있다.
      • ex) Order → name, email 을 Orderer라는 Value타입 객채를 만들어 관리 가능

2.4.2 애그리거트

  • 지도를 볼 때 매우 상세하게 나온 대축척 지도를 보면 큰 수준에서 어디에 위치하고 있는지 이해하기 어려우므로 큰 수준에서 보여주는 소축척 지도를 함께 봐야 현재 위치를 보다 정확하게 이해할 수 있다. 이와 비슷하게 도메인 모델도 개별 객체뿐만 아니라 상위 수준에서 모델을 볼 수 있어야 전체 모델의 관계와 개별 모델을 이해하는데 도움이 된다.
    • ex) ‘주문’, ‘배송지 정보’, ‘주문자’, ‘주문 목록’, ‘총 결제 금액’의 하위 모델들을 ‘주문’이라는 상위 개념으로 표현할 수 있다.

2.5 요청 처리 흐름

2.6 인프라스트럭처 개요

  • 표현 영역, 응용 영역, 도메인 영역을 지원한다.
  • DIP에서 언급한 내용처럼 인프라스트럭처의 기능을 직접 사용하는것보다 인터페이스를 만들어서 사용하게 되면 시스템을 더 유연하고 테스트하기 쉽게 만들어준다. 하지만 무조건 인프라스트럭처에 대한 의존을 없애게 되면 오히려 더 복잡하고 어려운 코드를 유도할 수 있다.
    • ex) 인프라 스트럭처를 직접 사용하지 않기 위해 @Transaction 어노테이션을 사용하지 않고 개발을 하려고 하면 한줄로 처리할 수 있는 코드를 복잡하고 개발시간만 더 늘어날 뿐이다.

2.7 모듈 구성

  • 도메인이 크면 하위 도메인으로 나누고 각 하위 도메인 마다 별도 패키지를 구성한다.
  • 모듈 구성에 대해서는 정답이 없으며, 한 패키지에 10~15개 미만으로 타입 개수를 유지하려고 노력한다.

반응형

'Study > DDD' 카테고리의 다른 글

[DDD] DDD 용어 정리  (1) 2024.12.20
[DDD] Chapter3. 애그리거트  (0) 2024.06.03
728x90

작성 목적

해당 게시글은 DDD관련하여 공부를 하던 중 개인적으로 모르거나 햇갈리는 용어에 대해 리마인드 하기 위해 요약한 내용입니다.

잘못 이해하고 있는 내용이 있으면 댓글로 남겨주시면 감사하겠습니다.

(DDD관련 학습을 진행하는 동안 지속적으로 업데이트 할 예정)

 

용어

  • 도메인 주도 설계(DDD(Domain-Driven Design))
    • 복잡한 소프트웨어를 설계 및 개발하는 과정에서 핵심적인 비지니스 개념과 비지니스 논리 중심으로 설계하는 방법
  • 도메인 전문가
    • 특정 분야나 사업에 대해 깊이있는 지식과 경험을 가진 사람
  • 개념모델 vs구현 모델
    • 개념모델 : "무엇"을 할 것인지에 대한 전략적 설계를 제공
    • 구현모델 : "어떻게" 실행할 것인지에 대한 실용적인 가이드를 제공
  • 유비쿼터스 언어 (보편언어)
    • 팀원 모두가 동일하게 이해하고 표현하는 공통적인 언어

참고 자료

반응형

'Study > DDD' 카테고리의 다른 글

[DDD] Chapter 2 아키텍처 개요  (0) 2024.12.20
[DDD] Chapter3. 애그리거트  (0) 2024.06.03
728x90

배열을 절반씩 나누면서 탐색 범위를 좁혀가는 방식

 

초기조건

  • 정렬된 배열 혹은 리스트가 필요
  • low, mid, hight 3개의 변수 필요

시간복잡도

  • O(n) → O(logn)

장점

  • 배열의 크기가 커질수록 성능 이점이 커, 큰 데이터셋에서도 빠르게 탐색 가능

단점

  • 배열이 정렬되어 있어야만 사용 가능
  • 배열의 구조를 변경하거나 삽입/삭제가 잦은 경우 적합하지 않음
반응형
728x90

1. 정의

슬라이딩 윈도우(Sliding Window)는 배열이나 리스트에서 특정 범위(윈도우)에 대한 계산을 반복적으로 수행해야 할 때 유용한 알고리즘 기법

2. 종류

  • 고정 크기 윈도우
    • 주어진 크기의 연속된 구간을 탐색하는 경우
  • 가변 크기 윈도우
    • 특정 조건을 만족하는 구간을 탐색하는 경우

3. 슬라이딩 윈도우 vs 투포인터

3.1 특징

  슬라이딩 윈도우 투 포인터
목적 고정되거나 조건에 따라 이동하는 구간(window)을 탐색 두 포인터를 사용해 배열의 특정 조건을 만족하는 값을 탐색
윈도우 크기 고정 또는 가변 일반적으로 가변
주요 동작 윈도우를 한 칸씩 이동하며 필요한 부분만 갱신 두 포인터를 독립적으로 이동하며 구간의 조건을 만족하도록 조정
사용 상황 연속된 구간에 대한 계산이 필요할 때 정렬된 배열에서 특정 조건을 만족하는 값을 찾거나, 구간을 탐색할 때

3.2 공통점

  1. 배열이나 리스트에서 효율적으로 탐색
  2. O(N)의 시간 복잡도로 문제 해결 가능

3.3 차이점

  1. 탐색 구간
    • 슬라이딩 윈도우
      • 연속된 구간을 유지하며 탐색
    • 투 포인터
      • 두 포인터가 독립적으로 움직이며 조건을 만족하도록 탐색.
  2. 조건
    • 슬라이딩 윈도우
      • 윈도우 내 값을 반복적으로 갱신
    • 투 포인터
      • 값이나 조건을 만족할 때까지 포인터를 확장/축소.

3.4 선택 조건

  • 슬라이딩 윈도우를 선택:
    • 구간 내 합, 곱, 최대/최소값을 반복적으로 계산해야 하는 경우.
    • 문제에서 “연속된 서브배열”이라는 조건이 있는 경우.
      • 예: 크기가 K인 서브배열의 최대합.
  • 투 포인터를 선택:
    • 정렬된 배열에서 조건을 만족하는 두 값이나 범위를 찾는 경우.
    • 문제에서 “특정 합”, “차이”, “곱” 등을 만족하는 두 요소를 요구하는 경우.
      • 예: 두 수의 합, 특정 값의 구간 찾기.

4. 예시

4.1 고정 크기 윈도우

import java.util.Arrays;

public class FixedWindowAverage {

    public static double[] findAverages(int[] arr, int k) {
        if (arr.length < k) {
            throw new IllegalArgumentException("Array length must be greater than or equal to k.");
        }

        double[] result = new double[arr.length - k + 1];
        double windowSum = 0;

        // 초기 윈도우 합 계산
        for (int i = 0; i < k; i++) {
            windowSum += arr[i];
        }

        result[0] = windowSum / k;

        // 슬라이딩 윈도우 이동
        for (int i = k; i < arr.length; i++) {
            windowSum += arr[i] - arr[i - k]; // 새 요소 추가, 오래된 요소 제거
            result[i - k + 1] = windowSum / k; // 평균 계산
        }

        return result;
    }

    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5};
        int k = 3;

        double[] averages = findAverages(arr, k);
        System.out.println("크기 " + k + "의 서브배열 평균값: " + Arrays.toString(averages));
        // 출력: [2.0, 3.0, 4.0]
    }
}

4.2 가변 크기 윈도우

public class VariableWindowExample {

    public static int minSubArrayLen(int target, int[] arr) {
        int n = arr.length;
        int left = 0, sum = 0;
        int minLength = Integer.MAX_VALUE;

        for (int right = 0; right < n; right++) {
            sum += arr[right]; // 윈도우 확장

            while (sum >= target) { // 조건 만족 시 윈도우 축소
                minLength = Math.min(minLength, right - left + 1);
                sum -= arr[left];
                left++;
            }
        }

        return minLength == Integer.MAX_VALUE ? 0 : minLength;
    }

    public static void main(String[] args) {
        int[] arr = {2, 3, 1, 2, 4, 3};
        int target = 7;

        int result = minSubArrayLen(target, arr);
        System.out.println("합이 " + target + " 이상인 가장 짧은 서브배열의 길이: " + result); // 출력: 2
    }
}
반응형

'Study > Algorithm' 카테고리의 다른 글

[Algorithm] 이분탐색  (0) 2024.12.20
[Algorithm] 탐욕법(그리디) 알고리즘(greedy algorithm)  (0) 2021.01.13
728x90

💡 TDD(Test-Driven Development): 실패에서 시작하는 개발의 예술

1. TDD(Test-Driven Development)

🎯1.1 TDD란 무엇인가요?

  • TDD는 개발자의 ‘습관’
    • “테스트부터 작성하자. 그리고 통과시키자!”
  • 정의: 테스트를 먼저 작성하고, 그 테스트를 통과하기 위해 코드를 작성하는 개발 방법론.
  • 왜 중요한가요?
    • 실패를 두려워하지 않고, 더 나은 코드를 만들어 가는 과정!
    • Red-Green-Refactor라는 단순하지만 강력한 루틴으로 코드 품질을 높임.

🚦 1.2 기본 사이클: Red-Green-Refactor

1.2.1 Red: 실패를 즐기자!

  • 테스트부터 작성 (아직 구현은 X).
  • 실패를 통해 요구사항과 문제를 명확히 이해.
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class CalculatorTest {

    @Test
    void testAddition() {
        Calculator calculator = new Calculator();
        int result = calculator.add(2, 3);
        assertEquals(5, result); // 실패! 왜? 구현 코드가 없으니까.
    }
}

1.2.2 Green: 테스트 통과는 최소 노력으로.

  • 테스트를 통과시키는 코드 작성.
  • 복잡하게 고민하지 말고 “빨리 테스트 통과”에 집중!
public class Calculator {
    public int add(int a, int b) {
        return a + b; // 통과! 왜? 테스트가 요구한 기능만 구현했으니까.
    }
}

1.2.3 Refactor: 이제 진짜 멋지게!

  • 읽기 좋은 코드, 유지보수하기 쉬운 코드로 개선.
  • 코드의 동작은 그대로, 구조는 깔끔하게!
public class Calculator {
    // 향후 확장을 고려해 더 나은 구조로 개선
    public int add(int a, int b) {
        return calculate(a, b, "+");
    }

    // 공통 로직 처리 메서드 추가
    private int calculate(int a, int b, String operation) {
        switch (operation) {
            case "+":
                return a + b;
            default:
                throw new UnsupportedOperationException("Operation not supported");
        }
    }
}

✨ 1.3 왜 중요한가요?

  • 🛡 안정성: 모든 기능에 대한 자동화된 테스트 보장.
  • 📈 생산성 향상: 리팩토링 부담 감소.

“TDD를 하면 시간 낭비라고?” 아니요! 미래의 당신이 감사할 겁니다. 😎

🔥 1.4 장단점

  • 장점
    • 코드의 신뢰성 증가.
    • 요구사항 변경에도 흔들리지 않는 튼튼한 구조.
    • 버그 감소로 디버깅 시간 절약.
  • 단점
    • 초기 진입장벽 존재.
    • 테스트 작성에 시간 투자 필요.
    • 잘못된 테스트 설계 시 오히려 발목을 잡을 수 있음.

🙀 1.5 DDD(Domain Driven Design)와의 관계

  • TDD는 테스트를 통해 코드를 어떻게 구현할지에 초점을 둔 개발 방법론
  • DDD는 도메인 모델을 설계하여 무엇을 구현할지를 정의하는 철학
질문 TDD DDD
왜 사용하는가? 기능의 동작을 보장하기 위해 비즈니스 요구사항을 시스템에 반영하기 위해
왜 시작하는가? 코드를 테스트하면서 명확한 피드백을 얻기 위해 요구사항과 도메인 모델을 기반으로 설계하기 위해
 중요한가? 코드의 안정성과 품질을 높이기 위해 복잡한 비즈니스 로직을 제대로 반영하기 위해
어떻게 다른가? 테스트 작성 → 코드 작성 → 리팩토링 요구사항 분석 → 도메인 설계 → 코드 작성

2. 테스트 코드

💡2.1 테스트 코드란 무엇인가요?

  • “내 코드, 정말 제대로 작동할까?”라는 질문에 대한 답을 미리 준비하는 개발자의 무기
  • 프로그램의 기능이 의도대로 동작하는지 자동으로 확인

🛠 2.2 종류

2.2.1 단위 테스트 (Unit Test)

  • 특징: 순수 비즈니스 로직(단일 메서드나 클래스)을 테스트
  • 도구: @Mock, Mockito.
  • 예시: UserService가 UserRepository를 올바르게 호출하는지 테스트
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.Optional;

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    private UserService userService;

    @Test
    void should_ReturnUser_When_ValidIdProvided() {
        // Mock 초기화
        MockitoAnnotations.openMocks(this);
        userService = new UserService(userRepository);

        // Given
        User mockUser = new User(1L, "Alice");
        when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser));

        // When
        User result = userService.getUserById(1L);

        // Then
        assertEquals("Alice", result.getName());
        verify(userRepository, times(1)).findById(1L);
    }
}

2.2.2 통합 테스트 (Integration Test)

  • 특징: 여러 계층(Service, Repository 등)이 올바르게 연동되는지 확인
  • 도구: @SpringBootTest
  • 예시: 데이터 저장 및 조회가 잘 동작하는지 확인.
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@Transactional // 테스트 후 DB 롤백
class UserIntegrationTest {

    @Autowired
    private UserService userService;

    @Autowired
    private UserRepository userRepository;

    @Test
    void should_SaveAndRetrieveUser() {
        // Given
        User newUser = new User(null, "Bob");

        // When
        userService.saveUser(newUser);
        User retrievedUser = userService.getUserById(newUser.getId());

        // Then
        assertNotNull(retrievedUser);
        assertEquals("Bob", retrievedUser.getName());
    }
}

2.2.3 End-to-End 테스트 (E2E Test)

  • 특징: 시스템의 전체 흐름이 사용자 관점에서 잘 작동하는지 검증
  • 도구: @SpringBootTest, TestRestTemplate
  • 예시: 사용자 생성 후 조회 API가 올바르게 동작하는지 확인.
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserE2ETest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void should_CreateAndRetrieveUser() {
        // Given
        String baseUrl = "http://localhost:" + port + "/users";
        UserRequest userRequest = new UserRequest("Charlie");

        // Step 1: 사용자 생성
        UserResponse createdUser = restTemplate.postForObject(baseUrl, userRequest, UserResponse.class);

        assertNotNull(createdUser);
        assertEquals("Charlie", createdUser.getName());

        // Step 2: 생성된 사용자 조회
        UserResponse retrievedUser = restTemplate.getForObject(baseUrl + "/" + createdUser.getId(), UserResponse.class);

        assertNotNull(retrievedUser);
        assertEquals("Charlie", retrievedUser.getName());
    }
}

🚦 2.3 Spring Boot 테스트에서 사용하는 Mock의 종류

2.3.1 @Mock

  • @Mock은 객체의 동작을 정의해서 의존성을 가짜로 만들어 줍니다.
  • 주로 단위 테스트에서 사용하며, Mock 라이브러리인 Mockito를 함께 사용합니다.
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

class UserServiceTest {

    @Mock
    private UserRepository userRepository; // 독립된 Mock 객체 생성

    private UserService userService;

    @Test
    void should_ReturnUser_When_ValidIdProvided() {
        // Mock 초기화
        MockitoAnnotations.openMocks(this);
        userService = new UserService(userRepository);

        // Given
        when(userRepository.findById(1L)).thenReturn(Optional.of(new User(1L, "Alice")));

        // When
        User result = userService.getUserById(1L);

        // Then
        assertEquals("Alice", result.getName());
        verify(userRepository, times(1)).findById(1L);
    }
}

2.3.2 @MockBean

  • @MockBeanSpring Context에 등록된 빈(bean)을 Mock 객체로 바꿔줍니다.
  • 덕분에 Controller, Service, Repository가 연동된 통합 테스트에서도 유용합니다.
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;

import java.util.Optional;

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class OrderServiceTest {

    @Autowired
    private OrderService orderService;

    @MockBean
    private OrderRepository orderRepository; // Repository 빈을 Mock으로 대체

    @Test
    void should_ReturnOrder_When_OrderExists() {
        // Given
        Order mockOrder = new Order(1L, "ProductA", 3, "CREATED");
        when(orderRepository.findById(1L)).thenReturn(Optional.of(mockOrder));

        // When
        Order result = orderService.getOrderById(1L);

        // Then
        assertNotNull(result);
        assertEquals("ProductA", result.getProductName());
        verify(orderRepository, times(1)).findById(1L);
    }
}

2.3.3 @Spy

  • @Spy는 실제 객체를 사용하면서 특정 메서드만 Mocking할 수 있습니다.
  • 로직의 일부만 제어하고 싶을 때 유용합니다.
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Spy;

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

class OrderServiceTest {

    @Spy
    private OrderService orderService = new OrderService();

    @Test
    void should_MockSpecificMethod() {
        // Given
        doReturn(500).when(orderService).calculateTotalPrice(100, 5);

        // When
        int totalPrice = orderService.calculateTotalPrice(100, 5);
        String status = orderService.getOrderStatus(1L); // 실제 메서드 실행

        // Then
        assertEquals(500, totalPrice); // Mocking된 결과 검증
        assertEquals("COMPLETED", status); // 실제 동작 검증

        // Verify
        verify(orderService, times(1)).calculateTotalPrice(100, 5);
        verify(orderService, times(1)).getOrderStatus(1L);
    }
}

2.3.4 @SpyBean

  • @SpyBeanSpring Context에 등록된 실제 빈Spy 객체로 감싸줍니다.
  • 즉, Spring 빈의 실제 동작을 수행하면서 일부 메서드만 Mocking 할 수 있습니다.
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class OrderServiceIntegrationTest {

    @Autowired
    private OrderService orderService;

    @SpyBean
    private OrderService spyOrderService;

    @Test
    void should_UseSpyBeanToMockSpecificMethod() {
        // Given
        doReturn(1000).when(spyOrderService).calculateTotalPrice(200, 5);

        // When
        int mockedResult = spyOrderService.calculateTotalPrice(200, 5); // Mock 동작
        String status = spyOrderService.getOrderStatus(1L); // 실제 동작 실행

        // Then
        assertEquals(1000, mockedResult); // Mocking 결과
        assertEquals("COMPLETED", status); // 실제 메서드 결과

        // Verify
        verify(spyOrderService, times(1)).calculateTotalPrice(200, 5);
        verify(spyOrderService, times(1)).getOrderStatus(1L);
    }
}
어노테이션 대상 설명 사용 예
@Mock 독립된 가짜 객체 Spring Context와 무관, 가짜 객체 생성 순수 단위 테스트
@MockBean Spring 빈을 가짜로 대체 Spring Context에서 Mock 객체로 교체 Controller와 Service 연결 테스트
@Spy 실제 객체 (부분만 Mocking) 일부 메서드만 Mocking, 나머지는 실제 실행 실제 객체의 부분 테스트
@SpyBean Spring 빈을 Spy 객체로 감싸기 Spring 빈을 감싸서 일부만 Mocking 실제 Spring 빈의 동작과 호출 검증

✨ 2.4 테스트코드를 구조적으로 깔끔하게 짜는 꿀팁

2.4.1 @Nested

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

@DisplayName("OrderService 단위 테스트")
class OrderServiceTest {

    private OrderService orderService = new OrderService();

    @Nested
    @DisplayName("주문 생성 테스트")
    class CreateOrderTests {

        @Test
        @DisplayName("정상적으로 주문을 생성한다.")
        void should_CreateOrder_Successfully() {
            Order order = orderService.createOrder("ProductA", 3);
            assertNotNull(order);
            assertEquals("ProductA", order.getProductName());
        }
    }

    @Nested
    @DisplayName("주문 조회 테스트")
    class GetOrderTests {

        @Test
        @DisplayName("존재하는 주문을 조회한다.")
        void should_ReturnOrder_When_OrderExists() {
            Order order = orderService.createOrder("ProductA", 3);
            Order retrievedOrder = orderService.getOrderById(order.getId());
            assertEquals(order, retrievedOrder);
        }
    }
}

2.4.2 @DisplayName

  • @DisplayName은 테스트의 목적이나 동작을 설명하는 이름을 추가
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

@DisplayName("OrderService 테스트")
class OrderServiceTest {

    private OrderService orderService = new OrderService();

    @Test
    @DisplayName("주문 생성 시 상품 이름과 수량이 올바르게 설정된다.")
    void should_CreateOrder_With_CorrectDetails() {
        Order order = orderService.createOrder("ProductA", 3);
        assertNotNull(order);
        assertEquals("ProductA", order.getProductName());
        assertEquals(3, order.getQuantity());
    }
}

2.4.3 @BeforeEach / @AfterEach

  • @BeforeEach: 각 테스트 실행 전 공통 작업을 수행
  • @AfterEach: 각 테스트 실행 후 정리 작업을 수행
import org.junit.jupiter.api.*;

import static org.junit.jupiter.api.Assertions.*;

class OrderServiceTest {

    private OrderService orderService;

    @BeforeEach
    void setUp() {
        orderService = new OrderService();
    }

    @Test
    @DisplayName("주문 생성 시 상품 이름이 설정된다.")
    void should_CreateOrder_With_ProductName() {
        Order order = orderService.createOrder("ProductA", 3);
        assertEquals("ProductA", order.getProductName());
    }

    @AfterEach
    void tearDown() {
        System.out.println("테스트 정리 작업 실행");
    }
}

2.4.4 @TestMethodOrder + @Order

import org.junit.jupiter.api.*;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;

import static org.junit.jupiter.api.Assertions.*;

@TestMethodOrder(OrderAnnotation.class) // @Order 어노테이션 기준으로 순서 지정
@DisplayName("OrderService 순서 지정 테스트")
class OrderServiceTest {

    private static OrderService orderService;

    @BeforeAll
    static void setUp() {
        orderService = new OrderService();
    }

    @Test
    @Order(1)
    @DisplayName("1. 주문을 생성한다.")
    void testCreateOrder() {
        Order order = orderService.createOrder("ProductA", 3);
        assertNotNull(order);
        assertEquals("ProductA", order.getProductName());
    }

    @Test
    @Order(2)
    @DisplayName("2. 주문을 조회한다.")
    void testGetOrder() {
        Order order = orderService.getOrderById(1L); // 1번 ID의 주문 조회
        assertNotNull(order);
        assertEquals("ProductA", order.getProductName());
    }

    @Test
    @Order(3)
    @DisplayName("3. 주문 상태를 변경한다.")
    void testUpdateOrderStatus() {
        Order updatedOrder = orderService.updateOrderStatus(1L, "COMPLETED");
        assertEquals("COMPLETED", updatedOrder.getStatus());
    }
}

2.4.5 @TestMethodOrder(MethodName.class)

import org.junit.jupiter.api.*;
import org.junit.jupiter.api.MethodOrderer.MethodName;

import static org.junit.jupiter.api.Assertions.*;

@TestMethodOrder(MethodName.class) // 메서드 이름 순서대로 실행
class OrderServiceTest {

    @Test
    void a_testCreateOrder() {
        System.out.println("1. 주문 생성");
    }

    @Test
    void b_testGetOrder() {
        System.out.println("2. 주문 조회");
    }

    @Test
    void c_testUpdateOrderStatus() {
        System.out.println("3. 주문 상태 변경");
    }
}
반응형

'Study > etc...' 카테고리의 다른 글

소프트웨어 아키텍처  (0) 2025.01.03
클린 코드  (1) 2024.12.13

+ Recent posts