잡다한 프로그래밍 :: 잡다한 프로그래밍
반응형

0. 개요

지난 시간에는 JPA의 핵심 구조인 영속성 컨텍스트엔티티 생명주기에 대해 알아봤습니다.
이번에는 영속성 컨텍스트가 우리에게 어떤 실질적인 장점을 제공하는지 하나씩 살펴보겠습니다.

 

1. 1차 캐시 (First-Level Cache)

영속성 컨텍스트는 엔티티를 관리하기 위해 메모리 캐시(1차 캐시) 를 내부에 가지고 있습니다.
이 캐시는 같은 트랜잭션 범위 안에서 반복되는 조회를 줄여주는 역할을 합니다.

// 엔티티를 생성한 상태 (비영속)
Member member = new Member();
member.setId("member1");

// 영속 상태 (1차 캐시에 저장)
em.persist(memeber);

// 1차 캐시에서 조회 (DB에서 조회하지않음)
Member findM = em.find(Member.class, "member1");

 

  • persist() 시점에서 1차 캐시에 저장됨
  • 이후 find()를 하면 DB가 아니라 캐시에서 조회
  • 복잡한 도메인 로직에서 중복 조회를 줄여주는 데 유용

 

2.  엔티티 동일성 보장 (Identity Guarantee)

JPA는 같은 트랜잭션 범위 내에서 같은 엔티티는 동일한 인스턴스로 보장합니다.

Member m1 = em.find(Member.class, 1);
Member m2 = em.find(Member.class, 1);

System.out.println(a == b); // true 동일성 보장
  • 두 객체는 equals() 비교가 아닌 == 비교에서도 true
  • 즉, 같은 1차 캐시 객체를 반환
  • 이는 도메인 모델을 안전하게 다루고, ORM의 객체 그래프 관리에 있어 중복 문제를 방지해줌

3. 트랜잭션을 지원하는 쓰기 지연

JPA는 persist() 시점에 곧바로 DB에 INSERT 쿼리를 날리지 않습니다.
대신 내부의 쓰기 지연 저장소(write-behind queue) 에 SQL을 모아뒀다가,
commit() 시점에 한꺼번에 DB에 반영합니다.

transaction.begin(); // 트랜잭션 시작

em.persist(member1);
em.persist(member2); // 영속 상태 (쿼리 날리지 않음)

// 커밋하는 시점에 INSERT SQL을 보냄
trasaction.commit();

 

  • INSERT 쿼리를 모아서 한번에 처리
  • DB와의 통신 횟수를 줄여줌 → 성능 최적화
  • Hibernate는 이걸 Batch Insert로 활용할 수도 있음 (설정 필요)

 

4. 변경 감지 (Dirty Checking)

JPA는 영속성 컨텍스트 안의 엔티티가 변경되면
자동으로 UPDATE SQL을 생성해서 DB에 반영합니다.

trasaction.begin();

// 영속 엔티티 조회
Member m = em.find(Member.class, 1);

// 영속 엔티티 수정
m.setUsername("name2");

trasaction.commit();
  • 영속성 컨텍스트에 엔티티가 등록될 때 스냅샷(초기 상태 복사본) 을 저장
  • 트랜잭션 종료 전 현재 값과 스냅샷을 비교
  • 변경된 필드가 있다면 → UPDATE SQL 생성
  • 이 쿼리도 마찬가지로 쓰기 지연 저장소에 모았다가 커밋 시 DB 반영

이걸 Dirty Checking이라고 부릅니다.

 

 

5. 지연로딩

추후 작성 예정

반응형

'프로그래밍 > JPA' 카테고리의 다른 글

[JPA] 필드와 컬럼 매핑  (1) 2025.06.19
[JPA] 데이터베이스 스키마 자동 생성  (0) 2025.06.19
[JPA] 객체와 테이블 매핑  (0) 2025.06.19
[JPA] JPA 의 영속성 관리  (0) 2025.06.10
[JPA] JPA 동작 방식  (2) 2025.06.08
반응형

0. 개요

지난 시간에 아래와 같이 member 엔티티를 DB에 자동으로 insert 하는 내용을 학습했다.

그런데 JPA는 어떻게 이걸 자동으로 처리할까?
em.persist()가 곧바로 insert 쿼리를 DB로 쏘는 함수일까?

 

오늘은 JPA가 엔티티를 어떻게 관리하고 DB와 상호작용하는지의 핵심인
“영속성 컨텍스트(Persistence Context)”와 엔티티 생명주기를 정리한다.

    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            Member member = new Member();
            member.setId(1L);
            member.setName("HelloA");
            em.persist(member);

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }

        emf.close();
    }

 

 

1. JPA에서 가장 중요한 두가지

  • 객체와 관계형 데이터베이스의 매핑 (설계에 가까운 부분으로, 후에 설계 방법도 설명할 예정입니다)
    (→ 객체 모델을 어떻게 RDB 테이블에 맞게 설계·매핑할 것인가)
  • 영속성 컨텍스트(Persistence Context)
    (→ JPA 내부 동작 원리, 실제 DB와의 상호작용 및 엔티티 생명주기 관리의 핵심)

 

