반응형

1. 클래스

코틀린에서 클래스를 작성하는 방법

class Person(val name: String, val age: Int) {
	fun hello() {
		println("hello I am ${name} and I am ${age} years old")
	}
}

코틀린의 클래스를 작성하는 방법은 자바와 유사하지만 생성자 부분에 있어서 조금 차이가 있다

자바에서 생성자는 클래스 명으로된 생성자를 클래스 내부에 작성하는 반면, 코틀린의 경우 클래스를 선언하는 부분에서 생성자의 파라미터를 지정하는 식으로 작성한다

1) 생성자

코틀린의 생성자는 주생성자와 부생성자로 나뉜다

  • 주생성자 : 클래스를 선언하는 부분에 작성된 것
  • 부생성자 : construct 키워드를 통해 작성된 것, 여러개 가질 수 있음
(1)
class Person(val name: String, val age: Int) {} // 주생성자

(2)
class Person {
    var name: String
    var age: Int

    constructor(name: String, age: Int) { // 부생성자 1
        this.name = name
        this.age = age
    }
    
    constructor(name: String) { // 부생성자 2
	    this.name = name
	    this.age = 10
}

(2)의 경우는 문제가 생길 수 있다 따라서 위처럼하기 보다는 프로퍼티에 기본 값을 할당하는 것이 더 낫다

주생성자를 통해 객체가 생성될 때 자바의 경우 생성자에 로직을 넣어서 처리할 수 있다

코틀린에서는 주생성자에 로직을 넣을 수 없는데, 이를 위해 init이라는 초기화 블록이 존재한다

 

이를 통해 객체 생성 시점에 초기화 로직을 작성 할 수 있다

class Person(val name: String, var age: Int) {

    init { // 초기화 블록
        println("Person is created: $name")
    }
}

 

2) getter, setter

코틀린에서는 기본적으로 프로퍼티에 대해 자동으로 getter, setter 가 생성 된다

(val의 경우 getter만 생성)

 

이렇게 생성된 getter와 setter를 아래와 같이 커스터마이징할 수 있다

var name: String
    get() {
        println("called get")
        return field
    }
    set(value) {
        println("called set")
        field = value
    }

 

field라고하는 식별자를 통해 프로퍼티의 값을 할당하거나 읽는 것을 볼 수 있다

field는 백킹 필드를 참조하기 위한 식별자로 프로퍼티의 실제 저장 공간을 의미한다

 

그렇다면 주생성자를 통해 만들어진 필드에 대해서는 어떻게 getter와 setter를 커스터마이징 할 수 있을까

class Person(private var _name: String) {
		var name: String
				get() {
					println("called get")
					return _name
				}
				set(value) {
					println("called set")
					_name = value
				}
}

 

이처럼 계산 프로퍼티를 이용하여 원래 백킹 프로퍼티를 숨기고, get, set을 커스텀하여 사용할 수 있다

 

백킹 프로퍼티:

  • 실제 데이터를 저장하는 공간이 존재
  • 값 변경 가능(var의 경우)
  • 데이터를 필드에서 직접 읽음

계산 프로퍼티:

  • 데이터를 저장하는 공간 없음
  • 데이터 접근 시 계산 프로퍼티의 getter를 거쳐서 접근

백킹 프로퍼티와 계산 프로퍼티의 차이는 크게 보면 데이터를 실제로 저장하는지 안하는지로 구분할 수 있다

반응형

'공부' 카테고리의 다른 글

Kotlin 기본 문법 정리 - 함수  (0) 2025.07.18
Kotlin 기본 문법 정리 - 타입과 변수  (0) 2025.07.17
Gradle 개념과 역할  (0) 2025.07.15
Redis 개념 정리 및 Spring 활용  (0) 2025.07.15
[DB] S/X Lock  (0) 2025.06.24
반응형

1. 함수

코틀린에서 함수는 fun 키워드를 통해 정의할 수 있다

java에서는 함수를 정의하기 위해서는 class 내에서 함수를 정의할 수 있지만, 코틀린에서는 클래스 외부에서도 함수를 정의할 수 있다.

  • 자바의 경우 함수는 class 내부에 종속된 상태로 구현 가능
  • 코틀린은 class에 종속되지 않고 함수를 구현할 수 있음

코틀린에서 함수는 자바와 동일하게 함수 파라미터와 반환형을 가지는데 아래와 같이 정의할 수 있다

(1)
fun add(x: Int = 1, y: Int): Int {
	return x+y
}

(2)
fun add(x:Int, y: Int): Int = x+y

(3)
val opr: (Int, Int) -> Int = {a, b -> a + b}

val opr = {a: Int, b: Int -> x+y}

 

