커넥션 풀

 

커넥션 풀(Connection Pool)의 핵심 개념

  • 소켓의 재사용: 데이터베이스 커넥션을 열고 닫는 것은 매우 비용이 많이 드는 작업입니다. 각 커넥션은 데이터베이스 서버와 애플리케이션 사이에 TCP 소켓을 열어서 연결을 유지합니다. 이 연결을 새로 열고 닫는 작업은 시간과 시스템 리소스를 많이 사용합니다.
  • 커넥션 풀을 사용하면 이미 열려 있는 커넥션을 재사용하여 이러한 오버헤드를 줄일 수 있습니다. 데이터베이스와의 소켓 연결을 유지하는 커넥션을 풀에 보관하고, 필요할 때마다 가져와 사용하고 나면 다시 반납하는 방식으로 동작합니다.

커넥션 풀의 장점

  1. 성능 향상: 매번 새로운 커넥션을 열고 닫지 않아도 되므로 데이터베이스 연결에 걸리는 시간을 크게 단축할 수 있습니다.
  2. 리소스 절약: 데이터베이스와의 연결 횟수가 줄어들어 애플리케이션과 데이터베이스 서버의 리소스를 절약할 수 있습니다.
  3. 스레드 동시성 지원: 여러 클라이언트가 동시에 데이터베이스와 통신하려 할 때, 커넥션 풀은 제한된 수의 커넥션을 효율적으로 관리해 동시성을 보장합니다.

커넥션 풀 작동 방식

  1. 애플리케이션이 시작될 때 사전에 일정 수의 커넥션을 생성하여 풀에 보관합니다.
  2. 애플리케이션이 데이터베이스에 접근해야 할 때 풀에서 커넥션을 할당받아 사용합니다.
  3. 커넥션 사용이 끝나면 이를 풀에 반납하여 다른 요청에서 재사용할 수 있도록 합니다.
  4. 커넥션의 개수는 최소 및 최대 연결 수로 제한되며, 설정에 따라 유휴 커넥션의 개수를 조절할 수 있습니다.

HikariCP와 같은 커넥션 풀 사용 시 특징

  • HikariCP 빠르고 경량화된 커넥션 풀로 유명하며, Spring Boot에서 기본적으로 많이 사용됩니다.
  • minimum-idle  maximum-pool-size와 같은 설정을 통해 커넥션 풀에서 유지할 유휴 커넥션의 수와 최대 커넥션 수를 정의합니다.
  • 자동 커밋 설정(auto-commit): 트랜잭션 처리를 효율적으로 관리하기 위해 자동 커밋을 비활성화할 수도 있습니다.

요약

  • 커넥션 풀의 핵심은 소켓 연결을 재사용하여 데이터베이스 연결에 소모되는 비용을 줄이는 것입니다.
  • 이를 통해 응답 시간 단축 리소스 효율성을 달성할 수 있으며, 데이터베이스와의 연결을 더욱 효과적으로 관리할 수 있습니다.
  • 데이터베이스 연결을 빈번히 여닫지 않고 이미 열린 연결을 재사용함으로써 성능 최적화 시스템 안정성을 확보할 수 있습니다.

따라서 커넥션 풀은 데이터베이스와의 효율적인 연결 관리와 성능 향상에 있어 매우 중요한 역할을 합니다.

 


**레파지토리(Repository), 서비스(Service), 컨트롤러(Controller)**는 모두 서로 연관되어 있으며, 이 세 가지는 함께 협력하여 애플리케이션의 기능을 구현합니다. 각 계층은 서로 다른 역할을 수행하며, 이를 통해 소프트웨어 아키텍처의 구조를 잘 정리하고, 유지보수를 쉽게 만듭니다. 이를 MVC 패턴이라고 부르며, 각 계층은 다음과 같은 역할을 담당합니다.

1. 컨트롤러(Controller)