2. 영속성 컨텍스트란?

 

  • 한마디로 “엔티티를 영구 저장하는 논리적 환경(캐시)”
  • 실제 DB에 바로 저장되는 게 아니라,
    먼저 영속성 컨텍스트에 저장·관리된다.
  • EntityManager를 통해서만 영속성 컨텍스트를 간접적으로 다룬다.
중요:
em.persist(entity)를 호출하면
바로 DB에 insert가 되는 게 아니라,
일단 영속성 컨텍스트(1차 캐시)에 저장됨!

 

더보기

📌 Spring JPA에서 EntityManager는 왜 N:1 구조인가?

 

예를들어 아래처럼 사용자의 요청이 와서 한트랜잭션 서비스 내에서 2개의 repo를 호출하고 있다고 가정합니다.

이때 각 repo는 각각의 em을 사용하고 있다고 가정하구요.

 

이럴때 두개의 em은 같은 영속성 컨텍스트를 사용하게 됩니다.

왜일까요? 

 

MembrerRepoA

@Repository
@RequiredArgsConstructor
public class MemberRepositoryA {

    private final EntityManager em;

    public void saveA(String name) {
        System.out.println("RepoA EntityManager: " + em); // 프록시 주소 출력
        Member member = new Member();
        member.setName("A_" + name);
        em.persist(member);
    }
}

 

MemberRepoB

@Repository
@RequiredArgsConstructor
public class MemberRepositoryB {

    private final EntityManager em;

    public void saveB(String name) {
        System.out.println("RepoB EntityManager: " + em); // 프록시 주소 출력
        Member member = new Member();
        member.setName("B_" + name);
        em.persist(member);
    }
}

 

 

MemberService

@Service
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepositoryA repoA;
    private final MemberRepositoryB repoB;

    @Transactional
    public void register(String name) {
        repoA.saveA(name);
        repoB.saveB(name);
    }
}

 

spring에서 em을 주입받아 사용했으니, 당연히 같은 객체 (entityManager) 아니야? 라고 생각할 수 있습니다.

실제로 둘을 비교하면 같은 객체로 나오구요

❗️하지만 진실은?

이건 프록시 객체입니다.
실제로는 내부에서 **현재 트랜잭션에 연결된 "진짜 EntityManager"**를 찾아서 위임하고 있는 구조 입니다.

그리고 이 "진짜 EntityManager"는 트랜잭션마다 다릅니다.

즉, 한 요청/트랜잭션 내에서는 같은 컨텍스트를 공유하지만,
다른 요청에서는 다른 컨텍스트가 사용됩니다.

 

🔍 그럼 왜 N:1?

여러 개의 클래스(RepoA, RepoB 등)에서 EntityManager를 주입받는다면 N개의 주입 지점이 생깁니다.
이 주입된 EntityManager들은 모두 같은 프록시 인스턴스를 공유하지만,

 

실제론 현재 트랜잭션에 맞는 하나의 영속성 컨텍스트로 위임되고 있습니다.

그래서 우리는 이를 N (여러 주입 지점) : 1 (현재 트랜잭션에 묶인 영속성 컨텍스트)로 부릅니다.

 

 

✅ 진짜 EntityManager를 싱글톤으로 주입하면 안 되는 근본 이유

EntityManager는 스레드에 안전하지 않다 (not thread-safe)
그래서 트랜잭션/스레드마다 분리된 객체가 필요하다
즉, 요청마다 다른 컨텍스트가 필요하기 때문에
싱글톤 EntityManager를 공유하면 큰 문제 발생

2. 엔티티 생명주기 4단계

1) 비영속(Transient)

  • 아직 JPA가 모르는 상태 (영속성 컨텍스트와 전혀 관계가 없는 상태)
  • 단순히 new로 객체를 만든 상태 (아직 em.persist() 호출 전)
 
            // 객체를 생성함 (비영속)
            Member member = new Member();
            member.setId(1L);
            member.setName("HelloA");

 

2) 영속(Managed)

  • 영속성 컨텍스트에서 관리되는 상태 (메모리내 1차 캐시 등록)
  • 아직 실제 DB insert는 아님 (commit 전까지는 DB에 적용 X)
  • 동일 트랜잭션 내에서 1차 캐시된 객체를 “동일성 비교(==)”로 찾을 수 있음 (추가로 설명하기...)
            Member member = new Member();
            member.setId(1L);
            member.setName("HelloA");
            
            // 영속(영속성 컨텍스트에서 관리를 함)
            em.persist(member);
            
            // 실제 DB insert 쿼리는 트랜잭션을 커밋하는 시점에 동작함
            tx.commit();

 

3) 준영속(Detached)

  • 영속 상태였다가 영속성 컨텍스트에서 분리된 엔티티
  • EntityManager가 더 이상 변경 추적, dirty checking, flush 등 관리를 하지 않음
em.detach(member); // 엔티티를 영속성 컨텍스트에서 분리

