환경 설정

 

FIRST 규칙

📌 테스트 코드가 지켜야 할 규칙

  • Fast: 테스트는 빠르게 동작해야 한다.
  • Independent : 각 테스트는 서로 의존해선 안되며, 독립적으로 그리고 아무 순서로 실행해도 괜찮아야 한다.
  • Repeatable : 테스트는 어떤 환경에서도 반복 가능해야 한다.
  • Self-Validating : 테스트는 성공 또는 실패로 bool 값으로 결과를 내어 검증해야 한다.
  • Timely : 테스트는 적시에 즉, 테스트하려는 실제 코드를 구현하기 직전에 구현해야 한다.
data class Item(
    val name: String,
    val weight: Int,
)

class Bag(
    val maxWeight: Int,
) {
    init {
        if (maxWeight <= 0) {
            throw Exception("가방의 최대 무게가 잘못 설정되었습니다.")
        }
    }

    val itemList: MutableList<Item> = mutableListOf()
    val currentWeight = this.itemList.fold(0) { acc, item -> acc + item.weight }

    fun putItem(item: Item) {
        if (item.weight + currentWeight > maxWeight) {
            throw Exception("가방에 아이템을 넣을 수 없습니다.")
        }

        this.itemList.add(item)
    }

    fun removeItem(item: Item) {
        this.itemList.remove(item)
    }
}

테스트 코드

class BagTest : BehaviorSpec({
    Given("a valid max weight") {
        val validMaxWeight = 10

        When("execute constructor") {
            val result = Bag(validMaxWeight)

            Then("max weight should be valid max weight") {
                result.maxWeight shouldBe validMaxWeight
            }
        }
    }

    Given("a max weight = 0") {
        val maxWeight = 0

        When("execute constructor") {
            val exception = shouldThrow<Exception> { Bag(maxWeight) }

            Then("exception message should be expected") {
                exception.message shouldBe "가방의 최대 무게가 잘못 설정되었습니다."
            }
        }
    }

    Given("a max weight is negative") {
        val maxWeight = -10

        When("execute constructor") {
            val exception = shouldThrow<Exception> { Bag(maxWeight) }

            Then("exception message should be expected") {
                exception.message shouldBe "가방의 최대 무게가 잘못 설정되었습니다."
            }
        }
    }
})

 

'Back-End (Web) > Kotlin' 카테고리의 다른 글

[Kotlin] Kotlin의 객체지향  (0) 2025.02.12
[Kotlin] Kotlin의 사용  (0) 2025.02.11
[Kotlin] Kotlin이란?  (0) 2025.02.10

객체지향 프로그래밍 기초

📌 책임, 역할, 상호작용

    • 객체지향 프로그래밍(OOP)은 프로그램을 여러 개의 독립된 객체로 나누고, 각 객체가 데이터와 기능을 가지며 상호작용하는 방식
    • 객체(Object): 상태와 그 상태를 처리하는 행동의 묶음
    • 책임(Responsibility): 객체가 수행해야 하는 역할 또는 기능을 말합니다. 예를 들어, 자동차 객체의 주행이나 정지는 그 객체의 책임입니다.
    • 역할(Role): 객체가 수행하는 책임의 집합을 의미합니다. 여러 객체가 동일한 역할을 수행할 수 있습니다.
    • 상호작용(Interaction): 객체 간에 서로 메시지를 주고받아 책임을 수행하고 상호작용하는 것을 의미합니다.
    • 객체지향 프로그래밍을 잘 한다는건 책임과 역할을 잘 나누어 코드를 작성한다는 것!

 

 

전략 패턴

/ 디자인 패턴들 / 행동 패턴 전략 패턴 다음 이름으로도 불립니다: Strategy 의도 전략 패턴은 알고리즘들의 패밀리를 정의하고, 각 패밀리를 별도의 클래스에 넣은 후 그들의 객체들을 상호교환

refactoring.guru

 

 

 

클래스와 인스턴스

1. 클래스와 인스턴스

더보기

클래스

  • 프로그램의 각 요소별 설계도라고 해석할 수 있어요
  • 코틀린에서는 class 키워드를 활용해서 클래스를 만들어요
  • 클래스에는 정보(프로퍼티)와 행위(메소드)를 작성해요
  • 보통 하나의 파일은 한개의 클래스를 의미하지만, 하나의 파일안에 여러개의 클래스가 존재할 수도 있어요

 

인스턴스

  • 클래스형태로 설계된 객체를 실체화하면 인스턴스가 생겨요
  • 인스턴스는 메모리 공간을 차지해요
  • 정보와 행위를 작성한 클래스를 실체화해서 프로그램에 로딩해요 (메모리에 적재)
  • 정보가 행위가 그대로 로딩되는것이 아니라 위치정보를 메모리에 로딩해요
  • 프로그램은 객체의 위치정보를 변수에 저장해두고, 필요할 때 참조해요

2. 클래스 선언과 인스턴스 생성

// **클래스 선언**
class Person {
    var name: String = "" // 속성 (멤버 변수)
    var age: Int = 0

    fun introduce() { // 메서드
        println("Hi, my name is $name and I am $age years old.")
    }
}

// **인스턴스 생성**
val person = Person() // Person 클래스의 인스턴스를 생성
person.name = "Alice" // 속성 값 설정
person.age = 25
person.introduce() // 메서드 호출

// **결과**
// 출력: Hi, my name is Alice and I am 25 years old.

3. 생성자(Constructor)

1) 기본 생성자

// **기본 생성자 선언**
class Person(val name: String, var age: Int) { // 주 생성자
    fun introduce() {
        println("Hi, my name is $name and I am $age years old.")
    }
}

val person = Person("Bob", 30) // 생성자 호출
person.introduce() // 출력: Hi, my name is Bob and I am 30 years old.

2) 부 생성자

// **부 생성자 사용**
class Person {
    var name: String
    var age: Int

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

    fun introduce() {
        println("Hi, my name is $name and I am $age years old.")
    }
}

val person = Person("Charlie", 35)
person.introduce() // 출력: Hi, my name is Charlie and I am 35 years old.

4. 프로퍼티 초기화

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

val person = Person("David", 40)
println("Name: ${person.name}, Age: ${person.age}") // 출력: Name: David, Age: 40

5. 초기화 블록

// **초기화 블록 (init) 사용**
class Person(val name: String, var age: Int) {
    init {
        println("Person initialized with name = $name and age = $age")
    }
}

val person = Person("Eve", 28) // 출력: Person initialized with name = Eve and age = 28

6. 기본값을 가진 클래스

class Person(val name: String = "Unknown", var age: Int = 0) {
    fun introduce() {
        println("Hi, my name is $name and I am $age years old.")
    }
}

val defaultPerson = Person() // 기본값 사용
defaultPerson.introduce() // 출력: Hi, my name is Unknown and I am 0 years old.

val customPerson = Person("Frank", 45) // 사용자 지정 값
customPerson.introduce() // 출력: Hi, my name is Frank and I am 45 years old.

7. 클래스의 주요 특징

1) 데이터 클래스

  • 데이터 저장용 클래스를 간결하게 선언.
data class User(val name: String, val age: Int)

val user = User("Alice", 25)
println(user) // 출력: User(name=Alice, age=25)

2) 상속

  • 클래스 상속은 open 키워드를 사용해야 가능.
open class Animal {
    open fun sound() {
        println("Some generic animal sound")
    }
}

class Dog : Animal() {
    override fun sound() {
        println("Bark")
    }
}

val dog = Dog()
dog.sound() // 출력: Bark

3) 인터페이스

  • Kotlin은 다중 상속 대신 인터페이스를 지원.
interface Flyable {
    fun fly()
}

class Bird : Flyable {
    override fun fly() {
        println("Bird is flying")
    }
}

val bird = Bird()
bird.fly() // 출력: Bird is flying

8. 객체 선언 (싱글톤)

object Singleton {
    var count = 0
    fun increment() {
        count++
    }
}

Singleton.increment()
println("Count: ${Singleton.count}") // 출력: Count: 1

요약

항목  설명 예제
클래스 정의 객체를 생성하기 위한 설계도. class Person { var name: String = "" }
인스턴스 생성 클래스에서 생성된 개별 객체. val person = Person()
생성자 객체 초기화를 위한 특별한 메서드. class Person(val name: String)
초기화 블록 초기화 로직을 정의하는 블록. init { ... }
데이터 클래스 데이터 저장 및 처리에 최적화된 클래스. data class User(val name: String)
상속 부모 클래스의 기능을 자식 클래스가 확장. class Dog : Animal()
인터페이스 다중 상속 대신 사용하는 구조. interface Flyable { fun fly() }
싱글톤 객체 하나의 인스턴스만 존재하는 객체. object Singleton { ... }

 

 

인터페이스

 

1. 인터페이스란?

  • 인터페이스는 클래스가 구현해야 할 메서드와 프로퍼티를 정의하는 틀입니다.
  • 인터페이스는 다중 상속을 지원하며, 구현체는 여러 인터페이스를 동시에 상속받을 수 있습니다.
  • 인터페이스는 구현부가 없는 추상 메서드와, 기본 구현을 가진 구체 메서드를 모두 포함할 수 있습니다.
  •  

2. 인터페이스 선언과 사용

// **인터페이스 선언**
interface Animal {
    fun sound() // 추상 메서드
    fun eat() { // 구체 메서드 (기본 구현 제공)
        println("This animal is eating.")
    }
}

// **클래스가 인터페이스 구현**
class Dog : Animal {
    override fun sound() {
        println("Bark")
    }
}

// **인스턴스 생성 및 사용**
val dog = Dog()
dog.sound() // 출력: Bark
dog.eat()   // 출력: This animal is eating.

3. 인터페이스와 다중 상속

// **두 개 이상의 인터페이스 정의**
interface Flyable {
    fun fly()
}

interface Swimable {
    fun swim()
}

// **클래스가 다중 인터페이스 구현**
class Duck : Flyable, Swimable {
    override fun fly() {
        println("Duck is flying.")
    }

    override fun swim() {
        println("Duck is swimming.")
    }
}

// **인스턴스 생성 및 사용**
val duck = Duck()
duck.fly()  // 출력: Duck is flying.
duck.swim() // 출력: Duck is swimming.

4. 인터페이스의 프로퍼티

// **인터페이스에서 프로퍼티 정의**
interface Shape {
    val name: String // 추상 프로퍼티
    fun area(): Double
}

// **클래스가 인터페이스 구현**
class Circle(val radius: Double) : Shape {
    override val name: String = "Circle" // 추상 프로퍼티 구현
    override fun area(): Double = Math.PI * radius * radius
}

val circle = Circle(5.0)
println("${circle.name} Area: ${circle.area()}") // 출력: Circle Area: 78.53981633974483

5. 인터페이스와 기본 구현 충돌

// **두 인터페이스의 기본 구현 충돌 처리**
interface A {
    fun show() {
        println("Interface A")
    }
}

interface B {
    fun show() {
        println("Interface B")
    }
}

// **클래스가 두 인터페이스를 구현**
class C : A, B {
    override fun show() {
        super<A>.show() // A의 show 호출
        super<B>.show() // B의 show 호출
    }
}

val c = C()
c.show()
// 출력:
// Interface A
// Interface B

6. 인터페이스 확장

// **인터페이스 확장**
interface Vehicle {
    fun drive()
}

interface ElectricVehicle : Vehicle {
    fun chargeBattery()
}

// **구현 클래스**
class Tesla : ElectricVehicle {
    override fun drive() {
        println("Tesla is driving.")
    }

    override fun chargeBattery() {
        println("Tesla is charging.")
    }
}

val tesla = Tesla()
tesla.drive()         // 출력: Tesla is driving.
tesla.chargeBattery() // 출력: Tesla is charging.

7. 인터페이스를 활용한 다형성

// **다형성 예제**
interface Animal {
    fun sound()
}

class Cat : Animal {
    override fun sound() {
        println("Meow")
    }
}

class Dog : Animal {
    override fun sound() {
        println("Bark")
    }
}

fun makeSound(animal: Animal) {
    animal.sound() // Animal 타입으로 다양한 구현 호출 가능
}

makeSound(Cat()) // 출력: Meow
makeSound(Dog()) // 출력: Bark

8. 요약

특징 설명 예제
인터페이스 정의 interface 키워드로 정의, 메서드와 프로퍼티 선언 가능. interface Flyable { fun fly() }
추상 메서드 구현체에서 반드시 오버라이드해야 하는 메서드. fun sound()
구체 메서드 기본 구현을 제공하며, 필요 시 오버라이드 가능. fun eat() { println("Eating") }
프로퍼티 추상 프로퍼티 또는 기본값을 가진 프로퍼티 선언 가능. val name: String
다중 상속 여러 인터페이스를 동시에 구현 가능. class Duck : Flyable, Swimable
다형성 지원 공통된 인터페이스를 통해 다양한 구현체를 동일한 방식으로 처리 가능. fun makeSound(animal: Animal)

 

상속

 

1. 상속이란?

  • 상속은 부모 클래스(상위 클래스)의 속성과 메서드를 자식 클래스(하위 클래스)가 물려받는 개념입니다.
  • Kotlin에서는 클래스가 기본적으로 final(상속 불가)로 설정되어 있으므로, 상속하려면 부모 클래스에 open 키워드를 붙여야 합니다.

2. 기본 상속 구조

// **부모 클래스**
open class Animal { // open 키워드를 붙여야 상속 가능
    open fun sound() { // open 키워드를 붙여야 오버라이드 가능
        println("Some generic animal sound")
    }
}

// **자식 클래스**
class Dog : Animal() { // Animal 클래스를 상속
    override fun sound() { // 부모 메서드를 오버라이드
        println("Bark")
    }
}

// **인스턴스 생성 및 사용**
val dog = Dog()
dog.sound() // 출력: Bark

3. 부모 클래스의 속성과 메서드 상속

// **부모 클래스**
open class Person(val name: String, var age: Int) {
    open fun introduce() {
        println("Hi, I'm $name and I'm $age years old.")
    }
}

// **자식 클래스**
class Student(name: String, age: Int, val grade: Int) : Person(name, age) { // 부모 생성자 호출
    override fun introduce() {
        super.introduce() // 부모 메서드 호출
        println("I'm in grade $grade.")
    }
}

// **사용 예제**
val student = Student("Alice", 20, 3)
student.introduce()
// 출력:
// Hi, I'm Alice and I'm 20 years old.
// I'm in grade 3.

4. 생성자와 상속

// **부모 클래스**
open class Vehicle(val brand: String) {
    init {
        println("Vehicle brand: $brand")
    }
}

// **자식 클래스**
class Car(brand: String, val model: String) : Vehicle(brand) {
    init {
        println("Car model: $model")
    }
}

// **사용 예제**
val car = Car("Toyota", "Corolla")
// 출력:
// Vehicle brand: Toyota
// Car model: Corolla

5. 상속에서의 메서드 호출 순서

open class Parent {
    init {
        println("Parent class initialized")
    }

    open fun greet() {
        println("Hello from Parent")
    }
}

class Child : Parent() {
    init {
        println("Child class initialized")
    }

    override fun greet() {
        println("Hello from Child")
    }
}

val child = Child()
child.greet()
// 출력:
// Parent class initialized
// Child class initialized
// Hello from Child

6. 상속에서의 final, open, override 키워드

  • final: 더 이상 상속이나 오버라이드가 불가능하게 설정.
  • open: 상속이나 오버라이드를 허용.
  • override: 부모 클래스의 메서드나 프로퍼티를 재정의.
open class Parent {
    open fun greet() {
        println("Hello from Parent")
    }

    final fun sayGoodbye() {
        println("Goodbye from Parent")
    }
}

class Child : Parent() {
    override fun greet() { // 부모 메서드를 오버라이드
        println("Hello from Child")
    }

    // fun sayGoodbye() {} // 컴파일 에러: final 메서드는 오버라이드 불가능
}

7. 추상 클래스

  • 추상 클래스는 인스턴스를 생성할 수 없으며, 일부 메서드나 프로퍼티를 구현하지 않고 남겨둠.
  • abstract 키워드를 사용하며, 반드시 하위 클래스에서 구현해야 함.
abstract class Shape {
    abstract fun area(): Double // 추상 메서드 (구현 없음)
    open fun display() { // 구체 메서드
        println("This is a shape.")
    }
}

class Circle(val radius: Double) : Shape() {
    override fun area(): Double = Math.PI * radius * radius // 추상 메서드 구현
}

val circle = Circle(5.0)
circle.display() // 출력: This is a shape.
println("Area: ${circle.area()}") // 출력: Area: 78.53981633974483

8. 인터페이스와 상속

  • Kotlin은 다중 상속을 지원하지 않지만, 다중 인터페이스 구현이 가능합니다.
interface Flyable {
    fun fly() {
        println("Flying...")
    }
}

open class Bird {
    open fun sound() {
        println("Tweet")
    }
}

class Eagle : Bird(), Flyable { // Bird를 상속하고 Flyable 인터페이스 구현
    override fun sound() {
        println("Screech")
    }
}

val eagle = Eagle()
eagle.sound() // 출력: Screech
eagle.fly()   // 출력: Flying...

9. 요약

키워드 설명 예제
open 상속 및 오버라이드 가능. open class Parent { open fun greet() }
final 상속 및 오버라이드 불가능. final fun sayGoodbye()
override 부모의 메서드나 프로퍼티를 재정의. override fun greet()
abstract 구현되지 않은 메서드나 프로퍼티를 정의. abstract fun area(): Double

10. 상속의 장점

  1. 코드 재사용성: 공통된 로직을 부모 클래스에 정의하여 중복 코드를 줄임.
  2. 유지보수 용이: 부모 클래스 수정만으로 자식 클래스의 동작도 일관성 유지.
  3. 다형성 구현: 동일한 부모 클래스를 공유하는 객체를 일관된 방식으로 처리 가능.

 

11. 상속의 단점

  • 유연성, 확장성이 떨어져요
  • 다중 상속에 의해 문제가 발생할 수 있어요. (그래서 kotlin에서는 다중상속을 막아놓았어요)
  • 자식 클래스에서 부모 클래스의 내부 구조를 잘 알아야만 해요

 

 

의존성 주입 (DI)

이것은 "의존성 주입"(Dependency Injection)과 관련된 개념으로, 중복되는 로직을 별도의 공통 객체로 추출하여, 이를 다른 클래스에서 재사용하는 방식입니다. 이는 **SOLID 원칙 중 DIP(의존성 역전 원칙)**에 해당하며, 코드의 재사용성과 유지보수성을 크게 향상시킵니다.


구현 방법

1. 중복되는 로직을 갖는 공통 객체 정의

  • 공통 로직을 별도의 인터페이스클래스로 정의합니다.

2. 공통 객체를 호출하는 클래스에 주입

  • 의존성 주입을 사용하여 공통 객체를 호출하는 클래스에 전달합니다.

구현 예제

1. 중복 로직이 들어갈 객체 정의

// **1. 인터페이스 정의**
interface CommonLogic {
    fun executeLogic(data: String): String
}

// **2. 구현체 정의**
class CommonLogicImpl : CommonLogic {
    override fun executeLogic(data: String): String {
        return "Processed: $data" // 중복 로직 구현
    }
}

2. 공통 객체를 주입받아 사용하는 클래스

// **1. 주입받아 사용하는 클래스**
class Service(private val commonLogic: CommonLogic) {
    fun performTask(input: String): String {
        return commonLogic.executeLogic(input) // 주입받은 객체의 로직 호출
    }
}

3. 의존성 주입 및 사용

fun main() {
    // **공통 로직 구현체 생성**
    val commonLogic = CommonLogicImpl()

    // **Service 클래스에 주입**
    val service = Service(commonLogic)

    // **Service 메서드 호출**
    val result = service.performTask("Hello Kotlin")
    println(result) // 출력: Processed: Hello Kotlin
}

의존성 주입(DI) 활용

의존성 주입을 직접 구현하는 방식

fun main() {
    val commonLogic = CommonLogicImpl() // 의존성 객체 생성
    val service = Service(commonLogic) // 생성자 주입

    println(service.performTask("DI Example")) // 출력: Processed: DI Example
}

의존성 주입 프레임워크 활용 (예: Spring, Koin)

// Koin DI 예제
import org.koin.core.context.startKoin
import org.koin.dsl.module

// **1. 모듈 정의**
val appModule = module {
    single<CommonLogic> { CommonLogicImpl() } // CommonLogic에 CommonLogicImpl 주입
    factory { Service(get()) } // Service에 CommonLogic 주입
}

// **2. Koin 시작**
fun main() {
    startKoin {
        modules(appModule)
    }

    // **3. Koin으로 객체 가져오기**
    val service: Service = getKoin().get()
    println(service.performTask("Koin DI")) // 출력: Processed: Koin DI
}