(1) 코틀린의 일반적인 함수 선언

  • 파라미터를 지정할 때는 형식을 지정해야한다
  • 파라미터의 default value를 지정할 수 있다
    • 함수를 사용할 때 파라미터 명을 지정하여 호출할 수 있다. 이때 파라미터에 값을 대입하지 않는 경우 default value가 존재하면 해당 파라미터의 값은 default value가 된다 ex) add(y=10) ⇒ x = 1, y = 10 파라미터들의 값이 대입되는 것과 같다
  • 함수의 반환형을 지정해야한다
    • 반환이 없다면 반환 타입을 생략해되지만 이럴 경우 반환 타입은 Unit이 된다

(2) (1)의 함수를 단일 식 함수로 선언한 것이다

 

(3) 람다식을 이용하여 함수를 정의한 것이다

  • 1번째 케이스는 파라미터 타입과 반환 타입을 지정하고 람다식으로 함수를 작성한 것이다
  • 2번째 케이스에는 반환 타입이 생략되었는데 이는 람다식의 계산 결과를 통해 반환 타입이 추론 된다

위의 예시에서 보면 한가지 특이해보이는 것이 있는데 람다식으로 작성한 함수를 변수에 할당하는것을 볼 수 있다

코틀린에서 함수는 일급 시민(first-class citizen)으로 함수를 변수에 대입할 수 있다

 

2. 일급시민

일급 시민은 언어에서 개체를 하나의 완전한 값으로 취급하고 다룰 수 있는 것이다

 

일급시민의 조건

  • 변수에 할당 가능
  • 함수의 인자로 전달 가능
  • 함수의 반환값으로 사용 가능
  • 동적으로 생성하거나 조작 가능

자바의 경우 함수는 일급시민이 아니다

물론 자바8 이후에는 람다와 함수 인터페이스가 들어왔지만 이는 함수형 프로그램을 위한 것이다

이를 통해 자바는 함수 인터페이스를 익명 클래스로 구현함으로서 함수형 프로그래밍이 가능해졌다

 

public class FunctionalP {
	public static BiFunction<Integer, Integer, Integer> add() {
		return (x, y) -> (x + y);
	}
	
	public static void main(String[] args) {
		
		// 람다를 통한 익명 클래스 구현
		BiFunction<Integer, Integer, Integer> func1 = (x, y) -> x+y;
		BiFunction<Integer, Integer, Integer> func2 = add();
		
		System.out.println(func1.apply(1,2));
		System.out.println(func2.apply(3,4));
	}
}

 

자바에서 함수가 일급시민이 아닌 이유

  • 독립 객체가 아니다 독립 객체 인 것처럼 보이나 실제로는 인터페이스를 구현한 것
  • 자바에서 함수는 값이 아닌 클래스 내 행동(메소드)이다
반응형

'공부' 카테고리의 다른 글

Kotlin 기본 문법 정리 - 클래스(Class)  (0) 2025.07.21
Kotlin 기본 문법 정리 - 타입과 변수  (0) 2025.07.17
Gradle 개념과 역할  (0) 2025.07.15
Redis 개념 정리 및 Spring 활용  (0) 2025.07.15
[DB] S/X Lock  (0) 2025.06.24
반응형

코틀린은 컴파일 시 타입을 체크하는 강타입 언어 이다

 

1. 타입(Type)

코틀린에서는 원시 타입(Primitive Type)을 사용하지 않는다

객체 타입으로만 처리한다

 

정수 타입

- Int, Byte, Short, Long

실수 타입

- Float, Double

문자 타입

- Char

불리언 타입

- Boolean

문자열 타입

- String

 

2. 변수

코틀린에서 변수를 선언하는 방법은 var과 val로 두가지가 있다

 

var(variable)의 경우는 변수에 저장된 값을 변경할 수 있다

// var 변수의 값 변경 예시

var a:Int = 10	// 변수 a를 선언하면서 10으로 초기화
a = 20		// 변수 a에 저장된 값을 20으로 변경

 

val(value)은 상수로 선언 시 값을 초기화하고 이후 값을 변경 할 수 없다(상수 이기 때문)

// val 상수 값 변경 불가능 예시

val b = 10	// 상수 b를 선언하고 값을 10으로 초기화, 타입 추론으로 Int타입 명시 생략
b = 20		// 상수 b의 값을 20으로 변경하려고 함으로 컴파일 불가능

 

타입 추론

코틀린의 경우 타입 추론을 지원한다

그렇기 때문에 대입하는 값에 따라 선언 시점에 타입이 지정된다(선언시 지정된 타입은 이후 변경되지 않음)

// 타입추론 예시

val a:Int = 10	// 타입을 Int로 명시
val a = 10	// 타입을 생략했으나 타입 추론에 의해 Int

 