em.clear(); // 영속성 콘텍스트를 비움

em.close(); // 영속성 콘텍스트를 종료
그럼 detach로 분리한순간 영속성 컨텍스트에서 바로 삭제될까?

정답 : 삭제한다

 

❗️ 비영속과 준영속의 미묘한 차이

  • 비영속: 처음부터 영속성 컨텍스트에 등록된 적이 없는 상태(new로 생성만 한 객체)
  • 준영속: 한때 관리받다가 detach/clear/close로 “분리된” 상태

 

4) 삭제(Removed)

  • 삭제 예약 상태
  • em.remove(entity) 호출
    → 영속성 컨텍스트에선 제거됨
    → flush/commit 시 실제 DB에서 DELETE 쿼리 실행
em.remove(member);

 

반응형

'프로그래밍 > JPA' 카테고리의 다른 글

[JPA] 필드와 컬럼 매핑  (1) 2025.06.19
[JPA] 데이터베이스 스키마 자동 생성  (0) 2025.06.19
[JPA] 객체와 테이블 매핑  (0) 2025.06.19
[JPA] JPA의 영속성 관리2  (0) 2025.06.10
[JPA] JPA 동작 방식  (2) 2025.06.08
반응형

1. 캐시 스탬피드 현상이란?

캐시 만료 시점에 대량의 요청이 한꺼번에 DB 또는 외부 API 등 원본 소스로 몰리는 현상입니다.

“동시에 캐시가 비어, 수많은 트래픽이 원본 시스템을 두드려
서비스 전체의 가용성과 응답속도를 위협하는 상황”

 

일반적인 캐시 동작 플로우

 

1. 클라이언트가 데이터를 요청

2. 서버는 캐시에서 해당 데이터를 조회

3. 캐시에 존재하면 그 값을 바로 반환

4. 캐시에 없으면 (Miss) → DB나 외부 API 등 원본 시스템에서 조회 후 캐시에 저장, 응답

 

애플리케이션 동작 흐름

 

🚨문제가 생기는 경우: 캐시 만료 시점

예를 들어 캐시가 만료된 시점에 동시에 N건의 요청이 들어오면 어떻게 될까?

  • 캐시 데이터가 만료되었고, 이 때 100만명의 사용자가 요청을 진행함
  • 100만건 요청 모두 DB or API 서버로 데이터를 요청
  • 특별한 조치가 없을 경우, 백엔드 서버는 100만번 캐시에 기록 후 사용자에게 데이터를 전달

이런 경우 백엔드, 캐시 모두 부하(장애, 응답 지연, 전체 다운 위험)를 일으키는 원인이 되며

이러한 현상을 캐시 스탬피드라고 합니다.

2. 해결 방법

2-1. TTL (Time To Live) 증가

캐시 만료 주기를 늘리면, 실제 리소스에 접근하는 횟수를 줄이고 위험도 줄어듭니다. (가장 간단한 방법)

 

단점:

  • 실시간성이 중요한 경우 사용자 경험이 나빠질 수 있음
  • 예) 어떤 상품이 오전 10시부터 판매일때, 10시 2분에도 아직 오래된 캐시로 사용자에게 판매 대기중으로 보일 수 있음

 

2-2. Lock을 통한 동시성 제어

하나의 요청에 Lock을 걸어 동시에 캐시를 쓰지 않도록 하여 단한번의 캐시 쓰기 작업만 이루어지도록 한다

더보기

AOP 구현

