앞서 로그와 로그를 관리하는게 얼마나 중요한지 알아보았는데 중앙 집중식 로그 관리에 대해 알아두면 좋을것 같아서 정리를 했다.

 

중앙 집중식 로그 관리의 필요성

1. 문제 진단과 모니터링

서버에 문제가 발생했을 때, 로그를 통해 문제의 원인을 빠르게 파악할 수 있다. 중앙 집중식 로그 관리 시스템은 모든 로그를 실시간으로 모니터링하고, 이상 징후가 보이면 즉시 알림을 보내어 빠른 대응이 가능하게 해준다.

 

2. 보안 및 규정 준수

로그는 보안 사건을 추적하는 데도 유용하다. 로그를 중앙에서 관리하면, 보안 침해나 이상 행동을 쉽게 감지할 수 있고, 법정 규정을 준수하기 위한 증거 자료로 활용할 수도 있다.

 

3. 성능 분석과 최적화

로그 데이터를 분석하면 애플리케이션의 성능 병목이나 자원 사용 패턴을 파악할 수 있다. 이 정보를 바탕으로 시스템의 성능을 최적화하는 전략을 수립할 수 있다.

 

 

중앙 집중식 로그 관리 시스템의 구성 요소

1. 로그 수집기 (Filebeat, Logstash 등)

  • Filebeat : 각 서버에서 로그 파일을 읽어 들여 중앙 서버(Elasticsearch 등)로 전송하는 가벼운 에이전트이다.
  • Logstash : 수집된 로그 데이터를 필터링, 변환, 그리고 적절한 포맷으로 재가공하여 저장소에 전달하는 역할을 한다.

 

2. 로그 저장소 (Elasticsearch 등) 

  • Elasticsearch : 로그 데이터를 저장하고, 빠른 검색과 분석이 가능하도록 하는 분산형 데이터베이스이다. 수백만 건의 로그도 실시간으로 검색할 수 있다.

 

3. 로그 조회 및 시각화 도구 (Kibana 등)

  • Kibana : Elasticsearch에 저장된 로그 데이터를 시각화하고, 대시보드 형태로 모니터링 할 수 있는 웹 인터페이스 도구이다. 사용자는 Kibana를 통해 로그 검색, 필터링, 시각화 작업을 쉽게 수행할 수 있다.

 

 

중앙 집중식 로그 관리 아키텍쳐

중앙 집중식 로그 관리 시스템은 다음과 같은 흐름으로 구성됩니다:

  1. 로그 생성:
    각 애플리케이션 서버에서 로그가 생성됩니다.
  2. 로그 수집:
    Filebeat나 Logstash 같은 에이전트가 로그 파일을 읽어 옵니다.
  3. 로그 전송:
    수집된 로그 데이터는 네트워크를 통해 중앙 로그 저장소(Elasticsearch)로 전송됩니다.
  4. 로그 저장:
    Elasticsearch는 로그 데이터를 색인(index)하여 저장하고, 빠른 검색이 가능하도록 합니다.
  5. 로그 조회 및 분석:
    Kibana를 통해 사용자가 로그 데이터를 검색하고 시각화하여 분석할 수 있습니다.

 

데이터 흐름과 처리 과정

  • 생성 단계:
    각 서버에서는 Logback이나 Log4J와 같은 로깅 프레임워크가 로그 메시지를 파일에 기록합니다.
  • 수집 단계:
    Filebeat가 정해진 경로의 로그 파일을 지속적으로 모니터링하고, 새로운 로그가 생성되면 이를 읽어들입니다.
  • 전송 단계:
    Filebeat는 읽어들인 로그 데이터를 JSON 등 표준 형식으로 변환해 Elasticsearch 클러스터로 보냅니다.
  • 저장 단계:
    Elasticsearch는 전송된 로그 데이터를 색인하고, 사용자가 빠르게 검색할 수 있도록 합니다.
  • 조회 단계:
    Kibana 대시보드에서 사용자는 검색 쿼리를 입력하거나 필터를 적용해 원하는 로그 메시지를 찾을 수 있습니다.

 

 

로그 전송을 위한 Filebeat 설정 예

 

Filebeat를 우선 설치하고, 각 서버에 다음과 같은 설정 파일 filebeat.yml  을 사용하여 로그를 중앙 서버로 전송한다.

