Prometheus 개요

Prometheus는 오픈소스 기반의 모니터링 시스템으로 Service discovery pattern을 통해 데이터 수집대상을 발견하고 주기적으로 풀링하여 매트릭 데이터를 수집합니다.

 

https://sysadmins.co.za/setup-prometheus-and-node-exporter-on-linux-for-epic-monitoring/

 

수집된 매트릭 정보들은 로컬 디스크에 있는 시계열 데이터베이스에 저장되어 Prometheus의 도메인 특화 언어인 PromQL을 통해 빠르게 검색할 수 있습니다.

 

Prometheus는 매트릭 수집을 위한 서버나 컨테이너 구성이 불필요(Single host)하며, 클라이언트가 매트릭 푸시를 위해 CPU를 사용할 필요도 없습니다. (Metric Push를 통한 응답 병목이나 서버 부하를 예방합니다.)

https://prometheus.io/docs/introduction/overview/

부가적으로 중앙 집중식 구성 방식과 관리 콘솔을 제공하기에 설치나 사용이 매우 쉽습니다.

 

 

 

Prometheus의 이점

  • Kubernetes 환경에서 설치하기가 쉽고 Grafana와 같은 Tool과 연동이 쉬우며 많은 Dashboard 템플릿들이 오픈소스로 공유되고 있습니다.
  • 기존에 구현된 다양한 Exporter를 제공합니다. (매트릭 수집을 위한 클라이언트들)
    • 또한 각 언어별 Client library를 제공함으로써 쉽게 Counter나 Custom 매트릭 들을 뽑아낼 수 있습니다.
      • Spring의 경우 Boot Actuator 모듈을 통해서 Hikari pool, memory 매트릭을 얻을 수 있습니다.
  • 수집된 매트릭은 용량을 압축하여 저장하고 시계열 데이터베이스의 뛰어난 성능을 기반으로 많은 매트릭을 빠르게 조회할 수 있습니다.

 

 

Prometheus의 한계, 주의할 점

  • 풀링을 기반으로 매트릭을 수집하기 때문에 장애 발생을 빠르게 감지하는 것에는 어려움이 있습니다.
    • 풀링하는 순간의 매트릭 정보만 가지고 있기 때문에 근삿값만을 알 수 있습니다.
  • 싱글 호스트 아키텍처 구조의 한계로 인해 이중화나 클러스터링을 적용하기가 매우 어렵습니다.
    • 저장용량이 부족한 경우에는 설치된 서버의 디스크 용량을 늘릴 수 밖에 없습니다.
      • 다른 오픈소스를 같이 사용하는 상황은 제외
  • 이중화 구성 시에는 Replication을 하지않고, 두개의 Prometheus를 띄워 같은 목록을 풀링시키고 저장하는 방법을 사용하게 됩니다.
    • Thanos라는 오픈소스를 사용하여 매트릭 정보의 집계하고, 스케일링 가능한 스토리지에 저장하여 특정 프로메테우스의 장애로 인한 매트릭 소실 등을 방지할 수 있습니다.
  • Prometheus는 매트릭 정보를 효율적으로 다루기 위해(Read, Write, Sampling 등) Memory에 Buffering을 진행합니다. 메모리와 관련된 직접적인 설정을 할 수 없기 때문에 여러 작업에서 사용하는 리소스의 량을 계산하여 램을 증설하거나 Cardinality가 높은(Selectivity가 낮은) label을 수집하지 않거나 수집 주기를 늘리는 식으로 대처하여야 합니다. 

 

이제 Prometheus를 통해 매트릭 정보를 뽑아올 Target Application 들을 구현해보겠습니다.

 

 

 

Sample Application Archtecture

이번 글에서 구현하게 될 구조입니다.

 

 

 

Publisher Application 구현

 

프로젝트 생성

우선은 RabbitMQ 메세지를 생산할 Publisher Application을 구현해보겠습니다.

 

제가 사용할 Spring Actuator Module에는 내장된 micrometer 라이브러리가 존재하는데, 이것은 JVM 기반의 매트릭 정보를 다루는 인터페이스의 역할을 합니다. (Facade Pattern)

 

해당 라이브러리를 통해 Prometheus에서 사용하는 매트릭 정보를 받기 위해서는 별도 의존성을 추가해야 합니다.

implementation 'io.micrometer:micrometer-registry-prometheus'

 

 

Application yml

# yml 출처 : https://meetup.toast.com/posts/237
# 추가한 micrometer 라이브러리을 이용하여 metric 데이터 응답을 제공할 API를 설정합니다.
management:
  endpoints:
    web:
      exposure:
        include: prometheus # {protocol}://{host:port}/prometheus
  metrics:
    tags:
      application: ${spring.application.name} # metric 정보에 대한 라벨링 설정
  endpoint:
    health:
      show-details: always

spring:
  application:
    name: "publisher_application" # 라벨링 설정 값

  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest

logging:
  level:
    root: info

server:
  port: 8080

사용할 RabbitMQ 설정과 Logging, port 설정을 진행하고 마무리합니다.

 

 

RabbitMQConfiguration

@Configuration
public class RabbitMQConfiguration {

    @Bean
    public Queue queue() {
        return new Queue("event-queue", true);
    }

    @Bean
    public DirectExchange exchange() {
        return new DirectExchange("event");
    }

    @Bean
    public Binding binding(Queue queue, DirectExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("event-pay");
    }

    @Bean
    public Jackson2JsonMessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter(new ObjectMapper());
    }

    @Bean
    public RabbitTemplate rabbitTemplate(final ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMessageConverter(messageConverter());
        return rabbitTemplate;
    }
}

연결된 RabbitMQ Container에 설정할 Queue와 DirectExchange, Routing Key를 설정하고, 객체를 Json 형태로 Converting 할 수 있게 Jackson2JsonMessageConverter를 설정합니다.

 

 

ScheduledConfiguration

@Configuration
@EnableScheduling
public class ScheduledConfiguration implements SchedulingConfigurer {

    // Scheduled 과 같은 스케줄링 작업에 대한 구성 정보를 설정하는 Registrar
    @Override
    public void configureTasks(ScheduledTaskRegistrar registrar) {
        final ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.setPoolSize(3);
        taskScheduler.setThreadNamePrefix("event-");
        taskScheduler.initialize();
        registrar.setTaskScheduler(taskScheduler);
    }
}

메세지를 생산하는 역할을 하는 Scheduled이 설정된 메서드를 최대 몇 개까지 실행할지 그리고 실행할 때 사용되는 스레드 정보를 커스텀할 수 있는 ThreadPoolTaskScheduler를 설정합니다.

 

 

Application

예제의 단순화를 위해서 별도의 Component를 정의하지 않고 Application Class에서 코드를 작성합니다.

단순한 예제 코드이므로 따로 설명을 하진 않겠습니다.

@Slf4j
@SpringBootApplication
@RequiredArgsConstructor
public class PublisherApplication {

    private final RabbitTemplate rabbitTemplate;
    private static final Random generator;

    static {
        generator = new Random();
    }

    public static void main(String[] args) {
        SpringApplication.run(PublisherApplication.class, args);
    }

    @Scheduled(fixedRate = 5000) // fixedRate 는 작업을 실행한 시점부터 다음 작업 수행 시간을 측정한다.
    public void publishEvent() {
        final int userId = generator.nextInt(10);
        final LocalDateTime eventTime = LocalDateTime.now();

        final PayEvent payEvent = PayEvent.of(userId, "Transaction finished", eventTime.toString());
        log.info("{}", payEvent);
        rabbitTemplate.convertAndSend("event", "event-pay", payEvent);
    }
}

----------
// 사용될 도메인 모델

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class PayEvent {
    private long userId;
    private String message;
    private String eventTime;

    protected PayEvent(long userId, String message, String eventTime) {
        this.userId = userId;
        this.message = message;
        this.eventTime = eventTime;
    }

    public static PayEvent of(long userId, String message, String eventTime) {
        return new PayEvent(userId, message, eventTime);
    }

    @Override
    public String toString() {
        return "PayEvent{" +
                "userId=" + userId +
                ", message='" + message + '\'' +
                ", eventTime='" + eventTime + '\'' +
                '}';
    }
}

PayEvent 라는 객체를 생성하여 메시지 큐에 넣을 Publisher Application 구현이 완료되었습니다. 이제 해당 요청을 In-memory DB에 저장하는 단순한 Subscriber Application을 구현해봅니다.

 

 

 

Subscriber Application 구현

프로젝트 생성

이미지와 별개로 Publisher application과 동일하게 micrometer 관련 의존성을 추가합니다.

 

 

Application yml

# yml 출처 : https://meetup.toast.com/posts/237
management:
  endpoints:
    web:
      exposure:
        include: prometheus
  metrics:
    tags:
      application: ${spring.application.name}
  endpoint:
    health:
      show-details: always

spring:
  application:
    name: "subscriber_application"

  datasource:
    url: jdbc:h2:mem:testdb
    username: sa
    driver-class-name: org.h2.Driver

  h2:
    console:
      enabled: true

  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest

  jpa:
    hibernate:
      ddl-auto: update
    properties:
      show-sql: true
      format_sql: true
    open-in-view: false

logging:
  level:
    root: info

server:
  port: 8081

기존 Application과 동일하게 micrometer 설정을 진행하고, Datasource, JPA 설정을 진행합니다.

 

 

RabbitMQConfiguration

@Configuration
public class RabbitMQConfiguration {

    @Bean
    public Jackson2JsonMessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter(new ObjectMapper());
    }

    @Bean
    public RabbitTemplate rabbitTemplate(final ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMessageConverter(messageConverter());
        return rabbitTemplate;
    }
}

Queue 관련 설정은 별도로 진행하지 않으며, 위와 같이 JSON 데이터를 객체로 변환하기 위해 ObjectMapper를 추가합니다.

 

 

Application

@Slf4j
@RequiredArgsConstructor
@SpringBootApplication
public class SubscriberApplication {

    private final PayEventRepository payEventRepository;

    public static void main(String[] args) {
        SpringApplication.run(SubscriberApplication.class, args);
    }

    @RabbitListener(queues = "event-queue")
    public void subscribeEvent(PayEvent payEvent) {
        log.info("{}", payEvent);
        payEventRepository.save(payEvent);
    }
}

----------
// 여기서는 JPA를 통한 영속화를 위해 Entity로 정의합니다.

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class PayEvent {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private long userId;
    private String message;
    private String eventTime;

    public PayEvent(long userId, String message, String eventTime) {
        this.userId = userId;
        this.message = message;
        this.eventTime = eventTime;
    }

    @Override
    public String toString() {
        return "PayEvent{" +
                "userId=" + userId +
                ", message='" + message + '\'' +
                ", eventTime='" + eventTime + '\'' +
                '}';
    }
}