장점

  1. 중복 제거: 중복 로직을 공통 객체로 추출하여 재사용성 증가.
  2. 유지보수성 향상: 공통 로직 수정 시, 한 곳에서 수정하면 모든 클래스에 반영.
  3. 의존성 관리 용이: 의존성 주입을 통해 클래스 간 결합도를 낮춤.
  4. 테스트 용이성: Mock 객체를 주입하여 테스트 가능.

요약

구성 요소  설명
공통 객체 정의 인터페이스나 클래스에 중복 로직 구현.
주입받는 클래스 공통 객체를 의존성으로 받아 로직 호출.
의존성 주입 방법 직접 주입(생성자, 메서드) 또는 DI 프레임워크(Koin, Dagger 등) 활용.

 

 

생성자

 

1. 생성자란?

  • **생성자(Constructor)**는 객체 생성 시 호출되는 함수.
  • Kotlin은 두 가지 생성자를 제공:
    • 주 생성자(Primary Constructor): 클래스 헤더에서 선언.
    • 부 생성자(Secondary Constructor): 클래스 본문에서 선언.


2. 주 생성자 (Primary Constructor)

1) 기본 주 생성자

// **주 생성자 정의**
class Person(val name: String, val age: Int)

// **객체 생성**
val person = Person("Alice", 25)
println("Name: ${person.name}, Age: ${person.age}")
// 출력: Name: Alice, Age: 25

2) 주 생성자와 초기화 블록

// **init 블록을 사용한 초기화**
class Person(val name: String, val age: Int) {
    init {
        println("Person initialized with name = $name and age = $age")
    }
}

// **객체 생성**
val person = Person("Bob", 30)
// 출력: Person initialized with name = Bob and age = 30

3) 디폴트 값을 가진 주 생성자

// **디폴트 값 설정**
class Person(val name: String = "Unknown", val age: Int = 0)

// **객체 생성**
val person1 = Person() // 기본값 사용
val person2 = Person("Charlie", 35) // 커스텀 값 사용

println("Person1: ${person1.name}, ${person1.age}") // 출력: Person1: Unknown, 0
println("Person2: ${person2.name}, ${person2.age}") // 출력: Person2: Charlie, 35

3. 부 생성자 (Secondary Constructor)

1) 기본 부 생성자

// **부 생성자 정의**
class Person {
    var name: String
    var age: Int

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

// **객체 생성**
val person = Person("Dave", 40)
println("Name: ${person.name}, Age: ${person.age}")
// 출력: Name: Dave, Age: 40

2) 주 생성자와 부 생성자 동시 사용

// **주 생성자와 부 생성자**
class Person(val name: String) {
    var age: Int = 0

    constructor(name: String, age: Int) : this(name) { // 주 생성자 호출
        this.age = age
    }
}

// **객체 생성**
val person = Person("Eve", 28)
println("Name: ${person.name}, Age: ${person.age}")
// 출력: Name: Eve, Age: 28

4. 데이터 클래스와 생성자

  • 데이터 클래스는 주 생성자에서 속성을 정의.
data class User(val id: Int, val name: String)

// **객체 생성**
val user = User(1, "Alice")
println(user)
// 출력: User(id=1, name=Alice)

5. 생성자와 가시성 (Visibility)

  • 생성자에 가시성 변경자를 추가해 접근 제어 가능.
class Person private constructor(val name: String) { // private 생성자
    companion object {
        fun create(name: String): Person {
            return Person(name)
        }
    }
}

// **객체 생성**
val person = Person.create("Alice")
println("Name: ${person.name}")
// 출력: Name: Alice

6. 추상 클래스와 생성자

// **추상 클래스**
abstract class Animal(val name: String) {
    abstract fun sound()
}

class Dog(name: String) : Animal(name) {
    override fun sound() {
        println("$name says: Woof!")
    }
}

// **객체 생성**
val dog = Dog("Buddy")
dog.sound()
// 출력: Buddy says: Woof!

요약

유형  설명 예제
주 생성자 클래스 헤더에서 선언되는 기본 생성자. class Person(val name: String)
부 생성자 클래스 본문에서 선언되며 주 생성자를 호출하거나 별도의 로직 추가 가능. constructor(name: String)
init 블록 주 생성자와 함께 사용되며, 초기화 로직을 정의. init { ... }
디폴트 값 주 생성자에 기본값을 제공하여 선택적 매개변수 구현 가능. val name: String = "Unknown"
데이터 클래스 데이터를 저장하기 위한 클래스로, 주 생성자에서 속성을 정의. data class User(val id: Int, name: String)
가시성 제어 생성자의 접근 범위를 제한. class Person private constructor(...)

7. 생성자 사용 예제

fun main() {
    // 주 생성자를 사용한 객체 생성
    val person = Person("Alice", 25)
    println("Name: ${person.name}, Age: ${person.age}")

    // 데이터 클래스를 사용한 객체 생성
    val user = User(1, "Bob")
    println(user)

    // 부 생성자를 사용한 객체 생성
    val person2 = Person("Charlie", 30)
    println("Name: ${person2.name}, Age: ${person2.age}")
}

 

 

접근제한자

 

1. 접근 제한자란?

  • 접근 제한자는 클래스, 함수, 프로퍼티, 생성자 등에 대한 접근 범위를 제어합니다.
  • Kotlin에서 지원하는 접근 제한자:
    • public: 어디서나 접근 가능 (기본값).
    • private: 해당 선언이 속한 클래스 또는 파일 내부에서만 접근 가능.
    • protected: 클래스와 그 하위 클래스에서만 접근 가능.
    • internal: 같은 모듈(컴파일 단위) 내에서만 접근 가능.

2. 접근 제한자 종류 및 사용 예제


1) public (기본값)

  • 모든 클래스, 파일, 모듈에서 접근 가능.
  • 선언하지 않으면 기본적으로 public으로 설정됨.
class PublicExample { // public이 기본값
    public val name = "Public Property" // 명시적 public
    fun show() {
        println("This is a public function.")
    }
}

val obj = PublicExample()
println(obj.name) // 접근 가능
obj.show() // 접근 가능

2) private

  • 선언된 클래스 또는 파일 내부에서만 접근 가능.
  • 클래스 외부다른 파일에서는 접근 불가.
class PrivateExample {
    private val secret = "Private Property" // private 프로퍼티
    fun accessSecret() {
        println(secret) // 클래스 내부에서 접근 가능
    }
}

val obj = PrivateExample()
// println(obj.secret) // 오류: secret은 private
obj.accessSecret() // 정상: 클래스 내부에서 secret 접근
  • 파일 스코프에서의 private
private fun fileScopedFunction() {
    println("This function is private to this file.")
}

// 다른 파일에서는 fileScopedFunction() 호출 불가
fileScopedFunction() // 동일 파일에서만 호출 가능

3) protected

  • 선언된 클래스 및 그 하위 클래스에서만 접근 가능.
  • 다른 클래스에서는 접근 불가.
open class Parent {
    protected val protectedProperty = "Protected Property"
}

class Child : Parent() {
    fun showProtected() {
        println(protectedProperty) // 하위 클래스에서 접근 가능
    }
}

val child = Child()
// println(child.protectedProperty) // 오류: protected는 외부에서 접근 불가
child.showProtected() // 정상 실행

4) internal

  • 같은 모듈 내에서만 접근 가능.
  • 다른 모듈(예: 다른 라이브러리)에서는 접근 불가.
internal class InternalExample {
    internal val internalProperty = "Internal Property"
    internal fun internalFunction() {
        println("This is an internal function.")
    }
}

// 같은 모듈에서는 접근 가능
val internalObj = InternalExample()
println(internalObj.internalProperty) // 정상
internalObj.internalFunction() // 정상
  • 다른 모듈에서의 접근
// 다른 모듈에서는 접근 불가
val internalObj = InternalExample()
// println(internalObj.internalProperty) // 오류

5) 생성자와 접근 제한자

  • 생성자에도 접근 제한자를 사용할 수 있음.
class Restricted private constructor() { // private 생성자
    companion object {
        fun create(): Restricted {
            return Restricted() // 클래스 내부에서는 호출 가능
        }
    }
}

val obj = Restricted.create() // 생성자는 외부에서 직접 호출 불가
// val obj2 = Restricted() // 오류: private 생성자

3. 접근 제한자 비교

제한자 클래스 내부 하위 클래스 같은 파일  같은 모듈 다른 모듈
public
private
protected
internal

4. 주요 사용 사례

제한자 사용 사례
public 대부분의 일반적인 코드, API, 또는 외부 모듈에 제공되는 클래스/메서드.
private 클래스 내부에서만 사용하는 데이터나 메서드를 숨기고 싶을 때.
protected 상속을 사용하는 경우 부모 클래스에서 하위 클래스에 로직을 공유할 때.
internal 같은 모듈 내에서만 사용되며 외부에 노출되지 않아야 하는 코드.

 

 

오버라이딩

 

1. 오버라이딩이란?

  • 오버라이딩은 부모 클래스에서 정의한 메서드나 프로퍼티를 자식 클래스에서 재정의하는 것.
  • Kotlin에서는 오버라이딩하려면 부모 메서드나 프로퍼티에 open 키워드를 붙이고, 자식 클래스에서는 override 키워드를 사용해야 함.

2. 메서드 오버라이딩

// **부모 클래스**
open class Animal {
    open fun sound() { // open 키워드를 사용해야 오버라이드 가능
        println("Some generic animal sound")
    }
}

// **자식 클래스**
class Dog : Animal() {
    override fun sound() { // 부모의 sound() 메서드 재정의
        println("Bark")
    }
}

// **사용 예제**
val animal: Animal = Dog() // 다형성
animal.sound() // 출력: Bark

3. 프로퍼티 오버라이딩

  • 프로퍼티도 open 키워드가 있어야 오버라이딩 가능.
  • val로 선언된 프로퍼티는 val이나 var로 오버라이딩 가능.
  • var로 선언된 프로퍼티는 반드시 var로만 오버라이딩 가능.
// **부모 클래스**
open class Animal {
    open val type: String = "Unknown Animal"
}

// **자식 클래스**
class Dog : Animal() {
    override val type: String = "Dog" // 부모의 type 프로퍼티 재정의
}

// **사용 예제**
val dog = Dog()
println(dog.type) // 출력: Dog

4. 메서드와 프로퍼티 오버라이딩

// **부모 클래스**
open class Shape {
    open val name: String = "Shape"
    open fun area(): Double {
        return 0.0
    }
}

// **자식 클래스**
class Circle(val radius: Double) : Shape() {
    override val name: String = "Circle" // 프로퍼티 재정의
    override fun area(): Double { // 메서드 재정의
        return Math.PI * radius * radius
    }
}

// **사용 예제**
val circle = Circle(5.0)
println("Shape: ${circle.name}, Area: ${circle.area()}") 
// 출력: Shape: Circle, Area: 78.53981633974483

5. 부모 메서드 호출 (super)

  • 오버라이드된 메서드에서 부모 클래스의 메서드를 호출할 때 super 사용.
open class Animal {
    open fun sound() {
        println("Some generic animal sound")
    }
}

class Dog : Animal() {
    override fun sound() {
        super.sound() // 부모 클래스의 메서드 호출
        println("Bark") // 추가 로직
    }
}

// **사용 예제**
val dog = Dog()
dog.sound()
// 출력:
// Some generic animal sound
// Bark

6. 오버라이딩 제한 (final)

  • final 키워드를 사용하면 해당 메서드나 프로퍼티는 오버라이딩 불가.
open class Animal {
    open fun eat() {
        println("Animal is eating")
    }

    final fun sleep() { // 오버라이딩 불가
        println("Animal is sleeping")
    }
}

class Dog : Animal() {
    override fun eat() {
        println("Dog is eating")
    }

    // override fun sleep() { ... } // 오류: final로 선언된 메서드는 오버라이딩 불가
}

7. 추상 클래스와 오버라이딩

  • 추상 클래스에서는 추상 메서드를 선언 가능.
  • 추상 메서드는 반드시 하위 클래스에서 오버라이딩해야 함.
// **추상 클래스**
abstract class Animal {
    abstract fun sound() // 구현 없음 (추상 메서드)
    open fun eat() { // 기본 구현 제공
        println("Animal is eating")
    }
}

// **구현 클래스**
class Dog : Animal() {
    override fun sound() {
        println("Bark") // 추상 메서드 구현 필수
    }
}

// **사용 예제**
val dog = Dog()
dog.sound() // 출력: Bark
dog.eat()   // 출력: Animal is eating

8. 요약

키워드 설명 예제
open 부모 클래스에서 메서드나 프로퍼티를 오버라이드 가능하게 설정. open fun sound()
override 자식 클래스에서 부모의 메서드나 프로퍼티를 재정의. override fun sound()
super 부모 클래스의 메서드나 프로퍼티에 접근. super.sound()
final 오버라이드를 제한. final fun sleep()
추상 메서드 반드시 하위 클래스에서 구현해야 하는 메서드. abstract fun sound()

9. 오버라이딩의 장점

  1. 코드 재사용성: 부모 클래스의 로직을 활용하여 중복 코드 제거.
  2. 다형성 지원: 상위 타입의 참조로 다양한 하위 클래스의 동작을 처리 가능.
  3. 유연성: 기본 동작을 수정하거나 확장 가능.

 

 

오버로딩

 

1. 오버로딩이란?

  • **오버로딩(Overloading)**은 같은 이름의 메서드나 생성자를 매개변수의 개수, 타입, 순서가 다르게 정의하는 것.
  • 컴파일러는 메서드 호출 시 매개변수의 타입과 개수를 기준으로 적절한 메서드를 선택.

2. 메서드 오버로딩

class Calculator {
    // **정수 두 개의 합**
    fun add(a: Int, b: Int): Int {
        return a + b
    }

    // **실수 두 개의 합**
    fun add(a: Double, b: Double): Double {
        return a + b
    }

    // **세 개의 정수의 합**
    fun add(a: Int, b: Int, c: Int): Int {
        return a + b + c
    }
}

// **사용 예제**
val calculator = Calculator()
println(calculator.add(2, 3))          // 출력: 5
println(calculator.add(2.5, 3.5))      // 출력: 6.0
println(calculator.add(1, 2, 3))       // 출력: 6

3. 생성자 오버로딩

  • 클래스의 생성자도 매개변수의 종류와 개수를 다르게 설정하여 오버로딩 가능.
class Person {
    var name: String
    var age: Int

    // **기본 생성자**
    constructor() {
        this.name = "Unknown"
        this.age = 0
    }

    // **이름만 받는 생성자**
    constructor(name: String) {
        this.name = name
        this.age = 0
    }

    // **이름과 나이를 받는 생성자**
    constructor(name: String, age: Int) {
        this.name = name
        this.age = age
    }
}

// **사용 예제**
val person1 = Person()
val person2 = Person("Alice")
val person3 = Person("Bob", 25)

println("${person1.name}, ${person1.age}") // 출력: Unknown, 0
println("${person2.name}, ${person2.age}") // 출력: Alice, 0
println("${person3.name}, ${person3.age}") // 출력: Bob, 25

4. 확장 함수 오버로딩

// **Int 확장 함수**
fun Int.times(value: Int): Int = this * value

// **Double 확장 함수**
fun Double.times(value: Int): Double = this * value

// **사용 예제**
val intResult = 5.times(2) // Int 타입 확장 함수 호출
val doubleResult = 5.5.times(2) // Double 타입 확장 함수 호출

println(intResult) // 출력: 10
println(doubleResult) // 출력: 11.0

5. 매개변수 기본값과 오버로딩

  • 기본값을 사용하여 오버로딩 대체 가능.
class Greeter {
    fun greet(message: String = "Hello", name: String = "Guest") {
        println("$message, $name!")
    }
}

// **사용 예제**
val greeter = Greeter()
greeter.greet()                    // 기본값 사용: 출력: Hello, Guest!
greeter.greet("Hi")                // 이름 기본값: 출력: Hi, Guest!
greeter.greet("Hi", "Alice")       // 출력: Hi, Alice!

6. 오버로딩의 주의점

  • 매개변수의 타입이나 개수가 같고 반환 타입만 다르면 컴파일러가 구분하지 못해 오류 발생.
class Example {
    // 오류: 반환 타입만 다른 경우 컴파일 불가
    // fun process(): Int = 0
    // fun process(): String = ""
}

7. 요약

항목 설명 예제
메서드 오버로딩 매개변수의 타입, 개수, 순서가 다르게 동일 이름의 메서드를 여러 개 정의. fun add(a: Int, b: Int)
생성자 오버로딩 클래스의 생성자를 매개변수의 개수와 타입에 따라 여러 개 정의. constructor(name: String, age: Int)
확장 함수 오버로딩 동일한 이름의 확장 함수를 타입에 따라 정의. fun Int.times(value: Int)
기본값 활용 매개변수 기본값으로 오버로딩을 대체하여 코드 간결화. fun greet(message: String = "Hello")

8. 오버로딩의 장점

  1. 코드 가독성 향상: 같은 이름으로 다양한 동작을 처리할 수 있어 가독성이 높아짐.
  2. 유연성 증가: 사용자가 다양한 방식으로 메서드나 생성자를 호출 가능.
  3. 코드 재사용성 증가: 같은 이름으로 다양한 동작을 구현하여 중복 코드 제거.

 

 

다양한 클래스들

 

1. 데이터 클래스 (data class)

  • 데이터를 저장하기 위해 설계된 클래스.
  • data 키워드를 사용하면 기본적인 메서드(예: toString, equals, hashCode, copy)가 자동으로 생성.
  • 특징:
    • 주 생성자에 최소 하나 이상의 프로퍼티를 선언해야 함.
    • 데이터만 관리하는 클래스에 적합.
// **데이터 클래스 선언**
data class User(val id: Int, val name: String)

fun main() {
    val user1 = User(1, "Alice")
    val user2 = User(1, "Alice")

    // **기본 메서드 사용**
    println(user1) // 출력: User(id=1, name=Alice) -> toString()
    println(user1 == user2) // 출력: true -> equals()
    println(user1.copy(name = "Bob")) // 출력: User(id=1, name=Bob) -> copy()
}

2. 열거 클래스 (enum class)

  • 상수값을 관리하기 위한 클래스.
  • 상수와 관련된 속성이나 메서드를 정의할 수 있음.
  • 특징:
    • 관리할 상수값을 명확히 정의.
    • 각 상수는 고유한 값이나 동작을 가질 수 있음.
// **기본 열거 클래스**
enum class ProgrammingLanguage {
    C, JAVA, KOTLIN
}

// **속성과 메서드를 가진 열거 클래스**
enum class ProgrammingLanguageWithInt(val code: Int) {
    C(10),
    JAVA(20),
    KOTLIN(30);

    fun description() = "Code: $code, Language: $name"
}

fun main() {
    println(ProgrammingLanguage.C) // 출력: C
    println(ProgrammingLanguageWithInt.KOTLIN.code) // 출력: 30
    println(ProgrammingLanguageWithInt.KOTLIN.name) // 출력: KOTLIN
    println(ProgrammingLanguageWithInt.KOTLIN.description()) // 출력: Code: 30, Language: KOTLIN
}

3. 실드 클래스 (sealed class)

  • 상속을 제한하고, 미리 정의된 자식 클래스만 허용.
  • 특징:
    • 새로운 자식 클래스를 추가하려면 같은 파일에서만 정의 가능.
    • when 표현식과 함께 사용하면, 모든 경우를 처리했는지 컴파일러가 확인 가능.
    • 불필요한 상속을 방지하고, 구조적 안정성을 제공.
// **실드 클래스 선언**
sealed class Shape {
    data class Circle(val radius: Double) : Shape()
    data class Rectangle(val width: Double, val height: Double) : Shape()
    object Unknown : Shape()
}

fun describeShape(shape: Shape): String {
    return when (shape) {
        is Shape.Circle -> "Circle with radius ${shape.radius}"
        is Shape.Rectangle -> "Rectangle with width ${shape.width} and height ${shape.height}"
        Shape.Unknown -> "Unknown shape"
    }
}

fun main() {
    val circle = Shape.Circle(5.0)
    val rectangle = Shape.Rectangle(4.0, 6.0)
    val unknown = Shape.Unknown

    println(describeShape(circle)) // 출력: Circle with radius 5.0
    println(describeShape(rectangle)) // 출력: Rectangle with width 4.0 and height 6.0
    println(describeShape(unknown)) // 출력: Unknown shape
}

요약

클래스  설명  주요 기능
데이터 클래스 데이터 저장 및 처리용 클래스. toString, equals, hashCode, copy 자동 생성.
열거 클래스 상수를 그룹화하여 관리. 상수별 속성 및 메서드 정의 가능.
실드 클래스 상속 가능한 클래스와 자식 클래스를 제한하여 안전성을 높임. when 표현식과 함께 모든 경우 처리 보장.

