기본적으로 @Transactional, @Cacheable, @Async 등의 애너테이션은 런타임에 동작하는 Spring AOP를 기반으로 동작합니다. Spring AOP가 제공하는 JDK Dynamic Proxy, CGLIB 방식 모두 타깃이 구현하는 인터페이스나 구체 클래스를 대상으로 프록시를 만들어서 타깃 클래스의 메서드 수행 전후에 횡단 관심사에 대한 처리를 할 수 있습니다.

Spring은 빈 생성시, 해당 빈에 AOP 애너테이션이 있는지 검사하고, 있다면 프록시 객체를 생성하여 빈을 대체합니다. AOP 적용 대상인 클래스의 경우, 즉, @Transactional과 같은 AOP 애너테이션이 하나라도 선언된 클래스는 프록시로 감싸집니다.

JDK Dynamic Proxy의 경우 타깃 클래스가 구현하는 인터페이스를 기준으로 프록시를 생성하여public 메서드만 AOP 적용 가능합니다. CGLIB 방식의 경우 인터페이스를 구현하지 않는 클래스를 상속하여 프록시를 생성하고, private을 제외한 public, protected, package-private 메서드에 AOP 적용 가능합니다.

@Slf4j  
@RequiredArgsConstructor  
@Service  
public class SelfInvocation {  
  
    private final MemberRepository memberRepository;  
  
    public void outerSaveWithPublic(Member member) {  
        saveWithPublic(member);  
    }  
  
    @Transactional  
    public void saveWithPublic(Member member) {  
        log.info("call saveWithPublic");  
        memberRepository.save(member);  
        throw new RuntimeException("rollback test");  
    }  
  
    public void outerSaveWithPrivate(Member member) {  
        saveWithPrivate(member);  
    }  
  
    @Transactional  
    private void saveWithPrivate(Member member) {  
        log.info("call saveWithPrivate");  
        memberRepository.save(member);  
        throw new RuntimeException("rollback test");  
    }  
}

public interface MemberRepository extends JpaRepository<Member, Long> {  
}
@SpringBootTest  
class SelfInvocationTest {  
  
    private static final Logger log = LoggerFactory.getLogger(SelfInvocationTest.class);  
  
    @Autowired  
    private SelfInvocation selfInvocation;  
  
    @Autowired  
    private MemberRepository memberRepository;  
  
    @AfterEach  
    void tearDown() {  
        memberRepository.deleteAllInBatch();  
    }  
  
    @Test  
    void aopProxyTest() {  
        // @Transactional 애너테이션을 가지고 있으므로, 빈이 Proxy 객체로 대체되어 주입된다.  
        assertThat(AopUtils.isAopProxy(selfInvocation)).isTrue();  
        // interface를 구현하지 않은 클래스이므로 CGLIB Proxy가 생성된다.  
        assertThat(AopUtils.isCglibProxy(selfInvocation)).isTrue();  
    }  
  
    @Test  
    void outerSaveWithPublic() {  
        Member member = new Member("test");  
  
        try {  
            selfInvocation.outerSaveWithPublic(member);  
        } catch (RuntimeException e) {  
            log.info("catch exception");  
        }  
  
        List<Member> members = memberRepository.findAll();  
        // self invocation 문제로 인해 트랜잭션이 정상 동작하지 않음.  
        // 예외 발생으로 인한 롤백이 동작하지 않고 남아있음.
    
      assertThat(members).hasSize(1);  
    }  
  
    @Test  
    void outerSaveWithPrivate() {  
        try {  
            selfInvocation.outerSaveWithPrivate(new Member("test"));  
        } catch (RuntimeException e) {  
            log.info("catch exception");  
        }  
  
        List<Member> members = memberRepository.findAll();  
  
        // self invocation 문제로 인해 트랜잭션이 정상 동작하지 않음.  
        // 예외 발생으로 인한 롤백이 동작하지 않고 남아있음.        assertThat(members).hasSize(1);  
    }  
  