filebeat.inputs:
  - type: log
    enabled: true
    paths:
      - /path/to/your/app/logs/*.log

output.elasticsearch:
  hosts: ["http://elasticsearch_server:9200"]

 

위 설정은 Filebeat가 /path/to/your/app/log/ 폴더에 있는 모든 로그 파일을 모니터링하고, 새로운 로그가 생기면 Elasticsearch로 전송하도록 한다.

 

 

 

중앙 집중식 로그 관리 시스템 구축 시 고려사항

1. 보안 및 개인정보 보호

  • 로그에는 민감한 정보가 포함될 수 있으므로, 로그 데이터에 접근할 수 있는 권한을 제한해야 합니다.
  • 로그 전송 시 암호화와 인증을 적용해, 로그 데이터가 중간에 탈취되지 않도록 해야 합니다.

2. 성능 및 확장성 고려

  • 로그 수집 에이전트(Filebeat 등)가 과도한 자원을 사용하지 않도록 최적화해야 합니다.
  • Elasticsearch 클러스터는 로그 데이터의 양과 검색 빈도에 맞춰 확장할 수 있어야 합니다.

3. 장애 대응 및 복원 전략

  • 중앙 집중식 로그 관리 시스템 자체가 장애가 발생할 경우, 로그를 잃을 수 있으므로 이중화(High Availability)를 고려해야 합니다.
  • 백업 및 복원 정책을 수립해, 중요한 로그 데이터를 안전하게 보관해야 합니다.

우선 로그란 컴퓨터 프로그램이 실핸되는 동안 일어나는 사건이나 상태를 기록한 기록부라고 생각하면 된다.

예를 들어, 사람이 일기를 쓰고 나중에 일기장으로 그날 하루를 돌아보듯 프로그램도 "어떤 일이 일어났는지"를 기록한다. 

 

로그 관리는 문제를 빠르게 찾아내거나, 시스템. 상태를 확인할 때 매우 중요하다.

 

Gradle을 이용한 프로젝트 설정 (Gradle로 개발을 하고 있기 때문에 Gradle에 대해서 작성)

더보기
더보기

Gradle에 대해 모르거나 까먹었을 수 있다. 

Gradle은 프로젝트를 빌드(컴파일, 테스트, 배포 등) 하는 도구이다.  그렇다면 또 빌드에 대해서 헷갈릴 수 있다. 빌드란? 소스코드 파일을 컴퓨터에서 실행할 수 있는 독립적인 형태로 변환하는 과정과 결과를 말한다. 즉, 개발자가 작성한 소스코드, 각각의 파일 자원 ( .xml, .jpa, .jpg, .properties)을 jvm이나 톰캣 같은 WAS가 인식할 수 있도록 패키징하는 과정 및 결과물을 빌드라고 한다.

 

다시 돌아와서 Gradle은 스프링 부트와 안드로이드에서 사용되며 빌드 속도가 Maven에 비해 10 ~ 100배 정도 빠르며, Java, C/C++, Python 등을 지원한다.

 

Gradle의 특징으로는 

1. 가독성이 좋다.

2. 재사용에 용이

3. 구조적인 장점

4. 편리함

5. 멀티 프로젝트

등이 있다. 

 

로그 관리를 위해서는 Spring Boot의 로깅 기능(기본적으로 Logback)을 사용한다.

build.gradle 파일에 아래와 같이 의존성을 추가한다.

plugins {
    id("org.springframework.boot") version "2.7.5" // Spring Boot 버전
    id("io.spring.dependency-management") version "1.0.15.RELEASE"
    kotlin("jvm") version "1.6.21"
    kotlin("plugin.spring") version "1.6.21"
}

group = "com.example"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11

repositories {
    mavenCentral()
}

dependencies {
    // Spring Boot 기본 기능 (웹, 로깅 등)
    implementation("org.springframework.boot:spring-boot-starter-web")
    
    // Spring Boot AOP, 로깅 관련 기능 (Logback은 기본 포함)
    implementation("org.springframework.boot:spring-boot-starter-aop")
    
    // Kotlin 관련 의존성
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

 

 

로그 저장 : Logback과 파일 관리

Logback과 SLF4J에 대해 먼저 알아보자

  • SLF4J는 여러 로깅 라이브러리(Logback, Log4 J 등)를 추상화하여 사용하는 인터페이스이다.
  • Logback 은 Spring Boot에서 기본적으로 사용하는 로깅 프레임워크이다. Logback은 로그를 파일에 저장하거나 콘솔에 출력하는 등의 기능을 제공한다.

 

로그 설정 파일 : logback-spring.xml

Spring Boot 에서는 src/main/resource 폴더 안에 logbak-spring.xml 파일을 만들면, 로깅의 형식, 저장 위치, 파일 회전(일정 시간마다 새 파일로 저장)등을 설정할 수 있다.

<configuration>
    <!-- FILE appender: 로그를 파일에 저장 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 로그 파일 저장 위치 -->
        <file>logs/app.log</file>
        <!-- 로그 파일 회전 정책: 매일 새 파일 생성하고, 30일간 보관 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>logs/app.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <!-- 로그 출력 형식: 날짜, 스레드, 로그 레벨, 로거 이름, 메시지 -->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 콘솔에도 로그 출력 (옵션) -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 루트 로거 설정: 기본 로그 레벨을 INFO로 설정 -->
    <root level="INFO">
        <appender-ref ref="FILE" />
        <appender-ref ref="CONSOLE" />
    </root>
</configuration>

 

위 설정은 

  • logs/app.log 라는 파일에 로그를 기록한다.
  • 매일 새로운 로그 파일을 만들고, 30일 지난 로그는 삭제한다
  • 콘솔에도 로그가 출력되어, 개발 중에 쉽게 확인할 수 있다.

이렇게 이해하면 된다.

 

 

위와 같이 저장한 로그 파일에서 로그를 조회하기 위한 방법은 몇 가지가 있다.

(1) 파일 탐색기나 텍스트 에디터 사용

  • 로그 파일을 텍스트 에디터(VS Code, Notepad++ 등)로 열어 직접 내용을 확인하는 방법

(2) 터미널 명령어 사용

  • tail : 최신 로그 및 몇 줄을 확인할때 사용한다. ( 예 : tail -f app.log )
  • grep : 특정 키워드(ERROR, 특정 메서드 이름)를 검색할 때 사용한다. ( 예 : grep "ERROR" logs/app.log )

 

 

중앙 집중식 로그 관리 시스템 

ELK 스택 : Elasticsearch, Logstash, Kibana 를 사용하면, 웹 인터페이스에서 로그를 필터링 하고 조회할 수 있따.

이 방법은 여러 서버의 로그를 한 곳에서 모아 볼 때 유용하다.

 

ELK 스택의 구성

  • Elasticsearch: 로그 데이터를 저장하고, 빠르게 검색할 수 있는 데이터베이스 역할을 합니다.
  • Logstash/Filebeat: 여러 서버에서 로그 파일을 수집하여 Elasticsearch로 전송합니다.
  • Kibana: Elasticsearch에 저장된 로그 데이터를 시각화하고, 웹 인터페이스에서 검색할 수 있도록 해 줍니다.

ELK 스택의 장점

  • 중앙 집중식 조회: 모든 서버의 로그를 한 곳에서 볼 수 있습니다.
  • 실시간 검색 및 필터링: 원하는 키워드나 시간대에 맞춰 로그를 빠르게 검색할 수 있습니다.
  • 대시보드: 시각화 도구를 사용해 서버 상태를 모니터링하고 문제를 빠르게 파악할 수 있습니다.

 

추후에 중앙 집중식 로그 관리 시스템에 대해서 정리가 필요할 것 같다.

AOP란 무엇일까?

AOP(Aspect-Oriented Programming)는 관점 지향 프로그래밍이라고 부른다. 너무 어려운데 풀어서 설명하면 프로그램의 핵심 로직과 공통 기능(예 : 로깅, 보안, 트랜잭션 관리)을 분리하여 작성할 수 있도록 도와주는 것이라고 보면 된다.

 

즉, 핵심 비즈니스 로직에 영향을 주지 않으면서도 여러 곳에서 반복되는 기능을 한 곳에 모아서 관리할 수 있게 해준다(너무 좋은것 아닌가?)

 

더보기
더보기

더 쉽게 예를 들어 학교에서 학생들이 공부하는 주요 수업(핵심 로직)이 있다고 해보자. 그런데, 시험 감독, 출석 체크, 학부모 연락 등과 같은 공통 작업도 있는데, 이 작업들은 모든 수업에 걸쳐 반복된다. 

AOP는 이처럼 "공통 작업(예 : 시험 감독)" 을 한 곳에서 관리하고, 각 수업(비즈니스 로직)에서는 신경 쓰지 않도록 분리하는 역할을 한다.

 

AOP의 주요 개념

  1.  Aspect (관점) : AOP의 핵심 모듈로, 여러 곳에 적용할 공통 기능을 모아둔 단위이다. 예를 들어, 로깅 Aspect는 애플리케이션 전반에 걸쳐 로그를 기록하는 기능을 한 곳에 모아서 관리한다.
  2.  Join Point (조인 포인트) : Aspect가 적용될 수 있는 지점을 의미한다. 메서드 호출, 예외 발생 등이 Join Point가 될 수 있다.
  3.  Pointcut (포인트컷) : 어떤 Join Poin에 Aspect(Advice)를 적용할 것인지 정하는 조건이다. "어떤 메서드가 호출될 때마다 로그를 남겨라"와 같은 조건을 정의한다.
  4.  Advice (어드바이스) : 실제로 수행되는 작업이다. 
  5.  Weaving (위빙) : Aspect를 실제 코드에 적용하는 과정이다. Srping AOP는 런타임(실행 시)에 동적으로 위빙을 수행하여, 개발자가 별도로 공통 기능 코드를 삽입하지 않아도 자동으로 적용된다.

 

Spring Boot 에서 Spring AOP 설정

별도의 XML 설정 없이, spring-boot-stater-aop 의존성을 추가하기만 하면 AOP 기능을 사용할 수 있다.

build.gradle 예시

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-aop'
    implementation 'org.springframework.boot:spring-boot-starter'
}

 

 

간단한 로깅 Aspect 예시

메서드 실행 전후에 로그를 남기는 간단한 예시

import org.aspectj.lang.ProceedingJoinPoint
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.annotation.Aspect
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component

@Aspect
@Component
class LoggingAspect {
    private val logger = LoggerFactory.getLogger(this.javaClass)

    // 모든 Service 패키지 내의 모든 메서드 실행 시점에 적용
    @Around("execution(* com.example.service.*.*(..))")
    fun logAround(joinPoint: ProceedingJoinPoint): Any? {
        val methodName = joinPoint.signature.name
        
        // 로그 출력 예시: "메서드 시작: getUserById"
        logger.info("메서드 시작: $methodName")  // 예시 로그: [INFO] 2025-02-26 12:34:56 - 메서드 시작: getUserById
        
        try {
            val result = joinPoint.proceed() // 실제 메서드 실행
            
            // 로그 출력 예시: "메서드 종료: getUserById"
            logger.info("메서드 종료: $methodName")  // 예시 로그: [INFO] 2025-02-26 12:34:57 - 메서드 종료: getUserById
            
            return result
        } catch (e: Throwable) {
            // 로그 출력 예시: "메서드 오류: getUserById, 예외: NullPointerException"
            logger.error("메서드 오류: $methodName, 예외: ${e.message}")  // 예시 로그: [ERROR] 2025-02-26 12:34:57 - 메서드 오류: getUserById, 예외: NullPointerException
            throw e
        }
    }
}

 

  • @Aspect: 이 클래스가 Aspect임을 선언한다.
  • @Component: Spring의 빈으로 등록하여 자동으로 관리한다.
  • @Around: 지정한 Pointcut(여기서는 com.example.service 패키지의 모든 메서드)에 대해, 메서드 실행 전후로 Advice를 실행한다.
  • joinPoint.proceed(): 실제 메서드 호출을 진행하는 부분으로, 이 전후로 로그를 남겨 전체 실행 흐름을 모니터링한다.

이렇게 작성한 Aspect는 지정된 패키지 내의 모든 메서드 실행 시 자동으로 로그를 남긴다. 

 

서비스 클래스 예시

package com.example.service

import org.springframework.stereotype.Service

@Service
class UserService {
    fun getUserById(id: Long): User {
        // 사용자 조회 로직...
        return User(id, "Alice")
    }
}

 

이때 getUserById 메서드가 호출되면, Aspect의 joinPoint.signature.name은 "getUserById"를 반환하게 되고, 로그에도 "메서드 시작: getUserById"와 같이 출력된다.

 

하지만 이런 AOP를 사용시 주의할 점이 있다.

 

  • 과도한 사용 주의 : 너무 많이 사용하면 코드의 흐름을 파악하기 어려울 수 있다.
  • 디버깅 어려움 : AOP에 의해 동적으로 적용된 로직은 디버깅 시 실제 코드 흐름을 파악하기 어렵게 만들 수 있다.
  • 성능 고려 : Aspect가 많은 경우, 메서드 호출 전후에 추가적인 처리 시간이 발생할 수 있으니 성능에 미치는 영향을 고려해야한다.

 

 

일을 하다 보면 데이터가 중복으로 들어가 있는 테이블에서 특정 조건을 만족하는 데이터만 조회하는 경우가 정말 많다. 

매번 찾아보기도 힘들고 한번 알아두면 누구보다 빠르게 쿼리를 작성할 수 있기 때문에 처음이자 마지막으로 정리를 하려고 한다. 

 

 

우선 GROUP BY는 각종 집계함수, 그룹함수와 함께 쓰이며 그룹화된 정보를 제공한다. 데이터를 그룹으로 나누어, 그룹별로 집계된 정보를 출력하고 비교할 때 GROUP BY 가 사용된다.

 

GROUP BY로 그룹화된 결과에 대해 조건을 걸 수 있는데 이것이 HAVING 절이다. 그룹별로 적용할 수 있는 함수 및 연산자를 사용할 수 있다.

 

1. HAVING에서 사용할 수 있는 연산자

연산자 설명 예제
= 특정 값과 같은 그룹 선택 HAVING COUNT(*) = 10
!=, <> 특정 값과 다른 그룹 선택 HAVING COUNT(*) <> 5
>, <, >=, <= 특정 값보다 크거나 작은 그룹 선택 HAVING SUM(salary) > 50000
IN 특정 값 목록에 포함된 그룹 선택 HAVING department IN ('HR', 'IT')
NOT IN 특정 값 목록에 포함되지 않은 그룹 선택 HAVING department NOT IN ('HR', 'IT')
BETWEEN 값의 범위를 지정 HAVING SUM(sales) BETWEEN 10000 AND 50000
LIKE 특정 패턴과 일치하는 그룹 선택 HAVING department LIKE 'Tech%'
IS NULL, IS NOT NULL 그룹 내 집계 결과가 NULL인지 확인 HAVING AVG(salary) IS NOT NULL

 

 

2. HAVING에서 사용할 수 있는 집계 함수

HAVING 절에서는 GROUP BY로 묶인 그룹에 대한 집계 함수(Aggregate Function)를 사용할 수 있다.

집계 함수 설명 예제
COUNT(*) 그룹 내 행 개수 HAVING COUNT(*) > 5
COUNT(column) 특정 컬럼 값이 NULL이 아닌 행 개수 HAVING COUNT(department) > 10
SUM(column) 그룹 내 합계 HAVING SUM(sales) > 50000
AVG(column) 그룹 내 평균 HAVING AVG(salary) >= 40000
MIN(column) 그룹 내 최소값 HAVING MIN(salary) > 20000
MAX(column) 그룹 내 최대값 HAVING MAX(salary) < 100000
STDDEV(column) 그룹 내 표준편차 HAVING STDDEV(salary) > 10000
VARIANCE(column) 그룹 내 분산 HAVING VARIANCE(salary) > 5000
MEDIAN(column) 그룹 내 중앙값 (Oracle 전용) HAVING MEDIAN(salary) > 45000

 

 

3. HAVING에서 여러 조건을 조합하는 논리 연산자

여러 조건을 AND, OR, NOT 연산자로 조합할 수 있다.

연산자 설명 예제
AND 여러 조건을 모두 만족해야 함 HAVING COUNT(*) > 5 AND SUM(sales) > 10000
OR 하나라도 만족하면 선택 HAVING COUNT(*) > 5 OR SUM(sales) > 10000
NOT 특정 조건을 제외 HAVING NOT COUNT(*) > 10

 

 

 

4. HAVING 절 예제

 

4-1. COUNT() 사용하여 특정 개수 이상인 그룹만 조회

SELECT department, COUNT(*) AS employee_count
FROM employees
GROUP BY department
HAVING COUNT(*) > 5;

 

* 각 부서별 직원 수가 5명 이상인 부서만 조회

 

 

4-2. SUM() 사용하여 총 매출이 50,000 이상인 그룹만 조회

SELECT region, SUM(sales) AS total_sales
FROM sales_data
GROUP BY region
HAVING SUM(sales) >= 50000;

 

* 각 지역(region)별 총 매출이 50,000 이상인 그룹만 조회

 

 

4-3. AVG() + MAX() 사용

SELECT department, AVG(salary) AS avg_salary, MAX(salary) AS max_salary
FROM employees
GROUP BY department
HAVING AVG(salary) > 40000 AND MAX(salary) < 100000;

* 부서별 평균 급여가 40,000 이상이고, 최대 급여가 100,000 미만인 부서만 조회

 

 

4-4. COUNT(DISTINCT column) 사용하여 중복 제거 후 개수 비교

SELECT category, COUNT(DISTINCT product_id) AS unique_products
FROM products
GROUP BY category
HAVING COUNT(DISTINCT product_id) > 10;

* 카테고리별 고유한 상품 개수가 10개 이상인 경우만 조회

 

 

4-5. HAVING + ORDER BY 함께 사용

SELECT department, COUNT(*) AS employee_count
FROM employees
GROUP BY department
HAVING COUNT(*) > 5
ORDER BY employee_count DESC;

* 직원 수가 5명 이상인 부서를 조회하면서, 직원 수가 많은 순으로 정렬

 

 

 

COUNT() 함수

 

오라클에서 COUNT() 함수는 특정 컬럼 또는 표현식의 개수를 세는 집계함수이다.

HAVING COUNT(*) 에서 괄호 안 * 에 들어갈 수  있는 요소를 정리해보자.

 

1. COUNT(*) (전체 행 개수)

SELECT department, COUNT(*) AS total_count
FROM employees
GROUP BY department
HAVING COUNT(*) > 5;

* 각 부서별 총 직원 수가 5명 이상인 부서만 조회

* NULL 값을 포함한 모든 행을 개수로 셈

 

 

2. 특정 컬럼 값 개수( COUNT(column) )

SELECT department, COUNT(salary) AS non_null_salaries
FROM employees
GROUP BY department
HAVING COUNT(salary) > 5;

* 부서별 salary가 NULL이 아닌 행의 개수를 카운트

* NULL 값은 개수에서 제외된다!!!!!!

 

 

3. DISTINCT을 사용하여 중복 제거한 개수( COUNT(DISTINCT column) )

SELECT department, COUNT(DISTINCT job_title) AS unique_jobs
FROM employees
GROUP BY department
HAVING COUNT(DISTINCT job_title) > 3;

* 부서별 중복되지 않은 직무(job_title) 개수가 3개 이상인 부서만 조회

* NULL 값은 개수에서 제외된다.

 

 

4. 특정 조건을 만족하는 값 개수 ( COUNT(CASE WHEN ...) ) 

SELECT department, 
       COUNT(CASE WHEN salary > 50000 THEN 1 END) AS high_salary_count
FROM employees
GROUP BY department
HAVING COUNT(CASE WHEN salary > 50000 THEN 1 END) > 3;

* 급여(salary)가 50,000 이상인 직원 수가 3명 이상인 부서만 조회

* 조건을 만족하지 않는 행은 개수에서 제외됨

 

 

5. 다중 도건 사용 ( COUNT(CASE WHEN ...) 여러개 ) ** 이걸 쓸 일이 많다.

SELECT department, 
       COUNT(CASE WHEN salary > 50000 THEN 1 END) AS high_salary_count,
       COUNT(CASE WHEN job_title = 'Manager' THEN 1 END) AS manager_count
FROM employees
GROUP BY department
HAVING COUNT(CASE WHEN salary > 50000 THEN 1 END) > 3
   AND COUNT(CASE WHEN job_title = 'Manager' THEN 1 END) > 2;

* 급여 50,000 이상 직원 수가 3명 이상이고, 매니저가 2명 이상인 부서만 조회

 

 

6. NULL 값 포함 여부 체크 ( COUNT(NULLIF(column, 조건)) )

SELECT department, COUNT(NULLIF(salary, 0)) AS non_zero_salaries
FROM employees
GROUP BY department
HAVING COUNT(NULLIF(salary, 0)) > 5;

* 급여가 0이 아닌 직원 수가 5명 이상인 부서만 조회

* NULLIF(salary, 0)는 salary가 0이면 NULL 로 변환하여 COUNT 에서 제외시킨다.

 

 

7. 조합 사용 (COUNT + SUM/AVG 등)

 SELECT department, COUNT(*) AS total_employees, SUM(salary) AS total_salary
FROM employees
GROUP BY department
HAVING COUNT(*) > 10 AND SUM(salary) > 500000;

* 직원 수가 10명 이상이고, 급여 총합이 500,000 이상인 부서만 조회

 

 

외울건 아니지만 이런 경우에 이런걸 쓰는구나~ 라는걸 알고 있으면 쿼리를 짤때 도움이 많이 될 것 같다.

 

Spring Boot는 빠르게 프로토타입을 만글고, 반복적인 설정 작업 없이 비즈니스 로직에 집중할 수 있게

Auto Configuration (자동 구성) 기능을 제공한다.

 

한번 Spring Boot Auto Configuration 기본 개념과 동작 원리, 활용예제에 대해서 알아보자.

 

Auto Configuration이란 무엇인가?

Auto Configuration은 개발자가 매번 복잡한 설정 파일이나 보일러플레이트 코드를 작성하지 않아도, 애플리케이션이 실행될 때 필요한 설정을 자동으로 구성해주는 기능이다. Spring Boot는 클래스패스에 포함된 라이브러리와 프로젝트 설정을 기반으로, 애플리케이션에 필요한(Bean)들을 자동으로 등록한다.

더보기

비유를 들어보자면 새로운 스마트폰을 구입했다고 생각해보자, 별도의 복잡한 설정 없이 기본 앱들과 기능들이 이미 준비되어 있는 상태일것이다. Spring Boot 의 Auto Configuration은 바로 그런 스마트폰과 같이, 개발자가 직접 설정하지 않아도 "기본값"으로 모든 것이 준비되도록 도와준다.

 

왜 Auto Configuration이 필요할까?

전통적인 Spring 애플리케이션은 XML이나 자바 기반의 설정 파일을 통해 수많은 설정을 해야 한다.( 정말 정말 복잡하고 귀찮은 작업이다..) 이러한 작업은 시간도 오래걸리고, 실수로 인한 오류가 발생하기 쉽다.

그렇기 때문에  Spring Boot 의 Auto Configuration 은 장점을 갖는다.

 

  • 개발 속도 향상: 복잡한 설정을 자동으로 처리하여 개발자가 비즈니스 로직에 집중할 수 있다.
  • 일관성 있는 설정: 기본적인 설정이 표준화되어 있어, 팀 내에서 일관된 개발 환경을 유지할 수 있다.
  • 쉬운 시작: 처음 프로젝트를 시작할 때 최소한의 설정만으로도 애플리케이션을 실행할 수 있다.

 

Auto Configuration의 동작 원리

Spring Boot의 Auto Configuration은 내부적으로 여러 가지 메커니즘과 어노테이션을 사용해 동작한다.

 

@EnableAutoConfiguration / @SpringBootApplication

Spring Boot 애플리케이션의 진입점에는 보통 @SpringBootApplication 어노테이션이 붙는다. 이 어노테이션은 

@EnableAutoConfiguration, @ComponentScan, @Configuration 등 여러 어노테이션을 합친 축약 표현이다.

@SpringBootApplication
class MyApplication

fun main(args: Array<String>) {
    runApplication<MyApplication>(*args)
}

 

@EnableAutoConfiguration : 해당 어노테이션은 Spring Boot에게 애플리케이션 시작 시 자동 구성을 활성화하라고 지시한다.

 

 

Auto Configuration은 클래스패스에 있는 라이브러리와 설정 파일을 확인한 후, 필요한 빈들을 등록한다.

이를 위해 조건부(Conditional) 어노테이션들이 사용된다. 대표적으로 @ConditionalOnClass, @ConditionalOnMissingBean, @ConditionalOnProperty 등이 있다.

 

예를 들어, H2 데이터베이스 라이브러리가 클래스패스에 있다면, Spring Boot는 자동으로 H2 데이터베이스 관련 빈을 등록한다.

만약 사용자가 직접 데이터 소스(DataSource)를 정의했다면, @ConditionalOnMissingBean 어노테이션 덕분에 자동 구성은 이를 무시하고 사용자가 정의한 설정을 우선시한다.

@Configuration
@ConditionalOnClass(DataSource.class)
public class DataSourceAutoConfiguration {
    
    @Bean
    @ConditionalOnMissingBean
    public DataSource dataSource() {
        // 기본 H2 데이터베이스 설정을 반환
        return new EmbeddedDatabaseBuilder()
                    .setType(EmbeddedDatabaseType.H2)
                    .build();
    }
}

 

 

 

Auto Configuration 파일의 구조

Spring Boot의 Auto Configuration 설정은 보통 spring.factories 파일에 정의되어 있다. 이 파일은 각 라이브러리별로 어떤 Auto Configuration 클래스를 적용할지 명시해 두며, 애플리케이션이 시작될 때 해당 클래스들이 로드된다.

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.autoconfig.DataSourceAutoConfiguration,\
com.example.autoconfig.WebMvcAutoConfiguration

 

 

이렇게 장점만 있을것 같은 Auto Configuration도 한계가 있다.

 

  • 숨겨진 동작:
    자동으로 이루어지는 설정들이 때로는 개발자가 의도하지 않은 방식으로 동작할 수 있으므로, 내부 동작을 잘 이해해야 한다.
  • 디버깅의 어려움:
    Auto Configuration이 복잡하게 작동하는 경우, 문제가 발생하면 원인을 파악하기 어려울 수 있다.
  • 학습 곡선:
    초보자가 Auto Configuration의 내부 메커니즘을 완벽하게 이해하기까지는 시간이 걸릴 수 있다.

이러한 한계에도 불구하고, Auto Configuration은 대부분의 애플리케이션 개발에 있어 생산성과 유지보수성을 크게 향상시키는 도구임에는 분명하다.

자바로 개발을 하다 보면 가장 자주 발생하는 오류 중 하나가 NullPointerException 이다. (진짜...)

 

NullPointerException은 프로그램이 '값이 없다'는 상태인 null 값을 사용하려고 할 때 발생하는 오류이다. 정말 초보 개발자일때 뿐만 아니라 숙련이 되더라도 언제든 큰 골칫거리가 될 수 있다고 생각한다.

 

Kotlin은 이러한 문제를 근본적으로 해결하기 위해 Null Safety라는 개념을 도입했다. 

 

우선 Null에 대해서 정의를 해보자

 

 

Null이란 무엇인가?

Null은 "값이 없음"을 의미하는 특별한 값이다.

프로그래밍에서 어떤 변수에 값이 할당되지 않았거나, 사용할 수 없는 상태를 표현할 때 null을 사용한다. 예를 들어, 친구 목록을 저장하는 변수에 친구가 한 명도 없다면, 그 변수는 null일 수 있다.

var friend: String? = null // 친구가 없는 상태

 

여기서 중요한 점은 null을 허용하는 변수와 허용하지 않는 변수가 있다는 점이다.

 

만약 null인 값을 잘못 사용하면 프로그램은 오류를 내며 멈춰버릴 수 있다. 실제로, 많은 프로그래밍 오류는 이런 null 관련 문제에서 발생한다.

 

 

Kotlin의 Null Safety 기능

Kotlin은 변수 선언 시, 해당 변수가 null을 가질 수 있는지 여부를 명확하게 표시한다. 이를 통해 컴파일러가 자동으로 null 관련 오류를 사전에 경고해준다.

 

위에서 언급했던 Non-Nullable 변수와 Nullable 변수에 대해서 알아보자

  • Non-Nullable 변수 : 변수 선언 시에 기본적으로 null을 허용하지 않는다. 예를 들어, var name: String = "Alice" 라고 선언하면, 이 변수는 null 값을 가질 수 없다.
  • Nullable 변수 : 변수 뒤에 ?를 붙여 선언하면 null 값을 허용한다. 예를 들어, var friend: String? = null 이라고 선언하면, 이 변수는 null일 수도 있고, 실제 문자열 값을 가질 수도 있다.
fun main() {
    // Non-Nullable 변수: null 할당 불가
    var name: String = "Alice"
    // name = null  // 컴파일 에러 발생

    // Nullable 변수: null 할당 가능
    var friend: String? = null
    println("친구: $friend")
}

 

안전 호출 연산자 (?.)

Nullable 변수에 접근할 때, 직접 접근하면 null이 반환될 수 있어 오류가 발생할 수 있다. Kotlin에서는 안전 호출 연산아 "?." 를 사용하여 null일 때 안전하게 처리할 수 있다.

fun main() {
    var friend: String? = null
    // friend가 null이면 toUpperCase()를 호출하지 않고 전체 결과가 null로 처리됨
    println(friend?.toUpperCase())  // 출력: null

    friend = "Bob"
    println(friend?.toUpperCase())  // 출력: BOB
}

 

여기서 friend?.toUpperCase()는 friend가 null이 아니기 때문에 대문자로 변환하고, null이면 null을 그대로 반환한다.

 

 

엘비스 연산자 (?:)

안전 호출 연산자와 함께 엘비스 연산자 "?:" 를 사용하면, null인 경우에 기본 값을 제공할 수 있다.

fun main() {
    var friend: String? = null
    // friend가 null이면 "알 수 없음"을 대신 출력
    val friendName = friend ?: "알 수 없음"
    println("친구 이름: $friendName")
}

 

이 예제에서 friend가 null이기 때문에, "알 수 없음"이 friendName에 할당되어 출력된다.

 

 

let 함수

Kotlin에서는 null이 아닐 때만 특정 작업을 실행하는 간단한 방법으로 let 함수를 제공한다.

fun main() {
    var friend: String? = "Charlie"
    
    // friend가 null이 아닐 때만 블록 안의 코드 실행
    friend?.let {
        println("친구 이름 길이: ${it.length}")
    }
    
    friend = null
    // friend가 null이면 let 블록은 실행되지 않음
    friend?.let {
        println("이 코드는 실행되지 않습니다.")
    }
}

 

위처럼 let 함수는 nullable 변수가 null이 아닐 때 안전하게 해당 값을 사용할 수 있도록 도와준다.

1. 변수와 상수

Kotlin에서는 변수를 선언할 때 var 와 상수를 선언할 때 val 키워드를 사용한다.

  • var : 값이 변경 가능한 변수 
  • val : 값이 변경 불가능한 변수
fun main() {
    var age: Int = 20       // 가변 변수
    val name: String = "Alice"  // 불변 변수 (상수)

    println("이름: $name, 나이: $age")

    age = 21  // 값 변경 가능
    println("변경된 나이: $age")
}

 

 

2. 타입 추론

Kotlin은 변수의 타입을 자동으로 추론한다. 명시적으로 타입을 지정하지 않아도, 초기 값으로부터 타입을 유추할 수 있다.

fun main() {
    val language = "Kotlin"  // String 타입으로 추론됨
    val version = 1.5        // Double 타입으로 추론됨

    println("언어: $language, 버전: $version") // "언어 : Kotlin, 버전 : 1.5"
}

 

따라서 타입을 생략해도 컴파일러가 올바른 타입을 추론해주므로 코드가 간결해진다.

 

 

 

3. 문자열 템플릿

Kotlin에서는 문자열 내에서 변수 값을 쉽게 사용할 수 있는 문자열 템플릿 기능을 제공한다.

fun main() {
    val name = "Bob"
    val greeting = "안녕하세요, $name 님!"
    println(greeting)
}

 

문자열 내에 $name 과 같이 사용하면 해당 변수의 값이 문자열에 삽입된다.

 

 

 

4. 조건문

if 문

Kotlin의 if 문은 값으로 반환될 수 있어, 삼항 연산자 대신에 많이 사용된다.

fun main() {
    val score = 85
    val result = if (score >= 90) "합격" else "불합격"
    println("시험 결과: $result")
    
    
    // 식으로도 사용 가능
    val max = if( a > b) {
    	print("Choose a") // "Choose a" 를 출력하고 max에 a를 대입
        a
    } else {
    	print("Choose b") // "Choose b" 를 출력하고 max에 b를 대입
        b
    }
}

 

when 문

when 문은 여러 조건을 처리할 때 사용하며, Java의 switch 문보다 더 유연하다.

fun main() {
    val day = 3
    val dayName = when(day) {
        1 -> "월요일"
        2 -> "화요일"
        3 -> "수요일"
        4 -> "목요일"
        5 -> "금요일"
        6 -> "토요일"
        7 -> "일요일"
        else -> "알 수 없음"
    }
    println("오늘은 $dayName 입니다.")
    
    
    
    // 값 비교
    val x = 5

	when (x) {
    	1 -> println("x는 1이다")
    	2, 3 -> println("x는 2 또는 3이다")
    	in 4..10 -> println("x는 4와 10 사이에 있다")
    	else -> println("x는 다른 수이다")
	}
    
    
    // 타입 비교
    val data: Any = "Hello, World!"

    when (data) {
        is String -> println("data is a String: $data")
        is Int -> println("data is an Int: $data")
        is Boolean -> println("data is a Boolean: $data")
        else -> println("data is something else: $data")
    }


	// 조건식
    val y = 7

    val z = when {
        y == 5 -> "y는 5이다"
        y == 7 -> "y는 7이다"
    	else -> "y는 다른 수이다."
    }
    println(z)



}

 

 

 

5. 반복문

for 문

for 문은 컬렉션이나 범위(range)를 쉽게 순회할 수 있다.

fun main() {
    for (i in 1..5) {  // 1부터 5까지의 범위
        println("현재 값: $i")
    }
    
    val fruits = listOf("사과", "바나나", "체리")
    for (fruit in fruits) {
        println("과일: $fruit")
    }
}

 

 

while 문

기본적인 while 루프도 Java와 동일하게 사용할 수 있다.

fun main() {
    var count = 0
    while (count < 3) {
        println("카운트: $count")
        count++
    }
}

 

 

 

6. 함수

Kotlin에서 함수는 fun 키워드를 사용해 정의한다.

// 인사를 출력하는 함수
fun sayHello(name: String): String {
    return "안녕하세요, $name 님!"
}

fun main() {
    val message = sayHello("Charlie")
    println(message)
}

 

또, 함수의 몸체가 단 한줄이라면 중괄호와 return 키워드를 생략할 수도 있다.

fun add(a: Int, b: Int) = a + b

fun main() {
    println("5 + 3 = ${add(5, 3)}")
}

 

 

 

7. 클래스와 객체

Kotlin에서는 클래스를 사용해 객체 지향 프로그래밍을 할 수 있다. 

// Person 클래스 정의
class Person(val name: String, var age: Int) {
    fun introduce() = "제 이름은 $name이고, 나이는 $age살입니다."
}

fun main() {
    val person = Person("David", 30)
    println(person.introduce())
    
    // age 값 변경 가능 (var로 선언)
    person.age = 31
    println("나이 업데이트: ${person.age}")
}

 

Person 클래스를 정의하고, 생성자를 통해 name과 age 값을 초기화하며, 멤버 함수 introduce()로 자기소개를 한다.

'Back-End > Spring Boot + Kotlin' 카테고리의 다른 글

Spring Boot Auto Configuration 이해해보기  (2) 2025.03.10
Kotlin의 Null Safety  (2) 2025.03.08
Spring Boot와 Kotlin: 기본 개념과 시작하기  (3) 2025.03.01
REST 와 RESTful  (0) 2024.02.07
Swagger  (1) 2023.11.08

복잡한 시스템을 신속하고 안정적으로 구축하는 것이 중요하다고 생각한다. 이에 따라 개발 생산성과 유지보수성을 극대화할 수 있는 도구와 언어의 선택은 필수적이다. Spring Boot 와 Kotlin은 이런 요구에 부합하는 강력한 조합으로 주목받고 있다.

 

Spring Boot란 무엇인가?

Spring Boot는 전통적인 스프링 프레임워크의 복잡한 설정 문제를 해결하기 위해 나온 경량화된 프레임워크입니다.
주요 특징과 장점을 구체적으로 살펴보면:

1. 자동 구성 (Auto-Configuration)

  • 설정의 단순화:
    Spring Boot는 애플리케이션 시작 시 프로젝트의 클래스패스와 설정 파일을 기반으로 자동으로 적절한 빈(Bean)들을 구성합니다.
    이는 개발자가 일일이 XML이나 복잡한 자바 설정을 작성할 필요 없이 비즈니스 로직에 집중할 수 있도록 도와줍니다.
  • 스프링 부트 스타터 (Starters):
    다양한 기능별 스타터 패키지를 제공하여, 의존성 관리가 간단하고, 필요한 라이브러리를 일괄 추가할 수 있습니다. 예를 들어, 웹 애플리케이션을 위한 spring-boot-starter-web이나 데이터베이스 연동을 위한 spring-boot-starter-data-jpa 등이 있습니다.

2. 독립 실행형 애플리케이션

  • 내장 서버:
    Tomcat, Jetty 등 내장 웹 서버가 포함되어 있어 별도의 서버 설치나 설정 없이 바로 실행할 수 있습니다.
    이로 인해 배포가 용이하고, 초기 개발 및 테스트 환경 구축이 신속해집니다.
  • 실행 파일:
    Spring Boot 애플리케이션은 단일 JAR 파일로 패키징되어, 명령어 한 줄로 실행할 수 있어 운영 환경에 배포하기 편리합니다.

3. 생산성과 유지보수성

  • 간결한 설정:
    복잡한 XML 설정이나 방대한 보일러플레이트 코드를 제거하여, 코드의 가독성과 유지보수성을 높입니다.
  • 모듈화 및 확장성:
    필요한 기능만 선택적으로 사용할 수 있도록 설계되어, 프로젝트가 커져도 쉽게 확장할 수 있습니다.

 

Kotlin의 기본 개념

Kotlin은 JetBrains에서 개발한 현대적인 프로그래밍 언어로, Java와 100% 호환되면서도 코드의 간결성과 안정성을 극대화합니다.
주요 개념과 특징은 다음과 같습니다.

1. 간결한 문법

  • 타입 추론:
    Kotlin은 변수 선언 시 타입을 명시하지 않아도 자동으로 타입을 추론하여 코드의 길이를 줄여줍니다.
    예를 들어, val name = "Kotlin"처럼 간단하게 선언할 수 있습니다.
  • 함수형 프로그래밍 지원:
    람다식, 고차 함수, 컬렉션 처리 등의 기능을 기본 제공하여, 반복적이거나 복잡한 로직을 짧고 명확하게 표현할 수 있습니다.
  • 데이터 클래스:
    단순히 데이터를 저장하기 위한 클래스를 작성할 때, data class 키워드를 사용하면 equals(), hashCode(), toString() 등 유용한 메서드를 자동 생성해 줍니다.

2. Null 안전성 (Null Safety)

  • 컴파일 단계에서의 검사:
    Kotlin은 Null에 안전한 타입 시스템을 도입하여, NullPointerException을 사전에 방지할 수 있습니다.
    변수에 ?를 붙여 Nullable 타입을 지정하는 방식으로, 코드 작성 시 Null 처리를 명확하게 할 수 있습니다.
  • 엘비스 연산자와 안전 호출:
    ?.와 ?: 등의 연산자를 사용하여 Null 체크를 간편하게 할 수 있어, 코드의 안정성을 높입니다.

3. 확장 함수와 프로퍼티

  • 기존 클래스에 기능 추가:
    확장 함수를 사용하면, 기존 클래스의 소스 코드를 수정하지 않고도 새로운 메서드를 추가할 수 있습니다.
    이를 통해 코드의 재사용성과 가독성을 크게 향상시킬 수 있습니다.
  • 프로퍼티 확장:
    확장 프로퍼티를 통해 기존 클래스에 새로운 속성을 추가하는 방식으로, 코드의 유연성을 높입니다.

4. 상호 운용성

  • Java와의 완벽한 호환:
    Kotlin은 JVM 언어로, Java 라이브러리와 자연스럽게 통합됩니다.
    기존의 Java 기반 프로젝트에 Kotlin을 점진적으로 도입할 수 있으며, 두 언어의 장점을 모두 활용할 수 있습니다.

 

왜 Spring Boot와 Kotlin을 함께 배워야 할까?

두 기술을 결합하여 학습하면 다음과 같은 여러 가지 장점이 있습니다.

1. 개발 생산성 향상

  • 간결함과 효율성 극대화:
    Kotlin의 간결한 문법과 null 안전성 덕분에 코드 작성 시간이 단축되고, 오류 가능성이 줄어듭니다.
    Spring Boot의 자동 구성과 스타터 패키지 기능은 복잡한 환경 설정을 단순화하여 개발 초기 진입 장벽을 낮춥니다.
  • 빠른 프로토타입 제작:
    두 기술 모두 빠른 애플리케이션 개발에 최적화되어 있어, 아이디어를 신속하게 구현하고 테스트할 수 있습니다.

2. 최신 개발 트렌드 반영

  • 모던한 언어와 프레임워크:
    Kotlin은 함수형 프로그래밍 패러다임을 포함한 최신 언어 기능들을 제공하며, Spring Boot는 클라우드 및 마이크로서비스 아키텍처에 적합한 설계 방식을 채택하고 있습니다.
  • 지속적인 업데이트와 커뮤니티 지원:
    두 기술 모두 활발한 커뮤니티와 정기적인 업데이트를 통해 최신 개발 트렌드를 반영하고 있어, 미래에도 경쟁력을 유지할 수 있습니다.

3. 안정성과 확장성

  • 코드의 안정성 강화:
    Kotlin의 컴파일 타임 Null 검사 및 안전한 타입 시스템은 런타임 오류를 줄여줍니다.
    Spring Boot의 모듈화된 아키텍처는 애플리케이션이 성장해도 체계적인 관리와 확장이 용이합니다.
  • 비즈니스 로직의 효과적 관리:
    Kotlin의 함수형 프로그래밍 기법과 확장 함수를 활용하면 복잡한 비즈니스 로직을 명확하고 직관적으로 관리할 수 있습니다.

4. 커뮤니티와 생태계의 장점

  • 풍부한 학습 자료와 오픈소스 프로젝트:
    Spring Boot와 Kotlin 모두 전 세계적으로 활발한 커뮤니티가 존재하며, 수많은 튜토리얼, 샘플 코드, 오픈소스 프로젝트가 학습에 큰 도움을 줍니다.
  • 실무 적용 사례:
    많은 기업과 스타트업들이 이미 이 조합을 채택해, 신속한 개발과 유지보수, 높은 확장성을 경험하고 있습니다. 실제 사례를 참고하면, 기술 선택의 타당성을 쉽게 이해할 수 있습니다.

 

'Back-End > Spring Boot + Kotlin' 카테고리의 다른 글

Kotlin의 Null Safety  (2) 2025.03.08
Kotlin 기본 문법  (1) 2025.03.05
REST 와 RESTful  (0) 2024.02.07
Swagger  (1) 2023.11.08
Post API  (0) 2023.11.02

24년 8월달에 개발한 소스를 운영서버에 반영후 새벽시간대 갑자기 테이블의 INACTIVE LOCK이 발생하였다. 

 

INACTIVE LOCK이 발생한 테이블은 기존에 아무런 문제가 없던 SELECT 쿼리였다. 

 

8월달에 적용된 목록은

  1. 새벽시간대 영화관련 서버의 인물 정보를 연동받아 데이터를 최신화하는 배치의 로직 주석처리 
  2. 쿼리 튜닝 2건
  3. 실시간으로 연동되던 이미지 정보를 배치작업으로 변경해 실시간 -> 배치로 변경 

이렇게 3가지 였다. 

 

원인을 찾던 중 3번 항목의 로직에서 트랜잭션 처리가 제대로 되지 않아 발생할 수 있다는 의견이 있어 트랜잭션 처리를 추가후 긴급빌드를 진행하게 되었다. 

 

빌드후 모니터링시에는 아무 문제가 없어 해결된줄 알았지만 이전과 동일한 새벽 시간대에 INACTIVE LOCK이 발생하였다. 이후 원인을 명확하게 발견하지 못해 우선 서비스는 정상적으로 진행되어야 하기 때문에 8월달 개발사항을 적용하기 전의 배포버전으로 원복을 진행하였다. 

 

8월에 적용된 목록중 1번과 2번 항목은 문제가 없을 것으로 판단하여 차주 하나씩 적용 후 모니터링을 진행하기로 하였고, 원인이라고 생각이 되는 3번에 대해 모든 로직과 쿼리등을 전수조사 하였다.

 

해당 로직을 확인해보니 과도하게 모든 쿼리가 MERGE INTO 문으로 처리되어 있다는 것을 발견하였다. 

 

오라클 MERGE

 

MERGE INTO 구문은 두 가지 작업을 결합한 구문이다.

  • UPDATE : 이미 존재하는 데이터를 업데이트 한다.
  • INSERT : 해당 조건에 맞는 데이터가 없으면 새 데이터를 삽입한다.

업데이트와 삽입을 동시에 처리할 수 있는 매력적인 구문으로 조건에 맞는 경우에는 업데이트, 조건에 맞지 않으면 삽입을 진행한다. UPDATE 와 INSERT 구문을 따로 따로 만들지 않아도 되고, 로직이 보기에 더 간결해져 해당 구문으로 INSERT 가 발생할 수 있는 배포작업 테이블을 해당 구문으로 처리하였다. 

 

하지만 MERGE INTO 구문은 업데이트와 삽입 작업이 모두 수행될 수 있기 때문에 이 작업들에 따른 락(LOCK)이 필요하다는 것을 알았다. 

 

오라클에서 MERGE 구문에 의해 발생하는 락은 주로 DML 락 (Data Manipulation Lock)이다. 

  • UPDATE 작업 : UPDATE를 수행하는 동안, 대상 행에 대해 TM락(Table Lock, Row-Level Lock)이 발생하여, 해당 행이 다른 트랜잭션에서 수정되지 않도록 보호한다.              

        * TM 락은 테이블에 대한 락이지만, 행 수준에서 발생하여 동시에 다른 행은 수정 가능하게 한다.

        * UPDATE 작업은 행 수준에서 공유 락(S) 또는 공유 - 행 - SRX 락을 발생시킬 수 있다.

 

  • INSERT 작업 : 새로운 데이터를 삽입할 때는, 새로 삽입되는 행에 대해 TX 락(Transactional Lock, Row Exclusive Lock)이 발생한다. 

        *삽입되는 동안 새롭게 생성되는 행들은 다른 트랜잭션에서 조회하거나 수정할 수 없다.

 

 

오라클의 MERGE INTO는 기본적으로 업데이트삽입 작업을 하나의 트랜잭션 안에서 처리하는 방식이므로, 다음과 같은 락 메커니즘이 발생한다.

  1. 매칭되는 행에 대한 UPDATE:
    • UPDATE 조건에 해당하는 행에 대해 Row Exclusive (RX) 락이 발생한다. 이는 해당 행이 수정 중이기 때문에 다른 트랜잭션이 해당 행에 접근하여 업데이트하거나 삭제할 수 없도록 보호한다.
    • 동시에 다른 트랜잭션에서 해당 행을 조회(SELECT)하는 것은 가능합니다. 즉, 읽기에는 영향을 주지 않으나 쓰기에는 제한이 발생한다.
  2. 매칭되지 않는 행에 대한 INSERT:
    • INSERT 구문은 새로운 행을 삽입하기 때문에 삽입되는 행에 대해 Row Exclusive (RX) 락이 발생한다.
    • 새로 삽입된 행은 트랜잭션이 커밋되기 전까지는 다른 트랜잭션에서 조회하거나 수정할 수 없다.
  3. 테이블 수준의 락(TX 락):
    • MERGE가 수행되는 테이블 자체에는 TX 락이 걸립니다. 이는 트랜잭션 단위로 데이터를 일관되게 관리하기 위한 락으로, 트랜잭션이 완료될 때까지 해당 테이블에 대한 DML 작업이 순차적으로 처리된다

 

락 경쟁과 동시성 문제

  • 경쟁 조건: MERGE INTO 구문은 업데이트와 삽입이 동시에 발생하기 때문에, 다수의 세션에서 같은 테이블에 대해 동시에 MERGE 작업을 수행할 경우 락 경쟁이 발생할 수 있다. 이는 다음과 같은 상황에서 발생할 수 있다.
    • 두 개 이상의 트랜잭션이 같은 행에 대해 업데이트하려고 시도할 때.
    • 두 개 이상의 트랜잭션이 동일한 조건에서 삽입하려고 할 때.
  • 해결 방안: 락 경쟁을 피하기 위해 행 수준에서 락을 최소화하는 방식으로 처리하거나, 가능한 비동시적으로 작업이 이루어질 수 있도록 스케줄링하는 것이 좋다.

 

 

이걸 알았다면 배포작업에 대한 로직을 MERGE INTO 구문으로 처리하지 않았을 것이다....

 

개발을 진행할때 여러가지 요소를 인지하고 시야를 넓게 가져야 겠다. 또 내가 개발하려는 방향이 맞는지 한번 더 확인해봐야겠다. 

 

 

얼마전 배치를 추가하여 운영서버에 반영하였는데 위와 같은 에러가 뜨면서 쿼리들이 Lock에 걸리기 시작했다. Lock이 걸린 세션을 Kill 한 후 killed session의 connection을 정리 하려고 하였으나 lock에 걸려 서비스에 문제가 생겨 서버를 재기동하는 상황까지 발생하였다. 원인 분석이 명확하게 되지 않아 여러가지 방법을 시도하여 보았다.

 

  1. 배치주기 변경 : 기존 5초마다 반복실행하던 배치를 6분마다 작동으로 배치주기를 변경하였다.
  2. Lock이 걸린 쿼리를 사용하는 배치 인덱스 생성 및 쿼리 수정 : Lock 이 걸린 쿼리를 포함하고 있는 배치내에 모든 쿼리들을 전수조사하여 실행속도가 늦거나 인덱스를 타지 않고 Full Scan을 타는 쿼리들을 수정하였다.

위 두가지 개선책을 운영에 반영하여 봤지만 여전히 배치 처리량이 많은 새벽 3시에 쿼리가 Lock이 걸렸다. jboss JDBC 내의 pool size 가 작게 잡혀있어 해당 에러가 발생하였나 알아보았으나 아니였다.

 

이렇게 갈피를 잡지 못하던 와중 로직을 확인해보니 트랜잭션 처리가 되어있지 않은 것이 확인되었다. 배포대상을 조회 후 UPDATE 쿼리 실행 후 트랜잭션 반환이 제대로 이루어지지 않은 것이다.

 

해결책은 

  1. 필요한 곳에 트랜잭션을 선언하여 사용
  2. 함수 자체에 트랜잭션을 선언

 

// 필요한 곳에 트랜잭션을 생성하여 사용
public class A {

	@Autowired
    private DataSourceTransactionManager txManager;
    
    public void B {
    	TransactionStatus transactionStatus = CommonUtils.getTransactionStatus(txManager);
        
        // select - update 로직 
        
        txManager.commit(transactionStatus); // commit 처리
    }

}

 

 

// 함수 자체에 트랜잭션 처리
public class A {
    
    @Transactional
    public void B {
    	
    }

}

 

위와 같이 2가지 방법중 택1을 하여 문제를 해결할 수 있는 것으로 확인되었고, 이후 운영에 반영 후 결과는 지켜봐야 할 것 같다.

+ Recent posts