사용 사례

  1. 데이터 클래스:
    • 사용자 정보, 설정값 저장 등 데이터 중심의 로직에서 사용.
  2. 열거 클래스:
    • HTTP 상태 코드, 프로그래밍 언어 목록 등 고정된 상수값 관리.
  3. 실드 클래스:
    • API 응답 모델, 상태(State) 관리, 예외 처리 등 구조적인 상속 설계.

장점

  1. 데이터 클래스: 코드 간결화, 기본 메서드 자동 생성으로 생산성 증가.
  2. 열거 클래스: 상수값 관리 일원화, 가독성과 유지보수성 향상.
  3. 실드 클래스: 무분별한 상속 방지, 안정성 있는 설계 가능.

 

'Back-End (Web) > Kotlin' 카테고리의 다른 글

[Kotlin] Kotlin의 테스트코드  (0) 2025.02.16
[Kotlin] Kotlin의 사용  (0) 2025.02.11
[Kotlin] Kotlin이란?  (0) 2025.02.10

기본 지식 - 변수와 자료

 

기본 연산자

// **1. 산술 연산자 (Arithmetic Operators)**
val sum = 10 + 5  // 덧셈
val diff = 10 - 5 // 뺄셈
val product = 10 * 5 // 곱셈
val quotient = 10 / 5 // 나눗셈
val remainder = 10 % 3 // 나머지

// **2. 복합 대입 연산자 (Assignment Operators)**
var num = 10
num += 5  // num = num + 5 (덧셈 후 대입)
num -= 3  // num = num - 3 (뺄셈 후 대입)
num *= 2  // num = num * 2 (곱셈 후 대입)
num /= 2  // num = num / 2 (나눗셈 후 대입)
num %= 3  // num = num % 3 (나머지 후 대입)

// **3. 증가 및 감소 연산자 (Increment and Decrement Operators)**
var count = 0
count++ // 후위 증가: count 값을 사용한 후 1 증가
++count // 전위 증가: count 값을 증가시킨 후 사용
count-- // 후위 감소: count 값을 사용한 후 1 감소
--count // 전위 감소: count 값을 감소시킨 후 사용

// **4. 비교 연산자 (Comparison Operators)**
val a = 10
val b = 20
val isEqual = (a == b) // 값이 같은지 비교
val isNotEqual = (a != b) // 값이 다른지 비교
val isGreater = (a > b) // a가 b보다 큰지 비교
val isLess = (a < b) // a가 b보다 작은지 비교
val isGreaterOrEqual = (a >= b) // a가 b보다 크거나 같은지 비교
val isLessOrEqual = (a <= b) // a가 b보다 작거나 같은지 비교

// **5. 논리 연산자 (Logical Operators)**
val condition1 = true
val condition2 = false
val andResult = condition1 && condition2 // AND 연산: 둘 다 true일 때만 true
val orResult = condition1 || condition2 // OR 연산: 둘 중 하나라도 true면 true
val notResult = !condition1 // NOT 연산: true → false, false → true

// **6. 비트 연산자 (Bitwise Operators)**
val x = 5  // 0b0101
val y = 3  // 0b0011
val bitAnd = x and y // AND 연산: 0b0001 (비트 단위)
val bitOr = x or y  // OR 연산: 0b0111
val bitXor = x xor y // XOR 연산: 0b0110
val bitInv = x.inv() // 비트 반전: -6 (0b...11111010)
val leftShift = x shl 2 // 왼쪽 시프트: 0b10100 (5 * 2^2)
val rightShift = x shr 1 // 오른쪽 시프트: 0b0010 (5 / 2)

// **7. 범위 연산자 (Range Operators)**
val range = 1..5 // 1에서 5까지 포함된 범위
val untilRange = 1 until 5 // 1에서 5 미만 (1, 2, 3, 4)
val downToRange = 5 downTo 1 // 5에서 1까지 감소
val stepRange = 1..10 step 2 // 1, 3, 5, ..., 9 (2씩 증가)

// **8. 인 연산자 (in Operator)**
val isInRange = 3 in range // true (3이 range 안에 포함)
val isNotInRange = 6 !in range // true (6이 range에 포함되지 않음)

// **9. 타입 연산자 (Type Operators)**
val str: Any = "Hello"
val isString = str is String // true (str이 String 타입인지 확인)
val isNotString = str !is String // false (str이 String 타입이 아닌지 확인)

// **10. 엘비스 연산자 (Elvis Operator)**
val nullable: String? = null
val result = nullable ?: "Default" // nullable이 null이면 "Default" 반환

// **11. 안전 호출 연산자 (Safe Call Operator)**
val nullableStr: String? = "Hello"
val length = nullableStr?.length // nullableStr이 null이 아니면 length 반환, null이면 null 반환

// **12. 강제 호출 연산자 (Not-null Assertion Operator)**
val nonNullLength = nullableStr!!.length // nullableStr이 null이 아니라고 확신하는 경우 사용 (null이면 예외 발생)

// **13. 기타 연산자**
val aStr = "Kotlin"
val bStr = "kotlin"
val ignoreCaseEquals = (aStr.equals(bStr, ignoreCase = true)) // 대소문자 무시하고 비교

요약

  • Kotlin 연산자는 Java의 연산자와 유사하지만, 더 간결하고 안전성을 높인 연산자를 제공합니다.
  • 특히 Elvis 연산자 (?:), 안전 호출 연산자 (?.), 강제 호출 연산자 (!!) 등은 Kotlin의 강력한 특징 중 하나로 Null 안전성을 보장합니다.
  • 기본적으로 자바보다도 더 가독성이 좋은 형태로 구성이 되어 있어, 코딩시에 유리합니다.

 

출력과 입력

📌 흔히 다른 장치로 데이터를 전송하는 행위를 Output(출력), 반대로 데이터를 불러오는 행위를 Input(입력)이라고 합니다.

  • 프로그램에서 스피커로 사운드 출력
  • 마이크에서 녹음한 목소리를 불러와서 프로그램에서 확인

 

1. 출력 (Output)

// **기본 출력**
println("Hello, Kotlin!") // 줄 바꿈과 함께 메시지 출력
print("Hello, ") // 줄 바꿈 없이 메시지 출력
print("World!") // 이어서 출력

// **문자열 보간(String Interpolation)**
val name = "Alice"
val age = 25
println("My name is $name, and I am $age years old.") // 변수 값을 문자열에 삽입
println("Next year, I will be ${age + 1} years old.") // 중괄호를 사용해 식도 삽입 가능

// **서식 지정 출력**
val pi = 3.14159
println("The value of pi is %.2f".format(pi)) // 소수점 둘째 자리까지 출력
println("Hello, %s! You are %d years old.".format(name, age)) // %s: 문자열, %d: 정수

2. 입력 (Input)

// **readLine() 사용 (기본 입력)**
print("Enter your name: ")
val userName = readLine() // 사용자 입력을 문자열로 읽음
println("Hello, $userName!")

// **숫자 입력 처리**
print("Enter your age: ")
val userAge = readLine()?.toIntOrNull() // 문자열을 정수로 변환 (null 안전)
if (userAge != null) {
    println("You are $userAge years old!")
} else {
    println("Invalid input. Please enter a valid number.")
}

// **여러 값 입력**
print("Enter two numbers separated by a space: ")
val (num1, num2) = readLine()!!.split(" ").map { it.toInt() } // 공백으로 구분된 두 숫자를 읽어 정수로 변환
println("Sum of $num1 and $num2 is ${num1 + num2}")

// **예외 처리와 함께 입력 처리**
try {
    print("Enter a decimal number: ")
    val decimal = readLine()!!.toDouble() // 문자열을 실수(Double)로 변환
    println("The value you entered is $decimal")
} catch (e: NumberFormatException) {
    println("Invalid input. Please enter a valid decimal number.")
}

3. 입력 및 출력 예제

fun main() {
    // 사용자 이름과 나이 입력받기
    print("What's your name? ")
    val name = readLine() ?: "Unknown" // Null 안전성을 위해 기본값 설정
    print("How old are you? ")
    val age = readLine()?.toIntOrNull() ?: 0 // 숫자 변환 실패 시 기본값 0
    
    // 입력 결과 출력
    println("Nice to meet you, $name!")
    println("You are $age years old.")
    
    // 두 숫자를 입력받아 합 계산
    print("Enter two numbers (e.g., 5 10): ")
    val (a, b) = readLine()!!.split(" ").map { it.toInt() }
    println("The sum of $a and $b is ${a + b}")
}

요약

작업 방법 설명
출력 print, println, format 사용 문자열 출력, 서식 지정 출력, 문자열 보간 등 다양한 방식 제공.
입력 readLine 사용 사용자로부터 문자열 입력받음. 숫자 변환 시 toIntOrNull, toDouble 활용.
에러 처리 try-catch 또는 null 체크 잘못된 입력에 대한 에러 처리 및 기본값 설정 가능.

 

 

조건식

 

1. if-else 조건문

// **기본 if-else**
val a = 10
val b = 20

if (a > b) {
    println("a is greater than b") // 조건이 참일 때 실행
} else {
    println("a is not greater than b") // 조건이 거짓일 때 실행
}

// **if-else if-else (여러 조건 처리)**
val number = 15

if (number > 0) {
    println("$number is positive") // 양수일 때 실행
} else if (number < 0) {
    println("$number is negative") // 음수일 때 실행
} else {
    println("$number is zero") // 0일 때 실행
}

// **if-else 표현식 (Expression)**
val max = if (a > b) a else b // 조건 결과를 변수에 저장
println("The maximum value is $max")

2. when 조건문

// **기본 when 사용**
val x = 2

when (x) {
    1 -> println("x is 1") // x가 1일 때
    2 -> println("x is 2") // x가 2일 때
    else -> println("x is neither 1 nor 2") // 어떤 경우에도 해당되지 않을 때
}

// **범위와 여러 값 검사**
val y = 10

when (y) {
    in 1..10 -> println("y is in the range 1 to 10") // 범위 검사
    in 11..20 -> println("y is in the range 11 to 20")
    else -> println("y is out of range")
}

// **is 연산자 사용**
val obj: Any = "Hello"

when (obj) {
    is String -> println("obj is a String") // obj가 String 타입일 때
    is Int -> println("obj is an Integer") // obj가 Int 타입일 때
    else -> println("obj is of an unknown type") // 다른 타입일 때
}

// **결과를 반환하는 when**
val z = 5
val result = when (z) {
    1 -> "One" // z가 1일 때 반환값
    2, 3 -> "Two or Three" // z가 2 또는 3일 때 반환값
    else -> "Other" // 어떤 경우에도 해당되지 않을 때
}
println("Result: $result")

3. 조건식 간소화

// **삼항 연산자 대체 (Kotlin은 삼항 연산자를 지원하지 않음)**
val isEven = if (x % 2 == 0) "Even" else "Odd" // 삼항 연산자 대신 if-else 표현식 사용
println("x is $isEven")

4. 조건식 활용 예제

fun main() {
    // 숫자 크기 비교
    val a = 10
    val b = 15
    println("Larger number: ${if (a > b) a else b}")

    // 범위와 타입 검사
    val input: Any = 42
    when (input) {
        in 1..50 -> println("Input is in range 1 to 50")
        is String -> println("Input is a string")
        else -> println("Input is something else")
    }

    // 다양한 조건 처리
    val score = 85
    val grade = when {
        score >= 90 -> "A"
        score >= 80 -> "B"
        score >= 70 -> "C"
        else -> "F"
    }
    println("Your grade is $grade")
}

요약

조건식 유형 설명 사용 예제
if-else 조건이 참인지 거짓인지 확인 후 실행. if (a > b) println("a > b") else println("a <= b")
if-else 표현식 조건 결과를 값으로 반환. val max = if (a > b) a else b
when 여러 조건을 확인하거나 값을 반환. when (x) { 1 -> ...; else -> ... }
범위/타입 검사 in으로 범위 확인, is로 타입 확인 가능. when (x) { in 1..10 -> ...; is String -> ... }
삼항 연산자 대체 Kotlin은 삼항 연산자를 지원하지 않아 if-else 표현식을 사용. val result = if (a > b) a else b

 

 

반복문

 

1. for 반복문

// **1. 기본 for문**
for (i in 1..5) { // 1부터 5까지 포함된 범위
    println("i = $i")
}

// **2. with step (간격 설정)**
for (i in 1..10 step 2) { // 1부터 10까지 2씩 증가
    println("i = $i")
}

// **3. downTo (역방향 반복)**
for (i in 5 downTo 1) { // 5부터 1까지 감소
    println("i = $i")
}

// **4. until (미포함 범위)**
for (i in 1 until 5) { // 1부터 4까지 (5는 포함하지 않음)
    println("i = $i")
}

// **5. 배열 및 리스트 반복**
val fruits = listOf("Apple", "Banana", "Cherry")
for (fruit in fruits) {
    println("Fruit: $fruit")
}

// **6. 인덱스와 함께 반복**
for ((index, value) in fruits.withIndex()) {
    println("Index $index: $value")
}

2. while 반복문

// **1. 기본 while문**
var count = 5
while (count > 0) {
    println("Count: $count")
    count--
}

// **2. 조건이 참일 때 실행**
val numbers = listOf(1, 2, 3, 4, 5)
var index = 0
while (index < numbers.size) {
    println("Number: ${numbers[index]}")
    index++
}

3. do-while 반복문

// **1. 기본 do-while문**
var num = 3
do {
    println("Number: $num")
    num--
} while (num > 0)

// **2. 무조건 한 번 실행**
do {
    println("This will run at least once!")
} while (false)

4. 반복문 제어 키워드

// **1. break (반복문 종료)**
for (i in 1..5) {
    if (i == 3) break // i가 3이면 반복문 종료
    println("i = $i")
}

// **2. continue (다음 반복으로 넘어감)**
for (i in 1..5) {
    if (i == 3) continue // i가 3이면 아래 코드 건너뜀
    println("i = $i")
}

// **3. 라벨(label)을 사용한 제어**
outer@ for (i in 1..3) {
    for (j in 1..3) {
        if (i == 2 && j == 2) break@outer // outer 반복문 종료
        println("i = $i, j = $j")
    }
}

5. 반복문 활용 예제

fun main() {
    // 1부터 10까지의 숫자 중 짝수만 출력
    for (i in 1..10) {
        if (i % 2 == 0) println("Even number: $i")
    }

    // 리스트 요소 출력
    val items = listOf("Kotlin", "Java", "Python")
    for (item in items) {
        println("Language: $item")
    }

    // 2중 반복문으로 구구단 출력
    for (i in 2..9) {
        for (j in 1..9) {
            print("${i * j}\t")
        }
        println()
    }

    // while문으로 합 계산
    var sum = 0
    var n = 1
    while (n <= 10) {
        sum += n
        n++
    }
    println("Sum of 1 to 10: $sum")
}

요약

반복문 설명 예제
for문 특정 범위 또는 컬렉션을 순회하며 반복. for (i in 1..5) println(i)
while문 조건이 참인 동안 반복 실행. while (x > 0) { x-- }
do-while문 조건과 관계없이 블록을 최소 한 번 실행한 후 조건을 확인. do { x-- } while (x > 0)
break 반복문을 즉시 종료. if (x == 3) break
continue 현재 반복을 건너뛰고 다음 반복 실행. if (x == 3) continue
라벨 사용 다중 반복문에서 특정 반복문을 제어. break@outer

 

 

형변환

📌 어떤 타입의 값을 다른 타입으로 변환하는 것을 말합니다. 흔히 타입 캐스팅이라고도 불립니다.

 

1. Kotlin에서의 형변환

  • Kotlin은 타입 안정성을 중시하여 명시적 형변환을 요구.
  • 형변환을 위해 toInt(), toDouble(), toString() 등의 변환 함수를 제공.
  • 안전한 형변환과 비안전한 형변환 연산자도 사용 가능.

2. 기본 형변환 (Primitive Type Casting)

// **정수형 변환**
val number = 42
val doubleValue = number.toDouble() // Int → Double
val stringValue = number.toString() // Int → String
println("Double: $doubleValue, String: $stringValue")

// **실수형 변환**
val decimal = 3.14
val intValue = decimal.toInt() // Double → Int (소수점 버림)
val longValue = decimal.toLong() // Double → Long
println("Int: $intValue, Long: $longValue")

// **문자열 변환**
val str = "123"
val parsedInt = str.toInt() // String → Int
val parsedDouble = str.toDouble() // String → Double
println("Parsed Int: $parsedInt, Parsed Double: $parsedDouble")

// **예외 처리**
val invalidStr = "abc"
val safeConversion = invalidStr.toIntOrNull() // 실패 시 null 반환
println("Safe Conversion: $safeConversion")

3. 스마트 캐스팅 (Smart Casting)

// **스마트 캐스팅이란?**
// Kotlin 컴파일러가 타입을 자동으로 추론하여 형변환을 수행.

fun printLength(obj: Any) {
    if (obj is String) { // is 연산자로 타입 검사
        println("String length: ${obj.length}") // obj는 자동으로 String으로 스마트 캐스팅
    } else {
        println("Not a String")
    }
}

printLength("Hello") // 출력: String length: 5
printLength(123)     // 출력: Not a String

4. 안전한 형변환 (Safe Casting)

// **as? 연산자 사용**
val obj: Any = "Kotlin"
val str: String? = obj as? String // 안전한 형변환 (실패 시 null 반환)
println("Safe Cast Result: $str") // 출력: Kotlin

val notString: Any = 123
val failedCast: String? = notString as? String // 실패 시 null 반환
println("Failed Cast Result: $failedCast") // 출력: null

5. 비안전한 형변환 (Unsafe Casting)

// **as 연산자 사용**
val obj: Any = "Kotlin"
val str: String = obj as String // 명시적 형변환
println("Unsafe Cast Result: $str") // 출력: Kotlin

val notString: Any = 123
// val failedCast: String = notString as String // ClassCastException 발생

6. Any, Unit, Nothing 타입

// **Any 타입**
val anyValue: Any = "This can be any type"
println("Any Value: $anyValue")

// **Unit 타입 (void와 유사)**
fun printMessage(): Unit { // 반환값이 없는 함수
    println("This function returns Unit")
}

// **Nothing 타입 (절대 반환되지 않음)**
fun throwError(): Nothing {
    throw IllegalArgumentException("This function always throws an exception")
}

7. 형변환 활용 예제

fun main() {
    // 1. 입력 값을 숫자로 변환 후 계산
    print("Enter a number: ")
    val input = readLine()?.toIntOrNull() ?: 0 // Null-safe 변환
    println("Double of your input: ${input * 2}")

    // 2. 다양한 타입을 처리하는 스마트 캐스팅
    val values: List<Any> = listOf(42, "Kotlin", 3.14, true)
    for (value in values) {
        when (value) {
            is Int -> println("$value is an Integer")
            is String -> println("$value is a String")
            is Double -> println("$value is a Double")
            else -> println("$value is of an unknown type")
        }
    }

    // 3. 안전한 형변환
    val unknown: Any = "Kotlin"
    val safeString: String? = unknown as? String
    println("Safe Cast: $safeString")
}

요약

형변환 종류 설명  예제
기본 형변환 toInt(), toDouble(), toString() 등 기본 제공 함수로 타입 변환. val x = "123".toInt()
스마트 캐스팅 is 연산자로 타입 확인 후 자동 형변환. if (obj is String) obj.length
안전한 형변환 as? 연산자로 실패 시 null 반환. val str = obj as? String
비안전한 형변환 as 연산자로 명시적 형변환, 실패 시 예외 발생. val str = obj as String
Any, Unit, Nothing Any: 모든 타입의 최상위 클래스.Unit: 반환값 없는 함수.Nothing: 반환 불가 타입. val x: Any = "Hello"

 

 

 

함수 사용

📌 특정 로직을 가지는 소스코드에 별명을 붙인 것으로, 특정 기능을 수행하는 로직을 함수라고 부릅니다.

 

1. 함수 기본 구조

// **기본 함수**
fun greet() {
    println("Hello, Kotlin!") // 출력만 수행
}

greet() // 함수 호출

2. 매개변수와 반환값이 있는 함수

// **매개변수와 반환값이 있는 함수**
fun add(a: Int, b: Int): Int { // 두 개의 정수 매개변수와 반환값(Int)을 선언
    return a + b // 두 값을 더해 반환
}

val sum = add(3, 5) // 함수 호출 후 결과를 변수에 저장
println("Sum: $sum") // 출력: Sum: 8

3. 단일 표현식 함수

// **단일 표현식 함수**
fun multiply(a: Int, b: Int): Int = a * b // 단일 식을 사용하는 함수

val product = multiply(4, 5) // 결과: 20
println("Product: $product")

4. 기본값을 가진 매개변수

// **기본값 매개변수**
fun greetUser(name: String = "Guest") {
    println("Hello, $name!")
}

greetUser("Alice") // 출력: Hello, Alice!
greetUser() // 출력: Hello, Guest!

5. 가변인자 (Varargs)