    @Test  
    void saveWithPublic() {  
        Member member = new Member("test");  
  
        try {  
            selfInvocation.saveWithPublic(member);  
        } catch (RuntimeException e) {  
            log.info("catch exception");  
        }  
  
        List<Member> members = memberRepository.findAll();  
  
        // 외부에서 프록시 객체를 통해 메서드가 호출되었기 때문에 트랜잭션 정상 동작, 롤백 성공.  
        assertThat(members).hasSize(0);  
    }  
}
Spring AOP는 외부에서 프록시 객체를 통해 메서드가 호출될 때만 AOP 어드바이스(트랜잭션 관리)를 적용합니다. 같은 클래스 내에서 메서드를 호출하면, 프록시를 거치지 않고 직접 호출되므로 트랜잭션 어드바이스가 적용되지 않습니다.

이를 해결하기 위해서는 자기 자신을 프록시로 주입 받아 프록시를 통해 메서드를 호출하거나, 별도의 클래스로 분리하거나, AspectJ를 이용하는 방법이 있습니다. AspectJ를 사용하면 동일 클래스 내에서의 메서드 호출에도 AOP 어드바이스를 적용할 수 있습니다.

자기 자신을 프록시로 주입 받는 방법
@Slf4j  
@RequiredArgsConstructor  
@Service  
public class SelfInvocation {  
  
    private final MemberRepository memberRepository;  
    private final SelfInvocation selfInvocation;  
  
    public void outerSaveWithPublic(Member member) {  
        selfInvocation.saveWithPublic(member);  
    }  
  
    @Transactional  
    public void saveWithPublic(Member member) {  
        log.info("call saveWithPublic");  
        memberRepository.save(member);  
        throw new RuntimeException("rollback test");  
    }
    ...
}
이 방법은 순환 의존성 문제를 일으킬 수 있어 권장되지 않습니다.

별도의 클래스로 분리하는 방법
@Slf4j  
@RequiredArgsConstructor  
@Service  
public class TransactionService {  
  
    @Transactional  
    public void outer() {  
        log.info("call outer");  
        logCurrentTransactionName();  
        logActualTransactionActive();  
        inner();  
    }  
  
    @Transactional(propagation = Propagation.REQUIRES_NEW)  
    public void inner() {  
        log.info("call inner");  
        logCurrentTransactionName();  
        logActualTransactionActive();  
    }  
  
    private void logActualTransactionActive() {  
        boolean actualTransactionActive = TransactionSynchronizationManager.isActualTransactionActive();  
        log.info("actualTransactionActive = {}", actualTransactionActive);  
    }  
  
    private void logCurrentTransactionName() {  
        String currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName();  
        log.info("currentTransactionName = {}", currentTransactionName);  
    }  
}

// 로그
// call outer  
// currentTransactionName = server.transaction.TransactionService.outer  
// actualTransactionActive = true  
// call inner  
// currentTransactionName = server.transaction.TransactionService.outer  
// actualTransactionActive = true
outer가 inner 메서드를 호출하는데, outer의 propagation 속성은 REQUIRED, inner는 REQUIRES_NEW로 서로 다른 트랜잭션으로 분리되어야 합니다. 하지만, 로그를 보면 동일한 outer의 트랜잭션에 속해있습니다. 이처럼 트랜잭션 전파 속성이 다른 두 메서드가 동일한 클래스 내부에서 self invocation 호출하면 의도대로 동작하지 않습니다. 이 때 outer와 inner 메서드를 각각 다른 클래스로 분리하여 호출하면 해결할 수 있습니다.

// OuterTransactionService
@Slf4j  
@RequiredArgsConstructor  
@Service  
public class OuterTransactionService {  
  
    private final InnerTransactionService innerTransactionService;  
  
    @Transactional  
    public void outer() {  
        log.info("call outer");  
        logCurrentTransactionName();  
        logActualTransactionActive();  
        innerTransactionService.inner();  
    }  
  