반응형

'공부' 카테고리의 다른 글

Kotlin 기본 문법 정리 - 클래스(Class)  (0) 2025.07.21
Kotlin 기본 문법 정리 - 함수  (0) 2025.07.18
Gradle 개념과 역할  (0) 2025.07.15
Redis 개념 정리 및 Spring 활용  (0) 2025.07.15
[DB] S/X Lock  (0) 2025.06.24
반응형

Gradle 이란?

Gradle Build Tool is a fast, dependable, and adaptable open-source build automation tool with an elegant and extensible declarative build language.

 

이라고 공식 문서에서 이야기하고 있다

Gradle은 빌드 자동화를 위한 빌드 툴 이다

Gradle은 Groovy, kotlin으로 작성되며, 작성된 정보를 바탕으로 빌드, 테스트, 배포를 자동화 한다

Gradle의 핵심 Concepts

  • Projects 프로젝트는 단일, 멀티 모듈 프로젝트로 구성 할 수 있다
  • 단일 프로젝트의 경우 하나의 루트 프로젝트를 가진다
  • 멀티 모듈 프로젝트는 하나의 루트 프로젝트와 하위에 1개 이상의 서브 프로젝트를 가진다
  • Build Scripts Gradle이 프로젝트 빌드를 위해 수행할 내용을 작성하는 것으로 각 프로젝트는 하나 이상의 빌드 스크립트를 포함할 수 있다
  • Dependency Management 빌드에 필요한 외부 의존성을 자동으로 관리한다
  • Tasks Task는 Gradle에 의해 수행되는 것으로 코드 컴파일, 테스트 수행 등을 이야기하고, 각 프로젝트는 빌드 스크립트나 플러그인에서 정의한 하나 이상의 Task를 가진다
  • Plugins Gradle의 기능을 확장하는데 사용된다

Gradle Project 구조

 

1. Gradle Wrapper 등을 저장하는 gradle 디렉토리

2. Gradle의 의존성 관리를 위한 파일

3. Gradle Wrapper 스크립트

4. 루트 프로젝트 명, 서브 프로젝트 정의하는 설정 파일

5. 서브 프로젝트의 빌드 스크립트

6. 서브 프로젝트의 소스 코드가 있는 디렉토리

 

 

 

 

 

 

 

 

 

 

 

Gradle build 방법

Gradle build를 위해서는 두가지 방법이 있다

1. gradle build
2. gradlew build (window: gradlew.bat build)
  1. 설치된 Gradle을 통해 빌드를 수행시키는 방법
  2. 설치된 Gradle이 아닌 프로젝트에 선언된 Gradle을 통해 build를 수행(공식적으로 추천되는 방법)

 

Gradle 구조 및 동작 흐름

Gradle의 동작 흐름은 위 그림과 같다

 

여기서 Wrapper의 역할을 살펴 보면 Wapper 스크립트는 Gradle이 수행되기 전에 필요한 경우 Wrapper에 정의 된 Gradle 버전을 다운로드한 후에 수행한다

이를 통해 일관된 Gradle Version으로 수행이 가능하도록 한다

 

Gradle 프로젝트 설정

1. settings.gradle

settings.gradle은 Gradle 프로젝트의 시작 지점이다

주된 목적은 서브 프로젝트들을 빌드에 포함시키기 위해서 존재한다

해당 파일 내 root 프로젝트 명을 지정하고 모든 하위 프로젝트를 선언 한다

  • 단일 모듈 프로젝트에서 해당 파일을 설정하는 것은 선택 사항
  • 멀티 모듈 프로젝트에서는 필수이며, 모든 서브 프로젝트에 대해 선언해야 한다

settings.gradle 파일은 아래와 같이 구성된다

 

 

1. 루트 프로젝트 명을 지정

2. 루트 프로젝트 하위에 포함된 서브 프로젝트에 대한 포함 선언

 

 

 

 

 

 

settings.gradle 파일 내에서 사용할 수 있는 다양한 속성이 존재한다

속성명 설명
buildCache 빌드 캐시 속성
plugins 플러그인에 대한 속성
rootDir 빌드를 위한 루트 디렉토리 속성
rootProject 빌드를 위한 루트 프로젝트에 대한 속성
settings 설정과 관련된 속성

 

2. build.gradle

Gradle은 루트 프로젝트와 루트 프로젝트의 하위에 있는 서브 프로젝트를 settings.gradle을 통해 찾고, settings.gradle에 포함된 프로젝트에 대한 Project 인스턴스를 생성한 후 설정에 필요한 build.gradle 파일을 찾는다