@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class DistributedLockAop {

    private static final String LOCK_PREFIX = "LOCK:";

    private final RedissonClient redissonClient;

    @Around("@annotation(distributedLock)")
    public Object lock(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) throws Throwable {
        String key = LOCK_PREFIX + parseKey(joinPoint, distributedLock.key());
        RLock lock = redissonClient.getLock(key);

        boolean locked = false;
        try {
            locked = lock.tryLock(
                distributedLock.waitTime(),
                distributedLock.leaseTime(),
                distributedLock.timeUnit()
            );
            if (!locked) {
                // 락 획득 실패시 예외 또는 커스텀 반환
                log.warn("분산락 획득 실패: {}", key);
                throw new IllegalStateException("Lock acquisition failed: " + key);
            }
            // 비즈니스 로직 실행
            return joinPoint.proceed();
        } finally {
            if (locked && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    private String parseKey(ProceedingJoinPoint joinPoint, String keyExpression) {
        // 파라미터를 SpEL로 파싱하는 유틸 (아래 참고)
        return CustomSpringELParser.getDynamicValue(
            ((MethodSignature) joinPoint.getSignature()).getParameterNames(),
            joinPoint.getArgs(),
            keyExpression
        ).toString();
    }
}

 

 

캐시 조회 / 갱신 서비스 구현

@Service
@RequiredArgsConstructor
public class ProductService {

    private final RedisTemplate<String, Object> redisTemplate;
    private final ProductRepository productRepository;

    @DistributedLock(key = "'product:' + #productId")
    public Product getProductWithCache(Long productId) {
        String cacheKey = "product:" + productId;
        Product product = (Product) redisTemplate.opsForValue().get(cacheKey);

        if (product == null) {
            // 캐시 미스 → 락 잡은 1명만 이 코드에 진입
            product = productRepository.findById(productId)
                    .orElseThrow(() -> new RuntimeException("상품 없음"));
            redisTemplate.opsForValue().set(cacheKey, product, 5, TimeUnit.MINUTES);
        }
        return product;
    }
}

 

단점

  • 결국 Lock걸린 작업이 끝날때까지 대기해야한다.

 

2-3. 캐시 웜업 (캐시 사전로딩)

별도의 배치(30초, 1분 등)를 통해서 많은 트래픽이 발생할 수 있는 캐시를 캐시가 만료되기 전에 미리 갱신해주는 방법

만료되기전 미리 갱신하므로 캐시에 있는 데이터만으로 조회하고, 트래픽 부하에 대비하고 응답지연현상도 방지할 수 있음

 

문제점

- 캐시로 넣어야하는 데이터를 미리 예측하지 못할경우 누락 가능

- 대량 데이터 갱신시, 부하 가능성 존재

🚩 카카오 프로모션 캐시 운영의 실제 이슈

카카오에서는 상품 프로모션 정보를 약 22,000개 운영 중입니다.
만약 이 모든 데이터를 캐시 웜업(warm-up) 방식으로 미리 메모리에 올리면

  • 최악의 경우 30GB라는 엄청난 캐시 공간이 소모되고
  • 전체 웜업에도 오랜 시간이 걸립니다.

 

2-4. PER (확률적 조기 갱신) 방식

PER 동작 원리

  • 캐시의 실제 TTL보다 조금 더 일찍, 일정 확률로 갱신
  • 예를 들어 TTL 10분짜리 캐시라면 9분부터 10분 사이에 들어오는 요청들은 확률적으로 캐시를 갱신함
  • 이때 "얼마나 확률적으로 조기갱신 할 것인가"를 수학적 "PER 공식"으로 계산

 

  • currentTime: 현재시간 (currentTimeMillis)
  • timeToCompute(Recompute time interval, delta): 캐시된 값을 다시 계산하는데 걸리는 시간
  • beta: 기본적으로 1. 0보다 큰 값 설정 가능. 갱신이 자주 일어나길 바랄 경우 beta를 수정하여 확률을 높일 수 있다.
  • rand(): 0과 1 사이의 랜덤 값을 반환하는 함수. log(rand())¹의 값은 0부터 마이너스 무한대까지의 범위를 가짐.
  • expiry (that is, time() + pttl)
  •  

 

@RestController
@RequiredArgsConstructor
public class ProductController {
    private final ProductService productService;

    @GetMapping("/products/{productId}")
    public ResponseEntity<ProductDto> getProduct(@PathVariable Long productId) {
        ProductDto product = productService.getProductWithPERCache(productId);
        return ResponseEntity.ok(product);
    }
}

 

 

@Service
@RequiredArgsConstructor
public class ProductService {
    private final CacheManager cacheManager;
    private final ProductRepository productRepository;

    // PER 파라미터
    private final long TIME_TO_COMPUTE_MS = 100; // DB조회 예상시간 (ms)
    private final double BETA = 1.0;

    public ProductDto getProductWithPERCache(Long productId) {
        Cache cache = cacheManager.getCache("productCache");
        ProductCacheValue cached = cache.get(productId, ProductCacheValue.class);

        long now = System.currentTimeMillis();

        if (cached != null) {
            // PER 공식 적용
            long expiry = cached.getExpiry(); // 캐시 만료 시각
            double rand = Math.random(); // 0~1
            double lhs = now + TIME_TO_COMPUTE_MS * BETA * Math.log(rand);

            // 캐시 만료 전이라도, 확률적으로 갱신
            if (lhs < expiry) {
                return cached.getProduct();
            }
            // 아니면 DB에서 새로 조회
        }

        // DB 조회 및 캐시 갱신
        ProductDto product = productRepository.findById(productId)
                .orElseThrow(() -> new EntityNotFoundException("Not Found"));

        // 캐시 저장 (예: TTL 5분)
        long newExpiry = now + 5 * 60 * 1000;
        cache.put(productId, new ProductCacheValue(product, newExpiry));

        return product;
    }

}

 

  • 캐시에 데이터가 있으면
    PER 수식 계산
    now + timeToCompute * beta * log(rand()) < expiry
    만족하면 캐시 리턴, 아니면 새로 갱신
  • 캐시에 없으면 무조건 DB 조회 후 캐시 저장
  • TTL(만료시각)은 value에 함께 기록, 실전에서는 Redis 등과 조합하면 더 견고
  • 스탬피드 현상 최소화
  • Hot data 예측/분산 효과
  • “적은 비용으로 큰 효과” (특히 대형 트래픽 서비스에서)

단점/주의

  • 확률식과 TTL/요청도에 따라 실제 조기갱신이 과도하거나 부족할 수 있음
  • 퍼포먼스 튜닝 필요

....

 

[실제 테스트 결과 분석] 미반영

1. 응답시간

 

CPU

 

Cache Hit

 

Cache Miss

 

CacheHit Rate

 

반응형
반응형

 

1. JPA란

**JPA(Java Persistence API)**는 자바에서 관계형 데이터베이스를 객체지향적으로 다룰 수 있도록 도와주는 ORM(Object-Relational Mapping) 표준 명세입니다.

즉, 복잡한 SQL 없이도 자바 객체와 데이터베이스 테이블 간의 매핑과 쿼리 작성을 가능하게 해줍니다.

Hibernate, EclipseLink, OpenJPA 등은 JPA의 구현체입니다.
Spring Data JPA는 이 위에 얹혀진 추상화 프레임워크입니다.

 

2. JPA 동작 방식 (PURE JAVA 환경)

1. Persistence가 persistence.xml을 읽고 EntityManagerFactory를 생성

  • EntityManagerFactory는 애플리케이션 전체에서 하나만 생성해야함

2. EntityManagerFactory로부터 EntityManager를 생성

  • EntityManager는 데이터베이스와의 실제 작업을 담당
  • 한 트랜잭션당 하나씩 생성해서 사용 후 반드시 close
  • 스레드간 공유 X

3. EntityTransaction을 통해 트랜잭션 시작 및 종료

[코드 예시]

persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2" xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
    <persistence-unit name="hello">
        <properties>
            <!-- 필수 속성 -->
            <property name="jakarta.persistence.jdbc.driver" value="org.h2.Driver"/>
            <property name="jakarta.persistence.jdbc.user" value="user"/>
            <property name="jakarta.persistence.jdbc.password" value="password"/>
            <property name="jakarta.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test"/>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>

            <!-- 옵션 -->
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.use_sql_comments"  value="true"/>
<!--            <property name="hibernate.hbm2ddl.auto" value="create" />-->
        </properties>
    </persistence-unit>

</persistence>

 

memeber 엔티티

package hellojpa;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Entity
public class Member {

    @Id
    private Long id;
    private String name;

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setId(Long id) {
        this.id = id;
    }
}

 

main.java

    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            Member member = new Member();
            member.setId(1L);
            member.setName("HelloA");
            em.persist(member);

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }

        emf.close();
    }

 

[Spring boot 환경 예시]

- persitence.xml에 설정한 내용을 yaml파일로 쉽게 대체 가능하다.

- trasaction 작업을 어노테이션으로 대체 가능하다.

 

application.yaml

spring:
  datasource:
    url: jdbc:h2:tcp://localhost/~/test
    driver-class-name: org.h2.Driver
    username: sa
    password: 
  
  # 기존xml을 yaml 설정으로 대체가능
  jpa:
    hibernate:
      ddl-auto: update 
    show-sql: true
    properties:
      hibernate:
        format_sql: true

 

 

service

@Service
public class MemberService {
    @PersistenceContext
    private EntityManager em;

    @Transactional
    public void createMember() {
        Member member = new Member();
        member.setId(1L);
        member.setName("SpringBootMember");
        em.persist(member);
    }
}
반응형

'프로그래밍 > JPA' 카테고리의 다른 글

[JPA] 필드와 컬럼 매핑  (1) 2025.06.19
[JPA] 데이터베이스 스키마 자동 생성  (0) 2025.06.19
[JPA] 객체와 테이블 매핑  (0) 2025.06.19
[JPA] JPA의 영속성 관리2  (0) 2025.06.10
[JPA] JPA 의 영속성 관리  (0) 2025.06.10
반응형

1. RestDoc이란

  • 테스트 코드 기반으로 Restful API 문서를 돕는 도구입니다.
  • Asciidoctor를 이용해서 HTML 등등 다양한 포맷으로 문서를 자동으로 출력할 수 있습니다.
  • RestDocs의 가장 큰 장점은 테스트 코드 기반으로 문서를 작성한다는 점입니다.
  • API Spec과 문서화를 위한 테스트 코드가 일치하지 않으면 테스트 빌드를 실패하게 되어 테스트 코드로 검증된 문서를 보장할 수 있습니다.

장점

  • 테스트 코드가 통과해야만 문서가 생성되므로 API 문서의 신뢰도가 높다
  • API 문서 주석이 비즈니스 코드에 묻어나지 않는다.(swagger의 흔한 단점)
  • 커스텀이 자유롭다

 

단점

  • API 테스트 코드가 추가되면 문서 조각(Asciidoc snippet)을 합쳐주는 작업이 필요하다.
  • Swagger 처럼 직접 API 테스트를 해볼 수 없고, 문서 가독성이 떨어진다. (디자인이 구림)
  • 설정 복잡함

테스트 코드 예시

    private MockMvc mockMvc;

    @BeforeEach
    void setUp(WebApplicationContext context, RestDocumentationContextProvider restDocumentation) {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
                .apply(MockMvcRestDocumentation.documentationConfiguration(restDocumentation))
                .build();
    }

    @Test
    void order() throws Exception {
        mockMvc.perform(get("/api/v1/order")
                        .param("test", "testValue")
                )
                .andExpect(status().isOk())
                .andDo(document("order Test",
                        preprocessRequest(prettyPrint()),
                        preprocessResponse(prettyPrint()),
                        resource(ResourceSnippetParameters.builder()
                                .tag("User API")
                                .summary("소셜 로그인 API")
                                .queryParameters(
                                        parameterWithName("test").description("테스트 파라미터"))
                                .responseFields(
                                        fieldWithPath("test").type(STRING).description("테스트 값"))
                                .requestSchema(Schema.schema("FormParameter-socialLogin"))
                                .responseSchema(Schema.schema("UserResponse.Login"))
                                .build())));
    }

 

RestDoc 결과 화면

2. Swagger란

  • Rest API를 편리하게 문서화 해주는 도구입니다.
  • 제 3의 사용자는 문서에서 직접 API 호출 테스트를 진행할 수 있습니다.

장점

  • 실시간으로 문서에서 API를 호출해보고, 응답 결과를 즉시 확인할 수 있음
  • 어노테이션으로 간편하게 API문서를 만들 수 있음 (장점이자 단점)
  • UI/UX 우수함
  • 비교적 설정 간단

 

단점

  • 비즈니스로직에 주석이 많아져서, 복잡해짐

  • 코드와 문서 사이에 불일치가 발생할 수 있음 (어노테이션동작이기 때문에)

3. RestDoc + Swagger

RestDoc의 장점과, Swagger의 장점만 모두 사용해보자

 

동작 개념

  1. RestDoc을 이용했던 것 처럼 테스트 코드를 통해 docs 문서를 생성
  2. docs 문서를 OpenAPI3 스펙으로 변환 (springdoc-openapi 라이브러리를 통해)
  3. 만들어진 OpenAPI3 스펙을 SwaggerUI로 생성
  4. 생성된 SwaggerUI를 static 패키지에 복사 및 정적리소스로 배포

 

적용방법

gradle

buildscript {
    ext {
        restdocsApiSpecVersion = '0.18.2'
    }
}

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.4.4'
    id 'io.spring.dependency-management' version '1.1.7'
    // REST Docs 테스트 결과(문서조각)을 바탕으로 openapi(openapi3.yaml)을 자동 생성하는 플러그인
    id 'com.epages.restdocs-api-spec' version "${restdocsApiSpecVersion}"
}