----------
// 어딘가 있을 JPA Repository...
public interface PayEventRepository extends JpaRepository<PayEvent, Long> {
}

@RabbitListener annotation을 통해 queue를 구독한 상태로 전달되는 PayEvent를 JPA로 영속화하는 단순한 Application입니다.

 

이제 Sample Application 코드들을 구현하였으니, RabbitMQ, Exporter, Prometheus와 Grafana를 등록하기 위해 yml file과 docker-compose.yml을 작성합니다.

 

 

 

RabbitMQ, Prometheus, Grafana 구성

Prometheus.yml

docker-compose.yml를 실행하기 전에 우선 Prometheus 정보를 설정합니다.

해당 yml은 https://katacoda.com/courses/prometheus/getting-started와 튜토리얼을 참고하여 작성하였습니다.

global:
  scrape_interval: 10s

scrape_configs:
  - job_name: "publisher_application"
    metrics_path: "/actuator/prometheus"
    static_configs:
      - targets: ["host.docker.internal:8080"]

  - job_name: "subscriber_application"
    metrics_path: "/actuator/prometheus"
    static_configs:
      - targets: ["host.docker.internal:8081"]

  - job_name: "rabbitmq_exporter"
    metrics_path: "/metrics"
    static_configs:
      - targets: ["host.docker.internal:9419"]

RabbitMQ_exporter의 매트릭 정보 응답 port는 9419가 default port 입니다.

 

 

docker-compose.yml

version: '3'

networks:
  back:

services:
  rabbitmq:
    image: rabbitmq:3-management
    container_name: rabbitmq
    environment:
      - RABBITMQ_NODENAME:rabbitmq
    ports:
      - "5672:5672"
      - "15672:15672"
    networks:
      - back

  rabbitmq-exporter:
    image: kbudde/rabbitmq-exporter
    container_name: rabbitmq-exporter
    environment:
      - RABBIT_URL=http://rabbitmq:15672
    depends_on:
      - rabbitmq
    ports:
      - "9419:9419"
    networks:
      - back

  prometheus-1:
    image: prom/prometheus
    container_name: prometheus-1
    environment:
      - --config.file=/etc/prometheus/prometheus.yml
    volumes:
      - C:\.\.\.\config\prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"
    networks:
      - back

  grafana:
    image: grafana/grafana
    depends_on:
      - prometheus-1
    ports:
      - "3000:3000"
    networks:
      - back

해당 yml의 bind mounting은 windows를 기준으로 작성되었습니다.

 

기본적으로 RabbitMQ exporter는 RabbitMQ의 container와 네트워크 인터페이스를 공유하는 방식으로 동작합니다. https://github.com/kbudde/rabbitmq_exporter

 

이제 해당 compose file을 실행하면 {http}://{host:port}를 통해 Prometheus와 Grafana Admin으로 접근할 수 있습니다.

  • Grafana의 기본 계정 정보는 admin / admin입니다.

 

Prometheus Dashboard

상단 메뉴바의 Status를 클릭하여 Targets 목록을 선택하면 yml에서 구성한 정보를 기준으로 구성된 Target Application 목록을 볼 수 있습니다.

 

이렇게 등록된 Target Application의 상태(health-check)와 마지막으로 pulling한 시간 정보 등을 간단하게 확인할 수 있습니다.

 

그럼 이제 Grafana에 접속하여 패널을 추가하고 가시화되는 데이터를 확인해보겠습니다.

  • 해당 글은 연동하는 것에만 의의를 두었기에 후술된 내용은 다른 튜토리얼을 참고하는 것이 좋을 수 있습니다.

 

Grafana Dashboard

이제 Prometheus의 매트릭 정보를 가져오기 위해 Data source를 설정해보겠습니다.

  • 좌측 상단 수직 메뉴바에서 톱니바퀴(Configuration)를 클릭하고 data source를 선택합니다.
  • 그다음 Add data source 버튼을 클릭합니다.
  • Time series databases 항목에서 Prometheus를 선택합니다.

 

이제 Prometheus가 실행되는 host 정보를 HTTP 항목의 URL에 입력하시고 맨 밑에 Save & test를 눌러 연결 상태를 확인한 다음 저장합니다.

 

 

Dashboard 설정하기

이제 메인화면으로 돌아와서 좌측 메뉴바의 + 버튼을 클릭하고 Dashboard를 선택합니다.

 

Add a new row를 선택하여 하나의 row를 만들고 좌측 상단의 그래프와 + 가 합쳐진 버튼을 눌러서 하나 더 추가합니다. (이름은 임의로 정해주세요.)

 

다시 방금 그 버튼을 눌러서 Add an empty panel을 선택합니다.

 

이제 현재 JVM 메모리 사용량을 나타내는 jvm_memory_used_bytes metric를 이용해 Time series를 만듭니다. 별도 설정이 없다면 위에서 만든 pub/sub application 정보가 모두 등록됩니다.

설정이 완료되었다면 Apply를 누릅니다.

 

여기서부터 기존에 존재하던 내용은 필요가 없어보여 제거했습니다. 그대신 몇가지 PromQL 예시를 보여드리는 것으로 변경하였습니다.

 

 

MySQL QPS (Query Per Second)

rate(mysql_global_status_queries[{Time}])

PromQL의 Range Vector 중 하나인 rate를 이용하여 특정 기간{Time} 동안 평균 값 혹은 변동 폭을 연산해 가시화할 수 있습니다.

 

 

MySQL Slow Queries

irate(mysql_global_status_slow_queries[Time])

PromQL의 Range Vector 중 하나인 irate를 사용하면 특정 기간의 순간적인 수치 증가율을 연산해 가시할 수 있습니다.

 

 

MySQL Connection Error

rate(mysql_global_status_connection_errors_total[Time])

 

 

MySQL available connections

100 * mysql_global_status_threads_connected / mysql_global_variables_max_connections

 

 

 

RabbitMQ memory used

https://grafana.com/grafana/dashboards/4371

rate(node_mem_used[{Time}])

 

 

RabbitMQ messages published total

rate(queue_messages_published_total[{Time}])

 

등등 Exporter에서 제공하는 다양한 메트릭을 이용하여 다양한 성능 지표를 확인할 수 있습니다.

그리고 만약 혼자서 구성하는 것이 어렵다면, https://grafana.com/grafana/dashboards/ 에서 완성된 대시보드를 가져와 사용할 수 있습니다. 

 

제공되는 메트릭 정보는 각 (Exporter에 대한) Github Project를 들어가셔서 readme 를 확인하시면 됩니다. 

타노스를 통한 HA 구성 등의 내용들은 더 열심히 공부를 한 뒤에 새로운 글을 작성하거나 보강하도록 하겠습니다. 

 

이 게시글의 결과물은 https://github.com/Lob-dev/The-Joy-Of-Java/tree/main/Sample-Spring-Boot-Prometheus 에서 보실 수 있습니다.

 

 

 

참고 자료

추가로 정리 중

RabbitMQ Message Queue 및 Message 보존 설정

Queue 생성시 Durable 설정

RabbitMQ server가 종료 후 재기동하면, 기본적으로 Queue는 모두 제거되게 된다. 이를 막기 위해서는 Queue를 생성할 때 Durable 옵션을 True로 설정하여야 하며, 메시지의 경우 PERSISTENT_TEXT_PLAIN 옵션을 주어야 Message가 보존된다.

MessageProperties.PERSISTENT_TEXT_PLAIN 

추가적으로 Queue에 있는 Message를 보존하는 속성으로 delivery-mode라는 것이 존재하는데, 기본 값은 1으로 메모리에서 메시지를 관리하는 상태가 되며, 2로 설정할 경우 RabbitMQ가 디스크에 메시지를 영속화시킨다. 

 

금융 거래 이벤트와 같이 비즈니스와 밀접하게 연관된 지속성 메시지와 로그인 이벤트와 같이 비즈니스에 영향을 주지 않는 비지속성 메시지를 구분하여 값을 설정한다.

 

 

 

RabbitMQ Message 손실 방지 설정

RabbMQ는 Consumer에게 전달된 Message의 손실을 방지하기 위한 기능을 제공한다. 이 속성은 기본적으로 활성화 되어 있고, RabbitMQ는 메시지에 대한 응답(작업 처리)을 전달받지 못한다면 다시 Queue에 집어넣고 다른 worker에게 전송할 수 있다.

https://www.rabbitmq.com/confirms.html

 

 

 

Message dispatch?

RabbitMQ dispatch 방법은 기본적으로 round robin 방식이며 MessageQueue에 담는 순서대로 worker들에게 전달한다.

균등한 메세지 처리 가 필요한 상황에선 위 방식으로 충분할 수 있으나 worker들이 메시지 중 특정 순서로 오랜 처리 시간이 필요한 상황 등의 특정한 경우 알맞지 않을 수 있다.

Prefectch Count none : 하나의 Worker의 작업이 지연되고 있다.

이런 경우 순차적으로 메세지를 제공받더라도 처리 시간으로 인해 다른 worker는 쉬는 상태에서 무거운 작업을 처리하는 worker에게 지속적으로 message가 전달되는 문제가 발생한다.

 

 

 

Fair dispatch 하도록 설정하기 : Prefetch Count

Prefetch Count는 Consumer에게 보내는 메시지 수를 지정하는 데 사용하는 옵션이며, 요청을 처리했음을 의미하는 Ack가 RabbitMQ에 전달되기 전까지 consumer가 전달받을 수 있는 message의 개수이다.

 

기본 설정 값은 클라이언트에게 크기 제한이 없는 버퍼를 제공하며, 기본적으로 요청을 받을 수 있는 Consumer에게 최대한 많은 메시지를 전달한다.

Prefectch Count 1

  • 전송된 메시지는 클라이언트의 클라이언트에 존재하는 Prefetch Buffer에 캐시 된다.
  • Prefetch 된 메시지는 Message Queue의 대기열에서 제거되고 다른 Consumer에게 표시되지 않는다.

rabbit.default_consumer_prefetch

 

 

 

Prefetch Count에 따른 성능 조정

1개로 설정해 두는 경우 (작을 수록 Fair Dispatch 하다.)

  • 하나의 메시지가 처리되기 전에는 새로운 메시지를 받지 않게 되므로, 여러 worker에게 동시에 작업을 분산시킬 수 있지만 여러 worker가 포함되어 있으나 각 단위 요청이 빨리 처리되는 상황에서는 각 worker의 다음 작업까지 대기시간이 증가할 수 있다.
  • worker가 많거나 한 작업 단위의 처리 시간이 긴 경우 모든 worker에게 균등하게 나눠지도록 값을 작게 설정하는 것이 좋다.

 