속성명 타입 설명
name String 프로젝트 디렉토리 명
path String 프로젝트의 전체 이름
description String 프로젝트 설명
dependencies DependencyHandler 프로젝트의 종속성(의존성) handler 설정
repositories RepositoryHandler 프로젝트의 repository handler 설정
layout ProjectLayout 프로젝트의 중요한 위치에 대한 접근 제공
group Object 프로젝트 그룹
version Object 프로젝트 버전

 

 

 

1. 빌드에 적용된 플러그인들을 작성

  • Gradle을 확장하고 프로젝트 구성을 모듈화하고 재사용하는데 사용

2. 의존성들을 찾을 수 있는 위치들 정의

3. 빌드에 포함될 의존성들 작성

4. 속성 설정(앞서 1에서 설정한 plugin에 대한)

확장을 사용하는 프로젝트에 속성과 메소드 추가

5. 빌드 스크립트에 작업을 구성하는 것

- 작업은 클래스 컴파일, 단위 테스트 실행, WAR 압축 등의 기본작업을 수행

- 별도의 작업을 등록하면 해당 작업이 프로젝트에 추가 된다

 

Gradle이 동작하는데 있어서 주요한 요소는 Project와 Task이다

Gradle이 실행되면 Project객체를 초기화하고 필요한 properties를 configuration한 후에 task를 실행하게 된다

 

그러면 gradle파일을 분석해보자

아래는 multi module project의 root project의 build.gradle.kts파일이다

plugins { (1)
	id("java")
	id("org.springframework.boot") version "3.3.2"
	id("io.spring.dependency-management") version "1.1.4"
}

ext["springCloudVersion"] = "2023.0.0"

java { (2)
	toolchain {
		languageVersion.set(JavaLanguageVersion.of(21))
	}
}

allprojects { (3)
	group = "kr.co.kimga"
	version = "0.0.1-SNAPSHOT"

	repositories {
		mavenCentral()
	}

}

subprojects { (4)
	apply {
		plugin("java")
		plugin("org.springframework.boot")
		plugin("io.spring.dependency-management")
		plugin("java-library")
	}

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

	dependencies {
		compileOnly("org.projectlombok:lombok")
		annotationProcessor("org.projectlombok:lombok")

		testImplementation("org.junit.jupiter:junit-jupiter")
		testImplementation("org.springframework.boot:spring-boot-starter-test")
	}

	tasks.test {
		useJUnitPlatform()
	}
}

 

(1) gradle root 프로젝트에서 사용할 plugin들을 정의 java: java를 빌드하기 위한 plugin으로 코드 컴파일, test 수행, jar파일 생성 등의 역할 수행 org.springframework.boot: spring boot 어플리케이션 실행을 위한 필수적인 플러그인 io.spring.dependency-management: spring boot의 의존성 버전을 관리하는 플러그인으로 의존성의 버전을 명시하지 않아도 권장되는 버전으로 의존성 사용되도록 만들어준다

(2) 프로젝트에서 사용되는 java의 버전을 명시하여 특정 버전으로 프로젝트가 컴파일되고 실행되도록 하기 위함

(3) allprojects는 루트 및 서브 프로젝트에 공통적으로 적용하는데 사용 group, version property를 지정하고 프로젝트에서 mavenCentral 저장소를 사용하도록 설정

(4) subprojects는 모든 서브 프로젝트에 공통적으로 적용하는데 사용(루트 프로젝트는 적용안됨) 서브 프로젝트에서 사용할 plugin 및 의존성 등을 설정 tasks.test(gradle)에서 기존적으로 제공하는 테스트 작업에 JUnit5를 사용하여 테스트가 실행되도록 설정

 

서브프로젝트의 build.gradle.kts 파일이다

dependencies { (1)
    implementation("org.springframework.boot:spring-boot-starter-data-mongodb")
    implementation("org.springframework.boot:spring-boot-starter-data-redis")
    implementation("org.testcontainers:testcontainers:1.20.1")
    api("org.springframework.cloud:spring-cloud-starter-stream-kafka")
}

 

해당 build.gradle.kts파일에는 의존성에 대한 내용만 존재한다

(root 프로젝트에서 하위 서브 프로젝트에 대한 공통 항목을 이미 설정했기 때문)

 

(1) 서브 프로젝트에 필요한 의존성을 설정한다(공통 항목은 이미 root 프로젝트의 build.gradle.kts에 설정됨)

implementation

- 해당 의존성을 사용하는 모듈을 다른 모듈이 접근할 때 해당 의존성에 접근할 없다

- 의존성 전파 및 모듈 간 결합도를 낮춘다

api

- 해당 의존성을 사용하는 모듈을 사용하는 다른 모듈이 해당 의존성에 접근할 수 있음

반응형
반응형

1. Redis란?