    private void logActualTransactionActive() {  
        boolean actualTransactionActive = TransactionSynchronizationManager.isActualTransactionActive();  
        log.info("actualTransactionActive = {}", actualTransactionActive);  
    }  
  
    private void logCurrentTransactionName() {  
        String currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName();  
        log.info("currentTransactionName = {}", currentTransactionName);  
    }  
}

// InnerTransactionService
@Slf4j  
@RequiredArgsConstructor  
@Service  
public class InnerTransactionService {  
  
    @Transactional(propagation = Propagation.REQUIRES_NEW)  
    public void inner() {  
        log.info("call inner");  
        logCurrentTransactionName();  
        logActualTransactionActive();  
    }  
  
    private void logActualTransactionActive() {  
        boolean actualTransactionActive = TransactionSynchronizationManager.isActualTransactionActive();  
        log.info("actualTransactionActive = {}", actualTransactionActive);  
    }  
  
    private void logCurrentTransactionName() {  
        String currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName();  
        log.info("currentTransactionName = {}", currentTransactionName);  
    }  
}

// 로그
// call outer  
// currentTransactionName = server.transaction.OuterTransactionService.outer  
// actualTransactionActive = true  
// call inner  
// currentTransactionName = server.transaction.InnerTransactionService.inner  
// actualTransactionActive = true
이처럼 각각 프록시를 생성할 수 있게 두 클래스로 분리하면 AOP 어드바이스가 적용되어 의도한 대로 독립적인 트랜잭션을 시작할 수 있게 됐습니다.

LIST


낙관적 업데이트는 성공적인 상태 업데이트가 이뤄질 거라는 가정 하에 서버 응답 이전에 UI를 미리 업데이트하는 방법입니다. 사용자 요청을 서버가 성공적으로 처리할 거라고 미리 예상하고, UI를 즉각적으로 변경해서 사용자에게 빠른 반응을 보여줍니다.

낙관적 업데이트의 대표적인 예시로 좋아요 기능을 들 수 있습니다. 예를 들어, 사용자가 좋아요 버튼을 클릭하면 서버 응답을 기다리지 않고, 화면에 바로 좋아요 클릭에 대한 상태를 보여주는 것입니다. 서버 응답이 성공적으로 돌아오면 그대로 두고, 혹시나 실패하면 UI에서 해당 좋아요 상태를 다시 해제하거나 오류 메시지를 보여주는 방식입니다.

낙관적 업데이트의 장점은, 서버 응답 속도와 관계 없이 즉각적인 피드백을 제공해서 사용자들이 시스템을 빠르게 쓸 수 있다는 점입니다. 특히 네트워크 상태가 좋지 않거나 응답 시간이 길어도 사용자 경험에는 영향을 덜 미치게 됩니다.

다만, 서버에서 오류가 발생하면 잠시동안 화면에 잘못된 정보가 표시될 수 있습니다. 따라서 이 경우를 대비한 오류 핸들링(롤백) 로직을 같이 설계해야 하는 주의점이 있습니다.

좋은 사용성을 위해서는 낙관적 업데이트를 가능한 한 많이 적용하는 것이 좋겠네요? 🤔
많은 곳에 낙관적 업데이트를 적용하는 게 항상 좋은 건 아닙니다. 낙관적 업데이트는 요청이 성공할 가능성이 높고, 사용자 경험을 즉시 개선하는 데 큰 장점이 있을 때 사용하는 게 적합합니다.

예를 들어 결제나 거래 내역과 같이 중요한 데이터를 다루는 경우에는 낙관적 업데이트가 오히려 사용자 경험을 저해할 수 있습니다. 낙관적 업데이트를 적용했을 때, 요청에 실패한다면 민감도 높은 정보가 순간적으로 잘못 표시되면서 사용자 경험을 크게 저해할 수 있기 때문입니다.

