반응형

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

 

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
반응형

getParameter() 그리고 getReader() 사용

HttpServletRequest 객체의 getParameter()를 통해 요청으로부터 파라미터 값을 가져와 작업을 하는 경우 주의가 필요하다.

만약 getParameter()를 통해 요청 파라미터 값을 받게 되는 경우 후속 작업에서 getReader를 통해 요청 값을 처리하는 경우가 생길 때 문제가 될 수 있다.

그 이유는 Servlet Spec에 있는데, Servlet Spec에 명시된 get Parameter에 대한 설명은 다음과 같다.

 

Parameter를 이용할 수 있는 상황에 대해 정의한 부분이다.

  1. 요청이 HTTP, HTTPS 인 경우
  2. HTTP POST 메소드 인 경우
  3. Content-Type 이 application/x-www-form-urlencoded 인 경우
  4. Servlet이 getPrarmeter 유형의 메소드를 최초 호출한 경우

위 조건이 만족하지 않아 사용할 수 없는 경우에는 요청 객체의 input stream을 통해 post data를 이용할 수 있다.

 

다만, 위 조건이 충족한 경우에는 요청 객체에 대한 input stream을 직접 이용할 수 없다.

 

getParameter함수와 getReader 함수를 같이 써야하는 경우 주의가 필요하다.

일반적으로 함수를 사용하거나 할 때 해당 함수의 스펙이나 자세한 문서를 찾아보지 않고 그냥 쓰는 경우가 많은데 그럴 경우 문제가 될 수 있다.

이러한 문제를 극복하기 위해 Wapper 클래스를 상속하여 Reusable하게 만든 HttpServletRequest 클래스를 사용하고 있다.

하지만 was 마다 해당 부분에 대한 처리가 다를 수 있기 때문에 관련 부분을 확인해본 후 사용할 필요가 있다.

 

JEUS의 겨우 : jeus.servlet.engine.WebtoBServletRequest

Tomcat의 경우 : org.apache.catalina.connector.RequestFacade

반응형

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

Java SSL 인증서 검증 흐름 및 확인  (0) 2025.06.17
Out Of Memory 문제 분석  (0) 2025.05.31
[Java] HashMap get 메서드에 관하여  (0) 2021.09.13
JavaAgnet  (0) 2021.05.09
Java Virtual Machine(JVM)  (0) 2021.05.08
반응형

JPA를 사용하면서 자칫하면 순환참조가 발생하는 경우가 있다

그렇다면 왜 JPA를 사용할 때 이러한 일이 발생할까

1. 원인

일반적으로 JPA에서 순환참조가 발생하는 이유는 양방향 연관관계가 존재하는 경우 이다

/* 양방향 연관관계 */
@Entity
data class Customer(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
    val name: String,

    @OneToMany(mappedBy = "customer")
    val orders: List<Order> = mutableListOf()
)

@Entity
data class Order(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
    val orderDate: String,

    @ManyToOne
    @JoinColumn(name = "customer_id")
    val customer: Customer
)

이 처럼 Customer(고객)가 여러 Order(주문)을 가지는 경우에 대해 필요 시 양방향 연관 관계를 설정할 수 있다

 

그렇다면 이러한 상황에서 Customer의 정보를 조회하여 조회된 결과를 그냥 반환한다고 하면 순환 참조가 발생할 수 있게 된다

@Service
class CustomerService(
    private val customerRepository: CustomerRepository
) {
	fun getCustomerById(id: Long): Customer? = 
		customerRepository.findById(id).orElseThrow(EntityNotFoundException("Customer not found"))
}

@Repository
interface CustomerRepository : JpaRepository<Customer, Long>

이렇게 조회된 Customer를 별도의 DTO 객체로 변환하거나 하지 않고 Service를 지나 Controller에서 반환된다면 JSON으로 변환될 때 순환 참조를 맛볼 수 있다

 

그렇다면 왜 JSON으로 변환하는 과정에서 순환 참조가 발생하게 되는 걸까?

 

2. 이유

그 이유를 알아보기 위해서는 객체를 JSON으로 변환해주는 라이브러리를 살펴볼 필요가 있다

Spring에서는 @RestController가 있는 Controller에서 객체를 반환하면 JSON 형식으로 변환되어 응답이 나가게 된다

이러한 것은 Spring 내부에 있는 Message Converter(HttpMessageConverter)가 우리의 객체를 Http 메시지로 변환하는 과정을 통해 이루어 진다

Controller에서 반환된 객체는 ReturnValue Handler에 의해 처리가 되고,

반환된 객체는 Jackson Message Coverter에 의해 처리가 되고 내부에 ObjectMapper에 의해 객체가 json으로 serialize 되는 과정에서 객체 내부의 필드에 대해 재귀 호출이 발생하고 이 과정에서 양방향 연관관계가 있는 객체에 대해 순환 참조가 발생하게 되고 StackOverFlow Exception이 발생하게된다

3. 해결 방법

1) @JsonIgnore

@Entity
class Child(
	@Id
	@GenerateValue
	val id: Long? = null,
	
	@ManyToOne
	@JoinColumn(name = "parent_id")
	@JsonIgnore
	val parent: Parent
)

JsonIgnore 어노테이션을 사용하면 직렬화를 할 때 해당 변수에 대한 직렬화를 제외하게 되고 순환 참조가 일어나지 않는다

단, 이러한 경우 해당 변수에 대한 값은 직렬화 되지 못하여 반환된 값에서 제외 된다

 

2) DTO활용 (가장 적절한 방법)

데이터를 반환할 때는 DTO를 통해 의미 있는 값만 전달 반환하는 것이 바람직하다

data class ParentDto(
    val id: Long?,
    val children: List<ChildDto>
)

data class ChildDto(
    val id: Long?,
    val parentId: Long?
)

fun Parent.toDto(): ParentDto = ParentDto(
    id = this.id,
    children = this.children.map {it.toDto()}
)

fun Child.toDto(): ChildDto = ChildDto(
    id = this.id,
    parentId = this.parent.id
)

이 처럼 원하는 데이터만 DTO에 포함하여 반환하면 순환참조를 제어할 수 있고, 진짜 필요한 값만 반환할 수 있기 때문에 가장 추천되는 방법이다

 

위에서 설명한 것 외에도 순환 참조가 발생하는 케이스들이 존재하기 때문에 JPA를 사용하는 경우 이를 유의하며 코드를 작성해야 한다

반응형

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

JPA N+1 문제 원인  (0) 2025.07.06
JPA 조회 함수에서 @Transactional이 필요한 경우(LazyInitializationException)  (1) 2025.07.06
JPA 시작  (0) 2021.04.25

+ Recent posts