값을 크게 해 둘 경우

  • 메시지 큐에서 다량의 메시지를 하나의 worker에게 전달하여 Buffer에 요청을 쌓고 계속 처리할 수 있도록 하기에 각 worker 대기 시간은 감소할 수 있지만 특정 요청의 처리 시간이 긴 경우에 다른 worker들이 일을 하지 않고 대기하는 상황이 발생할 수 있다.
  • worker가 적고 한 작업 단위의 처리 시간이 짧은 경우 값을 크게 설정할 수 있다.

 

 

참고 자료

추가로 정리 중

 

AMQP Frame Structure

AMQP Spec에서는 객체 지향 개념과 유사하게 Class와 Method라는 것을 사용하여 AMQP Command를 정의한다.

  • Class는 기능의 범위를 정의한다.
  • Method는 각 Class 내부에서 서로 다른 작업을 수행하는 작업 단위이다.
// example) Connection.Start
{class}.{method}

basic class : (메시지의 송신, 수신, 대기열의 접근, 클라이언트의 종료, 시작, 거부 등)

channel class : (채널 생성, 종료, 중지)

exchange class : (분기 설정, 생성, 삭제, 연결 등) 

queue class : (메시지 큐 생성, 삭제, 설정, 해제 등)

tx class : (트랜잭션 커밋, 롤백, 모드 설정 등)

 

 

 

AMQP Component

AMQP Spec에서 명령을 전송하거나 수신할 때 필요한 모든 인자들은 캡슐화되어 있는 Frame으로 Encoding 되어 전송된다.

  • Frame은 각각의 명령과 인자를 Encoding하여 각각 구분되어 관리하는 하나의 단위이다.
    • 이를 통해 여러가지 요청을 효율적으로 구분한다.
  • Frame의 기본 크기는 131KB이며 연결 과정 중에 32비트로 표현할 수 있는 범위 내에서 최대 크기를 서버와 협상한다.

 

저수준의 AMQP 프레임은 다섯 개의 별개 구성 요소를 지닌다.

  • Frame Type : 1Byte
  • Channel Number
  • Frame Size : size → Byte
  • Frame Payload
  • end byte marker : ASCII 206
  Frame Header  ||               Frame Payload              ||  
[ 1 ][ 0 ][ 335 ][ Frame Payload : Type 별로 Data가 다르다. ][ 0xce ]

Frame Payload는 각각의 Frame 간에 운반하는 내용을 무결성 있게 보호하도록 감싸게 설계되었다.

 

 

 

AMQP Frame Types

 

Protocol Header Frame

  • RabbitMQ Connection을 연결할 때 한 번만 사용되는 Frame
  • Client Library를 사용할 때 추상화되어 있는 Frame

 

Method Frame

  • RabbitMQ와 서로 주고받는 RPC 요청과 응답을 전달하는 Frame
  • Exchange, Routing key를 포함하여 전송한다.
  • data size를 최소화하기 위해 이진 데이터로 구성한다.

 

Content Header Frame

  • Message Size와 Message Property를 포함하는 Frame.
  • data size를 최소화하기 위해 이진 데이터로 구성한다.

 

Body Frame

  • Message Content를 포함하는 Frame
  • AMQP Spec에서는 Max Frame size가 지정되어 있으며, 이 크기를 초과하면 Body Frame을 여러 단위로 분할시킨다.
  • JPEG, JSON, XML, Text, Binary 형식으로 직렬 화한 데이터를 전송 가능하다.

 

Heartbeat Frame

  • Client와 Server가 주고받으며 서로 사용 가능한 상태인지 파악하는 것
  • RabbitMQ는 Client에게 해당 Frame을 보내며, 응답하지 않는 경우 연결을 끊는다.
    • 단일 쓰레드, 비동기 환경에서는 제한 시간을 약간 큰 값으로 늘린다.
    • 하트비트가 동작하기 어려운 상황의 경우 0으로 설정하여 disable 한다.
  • Client Library를 사용할 때 추상화되어 있는 Frame

 

 

RabbitMQ Message publishing

RabbitMQ에서 Message를 Publishing 할 때에는 Method, Header, Body Frame을 사용한다.

 

 

Publishing flow

  1. Method Frame
  2. Content Header Frame
  3. 1개 이상의 Body Frame

 

 

참고 자료

 

요즘 글을 거의 쓰지 못하고 있어서 어떤 주제로 쓸지 고민하던 중에 나의 근황에 대해서 정리하게 되었다.

 

직장을 가지고 지속적으로 글을 쓴다는 것은 정말 힘들고 존경스러운 일이라는 것을 다시 한번 느끼는 계기가 되었다. 

 

 

 

학습 근황

이제 백엔드 공부를 시작한 지 10여 개월 정도가 지났고 학습일지도 차근차근 쌓이고 있다.

회사에 다니면서는 귀찮거나 피곤하다는 이유로 제대로 하지 못한 날도 있었는데, 이러한 유혹을 잘 뿌리치는 것이 학습을 지속함에 있어 중요한 것 같다.

 

 

 

 

회사에 다니면서 간단한 단축 URL 서비스를 만들어보았다.

회사에서 기존 프로젝트를 유지보수하다가 신규 프로젝트로 넘어가게 되었었다.

넘어갈 신규 프로젝트의 기술 스택을 듣고 살짝 경험해보기 위해서 구상하고 진행하였다.

이를 통해 Spring에서 Redis를 사용하여 캐싱하는 것에 기본적인 이해와 Docker와 Compose를 경험할 수 있었고, 평소에 난감했던 JPA의 Transaction 경계 등을 어느 정도 체감할 수 있었다.

 

추가로 TestContainers 라는 라이브러리를 이용하여 실제 개발 환경과 구분되는 통합 테스트를 구성하는 방법을 알 수 있었다.

 

 

 

 

지금도 스터디중..!

카카오톡 오픈 채팅방과 디스코드의 개발자 커뮤니티를 통해 스터디를 구하고 진행할 수 있었다.

나름대로 안면이 있는 개발자분들과 진행하다 보니 스터디가 중간에 공중분해되는 불상사는 없어서 정말 다행인데, 이러한 스터디들도 가능한 선에서 지속하려고 한다.

 

 

 

 

사이드 프로젝트로 시작한 신입 개발자의 학습 자료 정리 저장소

오픈 커뮤니티에서 있다 보면 학습할 키워드나 방향을 혼자 잡지 못해서 물어보는 사람들이 많다. 하지만 이런 사람들의 경우 방향 자체를 알지 못하다 보니 모호하게 혹은 추상적으로 질문하여 대답을 받지 못하는 경우도 흔히 있는데 이러한 상황들을 보며 생각하게 된 프로젝트였다.

현재는 어떠한 내용을 보강할까 고민 중인데 의견이 온다면 최대한 반영할 생각이다!

 

 

 

 

이전에 언급했던 "올해 더 배우고 싶은 것들" 근황은..?

 

Kotlin

기본적인 문법의 차이(Java)와 기본적인 동작 원리 등을 학습하였다. 실제 프로젝트를 진행해보진 못했기 때문에 다음 토이 프로젝트에서 사용해볼 생각이다.

 

 

JPA

스터디와 회사 업무를 통해 기본적인 JPA 사용 방식은 학습할 수 있었던 것 같다. 하지만 객체 지향적인 혹은 DDD를 통한 설계 경험은 없다고 봐야 하며, 기존에 개발해오던 방식인 트랜잭션 스크립트 방식에만 익숙해져 있어 좀 더 나아가는 기회가 필요할 것 같다.

 

 

Network + HTTP

HTTP 완벽 가이드를 학습하진 않았고, 영한 님의 HTTP 강의를 통해 학습했었다. 좀 더 나아가는 내용은 "IT 엔지니어를 위한 네트워크 입문"이라는 서적을 통해 학습해보아야 할 것 같다.

 

 

Database

러닝 SQL을 조금씩 학습하고 있으며, 부족한 관계형 모델에 대한 기반 개념과 SQL 튜닝을 위해 SQL 튜닝 비법이라는 서적도 구매했다. (언제 볼지는 모른다...!)

 

 

React ( 새로운 아이템...! )

후술될 특별한(?) 상황 때문에 여유가 생겨서 학습하게 된 주제이다. 평소에 프론트엔드에 대해서 타임리프와 같은 SSR 방식을 제외하고는 정말 무지한 상태였는데 이런 상태를 조금이라도 벗어나기 위해 학습을 시작했다.

 

해보니 꽤나 흥미가 있어서 REST API와 통합하는 과정까지는 쭉 학습해볼 것 같다.

 

현재 러닝 리액트, Fast campus와 니꼴라스(안녕하세요!) 아저씨의 무료 강의를 보고 있다.

 

 

알고리즘, 자료구조

이것도 후술될 특별한 상황 때문에 중요성을 뼈저리게 느끼게 되었다. 학습을 계획하면서 일정 시간 이상을 분배하여야 할 것 같다.

 

 

 

 

가장 큰(?)일..

3월 8일에 합격하여 다니고 있던 곳을 7월 9일에 퇴사하게 되었다. ( 딱 4개월 하고 하루를 더 다녔다.)

 

우선 이전 근황 글에는

1년 차 스타트업이고 연봉이 높은 편은 아니지만 원하던 스킬 셋들을 사용하고, 고 경력의 개발자들이 많은 곳이기에 설계, 기술, 경험들을 기대하고 선택하게 되었다.

라고 작성했던 것 같은데, 이때 생각했던 것들과 괴리감을 느낀 부분들이 있었다.

 

 

스킬 셋은 설계에 영향을 미치지 않는다.

어떠한 기술 스택을 사용하더라도 Application의 구조는 이전의 개발 모델과 같을 수 있다.라는 것을 알 수 있었다.

 

나는 회사에 다니면서 1달 동안 지정받은 인강들을 수강하고 바로 유지보수 중이던 프로젝트에 투입되었는데, 회사를 나오기 전까지 총 2개의 프로젝트를 경험하게 됐으며, 이것들은 각각

  • Mybatis(Wrapper 용도) + Stored Procedure
  • JPA, QueryDSL

이라는 Persistence Framework와 Sub routine 기능을 사용하였다.

 

JPA, QueryDSL를 이용해서 하는 것이 Query Creation과 Dirty Checking을 통한 상태 변경뿐이었고, 전체적인 설계와 사용 방식은 Procedure를 사용하던 플랫폼과 같다는 것에서 내가 알던 것과는 괴리감이 존재했다.

Entity도 결국 Data Holder로써 존재했다.

 