또한 네트워크 환경이 불안정한 경우에는 요청에 대한 실패율이 높아지기 때문에 잦은 롤백이 발생할 수 있습니다. 이 경우 역시 사용자 경험을 저해할 수 있기 때문에 오히려 서버 응답을 기다리는 것이 더 나은 판단일 수 있습니다.

LIST


포워드 프록시(Forward Proxy)
포워드 프록시는 주로 클라이언트 측에 위치하여, 사용자가 인터넷에 접근할 때 중개자 역할을 합니다.

예를 들어, 회사 내부 네트워크에서 근무하는 직원이 외부 웹사이트에 접속하려고 할 때, 포워드 프록시 서버를 통해 요청이 전달됩니다. 이 과정에서 사용자의 실제 IP 주소는 숨겨지고, 프록시 서버의 IP 주소가 대신 사용됩니다.

포워드 프록시의 핵심 기능 중 하나는 익명성 제공입니다. 사용자의 실제 IP를 숨김으로써 개인정보 보호와 보안 측면에서 큰 장점을 제공합니다.

또한 캐싱을 통해 네트워크 성능을 향상시킵니다. 자주 요청되는 웹 페이지나 파일을 프록시 서버에 저장해두면, 동일한 요청이 다시 들어올 때 빠르게 응답할 수 있어 네트워크 대역폭을 절약할 수 있습니다.

이와 함께 보안 강화 기능도 포워드 프록시의 중요한 역할 중 하나입니다. 악성 웹사이트나 불법적인 콘텐츠에 대한 접근을 차단하여 네트워크 보안을 강화하고, 바이러스나 악성 코드의 유입을 예방할 수 있습니다.

리버스 프록시(Reverse Proxy)
리버스 프록시는 서버 측에 위치하여 외부에서 들어오는 클라이언트의 요청을 내부 서버로 전달하는 역할을 합니다.

리버스 프록시의 핵심 기능 중 하나는 로드 밸런싱입니다. 다수의 백엔드 서버로 트래픽을 분산시켜 서버 과부하를 방지하고, 서비스의 고가용성을 유지할 수 있습니다.

또한 외부에서 직접 백엔드 서버에 접근하지 못하게 하여 DDoS 공격이나 해킹 시도로부터 서버를 보호할 수 있습니다.

SSL 종료는 리버스 프록시의 또 다른 중요한 기능입니다. SSL/TLS 암호화를 리버스 프록시에서 처리함으로써 백엔드 서버의 부담을 줄이고, 중앙에서 인증서를 관리할 수 있습니다.

또한, 리버스 프록시는 캐싱 및 콘텐츠 최적화 기능을 통해 정적 콘텐츠를 캐싱하여 응답 속도를 향상시키고 서버 부하를 줄일 수 있습니다.

LIST

Flexbox Grid는 페이지에서 레이아웃을 구성할 때 자주 사용되는 CSS 속성입니다. 두 속성 모두 화면 요소를 배치하고 정렬하는 데에 사용되지만 몇가지 차이점이 존재합니다.

첫번째로 Flexbox는 1차원 레이아웃이지만, Grid는 2차원 레이아웃입니다.
Flexbox 1차원 레이아웃 속성으로, row 또는 column 중 하나를 기준으로 요소를 정렬하고 배치하는 데 최적화되어 있습니다. 주로 행이나 열 중 하나의 방향으로 정렬해야 할 때 유용하며, 복잡한 행과 열을 모두 포함하는 레이아웃에서는 다소 한계가 있습니다.
반면 Grid 2차원 레이아웃 속성으로, 행과 열을 모두 사용해 요소를 배치할 수 있습니다. 따라서 복잡한 레이아웃을 구성하거나, 웹페이지의 전체적인 구조를 잡는 데 적합합니다.