// **가변인자**
fun printAll(vararg numbers: Int) {
    for (num in numbers) {
        println(num)
    }
}

printAll(1, 2, 3, 4, 5) // 가변 개수의 매개변수를 전달

6. 함수 호출 시 이름 붙인 매개변수

// **이름 붙인 매개변수**
fun formatMessage(greeting: String, name: String): String {
    return "$greeting, $name!"
}

val message = formatMessage(greeting = "Hi", name = "Bob") // 매개변수 이름을 명시적으로 지정
println(message) // 출력: Hi, Bob!

7. 고차 함수 (함수를 매개변수로 받는 함수)

// **고차 함수**
fun calculate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
    return operation(a, b) // 전달받은 함수로 계산 수행
}

val sumResult = calculate(3, 7) { x, y -> x + y } // 람다 표현식으로 함수 전달
println("Sum Result: $sumResult") // 출력: Sum Result: 10

8. 확장 함수

// **확장 함수**
fun String.addExclamation(): String {
    return this + "!" // 문자열에 느낌표 추가
}

val excited = "Kotlin".addExclamation() // 기존 String 클래스에 함수 추가
println(excited) // 출력: Kotlin!

9. 인라인 함수

// **인라인 함수**
inline fun executeInline(action: () -> Unit) {
    action() // 전달받은 함수 실행
}

executeInline { println("Inline function executed!") } // 출력: Inline function executed!

10. 지역 함수

// **지역 함수**
fun outerFunction(a: Int, b: Int): Int {
    fun innerFunction(x: Int): Int { // 외부 함수 내부에서 정의된 함수
        return x * x
    }
    return innerFunction(a) + innerFunction(b)
}

val result = outerFunction(2, 3) // 결과: 13
println("Result: $result")

11. 재귀 함수

// **재귀 함수**
fun factorial(n: Int): Int {
    return if (n == 1) 1 else n * factorial(n - 1) // 자기 자신 호출
}

val fact = factorial(5) // 결과: 120
println("Factorial: $fact")

요약

유형 설명 예제
기본 함수 입력값 없이 실행되는 함수. fun greet() { ... }
매개변수와 반환값 매개변수와 반환 타입을 지정. fun add(a: Int, b: Int): Int
단일 표현식 함수 간단한 식으로 반환값을 처리. fun multiply(a: Int, b: Int): Int = a * b
기본값 매개변수 매개변수에 기본값을 설정. fun greet(name: String = "Guest")
가변인자 여러 개의 값을 배열처럼 받을 수 있음. fun printAll(vararg nums: Int)
고차 함수 함수를 매개변수로 전달하거나 반환. fun calculate(a: Int, b: Int, op: (Int, Int) -> Int)
확장 함수 기존 클래스에 새로운 함수 추가. fun String.addExclamation(): String
인라인 함수 컴파일 시 코드가 삽입되어 성능 최적화 가능. inline fun executeInline(action: () -> Unit)
지역 함수 함수 내에 정의된 함수로 외부 함수 내부에서만 사용 가능. fun outer() { fun inner() { ... } }
재귀 함수 자기 자신을 호출하여 작업을 반복. fun factorial(n: Int): Int

 

 

 

Collection과 반복

📌 Collection은 데이터를 수집하거나 그룹화하는 자료구조입니다. Kotlin에서는 주로 List와 Map을 사용합니다.

  • List: 순서가 있는 데이터의 목록으로, 중복된 값이 포함될 수 있습니다.
  • Map: 키와 값의 쌍으로 이루어진 데이터 구조로, 각 키는 유일해야 합니다.
  • Set: 중복되지 않는 요소의 집합

1. List

// **불변 리스트**
val immutableList = listOf("Apple", "Banana", "Cherry") // 불변 리스트 (수정 불가)
println(immutableList[1]) // 인덱스로 접근 (출력: Banana)

// **가변 리스트**
val mutableList = mutableListOf("Apple", "Banana")
mutableList.add("Cherry") // 요소 추가
mutableList[0] = "Orange" // 요소 수정
println(mutableList) // 출력: [Orange, Banana, Cherry]

// **리스트 반복**
for (item in immutableList) { // for-each 반복
    println(item)
}

// **인덱스와 함께 반복**
for ((index, value) in immutableList.withIndex()) {
    println("Index $index: $value") // 출력: Index 0: Apple, Index 1: Banana, ...
}

2. Set

// **불변 집합**
val immutableSet = setOf("Apple", "Banana", "Cherry", "Apple") // 중복 허용하지 않음
println(immutableSet) // 출력: [Apple, Banana, Cherry]

// **가변 집합**
val mutableSet = mutableSetOf("Apple", "Banana")
mutableSet.add("Cherry") // 요소 추가
mutableSet.remove("Banana") // 요소 제거
println(mutableSet) // 출력: [Apple, Cherry]

// **Set 반복**
for (item in mutableSet) {
    println(item)
}

3. Map

// **불변 맵**
val immutableMap = mapOf("A" to 1, "B" to 2, "C" to 3) // 키-값 쌍 생성
println(immutableMap["B"]) // 키로 값 접근 (출력: 2)

// **가변 맵**
val mutableMap = mutableMapOf("A" to 1, "B" to 2)
mutableMap["C"] = 3 // 키-값 추가
mutableMap["A"] = 10 // 기존 키의 값 수정
mutableMap.remove("B") // 키-값 제거
println(mutableMap) // 출력: {A=10, C=3}

// **Map 반복**
for ((key, value) in immutableMap) {
    println("$key -> $value") // 출력: A -> 1, B -> 2, ...
}

4. 반복문을 사용한 컬렉션 처리

// **forEach 사용**
val fruits = listOf("Apple", "Banana", "Cherry")
fruits.forEach { fruit -> println(fruit) } // 각 요소에 대해 작업 수행

// **map을 사용한 변환**
val lengths = fruits.map { it.length } // 각 문자열의 길이를 계산
println(lengths) // 출력: [5, 6, 6]

// **filter를 사용한 조건 필터링**
val filtered = fruits.filter { it.startsWith("A") } // "A"로 시작하는 요소만 필터링
println(filtered) // 출력: [Apple]

// **flatMap으로 중첩된 리스트 처리**
val nestedList = listOf(listOf(1, 2, 3), listOf(4, 5))
val flatList = nestedList.flatMap { it } // 중첩 리스트를 단일 리스트로 변환
println(flatList) // 출력: [1, 2, 3, 4, 5]

5. 기타 컬렉션 함수

// **find: 조건에 맞는 첫 번째 요소 반환**
val firstMatch = fruits.find { it.contains("e") } // "e"를 포함하는 첫 번째 요소
println(firstMatch) // 출력: Apple

// **any: 조건에 맞는 요소가 하나라도 있으면 true**
val hasLongName = fruits.any { it.length > 5 }
println(hasLongName) // 출력: true

// **all: 모든 요소가 조건을 만족하면 true**
val allShortNames = fruits.all { it.length < 7 }
println(allShortNames) // 출력: true

// **count: 조건에 맞는 요소의 개수**
val countStartsWithB = fruits.count { it.startsWith("B") }
println(countStartsWithB) // 출력: 1

// **sort: 정렬**
val sortedFruits = fruits.sorted() // 오름차순 정렬
println(sortedFruits) // 출력: [Apple, Banana, Cherry]

// **groupBy: 조건에 따라 그룹화**
val grouped = fruits.groupBy { it.first() } // 첫 글자로 그룹화
println(grouped) // 출력: {A=[Apple], B=[Banana], C=[Cherry]}

요약

컬렉션 타입  불변 가변
List listOf("A", "B") mutableListOf("A", "B")
Set setOf("A", "B") mutableSetOf("A", "B")
Map mapOf("A" to 1, "B" to 2) mutableMapOf("A" to 1, "B" to 2)

 

반복문 사용  설명 예제
for 각 요소에 대해 순회. for (item in list) { ... }
forEach 고차 함수를 사용해 각 요소에 대해 작업 수행. list.forEach { println(it) }
map 각 요소를 변환하여 새로운 컬렉션 생성. list.map { it.length }
filter 조건에 맞는 요소만 포함한 새로운 컬렉션 생성. list.filter { it.startsWith("A") }
flatMap 중첩된 리스트를 단일 리스트로 변환. nestedList.flatMap { it }
groupBy 조건에 따라 요소를 그룹화. list.groupBy { it.first() }

 

고차함수

📌 고차 함수는 함수를 매개변수로 받거나, 함수를 반환하는 함수입니다.

  • Kotlin은 함수형 프로그래밍을 지원하며, 고차 함수는 코드의 간결성과 재사용성을 높입니다.

 

 

1. 고차 함수의 정의

// **함수를 매개변수로 받는 고차 함수**
fun calculate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
    return operation(a, b) // 전달받은 함수를 호출
}

// 고차 함수 호출
val sum = calculate(5, 10) { x, y -> x + y } // 람다식으로 함수 전달
println("Sum: $sum") // 출력: Sum: 15

// **함수를 반환하는 고차 함수**
fun getOperation(type: String): (Int, Int) -> Int {
    return when (type) {
        "add" -> { x, y -> x + y } // 덧셈 함수 반환
        "multiply" -> { x, y -> x * y } // 곱셈 함수 반환
        else -> { _, _ -> 0 } // 기본값 반환
    }
}

// 반환된 함수 호출
val operation = getOperation("add")
println("Result: ${operation(3, 7)}") // 출력: Result: 10

2. 람다 표현식 (Lambda Expressions)

// **람다 기본 문법**
val add: (Int, Int) -> Int = { x, y -> x + y }
println("Add: ${add(3, 4)}") // 출력: Add: 7

// **단일 매개변수 사용**
val square: (Int) -> Int = { it * it } // `it` 키워드로 단일 매개변수 사용
println("Square: ${square(5)}") // 출력: Square: 25

// **다양한 람다 사용법**
val printMessage: (String) -> Unit = { message -> println("Message: $message") }
printMessage("Hello, Kotlin!") // 출력: Message: Hello, Kotlin!

3. 익명 함수 (Anonymous Functions)

// **익명 함수**
val subtract = fun(a: Int, b: Int): Int {
    return a - b
}

println("Subtract: ${subtract(10, 4)}") // 출력: Subtract: 6

4. 주요 고차 함수 예제

1. forEach

  • 컬렉션의 각 요소에 대해 작업 수행.
val numbers = listOf(1, 2, 3, 4, 5)
numbers.forEach { println("Number: $it") } // 각 요소 출력

2. map

  • 컬렉션의 각 요소를 변환하여 새 컬렉션 생성.
val squaredNumbers = numbers.map { it * it } // 각 요소를 제곱
println("Squared: $squaredNumbers") // 출력: Squared: [1, 4, 9, 16, 25]

3. filter

  • 조건에 맞는 요소만 포함한 새 컬렉션 생성.
val evenNumbers = numbers.filter { it % 2 == 0 } // 짝수만 필터링
println("Even Numbers: $evenNumbers") // 출력: Even Numbers: [2, 4]

4. reduce

  • 컬렉션의 모든 요소를 하나의 값으로 누적.
val sum = numbers.reduce { acc, num -> acc + num } // 요소들의 합 계산
println("Sum: $sum") // 출력: Sum: 15

5. fold

  • 초기값과 함께 컬렉션을 누적.
val sumWithInitial = numbers.fold(10) { acc, num -> acc + num } // 초기값 10 포함
println("Sum with Initial: $sumWithInitial") // 출력: Sum with Initial: 25

6. flatMap

  • 중첩된 리스트를 단일 리스트로 변환.
val nestedList = listOf(listOf(1, 2), listOf(3, 4, 5))
val flatList = nestedList.flatMap { it }
println("Flat List: $flatList") // 출력: Flat List: [1, 2, 3, 4, 5]

7. take / drop

  • 일부 요소를 선택하거나 제외.
val firstTwo = numbers.take(2) // 처음 두 요소 선택
println("First Two: $firstTwo") // 출력: First Two: [1, 2]

val dropTwo = numbers.drop(2) // 처음 두 요소 제외
println("After Drop: $dropTwo") // 출력: After Drop: [3, 4, 5]

5. 고차 함수의 장점

장점 설명
코드 간결화 반복적이고 장황한 코드 대신 고차 함수를 사용해 간결하게 작성 가능.
재사용성 고차 함수는 추상화 수준을 높여 다양한 상황에서 재사용 가능.
유연성 매개변수로 함수를 전달하여 동작을 동적으로 변경 가능.
함수형 프로그래밍 지원 Kotlin의 함수형 프로그래밍 특성을 활용하여 더 높은 수준의 코드를 작성 가능.

6. 예제: 고차 함수 활용

fun performOperation(numbers: List<Int>, operation: (Int) -> Int): List<Int> {
    return numbers.map { operation(it) }
}

// 고차 함수 사용
val doubledNumbers = performOperation(listOf(1, 2, 3, 4)) { it * 2 } // 각 숫자를 2배로
println("Doubled: $doubledNumbers") // 출력: Doubled: [2, 4, 6, 8]

val squaredNumbers = performOperation(listOf(1, 2, 3, 4)) { it * it } // 각 숫자를 제곱
println("Squared: $squaredNumbers") // 출력: Squared: [1, 4, 9, 16]

요약

항목 설명 예제
고차 함수 함수를 매개변수로 받거나 반환하는 함수. fun calculate(a: Int, b: Int, op: (Int, Int) -> Int)
람다 표현식 익명 함수의 간결한 표현으로 함수형 프로그래밍 지원. { x, y -> x + y }
주요 고차 함수 map, filter, reduce, fold, flatMap, forEach. list.map { it * 2 }
장점 코드 간결화, 유연성, 재사용성 증가.  

 

 

Nullable type

📌 null이 될 수 있는 변수를 의미한다.

  • null은 값이 없음, 값이 존재하지 않음을 의미한다. 변수를 만들었지만 그 안에 값을 넣지 않은 경우를 흔히 말한다.
  • Kotlin에서 모든 타입은 기본적으로 NULL을 허용하지 않는다. 다만 타입에 ? 을 명시하면 사용이 가능하다.

1. Nullable 타입 선언

// **Nullable 타입 선언**
var name: String? = null // `?`를 사용해 Null을 허용
name = "Kotlin" // Null이 아닌 값 할당 가능
println(name) // 출력: Kotlin

// **Non-null 타입**
var nonNullableName: String = "Hello"
// nonNullableName = null // 컴파일 에러: Null 불허

2. Null 안전성 연산자

1) Safe Call Operator (?.)

  • Null 값을 허용하는 변수에서 안전하게 프로퍼티나 메서드를 호출.
val nullableName: String? = null
println(nullableName?.length) // 출력: null (예외 발생 없이 안전)

val nonNullName: String? = "Kotlin"
println(nonNullName?.length) // 출력: 6

2) Elvis Operator (?:)

  • Null 값일 경우 기본값을 반환.
val nullableText: String? = null
val result = nullableText ?: "Default Value" // Null이면 "Default Value" 반환
println(result) // 출력: Default Value

3) Non-null Assertion Operator (!!)

  • 변수 값이 Null이 아니라고 확신할 때 사용.
  • Null일 경우 NullPointerException 발생.
val nullableString: String? = "Kotlin"
println(nullableString!!.length) // 출력: 6

val nullValue: String? = null
// println(nullValue!!.length) // NullPointerException 발생

3. Nullable 타입 활용

1) if 문을 사용한 Null 체크

  • Null 여부를 확인하여 처리.
val str: String? = null
if (str != null) {
    println("Length: ${str.length}")
} else {
    println("String is null")
}

2) Smart Casting (is)

  • Kotlin은 Null 체크 후 해당 변수의 타입을 자동으로 변환(스마트 캐스팅).
val nullable: String? = "Kotlin"
if (nullable is String) {
    println("Length: ${nullable.length}") // 스마트 캐스팅으로 안전하게 호출
}

4. Safe Call with Let

  • **let**은 Null 값을 제외하고 실행할 코드를 작성 가능.
val nullableValue: String? = "Hello"
nullableValue?.let { println("Length: ${it.length}") } // 출력: Length: 5

val nullValue: String? = null
nullValue?.let { println(it) } // 아무 작업도 수행하지 않음

5. Nullable 타입과 컬렉션

1) 컬렉션에 Nullable 타입 포함

  • 컬렉션에 Null 값을 허용.
val list: List<String?> = listOf("A", null, "C")
println(list) // 출력: [A, null, C]

2) Null 값 필터링

  • 컬렉션에서 Null 값을 제거.
val nullableList: List<String?> = listOf("A", null, "C")
val nonNullList = nullableList.filterNotNull() // Null 제거
println(nonNullList) // 출력: [A, C]

6. Nullable 타입 예제

fun printLength(text: String?) {
    val length = text?.length ?: 0 // Null이면 0 반환
    println("Length: $length")
}

fun main() {
    printLength("Kotlin") // 출력: Length: 6
    printLength(null) // 출력: Length: 0

    // Safe call과 let 활용
    val message: String? = "Hello Kotlin"
    message?.let { println("Message length: ${it.length}") } // 출력: Message length: 12
}

요약

연산자/기법 설명 예제
Safe Call (?.) Null일 경우 호출하지 않고 Null 반환. nullable?.length
Elvis Operator (?:) Null일 경우 기본값 반환. val result = nullable ?: "Default"
Non-null Assertion (!!) Null이 아니라고 확신할 때 사용, Null이면 예외 발생. nullable!!.length
if 문을 사용한 체크 Null 여부를 확인하여 안전하게 처리. if (str != null) { ... }
Smart Casting (is) Null 체크 후 자동으로 Non-null 타입으로 변환. if (nullable is String) { ... }
let 블록 사용 Null이 아닌 경우에만 코드 블록 실행. nullable?.let { println(it.length) }
Null 필터링 컬렉션에서 Null 값을 제거. list.filterNotNull()

 

 

예외 처리

📌 예외(Exception)는 프로그램 실행 중 발생하는 오류 상황을 말하며 이런 상황에 대한 대응을 예외 처리라 한다.

  • Kotlin은 try-catch-finally 블록을 사용하여 예외를 처리.
  • 모든 예외는 Throwable 클래스를 상속받음.

 

1. try-catch 블록

// **기본 구조**
try {
    // 예외 발생 가능성이 있는 코드
    val number = "abc".toInt() // 문자열을 정수로 변환하려다 예외 발생
    println("Number: $number")
} catch (e: NumberFormatException) {
    // 예외 처리
    println("Exception occurred: ${e.message}")
}

// **결과**
// 출력: Exception occurred: For input string: "abc"

2. 여러 catch 블록

try {
    val result = 10 / 0 // ArithmeticException 발생
    println("Result: $result")
} catch (e: ArithmeticException) {
    println("ArithmeticException: Division by zero is not allowed.")
} catch (e: Exception) {
    println("Generic Exception: ${e.message}")
}

// **결과**
// 출력: ArithmeticException: Division by zero is not allowed.

3. finally 블록

// **try-catch-finally 구조**
try {
    val value = "123".toInt() // 정상적으로 실행
    println("Value: $value")
} catch (e: Exception) {
    println("Exception: ${e.message}")
} finally {
    // 예외 발생 여부와 관계없이 항상 실행
    println("Finally block executed.")
}

// **결과**
// 출력:
// Value: 123
// Finally block executed.

4. 예외 발생시키기 (throw)

// **throw 키워드로 예외 발생**
fun divide(a: Int, b: Int): Int {
    if (b == 0) throw IllegalArgumentException("Division by zero is not allowed.") // 예외 발생
    return a / b
}

try {
    println(divide(10, 0)) // 예외 발생
} catch (e: IllegalArgumentException) {
    println("Caught Exception: ${e.message}")
}

// **결과**
// 출력: Caught Exception: Division by zero is not allowed.

5. 사용자 정의 예외

// **사용자 정의 예외 클래스**
class CustomException(message: String) : Exception(message)

fun checkValue(value: Int) {
    if (value < 0) throw CustomException("Negative values are not allowed.") // 사용자 정의 예외 발생
}

try {
    checkValue(-10) // 예외 발생
} catch (e: CustomException) {
    println("Custom Exception: ${e.message}")
}

// **결과**
// 출력: Custom Exception: Negative values are not allowed.

6. 예외 처리와 Null 안전성

// **toIntOrNull 사용으로 Null 안전 처리**
val number = "abc".toIntOrNull() ?: 0 // 변환 실패 시 Null 반환
println("Number: $number") // 출력: Number: 0

7. 예외 처리 활용 예제

fun readNumber(): Int {
    print("Enter a number: ")
    val input = readLine()
    return try {
        input?.toInt() ?: 0 // 입력값을 정수로 변환, 실패 시 0 반환
    } catch (e: NumberFormatException) {
        println("Invalid number format: ${e.message}")
        -1 // 예외 발생 시 -1 반환
    }
}

val result = readNumber()
println("Result: $result")

8. 예외 처리의 주요 키워드