경험 없는 신입의 망상이었던 걸까? 나는 그렇게 생각하지 않고 싶다.

 

 

1년 차 스타트업이었지만 돈을 벌기 위해서는...

사실 위의 문제가 이 항목의 영향을 어느 정도 받았을 수도 있다. 아직 자체 서비스가 없던 회사는 SI, SM 사업을 통해 돈과 레퍼런스를 쌓아가고 있었다.

 

회사의 인력이 모두 개발자였지만 다들 담당하는 것들이 많았기 때문에 일정에 많이 쫓기곤 하였는데, 이게 새로운 변화(설계, 방법론, 프로세스 등의 구상)를 가져올 수 없었던 주요한 요인인 것 같다.

 

회사의 개발 문화적인 부분(스프린트, 스터디와 발표, 지라를 이용한 작업 관리 등)도 좋았었지만, 사원들이 늘어나고 프로젝트가 추가됨에 따라 확장되어야 하는 요소들이 일정 때문에 방치되기도 했다.

 

 

개발자의 편의를 위한 개발

이 항목은 개인적으로 많은 사람의 생각이 갈릴 것 같은 내용이다. 그렇기에 내 사심(?)을 적기보단 두 가지 사례만 보이고 넘어갈 생각이다.

  1. "개발 편의를 위해서 구현된 REST API가 응답하는 DTO와 Entity의 Model이 대부분 혹은 완전히 같았다." 이 말은 즉 매 요청이 Over fetching 되었다는 이야기이며, 변경되는 요구사항에 따라 이러한 값들을 프론트에서 필터링을 했다.
  2. 실제 비즈니스 개발 시에는 필요 없는 Lombok Annotation들을 Test code 나 혹시나 사용할 수 있는(?) 상황을 위해 모두 달아놓고 개발했다.

 

 

물론 회사의 좋은 점도 많았다.

 

개발자들이 학습하는 것에 열린 사고를 하고 있었다.

  • 여유만 있다면 스터디 주제를 정해 정리하고 발표 일정을 잡은 뒤 회의실을 빌려 주제에 대한 리뷰 등을 진행할 수 있었다.
  • 흥미 혹은 필요 때문에 Sample Project를 만들어서 시연해보는 때도 있었다.

 

 

연차와 직급에 상관없이 맞는 말을 하는 사람의 의견을 수용하는 편이었다.

  • 모델 설계나 요청 흐름에 있어서 제시한 상황 혹은 의견을 바로 프로젝트에 반영시키기도 하였다.

 

 

모난 사람은 없었다. (그럼 내가 문제인가..??)

 

 

막히는 부분이 있다면 다른 개발자와 같이 이야기하며 나아갈 수 있었다.

  • 디버깅 같은 경우 Code with me를 사용하여 같이 살펴보기도 했었다.

이 내용은 이만 줄여야 할 것 같다. 그래도 회사 분들과 나쁘게 끝나진 않았다. (라고 생각한다..)

 

 

 

 

그래서 면접을 봤다.

위와 같은 상황에서 현재 처우까지 생각을 비집고 들어오니 여러 불만을 가지게 되었다. 그래서 6월 중순쯤 3개의 회사에 지원하게 되었는데 직접 언급하는 것은 조심스럽기 때문에 알파벳 순서로 명명하겠다.

 

3곳만 지원한 이유는 회사에 다니면서 이직 준비를 병행하여야 했고, 제한된 자원(시간, 연차..) 때문에 한 번에 여러 곳을 지원하는 것은 어렵다고 생각해서였다. 다 떨어질 때에는 9월이나 10월쯤 다시 지원하려고 했었다.

 

 

코딩 테스트

해당 부분은 거의 준비하지 못했고, 문자열 위주의 문제만 20개 정도 뽑아 풀었던 것 같다. (브3~실3?) 그렇게 효과를 보진 못했다.

 

 

CS

개인적으로 정리하던 면접 질문과 Github의 몇몇 Repository를 참고하여 준비하였다.

  • 운영체제, 네트워크, 간단한 암호학, 자료구조

보았던 것 대비 면접에서 나온 빈도는 30% 정도였던 것 같다.

 

 

언어

Java를 학습했을 때 정리했던 노션 글들을 2번 정도 읽었는데, 열심히 정리한 덕분에(?) 금방 복기할 수 있었다.

보았던 것 대비 면접에서 나온 빈도는 10~15% 정도였던 것 같다.

 

 

프레임워크

Spring Framework는 사실 아예 준비하지 않았다..

포지션 자체는 모두 Java 혹은 Kotlin 기반의 Spring 이였는데 A사 면접에서 나온 3개의 질문 빼고는 물어보지 않았다.

 

 

이력서

작성한 이력서의 프로젝트를 위주로 준비하였었는데, 예상치 못한 질문을 받기도 했다.

면접마다 2~3개 정도의 질문이 나왔다.

 

 

 

A사

여러 분야의 소프트웨어를 개발하고 있는 중견 기업이었다.

 

 

전형 프로세스

서류 → 코딩 테스트 → 전화 면접 → 최종 면접 → 합격 발표

 

 

정리

해당 회사의 경우 추천인을 통해서 전형을 진행하다 보니 서류의 경우 금방 통과가 되었으며, 코딩 테스트를 진행하게 되었지만 나온 문제에 대해 잘못 접근하여 완전히 풀지 못했었다.

이때 "마이너스로 깔고 들어가는 것 같아서 통과하면 면접 경험이라도 쌓아야지…." 했었다.

 

다행히 기회를 주셨는지 다음 전형을 진행할 수 있었다.

 

 

전화 면접의 경우 35분 정도 진행되었으며, 기술 위주로 질문을 받게 되었다.

 

자바 GC 위주 질문, SOLID 원칙, Proxy, AOP, 자료구조, HTTP 관련, 데이터베이스, JWT, AWS, CI/CD 질문 등이 나왔었다. 3개의 질문을 제외하고는 다 어느 정도 대답을 할 수 있었고, 좋게 보셨는지 통과할 수 있었다.

 

 

최종 면접의 경우 화상으로 1시간 정도 진행되었으며, 이것 또한 기술 위주로 질문을 받게 되었다.

 

코딩 테스트에서 나왔던 문제에 대한 이야기, 이력서에 있던 Docker, Redis나 RabbitMQ 등을 어떻게 알게 되었고 어떤 학습 방법을 취했는지, 어떤 개발 방식을 선호하는지, 자료구조, 스프링, JPA, JWT, 네트워크, 젠킨스 등을 질문받았으며, 상황을 주고 어떻게 데이터를 처리할 것인지도 물어보았다. 그 시간 동안 최대한 내 생각을 말하기 위해 노력했다.

 

그리고 약 10일 뒤 기대하지 못했던 최종 합격 통보를 받을 수 있었다. 이번 달 26일에 입사할 예정이고 그전까지 개인 공부를 하며 쉬고 있다.

 

 

 

B사

차량과 관련해서 사업을 벌이고 있는 기업이었다.

 

 

전형 프로세스

서류 → 전화 면접 → 기술 면접 → 최종 면접 → 합격 발표

 

 

정리

해당 회사도(?) 추천인을 통해서 전형을 진행하게 되어 서류는 면제되었으며, 지원한 지 1주일이 안 되어서 바로 전화 면접을 진행하게 되었다.

 

 

전화 면접의 경우 13분 정도 진행되었으며, 2개의 질문을 받을 수 있었다. 하나는 CS 질문이었고, 하나는 경험에 대한 질

문이었던 것으로 기억한다.

 

경험을 물어보는 질문에 대해서 생각해본 적이 없었기에 다른 상황으로 대체하여 대답하였고 해당 면접을 통과하여 기술 면접을 진행하게 되었다.

 

 

기술 면접은 대면 면접으로 진행하였으며, 이력서 위주 질문, 자바 JVM, GC 관련 질문, OS, 대용량 처리 관련 질문, JPA와 쿼리 매퍼에 대한 질문 등을 받았고 상황을 제시받아 특정 데이터를 처리하는 방법들을 제시하는 질문을 받았다. (마지막의 경우 정말 생소한 경험이었고 정말 많이 떨었던 것 같다.)

 

면접을 보고 나오면서 아쉬운 것들이 정말 많았었는데, 그 감이 정확했던 것인지 탈락하게 되었다.. 

 

이 회사의 면접을 보면서 자료구조, 알고리즘 지식의 중요성을 다시 한번 되뇔 수 있었다.

 

 

 

C사

부동산과 관련된 스타트업이었으며, 특별한 점은 로켓 펀치에서 공고 지원 제안이 와서 이력서를 넣게 되었다.

 

 

전형 프로세스

서류 → 기술 면접 → 합격 발표

 

 

정리

로켓 펀치를 잘 들여다보지 않다 보니 제안을 늦게 확인하여 5일 후에 이력서 지원을 했었는데 TO가 1명뿐이었는지 그 사이에 개발자를 채용하였었고, 해당 내용을 안내받고 탈락하게 되었다.

 

 

 

요즘 인생을 정말 알차게 보내고 있는 것 같은데.. 번아웃을 좀 조심해야 할 것 같다. (약간 낌세가 오고 있다...)

메인 이미지는 https://wiki.openjdk.java.net/display/zgc/Main 에서 가져왔습니다.

최근 ZGC에 대한 질문을 받았으나 대답하지 못해 아쉬웠던 상황이 있었습니다. 완벽하게 이해하고 쓰는 글이 아니라 학습을 위해 정리하는 글이기 때문에 잘못된 내용이 있을 수 있으니 그 점 양해 부탁드립니다.

 

 

ZGC

Heap Memory 공간이 커지더라도 S-T-W 시간이 증가하지 않는 특징을 가진 GC이며 각각의 객체를 단일 세대로써 관리되며, 객체들은 G1 GC와 유사한 Region 방식으로 관리된다.

https://hub.packtpub.com/getting-started-with-z-garbage-collectorzgc-in-java-11-tutorial/


해당 GC는 Java 11부터 Preview로 추가되었으며, 15에서 Production Ready 상태가 되었다.

 

 

ZGC의 목표

G1 보다 처리량이 15% 이상 떨어지지 않으면서, S-T-W 시간은 10ms를 초과하지 않는 것.

  • S-T-W 가 매우 짧다.
    http://cr.openjdk.java.net/~pliden/slides/ZGC-FOSDEM-2018.pdf

 

 

ZGC의 특징

대기 시간이 낮으면서 규모 확장이 가능한 GC이다.

  • GC에 관련된 모든 작업을 Application과 동시에 작업한다. (Concurrently)
    • CMS나 G1의 경우 Mark 작업의 일부를 Application과 Concurrently 하게 수행한다.
    • Application이 수행되는 동안 GC Thread를 여러 개 동작시킬 수 있다.
      • 동시 수행 Thread가 적은 경우에는 Garbage가 점점 누적되며 (메모리 누수), Thread가 많은 경우에는 Application의 CPU 수행 시간을 많이 소모함으로 요청 처리량을 떨어트린다.

 

 