컨트롤러는 사용자의 요청을 처리하고, 사용자가 무엇을 원하고 있는지를 이해하여 알맞은 서비스를 호출하는 역할을 합니다.

  • 역할: 웹 브라우저나 Postman 같은 클라이언트로부터 오는 HTTP 요청을 받아들여 처리합니다.
  • 서비스 호출: 필요한 비즈니스 로직을 수행하기 위해 서비스 계층을 호출합니다.
  • 응답 반환: 사용자에게 적절한 HTTP 응답을 반환합니다.

연관성: 컨트롤러는 서비스 계층을 사용하여 필요한 비즈니스 로직을 수행합니다.

2. 서비스(Service)

서비스 계층은 컨트롤러와 레파지토리 사이에서 비즈니스 로직을 처리하는 중간 다리 역할을 합니다.

  • 역할: 애플리케이션의 주요 비즈니스 로직을 처리하고, 레파지토리를 사용하여 데이터베이스 작업을 수행합니다.
  • 레파지토리 호출: 서비스는 데이터 접근을 위해 레파지토리를 사용하며, 엔티티와 DTO 사이의 변환을 담당합니다.

연관성: 서비스는 비즈니스 로직을 수행하기 위해 레파지토리를 호출하며, 데이터를 처리하고 가공한 후 컨트롤러로 결과를 반환합니다.

3. 레파지토리(Repository)

레파지토리는 데이터베이스와의 통신을 담당합니다.

  • 역할: 데이터베이스에 직접 접근하여 CRUD(Create, Read, Update, Delete) 작업을 수행합니다.
  • 데이터 조회 및 저장: 레파지토리는 데이터베이스에서 정보를 조회하거나 새로운 정보를 저장합니다.

연관성: 레파지토리는 서비스 계층에 의해 호출되며, 데이터베이스에서 필요한 데이터를 가져오거나 저장하는 역할을 합니다.

각 계층 간의 관계

  • 컨트롤러는 사용자의 요청을 받아 서비스를 호출합니다.
  • 서비스는 비즈니스 로직을 처리하고 데이터가 필요할 경우 레파지토리를 호출하여 데이터베이스와 통신합니다.
  • 레파지토리는 데이터베이스와 직접 통신하여 데이터를 저장, 조회, 수정, 삭제 등의 작업을 수행합니다.

즉, 이 세 계층은 서로 긴밀하게 연관되어 있으며, 각 계층이 독립적으로 자신의 역할을 수행하면서 다른 계층과 협력합니다. 이렇게 계층을 분리하면 다음과 같은 이점이 있습니다:

  • 유지보수성 향상: 각 계층이 독립적이기 때문에 특정 계층에 변화가 있어도 다른 계층에 미치는 영향을 최소화할 수 있습니다.
  • 재사용성: 서비스 계층의 비즈니스 로직은 여러 컨트롤러에서 재사용이 가능합니다.
  • 테스트 용이성: 각 계층을 별도로 테스트할 수 있으므로 단위 테스트와 통합 테스트가 더 쉽습니다.

예시로 살펴보기

컨트롤러, 서비스, 레파지토리 간의 협력 관계를 예시로 이해해 보겠습니다:

  1. 사용자가 'POST /api/users' 요청을 보내 새로운 유저를 추가하려고 합니다.
    • **컨트롤러(UserController)**는 이 요청을 받아서 JSON 데이터를 UserDTO로 변환한 후, **서비스(UserService)**에게 "새로운 유저를 추가해 주세요"라고 요청합니다.
  2. **서비스(UserService)**는 비즈니스 로직을 수행합니다.
    • 서비스는 UserDTO를 User 엔티티로 변환하고, **레파지토리(UserRepository)**에 "이 데이터를 데이터베이스에 저장해 주세요"라고 요청합니다.
  3. **레파지토리(UserRepository)**는 데이터베이스와 통신합니다.
    • 레파지토리는 해당 User 객체를 데이터베이스에 저장하고, 성공적으로 저장된 User 객체를 서비스로 반환합니다.
  4. **서비스(UserService)**는 이 저장된 User 객체를 다시 UserDTO로 변환한 후 컨트롤러로 반환합니다.
  5. **컨트롤러(UserController)**는 사용자의 요청에 대한 결과로 새로운 유저 정보가 포함된 응답을 반환합니다.