Remote Dictionary Server의 약자로 키-값 구조의 비관계형 데이터베이스 시스템 (NoSql의 한 종류)

Redis는 인메모리 DB 특성상 데이터를 영속화 하지 않으면 사라지는 특성을 가지고 있다

그리고 빠른 성능과 제한된 용량으로 보통 자주 읽는 데이터를 캐싱하는데 자주 사용된다

2. Redis 구성 방법

Redis를 구성하는 방법에는 Cluster, Sentinel 두 가지 방법이 존재한다

https://medium.com/@khandelwal.praful/understanding-redis-high-availability-cluster-vs-sentinel-420ecaac3236

 

1) Sentinel

Sentinel 방식은 말 그대로 보초로서 Redis서버(Master, Replica)에 대한 모니터링을 수행하고 장애가 발생했을 때 Failover를 수행함으로서 가용성을 보장하는 구성이다

Sentinel의 역할

  1. 모니터링/장애 조치
    1) Redis서버에 지속적으로 Ping을 보내어 Redis서버가 alive 상태인지 확인
    2) Redis서버에 장애가 발생하는 경우 Sentinel이 이를 감지하고 다른 센티넬들에게 이를 공유하고 정족수(quorum)이상의 Sentinel들이 down으로 판단한 경우 실질적 down으로 판단
    3) 리더 Sentinel이 선출되고 리더 Sentinel에 의해 장애 Redis서버에 대해 Failover 수행(단, Master에 대해서만 Failover 수행하며 Replica에 대해서는 Failover 수행하지 않음)

  2. 구성 정보 제공
    Sentinel은 Redis 노드를 감시하고 마스터 노드에 대한 정보를 제공하는 역할을 한다
    Sentinel로 구성된 Redis르 Client가 연결할 때는 Sentinel을 통해 구성 정보를 받아야 한다(Failover가 발생했을 때 Master가 변경되기 때문)
  • Sentinel구성은 Sentinel이 장애가 발생하여 quorum(정족수)를 미달하는 경우 Failover가 정상적으로 이루어지지 않을 수 있기 때문에 Sentinel에 대한 모니터링이 중요하다

 

2) Cluster

Cluster 방식은 확장성 및 고가용성을 갖춘 방식으로 데이터를 분산(Sharding)하고 자동으로 Failover를 지원하는 방식이다

 

  1. 데이터 분산
    16384개의 Hash Slot으로 분할된 키를 각 마스터가 담당하여 처리한다
  2. 장애조치
    Master 별로 1개 이상의 Replica를 가지고 Master가 장애로 감지되면 Replica중 하나가 Master로 승격된다
    이때, Cluster 내 다수의 노드가 동의 하는 경우 장애로 간주 된다(quorum 기준 이상)
  3. 노드 간 통신
    Cluster 노드 간에는 gossip protocol로 서로 통신하고 주기적으로 상태 정보를 교환 한다
    이를 통해 각 노드는 Cluster 내 다른 노드 정보를 알고 있다
  • 노드별로 슬롯을 담당하고 있는 형태인 Cluster는 노드가 추가되는 경우 리벨런싱 작업이 필요하고 이는 자동으로 이루어지지 않는다

 

3. Spring에서 Redis 사용

의존성

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-data-redis")
}

이때 기본 Redis 클라이언트는 Lettuce 이다

yaml 설정

1. Sentinel

spring:
  redis:
    sentinel:
      master: master (master 이름)
      nodes:
        - 127.0.0.1:26379
        - 127.0.0.1:26380
        - 127.0.0.1:26381
    password: password (패스워드 설정한 경우 입력)

2. Cluster

spring:
  redis:
    cluster:
      nodes:
        - 127.0.0.1:7000
        - 127.0.0.1:7001
        - 127.0.0.1:7002
    password: password (패스워드 설정한 경우 입력)

 

Spring에서 활용

@Configuration
class RedisConfig {

    @Bean
    fun redisTemplate(connectionFactory: RedisConnectionFactory): RedisTemplate<String, Any> {
        val template = RedisTemplate<String, Any>()
        template.setConnectionFactory(connectionFactory)
        template.keySerializer = StringRedisSerializer()
        template.valueSerializer = GenericJackson2JsonRedisSerializer()
        return template
    }
}

Redis Config 클래스의 경우 Custom이 필요한 경우 생성하면 되고 기본적으로도 redisTemplate에 대한 Bean이 생성 된다

 

@RestController
@RequestMapping("/redis")
class RedisController(
    private val redisService: RedisService
) {

    @PostMapping("/set")
    fun setValue(@RequestParam key: String, @RequestParam value: String): ResponseEntity<String> {
        redisService.addValue(key, value)
        return ResponseEntity.ok()
    }

    @GetMapping("/get")
    fun getValue(@RequestParam key: String): ResponseEntity<Any?> {
        return ResponseEntity.ok(redisService.findValueByKey(key))
    }
}

 