두번째는 사용 목적의 차이입니다.
Flexbox 콘텐츠 중심으로, 콘텐츠가 추가되거나 줄어들 때 유연하게 대처하기 좋습니다. 예를 들어, 수평 또는 수직 방향으로 콘텐츠를 정렬하고 간격을 조절하는 데 유용하며, 버튼 그룹, 내비게이션 바 등 한 줄의 콘텐츠가 주가 되는 구성에 적합합니다.
Grid 레이아웃 중심으로 페이지 구조를 구성하는 데 최적화되어 있습니다. 예를 들어, 카드 레이아웃, 갤러리 형식 등 명확하게 구분된 영역을 기반으로 레이아웃을 구성할 때 Grid가 효과적입니다.

마지막으로는 기본 동작의 차이입니다.
Flexbox에서는 주로 요소가 컨테이너의 크기나 위치에 맞춰 자동으로 정렬됩니다. Flexbox의 justify-content와 align-items 속성을 사용해, 주 축 방향으로 요소들을 배치하고 여백을 조절할 수 있습니다.
Grid 행과 열을 사전에 정의하고 그 격자(grid cell)에 요소를 배치하는 방식입니다. Grid에서는 grid-template-rows, grid-template-columns와 같은 속성으로 행과 열의 크기를 정의하고, 각 요소의 위치를 세밀하게 설정할 수 있습니다.

LIST

Phantom Read란 무엇인가요?

Phantom Read는 트랜잭션이 동일한 조건의 쿼리를 반복 실행할 때, 나중에 실행된 쿼리에서 처음에는 존재하지 않았던 새로운 행이 나타나는 현상을 말합니다. 이는 주로 읽기 일관성(Read Consistency) 을 유지하는 과정에서 발생할 수 있는 문제로, 데이터의 삽입이나 삭제가 다른 트랜잭션에 의해 이루어질 때 발생합니다.

-- 트랜잭션 A 시작
START TRANSACTION;

-- 트랜잭션 A 첫 번째 조회
SELECT * FROM orders WHERE amount > 150;

-- 트랜잭션 B 시작
START TRANSACTION;

-- 트랜잭션 B 새로운 행 삽입
INSERT INTO orders (customer_id, amount) VALUES (4, 250);

-- 트랜잭션 B 커밋
COMMIT;

-- 동일한 조건으로 트랜잭션 A 두 번째 조회시, 트랜잭션 A의 첫 번째 조회에는 존재하지 않던, 트랜잭션 B에서 삽입된 새로운 행이 함께 조회된다.
-- 단, MVCC를 지원하는 경우 해당 케이스에서 팬텀 리드가 발생하지 않는다.
SELECT * FROM orders WHERE amount > 150;

갭락(Gap Lock)이란?

갭 락은 특정 인덱스 값 사이의 공간을 잠그는 락입니다. 기존 레코드 간의 간격을 보호하여 새로운 레코드의 삽입을 방지합니다. 갭 락은 범위 내에 특정 레코드가 존재하지 않을 때 적용됩니다. 트랜잭션이 특정 범위 내에서 데이터의 삽입을 막아 팬텀 읽기(Phantom Read) 현상을 방지합니다. 예를 들어, 인덱스 값 10과 20 사이의 갭을 잠그면 이 범위 내에 새로운 레코드 15를 추가할 수 없습니다.

-- id 1, 3, 5가 저장된 orders 테이블

-- 트랜잭션 A 시작
START TRANSACTION;

-- 트랜잭션 A 1-3과 3-5 사이의 갭과 3 레코드 락 설정(넥스트키 락)
SELECT * FROM orders WHERE orders_id BETWEEN 2 AND 4 FOR UPDATE;

-- 트랜잭션 B 시작
START TRANSACTION;

-- 트랜잭션 B가 id 4에 데이터 삽입 시도 시, 갭락으로 인해 삽입이 차단되어 대기
INSERT INTO orders (orders_id, orders_amount) VALUES (4, 200);
...

넥스트키 락(Next-Key Lock)이란?

넥스트키 락 레코드 락 갭락을 결합한 형태로, 특정 인덱스 레코드와 그 주변의 갭을 동시에 잠그는 락입니다. 이를 통해 레코드 자체의 변경과 함께 그 주변 공간의 변경도 동시에 제어할 수 있습니다.