키워드 설명 예제
try 예외 발생 가능성이 있는 코드를 실행. try { ... }
catch 발생한 예외를 처리. catch (e: Exception) { ... }
finally 예외 발생 여부와 관계없이 항상 실행되는 코드. finally { ... }
throw 예외를 명시적으로 발생. throw IllegalArgumentException("Error")
toIntOrNull 예외 없이 문자열을 안전하게 정수로 변환, 실패 시 Null 반환. "123".toIntOrNull()

9. 예외 처리의 장점

장점 설명
프로그램 안정성 유지 예외를 처리하여 프로그램이 중단되지 않고 계속 실행 가능.
문제 디버깅 용이 예외 메시지를 통해 문제를 정확히 파악 가능.
NullPointerException 방지 Kotlin의 toIntOrNull 등 Null 안전 기능과 결합하여 예외를 줄임.

 

'Back-End (Web) > Kotlin' 카테고리의 다른 글

[Kotlin] Kotlin의 테스트코드  (0) 2025.02.16
[Kotlin] Kotlin의 객체지향  (0) 2025.02.12
[Kotlin] Kotlin이란?  (0) 2025.02.10

Kotlin

📌Kotlin은 JetBrains(intelliJ IDEA의 개발사)가 2011년에 발표한 현대적인 프로그래밍 언어입니다.

  • JVM(Java Virtual Machine) 위에서 실행되며, Java와 100% 호환됩니다.
  • 간결하고 안전하며, 함수형 프로그래밍과 객체지향 프로그래밍(OOP)을 지원합니다.
  • Android의 공식 프로그래밍 언어로 채택되었습니다(Google I/O 2017).

 


Kotlin의 주요 특징

특징 설명
Java와 100% 호환 Java 코드와 상호운용 가능하며, 기존 Java 프로젝트에 Kotlin을 통합 가능.
간결한 문법 Java보다 코드 길이가 짧고 명확해 유지보수 용이.
Null 안전성 NullPointerException을 방지하기 위한 안전한 설계.
함수형 프로그래밍 지원 람다 표현식, 고차 함수, 확장 함수 등 함수형 프로그래밍에 적합한 기능 제공.
안드로이드 최적화 Android 앱 개발에 최적화된 도구와 확장 기능 제공.
코루틴 지원 비동기 프로그래밍을 간결하고 효율적으로 처리.
멀티플랫폼 개발 지원 Kotlin Multiplatform을 통해 Android, iOS, 웹 등 다양한 플랫폼에서 코드 공유 가능.

3. Kotlin의 장점

항목 설명
코드 간결성 Java 코드보다 단순하고 짧은 코드로 동일한 기능 구현 가능.
생산성 향상 간결한 문법과 다양한 내장 기능으로 개발 속도 증가.
NullPointerException 방지 컴파일 단계에서 Null 체크를 강제하여 안정성 제공.
확장 함수 기존 클래스나 라이브러리를 수정하지 않고도 기능 추가 가능.
다중 패러다임 지원 객체지향(OOP)과 함수형 프로그래밍(FP)을 모두 지원.

4. Kotlin의 단점

항목 설명
학습 곡선 Java에 비해 문법이 다소 복잡해 초기 학습이 필요.
컴파일 속도 일부 경우 Java보다 느릴 수 있음(특히 처음 빌드 시).
생태계 규모 Java에 비해 라이브러리와 커뮤니티 규모가 작지만, 빠르게 성장 중.
런타임 크기 증가 Kotlin 표준 라이브러리로 인해 애플리케이션 크기가 약간 증가할 수 있음(Android 환경에서 주의 필요).

5. Kotlin의 주요 사용 사례

분야  사용 사례
Android 앱 개발 Android의 공식 언어로 사용. 기존 Java 프로젝트와 통합 용이.
서버 개발 Ktor, Spring과 같은 프레임워크로 서버 애플리케이션 개발 가능.
멀티플랫폼 개발 Kotlin Multiplatform을 통해 Android, iOS, 웹 등 다양한 플랫폼에서 코드 공유.
데스크톱 애플리케이션 JavaFX와 통합하여 데스크톱 애플리케이션 개발 가능.

6. Kotlin의 핵심 기능

기능 설명
데이터 클래스 간단한 구문으로 데이터 객체 생성. 자동으로 getter, setter, equals, hashCode 생성.
코루틴(Coroutines) 비동기 작업을 간단하고 효율적으로 처리 가능.
확장 함수 기존 클래스에 새로운 메서드를 추가하는 기능 제공.
Null 안전성 ?와 !! 연산자를 통해 NullPointerException 방지.
람다(Lambda) 표현식 간결한 함수 표현으로 함수형 프로그래밍 지원.

7. Kotlin과 Java 비교

항목 Kotlin Java
출시 연도 2011년 (JetBrains) 1995년 (Sun Microsystems, 현재 Oracle)
코드 길이 간결하고 명확. 더 장황하고 반복적인 코드 요구.
Null 안전성 기본적으로 NullPointerException 방지. NullPointerException 직접 관리 필요.
모던 기능 지원 코루틴, 확장 함수, 고차 함수 등 최신 프로그래밍 기능 지원. 일부 최신 기능(Java 8 이상)에서만 지원.

8. Kotlin의 현재와 미래

  • Android 개발의 표준:
    • Google이 Kotlin을 Android의 공식 프로그래밍 언어로 채택.
  • 빠르게 성장 중인 생태계:
    • Spring Framework, Gradle 등 다양한 툴과의 통합으로 서버 개발에서도 인기 상승.
  • 멀티플랫폼 개발:
    • Kotlin Multiplatform으로 iOS, 웹, 데스크톱 등 다양한 플랫폼 지원.

 

'Back-End (Web) > Kotlin' 카테고리의 다른 글

[Kotlin] Kotlin의 테스트코드  (0) 2025.02.16
[Kotlin] Kotlin의 객체지향  (0) 2025.02.12
[Kotlin] Kotlin의 사용  (0) 2025.02.11

N + 1 문

📌 JPA가 연관된 엔티티를 조회할 때 추가적인 쿼리를 반복적으로 실행하기 때문에 발생하는 문제

 

  • 지연 로딩의 N+1
    • Tutor N : 1 Company 양방향 연관관계
@Entity
@Table(name = "tutor")
public class Tutor {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "company_id")
    private Company company;

    public Tutor() {
    }

    public Tutor(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setCompany(Company company) {
        this.company = company;
    }
}
@Entity
@Table(name = "company")
public class Company {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "company")
    private List<Tutor> tutors = new ArrayList<>();

    public Company() {
    }

    public Company(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public List<Tutor> getTutors() {
        return tutors;
    }

}
public class LazyMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("entity");

        EntityManager em = emf.createEntityManager();

        EntityTransaction transaction = em.getTransaction();

        transaction.begin();

        try {

            Company sparta = new Company("sparta");
            Company etc = new Company("etc");

            em.persist(sparta);
            em.persist(etc);

            Tutor tutor1 = new Tutor("tutor1" );
            Tutor tutor2 = new Tutor("tutor2" );
            Tutor tutor3 = new Tutor("tutor3" );

            tutor1.setCompany(sparta);
            tutor2.setCompany(etc);
            tutor3.setCompany(sparta);

            em.persist(tutor1);
            em.persist(tutor2);
            em.persist(tutor3);

            em.flush();
            em.clear();

            String query = "select t from Tutor t";
            List<Tutor> tutorList = em.createQuery(query, Tutor.class).getResultList();

            for (Tutor tutor : tutorList) {
                System.out.println("tutor.getName() = " + tutor.getName());
                System.out.println("tutor.getCompany().getName() = " + tutor.getCompany().getName());
            }


            transaction.commit();
        } catch (Exception e) {
            transaction.rollback();
        } finally {
            em.close();
        }

        emf.close();
    }
}

  • 반복문 1
    1. tutor1~3 조회 및 1차 캐시에 저장
    2. tutor1 출력
    3. sparta 조회 및 1차 캐시에 저장
    4. sparta 출력
  • 반복문 2
    1. 1차 캐시의 tutor2 출력
    2. etc 조회 및 1차 캐시에 저장
    3. etc 출력
  • 반복문 3
    1. 1차 캐시의 tutor3 출력
    2. 1차 캐시의 sparta 출력
  • 튜터(N), 회사(1)가 많아질수록 더 많은 N + 1 문제가 생긴다.

 

Entity fetch join

📌 JPQL에서 성능 최적화를 위해 fetch join을 제공하며 연관된 엔티티나 컬렉션을 SQL 한번으로 조회할 수 있도록 해주는 기능이다.

  • fetch join은 SQL의 JOIN과는 다른 종류이고 객체 그래프를 SQL 한 번에 조회한다. 실무에서 굉장히 많이 활용한다.

 

  • N:1 fetch join(Entity)
    • Tutor N : 1 Comapny 연관관계
    • 일반 JOIN

  • fetch join

  • 튜터 조회시 연관된 회사도 함께 조회

더보기

지연 로딩과 Fetch Join의 동작 원리


1. 지연 로딩(Lazy Loading)

지연 로딩은 연관된 엔티티 데이터를 처음에는 로드하지 않고, 실제로 필요할 때 데이터베이스에서 가져오는 전략입니다.

  • 장점:
    • 초기 로딩 시 불필요한 데이터를 가져오지 않아 성능 최적화.
    • 연관된 엔티티가 필요하지 않을 경우 데이터베이스 접근을 줄일 수 있음.
  • 작동 방식:
    • 연관된 엔티티는 프록시(Proxy) 객체로 초기화.
    • 프록시 객체는 원본 데이터를 대신하며, 실제 데이터가 필요할 때 데이터베이스에서 조회.

예제:

@Entity
public class Member {
    @Id
    @GeneratedValue
    private Long id;
    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    private Team team; // 지연 로딩
}

// 사용 시
Member member = entityManager.find(Member.class, 1L);
System.out.println(member.getName()); // 즉시 로드
System.out.println(member.getTeam().getName()); // 이 시점에 Team 데이터를 DB에서 조회

2. Fetch Join

Fetch JoinJPQL에서 연관된 엔티티를 한 번에 가져오도록 명시적으로 지정하는 방법입니다.

  • 장점:
    • 여러 테이블을 조인하여 필요한 데이터를 한 번에 가져옴.
    • N+1 문제를 방지.
  • 작동 방식:
    • JPQL에서 FETCH JOIN 키워드를 사용하면, 연관된 엔티티를 즉시 로딩하여 프록시 객체 대신 실제 엔티티 객체를 가져옵니다.
    • 데이터베이스 조회 시점에 JOIN 쿼리가 실행되며, 필요한 모든 데이터를 로드.

예제:

String jpql = "SELECT m FROM Member m JOIN FETCH m.team";
List<Member> members = entityManager.createQuery(jpql, Member.class).getResultList();

for (Member member : members) {
    System.out.println(member.getName()); // 즉시 로드
    System.out.println(member.getTeam().getName()); // Team도 즉시 로드 (프록시 X)
}

Fetch Join이 우선권을 가지는 이유

1. Fetch Join은 JPQL에서 명시적으로 즉시 로딩을 요구

  • 지연 로딩(LAZY) 설정이 되어 있더라도, FETCH JOIN이 사용되면 JPA가 지연 로딩 대신 즉시 로딩을 수행합니다.
  • 이는 JPQL에서 개발자가 명시적으로 데이터 로딩 방식을 지정했기 때문입니다.

2. 프록시 대신 실제 엔티티 반환

  • FETCH JOIN을 사용하면 연관된 엔티티도 함께 로드됩니다.
  • 따라서 연관된 엔티티는 프록시 객체가 아닌 실제 엔티티 객체로 초기화됩니다.

예제 비교:

지연 로딩 설정 (기본):

Member member = entityManager.find(Member.class, 1L); // Team은 로드되지 않음
System.out.println(member.getTeam().getName()); // 이 시점에 Team을 DB에서 조회

Fetch Join 사용:

String jpql = "SELECT m FROM Member m JOIN FETCH m.team";
Member member = entityManager.createQuery(jpql, Member.class).getSingleResult();

// 이미 Team이 로드됨
System.out.println(member.getTeam().getName()); // 추가 DB 조회 없음

Fetch Join 후 모든 엔티티가 영속성 컨텍스트로 관리

1. 영속성 컨텍스트의 기본 동작

  • JPA는 엔티티를 조회할 때, 해당 엔티티를 영속성 컨텍스트에 저장하고 관리합니다.
  • Fetch Join으로 조회한 연관된 엔티티도 영속성 컨텍스트에서 관리됩니다.

2. Fetch Join의 효과

  • Fetch Join을 사용하면 연관된 엔티티도 데이터베이스에서 즉시 로드되며, 영속성 컨텍스트에 저장됩니다.
  • 결과적으로 Fetch Join으로 조회된 엔티티는 모두 영속 상태로 관리됩니다.

예제:

String jpql = "SELECT m FROM Member m JOIN FETCH m.team";
List<Member> members = entityManager.createQuery(jpql, Member.class).getResultList();

for (Member member : members) {
    System.out.println(entityManager.contains(member)); // true (영속 상태)
    System.out.println(entityManager.contains(member.getTeam())); // true (영속 상태)
}

Fetch Join이 Proxy 객체 대신 실제 객체를 반환하는 이유

1. 명시적 로딩

  • Fetch Join은 연관된 엔티티를 즉시 로딩하기 때문에, 데이터베이스 조회 시점에 실제 데이터로 초기화된 엔티티를 반환합니다.

2. N+1 문제 방지

  • Fetch Join은 한 번의 쿼리로 여러 엔티티를 로드하므로, **지연 로딩에서 발생할 수 있는 추가 쿼리(N+1 문제)**를 방지합니다.

3. 프록시 대신 실제 엔티티 반환

  • Fetch Join은 데이터베이스에서 가져온 데이터를 기반으로 연관된 엔티티를 실제 객체로 초기화하므로, 프록시 객체가 필요 없습니다.

Fetch Join의 쿼리 예제

JPQL:

String jpql = "SELECT m FROM Member m JOIN FETCH m.team";
List<Member> members = entityManager.createQuery(jpql, Member.class).getResultList();

실행된 SQL:

SELECT m.*, t.* 
FROM Member m
JOIN Team t ON m.team_id = t.id;
  • 결과: Member와 Team 엔티티가 함께 로드되며, 추가 쿼리가 발생하지 않음.

Fetch Join의 장점

  1. 성능 최적화:
    • 한 번의 쿼리로 연관된 엔티티를 함께 로드하므로 데이터베이스 접근 횟수를 줄임.
  2. N+1 문제 해결:
    • Fetch Join을 사용하면 지연 로딩으로 인한 추가 쿼리(N+1 문제)가 발생하지 않음.
  3. 일관된 데이터 상태:
    • 연관된 엔티티가 즉시 로드되어 프록시 객체 대신 실제 객체로 사용 가능.

주의사항

  1. 데이터 양 증가:
    • Fetch Join으로 많은 연관 데이터를 한 번에 로드하면, 불필요한 데이터까지 조회하여 메모리 사용량이 증가할 수 있음.
  2. Fetch Join 제한:
    • JPQL에서 하나의 Fetch Join만 허용되며, 복잡한 다중 Fetch Join은 지원하지 않음.
    • 예: @OneToMany와 @ManyToOne을 동시에 Fetch Join하면 문제가 발생할 수 있음.

결론

  • Fetch Join이 지연 로딩보다 우선:
    • JPQL에서 명시적으로 Fetch Join을 사용하면 지연 로딩 설정을 무시하고 즉시 로딩이 수행됩니다.
  • 영속성 컨텍스트 관리:
    • Fetch Join으로 조회된 모든 엔티티는 프록시 객체가 아닌 실제 객체로 초기화되며, 영속성 컨텍스트에서 관리됩니다.
  • 성능 최적화:
    • Fetch Join은 한 번의 쿼리로 연관된 데이터를 가져와 N+1 문제를 해결하고 성능을 최적화하는 강력한 도구입니다.

 

Collection fetch join

📌 @OneToMany 의 기본 FetchType은 LAZY 이다.

 

1:N fetch join(Collection)

String query = "select c from Company c join fetch c.tutorList";
List<Company> companyList = em.createQuery(query, Company.class).getResultList();

for (Company company : companyList) {
		System.out.println("company.getName() = " + company.getName());
		System.out.println("company.getTutorList().size() = " + company.getTutorList().size());
}

 

데이터 중복이 발생하지 않는다.

 

SQL Query의 조회 결과는 데이터가 중복된다.

Hibernate 6.0 이상 부터는 DISTINCT가 자동으로 적용된다.

 

 

JPQL의 DISTINCT

  • Database의 DISTINCT 는 완전히 데이터가 같아야 중복이 제거된다.
  • JPQL의 DISTINCT 는 같은 PK값을 가진 Entity를 제거한다.

 

주의점

  • Collection 에 fetch join을 사용하면 페이징을 메모리에서 수행한다.
  • 코드 예시
String query = "select c from Company c join fetch c.tutorList";
List<Company> companyList = em.createQuery(query, Company.class)
        .setFirstResult(0)
        .setMaxResults(1)
        .getResultList();

  • 전체를 조회하는 SQL이 실행된다.
    • setFirstResult(), setMaxResult() 가 SQL에 반영되지 않는다.
  • 모든 데이터를 조회하여 메모리에서 페이징을 처리한다.
    • 필요없는 데이터까지 전체 로드한 후 필터링한다.

 

@BatchSize

📌 JPA에서 N+1 문제를 해결하기 위해 사용되는 설정으로 지연 로딩(Lazy Loading) 시 한 번에 로드할 엔티티의 개수를 조정하여 여러 개의 엔티티를 효율적으로 조회할 수 있다.

String query = "select c from Company c";

List<Company> companyList = em.createQuery(query, Company.class)
        .setFirstResult(0)
        .setMaxResults(2)
        .getResultList();

System.out.println("companyList.size() = " + companyList.size());

for (Company company : companyList) {
    System.out.println("company.getName() = " + company.getName());
    
    for (Tutor tutor : company.getTutorList()) {
        System.out.println("tutor.getName(): " + tutor.getName());
    }
}

  1. Company 전체 조회
  2. 조회 결과 2개(sparta, etc)에 각각 지연 로딩

 

@BatchSize

@Entity
@Table(name = "company")
public class Company {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @BatchSize(size = 100)
    @OneToMany(mappedBy = "company")
    private List<Tutor> tutorList = new ArrayList<>();

    public Company() {
    }

    public Company(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public List<Tutor> getTutorList() {
        return tutorList;
    }

}

  • 한 번의 IN Query에 식별자(PK)를 조회된 개수만큼 넣어준다.
  • 설정 파일의 hibernate.jdbc.batch_size 를 통해 Global 적용이 가능하다.
    • xml, yml, properties 모두 가능

 

 


 

정리

  • JPQL의 한계
    1. 동적 쿼리를 사용하기 어렵다.
    2. SQL을 문자열로 작성하여 사용하기 까다롭다.
    3. SQL의 모든 기능을 사용할 수 없다(Native Query 사용).
  • fetch join 정리
    1. SQL의 JOIN과 비슷하지만 연관된 엔티티나 컬렉션을 한 번의 쿼리로 함께 로드하는 기능.
      • 지연 로딩 설정된 엔티티도 fetch join으로 사용할 수 있다.
    2. N + 1 문제를 해결할 수 있다.
    3. 데이터의 중복이 발생할 수 있다. (DISTINCT)
    4. 컬렉션 페이징 처리가 힘들다.
      • @BatchSize 적용
    5. 조회 시 여러 테이블이 JOIN 된다면 일반 JOIN을 사용하면 된다.
      • JPQL 또는 QueryDSL로 필요한 데이터만 조회하여 DTO로 반환한다.

 

'Back-End (Web) > Spring' 카테고리의 다른 글

API 예외처리  (0) 2025.01.19
Formatter  (0) 2025.01.11
TypeConverter  (0) 2025.01.10
HttpMessageConverter  (0) 2025.01.09
ArgumentResolver  (0) 2025.01.08

@ExceptionHandler

📌 컨트롤러 클래스 내에서 특정 예외를 처리하는 메서드를 정의하는 Annotation으로 예외가 발생했을 때 설정된 메서드가 호출되어 예외를 처리한다.

  • 특정 컨트롤러에서 발생한 예외를 처리하기 위한 메서드를 지정한다.
  • 단일 컨트롤러 내에서 예외를 처리한다.
  • 계층별로 알맞은 예외를 발생(throw new)시키기만 하면된다.
더보기
@RestController
@RequiredArgsConstructor
public class ExceptionHandlerController {

    private final ExceptionHandlerService exceptionHandlerService;

    @RequestMapping("/v1/exception")
    public void illegalArgumentException() {

        throw new IllegalArgumentException("IllegalArgumentException 발생");

    }

    @RequestMapping("/v2/exception")
    public void nullPointerException() {

        throw new NullPointerException("NPE 발생");
    }

