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를 거쳐서 접근
백킹 프로퍼티와 계산 프로퍼티의 차이는 크게 보면 데이터를 실제로 저장하는지 안하는지로 구분할 수 있다
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));
}
}
(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를 사용하여 테스트가 실행되도록 설정
Sentinel 방식은 말 그대로 보초로서 Redis서버(Master, Replica)에 대한 모니터링을 수행하고 장애가 발생했을 때 Failover를 수행함으로서 가용성을 보장하는 구성이다
Sentinel의 역할
모니터링/장애 조치 1) Redis서버에 지속적으로 Ping을 보내어 Redis서버가 alive 상태인지 확인 2) Redis서버에 장애가 발생하는 경우 Sentinel이 이를 감지하고 다른 센티넬들에게 이를 공유하고 정족수(quorum)이상의 Sentinel들이 down으로 판단한 경우 실질적 down으로 판단 3) 리더 Sentinel이 선출되고 리더 Sentinel에 의해 장애 Redis서버에 대해 Failover 수행(단, Master에 대해서만 Failover 수행하며 Replica에 대해서는 Failover 수행하지 않음)
구성 정보 제공 Sentinel은 Redis 노드를 감시하고 마스터 노드에 대한 정보를 제공하는 역할을 한다 Sentinel로 구성된 Redis르 Client가 연결할 때는 Sentinel을 통해 구성 정보를 받아야 한다(Failover가 발생했을 때 Master가 변경되기 때문)
Sentinel구성은 Sentinel이 장애가 발생하여 quorum(정족수)를 미달하는 경우 Failover가 정상적으로 이루어지지 않을 수 있기 때문에 Sentinel에 대한 모니터링이 중요하다
2) Cluster
Cluster 방식은 확장성 및 고가용성을 갖춘 방식으로 데이터를 분산(Sharding)하고 자동으로 Failover를 지원하는 방식이다
데이터 분산 16384개의 Hash Slot으로 분할된 키를 각 마스터가 담당하여 처리한다
장애조치 Master 별로 1개 이상의 Replica를 가지고 Master가 장애로 감지되면 Replica중 하나가 Master로 승격된다 이때, Cluster 내 다수의 노드가 동의 하는 경우 장애로 간주 된다(quorum 기준 이상)
노드 간 통신 Cluster 노드 간에는 gossip protocol로 서로 통신하고 주기적으로 상태 정보를 교환 한다 이를 통해 각 노드는 Cluster 내 다른 노드 정보를 알고 있다
노드별로 슬롯을 담당하고 있는 형태인 Cluster는 노드가 추가되는 경우 리벨런싱 작업이 필요하고 이는 자동으로 이루어지지 않는다
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 문제를 해결하기 위한 다양한 방법이 존재하는데 상황에 맞게 적절한 방법을 사용할 필요가 있다
이러한 연관관계를 지정하는 경우 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)