ZGC Core Concepts

ZGC는 Colored pointers와 Load barriers라는 2가지 주요 요소를 사용한다.

 

 

Colored pointers

https://hub.packtpub.com/getting-started-with-z-garbage-collectorzgc-in-java-11-tutorial/

  • ZGC가 객체를 찾아낸 뒤, 마킹하고, 재 배치하는 등의 작업을 지원한다.
  • 객체 포인터의 메모리 공간을 활용하여 객체의 상태 값을 저장하고 사용한다.
  • 해당 알고리즘 방식은 64bit 메모리 공간을 필요로 하기 때문에 32 bit 기반의 플랫폼에서는 사용이 불가능하다.
  • 18 bit의 미 사용 공간, 42 bit의 객체의 참조 주소와 총 4 bit의 공간을 차지하는 4개의 color pointer가 존재한다. 이러한 bit들을 meta bits라고 한다.
    • Finalizable : Finalizer(Finalize queue??)을 통해서 참조되는 객체로 해당 pointer가 Mark 되어 있다면 non-live Object이다.
    • Remapped : 해당 객체의 재배치 여부를 판단하는 pointer이며, 해당 Bit의 값이 1이라면 최신 참조 상태임을 의미한다.
    • Marked 0, Marked 1 : 해당 객체가 Live된 상태 인지 확인하는 여부이다. - Load Barrier에 의해서도 사용되기도 한다.

 

 

Load Barriers

http://cr.openjdk.java.net/~pliden/slides/ZGC-Jfokus-2018.pdf

  • JIT가 특정 위치에 주입한 코드를 말한다. 이 코드를 통해 참조가 연결되는 객체의 Meta bits 상태를 확인한다.
  • Load barriers는 RemapMark와 Relocation Set을 확인하여 참조 값과 Mark 상태를 업데이트할 수 있다.

http://cr.openjdk.java.net/~pliden/slides/ZGC-Jfokus-2018.pdf

  • Load Barriers는 Thread가 Stack으로 Heap Object 참조 값을 불러올 때 실행된다.

 

 

Reference Value Check Flow

  • mark pointer의 색이 나쁜 경우 mark, relocate, re-mapping을 진행하여 좋은 상태 (색상)로 변경하는 작업을 진행한다. repair or heal
  • mark pointer의 색이 좋은 경우 그대로 작업을 진행한다.
  • Remap bit가 1인 경우 바로 참조 값을 반환하며 그렇지 않은 경우에는 참조된 개체가 Relocation Set에 있는지 확인한다.
  • Set에 없는 경우 Remap bit를 1로 설정한다. (재 배치 되었음을 의미하기에)
  • Set에 있는 경우에는 Relocation 하고 forwarding table에 해당 정보를 기록한 뒤 Remap bit를 1로 설정한다.
  • 참조 값을 반환한다.

 

 

Forwarding table

Relocation 대상인 객체의 현재 참조 값과 변경 후 참조 값을 기록하는 일종의 Mapping Table을 말한다. 이를 이용하여 현재 Relocation 된 객체를 바로 접근하고 참조할 수 있다.


 

ZGC Flow

http://cr.openjdk.java.net/~pliden/slides/ZGC-FOSDEM-2018.pdf

 

 

ZGC Marking Flow

해당 Flow는 Pause Mark Start → Pause Mark End를 포함한다.

 

Root Set Mark (S-T-W) :

  • 객체를 참조하는 Root set을 찾아 Marking 하는 작업이다. Root Set은 상대적으로 적기 때문에 매우 짧은 S-T-W를 가진다.


Concurrent Mark & Concurrent Remap

  • Application과 동시에 수행되는 단계로 Marking 된 Root set으로부터 객체 간의 참조 관계(그래프)를 추적하여 접근한 모든 객체를 Marking 한다. (Marked bit check)
  • Load barrier를 활용하여, Marking 되지 않은 Object load를 감지하고 해당 객체의 mark pointer도 표시한다.


Concurrent Prepare & Edge Handle ( Week Reference Clear... S-T-W)

  • Local Thread 간의 동기화를 진행한다. Thread local handshakes
  • 이후 Week, Phantom Reference와 같은 일부 edge case를 확인하고 정리한다.

 

 

 

ZGC Relocation Flow

해당 Flow는 Pause Relocation Start ~ End를 포함한다.



Concurrent Relocate

  • Mark Flow가 끝나고 재배치할 대상을 찾아 Relocation Set에 배치한다.
  • Mapping 되지 않은 대상들은 Heap Memory에서 정리한다.
  • Relocation Set에 연결된 대상 중 Root Set을 통해 참조되는 모든 객체를 재 배치 후 업데이트한다.


Concurrent Relocation and update

  • Relocation Set에 남아있는 대상들을 추적하며 재배치하고 이전 참조 값과 변경된 참조 값을 Mapping 하는 forwarding table에 저장한다.
  • Load barrier를 이용하여 Relocation Set에 배치된 대상을 참조하는 Pointer를 감지할 수 있다.


이후 생성되는 참조 관계는 다음 Mark 단계부터 다시 진행된다.


 

참고 자료, 출처

 

점층적 생성자 패턴?

개발을 하다 보면 특정 클래스의 상태, 선택적인 매개변수가 많아질수록 점층적인 모양의 생성자들이 만들어지게 되는 경우가 있습니다. 그렇기에 코드에서 필수적인 매개변수 생성자부터 선택적인 변수 1.... N개 생성자까지 5개 이상의 생성자를 보게 될 수도 있습니다.

public Sample(Long id, String name) {
//...
}

public Sample(Long id, String name, Int age) {
//...
}

public Sample(Long id, String name, Int age, String profile) {
//...
}

// ~~~

public Sample(Long id, String name, Int age, String profile, String email, LocalDateTime createBy, LocalDateTime updateBy) {
//...
}

이러한 클래스를 비즈니스 로직, 전달되는 값에 따라 상황에 맞게 생성하기 위해 생성자 중 하나를 선택하게 되거나 의미를 위해 Item1에서 설명된 정적 팩토리 메서드를 사용할 수도 있습니다.

 

개인적으로 다양한 생성자와 (개발자를 위해) 의미를 부여하기 위한 정적 팩토리 메서드가 클래스에 많이 존재하는 것도 코드를 보는 입장에서는 보기 좋지는 않다고 생각합니다.

 

 

자바 빈즈 패턴?

이러한 문제의 대안으로 자바 빈즈 패턴이 존재합니다. 해당 방식은 No-arg 생성자를 통해 인스턴스를 생성하고 각각의 필드를 Setter를 이용해 값을 저장하는 것인데, 이 패턴도 코드를 작업하다 보면 필드를 누락할 수 있고, 그로 인해 발생하는 예외 지점을 찾는 것은 생각보다 쉽지 않습니다. 세터 자체가 어떠한 의미를 지니지도 않기 때문에 별로 좋아하지 않습니다.

 

추가적으로 해당 객체를 불변으로 만들 수 없고. 상황에 따라 스레드 안정성도 고려하여야 합니다.

Sample sample = new Sample();
sample.setId(1L);
sample.setName("Lob");
sample.setProfile("Serrl");
sample.setEmail("Example@hello.world");
sample.setCreateBy(LocaldateTime.now());
sample.setUpdateBy(LocaldateTime.now());

 

빌더 패턴?

이러한 상황에서 점층적인 생성자 패턴의 일관성, 안정성과 가독성을 챙기는 대안이 나오게 되는데 그것이 바로 빌더 패턴입니다.

public static class Builder {

    private Long requestId;
    private String requestCode;
    private Long userId;
    private String hrOrgan;
    private String username;
    private String password;
        private LocalDateTime createDate;

    public Builder requestId(Long val){
        requestId = val;
        return this;
    }

    public Builder requestCode(String val){
        requestCode = val;
        return this;
    }

    public Builder userId(Long val){
        userId = val;
        return this;
    }

    public Builder createDate(LocalDateTime val){
        LocalDateTime = val;
        return this;
    }

    public Builder hrOrgan(String val){
        hrOrgan = val;
        return this;
    }

    public Builder username(String val){
        username = val;
        return this;
    }

    public Builder password(String val){
        password = val;
        return this;
    }

    public ExcelFileDto build() {
        return new ExcelFileDto(this);
    }
}

protected HostRequest() {
}

public HostRequest(Builder builder){
    requestId   = builder.requestId;
    requestCode = builder.requestCode;
    userId      = builder.userId;
    createDate  = builder.createDate;
    hrOrgan     = builder.hrOrgan;
    username    = builder.username;
    password    = builder.password;
}

//....................

HostRequest request = new HostRequest.Builder()
        .requestId(1L)
        .requestCode("code")
        .userId(1L)
        .hrOrgan("platform service")
        .username("Lob")
        .password("examplePassword")
        .createDate(LocalDateTime.now())
        .build();

해당 예시의 경우 클래스 선언부에 빌더 코드가 많이 생성되지만, 사용하는 지점에서는 메서드의 이름으로 가독성이 향상되고 메서드 체이닝을 통해 인스턴스 자체가 원자적으로 생성되는 것처럼 보이는 효과를 지닙니다.

 

빌더 패턴을 사용하면서 클래스 래밸에서는 이러한 코드를 감추기 위해 Lombok의 Builder를 사용합니다.

해당 기능의 경우, Type래밸에서 Lombok의 No-Arg, Required 생성자와 같이 사용할 때 이슈가 합니다. 그렇기에 아래와 같이 생성자를 정의하고 해당 어노테이션을 사용하는 것을 선호합니다.

@Builder
public HostRequest(Builder builder){
    requestId   = builder.requestId;
    requestCode = builder.requestCode;
    userId      = builder.userId;
    createDate  = builder.createDate;
    hrOrgan     = builder.hrOrgan;
    username    = builder.username;
    password    = builder.password;
}

추가적으로, 계층적인 상속 구조에서 제네릭 빌더를 통해 하위 타입 객체에도 빌더를 상속하는 등 유연성을 지닐 수 있다고 합니다. 개인적으로 계층 상속 구조를 자주 사용하지는 않기 때문에.. 해당 부분은 넘어가도록 하겠습니다.

 

 

참고 자료

  • Effective Java 3판

일반적으로 Class의 Instance를 생성하는 방법은 public Constructor를 사용하지만, 그와 별도로 Static factory method를 사용할 수 있습니다.

 