넥스트키 락은 특정 레코드와 그 주변 공간을 잠그기 때문에, 다른 트랜잭션이 새로운 레코드를 삽입하여 팬텀 리드를 발생시키는 것을 방지합니다.

orders_idorders_amount

1 100
2 200
3 300
-- 트랜잭션 A 시작
START TRANSACTION;

-- 트랜잭션 A amount = 200인 orders_id = 2 레코드에 대한 레코드 락과 1-2, 2-3에 대한 갭락을 동시에 잠금으로써 넥스트키 락을 설정
SELECT * FROM orders WHERE orders_amount = 200 FOR UPDATE;

-- 트랜잭션 B 시작
START TRANSACTION;

-- 트랜잭션 B orders_id = 4, orders_amount = 200인 레코드 삽입 시도 시, 넥스트키 락으로 인해 차단되어 대기
INSERT INTO orders (orders_id, order_amount) VALUES (4, 200);
...

갭락과 넥스트키 락을 통한 팬텀 리드 방지 메커니즘

트랜잭션 A가 특정 범위의 데이터를 조회할 때, 해당 범위에 대해 갭락 또는 넥스트키 락을 설정합니다. 락이 설정된 범위 내에서는 트랜잭션 B가 새로운 레코드를 삽입하거나 기존 레코드를 수정하는 것이 차단됩니다. 따라서, 트랜잭션 A가 다시 동일한 조건으로 조회를 수행하더라도, 트랜잭션 B에 의해 새로운 데이터가 삽입되지 않아 팬텀 리드가 발생하지 않습니다.

LIST

대표적인 동시성 제어 방식으로 MVCC(Multi-Version Concurrency Control) 와 Lock-Based Concurrency Control이 있습니다.

MVCC(Multi-Version Concurrency Control)
MVCC는 데이터의 여러 버전을 유지하여 트랜잭션이 동시에 데이터를 읽고 쓸 수 있도록 하는 방식입니다. 각 트랜잭션은 자신만의 일관된 스냅샷을 기반으로 데이터를 읽어, 다른 트랜잭션의 변경 사항에 영향을 받지 않습니다.

데이터의 각 버전을 유지하여 읽기 작업이 쓰기 작업과 독립적으로 이루어질 수 있습니다. 트랜잭션은 시작 시점의 스냅샷을 기반으로 데이터를 읽어, 다른 트랜잭션의 변경 사항을 보지 못합니다.

또한 읽기 작업 시 잠금을 사용하지 않아 높은 동시성을 제공합니다. 읽기 작업이 잠금에 의해 지연되지 않아, 읽기 중심의 애플리케이션에서 우수한 성능을 보입니다. 읽기 작업 시 잠금을 사용하지 않으므로, 쓰기 작업과의 충돌이 줄어듭니다. 하지만 여러 버전의 데이터를 유지해야 하므로 저장 공간이 더 많이 필요할 수 있습니다.

트랜잭션이 시작된 시점의 데이터 상태를 기반으로 읽기 작업을 수행하여 일관성을 유지합니다. 또 갭락과 넥스트키 락을 통해 팬텀 리드를 방지합니다.

Lock-Based Concurrency Control
Lock-Based 방식은 데이터에 접근할 때 잠금(Lock) 을 사용하여 동시성을 제어합니다. 트랜잭션이 데이터를 읽거나 수정할 때 해당 데이터에 잠금을 걸어 다른 트랜잭션의 접근을 제한합니다. 즉, 잠금을 통해 데이터의 일관성과 무결성을 직접적으로 제어합니다.

데이터에 접근할 때 잠금을 걸어 다른 트랜잭션의 접근을 제한합니다. 읽기 작업은 공유 잠금을, 쓰기 작업은 배타 잠금을 사용하여 동시성을 제어합니다. 많은 다수의 트랜잭션이 동일한 데이터에 접근할 경우 성능 저하가 발생할 수 있습니다. 또 잘못된 잠금 순서나 설계로 인해 교착 상태(Deadlock)가 발생할 위험이 있습니다.