@Service
class RedisService(
    private val redisTemplate: RedisTemplate<String, Any>
) {

    fun addValue(key: String, value: Any, ttlSec: Long = 3600) {
        redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(ttlMinutes))
    }

    fun findValueByKey(key: String): Any? {
        return redisTemplate.opsForValue().get(key)
    }

    fun deleteByKey(key: String) {
        redisTemplate.delete(key)
    }
}

 

위 코드는 예제 코드로 실무에서 활용시에는 Redis를 직접 서비스에서 사용하기 보다는 별도의 Redis 전용 Repository나 인터페이스를 만들고 용도별로 구현체를 분리하는 방식이 유지보수나 활용 측면에서 더 효율적이다

반응형

'공부' 카테고리의 다른 글

Kotlin 기본 문법 정리 - 함수  (0) 2025.07.18
Kotlin 기본 문법 정리 - 타입과 변수  (0) 2025.07.17
Gradle 개념과 역할  (0) 2025.07.15
[DB] S/X Lock  (0) 2025.06.24
레거시 시스템에서 발생하는 DeadLock에 대한 고찰  (0) 2025.06.18
반응형

JPA를 사용하면서 발생할 수 있는 N+1 문제는 어플리케이션의 성능을 저하시킬 수 있는 문제 이다

이 문제에 대해 발생하는 이유와 해결 방법에 대해 알아보고자 한다

 

N+1 문제 그리고 발생 원인

최초 실행한 한 개의 쿼리 + N개의 쿼리라고하여 N+1 문제라고 한다

 

JPA의 FetchType이 Lazy인 경우 발생 하는 것이다 (기본은 FetchType Eager)

N+1 문제는 하나의 쿼리를 실행 후, 조회된 엔티티에 대한 연관된 엔티티를 조회하기 위해 추가로 N개의 쿼리가 실행되는 것을 말한다

 

FetchType이 Lazy인 것이 자체가 문제가 되는 것은 아니다

Lazy로 연관 엔티티를 조회하는 경우 불필요한 데이터를 조회하지 않아도 되는 이점이 있다

그러나 이를 잘못 사용하여 연관된 데이터가 많은 엔티티에 접근하게 되면 해당 연관 엔티티의 수 만 큼 N번 조회가 발생하게 된다

 

이러한 N번의 조회에 의해 아래와 같은 문제점이 발생하게 되고 어플리케이션의 성능 저하가 발생할 수 있다

1. 쿼리 수가 급격히 증가

    : 연관 엔티티가 100개면 총 101개의 쿼리가 실행된다

2. DB 커넥션 낭비

    : 하나의 커넥션을 오래 점유하기 때문에 커넥션 풀 고갈 위험이 있다

 

N+1 문제가 발생할 수 있는 예시

@Entity
@Table(name = "orders")
@EntityListeners(AuditingEntityListener::class)
class Order(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
	
    .......
    
    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY, cascade = [CascadeType.PERSIST])
    val orderItems: MutableList<OrderItem> = mutableListOf(),
	
    .......
    
    @CreatedDate
    val createdAt: Instant? = null,

    @LastModifiedDate
    val modifiedAt: Instant? = null
)


@Entity
@Table(name = "orderitems")
class OrderItem(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
	
    @Column(nullable = false)
    val productId: Long? = null,
	
    .......
    
    @CreatedDate
    val createdAt: Instant? = null,

    @LastModifiedDate
    val modifiedAt: Instant? = null
)

 

위 예시를 보면 Order 안에 연관 엔티티로 OrderItem이 있는 것을 볼 수 있다

만약 Order에 연관된 OrderItem이 1000개 라고 가정하고 Order 엔티티를 조회한 후 OrderItem 엔티티에 접근하게 되면

총 1000 + 1으로 1001 번의 쿼리가 수행되게 된다

 

해결 방법

1. Fetch Eager 사용

JPA는 기본적으로 연관 관계에 대해 Fetch Eager를 기본으로 한다

Eager를 사용하게 되면 조회할 때 연관 엔티티도 함께 조회를 하게 되는데, 연관 엔티티를 함께 조회하기 때문에 N+1문제가 발생하지 않는다

하지만 실제 필요하지 않은 엔티티도 함께 조회된다

 

2. Fetch Join 활용

Fetch Join을 하게되면 필요한 연관 엔티티를 한 쿼리로 조회할 수 있다

@Query("SELECT o FROM Order o JOIN FETCH o.items")
fun findOrders(): List<Order>

 

3. EntityGraph 활용