이러한 메서드는 instance를 반환하는 단순한 형태나 별도 로직을 포함하는 형태를 지니게 됩니다.

 

 

Effective Java에서는 이를 통해 여러 장점과 단점이 존재한다고 하는데. 하나하나 정리해보았습니다.

 

 

 

1. Class를 생성하는 행위, 특성에 대해 이름을 부여할 수 있다.


객체의 Constructor 그 자체와 넘기는 Parameter 만으로는 해당 객체의 특성이나 의미를 자세히 알아내기가 어렵고 이를 이해하기 위해 API 문서를 더 확인해야 할 수 있습니다. 이때 Static factory method의 Naming을 통해 뜻을 나타내는 건 개발자에게 좋은 정보가 될 수 있습니다.

List<Value> empties = new ArrayList<>();

List<Value> empties = ArrayListUtils.emptyValues();

위의 예제는 어떠한 비즈니스를 설정하지 않고 단순하게 작성한 것이기에 공감이 되지 않을 수 있습니다. 그런 분들은 다른 상황에서 좋은 케이스를 만나볼 수 있을 것이라고 생각합니다.

 

예제의 의미는 알겠지만 위의 장점을 동의하지 못하신다면 프로젝트, 비즈니스 이해도가 존재하지 않는 프로젝트로 투입되었을 때를 생각해보면 좋을 것 같습니다. 그럴 때에 값 객체나, 도메인 모델 등이 method 방식으로 생성이 된다면 후자가 더 좋은 가독성을 지닌다고 생각합니다.

 

그리고 동일한 시그니처를 가지는 Constructor는 하나만 존재할 수 있습니다. 점층적인 생성자 패턴을 통해 여러 생성자를 생성하고 다른 로직이나 값을 끼워 넣을 수 있지만, 이는 개발자에게 혼란을 줄 수 있습니다.

개인적으로 생성자에 로직이 들어가는 것은 좋다고 생각하지 않습니다.

 

하지만 Static factory method를 사용한다면 method의 이름을 통해 이러한 문제를 해결할 수 있습니다.

 

 

 

2. 객체를 사용할 때마다 새로운 인스턴스를 생성하지 않아도 된다.


Static factory method를 사용하면 내부에 특정 상태를 가지는 인스턴스를 생성하여 가지고 있다가 전달하거나, 사용한, 사용할 객체를 캐싱하여 계속해서 반환할 수 있습니다.

private static class IntegerCache {
        //...
    static final Integer cache[];

    // static block 초기화 로직

        public static Integer valueOf(int i) {
            if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
            return new Integer(i);
        }

캐싱과 관련해서는 Integer class의 cache 배열을 통해 확인할 수 있는데, 인자 값에 따라서 캐싱한 인스턴스를 반환하는 로직을 확인할 수 있습니다.

 

 

 

3. 반환 타입의 하위 타입 객체를 반환할 수 있다.


// Collections의 정적 메서드들

unmodifiableCollection()
unmodifiableSet()
unmodifiableSortedSet()
unmodifiableNavigableSet()
unmodifiableList()
UnmodifiableRandomAccessList()
...

이러한 정적 메서드들은 각각의 하위 구현체를 반환하고 있습니다. 개발자는 직접 그 구현체에 대한 API를 확인하지 않더라도 Static factory method를 통해 이를 반환받고 사용할 수 있습니다.

 

이는 API의 크기와 개발자가 알고 있는 범주를 최소화하고 명시된 메서드의 이름과 간략한 설명을 통해 어떠한 객체를 얻을지 알 수 있어 개발에 대한 편의성을 가지게 됩니다.

 

 

 

4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.


public static List<SimpleGrantedAuthority> ofRole(Domain model) {
    return domain.hasRole() 
         ? Collections.unmodifiableList(new SimpleGrantedAuthority("ADMIN"))
         : ListUtils.emptyRoleList(); // 가상의 Static factory method를 사용하였다.
}

----

List<GrantedAuthority> role = ofRole(model);

권한 상태 정보를 가지는 특정 Domain model이 있다고 가정한 예제입니다. Static factory method 내부에서는 전달받은 model 권한 상태에 따라 두 가지 유형의 리스트를 전달하게 됩니다. 클라이언트에서는 이를 알 수 없지만 로직 자체는 정상적으로 동작하기에, 개인적으로는 이를 통해 상황에 따라 유연한 로직 처리도 가능할 것이라고 생각합니다.

 

그리고 Java API의 EnumSet의 경우에는 전달받은 인자의 길이에 따라서 RegularEnumSet이나 JumboEnumSet 등을 반환하게 되는데 이는 값에 따라 메모리 공간을 개선하는 효과를 가지기도 합니다. 즉 성능이나 메모리 관점에서의 최적화도 Static factory method를 통해 가능합니다.

 

 

 

5. 메서드가 작성되는 시점에는 객체 클래스가 존재하지 않아도 된다.


Static factory method를 구현하는 시점에 Interface를 반환 타입을 지정한다면, 이후 시점에서 Interface의 구현체들이 추가되고 별도 모듈을 통해 주입되더라도 문제없이 주입받아 동작되게 만들 수 있습니다.

 

JDBC와 같이 다양한 벤더들이 하나의 서비스 API를 구성하는 구조(서비스 제공자 프레임워크)에서 서비스 인스턴스를 생성하고 주입해주는 접근 API 모듈은 이러한 특성을 활용한 것입니다.

접근 API 모듈 즉, Driver Manager는 getConnection() 이란 정적 팩터리 메서드를 가지고 있는데, 프로젝트에서 정의한 데이터 소스, 의존성을 통해 MySQL에서 제공하는 JDBC Driver가 추가되고 해당 구현체가 구현한 커넥션을 반환받을 수 있습니다.

 

 

여기까지가 이펙티브 자바에서 다루는 장점입니다. 이번엔 단점을 보도록 하겠습니다.

 

 

 

1. 상속을 하려면 public이나 protected 생성자가 필요하므로 정적 팩터리 메서드만 제공하게 하면 하위 클래스를 만들 수 없다.


사실 이 것은 상속 관점에서의 제한적인 시선이라고 생각이 듭니다. 최근에는 객체에서 기능을 제공하기 위해서 결합성을 올리고, 하위 구현체가 잘못된 동작을 할 수 있는 상속 방식을 사용하는 것보다 상태 변수로 가지는 조합 방식을 더 선호하기 때문입니다.

 

그렇기에 조합 방식을 사용하여 개발하는 사람들에게는 오히려 장점이 된다고 생각이 듭니다.

 

 

 

2. Static factory method에 대한 설명은 프로그래머가 찾기 어렵다.


정적 팩터리 메서드는 생성자처럼 명확한 API 설명을 가지지 않으므로 의미상 이해하기 어려운 부분에 대해서는 찾는 것이 더 어렵게 된다고 합니다. 이건 Java doc의 문제점..?

 

이를 완화하기 위해서 Static factory method를 작성할 때 흔히 사용되는 명명 규약을 준수하는 것이 좋다고 합니다.

from, of, valueOf, getInstance 등이 존재합니다.

 

 

 

참고 자료

  • Effective Java 3판

이 글은 특정 구현에 종속되는 내용을 제외한 이론 위주의 정리 글입니다.

 

AOP (Aspect-Oriented Programming) 란?


AOP는 프로그래밍 개발 사상 중 하나이며, Spring Framework을 구현한 주요한 개념입니다.

 

저는 이 개념을 애플리케이션 내부의 컴포넌트들에 존재하는 비즈니스 로직이 아닌 보일러 템플릿 코드들을 (저는 개인적으로 인프라 로직이라고 명명하여 부릅니다.) 모아 응집시켜 각각의 컴포넌트로 분리하는 것으로 이해하고 있으며, 개발자들은 이를 활용하여 애플리케이션 서비스에는 비즈니스 로직들만을 남겨 개발 유지보수 경험을 향상시킵니다.

 

 

인프라 로직?


Application 전 영역에서 나타날 수 있는, 중복될 수 있으며 비즈니스가 아닌 로직을 의미합니다.

  • 성능 검사, Flag 처리(활성화, 비활성화)
  • 로거 - 로깅
  • 알림
  • 예외처리
  • 인증 - 인가
  • 트랜잭션 처리
  • 의존성 주입

등 실제 도메인에서 필요한 비즈니스 로직이 아닌 것들을 의미합니다.

 

AOP는 OOP를 대체하는 것이 아니라 보완하는 성격의 프로그래밍 사상입니다. OOP는 클래스를 이용하여 역할에 맞게 로직을 응집하고, 가시하게끔 하는 것이고 AOP는 그런 클래스들을 바라보는 거시적인 시점에서 좀 더 역할에 맞게끔 로직들을 분리해냄으로써 컴포넌트의 결합성을 떨어트리고 재사용 가능케하는 것이기 때문입니다.

 

즉 "AOP와 OOP 중 무엇이 좋냐" 라고 비교하는 것은 잘못된 질문이라고 생각합니다.

 

이러한 AOP는 방문자, 데코레이터, 프록시 패턴 등을 통해 적용할 수 있습니다.

 

 

AOP의 개념?


  • Aspect : 비즈니스 로직을 제외한 부가 기능에 대한 코드들을 응집시켜 컴포넌트로 만든 것입니다.
  • Target : Aspect를 적용할 대상을 의미합니다. (Class, Method)
  • Advice : 어느 시점에 Aspect를 적용할지 결정하는 것을 의미합니다.
  • JoinPoint : Advice가 적용될 수 있는 위치들, 즉 Method 진입 지점, 생성자 호출 시점, 객체 동작 시점이나 필드에서 값을 꺼낼 때 등 적용 가능한 다양한 상황을 의미합니다.
  • PointCut : 실제 Advice가 적용될 지점을 설정합니다.

 

 

 

Spring AOP?


Spring AOP는 Spring에서 기본적으로 사용할 수 있는 Dynamic Proxy 기반의 AOP 구현체입니다.

  • JDK Dynamic proxy, CGLIB API 통해 동작합니다.
  • Spring Container에 등록되는 Bean들에만 적용 가능합니다.

해당 라이브러리의 목적은 모든 AOP 스펙을 제공하기보다는 기능을 간편하게 적용하면서 메서드 래밸의 중복 코드의 제거와 객체 간의 강결합을 해결하기 위함입니다.

 

 

DK Dynamic Proxy와 CGLIB가 사용되는 시점?


JDK Dynamic Proxy

대상 객체가 최소 하나의 인터페이스를 구현하였을 경우 사용합니다.

 

JDK Dynamic Proxy의 문제점

Advise 대상이든 아니든 모든 Method Call 마다 reflection API의 invoke를 진행하게 됩니다.