MVCC와 Lock-Based Concurrency Control 둘 중 어떤 걸 사용해야 하나요? 🤔
실제 데이터베이스 시스템, 특히 MySQL의 InnoDB는 MVCC와 Lock-Based 방식의 장점을 결합하여 동시성 제어를 최적화합니다.

읽기 트랜잭션은 MVCC를 사용하여 일관된 스냅샷을 기반으로 데이터를 읽으므로, 잠금을 최소화하고 높은 동시성을 유지할 수 있습니다.

쓰기 트랜잭션은 잠금을 사용하여 데이터의 일관성과 무결성을 유지하면서, 동시에 데이터 충돌을 방지합니다.

LIST


연산을 여러 번 적용하더라도 결과가 달라지지 않는 성질을 멱등성이라고 합니다. HTTP 메서드의 멱등성은 동일한 요청을 한번 보내는 것과 여러번 보내는 것이 서로 동일한 효과를 지니며, 서버의 상태도 동일하게 남을 경우에 멱등하다고 이야기할 수 있습니다. 대표적으로 멱등한 메서드는 GET, HEAD, PUT, DELETE, TRACE, OPTIONS 가 있습니다.

멱등성은 어떻게 활용될 수 있나요? 🤔
모종의 이유로 전송 커넥션이 끊어졌을 때, 멱등성은 클라이언트가 다시 같은 요청을 해도 되는가에 대한 판단 근거가 될 수 있습니다. 멱등하다면 요청을 재시도할 때 같은 서버의 상태를 보장하기 때문에 문제가 없습니다. 반면, 멱등하지 않다면 재시도 요청시 중복 요청을 보내 문제를 발생 시킬 수 있습니다. 예를 들어, 사용자가 결제하는 시점에 타임아웃으로 인해 정상 응답을 못받는 상황을 생각해 볼 수 있습니다. 해당 경우에서 멱등하지 않은 결제 API 경우에는 결제가 성공했는지 수동으로 확인하고 재요청해야합니다. 반면, 멱등한 결제 API의 경우에는 안심하고 여러 번 요청할 수 있으며 중복 요청으로 발생하는 문제(중복 결제)를 방지할 수 있습니다.

LIST

 

useEffect와 useLayoutEffect는 모두 렌더링된 후에 특정 작업을 수행하기 위해 사용됩니다. 하지만 실행되는 타이밍 용도가 다릅니다.

먼저, useEffect는 렌더링이 완료되는 시점 비동기적으로 실행됩니다. 즉, 화면이 실제로 사용자에게 그려진 후에 useEffect가 실행되는 방식입니다. 그래서 useEffect는 보통 데이터를 가져오는 작업이나 이벤트 리스너 추가 등 렌더링 후에 화면에 직접적인 영향을 주지 않는 작업에 주로 사용됩니다.

반면에 useLayoutEffect는 렌더링 후 DOM이 업데이트되기 직전의 시점 동기적으로 실행됩니다. 여기서 동기적이라는 것은 화면에 내용이 그려지기 전에 모든 레이아웃 관련 작업이 완료된다는 의미입니다. 예를 들어, DOM의 크기를 측정하거나 위치를 조정해야 할  useLayoutEffect를 사용하면 즉각적으로 그 변경사항이 반영되어 화면 깜빡임이나 불필요한 재렌더링을 방지할 수 있습니다.

정리하면, 렌더링 후 실행되는 비동기 작업에는 useEffect가 적합하고, 레이아웃 작업이나 DOM 조작과 같이 화면이 그려지기 전에 완료되어야 하는 작업에는 useLayoutEffect가 적합합니다.