group = 'com.run'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    // rest docs 기본 라이브러리
    testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
    // rest docs와 openapi spec 플러그인을 연결해주는 Adapter
    testImplementation "com.epages:restdocs-api-spec-mockmvc:${restdocsApiSpecVersion}"
}

// yaml파일 생성
openapi3 {
    server = 'http://localhost:8080'
    title = "RF API"
    description = "RF API description"
    version = "0.1.0"
    format = "yaml"
}

// openapi.yaml 생성 + 정적 리소스 위치로 복사
tasks.register('copyOasToSwagger', Copy) {
    from layout.buildDirectory.file("api-spec/openapi3.yaml")
    into layout.projectDirectory.dir("src/main/resources/static/swagger-ui") // JAR 내 포함될 정적 리소스 경로
}

// 빌드 전에 swagger 파일 복사
tasks.named('build') {
    dependsOn("openapi3", "copyOasToSwagger")
}

// 수동 실행용 문서 생성 task (test + openapi3 + copy까지)
tasks.register('generateSwaggerDoc') {
    group = "documentation"
    description = "Generate OpenAPI YAML and copy to static folder"
    dependsOn("test", "openapi3", "copyOasToSwagger")
}

tasks.named('test') {
    useJUnitPlatform()
}

 

결과 화면