  • 즉 invoke를 우선 진행하고 Advise 유무를 판단합니다.

 

CGLIB

대상 객체가 인터페이스를 가지지 않았을 경우 사용합니다.

  • 인터페이스를 가져도 사용할 수는 있습니다 aop:config의 proxy-target-class를 true로 설정하면 됩니다.
  • 대상 객체가 정의한 모든 메서드를 프록시 하여야하는 경우 사용합니다. 하지만 final 지시자는 Override 할 수 없으므로 Advice 할 수 없습니다.

CGLIB의 문제점

  • 성능면에서 JDK 에 비해 우수하나 final method, class 은 Advice 할 수 없습니다.
  • 버전 별로 API가 급변함으로 호환성이 좋지 않습니다. 그렇기에 하이버네이트와 같은 프레임워크들은 특정 버전을 내장하여 개발됩니다.

 

AOP Weaving


Compile-Time Weaving : AspectJ
컴파일 시에 소스코드를 받아 바이트코드 변환할 때 Aspect를 적용합니다. ( .java → .class )

  • 기존 Java Compiler를 확장한 AspectJ Compiler 라는 것을 사용하게 됩니다.
  • 컴파일 시에 바이트 코드 조작을 통해 구현부에 코드를 직접 삽입하여 위빙을 수행합니다.
  • 해당 방법의 경우 Lombok, MapStruct 과 같은 Compile 시 간섭하는 라이브러리와 충돌이 일어날 수 있다고 합니다.
  • 위빙 방식 중에서 제일 빠른 퍼포먼스를 보여줍니다.

 

Post-Compile Weaving (Binary Weaving) : AspectJ
이미 컴파일된 클래스 파일에 바이트코드를 삽입하여 Weaving을 적용하는 방식입니다. (.class → .jar)

 

 

Class-Load Time Weaving : AspectJ
Class Loader가 클래스를 로딩할 때 바이트코드를 삽입하여 Weaving 합니다. (객체가 메모리에 올라갈 때)

  • Spring Container 에 객체가 로드되기 전에, 객체 정보를 핸들링함으로 성능이 저하됩니다.
  • JVM에서 제공하는 agent를 통해서 기능을 지원받아 적용합니다.

 

Runtime Weaving : Spring AOP
실제 코드에 변형이 존재하지 않으며, 메서드 호출 시 프록시를 통해 이루어지는 방식입니다.

  • Spring Container에 객체가 로드될 때, ProxyPostProcessor와 ProxyFactoryBean을 통해 객체 정보를 생성하고 Bean으로 반환하여 컨택스트에 저장하게 됩니다. 즉 Spring Bean에게만 적용되는 것입니다.
  • 메서드 수준의 AOP 만을 지원합니다.
  • Point Cut에 대한 Advice수가 늘어날수록 성능이 떨어진다는 단점이 있습니다. (성능 퍼포먼스 상 8~35배 차이)

Spring AOP의 ProxyFactoryBean 은 설정 대상 객체의 Interface 유무에 따라 proxy를 자동 설정합니다. 있으면 JDK, 없으면 CGLIB입니다. (Boot 2.0 이후는 밑에 언급하였습니다.)

 

DefaultAdvisorAutoProxyCreator 후처리기가 추가되어 있는 경우에는 ProxyFactoryBean이 없더라도 프록시 설정을 적용할 수 있습니다. 이 빈은 어드바이저 정보를 통해 Bean을 프록시로 Wrapping 합니다.

 

 

Spring AOP와 AspectJ를 언제 사용하여야 할까?


Spring AOP

  • Spring Bean에서 메서드 실행만을 Advice하는 것이 AOP 요구사항의 전부라면 Spring AOP를 도입할만 합니다.
    • AspactJ 컴파일러나 위버 등 별도의 도입 요구사항이 존재하지 않습니다.

AspectJ

