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 "가방의 최대 무게가 잘못 설정되었습니다."
}
}
}
})
보통 하나의 파일은 한개의 클래스를 의미하지만, 하나의 파일안에 여러개의 클래스가 존재할 수도 있어요
인스턴스
클래스형태로 설계된 객체를 실체화하면 인스턴스가 생겨요
인스턴스는 메모리 공간을 차지해요
정보와 행위를 작성한 클래스를 실체화해서 프로그램에 로딩해요 (메모리에 적재)
정보가 행위가 그대로 로딩되는것이 아니라 위치정보를 메모리에 로딩해요
프로그램은 객체의 위치정보를 변수에 저장해두고, 필요할 때 참조해요
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
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. 상속의 장점
코드 재사용성: 공통된 로직을 부모 클래스에 정의하여 중복 코드를 줄임.
유지보수 용이: 부모 클래스 수정만으로 자식 클래스의 동작도 일관성 유지.
다형성 구현: 동일한 부모 클래스를 공유하는 객체를 일관된 방식으로 처리 가능.
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
}
// **주 생성자 정의**
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. 오버로딩이란?
**오버로딩(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
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. 데이터 클래스 (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
}
// **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)) // 대소문자 무시하고 비교
특히 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")
// **가변인자**
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) } // 아무 작업도 수행하지 않음
📌 예외(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")
지연 로딩은 연관된 엔티티 데이터를 처음에는 로드하지 않고, 실제로 필요할 때 데이터베이스에서 가져오는 전략입니다.
장점:
초기 로딩 시 불필요한 데이터를 가져오지 않아 성능 최적화.
연관된 엔티티가 필요하지 않을 경우 데이터베이스 접근을 줄일 수 있음.
작동 방식:
연관된 엔티티는 프록시(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 Join은 JPQL에서 연관된 엔티티를 한 번에 가져오도록 명시적으로 지정하는 방법입니다.
장점:
여러 테이블을 조인하여 필요한 데이터를 한 번에 가져옴.
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의 장점
성능 최적화:
한 번의 쿼리로 연관된 엔티티를 함께 로드하므로 데이터베이스 접근 횟수를 줄임.
N+1 문제 해결:
Fetch Join을 사용하면 지연 로딩으로 인한 추가 쿼리(N+1 문제)가 발생하지 않음.
일관된 데이터 상태:
연관된 엔티티가 즉시 로드되어 프록시 객체 대신 실제 객체로 사용 가능.
주의사항
데이터 양 증가:
Fetch Join으로 많은 연관 데이터를 한 번에 로드하면, 불필요한 데이터까지 조회하여 메모리 사용량이 증가할 수 있음.
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());
}
}
Company 전체 조회
조회 결과 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의 한계
동적 쿼리를 사용하기 어렵다.
SQL을 문자열로 작성하여 사용하기 까다롭다.
SQL의 모든 기능을 사용할 수 없다(Native Query 사용).
fetch join 정리
SQL의 JOIN과 비슷하지만 연관된 엔티티나 컬렉션을 한 번의 쿼리로 함께 로드하는 기능.
@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");
}
}
문제점
Controller 코드에 Exception 처리를 위한 책임이 추가된다.(단일 책임 원칙 위반)
단일 컨트롤러 내의 예외만 처리가 가능하다.(컨트롤러 예외처리 중복코드)
코드 재사용, 유지보수성 부족
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);
}
}
@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
📌 ConversionService와 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를 사용할 수 있도록 기능을 제공한다.
📌 Spring에서 객체의 타입을 서로 변환하는 데 사용되는 인터페이스로 Spring의 데이터 바인딩 과정에서 문자열을 특정 객체로 변환하거나 하나의 객체 타입을 다른 타입으로 변환할 때 사용한다.
문자를 숫자로, 숫자를 문자로 변환하는 등 Web Application을 만들다보면 Type을 변환해야 하는 경우가 많이 발생한다.
결론
요청 파라미터로 전달하는 10 값은 실제로는 문자열(String) 10이다.
@RequestParam을 사용하면 문자 10을 Integer 타입의 숫자 10으로 변환된다.
@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를 통해 등록 및 관리된다.
Converter
기본적인 변환을 담당하는 인터페이스
단일 타입에서 단일 타입으로 변환할 때 사용한다.
Converter<Source, Type>
ConverterFactory
클래스 계층 구조가 복잡한 경우 사용
기본 타입과 다양한 서브 타입 간의 변환을 지원한다.
GenericConverter
다양한 타입 간의 유연한 변환을 지원한다.
복잡한 타입 변환 로직을 구현할 때 유리하다.
ConditionalGenericConverter
GenericConverter 의 확장형으로 특정 조건에서만 타입 변환을 수행한다.
추가적으로 matches() 를 통해 변환 가능 여부를 판단할 수 있다.
ConversionService
📌 Spring은 Converter를 모아서 편리하게 관리하고 사용할 수 있게 해주는 기능을 제공한다. 이것이 Conversion Service 이다.
ConversionService 인터페이스
canConvert()
Convert 가능 여부를 확인하는 기능
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
ConversionRegistry : 컨버터 등록
ConversionService ****: ****컨버터 사용
인터페이스를 분리하면 컨버터를 사용하는 클라이언트는 필요한 메서드만 알면된다.
ConversionRegistry 가 변경되어도 ConversionService와 연관이 없다.
Spring은 내부적으로 위와같이 등록, 사용이 분리된 인터페이스들이 아주 많다.
Spring은 내부적으로 ConversionService를 사용해 타입을 변환한다. 대표적으로 @RequestParam , @PathVariable, @ModelAttribute 등이 해당 기능을 사용한다.
Converter 요약
Spring의 **Converter**는 데이터 타입 간 변환을 처리하는 인터페이스입니다. 주로 Spring의 데이터 바인딩 또는 사용자 정의 변환 작업에 사용됩니다. 간단한 입력 값 변환에서부터 복잡한 객체 변환까지 유연하게 지원합니다.
Converter의 주요 특징
데이터 타입 변환:
소스 타입(Source Type)에서 대상 타입(Target Type)으로 변환.
예: String → Integer, String → LocalDate.
간결한 인터페이스:
구현이 간단하며 특정 변환 작업에 집중.
범용 사용 가능:
Spring의 데이터 바인딩, 요청 파라미터 변환, 커스텀 변환 로직 등에 사용.
확장 가능:
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());
}
}
📌 클라이언트와 서버 간의 HTTP 요청과 응답을 처리할 때 데이터 형식 변환을 담당 한다. 클라이언트가 보낸 데이터를 서버가 이해할 수 있는 형태로 변환하거나, 서버가 응답으로 보내는 데이터를 클라이언트가 이해할 수 있는 형태로 변환할 때 사용됩니다.[ View를 응답하는 것이 아닌, Rest API(HTTP API)로 JSON, TEXT, XML 등의 데이터를 응답 Message Body에 직접 입력하는 경우 HttpMessageConverter를 사용한다. ]
1. SSR → @Controller + View Template → 서버 측에서 화면을 동적으로 그린다.
2. CSR → @RestController + Data → 클라이언트 측에서 화면을 동적으로 그린다.
boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType)는 Spring Framework의 HttpMessageConverter 인터페이스에서 정의된 메서드 중 하나로, 특정 타입의 객체를 지정된 미디어 타입(MediaType)으로 변환할 수 있는지 확인합니다. 이 메서드는 HTTP 응답을 생성할 때 사용됩니다.
이 매개변수를 통해 현재 HttpMessageConverter가 처리 가능한 객체인지 확인합니다.
@Nullable MediaType mediaType:
변환 대상의 **미디어 타입(MediaType)**입니다. 예: application/json, application/xml, text/plain 등.
@Nullable이므로 null일 수도 있습니다.
null인 경우 미디어 타입과 관계없이 클래스 타입만 확인합니다.
반환값
true: 이 HttpMessageConverter가 지정된 클래스와 미디어 타입에 대해 쓰기 작업(HTTP 응답 데이터 변환)을 처리할 수 있습니다.
false: 처리할 수 없으면 false를 반환합니다.
메서드 동작
이 메서드는 Spring이 적절한 **HttpMessageConverter**를 선택할 때 사용됩니다. 요청 데이터의 Java 객체 타입과 응답 데이터의 미디어 타입을 비교하여 적합한 컨버터를 결정합니다.
Spring은 등록된 여러 HttpMessageConverter를 순차적으로 탐색합니다.
각 컨버터의 canWrite 메서드를 호출하여, 특정 객체와 미디어 타입을 처리할 수 있는지 확인합니다.
적합한 컨버터를 찾으면 해당 컨버터가 쓰기 작업을 수행합니다.
사용 예시
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 외 미디어 타입은 변환 불가
}
void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)는 Spring의 HttpMessageConverter 인터페이스에서 정의된 메서드로, 데이터를 HTTP 응답 메시지에 쓰는 역할을 합니다. 이 메서드는 REST API에서 서버가 클라이언트로 데이터를 반환할 때 사용됩니다.
**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**는 다음처럼 동작합니다:
HttpMessageConverter는 Spring MVC에서 클라이언트와 서버 간 데이터를 변환하는 데 사용되는 컴포넌트입니다. HTTP 요청과 응답의 본문을 Java 객체와 JSON, XML, TEXT 등으로 상호 변환합니다.
HttpMessageConverter의 역할
요청 처리:
클라이언트가 JSON, XML 등 형식으로 요청 본문을 보내면 이를 Java 객체로 변환.
응답 생성:
컨트롤러가 반환한 Java 객체를 JSON, XML, TEXT 등으로 변환하여 클라이언트로 전송.
일상적인 비유
비유: 번역가(Translator)
한 사람이 영어로 말을 하고, 다른 사람이 한국어로 이해하려면 번역가가 필요합니다.
여기서 영어는 JSON, XML 등의 데이터 형식이고, 한국어는 Java 객체입니다.
번역가(HttpMessageConverter)가 데이터를 양쪽에서 서로 이해할 수 있도록 변환합니다.
HttpMessageConverter의 동작 흐름
클라이언트 요청:
클라이언트가 JSON 요청을 보냅니다.
예: {"name": "John", "age": 30}
요청 데이터 변환:
HttpMessageConverter가 JSON 데이터를 Java 객체로 변환.
컨트롤러 처리:
컨트롤러 메서드에서 비즈니스 로직 처리.
응답 데이터 변환:
컨트롤러 메서드가 반환한 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();
}
}
클라이언트 요청:
POST /api/users Content-Type: application/json { "name": "John", "age": 30 }
HttpMessageConverter 동작:
MappingJackson2HttpMessageConverter가 JSON 데이터를 UserDto 객체로 변환.
컨트롤러는 변환된 UserDto 객체를 사용.
컨트롤러 결과:
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 객체 반환
}
}