http://localhost:8080/docs/index.html 에서 확인 가능

 

 

 

 

반응형
반응형

책재목이 흥미로워서 읽게 되었다

 

내가 진짜 좋아하는 일만 하고 살 수 있을까? 세상에는 하기 싫은 일, 혹은 해야만 하는 일 다양한 것들이 있는데 어떻게 그럴 수 있을까 라는 생각에 책을 보게 되었다.

 

대부분의 사람들은 내가 이 일을 왜 하는지 모르고 그냥 한다. 또는 사실 내가 이걸 좋아하고 하고싶어한다고 믿는다. 예를 들면 사람들은 무언가를 원한다고 말하지만 행동으로는 옮기지 못한다. 이건 진정한 나의 가치관이 아니다. 내가 하고 싶어 하는 일이 아니라는 것이다.

 

그렇다면 진짜 좋아하는 일을 하고 살기위해서는 어떻게 해야 할까?

- 현재 지향적, 미래 지향적 모든 시간관이 필요하다

사람들의 가치관은 초점을 바꾼다. 그런 의미에서 모두 필요하다 예를 들어 인생을 즐기기 위해선 현재 지향적이어야 하고 또한 너무 현재지향적이라면 절제하지 못하고 다소 성급한 행동으로 인해 미래를 내다볼 수 없게 된다. 어찌 보면 당연한 말이지만 가장 간단한 말이라 적어보았다.

 