    @RequestMapping("/v3/exception")
    public void serviceLayerException() {

        exceptionHandlerService.throwNewIllegalArgumentException();
    }

    // IllegalArgumentException을 처리하는 메서드
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<Map<String, String>> handleIllegalArgumentException(IllegalArgumentException ex) {

        Map<String, String> response = new HashMap<>();
        response.put("error", ex.getMessage());

        return ResponseEntity
                .status(HttpStatus.BAD_REQUEST)
                .body(response);
    }

}
@RestController
public class NonExceptionHandlerController {

    @RequestMapping("/v4/exception")
    public void illegalArgumentException() {

        throw new IllegalArgumentException("IllegalArgumentException 발생");

    }

}
@Service
public class ExceptionHandlerService {

    public void throwNewIllegalArgumentException() {
        throw new IllegalArgumentException("ServiceLayer Exception");
    }

}

 

  • 문제점
    1. Controller 코드에 Exception 처리를 위한 책임이 추가된다.(단일 책임 원칙 위반)
    2. 단일 컨트롤러 내의 예외만 처리가 가능하다.(컨트롤러 예외처리 중복코드)
    3. 코드 재사용, 유지보수성 부족
CustomException 사용자 정의 Exception을 만들어서 처리할 수 있다.

 

 

@ControllerAdvice

📌 @ControllerAdvice와 동일한 기능을 제공하지만 @RestController를 포함하고 있어 반환 값이 자동으로 JSON 형태로 변환되며 REST API에서 발생하는 예외를 처리할 때 사용한다.

  • Application 전역에서 발생한 예외를 처리한다.

 

  • @RestControllerAdvice
    • REST API의 예외를 처리한다.
    • 반환 값이 JSON이다.
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<Map<String, String>> handleIllegalArgumentException(IllegalArgumentException ex) {

        Map<String, String> response = new HashMap<>();
        response.put("error", ex.getMessage());

        return ResponseEntity
                .status(HttpStatus.BAD_REQUEST)
                .body(response);
    }

}

  • @ExceptionHandler 이 선언되지 않은 컨트롤러의 예외처리 가능
일반적으로 문자열이 아닌 공통된 응답 형태를 만들어서 반환한다.
 
Annotation 정리
Annotation
용도
반환 형식
예시 처리 범위
@ExceptionHandler
특정 컨트롤러 내에서 발생한 예외 처리
뷰 이름, String 등
단일 컨트롤러 내
@ControllerAdvice
애플리케이션 전체에서 발생한 예외 처리
뷰 이름, String 등
모든 컨트롤러에 전역 적용
@RestControllerAdvice
REST API에서 발생한 예외를 JSON 형식으로 처리
ResponseEntity (JSON)
모든 컨트롤러에 전역 적용 (RESTful 서비스)

 

'Back-End (Web) > Spring' 카테고리의 다른 글

fetch join  (1) 2025.01.21
Formatter  (0) 2025.01.11
TypeConverter  (0) 2025.01.10
HttpMessageConverter  (0) 2025.01.09
ArgumentResolver  (0) 2025.01.08

Formatter

📌 주로 사용자 지정 포맷을 적용해 데이터 변환을 처리할 때 사용된다. FormatterConversionService와 비슷한 목적을 가지지만 문자열을 객체로 변환하거나 객체를 문자열로 변환하는 과정에서 포맷팅을 세밀하게 제어할 수 있다.

  • 객체를 특정한 포맷에 맞춰서 문자로 출력하는 기능에 특화된것이 Fomatter이다.
  • Converter보다 조금 더 세부적인 기능이라고 생각하면 된다.
더보기

Spring에서 FormatterConverter는 모두 데이터 변환과 관련이 있지만, 주요 사용 목적기능에서 차이가 있습니다. 이 둘은 서로 보완적으로 사용되며, 특정 상황에서 더 적합한 도구를 선택할 수 있습니다.


Formatter와 Converter의 차이

특징 Formatter Converter
주요 목적 데이터 변환과 포맷팅(formatting) 지원. 단순히 타입 변환에 집중.
양방향 변환 String ↔ Object 형태로 양방향 변환 지원. 한 방향으로만(Source → Target) 변환 가능.
대상 데이터 주로 사용자 입력 데이터를 처리하거나 포맷팅이 필요한 데이터. 데이터 타입 간 변환(예: String → Integer, String → Enum).
인터페이스 구조 Formatter<T> (포맷 변환 및 역변환 메서드 포함). Converter<S, T> (단일 변환 메서드만 포함).
사용 위치 사용자 입력 데이터(폼 데이터, 요청 파라미터 등) 처리. 내부적으로 데이터 바인딩 및 단순 변환 처리.
Spring 사용 방식 FormattingConversionService를 통해 등록 및 관리. ConversionService를 통해 등록 및 관리.

Formatter

Formatter는 주로 데이터를 특정 형식(포맷)으로 변환하거나, 문자열 데이터를 객체로 역변환하는 데 사용됩니다.

인터페이스 정의

public interface Formatter<T> {
    String print(T object, Locale locale);   // 객체 → 문자열 (포맷팅)
    T parse(String text, Locale locale) throws ParseException; // 문자열 → 객체 (역변환)
}

Formatter 사용 예: String ↔ LocalDate

public class LocalDateFormatter implements Formatter<LocalDate> {

    @Override
    public LocalDate parse(String text, Locale locale) throws ParseException {
        return LocalDate.parse(text, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
    }

    @Override
    public String print(LocalDate object, Locale locale) {
        return object.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
    }
}

Formatter 등록

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addFormatter(new LocalDateFormatter());
    }
}

사용 예

컨트롤러 메서드에서 LocalDate를 자동 변환:

@GetMapping("/date")
public String getDate(@RequestParam LocalDate date) {
    return "Formatted Date: " + date.toString();
}

요청:

GET /date?date=2023-12-25

Converter

Converter는 단순히 하나의 데이터 타입을 다른 데이터 타입으로 변환합니다.

인터페이스 정의

public interface Converter<S, T> {
    T convert(S source); // 소스 타입 → 대상 타입
}

Converter 사용 예: String → Integer

public class StringToIntegerConverter implements Converter<String, Integer> {

    @Override
    public Integer convert(String source) {
        return Integer.valueOf(source);
    }
}

Converter 등록

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToIntegerConverter());
    }
}

사용 예

컨트롤러 메서드에서 Integer를 자동 변환:

@GetMapping("/number")
public String getNumber(@RequestParam Integer number) {
    return "Number: " + number;
}

요청:

GET /number?number=123

Formatter와 Converter의 사용 사례 비교

기능 Formatter Converter
날짜 변환 2023-12-25 ↔ LocalDate 지원하지 않음 (포맷팅 기능 없음).
Enum 변환 ACTIVE ↔ Status.ACTIVE String → Status.ACTIVE.
사용자 입력 처리 사용자 입력 데이터를 읽어 포맷팅. 단순 데이터 변환 작업.
예제 요청 파라미터 GET /date?date=2023-12-25 GET /number?number=123.

Converter와 Formatter의 동작 구조

  1. Converter:
    • 입력 데이터가 단순히 변환되기만 하면 되는 경우 사용.
    • : 쿼리 파라미터 123 → Integer.
  2. Formatter:
    • 사용자 입력 데이터가 특정 형식에 따라 변환되고 역변환이 필요한 경우 사용.
    • : yyyy-MM-dd → LocalDate.

결론

  • Converter: 단순히 데이터 타입을 변환할 때 사용.
    • 비유: 변환기. 한 단위를 다른 단위로 바꾸는 도구.
  • Formatter: 데이터 변환과 동시에 포맷팅이 필요한 경우 사용.
    • 비유: 포맷터. 데이터를 사람이 읽기 좋은 형식으로 변환하는 도구.

선택 기준:

  • 단순 타입 변환: Converter 사용.
  • 포맷팅과 역변환 모두 필요: Formatter 사용.
  • Locale
    • 지역 및 언어 정보를 나타내는 객체.
      • 언어코드 en, ko
      • 국가코드 US, KR
    • 특정 지역 및 언어에 대한 정보를 제공하여 국제화 및 지역화 기능을 지원한다.
    • 국제화
      • Locale 정보에 따라서 한글을 보여줄지 영문을 보여줄지 선택할 수 있다.

 

Formatter Interface

  • Printer, Parser 상속
  • 객체를 문자로 변환하고 문자를 객체로 변환하는 두가지 기능을 모두 가지고 있다.
  • Printer

Object를 String으로 변환한다.

 

Parser

String을 Object로 변환한다.

 

 

Formatter 적용

@Slf4j
public class PriceFormatter implements Formatter<Number> {
	
	@Override
  public Number parse(String text, Locale locale) throws ParseException {
    log.info("text = {}, locale={}", text, locale);
		
		// 변환 로직
		// NumberFormat이 제공하는 기능
		NumberFormat format = NumberFormat.getInstance(locale);
		// "10,000" -> 10000L
		return format.parse(text);
  }

  @Override
  public String print(Number object, Locale locale) {
			log.info("object = {}, locale = {}", object, locale);
			// 10000L -> "10,000"
      return NumberFormat.getInstance(locale).format(object);
  }
	
}

 

Number

  • Integer, Long, Double 등의 부모 클래스
class PriceFormatterTest {

		PriceFormatter formatter = new PriceFormatter();

    @Test
    void parse() throws ParseException {
        // given, when
        Number result = formatter.parse("1,000", Locale.KOREA);

        // then
        // parse 결과는 Long
        Assertions.assertThat(result).isEqualTo(1000L);
    }

    @Test
    void print() {
        // given, when
        String result = formatter.print(1000, Locale.KOREA);

        // then
        Assertions.assertThat(result).isEqualTo("1,000");
    }
}

 

 

 

Spring Formatter

📌 Spring의 Formatter는 문자열 데이터를 특정 객체로 변환하거나, 객체를 특정 문자열 형식으로 변환(포맷팅)하는 데 사용되는 인터페이스입니다. 데이터 포맷팅과 역변환을 쉽게 처리할 수 있도록 Spring에서 제공됩니다

 

 

FormattingConversionService

📌 ConversionServiceFormatter를 결합한 구현체로 타입 변환과 포맷팅이 필요한 모든 작업을 한 곳에서 수행할 수 있도록 설계되어 있어서 다양한 타입의 변환과 포맷팅을 쉽게 적용할 수 있다.

  • 어댑터 패턴을 사용하여 Formatter가 Converter처럼 동작하도록 만들어준다.

 

DefaultFormattingConversionService

📌 FormattingConversionService + 통화, 숫자관련 Formatter를 추가한것

public class FormattingConversionServiceTest {

    @Test
    void formattingConversionService() {

        // given
        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();

        // Converter 등록
        conversionService.addConverter(new StringToPersonConverter());
        conversionService.addConverter(new PersonToStringConverter());
        // Formatter 등록
        conversionService.addFormatter(new PriceFormatter());

        // when
        String result = conversionService.convert(10000, String.class);

        // then
        Assertions.assertThat(result).isEqualTo("10,000");

    }

}
  • ConversionService가 제공하는 convert()를 사용하면 된다.

 

SpringBoot의 기능

📌 SpringBoot는 기본적으로 WebConversionService를 사용한다.

  • DefaultFormattingConversionService 상속

 

 

Spring이 제공하는 Formatter

📌 Spring은 어노테이션 기반으로 원하는 형식Formatter를 사용할 수 있도록 기능을 제공한다.

  • Annotation
    • DTO 필드들에 적용 가능
    1. @NumberFormat
      • 숫자 관련 지정 Formatter 사용
      • NumberFormatAnnotationFormatterFactory
    2. @DateTimeFormat
      • 날짜 관련 지정 Formatter 사용
      • Jsr310DateTimeFormatAnnotationFormatterFactory
 

Spring Field Formatting :: Spring Framework

As discussed in the previous section, core.convert is a general-purpose type conversion system. It provides a unified ConversionService API as well as a strongly typed Converter SPI for implementing conversion logic from one type to another. A Spring conta

docs.spring.io

 

 

 

'Back-End (Web) > Spring' 카테고리의 다른 글

fetch join  (1) 2025.01.21
API 예외처리  (0) 2025.01.19
TypeConverter  (0) 2025.01.10
HttpMessageConverter  (0) 2025.01.09
ArgumentResolver  (0) 2025.01.08

TypeConverter

📌 Spring에서 객체의 타입을 서로 변환하는 데 사용되는 인터페이스로 Spring의 데이터 바인딩 과정에서 문자열을 특정 객체로 변환하거나 하나의 객체 타입을 다른 타입으로 변환할 때 사용한다.

  • 문자를 숫자로, 숫자를 문자로 변환하는 등 Web Application을 만들다보면 Type을 변환해야 하는 경우가 많이 발생한다.

 

  • 결론
    1. 요청 파라미터로 전달하는 10 값은 실제로는 문자열(String) 10이다.
    2. @RequestParam을 사용하면 문자 10을 Integer 타입의 숫자 10으로 변환된다.
    3. @ModelAttribute, @PathVariable 에서도 타입 변환을 확인할 수 있다.
    • Spring 내부에서 누군가가 타입을 자동으로 변환한다.

 

Converter Interface

  • Spring이 제공하는 인터페이스
    • implements하여 Converter로 등록하면 된다.
  • Converter는 모든 타입(T)에 적용할 수 있다.
  • 개발자가 새로운 Type을 만들어서 사용할 수 있도록 만든다.
    • 변환하고자 하는 타입에 맞춰서 Type Converter를 구현하고 등록하면 된다.

 

Converter

📌 데이터 타입 간 변환을 처리하는 인터페이스 , 주로 웹 요청 파라미터를 Java 객체로 변환하거나 그 반대로 변환할 때 사용되며 커스텀 변환 로직을 정의할 수 있다.

 

  • 주의점
    • org.springframework.core.convert.converter
    • Spring의 Converter와 같은 이름을 가진 Interface가 많으니 주의해야 한다.

 

 

  • 코드 예시
    • String → Integer
    • Converter<S, T> 에서 S는 변환할 Source T는 변환할 Type으로 설정하면 된다.
@Slf4j
public class StringToIntegerConverter implements Converter<String, Integer> {
	
	@Override
	public Integer convert(String source) {
		log.info("source = {}", source);
		// 검증
		return Integer.valueOf(source);
	}
	
}
  • 파라미터로 들어온 source가 Interger로 변환된다.
  • Integer → String
@Slf4j
public class IntegerToStringConverter implements Converter<Integer, String> {
	
	@Override
	public String convert(Integer source) {
		log.info("source = {}", source);
		return String.valueOf(source);
	}
}
  • 파라미터로 들어온 source가 String으로 변환된다.
  • String → Person
@Getter
public class Person {
	
		// 이름
		private String name;
		// 나이
		private int age;
	
		public Person(String name, int age) {
			this.name = name;
			this.age = age;
		}
	
}
  • 요청 예시
    • localhost:8080/type-converter?person=wonuk:1200
public class StringToPersonConverter implements Converter<String, Person> {
		// source = "wonuk:1200"
		@Override
		public Person convert(String source) {
			// ':' 를 구분자로 나누어 배열로 만든다.
			String[] parts = source.split(":");
	
			// 첫번째 배열은 이름이다. -> wonuk
	    String name = parts[0];
	    // 두번째 배열은 개월수이다. -> 1200
	    int months = Integer.parseInt(parts[1]);
	    
			// 개월수 나누기 12로 나이를 구하는 로직 (12개월 단위만 고려)
			int age = months / 12;
	
			return new Person(name, age);
		}
}
public class PersonToStringConverter implements Converter<Person, String> {
		
		@Override
		public String convert(Person source) {
				// 이름
				String name = source.getName();
				// 개월수
				int months = source.getAge * 12;
				// "wonuk:1200"
				return name + ":" + months;
		}
	
}

 

  • TypeConverter 사용
    • 구현은 단순하게 직접 메서드를 구현하여 모듈화 하면된다.
    • TypeConverter 를 생성하여 직접 사용하면 컨트롤러에서 변환하는 방식과 큰 차이가 없다.
PersonToStringConverter converter = new PersonToStringConverter();
String source = "wonuk:1200";
converter.convert(source);

 

  • Converter를 편리하게 등록하고 사용할 수 있도록 만들어주는 기능이 필요하다.
  • Spring은 String, Integer, Enum등 자주 사용되는 타입에 대한 컨버터를 제공하고 사용할 수 있도록 등록되어 있다.

 

 

Spring의 다양한 Converter

📌 Spring에서 제공하는 다양한 Converter 인터페이스가 존재하며 이들은 Spring의 데이터 바인딩, 요청/응답 처리, 속성 값 주입 등에 사용되고 ConversionService를 통해 등록 및 관리된다.

 

  1. Converter
    • 기본적인 변환을 담당하는 인터페이스
    • 단일 타입에서 단일 타입으로 변환할 때 사용한다.
      • Converter<Source, Type>
  2. ConverterFactory
    • 클래스 계층 구조가 복잡한 경우 사용
    • 기본 타입과 다양한 서브 타입 간의 변환을 지원한다.
  3. GenericConverter
    • 다양한 타입 간의 유연한 변환을 지원한다.
    • 복잡한 타입 변환 로직을 구현할 때 유리하다.
  4. ConditionalGenericConverter
    • GenericConverter 의 확장형으로 특정 조건에서만 타입 변환을 수행한다.
    • 추가적으로 matches() 를 통해 변환 가능 여부를 판단할 수 있다.

 

ConversionService

📌 Spring은 Converter를 모아서 편리하게 관리하고 사용할 수 있게 해주는 기능을 제공한다. 이것이 Conversion Service 이다.

 

ConversionService 인터페이스

 

  1. canConvert()
    • Convert 가능 여부를 확인하는 기능
  2. convert()
    • 실제 변환하는 기능

 

 

DefaultConversionService

📌 Spring의 표준 ConversionService로 기본 제공 Converter와 확장 가능성을 통해 다양한 타입 변환을 유연하게 처리할 수 있도록 지원한다.

 

  • DefaultConversionService
    • ConversionService를 구현한 구현체

 

ConvertRegistry에 다양한 Converter를 등록한다.

 

 

 

  • ConverterRegistry
    • Converter를 등록하고 관리하는 기능을 제공한다.

import static org.assertj.core.api.Assertions.*;

public class ConversionServiceTest {

    @Test
    void defaultConversionService() {
        // given
        DefaultConversionService dcs = new DefaultConversionService();
        dcs.addConverter(new StringToPersonConverter());
        Person wonuk = new Person("wonuk", 100);
        
				// when
        Person stringToPerson = dcs.convert("wonuk:1200", Person.class);
        
				// then
        assertThat(stringToPerson.getName()).isEqualTo(wonuk.getName());
        assertThat(stringToPerson.getAge()).isEqualTo(wonuk.getAge());
    }

}
  • 컨버터를 사용할 때는 종류를 몰라도된다.
  • 컨버터는 ConversionService 내부에서 숨겨진채 제공된다.
    • 반환 타입, 파라미터 타입, 제네릭 등으로 ConversionService가 컨버터를 찾는다.
  • 즉, 클라이언트는 ConversionService 인터페이스만 의존하면 된다.
    • 컨버터 등록과 사용의 분리

 

ISP(인터페이스 분리 원칙, Interface Segregation Principal)

📌 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않도록 인터페이스를 분리하는 원칙

  • DefaultConversionService
    1. ConversionRegistry : 컨버터 등록
    2. ConversionService ****: ****컨버터 사용
  • 인터페이스를 분리하면 컨버터를 사용하는 클라이언트는 필요한 메서드만 알면된다.
  • ConversionRegistry 가 변경되어도 ConversionService와 연관이 없다.
  • Spring은 내부적으로 위와같이 등록, 사용이 분리된 인터페이스들이 아주 많다.
Spring은 내부적으로 ConversionService를 사용해 타입을 변환한다. 대표적으로 @RequestParam , @PathVariable, @ModelAttribute 등이 해당 기능을 사용한다.

 

Converter 요약

Spring의 **Converter**는 데이터 타입 간 변환을 처리하는 인터페이스입니다. 주로 Spring의 데이터 바인딩 또는 사용자 정의 변환 작업에 사용됩니다. 간단한 입력 값 변환에서부터 복잡한 객체 변환까지 유연하게 지원합니다.


Converter의 주요 특징

  1. 데이터 타입 변환:
    • 소스 타입(Source Type)에서 대상 타입(Target Type)으로 변환.
    • 예: String → Integer, String → LocalDate.
  2. 간결한 인터페이스:
    • 구현이 간단하며 특정 변환 작업에 집중.
  3. 범용 사용 가능:
    • Spring의 데이터 바인딩, 요청 파라미터 변환, 커스텀 변환 로직 등에 사용.
  4. 확장 가능:
    • Spring이 기본으로 제공하는 변환기 외에도 사용자 정의 변환기를 구현할 수 있음.