Fetch Join 과 비교해서 단순화된 방법이다

명시적으로 Eager로 수행되도록 하는 방식 이다

@EntityGraph(attributePaths = ["items"])
@Query("SELECT o FROM Order o")
fun findOrders(): List<Order>

 

4. DTO Projection 활용

필요한 컬럼의 데이터만 조회하는 방법이다

data class OrderInfoDto (
    val orderId: Long,
    val itemName: String
)

@Query("""
    SELECT new com.example.dto.OrderInfoDto(o.orderId, i.itemName)
    FROM Order o
    JOIN o.items i
""")
fun findOrderItem(): List<OrderInfoDto>

위 코드처럼 필요한 항목의 데이터만 조회할 수 있다

Dto의 package 경로 전체를 기입해야 하기 때문에 작성해야하는 구문이 길어질 수 있다

 

data class OrderInfoDto (
    val orderId: Long,
    val itemNames: List<String>
)

아래와 같은 Dto의 구성으로 List 항목으로 데이터를 조회할 수 는 없다

따라서 단일 항목으로 조회하고 조회 이후에 별도 가공을 해야한다

 

N+1 문제를 해결하기 위한 다양한 방법이 존재하는데 상황에 맞게 적절한 방법을 사용할 필요가 있다

반응형

'공부 > JPA' 카테고리의 다른 글

JPA 조회 함수에서 @Transactional이 필요한 경우(LazyInitializationException)  (1) 2025.07.06
JPA 순환 참조  (1) 2025.06.24
JPA 시작  (0) 2021.04.25
반응형

JPA를 사용하여 개발할 때 Entity간 연관관계를 지정하는 것은 일반적이다

 

이러한 연관관계를 지정하는 경우 N+1문제가 발생하는 경우 때문에 아래와 같이 FetchType을 Lazy로 한다

(물론 JPA에서는 Eager가 기본)

 

연관관계의 FetchType을 Lazy로 한 경우 조회 기능을 개발할 때 아래와 같이 LazyInitializationException 가 발생하는 경우가 있다

LazyInitializationExceptionorg.hibernate.LazyInitializationException: 
failed to lazily initialize a collection of role: 
kr.co.kimga.order.domain.entity.order.Order.orderItems: 
could not initialize proxy - no Session at org.hibernate.collection.spi.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:635) 
at org.hibernate.collection.spi.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:219) 
at org.hibernate.collection.spi.AbstractPersistentCollection.readSize(AbstractPersistentCollection.java:150) 
at org.hibernate.collection.spi.PersistentBag.size(PersistentBag.java:353) 
at kotlin.collections.CollectionsKt__IterablesKt.collectionSizeOrDefault(Iterables.kt:39) 
at kr.co.kimga.order.infrastructure.service.order.OrderService.findOrderDetails(OrderService.kt:82) 
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) 
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) 
at java.base/java.lang.reflect.Method.invoke(Method.java:568)

 

 

 

발생 이유

왜 이런 문제가 발생하는 것일까?

LazyInitializationException이 발생하는 코드를 살펴보자

Order (
	.....

    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY, cascade = [CascadeType.PERSIST])
    val orderItems: MutableList<OrderItem> = mutableListOf
    
	.....
) {
	.....
}
fun findOrderDetails(orderId: Long): FindOrderDetailsDto {

    val findOrder = orderRepository.findById(orderId)
        .orElseThrow { throw CanNotFindOrder() }

    return FindOrderDetailsDto(
        orderId = findOrder.id!!,
        orderStatus = findOrder.status,
        orderDate = findOrder.orderDate,
        items = findOrder.orderItems.map {
            FindOrderItemDto(
                productId = it.productId!!,
                productName = it.productName,
                quantity = it.remainQuantity()
            )
        }.toList(),
        payedAmount = findOrder.orderPays.sumOf { it.amount },
        discountAmount = findOrder.orderPays.sumOf { it.discountAmount },
        totalAmount = findOrder.orderPays.sumOf { it.amount + it.discountAmount }
    )
}

 

위 코드는 주문 정보를 조회하고 이를 DTO에 맞춰 가공한 후 전 반환하는 코드이다

코드 자체로만 보면 특별히 문제가 없다고 볼 수 도 있지만, 실행하면 LazyInitializationException이 발생하게 된다

 

그 이유는 FetchType이 Lazy이기 때문이다

 

해당 함수의 트랜잭션 범위는 orderRepository.findById로 조회를 하는 전후 이다

findById로 조회가 끝나는 경우 트랜잭션이 종료가 되는데, DTO객체로 Entity를 가공하는 과정에서 Lazy로 지정된 객체에 접근하게 된다

Lazy로 지정된 객체에 접근하게 되면 JPA는 해당 객체의 값을 데이터베이스로 부터 읽어오게 된다