-  모든 것들에 '노'라고 답하기

우리가 '노'라고 말할 줄 알게 되면 '예스'는 더 강력해진다고 한다. 예를 들어 작은 것들을 거절하지 못해 가장 중요한 일을 하지 못하면 어떻게 될까? 이게 이 구절의 핵심이다. 제일 중요한 것 이외에 전부 거절하자

 

- 나쁜 마음 상태에서 벗어나는 방법

나쁜 마음을 가지면 상황이 점점 나빠지는 것은 당연하다. 빠르게 이 상태에서 벗어나는 것이 중요하다.

스스로 뭐가 문제인지 자문해 보고, 나를 관찰하고 행동을 뒤로 미루도록 한다. 기준을 높여 위대하지 않은 일부터 거절하도록 하고, 그 위대한일에는 집중하도록 하여 나쁜 마음에서 벗어나자

 

- 좋아하는 일은 간단해 보인다

책에서 달리기를 좋아하는 사람과 싫어하는 사람을 예시로 설명하는데 쉽게 얘기하면 그냥 좋아하는 일은 더 간단하고 쉬워 보인다는 것이다.

 

- 다 내 잘못이다

누군가에게 그들 잘못이라고 느끼는 건 인간의 본성이다. 하지만 모두 내 잘못이라고 생각해 보면 어떨까? 오히려 기분이 좋아지는 경우가 있을 것이다. 내가 부당한 취급을 당했다고 느끼는 일도 적어지고, 나는 그 실수에서 배움을 얻을 수 있는 사람이 되었다.

좋은 말이지만 실제로 이렇게 할 수 있는 일이 얼마나 될까 생각이 든다.

 

- 나에게는 당연하지만 누군가에게는 놀라울 수 있다.

나에게는 당연하고 쉬운 일이 누군가에게는 놀랍고 어려운 일이 될 수 있다. 작곡가들을 예시로 책에선 설명하고 있다. 좋은 노래 같지 않아서 공유하지 않았으나 사실 대중은 너무나 좋아했던 노래 이것처럼 당연하다고 공유하지 않는 건 바보 같은 일이다.

 

책에선 66가지를 통해서 내가 진짜 좋아하는 일을 하고 사는 법을 깨닫게 해 준다. 책을 읽기 전엔 어떤 방법으로 내가 좋아하는 일을 찾을 수 있는지를 제시해 주는 책일 것을 기대하고 봤으나 내 생각과는 조금 다른 책이었다.

당연한 이야기도 꽤 많았고, 가볍게 읽기 좋은 것 같다.

반응형

'일상 > 독서' 카테고리의 다른 글

독서 - 이기적 유전자  (4) 2024.12.02
독서 - 몰입  (1) 2024.11.24
독서 - 요즘 우아한 개발  (2) 2024.11.24
독서 - 요즘 팀장은 이렇게 일합니다.  (2) 2024.11.20
경제기사 궁금증 300문 300답  (8) 2024.11.17
반응형

이기적 유전자라는 책을 읽어보았다.

 

"인간을 포함한 생명체는 DNA 또는 유전자에 의해 창조된 생존 기계에 불과하다"라는 말을 저자는 하고 있다. 정리하면 살아남기 위해 우리는 경쟁해서 생존하는 유전자만 살아남게 되는데 이러한 유전자는 몸을 통해 다음세대로 이어지는데 즉 우리는 생존기계라고 표현한다. 이러한 생각을 가지고 이 책을 시작하면 준비가 되었다고 본다.

 

책에서는 최초에 물, 암모니아 이산화탄소 같은 물질들이 전기, 자외선 등으로 다른 물질로 변하는 경우가 있다는 사실을 제공한다. 

즉 A라는 물질이 계속 A로 복제하지않고 돌연변이인 B로 복제되는 경우가 있다는 것이다.

이러한 자기복제자는 "생존 본능"만 가지고 있기 때문에, 서로 모여 경쟁하거나 모여서 세포를 이루고 이는 또 모여서 인간이나 꽃 같은 다른 생물이 된다. 이는 즉 생존을 하기 위해 날 지켜주는 기계라고 책에서는 설명한다.

이러한 생존본능은 동물들을 책에서 예시로들며 설명한다. 동물의 먹이사슬은 이미 생존기계에 프로그래밍되어있을 뿐이라고.

 

책에서는 이기적 유전자와, 이타적인 유전자를 또 설명하는데 굉장히 복잡하다. 근연도를 통한 점수를 책에서는 매기는가 하면, 유전자가 어떤 식으로 전달되었는지 혈연선택에 의한 설명등 복잡하게 되어있는데 전부 설명할 수 없으므로 기억나는 부분을 정리하면 부모와 자식의 관계 또한 이기적 유전자, 이타적 유전자로 설명할 수 있다. 부모가 자식을 보살피는 것은 이타적인 유전자일까? 책에서는 이기적인 유전자라고 한다. 자식을 보살펴서 유전자를 잘 전달하는 것 또한 부모가 유전자를 후손에게 살아남 겨 전달하기 위한 기계적인 방법 중 하나이기 때문이다.

 