Converter 인터페이스

public interface Converter<S, T> {
    T convert(S source);
}
  • S: 소스 데이터 타입 (변환 전 데이터 타입).
  • T: 대상 데이터 타입 (변환 후 데이터 타입).

Converter의 사용 사례

1. 문자열을 날짜로 변환 (String → LocalDate)

public class StringToLocalDateConverter implements Converter<String, LocalDate> {

    @Override
    public LocalDate convert(String source) {
        // 문자열을 LocalDate로 변환
        return LocalDate.parse(source, DateTimeFormatter.ISO_DATE);
    }
}

Spring에서 Converter 등록

Spring에서는 변환기를 전역적으로 등록하거나, 특정 컨텍스트에서 사용할 수 있습니다.

1. 전역 등록

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        // 커스텀 Converter 등록
        registry.addConverter(new StringToLocalDateConverter());
    }
}

Spring의 기본 Converter

Spring은 이미 다양한 기본 변환기를 제공합니다:

  • Primitive 타입 변환: String → int, String → double 등.
  • Java 날짜/시간 변환: String → LocalDate, String → LocalDateTime.
  • Enum 변환: String → Enum.

Converter와 일상적인 비유

비유: 데이터를 변환하는 "도구 상자"

  • Converter는 한 가지 타입의 데이터를 다른 타입으로 변환하는 간단한 도구입니다.
  • 예를 들어, 숫자 데이터를 텍스트로 변환하는 계산기처럼, Converter는 입력 값을 원하는 형태로 만들어줍니다.

Converter 사용 예제

컨트롤러에서 사용

@RestController
@RequestMapping("/api")
public class ExampleController {

    @GetMapping("/date")
    public String getDate(@RequestParam String date, Converter<String, LocalDate> converter) {
        LocalDate localDate = converter.convert(date);
        return "Converted date: " + localDate.toString();
    }
}

요청 예

GET /api/date?date=2023-12-25

결과

Converted date: 2023-12-25

Converter와 관련된 확장

  1. Formatter:
    • Converter의 확장형으로, 데이터를 특정 형식(포맷)으로 변환하고 역변환도 지원.
    • 예: 날짜를 특정 형식으로 변환하거나 파싱.
  2. GenericConverter:
    • 보다 범용적인 변환을 지원하는 고급 형태의 Converter.
  3. ConversionService:
    • 여러 변환기를 한데 모아 관리하고, 필요한 변환기를 자동으로 찾아주는 Spring의 변환 관리 서비스.

Converter와 HttpMessageConverter의 차이

  • Converter:
    • 데이터 타입 간 변환.
    • String → Integer, String → LocalDate 등 간단한 데이터 변환에 초점.
  • HttpMessageConverter:
    • HTTP 요청/응답 데이터를 변환.
    • JSON → Java 객체, Java 객체 → JSON 등, 네트워크 통신에 초점.

결론

Spring의 Converter는 데이터 타입을 변환하는 단순하고 강력한 도구입니다. 데이터를 변환하는 간단한 작업부터, 프로젝트 전반에서 데이터 바인딩에 활용됩니다.

비유: 필요한 데이터를 원하는 형태로 "가공"하는 변환기입니다.

 

 

'Back-End (Web) > Spring' 카테고리의 다른 글

API 예외처리  (0) 2025.01.19
Formatter  (0) 2025.01.11
HttpMessageConverter  (0) 2025.01.09
ArgumentResolver  (0) 2025.01.08
[Spring] 스프링 정리  (0) 2025.01.07

HttpMessageConverter

📌 클라이언트와 서버 간의 HTTP 요청과 응답을 처리할 때 데이터 형식 변환을 담당 한다. 클라이언트가 보낸 데이터를 서버가 이해할 수 있는 형태로 변환하거나, 서버가 응답으로 보내는 데이터를 클라이언트가 이해할 수 있는 형태로 변환할 때 사용됩니다. [ View를 응답하는 것이 아닌, Rest API(HTTP API)로 JSON, TEXT, XML 등의 데이터를 응답 Message Body에 직접 입력하는 경우 HttpMessageConverter를 사용한다. ]

  • 1. SSR → @Controller + View Template → 서버 측에서 화면을 동적으로 그린다.
  • 2. CSR → @RestController + Data → 클라이언트 측에서 화면을 동적으로 그린다.
  • 3. 실제로는 두가지 기술이 함께 사용되는 경우가 많다.

 

  • HTTP 응답 메세지 Body에 데이터를 직접 입력 후 반환한다.
  • 요청 Accept Header + Controller 반환 타입

ViewResolver가 아닌 HttpMessageConverter가 동작한다.

 

  • HttpMessageConverter가 적용되는 경우
    1. HTTP 요청 : @RequestBody, HttpEntity<>, RequestEntity<>
    2. HTTP 응답 : @ResponseBody, HttpEntity<>, ResponseEntity<>
    • HttpMessageConverter는 요청과 응답 모두 사용된다.
@RestController = @Controller + @ResponseBody

 

 

우선순위

📌 Spring은 다양한 HttpMessageConverter를 제공하고 있고 우선순위가 있다. 대상 Class와 MediaType을 체크해서 어떤 Converter를 사용할지 결정한다.

 

 

동작 순서와 예시

 

요청 데이터 읽기

더보기

위 코드는 Spring Framework에서 REST API를 구현한 예제입니다. RESTful 엔드포인트를 제공하며, 클라이언트로부터 JSON 데이터를 받고, 처리 후 JSON 데이터를 응답으로 반환합니다. 아래에서 각 부분을 상세히 분석하겠습니다.


1. 클래스 수준 어노테이션

@RestController
  • @RestController:
    • **@Controller**와 **@ResponseBody**를 합친 기능을 합니다.
    • 이 클래스의 모든 메서드는 기본적으로 JSON 또는 XML 형식으로 응답을 생성합니다. (HTML View가 아닌 데이터만 반환)
    • 따라서 각 메서드에 **@ResponseBody**를 명시할 필요가 없습니다.

2. 메서드 수준 어노테이션

@PostMapping(value = "/example", produces = "application/json")
  • @PostMapping:
    • HTTP POST 요청을 처리하는 메서드를 정의합니다.
    • value = "/example": 이 API는 /example URL에 매핑됩니다.
    • produces = "application/json": 이 API는 클라이언트에게 JSON 형식으로 응답을 반환합니다.

3. 메서드 파라미터

public ResponseDto example(@RequestBody RequestDto dto)
  • @RequestBody:
    • 클라이언트가 요청 본문에 포함한 데이터를 읽어와, Java 객체(RequestDto)로 변환합니다.
    • Spring은 **HttpMessageConverter**를 사용해 JSON 데이터를 RequestDto로 변환합니다.
    • 이 메서드는 클라이언트가 보낸 JSON 데이터를 받아, 내부에서 사용할 수 있는 DTO 객체로 변환하여 사용합니다.
    • 예: 클라이언트가 다음과 같은 JSON 데이터를 보냈다고 가정:
      {
        "id": 123,
        "name": "Example"
      }
      
      -> Spring이 이 JSON 데이터를 RequestDto 객체로 변환합니다.

4. 메서드 로직

ResponseDto responseDto = service.example(dto);
  • service.example(dto):
    • 서비스 계층으로 RequestDto 데이터를 전달하여, 요청을 처리합니다.
    • 서비스 계층은 보통 비즈니스 로직을 처리하는 곳입니다. (데이터베이스 호출, 데이터 변환 등)
    • 처리 결과를 ResponseDto 객체로 반환받습니다.
return responseDto;
  • 반환값:
    • ResponseDto 객체를 반환합니다.
    • Spring은 **HttpMessageConverter**를 사용해 ResponseDto 객체를 JSON으로 변환하여 클라이언트에게 응답합니다.

5. DTO(Data Transfer Object)

  • RequestDto:
    • 클라이언트에서 전송한 데이터를 담는 객체입니다.
    • 예시:
      public class RequestDto {
          private int id;
          private String name;
      
          // Getters and Setters
      }
      
  • ResponseDto:
    • 처리 결과를 클라이언트로 반환할 때 사용하는 객체입니다.
    • 예시:
      public class ResponseDto {
          private String status;
          private String message;
      
          // Getters and Setters
      }
      

6. 전체 실행 흐름

  1. 클라이언트가 /example로 POST 요청을 보냅니다. 요청 본문에 JSON 데이터를 포함합니다.
    {
        "id": 123,
        "name": "Example"
    }
    
  2. Spring은 JSON 데이터를 RequestDto 객체로 변환합니다.
  3. example 메서드가 호출되어 RequestDto 데이터를 서비스 계층으로 전달합니다.
  4. 서비스 계층이 요청 데이터를 처리하고, 결과를 ResponseDto로 반환합니다.
  5. ResponseDto 객체는 다시 JSON으로 변환되어 클라이언트에게 응답됩니다.
    {
        "status": "success",
        "message": "Operation completed"
    }
    

7. 주요 특징

  • RESTful API: 데이터만을 전송하고 반환하는 RESTful 스타일의 API입니다.
  • JSON 데이터 처리: HttpMessageConverter를 통해 JSON 데이터를 주고받습니다.
  • 계층화: 서비스 계층을 사용해 비즈니스 로직과 컨트롤러 로직을 분리합니다.
  • 재사용성: DTO 객체를 통해 데이터를 깔끔하게 전달하고 관리합니다.
더보기

boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType)는 Spring Framework의 HttpMessageConverter 인터페이스에서 정의된 메서드 중 하나로, 특정 타입의 객체를 지정된 미디어 타입(MediaType)으로 변환할 수 있는지 확인합니다. 이 메서드는 HTTP 응답을 생성할 때 사용됩니다.


메서드 정의

boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);

파라미터

  1. Class<?> clazz:
    • HTTP 응답으로 변환하려는 Java 객체의 클래스 타입입니다.
    • 예: ResponseDto.class, String.class 등.
    • 이 매개변수를 통해 현재 HttpMessageConverter가 처리 가능한 객체인지 확인합니다.
  2. @Nullable MediaType mediaType:
    • 변환 대상의 **미디어 타입(MediaType)**입니다.
      예: application/json, application/xml, text/plain 등.
    • @Nullable이므로 null일 수도 있습니다.
      • null인 경우 미디어 타입과 관계없이 클래스 타입만 확인합니다.

반환값

  • true: 이 HttpMessageConverter가 지정된 클래스와 미디어 타입에 대해 쓰기 작업(HTTP 응답 데이터 변환)을 처리할 수 있습니다.
  • false: 처리할 수 없으면 false를 반환합니다.

메서드 동작

이 메서드는 Spring이 적절한 **HttpMessageConverter**를 선택할 때 사용됩니다. 요청 데이터의 Java 객체 타입과 응답 데이터의 미디어 타입을 비교하여 적합한 컨버터를 결정합니다.

  1. Spring은 등록된 여러 HttpMessageConverter를 순차적으로 탐색합니다.
  2. 각 컨버터의 canWrite 메서드를 호출하여, 특정 객체와 미디어 타입을 처리할 수 있는지 확인합니다.
  3. 적합한 컨버터를 찾으면 해당 컨버터가 쓰기 작업을 수행합니다.

사용 예시

JSON 변환 컨버터 예

MappingJackson2HttpMessageConverter는 JSON 데이터를 처리하는 HttpMessageConverter 구현체입니다. 이를 기준으로 canWrite 메서드를 구현하면 다음과 같이 동작합니다:

@Override
public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
    // JSON 컨버터는 기본적으로 모든 객체 타입을 지원
    if (mediaType == null || mediaType.isCompatibleWith(MediaType.APPLICATION_JSON)) {
        return true; // JSON 형식으로 변환 가능
    }
    return false; // JSON 외 미디어 타입은 변환 불가
}

동작 방식

  1. clazz: 응답으로 반환할 객체의 클래스가 전달됩니다. 예: ResponseDto.class.
  2. mediaType: 응답 헤더의 Content-Type에 설정된 미디어 타입이 전달됩니다.
    • 예: application/json, application/xml, text/plain.

만약 다음 요청이 왔다고 가정:

GET /api/example HTTP/1.1
Accept: application/json

Spring은 다음을 수행합니다:

  • MappingJackson2HttpMessageConverter.canWrite(ResponseDto.class, application/json)를 호출합니다.
  • canWrite가 true를 반환하면, JSON으로 응답을 변환합니다.

유스 케이스

  1. 클라이언트 요청 처리:
    • 클라이언트가 Accept 헤더에 특정 미디어 타입을 요청한 경우, 해당 미디어 타입으로 응답을 생성할 수 있는지 확인합니다.
  2. 다양한 컨버터 사용:
    • Spring은 여러 컨버터를 지원하므로, 각 컨버터가 자신이 지원하는 클래스와 미디어 타입만 처리하도록 canWrite를 구현합니다.
      • 예: MappingJackson2HttpMessageConverter는 JSON 처리.
      • StringHttpMessageConverter는 단순 텍스트 처리.

흐름 예시

다음은 Spring이 canWrite를 호출하는 시나리오입니다:

  1. 컨트롤러에서 객체를 반환:
  2. @GetMapping("/example") public ResponseDto example() { return new ResponseDto("Success", "Operation completed"); }
  3. Spring이 등록된 HttpMessageConverter들을 순회하며 canWrite를 호출:
    • MappingJackson2HttpMessageConverter.canWrite(ResponseDto.class, application/json)
    • 반환값: true
  4. MappingJackson2HttpMessageConverter를 사용해 ResponseDto를 JSON으로 변환:
  5. { "status": "Success", "message": "Operation completed" }

정리

  • **canWrite**는 특정 객체(clazz)를 특정 미디어 타입(mediaType)으로 변환 가능한지 확인합니다.
  • Spring은 이 메서드를 사용해 적합한 HttpMessageConverter를 결정합니다.
  • REST API 응답에서 주로 활용되며, JSON, XML, TEXT 등의 데이터 형식을 처리할 수 있는지 판단하는 핵심 메서드입니다.
더보기

void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)는 Spring의 HttpMessageConverter 인터페이스에서 정의된 메서드로, 데이터를 HTTP 응답 메시지에 쓰는 역할을 합니다. 이 메서드는 REST API에서 서버가 클라이언트로 데이터를 반환할 때 사용됩니다.


메서드 정의

void write(
    T t,
    @Nullable MediaType contentType,
    HttpOutputMessage outputMessage
) throws IOException, HttpMessageNotWritableException;

파라미터 설명

  1. T t:
    • HTTP 응답 본문에 쓰려는 데이터 객체입니다.
    • 예를 들어, 컨트롤러에서 반환하는 DTO 객체가 여기에 전달됩니다.
    • 데이터의 타입은 HttpMessageConverter의 제네릭 타입 T로 제한됩니다.
  2. @Nullable MediaType contentType:
    • 응답의 Content-Type을 나타냅니다.
    • 예: application/json, application/xml, text/plain.
    • @Nullable이므로 null일 수 있습니다.
      • null인 경우, 기본값이나 자동 결정된 MediaType이 사용됩니다.
  3. HttpOutputMessage outputMessage:
    • HTTP 응답 메시지를 나타내는 객체입니다.
    • 실제로 응답 데이터를 쓰는 데 필요한 출력 스트림(OutputStream)과 헤더(HttpHeaders)를 포함합니다.

예외

  1. IOException:
    • 데이터 쓰기 중 IO 문제가 발생할 경우 던집니다.
    • 예: 네트워크 연결 문제.
  2. HttpMessageNotWritableException:
    • 데이터 객체를 변환하거나 쓰는 데 실패한 경우 던집니다.
    • 예: 객체를 JSON으로 변환하지 못하거나 변환 로직에 오류가 있는 경우.

메서드의 역할

이 메서드는 데이터(t)를 지정된 형식(contentType)으로 변환하고, 이를 HTTP 응답 메시지(outputMessage)의 본문에 쓰는 작업을 수행합니다.


실제 동작 흐름

  1. 컨트롤러에서 데이터 반환:
    • 예: 컨트롤러 메서드가 DTO 객체를 반환.
    @GetMapping("/example")
    public ResponseDto example() {
        return new ResponseDto("success", "Operation completed");
    }
    
  2. Spring이 적절한 HttpMessageConverter 선택:
    • canWrite 메서드를 사용해 변환 가능한 컨버터를 선택합니다.
    • 예: MappingJackson2HttpMessageConverter가 JSON 변환을 담당.
  3. write 메서드 호출:
    • Spring이 선택한 컨버터의 write 메서드를 호출해, 반환 객체(ResponseDto)를 JSON으로 변환하고 HTTP 응답 본문에 씁니다.

구현 예: JSON 변환기

MappingJackson2HttpMessageConverter의 write 메서드가 어떻게 동작할지 간단히 구현 예로 살펴보겠습니다.

@Override
public void write(
    Object t,
    @Nullable MediaType contentType,
    HttpOutputMessage outputMessage
) throws IOException {
    // 1. JSON 변환기 초기화 (예: ObjectMapper 사용)
    ObjectMapper objectMapper = new ObjectMapper();

    // 2. Content-Type 설정
    if (contentType != null) {
        outputMessage.getHeaders().setContentType(contentType);
    } else {
        outputMessage.getHeaders().setContentType(MediaType.APPLICATION_JSON);
    }

    // 3. 객체를 JSON으로 변환하여 OutputStream에 쓰기
    OutputStream bodyStream = outputMessage.getBody();
    objectMapper.writeValue(bodyStream, t);

    // 4. OutputStream 닫기 (Spring이 자동으로 관리)
}

사용 예제

클라이언트 요청

GET /example HTTP/1.1
Accept: application/json

컨트롤러 반환

@GetMapping("/example")
public ResponseDto example() {
    return new ResponseDto("success", "Operation completed");
}

write 메서드 동작

  1. 파라미터 전달:
    • t: ResponseDto 객체 ({"status":"success", "message":"Operation completed"}).
    • contentType: application/json.
    • outputMessage: 클라이언트로 보낼 HTTP 응답 메시지.
  2. 동작:
    • ObjectMapper가 ResponseDto 객체를 JSON 문자열로 변환.
    • 변환된 JSON 문자열을 outputMessage.getBody()의 출력 스트림에 씀.
    • outputMessage.getHeaders()를 사용해 Content-Type 헤더를 설정.

결과 응답

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 58

{"status":"success","message":"Operation completed"}

주요 특징

  1. 데이터 변환:
    • write 메서드는 Java 객체를 HTTP 응답 형식(JSON, XML 등)으로 변환합니다.
    • 변환은 HttpMessageConverter 구현체에 따라 다릅니다.
  2. 스트림 기반 쓰기:
    • 변환된 데이터를 직접 HttpOutputMessage의 출력 스트림에 씁니다.
    • 이를 통해 대규모 데이터 처리도 효율적으로 수행할 수 있습니다.
  3. 유연성:
    • 미디어 타입이 null인 경우 기본값을 사용할 수 있습니다.
    • 개발자가 원하는 방식으로 데이터 변환을 커스터마이징할 수 있습니다.

정리

  • **write**는 데이터를 HTTP 응답 본문에 쓰는 메서드로, REST API에서 클라이언트에 데이터를 반환할 때 사용됩니다.
  • Spring의 HttpMessageConverter 구현체마다 이 메서드가 다르게 동작하여 다양한 데이터 형식(JSON, XML, TEXT 등)을 처리합니다.
  • 사용자는 커스터마이징된 HttpMessageConverter를 구현해 특정 데이터 포맷을 지원할 수도 있습니다.

 

응답 데이터 쓰기

 

HttpMessageConverter 구조

📌 HttpMessageConverter를 주로 사용하는 어노테이션 @RequestBody, @ResponseBody

  • 요청시에는 Argument Resolver가 사용하는것이다.
  • 응답시에는 ReturnValueHandler가 사용한다.

 

대표적인 ArgumentResolver, ReturnValueHandler

1. ArgumentResolver

**ArgumentResolver**는 컨트롤러 메서드 파라미터에 요청 데이터를 바인딩하는 역할을 합니다.

대표적인 ArgumentResolver

Resolver 이름 설명