그러나 이미 트랜잭션의 범위가 끝났기 때문에 조되된 Entity는 영속 상태가 아니게 되고 이 때문에 LazyInitializationException이 발생하게 된다

 

 

 

해결 방법

이를 해결하는 대표적인 두가지 방법은 Eager로 설정하는 방법과 Transaction 범위를 넗히는 것이다

 

1. FetchType Eager

Entity 조회할 때 연관관계의 Entity도 함께 조회하는 방법이다

연관관계의 Entity를 한번에 조회하게 되면 조회 이후에 연관관계 Entity를 추가로 조회할 필요가 없기 때문에 영속 상태가 아니여도 데이터를 활용할 수 있다

Order (
	.....

    @OneToMany(mappedBy = "order", fetch = FetchType.EAGER, cascade = [CascadeType.PERSIST])
    val orderItems: MutableList<OrderItem> = mutableListOf
    
	.....
) {
	.....
}

 

❗️Eager를 사용하게 되면 한번에 많은 데이터를 가져오는 것을 조심해야한다

Entity를 조회할 때 연관관계 Entity를 모두 가져오게 되면 자칫 많은 데이터를 가져올 수 있고, Out Of Memory Exception이 발생할 수 있다

 

2. @Transactional 활용

Fetch Lazy 상태에서 연관관계 Entity의 데이터에 접근하게 되면 해당 Entity의 데이터를 DB로 부터 읽어오게 된다

이를 위해서 Entity가 영속 상태인 것이 중요한데, 영속 상태를 유지하기 위해 Transaction의 범위 확장할 필요가 있다

이를 위해 메소드에 @Transactional 어노테이션을 활용할 수 있다

 

메소드에 @Transactional 어노테이션을 붙이게 되면 트랜잭션 범위를 함수 내로 확장할 수 있다

이를 통해 Entity는 영속 상태를 유지하게 되고 연관관계의 Entity를 조회해올 수 있게 된다

@Transactional(readOnly = true)
fun findOrderDetails(orderId: Long): FindOrderDetailsDto {

    val findOrder = orderRepository.findById(orderId)
        .orElseThrow { throw CanNotFindOrder() }

    return FindOrderDetailsDto(
        orderId = findOrder.id!!,
        orderStatus = findOrder.status,
        orderDate = findOrder.orderDate,
        items = findOrder.orderItems.map {
            FindOrderItemDto(
                productId = it.productId!!,
                productName = it.productName,
                quantity = it.remainQuantity()
            )
        }.toList(),
        payedAmount = findOrder.orderPays.sumOf { it.amount },
        discountAmount = findOrder.orderPays.sumOf { it.discountAmount },
        totalAmount = findOrder.orderPays.sumOf { it.amount + it.discountAmount }
    )
}

 

* Transactional 어노테이션의 readOnly 속성을 true로 지정하게 되면 조회만을 수행하는 함수에 불필요한 동작(flush, drity checking 등)을 수행하지 않아 성능 최적화를 할 수 있다

 

 

반응형

'공부 > JPA' 카테고리의 다른 글

JPA N+1 문제 원인  (0) 2025.07.06
JPA 순환 참조  (1) 2025.06.24
JPA 시작  (0) 2021.04.25
반응형

Zookeeper

Zookeeper는 Broker들을 관리하는 역할을 한다

  • Broker 등록 / 상태 관리
  • 브로커의 상태를 감시
  • Controller 선출
  • 파티션 리더 선출 및 정보 관리
  • 클러스터 메타데이터 관리

주키퍼는 카프카에 종속되지 않은 별개의 시스템으로 브로커 관리를 위해 존재한다

Kafka 2.8 부터 이에 대한 의존성을 제거하고자 KRaft를 통해 이를 대처하려고 하고 있다

Broker

Broker는 메시지 저장 및 메시지 제공 등의 역할 수행하며 Producer/Consumer와 직접 통신하는 주체이다

  • 데이터 저장소
  • 메시지 수신 / 전달
  • 파티션 및 레플리카 관리
  • 파티션 리더 역할 수행
  • Controller 역할 수행

 

Kafka의 Broker들의 모음을 Kafka Cluster라고 한다

Zookeeper는 Kafka Cluster 내 Controller를 선출하는 역할, Broker들을 관리하는 역할을 하며 Broker들의 상태를 감시하고 브로커의 상태를 Controller에게 전달하여 Controller가 Failover를 할 수 있도록 한다

반응형

'공부 > Kafka' 카테고리의 다른 글

[Kafka] Kafka의 특징  (0) 2021.06.13
[Kafka: 카프카] Kafka란?  (0) 2021.06.07

+ Recent posts