책에서는 또한 형제 관계는 경쟁관계로 표현한다. 자식 입장에서 부모의 공급은 한정적일 테니, 자신이 더 살아남기 위해 노력해야할 것이고 이 또한 이기적인 유전자로, 자식들은 서로 경쟁관계에 있다는 의미이다.

 

그렇다면 이타적인 유전자는 있을까? 협력이다. 공생이라고 설명하고 이해하면 좋을 것 같다. 책에서는 집단(무리), 공생관계 같은 것을 예시로 들고있다.

예를 들어 동물이 무리 지어 살게 될 때 먹이를 나눠야 해서 공급량은 줄겠지만, 함께 더 큰 먹이를 사냥한다면 이득이 되지 않겠는가? 즉 이타적인 유전자 또한 이기적인 유전자에서 파생된 내용이라고 책에서는 설명하고 있다.

 

 

마지막으로 인간의 경우에는 전부 이기적인 유전자일까? 남들을 위해 희생하는 사람들이 있지 않은가? 

책에서는 이러한 행위를 이기적인 유전자에 반역하는 행위라고 한다. 즉 생존본능이 아닌 정말 말 그대로 이타적인 행동을 한다는 의미이다.

모든 기계들은 이렇게 프로그래밍되어있지만 유일하게 인간만이 자기 복제자의 폭정 즉 이기적인 유전자에 반역할 수 있다고 한다.

 

책에서는 우리가 남에게 이타적으로 하는 행위 또한 생존 때문이라고 설명한다. 흥미로운 부분이 꽤 많았지만 너무 내용이 어려워서 가볍게 읽어볼 만한 책은 아닌 것 같다.

 

 

반응형
반응형

무언가에 몰입하는 것은 엔트로피의 관점에서 설명할 수 있다.

엔트로피 법칙은 모든현상은 언제나 전체 엔트로피가 증가하는 방향으로 진행된다. 여기서 엔트로피는 무질서한 정도로 이해하면 쉽다. 따라서 우리는 몰입보다 집중력을 흐트러트리는 게 더 쉽다. 

몰입은 상당 기간 집중을 유지하는 상태로, 이때 의식의 엔트로피는 낮다. 이와 반대로 산만한 상태에서의 의식의 엔트로피가 높다. 그럼 인간은 영원히 몰입할 수 없을까?

자연의 전체 엔트로피는 항상 증가하는 방향으로 진행되지만 인간을 포함한 생명체가 생명현상을 유지하는 것은 엔트로피를 낮추는 활동이다. 

즉 고도의 집중된 상태로 우리는 몰입할 수 있다.

 

몰입은 어떤식으로 정의할 수 있을까?

- 과제의 난이도 하, 내 실력 하 = 무관심

- 과제의 난이도 하, 내 실력 중 = 권태 (잡일)

- 과제의 난이도 하, 내실력 상 = 느긋함

- 과제의 난이도 중, 내 실력 하 = 걱정 (언쟁, 격론)

- 과제의 난이도 중, 내 실력 상 = 자신

- 과제의 난이도 상, 내 실력 하 = 업무, 공부

- 과제의 난이도 상, 내실력 중 = 배움, 학습

- 과제의 난이도 상, 내실력 상 = 몰입 (취미)

 

즉 현재 나의 상태, 과제의 상태를 보고 몰입에 도달하기 위한 방향을 정할 수 있다.

 

그럼 실제 몰입은 어떻게 할 수 있을까?

1. 앞서 작성한것 처럼 문제를 설정한다.

2. 몰입할 수 있는 환경을 확보한다 (기간 1주일 이상)

3. 불필요한 외부 정보 차단 (스마트폰, TV, SNS 등)

해당 과정을 거쳐 열심히 일하는 것이 아닌, 열심히 생각하는 몰입을 진행한다.

먼저 설정한 문제를 천천히 분석하고 생각한다. 이때 생각의 속도는 의식적으로 느리게 하고 생각에 진전이 없어도 집중도는 조금씩 올라가기에 계속 진행한다. 첫날이 가장 어렵기 때문에 여유를 갖고 천천히 생각하는 습관을 들이도록 한다.

둘째 날도 동일하게 의식적인 노력을 통해 생각을 이어간다. 하지만 첫날보다는 덜 지루할 것이고, 첫날보다 더 좋은 아이디어가 떠오를 것이다.

셋째 날은 주어진 문제를 생각하기 훨씬 쉬워진다. 다른 생각을 잠시 하다가 돌아와도 빠르게 집중하기에 수월해진다. 이때가 약 70~80% 몰입한 상태라고 한다.

 

이러한 생각하는 몰입을 통해서 우리는 문제해결에 대한 새로운 아이디어를 찾을 수 있고, 내 안에 숨겨진 능력을 최대한 발휘할 수 있을 것이다.

반응형

+ Recent posts