  • Spring Container에서 관리하지 않는 객체(도메인 객체 등)를 Advice 해야한다면, AspectJ를 도입하여야 합니다.
  • Self Invocation 시 @Transaction, @Caching 처리를 적용하기 위해서는 AspactJ 를 고려할만 합니다.
  • Public 이외의 메서드, 필드, 클래스 등에 Advice를 적용하고 싶은 경우 AspactJ를 고려할만 합니다.

 

Spring AOP - JDK Dynamic Proxy는 Target 메서드 호출마다 인터셉팅하는가?


Spring은 Bean을 등록할 때 Reader를 통해 읽어들여진 Bean Definition을 Parser로 해석하고 대해 PostProcessor를 통해 등록 Process가 진행되게 됩니다.

 

객체 정보에 선언적인 AOP와 Transaction 등이 적용되었다면, ProxyFactoryBean을 통해 Proxy 객체를 생성하고, 해당 객체를 ApplicationContext에 반환하게 됩니다. 그리고 business Logic에서 DI가 있어야 하였을 때, 해당 Proxy 객체를 Injection하여 Proxy를 통해 Logic을 실행하게 됩니다.

 

이러한 흐름을 가지기 때문에, 어플리케이션에서 Business Logic을 처리할 때 AOP가 적용된 모든 객체 호출은 Proxy를 통해 인터셉트되는 요청 흐름을 가지게 됩니다.

@Autowired
XxxService xxxService;

// 위의 로직은 ApplicationContext에서 발생하는 DL, DI 생략하면 밑의 코드와 같다고 볼 수 있습니다.
XxxService xxxService = (XxxService) Proxy.newProxyInstance(
        XxxService.class.getClassLoader(), new Class[]{XxxService.class},
            (InvocationHandler) (proxy, method, args) -> {
                XxxService xxxService = new DefaultXxxService();
                                Method targetMethod = null;

                                // Verification?
                                if (proxyMethodMap.containsKey(method)) {
                                        targetMethod = cacheMethodMap.get(method);
                                } else {
                                        Object invoke = method.invoke(xxxService, args);
                                        return invoke;
                              }

                // Before Proxy....

                                // Invoke
                Object invoke = targetMethod.invoke(xxxService, args);

                                // After Proxy....

                return invoke;
            });

 

추가적으로 Spring Boot 2.0 부터는 CGLIB 설정을 변경하여 기본적으로 강제하게 됩니다.  spring.aop.proxy-target-class=true 그러므로 인터페이스 유무와 상관없이 CGLIB가 사용됩니다. 

 

 

잘못된 내용은 댓글로 작성 부탁드립니다!

 

 

참고자료

제가 학습했던, 추천받거나 추천드리고 싶은 자료들을 정리한 Github Repo입니다.

 

링크

github.com/Lob-dev/Junior-Back-end-Developer-Concepts

 

개정 이력

- 2021.04.29 14:01 : 공통, 자바, 객체 지향, 스프링 추가 (서적, 인강, 참고로 구분)

- 2021.04.29 14:47 : 내용 보강 (기타)

- 2021.04.30 16:07 : 내용 보강 (자바 서적 추가, 스프링 질문 5개 추가)

- 2021.05.03 01:58 : 개인적으로 추천하는 학습 흐름 추가

- 2021.05.03 11:30 : 서적 관련 설명 추가, 문서 포맷 변경, Java 면접 질문 강화. SQL, 설계 항목 템플릿 추가 

- 2021.05.03 11:30 : 서적 관련 설명 추가, 문서 포맷 변경, Java 면접 질문 강화. SQL 과 설계 항목 템플릿 추가

- 2021.05.05 00:14 : 각각의 설명 수정, 내용 보강

- 2021.05.06 23:01 : SQL 항목 업데이트, 참고 영상 추가

- 2021.05.23 02:09 : Job Interview.md에 채용 공고 링크와 이동욱님 Repo 링크 추가

- 2021.05.23 21:01 : Architecture Cleancode md 내용 업데이트

- 2021.06.03 20:14 : DB 면접 질문 업데이트

- 2021.06.09 10:46 : 스프링, 면접 관련 참고 링크 추가 및 위치 수정 (Common에 있던 Spring 관련 링크 이동)

- 2021.06.30 13:45 : 스프링 입문 부분 내용 보강 ( 개인 프로젝트 진행 전 etc docker, aws 추가 )

 

 

회사에서 발표한 개인 발표 자료를 옮긴 글입니다.

 

 

GC 기본 개념

 

GC는 무엇일까?

GC란 Garbage를 모으는 작업 방식 혹은 작업을 진행하는 모듈들을 의미하는 용어로 이때 Garbage란 애플리케이션에서 사용되지 않는 Object를 의미합니다.

 

Garbage의 여부는 Root Set 즉 접근 가능한 메모리를 통한 참조 관계로 판단하게 됩니다.

 

GC 수행 목적은 한정된 메모리 공간을 계속해서 정리함으로써 공간을 재활용하고 새로운 객체를 할당받기 위함입니다.

 

 

 

GC가 있거나, 없거나..? 차이가 무엇일까?

+ 2023-12-21 : Un-managed language와 Managed language의 유무는 GC로 결정되는 것이 아닌, 중간 언어 (특정 런타임에게 해석되는 바이트 코드 등의 CIL을 의미함)로 변환되고 런타임(CLR)에 의해 실행되는 것을 기준으로 분류됩니다.

 

Manage Code 개념은 Microsoft에 의해 제안되었으며, 관련 문서에서 CIL과 CLR로 분류함을 확인할 수 있었습니다.

 

Un-managed language란?

C, C++, Assembly와 같이 하드웨어에 가까운 언어들이 대부분 이 부류에 속하며, 직접 해석하여 실행할 수 있도록 네이티브 언어로 변환된다는 점과 함께 저수준 API를 통해 직접적으로 CPU나 Memory에 접근, 할당, 해제할 수 있는 특징을 가지고 있습니다.

 

C의 경우 pointer, malloc or calloc, free가 있으며, C++/CLI도 동일한 형태의 흐름을 가져가지만 특별한 경우에 Smart Pointer를 통한 Reference Counting 방식의 GC를 사용할 수 있습니다.

 

이러한 언어적인 특징 때문에 메모리 구조와 관리 방식, 저수준 API와 커널, 시스템 간의 상호 동작 등에 대해서 좀 더 명확히 이해할 수 있고(이해해야 함..) 추상화에 의한 오버헤드도 줄어 상대적으로 더 나은 성능을 제공합니다.

 

하지만 개발자가 예측하지 못한, 혹은 실수를 통해 발생한 메모리 누수를 잡아내기가 어렵고, 규모에 따라 관리 비용이 커지기에 개발 생산성이나 유지보수 측면의 추가적인 비용 문제가 있고 이를 이해하고 숙련된 개발자들을 구하는 것이 어렵습니다.

 

 

Managed language란?

php는 조금..

Java, C#, Javascript와 같이 별도의 런타임 환경에서 동작하는 고수준의 언어들을 의미합니다. 이러한 언어들은 개발자가 직접적으로 메모리에 접근할 방법이 없거나 제한되어 있으며, 추상화된 모듈을 관리 받게 됩니다.

(런타임 엔진 내에서 사용하는 해석 방법은 큰 의미가 없습니다.)

 

이러한 특징 때문에 저수준의 구조, API를 모르는 상태에서도 고수준의 API를 통해 개발이 가능하며, GC나 실행 엔진 등을 통해 일정 부분이 최적화되고 런타임에 의해 자동화된 관리 기능들을 제공받을 수 있습니다.

 

하지만 GC를 통한 메모리 관리 방식은 Un-Managed 언어의 명시적인 메모리 관리보다 느리고 추가적인 리소스를 소모하며, 사용되는 GC 방식에 따라 추가적인 트레이드오프가 발생하게 됩니다.

 

 

 

기본적인 GC 방식

 

Reference Counting

각 객체의 헤더에 해당 객체가 참조되는 횟수를 저장하고 이값을 확인하여 (0인 경우) GC를 진행하는 방식의 GC Collector입니다.

해당 방식은 참조 값이 0이라면 즉각적으로 회수하기에 실시간 애플리케이션 실행에 영향을 주지 않습니다. 하지만 참조 수를 계속해서 최신 상태로 유지하여야 하기 때문에 참조 값 유지 비용이 크게 발생하며, 로직상 의도치 않은 연쇄 GC가 발생할 수 있습니다.

 

PHP, Swift, C++ (Optional)...

 

Incremental Garbage Collection 방식 등이 더 존재하지만, 현재 JVM 환경에서 개발하고 있기 때문에 이 부분은 생략하도록 하겠습니다.

 

 

 

Tracing Garbage Collection

애플리케이션의 실행 중 특정 조건을 만족하는 시점에 동작하는 스레드들을 중지시키고, 메모리 할당 영역에서 객체 간의 참조 관계를 통해 정리 가능한, 불가능한 객체를 식별하고 정리하는 방식의 GC 방식입니다.

Reference Counting 방식의 문제점인 참조 최신화에 대한 유지 비용과 순환 참조 방식을 해결할 수 있지만, 방식의 특성상 애플리케이션이 중단되는 경우가 발생하고 중단 시간을 조정하기 어렵기 때문에 여러 방법들을 도입하여 이를 최소화하거나 우회하게 됩니다.

 

이를 Thread Suspend 혹은 S-T-W라고 하며, 여기서 말하는 여러 방법들이란 처리량 중점, 지연 시간 중점, 처리량, 지연 시간의 균형 배분 등의 GC 콘셉트들을 의미한 것입니다.

 

 

Mark - Sweep Algorithm

Reference Counting의 단점을 해결하기위해 나왔던 Tracing Garbage Collection 기반의 초창기 알고리즘으로 Root Set을 통해 참조 관계를 추적하는 매우 기본적인 알고리즘입니다.

 

이름 그대로 Mark와 Sweep 단계를 가지고 있습니다.

Mark에서는 Garbage 대상이 아닌 객체에 Marking을 진행하게 되는데, 이때 객체 헤더의 Flag 값 등을 이용하게 됩니다. 그리고 Marking이 끝나면 바로 Garbage 대상들을 지우는 작업을 진행하게 되는데 이를 Sweep라고 하며, 이후 Marking 되었던 정보를 초기화합니다.

 

해당 GC 작업 이후 메모리 상태를 확인해보면 이가 빠진 모양의 메모리 분포 상태를 가지는데, 이를 메모리의 단편화 혹은 파편화라고 합니다.

 

이 문제로 인해 실제 메모리 공간을 채우지 못하고 할당 불가능한 상태에 빠져 OOM이 발생할 수 있고, 적절한 메모리 할당 지점을 찾는 오버헤드가 발생하게 됩니다.

 

 

Mark - Sweep - Compact Algorithm

Mark Sweep Algorithm의 메모리 단편화를 개선한 방식의 GC이며, Mark-Sweep 방식과 거의 유사한 메커니즘으로 동작한 뒤에 살아남은 객체를 한쪽으로 모으는 방식입니다.

 

이를 통해 메모리의 단편화를 해결하였지만, 객체의 참조 값이 실제 메모리의 위치 값이기에 살아남은 객체들에 대한 참조 값을 변경, 수정하는 작업 등을 진행하여야 하며 이를 통한 부가적인 중단 시간과 오버헤드가 발생하게 됩니다.

 

 

Copy - Scanvenge Alogorithm

해당 알고리즘은 실제 메모리 영역을 논리적으로 객체가 할당되는 Active 영역과 InActive라는 별도의 영역으로 분리하여 Active 영역 내에서 접근 가능한 객체들을 Marking 하고 이것들을 InActive라고 하는 영역으로 복사한 다음 기존 영역의 접근 불가능한 객체들을 해제하는 방식을 취하고 있습니다.

 

추가적으로 복사된 이후에는 객체 간의 참조 값을 업데이트하면서 한쪽 끝부터 순서대로 할당하게 됩니다. 해당 알고리즘도 Mark - Sweep Algorithm의 메모리 단편화를 해결하기 위해 구상된 방식으로 Generation Algorithm의 기반이 됩니다.

예시는 이렇게 작성되어 있지만 해당 방식의 GC는 일반적으로 Active 영역에 객체가 할당되지 못하는 경우 발생하게 되며, 논리적으로 실제 메모리 영역을 분할하기 때문에 메모리 영역의 크기만큼 객체를 할당할 수는 없습니다.

JVM의 Minor GC도 이 알고리즘을 기반으로 동작합니다.

 

 

Concurrent - Mark - Sweep Algorithm

기존의 Mark Sweep Algorithm 방식을 사용하는 대신에 최대한 작업을 애플리케이션과 동시 수행시키며 발생하는 전체적인 S-T-W 시간을 감소시키는데 중점을 둔 방식입니다.

 

JVM의 CMS GC가 이 알고리즘을 기반으로 동작합니다.

Initail mark -> Concurrent mark -> Concurrent pre-clean -> Remark -> Concurrent sweep ... -> Full GC (Compact)

 

 

 

Generational algorithm and concept in JVM

 

S-T-W (Stop The World)

GC가 발생함으로써 GC 수행 스레드를 제외한 모든 애플리케이션의 스레드가 중단되는 상황을 의미합니다. 

일반적으로 Generation 방식의 Mark, Copy, Sweep, Compaction 시 발생하는 현상입니다.

 

 

Root Set

Root Set은 Garbage Detection을 마킹하기 위한 출발점이며, Heap 외부에서 내부로 접근한 상태의 참조 값들을 의미합니다. Garbage Detection 방식의 GC 모듈들은 Root Set을 기준으로 연결된 참조 관계를 따라 탐색을 진행하게 됩니다. (Mark)

 

 

Root Set이 되기 위한 조건

  • 각각의 쓰레드의 Stack 영역에 존재하는 Local Variable, Operand Stack에 존재하는 참조 값 등
  • Heap 영역에 존재하는 Constant Pool 참조 관계
  • JNI (Java Native Interface)를 통해 생성된 객체들
  • Meta 영역에 존재하는 Load 된 Class 의 Data들
  • Heap 영역 내부에서 다른 객체를 참조 중인 객체

 

Reachable, Unreachable

Heap 영역에 존재하고 있는 객체의 유효한 참조가 존재하는 경우(Root Set을 기준으로 한)를 Reachable이라고 하며, 그렇지 않은 상황을 Unreachable이라고 합니다.

 

파란색은 Reachable, 빨간색은 Unreachable 이다.

 

Root Set에 참조되지 않는다면, 내부에서 다른 객체끼리 참조 관계가 연결되어도 Unreachable 하다.

 

 

Strengths of Reachability

Reachable 한 객체들은 모두 다른 접근성 수준을 가질 수 있으며 각각의 단계는 strongly, softly, weakly, phantomly reachable, unReachable Object이라고 명명합니다.

 

일반적으로 생성되는 Java 객체는 Strongly Reachable Object를 의미하며, 이는 Root Set과의 참조 관계가 연결되어 있다면 제거되지 않는 객체를 의미합니다.

 

그 외의 reachable Obejct들은 별도의 Class 형태로 제공됩니다.

 

Weakly Reachable Object는 WeakReference이나 WeakHashMap으로 제공되며, 해당 객체들은 GC가 발생하였을 때 어떠한 참조 관계를 가지던지 Sweep 됩니다.

이는 JVM이 해당 Object의 참조를 Null로 설정하여 unReachable 한 Object로 만들기 때문입니다.

 

Weakly Reachable Object와 Root Set 혹은 다른 Strongly Reachable Object에게 동시에 참조되는 객체는 Strongly Reachable Object로 취급합니다.

 

Reachability가 강할수록(Strengths) 객체 간의 참조 연결 시 다른 단계의 참조를 덮어씁니다.

 

 

Softly Reachable Object는 SoftReference으로 제공되며, 해당 객체들은 JVM 메모리가 부족한 순간이 오는 경우나 사용되는 빈도수가 높을수록 어떠한 참조 관계를 가지더라도 GC 되지 않습니다.

 

사용 빈도수 계산 XX:SoftRefLRUPolicyMSPerMB = NUMBER

 

strongly Object GC 시간 > NUMBER * Heap Memory에 남은 공간

 

옵션 설정 값이 1000이라면 1,000ms / MB * 100MB = 100,000 ms = 100 sec 즉 회수 시간은 100초

Softly Reachable Object도 Weakly Reachable Object와 같이 GC의 대상이 된다면 참조가 null로 설정되고 Finalization Queue에 포함된 뒤 다음 GC에 메모리가 회수되게 됩니다.

 

Finalization Queue = ReferenceQueue

 

Phantomly Reachable Object는 다른 모든 단계의 형태에 포함되지 않는 Object를 의미하는데, 이러한 객체는 finalize를 통해 다음 GC의 수거 대상으로써 특정 Queue에 포함된 객체들을 의미합니다.

해당 메서드를 호출한 경우 해당 객체는 참조하던 변수가 사라지고 Finalization Queue를 이용하여 참조하게 되지만 바로 GC 되는 것은 아닙니다.

 

JVM GC의 Marking 절차는 Strengths of Reachability 단계에 따라 순서대로 처리하게 됩니다.

Strongly Reachability, unReachability를 제외한 단계를 대상으로 합니다.

 

 

GC Mark 절차

  1. Softly References
  2. Weakly References
  3. finalize
  4. Phantomly References
  5. Memory Sweep

 

 

 

참고, 학습 자료

+ Recent posts