예를 들면, useEffect는 사용자 데이터를 API로부터 가져오는 상황에 자주 사용합니다. 데이터가 렌더링 후에 설정되면 화면이 자연스럽게 업데이트되는 것입니다.

useEffect(() => {
  fetchData().then(data => setData(data));
}, []);

useLayoutEffect는 DOM의 크기를 측정해서, 다른 요소의 위치를 조정해야 할 때 유용합니다. 예를 들어, 어떤 요소의 높이를 측정해 그 높이에 맞춰 레이아웃을 맞추고 싶을 때 사용합니다:

useLayoutEffect(() => {
  const height = ref.current.offsetHeight;
  setHeight(height);
}, []);

단, useLayoutEffect 사용 시 성능 면에서 주의할 점이 있습니다. useLayoutEffect는 동기적으로 실행되기 때문에 너무 많은 작업이 실행되면 렌더링이 느려질 수 있습니다. 따라서 보통은 useEffect를 기본적으로 사용하고, 화면에 영향을 주는 작업만 useLayoutEffect로 처리하는 것이 좋습니다.

LIST


애플리케이션과 데이터베이스가 통신을 하기 위해서는 데이터베이스 커넥션이 필요합니다.

데이터베이스 커넥션의 생애주기 :

데이터베이스 드라이버를 사용하여 데이터베이스에 연결
데이터 읽기/쓰기를 위한 TCP 소켓 열기
소켓을 통한 데이터 읽기/쓰기
연결 종료
소켓 닫기
커넥션 풀이 없다면 애플리케이션에서 데이터베이스에 접근해야하는 요청을 처리할 때마다 커넥션을 새로 생성하여 연결하고 해제하는 과정을 반복해야 합니다. 이 과정은 비용이 상당히 많이 들기 때문에 요청의 응답시간이 길어집니다.

또 동시에 많은 요청이 들어올 경우 매번 새로운 커넥션을 생성하게 되는데, 데이터베이스의 최대 연결 수를 초과할 수 있습니다. 데이터베이스는 일반적으로 동시에 처리할 수 있는 요청 개수에 제한이 있는데, 이 제한을 초과하면 요청이 거부되어 사라지거나, 데이터베이스 자체가 비정상 종료될 수 있습니다.

데이터베이스 커넥션 풀을 사용함으로써 얻을 수 있는 장점은 무엇인가요?
커넥션 풀(Connection Pool)은 애플리케이션과 데이터베이스 간의 데이터베이스 연결(Connection)을 미리 생성해두고, 이를 재사용하는 기법을 말합니다. 데이터베이스에 접근할 때마다 새로운 연결을 생성하고 종료하는 대신, 미리 준비된 연결을 재사용함으로써 성능을 향상시키고 자원 사용을 최적화할 수 있습니다.

커넥션 풀의 주요 구성 요소는 초기 풀 크기(Initial Pool Size), 최소 풀 크기(Minimum Pool Size), 최대 풀 크기(Maximum Pool Size), 연결 대기 시간(Connection Timeout) 등이 있고, 이를 통해 커넥션을 효율적으로 관리하고 사용할 수 있습니다.

그럼 커넥션 풀 사이즈는 클 수록 좋나요? 🤔
커넥션을 사용하는 주체는 스레드(Thread)이기 때문에, 커넥션과 스레드를 연결지어 생각해야 합니다. 만약 커넥션 풀 사이즈가 스레드 풀 사이즈보다 크면, 스레드가 모두 사용하지 못해서 리소스가 낭비됩니다. 반대로 커넥션 풀 사이즈가 스레드 풀 사이즈보다 작으면, 스레드가 커넥션이 반환되기를 기다려야 하기 때문에 작업이 지연됩니다.

커넥션 풀 사이즈와 스레드 풀 사이즈의 균형이 맞더라도, 너무 큰 사이즈로 설정하면, 데이터베이스 서버, 애플리케이션 서버의 메모리와 CPU를 과도하게 사용하게 되므로 성능이 저하됩니다.

LIST

+ Recent posts