이렇게 각 계층은 서로 다른 역할을 수행하면서 데이터를 주고받아 협력하여 전체 기능을 구현합니다. 이런 구조 덕분에 각 계층의 역할이 명확해지고, 유지보수가 쉬워지며, 코드의 가독성과 재사용성이 높아집니다.

 


프로젝트 구조에서의 역할 분담

  1. Controller: 사용자의 요청을 받아서 적절한 서비스를 호출하고 응답을 반환하는 역할을 합니다.
  2. Service: 비즈니스 로직을 처리하고, Repository나 DAO를 호출하여 데이터를 처리하는 역할을 합니다.
  3. Repository: 데이터베이스와의 상호작용을 처리하는 기본적인 역할을 합니다. CRUD 작업을 쉽게 할 수 있게 도와줍니다.
  4. DAO: 데이터베이스와의 세밀한 상호작용을 위해 Repository보다 더 복잡한 쿼리 로직을 다룰 수 있습니다.
  5. Config: 프로젝트의 다양한 설정을 관리하여, 전반적인 환경 설정을 쉽게 변경하거나 관리할 수 있게 합니다.
  6. Filter: 요청과 응답을 가로채 추가적인 작업(보안, 로깅 등)을 할 수 있게 합니다.

@PrePersist @PostLoad

 

@PrePersist

  • 설명: @PrePersist는 엔티티가 영속화되기 직전에 호출되는 메서드에 적용합니다.
  • 용도: 주로 엔티티가 처음 데이터베이스에 저장되기 전에 특정 작업을 수행하고자 할 때 사용합니다.
    • 예를 들어, 엔티티 생성 시간, 등록자 등의 정보를 자동으로 설정하고 싶을 때 활용할 수 있습니다.
@Entity
public class User {

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

    private String name;
    private LocalDateTime createdAt;

    // 엔티티가 영속화되기 직전에 호출되는 메서드
    @PrePersist
    public void prePersist() {
        this.createdAt = LocalDateTime.now(); // 생성 시간 설정
    }

    // 기타 필드 및 메서드 생략
}

위 코드에서 prePersist() 메서드는 @PrePersist 어노테이션 덕분에 User 객체가 데이터베이스에 저장되기 전에 자동으로 호출됩니다. 이 메서드에서 createdAt 필드가 현재 시간으로 설정됩니다.

 

@PostLoad

  • 설명: @PostLoad는 엔티티가 데이터베이스에서 로드된 후에 호출되는 메서드에 적용합니다.
  • 용도: 데이터베이스에서 값을 읽어올 때 추가 작업을 수행하고 싶을 때 사용합니다. 예를 들어, 특정 필드의 값을 계산하거나, 엔티티의 필드 값을 변환하는 작업을 수행할 수 있습니다.
@Entity
public class Product {

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

    private String name;
    private double price;
    private double discountedPrice;

    // 데이터베이스에서 로드된 후 호출되는 메서드
    @PostLoad
    public void postLoad() {
        this.discountedPrice = calculateDiscount(); // 할인 가격 계산
    }

    private double calculateDiscount() {
        // 할인 계산 로직 (예: 10% 할인)
        return this.price * 0.9;
    }

    // 기타 필드 및 메서드 생략
}

위 코드에서 postLoad() 메서드는 Product 엔티티가 데이터베이스에서 로드된 후 호출되며, 이 메서드를 통해 discountedPrice 필드가 자동으로 계산됩니다. calculateDiscount() 메서드는 할인된 가격을 계산하여 discountedPrice에 설정합니다.

 

요약

  • @PrePersist: 엔티티가 영속화되기 직전에 호출되어 등록 시간 설정, 기본값 설정 등을 할 때 유용합니다.
  • @PostLoad: 엔티티가 데이터베이스에서 로드된 직후 호출되어 필요한 추가 계산이나 필드 변환을 수행할 때 유용합니다.

 

+ Recent posts