RequestParamMethodArgumentResolver @RequestParam 어노테이션 처리. 요청의 쿼리 파라미터 또는 폼 데이터를 메서드 파라미터로 바인딩.
PathVariableMethodArgumentResolver @PathVariable 어노테이션 처리. URL 경로 변수 값을 메서드 파라미터로 바인딩.
RequestHeaderMethodArgumentResolver @RequestHeader 어노테이션 처리. HTTP 요청 헤더 값을 메서드 파라미터로 바인딩.
CookieValueMethodArgumentResolver @CookieValue 어노테이션 처리. HTTP 쿠키 값을 메서드 파라미터로 바인딩.
RequestBodyArgumentResolver @RequestBody 어노테이션 처리. 요청 본문(JSON/XML)을 Java 객체로 변환.
ModelAttributeMethodProcessor @ModelAttribute 어노테이션 처리. 요청 데이터를 객체에 바인딩하고, 모델에 추가.
SessionAttributeMethodArgumentResolver @SessionAttribute 어노테이션 처리. 세션에서 특정 속성을 가져와 메서드 파라미터에 주입.
RequestAttributeMethodArgumentResolver @RequestAttribute 어노테이션 처리. 요청 속성(request scope)을 메서드 파라미터에 바인딩.
HttpEntityMethodProcessor HttpEntity<T> 및 RequestEntity<T> 처리. 요청 전체(헤더와 본문 포함)를 객체로 바인딩.
PrincipalMethodArgumentResolver 현재 인증된 사용자의 정보를 나타내는 java.security.Principal 객체 처리.
DefaultMethodArgumentResolver HTTP 요청 및 응답 객체(HttpServletRequest, HttpServletResponse) 처리.

 


ArgumentResolver의 예제

@RequestParam과 @RequestBody 사용

@GetMapping("/greet")
public String greetUser(@RequestParam String name) {
    return "Hello, " + name;
}

@PostMapping("/process")
public String processData(@RequestBody MyDto data) {
    return "Processed: " + data.getValue();
}
  • RequestParamMethodArgumentResolver: name 값을 쿼리 파라미터에서 추출.
  • RequestBodyArgumentResolver: 요청 본문(JSON)을 MyDto 객체로 변환.

2. ReturnValueHandler

**ReturnValueHandler**는 컨트롤러 메서드가 반환한 데이터를 클라이언트에게 적절히 변환하여 응답으로 전달합니다.

대표적인 ReturnValueHandler

Handler 이름 설명

RequestResponseBodyMethodProcessor @ResponseBody와 HttpEntity를 처리. Java 객체를 JSON/XML 등으로 변환.
ModelAndViewMethodReturnValueHandler ModelAndView 객체를 처리하여 뷰를 렌더링.
ViewMethodReturnValueHandler View 객체를 처리하여 뷰를 렌더링.
HttpEntityMethodProcessor HttpEntity<T> 및 ResponseEntity<T>를 처리. 헤더와 본문을 설정하여 응답.
ModelMethodProcessor 모델 객체를 처리하여 View에서 사용할 데이터로 추가.
DeferredResultMethodReturnValueHandler 비동기 작업 결과를 처리(DeferredResult, CompletableFuture 등).
CallableMethodReturnValueHandler Callable 객체를 처리하여 비동기 응답 생성.
StringMethodReturnValueHandler 단순 문자열(String)을 처리하여 응답 본문에 쓰거나, 뷰 이름으로 해석.


ReturnValueHandler의 예제

@ResponseBody와 ResponseEntity 사용

@RestController
public class ExampleController {

    @GetMapping("/json")
    public MyDto getJson() {
        return new MyDto("data", 123); // RequestResponseBodyMethodProcessor 처리
    }

    @GetMapping("/response")
    public ResponseEntity<String> getResponse() {
        return ResponseEntity.ok("OK"); // HttpEntityMethodProcessor 처리
    }
}
  • RequestResponseBodyMethodProcessor:
    • MyDto 객체를 JSON 형식으로 변환하여 응답 본문에 작성.
  • HttpEntityMethodProcessor:
    • ResponseEntity의 상태 코드, 헤더, 본문을 설정하여 응답.

전체 흐름 정리

Spring MVC의 요청 처리 과정에서 **ArgumentResolver**와 **ReturnValueHandler**는 다음처럼 동작합니다:

  1. ArgumentResolver:
    • 클라이언트 요청 데이터를 컨트롤러 메서드 파라미터에 맞게 변환.
    • 예: 쿼리 파라미터 → @RequestParam, 본문(JSON) → @RequestBody.
  2. ReturnValueHandler:
    • 컨트롤러 메서드의 반환값을 클라이언트 응답으로 변환.
    • 예: 객체 → JSON, 문자열 → 뷰 이름 해석.

예제 통합

@RestController
public class ExampleController {

    @PostMapping("/submit")
    public ResponseEntity<String> submit(@RequestBody MyDto dto) {
        // ArgumentResolver: RequestBodyArgumentResolver 처리
        System.out.println("Received: " + dto.getValue());
        return ResponseEntity.ok("Processed successfully"); // ReturnValueHandler: HttpEntityMethodProcessor 처리
    }
}

요청:

POST /submit
Content-Type: application/json

{
    "value": "example"
}

응답:

HTTP/1.1 200 OK
Content-Type: text/plain

Processed successfully

이렇게 Spring MVC는 ArgumentResolver로 요청 데이터를 바인딩하고, ReturnValueHandler로 응답 데이터를 변환합니다!

 

요청과 응답

📌 ArgumentResolverHttpMessageConverter는 다르다.

 

 

 

WebMvcConfigurer

📌 Spring MVC의 설정을 사용자 정의할 수 있도록 제공되는 인터페이스로 implements하여 설정을 확장하거나 커스터마이징할 수 있다.

 

  • 주요 인터페이스
    1. HandlerMethodArgumentResolver
    2. HandlerMethodReturnValueHandler
    3. HttpMessageConverter
    • 모두 인터페이스로 구현되어 있으며 대부분 구현되어 있다.
      • Spring에서 기본적으로 제공하고 있다.
    • 개발자는 잘 사용하면 된다.

 

  • 기능의 확장
    • WebMvcConfigurer를 상속받고 Spring Bean으로 등록
public interface WebMvcConfigurer {
    default void configurePathMatch(PathMatchConfigurer configurer) {
    }

    default void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
    }

    default void configureAsyncSupport(AsyncSupportConfigurer configurer) {
    }

    default void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
    }

    default void addFormatters(FormatterRegistry registry) {
    }

    default void addInterceptors(InterceptorRegistry registry) {
    }

    default void addResourceHandlers(ResourceHandlerRegistry registry) {
    }

    default void addCorsMappings(CorsRegistry registry) {
    }

    default void addViewControllers(ViewControllerRegistry registry) {
    }

    default void configureViewResolvers(ViewResolverRegistry registry) {
    }

    default void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
    }

    default void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
    }

    default void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    }

    default void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    }

    default void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
    }

    default void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
    }

    @Nullable
    default Validator getValidator() {
        return null;
    }

    @Nullable
    default MessageCodesResolver getMessageCodesResolver() {
        return null;
    }
}
  1. addArgumentResolvers()
  2. addReturnValueHandlers()
  3. extendMessageConverters()
  • 필요한 메서드를 오버라이딩 하면된다.
  • @Configuration
    • @Component 를 포함하고 있다. (Spring Bean 등록이 된다.)

 

 

HttpMessageConverter 요약

HttpMessageConverter는 Spring MVC에서 클라이언트와 서버 간 데이터를 변환하는 데 사용되는 컴포넌트입니다. HTTP 요청과 응답의 본문을 Java 객체와 JSON, XML, TEXT 등으로 상호 변환합니다.


HttpMessageConverter의 역할

  1. 요청 처리:
    • 클라이언트가 JSON, XML 등 형식으로 요청 본문을 보내면 이를 Java 객체로 변환.
  2. 응답 생성:
    • 컨트롤러가 반환한 Java 객체를 JSON, XML, TEXT 등으로 변환하여 클라이언트로 전송.

일상적인 비유

  • 비유: 번역가(Translator)
    • 한 사람이 영어로 말을 하고, 다른 사람이 한국어로 이해하려면 번역가가 필요합니다.
    • 여기서 영어는 JSON, XML 등의 데이터 형식이고, 한국어는 Java 객체입니다.
    • 번역가(HttpMessageConverter)가 데이터를 양쪽에서 서로 이해할 수 있도록 변환합니다.

HttpMessageConverter의 동작 흐름

  1. 클라이언트 요청:
    • 클라이언트가 JSON 요청을 보냅니다.
    • 예: {"name": "John", "age": 30}
  2. 요청 데이터 변환:
    • HttpMessageConverter가 JSON 데이터를 Java 객체로 변환.
  3. 컨트롤러 처리:
    • 컨트롤러 메서드에서 비즈니스 로직 처리.
  4. 응답 데이터 변환:
    • 컨트롤러 메서드가 반환한 Java 객체를 JSON 형식으로 변환하여 클라이언트에 응답.

대표적인 HttpMessageConverter 구현체

Converter 이름 역할

MappingJackson2HttpMessageConverter JSON 데이터를 Java 객체로 변환 또는 반대로 변환.
MappingJackson2XmlHttpMessageConverter XML 데이터를 Java 객체로 변환 또는 반대로 변환.
StringHttpMessageConverter 문자열 데이터를 처리.
FormHttpMessageConverter application/x-www-form-urlencoded 폼 데이터를 처리.
ByteArrayHttpMessageConverter 바이너리 데이터를 처리 (예: 파일 다운로드).

HttpMessageConverter 사용 예제

요청 본문(JSON) → Java 객체 (@RequestBody)

@RestController
@RequestMapping("/api/users")
public class UserController {

    @PostMapping
    public String createUser(@RequestBody UserDto userDto) {
        return "User created: " + userDto.getName();
    }
}
  1. 클라이언트 요청:
  2. POST /api/users Content-Type: application/json { "name": "John", "age": 30 }
  3. HttpMessageConverter 동작:
    • MappingJackson2HttpMessageConverter가 JSON 데이터를 UserDto 객체로 변환.
    • 컨트롤러는 변환된 UserDto 객체를 사용.
  4. 컨트롤러 결과:
    • User created: John

Java 객체 → JSON 응답 (@ResponseBody)

@RestController
public class ExampleController {

    @GetMapping("/user/{id}")
    public UserDto getUser(@PathVariable Long id) {
        return new UserDto(id, "John", 30); // Java 객체 반환
    }
}
  1. 클라이언트 요청:
  2. GET /user/1 Accept: application/json
  3. HttpMessageConverter 동작:
    • 컨트롤러에서 반환한 UserDto 객체를 MappingJackson2HttpMessageConverter가 JSON으로 변환.
  4. 클라이언트 응답:
  5. HTTP/1.1 200 OK Content-Type: application/json { "id": 1, "name": "John", "age": 30 }

HttpMessageConverter의 장점

  1. 자동 변환:
    • JSON, XML, TEXT 등 다양한 데이터 형식을 Java 객체로 쉽게 변환 가능.
    • 개발자가 수동으로 데이터 변환 코드를 작성할 필요 없음.
  2. 확장 가능:
    • 필요에 따라 커스텀 HttpMessageConverter를 작성해 특별한 데이터 형식을 처리 가능.
  3. REST API 친화적:
    • RESTful 서비스를 개발할 때 요청과 응답 데이터를 간단하게 처리.

커스텀 HttpMessageConverter

Spring MVC에서 특정 데이터 포맷을 처리하기 위해 커스텀 HttpMessageConverter를 구현할 수 있습니다.

예제: CSV 데이터를 처리하는 HttpMessageConverter

  1. 커스텀 HttpMessageConverter 구현:
  2. public class CsvHttpMessageConverter extends AbstractHttpMessageConverter<MyCsvDto> { public CsvHttpMessageConverter() { super(new MediaType("text", "csv")); } @Override protected boolean supports(Class<?> clazz) { return MyCsvDto.class.isAssignableFrom(clazz); } @Override protected MyCsvDto readInternal(Class<? extends MyCsvDto> clazz, HttpInputMessage inputMessage) throws IOException { // CSV 데이터를 Java 객체로 변환하는 로직 return new MyCsvDto("example", 123); } @Override protected void writeInternal(MyCsvDto myCsvDto, HttpOutputMessage outputMessage) throws IOException { // Java 객체를 CSV 데이터로 변환하는 로직 String csv = myCsvDto.getName() + "," + myCsvDto.getValue(); outputMessage.getBody().write(csv.getBytes()); } }
  3. Spring에 등록:
  4. @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(new CsvHttpMessageConverter()); } }

결론

HttpMessageConverter는 클라이언트와 서버 간 데이터 변환을 자동화하여 개발자의 작업을 간소화합니다. 데이터를 JSON, XML 등 다양한 포맷으로 처리할 수 있으며, 필요에 따라 커스터마이징하여 확장할 수도 있습니다.

비유: 데이터를 주고받는 번역가로, 클라이언트와 서버가 서로 다른 언어(JSON, XML, Java 객체 등)를 이해할 수 있게 해주는 역할을 합니다.

'Back-End (Web) > Spring' 카테고리의 다른 글

Formatter  (0) 2025.01.11
TypeConverter  (0) 2025.01.10
ArgumentResolver  (0) 2025.01.08
[Spring] 스프링 정리  (0) 2025.01.07
[Spring] 의존관계 주입  (0) 2024.12.26

RequestMappingHandlerAdapter

📌 Spring MVC에서 HTTP 요청을 컨트롤러 메서드에 매핑하고 실행하는 핵심 구성 요소로, 클라이언트 요청을 적절한 컨트롤러 메서드와 연결한 후 이 메서드를 호출하여 결과를 반환하는 역할을 수행한다.

 

Spring MVC 구조

  • 요청 데이터가 변환이된다.(HttpMessageConverter)
  • 요청이 Controller에 전달되는 HandlerAdapter와 Handler 사이에서 어떤 일이 일어난다!

 

RequestMappingHandlerAdapter

 

@RequestMapping 을 처리하는 HandlerAdapter의 구현체

@PostMapping , @GetMapping, @PutMapping, @PatchMapping, @DeleteMapping 등은 모두 @RequestMapping의 일종이다.

 

ArgumentResolver

  • RequestMappingHandlerAdapter는 ArgumentResolver를 호출하여 Controller가 필요한 다양한 파라미터의 값을 생성한다.
  • HttpServletRequest, Model, HttpEntity,@ModelAttribute, @RequestBody, @RequestParam 등 다양한 파라미터 바인딩을 할 수 있는 이유이다.
  • ArgumentResolver를 통하여 값이 준비되면 해당값을 가지고 실제 Controller를 호출한다.
  • 어노테이션도 매핑 및 바인딩이 필요했다...

 

 

 

ArgumentResolver

📌 Spring MVC에서 컨트롤러 메서드의 파라미터를 자동으로 바인딩하는 역할을 하는 인터페이스로 요청이 컨트롤러 메서드에 전달될 때 각 파라미터를 적절한 객체로 변환하여 주입하는 것을 담당한다.

 

ArgumentResolver 종류

  • Spring은 다양한 Argument Resolver들을 기본적으로 제공한다.
    1. RequestBodyArgumentResolver(@RequestBody)

            2. RequestHeaderArgumentResolver(@RequestHeader)

 

HandlerMethodArgumentResolver

  • ArgumentResolver의 실제 이름
    • 인터페이스로 구성되어 있다.
    • implements 하여 커스텀하게 파라미터를 만들 수 있다.(확장)
  • supportsParameter(MethodParameter parameter);
    • 컨트롤러가 필요로하는 메서드의 파라미터를 지원하는지 여부를 검사한다.
  • 지원한다면 resolveArgument() 메서드를 통해 Object(객체)로 만들어준다.
    • 만들어진 Object(객체)가 Controller 호출시 메서드의 파라미터로 전달된다.
  • supportsParameter() 를 사용하는 다양한 ArgumentResolver 구현체

 

 

 

 

ReturnValueHandler

📌 Spring MVC에서 컨트롤러 메서드가 반환하는 값을 처리하여 HTTP 응답에 맞게 변환하는 역할을 하는 인터페이스로 컨트롤러 메서드가 실행된 후 그 반환값을 HTTP 응답의 본문에 적절히 담아 전송할 수 있도록 도와준다.

 

 

Spring MVC의 주요 컴포넌트 요약 

1. ArgumentResolver

  • 역할: 컨트롤러 메서드의 입력 파라미터를 동적으로 생성하여 주입.
  • 일상 예시:
    • "음식점에서 메뉴를 보고 '김치찌개'를 주문하면, 요리사가 요청에 맞는 재료를 준비해서 요리를 시작한다."
    • 여기서 김치찌개라는 요청을 기반으로 필요한 재료(데이터)를 동적으로 준비하는 역할이 ArgumentResolver입니다.

예제 코드:

@GetMapping("/user")
public String getUser(@RequestParam String name) {
    return "Hello " + name;
}

동작:

  • 클라이언트 요청: /user?name=John
  • **ArgumentResolver**가 name 값을 쿼리 파라미터에서 추출해 메서드 파라미터에 전달.

2. RequestMappingHandlerAdapter

  • 역할:
    • 클라이언트의 HTTP 요청을 적절한 컨트롤러 메서드에 매핑.
    • 요청 파라미터를 ArgumentResolver로 처리하고, 응답 값을 ReturnValueHandler로 처리.
  • 일상 예시:
    • "레스토랑에서 웨이터가 손님의 요청을 듣고 적절한 요리사에게 요청을 전달한다."
    • 웨이터(RequestMappingHandlerAdapter)가 요청을 받아, 요리사(Controller)에게 적절히 전달하고 결과를 받아 클라이언트에 응답.

예제 코드:

@PostMapping("/order")
public String placeOrder(@RequestBody OrderDto order) {
    return "Order received for: " + order.getItemName();
}

동작:

  1. **RequestMappingHandlerAdapter**가 요청 /order를 탐지하고, 해당 메서드를 호출.
  2. ArgumentResolver를 사용해 요청 본문(JSON)을 OrderDto로 변환.
  3. 메서드 실행 후 반환된 값을 ReturnValueHandler로 처리.

3. ReturnValueHandler

  • 역할: 컨트롤러 메서드가 반환한 값을 적절한 형식으로 변환하여 클라이언트로 전송.
  • 일상 예시:
    • "요리사가 음식을 만들어서 웨이터가 손님에게 적절한 포장(접시 또는 테이크아웃 박스)으로 전달한다."
    • 반환된 결과 데이터를 JSON, HTML, XML 등 클라이언트가 원하는 형식으로 변환하는 역할이 ReturnValueHandler입니다.

예제 코드:

@GetMapping("/status")
public ResponseEntity<String> getStatus() {
    return ResponseEntity.ok("Service is running");
}

동작:

  1. 컨트롤러 메서드가 ResponseEntity를 반환.
  2. **ReturnValueHandler**가 반환된 값을 HTTP 응답 본문에 쓰고, 상태 코드와 헤더를 설정.

요약된 동작 흐름 (일상 비유로 연결)

  1. 클라이언트 요청: 손님이 "김치찌개 주문" 요청.
    • 클라이언트가 /order?item=kimchiStew로 HTTP 요청.
  2. RequestMappingHandlerAdapter: 웨이터가 요청을 접수.
    • URL, HTTP 메서드 등에 따라 적절한 컨트롤러 메서드 매핑.
  3. ArgumentResolver: 요리사가 요청에 필요한 재료 준비.
    • 쿼리 파라미터, 요청 본문 등을 읽어 메서드 파라미터에 맞는 데이터 생성.
  4. Controller 처리: 요리사가 음식을 만듦.
    • 컨트롤러 로직이 비즈니스 처리를 수행.
  5. ReturnValueHandler: 웨이터가 음식을 손님에게 전달.
    • 컨트롤러 결과를 JSON, HTML 등 클라이언트가 이해할 수 있는 형식으로 변환.

통합 예제

@RestController
public class ExampleController {

    @GetMapping("/user")
    public String getUser(@RequestParam String name) { // ArgumentResolver 처리
        return "Hello " + name; // ReturnValueHandler 처리
    }

    @PostMapping("/order")
    public ResponseEntity<OrderResponse> placeOrder(@RequestBody OrderDto order) {
        // ArgumentResolver 처리
        OrderResponse response = new OrderResponse(order.getItemName(), "Processing");
        return ResponseEntity.ok(response); // ReturnValueHandler 처리
    }
}

요청/응답 흐름:

  1. 클라이언트 요청: GET /user?name=John
  2. ArgumentResolver:
    • RequestParamMethodArgumentResolver가 쿼리 파라미터 name=John을 추출해 메서드 파라미터에 전달.
  3. 컨트롤러 실행: return "Hello John";
  4. ReturnValueHandler:
    • 반환값을 HTTP 응답 본문(Hello John)에 작성.

비유와 코딩을 연결한 간단 정리

  • ArgumentResolver: 요청, 데이터(재료)를 준비.
  • RequestMappingHandlerAdapter: 요청을 전달하고 전체 과정을 조율(웨이터).
  • ReturnValueHandler: 응답, 반환값(요리 결과)을 클라이언트에게 적절히 포장해 전달.

이 과정을 통해 Spring MVC는 클라이언트의 요청을 유연하고 효과적으로 처리합니다!

'Back-End (Web) > Spring' 카테고리의 다른 글

TypeConverter  (0) 2025.01.10
HttpMessageConverter  (0) 2025.01.09
[Spring] 스프링 정리  (0) 2025.01.07
[Spring] 의존관계 주입  (0) 2024.12.26
[Spring] Bean 등록  (0) 2024.12.25

+ Recent posts