├── .github
├── CODEOWNERS
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── README.md
├── week_1
├── char-yb
│ ├── images
│ │ └── Thread-life-cycle.jpeg
│ ├── week1
│ │ ├── .gitattributes
│ │ ├── .gitignore
│ │ ├── build.gradle.kts
│ │ ├── gradle
│ │ │ └── wrapper
│ │ │ │ ├── gradle-wrapper.jar
│ │ │ │ └── gradle-wrapper.properties
│ │ ├── gradlew
│ │ ├── gradlew.bat
│ │ ├── settings.gradle.kts
│ │ └── src
│ │ │ ├── main
│ │ │ ├── kotlin
│ │ │ │ └── com
│ │ │ │ │ └── sipe
│ │ │ │ │ └── week1
│ │ │ │ │ └── Week1Application.kt
│ │ │ └── resources
│ │ │ │ └── application.properties
│ │ │ └── test
│ │ │ └── kotlin
│ │ │ └── com
│ │ │ └── sipe
│ │ │ └── week1
│ │ │ ├── CallableTest.kt
│ │ │ ├── ExecutorServiceTest.kt
│ │ │ ├── ExecutorTest.kt
│ │ │ ├── FutureTest.kt
│ │ │ └── Week1ApplicationTests.kt
│ ├── week_1_1.md
│ └── week_1_2.md
├── jaeyeong
│ ├── res
│ │ ├── image 1.png
│ │ ├── image 2.png
│ │ ├── image 3.png
│ │ ├── image 4.png
│ │ ├── image 5.png
│ │ ├── image 6.png
│ │ └── image.png
│ └── week 1.md
├── junseo
│ └── 1주차
│ │ ├── 1주차.md
│ │ └── res
│ │ ├── image.png
│ │ ├── image1.png
│ │ ├── image2.png
│ │ └── image3.png
├── positivehun
│ └── 1주차.pdf
└── 김동건.md
├── week_2
├── char-yb
│ ├── images
│ │ ├── week2_2_program.png
│ │ └── week2_3_nio_connector.png
│ ├── week2
│ │ ├── .gitattributes
│ │ ├── .gitignore
│ │ ├── build.gradle.kts
│ │ ├── gradle
│ │ │ └── wrapper
│ │ │ │ ├── gradle-wrapper.jar
│ │ │ │ └── gradle-wrapper.properties
│ │ ├── gradlew
│ │ ├── gradlew.bat
│ │ ├── settings.gradle.kts
│ │ └── src
│ │ │ ├── main
│ │ │ ├── kotlin
│ │ │ │ └── com
│ │ │ │ │ └── sipe
│ │ │ │ │ └── week2
│ │ │ │ │ ├── AtomicExample.kt
│ │ │ │ │ ├── Problem1.kt
│ │ │ │ │ ├── SynchronizedExample.kt
│ │ │ │ │ ├── VolatileExample.kt
│ │ │ │ │ └── Week2Application.kt
│ │ │ └── resources
│ │ │ │ └── application.properties
│ │ │ └── test
│ │ │ └── kotlin
│ │ │ └── com
│ │ │ └── sipe
│ │ │ └── week2
│ │ │ └── Week2ApplicationTests.kt
│ ├── week_2_1.md
│ ├── week_2_2.md
│ └── week_2_3.md
├── jaeyeong
│ ├── res
│ │ ├── image 1.png
│ │ ├── image 2.png
│ │ └── image.png
│ └── week_2.md
├── junseo
│ ├── 2주차.md
│ └── res
│ │ ├── image.png
│ │ ├── image1.png
│ │ ├── image10.png
│ │ ├── image11.png
│ │ ├── image2.png
│ │ ├── image3.png
│ │ ├── image4.png
│ │ ├── image5.png
│ │ ├── image6.png
│ │ ├── image7.png
│ │ ├── image8.png
│ │ └── image9.png
├── positive
│ └── 2주차.pdf
└── 김동건.md
├── week_3
├── char-yb
│ ├── images
│ │ ├── async_thread.png
│ │ ├── coroutine2-1.png
│ │ ├── coroutine2-2.png
│ │ ├── coroutine_example.png
│ │ └── hierarchical_routing.png
│ ├── week3
│ │ ├── .gitattributes
│ │ ├── .gitignore
│ │ ├── build.gradle.kts
│ │ ├── gradle
│ │ │ └── wrapper
│ │ │ │ ├── gradle-wrapper.jar
│ │ │ │ └── gradle-wrapper.properties
│ │ ├── gradlew
│ │ ├── gradlew.bat
│ │ ├── settings.gradle.kts
│ │ └── src
│ │ │ ├── main
│ │ │ ├── kotlin
│ │ │ │ └── com
│ │ │ │ │ └── sipe
│ │ │ │ │ └── week3
│ │ │ │ │ ├── CoRoutineExample1.kt
│ │ │ │ │ ├── CoRoutineExample2.kt
│ │ │ │ │ ├── ThreadExample.kt
│ │ │ │ │ ├── TicketingExample.kt
│ │ │ │ │ ├── Week3Application.kt
│ │ │ │ │ ├── application
│ │ │ │ │ ├── AsyncService.kt
│ │ │ │ │ └── ThreadService.kt
│ │ │ │ │ └── presentation
│ │ │ │ │ └── ExampleController.kt
│ │ │ └── resources
│ │ │ │ └── application.properties
│ │ │ └── test
│ │ │ └── kotlin
│ │ │ └── com
│ │ │ └── sipe
│ │ │ └── week3
│ │ │ └── Week3ApplicationTests.kt
│ ├── week3_1.md
│ ├── week3_2.md
│ └── week3_3.md
├── donggeon
│ └── README.md
├── jaeyeong
│ ├── res
│ │ ├── Screenshot_2024-11-20_at_5.24.59_PM.png
│ │ ├── image 1.png
│ │ ├── image 2.png
│ │ └── image.png
│ └── week_3_jaeyeong.md
├── junseo
│ ├── 3주차.md
│ └── res
│ │ ├── 625998e7-7582-4da9-b167-10d47628a923.png
│ │ ├── 9b28b523-7766-4e0d-80ef-4857806b982b.png
│ │ ├── image 1.png
│ │ ├── image 2.png
│ │ ├── image 3.png
│ │ └── image.png
└── positive
│ └── 3주차.pdf
├── week_4
├── char-yb
│ ├── images
│ │ ├── async_1.png
│ │ ├── dispatcher_1.png
│ │ ├── dispatcher_2.png
│ │ ├── runblocking_1.png
│ │ ├── suspend_1.png
│ │ └── suspend_2.png
│ ├── week4
│ │ ├── .gitattributes
│ │ ├── .gitignore
│ │ ├── build.gradle.kts
│ │ ├── gradle
│ │ │ └── wrapper
│ │ │ │ ├── gradle-wrapper.jar
│ │ │ │ └── gradle-wrapper.properties
│ │ ├── gradlew
│ │ ├── gradlew.bat
│ │ ├── settings.gradle.kts
│ │ └── src
│ │ │ ├── main
│ │ │ ├── kotlin
│ │ │ │ └── com
│ │ │ │ │ └── sipe
│ │ │ │ │ └── week4
│ │ │ │ │ ├── AsyncExample.kt
│ │ │ │ │ ├── ContinuationExample.kt
│ │ │ │ │ ├── CoroutineContextExample.kt
│ │ │ │ │ ├── CoroutineScopeExample.kt
│ │ │ │ │ ├── LaunchExample.kt
│ │ │ │ │ ├── RunBlockingExample.kt
│ │ │ │ │ ├── SuspendExample.kt
│ │ │ │ │ ├── Week4Application.kt
│ │ │ │ │ ├── WithContextExample.kt
│ │ │ │ │ ├── YieldExample.kt
│ │ │ │ │ ├── config
│ │ │ │ │ ├── SpringCoroutineDispatcher.kt
│ │ │ │ │ └── TaskExecutorConfig.kt
│ │ │ │ │ ├── controller
│ │ │ │ │ └── ExampleController.kt
│ │ │ │ │ └── service
│ │ │ │ │ └── ExampleService.kt
│ │ │ └── resources
│ │ │ │ └── application.properties
│ │ │ └── test
│ │ │ └── kotlin
│ │ │ └── com
│ │ │ └── sipe
│ │ │ └── week4
│ │ │ └── Week4ApplicationTests.kt
│ └── week_4_1.md
├── junseo
│ ├── 4주차.md
│ └── res
│ │ ├── image (1).png
│ │ ├── image (2).png
│ │ ├── image (3).png
│ │ └── image.png
└── positive
│ └── 4주차.pdf
└── week_5
├── char-yb
└── week5
│ ├── .editorconfig
│ ├── .gitattributes
│ ├── .gitignore
│ ├── README.md
│ ├── build.gradle.kts
│ ├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
│ ├── gradlew
│ ├── gradlew.bat
│ ├── scripts
│ └── pre-commit
│ ├── settings.gradle.kts
│ └── src
│ ├── main
│ ├── kotlin
│ │ └── com
│ │ │ └── sipe
│ │ │ └── week5
│ │ │ ├── Week5Application.kt
│ │ │ ├── domain
│ │ │ ├── auth
│ │ │ │ ├── application
│ │ │ │ │ └── AuthService.kt
│ │ │ │ ├── dto
│ │ │ │ │ ├── AccessTokenDto.kt
│ │ │ │ │ ├── RefreshTokenDto.kt
│ │ │ │ │ ├── TokenPairResponse.kt
│ │ │ │ │ ├── TokenType.kt
│ │ │ │ │ └── request
│ │ │ │ │ │ ├── SignInRequest.kt
│ │ │ │ │ │ └── SignUpRequest.kt
│ │ │ │ ├── exception
│ │ │ │ │ └── AuthenticationException.kt
│ │ │ │ └── presentation
│ │ │ │ │ └── AuthController.kt
│ │ │ ├── common
│ │ │ │ └── BaseEntity.kt
│ │ │ ├── member
│ │ │ │ ├── application
│ │ │ │ │ └── MemberService.kt
│ │ │ │ ├── domain
│ │ │ │ │ ├── Member.kt
│ │ │ │ │ └── MemberRole.kt
│ │ │ │ ├── dto
│ │ │ │ │ └── response
│ │ │ │ │ │ └── FindOneMemberResponse.kt
│ │ │ │ ├── infrastructure
│ │ │ │ │ └── MemberRepository.kt
│ │ │ │ └── presentation
│ │ │ │ │ └── MemberController.kt
│ │ │ └── todo
│ │ │ │ ├── application
│ │ │ │ └── TodoService.kt
│ │ │ │ ├── domain
│ │ │ │ ├── TodoEntity.kt
│ │ │ │ └── TodoStatus.kt
│ │ │ │ ├── dto
│ │ │ │ └── request
│ │ │ │ │ └── CreateTodoRequest.kt
│ │ │ │ ├── infrastructure
│ │ │ │ └── TodoRepository.kt
│ │ │ │ └── presentation
│ │ │ │ └── TodoController.kt
│ │ │ └── global
│ │ │ ├── common
│ │ │ ├── constants
│ │ │ │ └── SecurityConstants.kt
│ │ │ └── response
│ │ │ │ ├── GlobalResponse.kt
│ │ │ │ └── GlobalResponseAdvice.kt
│ │ │ ├── config
│ │ │ ├── database
│ │ │ │ └── R2dbcConfig.kt
│ │ │ ├── properties
│ │ │ │ ├── JwtProperties.kt
│ │ │ │ └── PropertiesConfig.kt
│ │ │ ├── reactor
│ │ │ │ └── ReactorSchedulerConfig.kt
│ │ │ ├── security
│ │ │ │ ├── JwtTokenProvider.kt
│ │ │ │ ├── PrincipalDetails.kt
│ │ │ │ └── WebSecurityConfig.kt
│ │ │ └── webflux
│ │ │ │ └── WebFluxConfig.kt
│ │ │ ├── error
│ │ │ ├── ErrorResponse.kt
│ │ │ └── GlobalExceptionHandler.kt
│ │ │ ├── exception
│ │ │ ├── CustomException.kt
│ │ │ └── ErrorCode.kt
│ │ │ ├── filter
│ │ │ └── JwtAuthenticationFilter.kt
│ │ │ └── util
│ │ │ ├── logging
│ │ │ └── LoggingUtils.kt
│ │ │ ├── member
│ │ │ └── MemberUtil.kt
│ │ │ └── security
│ │ │ └── SecurityUtil.kt
│ └── resources
│ │ └── application.yaml
│ └── test
│ ├── kotlin
│ └── com
│ │ └── sipe
│ │ └── week5
│ │ ├── Week5ApplicationTests.kt
│ │ └── domain
│ │ └── todo
│ │ └── application
│ │ └── TodoServiceTest.kt
│ └── resources
│ └── application-test.yaml
└── positive
├── .gitattributes
├── .gitignore
├── build.gradle.kts
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
└── src
├── main
├── kotlin
│ └── com
│ │ └── todolist
│ │ └── positive
│ │ ├── PositiveApplication.kt
│ │ ├── controller
│ │ └── TodoController.kt
│ │ ├── model
│ │ └── Todo.kt
│ │ ├── repository
│ │ └── TodoRepository.kt
│ │ └── service
│ │ └── TodoService.kt
└── resources
│ ├── application.yml
│ └── schema.sql
└── test
└── kotlin
└── com
└── todolist
└── positive
└── PositiveApplicationTests.kt
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @DongGeon0908 @jaeyeong951 @positivehun @sunseo18 @char-yb
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## 📌 Study
2 | -
3 |
4 | ## 🙏 Focus on me
5 | -
6 |
7 | ## 📚 Reference
8 | -
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .DS_Store
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 3_1_spring_webflux_coroutines
2 |
3 | 어렵게 배워서, 쉽게 사용하는 Spring Webflux + Coroutines
4 |
5 | ---
6 |
7 | ### 팀 이름
8 |
9 | 극락코딩
10 |
11 | ---
12 |
13 | ### 발표순서
14 |
15 | 발표순서 정하기
16 | https://lazygyu.github.io/roulette/
17 |
18 | ---
19 |
20 | ### 팀원
21 |
22 |
23 | 김동건 |
24 | 김재영 |
25 | 김지헌 |
26 | 정준서 |
27 | 차윤범 |
28 |
29 |
30 |
31 | |
32 |
33 |
34 | |
35 |
36 |
37 | |
38 |
39 |
40 | |
41 |
42 |
43 | |
44 |
45 |
46 |
47 |
48 | ---
49 |
50 | ### 주제
51 |
52 | 어렵게 배워서, 쉽게 사용하는 Spring Webflux + Coroutines
53 |
54 | ---
55 |
56 | ### 목표가 무엇인가요?
57 |
58 | - Thread부터 RxJava를 거쳐~ Reactor로 달려가~~코루틴으로 종지부를 찍어요.
59 | - Tomcat에서 시작해서 Netty(이희승님을 기리며) 까지 공부해요. (Spring mvc -> Spring Webflux)
60 | - 위에 소개된 내용들에 대해, 깊이 있는 동작원리와 간단한 사용방식 및 차이를 학습해요.
61 | - 코드 베이스적인 내용 + CS적인 내용을 함께~ (꼬리에 꼬리를 무어, 계속 계속 딥하게~)
62 | - 결과적으로 Thread에 대한 깊이 있는 이해를 얻고자 합니다!!
63 |
64 | ---
65 |
66 | ### 일정
67 |
68 | - 수요일 오후 10시 30분 (1시간 진행)
69 | - 벌금
70 | - 참여, 자료 제출은 별로 벌금... 각 2만원...
71 | - 전날까지 PR 올리기 (화요일)
72 | - 참여 까방권 1회 (그래도 자료 제출은 그날까지 제출하기.)
73 | - 수요일 10시까지는 자료 제출하기!
74 |
75 | ---
76 |
77 | ### 어떻게 진행되나요?
78 |
79 | - 1주차 (동작방식에 대해 깊이 있게 설명, 시범기간...)
80 |
81 | - Thread, Runnable, Callable, ExecutorService, Async, CompletableFuture, ThreadLocal
82 | - Atomic (CAS), Syncronized (lock), voilate, FolkJoinPool, BlockingDeque
83 | - JVM에서 스레드 동작하는 방식
84 | - 컨텍스트 스위칭 비용이란?
85 | - 병렬 프로그램시 알아야할 인프라 리소스
86 | - 요거하고 피드백 진행!!
87 |
88 | - 2주차
89 | - 컨텍스트 스위치 비용 (하드웨어적으로) - 10분
90 | - Atomic (CAS), Syncronized (lock), voilate, (FolkJoinPool, BlockingDeque, java.util.concurrent) - 10분
91 | - Tomcat 네트워크 요청을 받아서, 스레드를 할당받고, 이게 스프링까지 넘어와서 어떤식으로 스레드가 처리되는지? - 10분
92 |
93 | - 3주차
94 | - 코루틴 개념, 코루틴을 왜 쓰는가? - 10분
95 | - [코루틴 개념](https://en.wikipedia.org/wiki/Coroutine)
96 | - [코루틴 docs](https://kotlinlang.org/docs/coroutines-overview.html#documentation)
97 | - 코루틴의 동작원리, 스레드와의 차이 - 10분
98 | - 간단 실습? - 10분
99 | - api 하나 만드는데, io작업 3개 이상이 있다.
100 | - 3개를 동시에 실행시키고, 동시에 완료된 이후에 return하도록 코루틴을 기반으로 구성.
101 | - 하나는 스레드 기반으로 해보기~
102 |
103 | - 4주차
104 | - 다음의 키워드에 대해 학습을 진행 (실습 포함)
105 | - continuation, Dispatchers, async, launch, suspend, coroutineScope, coroutineContext, yield, runBlocking, withContext
106 |
107 | - 5주차
108 | - todo-list 구현해보기, CRUD
109 | - todo 생성하기
110 | - todo 단건 조회
111 | - todo 상태로 조회 (예정, 진행중, 완료), 상태는 index 없이.
112 | - equals 비교
113 | - test case를 만드는 api 구성. 5000만건..
114 | - throughput -> jmeter
115 | - latency -> jmeter
116 | - webflux + coroutines 구현해보기!
117 | - mvc + virtual thread
118 | - throughput and memory, cpu resource 점검...
119 |
120 | - 1주차 ~ 2주차: Thread 기반 학습, M-threads, Spring MVC 등을 공부해요.
121 | - 모든 Task는 CPU에 스레드가 올라가며, 동작을 진행합니다. 그렇기 때문에 OS단과 JVM에서의 Thread 동작원리를 같이 공부해요.
122 | - M-Threads를 대표하는 키워드 스프링 키워드에 대해 공부해요. CompletableFuture, Runnable, Callable, Executor, async 등..
123 | - Spring Tomcat의 스레드로부터, 비즈니스로직에서 사용되는 M-Threads와의 연관성을 같이 공부해요.
124 | - 3주차 ~ 4주차: Corotuines에 대해 공부해요.
125 | - 요즘 유행하는 코루틴.. kotlin-coroutines에 대해 학습해요. (제일 중요한건 동작원리!)
126 | - 경량 스레드가 무엇일까요? OS와 JVM Level에서 공부해요.
127 | - 5주차: Spring Webflux + Corotuines에서 사용되는 스레드 처리를 공부해요.
128 | - Webflux와 Corotuines를 엮었을 때, peer to peer로 어떻게 동작하는지 동작원리를 확인해요.
129 | - 6주차: 비동기 서비스를 이용할 때 장점 그리고, 발생할 수 있는 이슈 등에 대해 경험을 공유해요!
130 |
131 | ---
132 |
133 | ### 학습 방법
134 |
135 | - 각 주차에 정해진 주제와 목표를 달성해요!
136 | - 서로 준비한 내용을 발표해요!
137 | - 10분에서 15분 동안 강의를 진행한다고 생각하며 진행.
138 | - 질문과 대답을 반복하며, 동료들과 지식을 공유
139 | - 만약... 준비하지 못하면 벌금..
140 | - 온라인으로 진행해요. 만약 지원금이 있다면..오프라인도~
141 |
142 | ---
143 |
144 | ### 주요키워드
145 |
146 | Kotlin, Thread, Reactor, RxJava, Webflux, Tomcat, Netty, Coroutines, Mvc, M-threads
147 |
148 | ---
149 |
150 | ### 그래서 우리는... 이걸 중점으로 공부할거에요.
151 |
152 | - 왜? Non-Blocking IO를 사용할까?
153 | - 그렇다면, 이런 기술들이 OS Level과 JVM Level에서 어떤식으로 변할까?
154 | - (궁극적으로 우리가 쓰는 기술이 하드웨어 장비와 연결되어, 어떤 방식으로 동작하는지 이해하는게, 제일 중요할 것 같습니다.)
155 |
--------------------------------------------------------------------------------
/week_1/char-yb/images/Thread-life-cycle.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_1/char-yb/images/Thread-life-cycle.jpeg
--------------------------------------------------------------------------------
/week_1/char-yb/week1/.gitattributes:
--------------------------------------------------------------------------------
1 | /gradlew text eol=lf
2 | *.bat text eol=crlf
3 | *.jar binary
4 |
--------------------------------------------------------------------------------
/week_1/char-yb/week1/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | .gradle
3 | build/
4 | !gradle/wrapper/gradle-wrapper.jar
5 | !**/src/main/**/build/
6 | !**/src/test/**/build/
7 |
8 | ### STS ###
9 | .apt_generated
10 | .classpath
11 | .factorypath
12 | .project
13 | .settings
14 | .springBeans
15 | .sts4-cache
16 | bin/
17 | !**/src/main/**/bin/
18 | !**/src/test/**/bin/
19 |
20 | ### IntelliJ IDEA ###
21 | .idea
22 | *.iws
23 | *.iml
24 | *.ipr
25 | out/
26 | !**/src/main/**/out/
27 | !**/src/test/**/out/
28 |
29 | ### NetBeans ###
30 | /nbproject/private/
31 | /nbbuild/
32 | /dist/
33 | /nbdist/
34 | /.nb-gradle/
35 |
36 | ### VS Code ###
37 | .vscode/
38 |
39 | ### Kotlin ###
40 | .kotlin
41 |
--------------------------------------------------------------------------------
/week_1/char-yb/week1/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("jvm") version "1.9.25"
3 | kotlin("plugin.spring") version "1.9.25"
4 | id("org.springframework.boot") version "3.3.5"
5 | id("io.spring.dependency-management") version "1.1.6"
6 | }
7 |
8 | group = "com.sipe"
9 | version = "0.0.1-SNAPSHOT"
10 |
11 | java {
12 | toolchain {
13 | languageVersion = JavaLanguageVersion.of(21)
14 | }
15 | }
16 |
17 | repositories {
18 | mavenCentral()
19 | }
20 |
21 | dependencies {
22 | implementation("org.springframework.boot:spring-boot-starter")
23 | implementation("org.jetbrains.kotlin:kotlin-reflect")
24 | testImplementation("org.springframework.boot:spring-boot-starter-test")
25 | testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
26 | testRuntimeOnly("org.junit.platform:junit-platform-launcher")
27 | }
28 |
29 | kotlin {
30 | compilerOptions {
31 | freeCompilerArgs.addAll("-Xjsr305=strict")
32 | }
33 | }
34 |
35 | tasks.withType {
36 | useJUnitPlatform()
37 | }
38 |
--------------------------------------------------------------------------------
/week_1/char-yb/week1/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_1/char-yb/week1/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/week_1/char-yb/week1/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/week_1/char-yb/week1/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/week_1/char-yb/week1/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "week1"
2 |
--------------------------------------------------------------------------------
/week_1/char-yb/week1/src/main/kotlin/com/sipe/week1/Week1Application.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week1
2 |
3 | import org.springframework.boot.autoconfigure.SpringBootApplication
4 | import org.springframework.boot.runApplication
5 |
6 | @SpringBootApplication
7 | class Week1Application
8 |
9 | fun main(args: Array) {
10 | // 스레드 생성
11 | val exThread = ExThread()
12 |
13 | // start를 통해 run 메서드가 실행되는데, run을 직접 실행하는게 아닌 start를 실행하는 것이다.
14 | exThread.start()
15 | println("MyMain Thread Name: ${Thread.currentThread().name}")
16 | }
17 |
18 | class ExThread : Thread() {
19 | override fun run() {
20 | println("ThreadName: ${currentThread().name}")
21 | }
22 | }
--------------------------------------------------------------------------------
/week_1/char-yb/week1/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | spring.application.name=week1
2 |
--------------------------------------------------------------------------------
/week_1/char-yb/week1/src/test/kotlin/com/sipe/week1/CallableTest.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week1
2 |
3 | import org.junit.jupiter.api.Test
4 | import java.util.concurrent.Callable
5 | import java.util.concurrent.Executors
6 |
7 | class CallableTest {
8 |
9 | @Test
10 | fun callable_void() {
11 | val executorService = Executors.newSingleThreadExecutor()
12 |
13 | val callable = Callable {
14 | // Thread: pool-1-thread-1 is running
15 | println("Thread: ${Thread.currentThread().name} is running")
16 | null
17 | }
18 |
19 | executorService.submit(callable)
20 | executorService.shutdown()
21 | }
22 |
23 | @Test
24 | fun callable_String() {
25 | val executorService = Executors.newSingleThreadExecutor()
26 |
27 | val callable = Callable { "Thread: " + Thread.currentThread().name }
28 |
29 | executorService.submit(callable)
30 | executorService.shutdown()
31 | }
32 |
33 | }
--------------------------------------------------------------------------------
/week_1/char-yb/week1/src/test/kotlin/com/sipe/week1/ExecutorServiceTest.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week1
2 |
3 | import org.assertj.core.api.Assertions.assertThat
4 | import org.junit.jupiter.api.Assertions.assertThrows
5 | import org.junit.jupiter.api.Test
6 | import java.time.Duration
7 | import java.time.Instant
8 | import java.util.*
9 | import java.util.concurrent.*
10 |
11 |
12 | class ExecutorServiceTest {
13 |
14 | // 라이프사이클 관리
15 | @Test
16 | fun shutdown() {
17 | val executorService: ExecutorService = Executors.newFixedThreadPool(10)
18 |
19 | val runnable =
20 | Runnable { println("Thread: " + Thread.currentThread().name) }
21 | executorService.execute(runnable)
22 |
23 | // shutdown이 호출되기 전까지 계속해서 다음 작업이 대기되는데, 작업이 완료되었다면 shutdown을 명시적 호출해야 한다.
24 | // 만약 작업 실행 후에 shtudown을 해주지 않으면 다음과 같이 프로세스가 끝나지 않고, 계속해서 다음 작업을 기다리게 된다
25 | executorService.shutdown()
26 |
27 | val result: RejectedExecutionException = assertThrows(RejectedExecutionException::class.java) {
28 | executorService.execute(
29 | runnable
30 | )
31 | }
32 | assertThat(result).isInstanceOf(RejectedExecutionException::class.java)
33 | }
34 |
35 | @Test
36 | @Throws(InterruptedException::class)
37 | // shutdownNow()는 현재 진행 중인 작업을 취소하고 대기 중인 작업을 무시한다.
38 | // interrupt 여부에 따른 처리 코드가 존재하지 않다면 계속 실행되므로 interrupt 여부를 확인하여 종료해야 한다.
39 | fun shutdownNow() {
40 | val runnable = Runnable {
41 | println("Start")
42 | while (true) {
43 | if (Thread.currentThread().isInterrupted) {
44 | break
45 | }
46 | }
47 | println("End")
48 | }
49 |
50 | val executorService: ExecutorService = Executors.newFixedThreadPool(10)
51 | executorService.execute(runnable)
52 |
53 | executorService.shutdownNow()
54 | Thread.sleep(1000L)
55 | }
56 |
57 | // 비동기 작업
58 | @Test
59 | @Throws(InterruptedException::class, ExecutionException::class)
60 | fun invokeAll() {
61 | val executorService: ExecutorService = Executors.newFixedThreadPool(10)
62 | val start = Instant.now()
63 |
64 | val hello = Callable {
65 | Thread.sleep(1000L)
66 | val result = "Hello"
67 | println("result = $result")
68 | result
69 | }
70 |
71 | val mang = Callable {
72 | Thread.sleep(2000L)
73 | val result = "Mang"
74 | println("result = $result")
75 | result
76 | }
77 |
78 | val kyu = Callable {
79 | Thread.sleep(3000L)
80 | val result = "kyu"
81 | println("result = $result")
82 | result
83 | }
84 |
85 | val futures: List> = executorService.invokeAll(Arrays.asList(hello, mang, kyu))
86 | for (f in futures) {
87 | println(f.get())
88 | }
89 |
90 | System.out.println("time = " + Duration.between(start, Instant.now()).getSeconds())
91 | executorService.shutdown()
92 | }
93 |
94 | @Test
95 | @Throws(InterruptedException::class, ExecutionException::class)
96 | fun invokeAny() {
97 | val executorService: ExecutorService = Executors.newFixedThreadPool(10)
98 | val start = Instant.now()
99 |
100 | val hello = Callable {
101 | Thread.sleep(1000L)
102 | val result = "Hello"
103 | println("result = $result")
104 | result
105 | }
106 |
107 | val mang = Callable {
108 | Thread.sleep(2000L)
109 | val result = "Mang"
110 | println("result = $result")
111 | result
112 | }
113 |
114 | val kyu = Callable {
115 | Thread.sleep(3000L)
116 | val result = "kyu"
117 | println("result = $result")
118 | result
119 | }
120 |
121 | val result: String = executorService.invokeAny(listOf(hello, mang, kyu))
122 | println("result = " + result + " time = " + Duration.between(start, Instant.now()).seconds)
123 |
124 | executorService.shutdown()
125 | }
126 | }
--------------------------------------------------------------------------------
/week_1/char-yb/week1/src/test/kotlin/com/sipe/week1/ExecutorTest.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week1
2 |
3 | import org.junit.jupiter.api.Test
4 | import java.util.concurrent.Executor
5 |
6 | class ExecutorTest {
7 |
8 | @Test
9 | fun executorRun() {
10 | val runnable = Runnable {
11 | // Thread: Test worker
12 | println("Thread: ${Thread.currentThread().name}")
13 | }
14 |
15 | val executor: Executor = RunExecutor()
16 | executor.execute(runnable)
17 | }
18 |
19 | class RunExecutor : Executor {
20 | override fun execute(command: Runnable) {
21 | command.run()
22 | }
23 | }
24 |
25 | @Test
26 | fun executorStart() {
27 | val runnable =
28 | Runnable { println("Thread: " + Thread.currentThread().name) }
29 |
30 | val executor: Executor = StartExecutor()
31 | executor.execute(runnable)
32 | }
33 |
34 | class StartExecutor : Executor {
35 | override fun execute(command: Runnable) {
36 | Thread(command).start()
37 | }
38 | }
39 |
40 | }
--------------------------------------------------------------------------------
/week_1/char-yb/week1/src/test/kotlin/com/sipe/week1/FutureTest.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week1
2 |
3 | import org.assertj.core.api.Assertions.assertThat
4 | import org.junit.jupiter.api.Test
5 | import java.util.concurrent.Callable
6 | import java.util.concurrent.ExecutionException
7 | import java.util.concurrent.Executors
8 |
9 |
10 | class FutureTest {
11 |
12 | @Test
13 | @Throws(ExecutionException::class, InterruptedException::class)
14 | fun get() {
15 | val executorService = Executors.newSingleThreadExecutor()
16 |
17 | val callable = callable()
18 |
19 | // It takes 3 seconds by blocking(블로킹에 의해 3초 걸림)
20 | val future = executorService.submit(callable)
21 |
22 | println(future.get())
23 |
24 | executorService.shutdown()
25 | }
26 |
27 | @Test
28 | fun isCancelled_False() {
29 | val executorService = Executors.newSingleThreadExecutor()
30 |
31 | val callable = callable()
32 |
33 | val future = executorService.submit(callable)
34 | assertThat(future.isCancelled).isFalse()
35 |
36 | executorService.shutdown()
37 | }
38 |
39 | @Test
40 | fun isCancelled_True() {
41 | val executorService = Executors.newSingleThreadExecutor()
42 |
43 | val callable = callable()
44 |
45 | val future = executorService.submit(callable)
46 | future.cancel(true)
47 |
48 | assertThat(future.isCancelled).isTrue()
49 | executorService.shutdown()
50 | }
51 |
52 | @Test
53 | fun isDone_False() {
54 | val executorService = Executors.newSingleThreadExecutor()
55 |
56 | val callable = callable()
57 |
58 | val future = executorService.submit(callable)
59 |
60 | assertThat(future.isDone).isFalse()
61 | executorService.shutdown()
62 | }
63 |
64 | @Test
65 | fun isDone_True() {
66 | val executorService = Executors.newSingleThreadExecutor()
67 |
68 | val callable = callable()
69 |
70 | val future = executorService.submit(callable)
71 |
72 | while (future.isDone) {
73 | assertThat(future.isDone).isTrue()
74 | executorService.shutdown()
75 | }
76 | }
77 |
78 | private fun callable(): Callable = Callable {
79 | Thread.sleep(3000L)
80 | "Thread: " + Thread.currentThread().name
81 | }
82 | }
--------------------------------------------------------------------------------
/week_1/char-yb/week1/src/test/kotlin/com/sipe/week1/Week1ApplicationTests.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week1
2 |
3 | import org.junit.jupiter.api.Test
4 | import org.springframework.boot.test.context.SpringBootTest
5 |
6 | @SpringBootTest
7 | class Week1ApplicationTests {
8 |
9 | @Test
10 | fun contextLoads() {
11 | }
12 |
13 | @Test
14 | fun threadStart() {
15 | val thread = MyThread()
16 |
17 | thread.start()
18 | }
19 |
20 | class MyThread : Thread() {
21 | override fun run() {
22 | println("Thread is running")
23 | }
24 | }
25 |
26 | @Test
27 | fun runnable() {
28 | val runnable = Runnable {
29 | println("Thread: ${Thread.currentThread().name} is running")
30 | }
31 |
32 | val thread = Thread(runnable)
33 | thread.start()
34 | println("Hello: ${Thread.currentThread().name}")
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/week_1/char-yb/week_1_2.md:
--------------------------------------------------------------------------------
1 | ## 1주차
2 |
3 | - JVM에서 스레드 동작하는 방식
4 | - 컨텍스트 스위칭 비용이란?
5 | - 병렬 프로그램 시 알아야할 인프라 리소스
6 |
7 | ---
8 |
9 | ## JVM에서 스레드 동작하는 방식
10 |
11 | JVM(Java Virtual Machine)에서 스레드는 운영 체제의 스레드와 밀접하게 연관되어 동작하며, JVM 내에서의 스레드 동작 방식과 원리를 이해하는 것은 멀티스레드 프로그래밍에서 중요한 요소이다.
12 | JVM은 플랫폼 독립적 자바 프로그램의 실행을 가능하게 한다.
13 | JVM은 크게 클래스 로더 시스템, 런타임 데이터 영역, 실행 엔진으로 구성된다. 이 중 런타임 데이터 영역은 자바 프로그램 실행 중 생성되는 데이터를 저장하는 공간이다.
14 |
15 | 왜냐하면 JVM의 런타임 데이터 영역은 프로그램의 실행 상태를 관리하고, 메모리 관리의 핵심을 담당하고 있다. 이 영역은 메소드 영역, 힙 영역, 스택 영역, PC 레지스터, Native 메소드 스택으로 구분된다.
16 |
17 | 메소드 영역에는 클래스 정보, 상수, 정적 변수 등이 저장되며, 힙 영역은 객체와 배열이 할당되는 곳입니다. 스택 영역은 스레드마다 생성되며 메소드 호출과 로컬 변수 등을 저장합니다.
18 |
19 | 자바는 멀티 스레딩을 지원하는 언어입니다. 멀티 스레딩은 여러 스레드가 동시에 작업을 수행할 수 있게 해주며, 멀티 스레드 환경에서 각 스레드는 자신만의 스택 영역을 가지지만, 힙 영역과 메소드 영역은 모든 스레드가 공유한다. 이는 스레드 간의 데이터 공유와 동시성 제어를 가능하게 한다.
20 |
21 | 왜냐하면 멀티 스레딩은 동시에 여러 작업을 처리할 수 있기 때문에, I/O 작업이 많거나 복잡한 계산이 필요한 애플리케이션에서 성능을 향상시킬 수 있다.
22 |
23 | 예를 들어, 웹 서버는 동시에 여러 클라이언트의 요청을 처리하기 위해 멀티 스레딩을 사용한다. 추후 VM을 사용할 수도 있지만, 내부에 멀티 스레딩에 대한 핸들링을 통해 정합성 또한 제어가 가능하여 성능 향상과 무결성, 일관성에 초점을 맞춰 개발할 수 있다.
24 |
25 | 1. 스레드의 생성과 관리
26 | 이전에 실습한 내용으로 Thread, Runnable을 활용한 코드 참고.
27 | • 스레드 생성: JVM에서 Thread 클래스나 Runnable 인터페이스를 구현한 객체를 통해 스레드를 생성할 수 있다. 새로운 스레드가 생성되면 JVM은 운영 체제의 스레드 관리 API를 호출하여 운영 체제의 스레드를 할당한다.
28 | • 스레드 실행: 생성된 스레드의 start() 메서드를 호출하면 스레드는 RUNNABLE 상태로 진입하여 JVM에 의해 관리되기 시작한다. JVM은 운영 체제에 이 스레드를 등록하여 CPU 시간을 할당받을 수 있도록 한다.
29 |
30 | 2. 스레드 상태와 전환
31 | 스레드는 생명 주기 동안 여러 상태를 거친다.
32 |
33 | 
34 |
35 | • NEW: 스레드가 생성되었지만 아직 실행되지 않은 초기 상태.
36 | • RUNNABLE: 스레드가 실행 가능한 상태로, CPU를 할당받으면 코드가 실행되며, JVM은 스레드를 스케줄링하여 CPU 리소스를 할당합니다.
37 | • BLOCKED: 다른 스레드에 의해 잠금이 걸린 객체를 기다리고 있을 때.
38 | • WAITING: 특정 조건이 충족될 때까지 대기하는 상태로, wait(), join(), sleep() 메서드 등이 호출되면 이 상태가 된다.
39 | • TERMINATED: 스레드의 실행이 완료되거나 종료되었을 때의 상태이다.
40 |
41 | 3. JVM의 스레드 스케줄링
42 |
43 | • 스레드 우선순위: 각 스레드는 우선순위(priority) 속성을 가지고 있으며, JVM은 우선순위에 따라 스레드 스케줄링을 수행할 수 있다. 다만, 우선순위가 높은 스레드가 반드시 우선적으로 실행되는 것은 내부 운영 체제의 정책에 따라 달라질 수 있다.
44 | • 타임 슬라이스: JVM은 운영 체제의 타임 슬라이스(time slice) 정책을 따라 각 스레드에 CPU 시간을 할당하며, 해당 시간이 만료되면 다른 스레드가 CPU를 사용할 수 있도록 한다. 이 방식은 선점형 스케줄링(preemptive scheduling) 방식으로 이루어진다.
45 | • 스레드 동시성 제어: JVM은 동기화 블록(synchronized 키워드)이나 Lock API를 통해 스레드 간의 동시성 제어를 합니다.(요즘엔 synchronized 키워드 안하고 Redission? Lettuce로도 제어를 한다고 함) 동기화는 특정 코드 블록에서 하나의 스레드만 접근할 수 있도록 잠금을 설정하여 데이터 일관성을 유지한다. (중요!)
46 |
47 | 4. 메모리 모델과 스레드 간 통신
48 |
49 | • JVM 메모리 모델: JVM은 스레드마다 별도의 스택(stack) 메모리를 가지고 있으며, 이 스택에는 각 스레드의 지역 변수 및 호출 스택이 저장된다. 반면, 모든 스레드가 공유하는 힙(heap) 메모리에는 객체 인스턴스와 클래스 데이터가 저장된다. // 면접 질문에 가끔 나옴
50 | • 가시성(Visibility): 한 스레드에서 변경된 메모리 상태가 다른 스레드에게 즉시 보장되지 않을 수 있다. volatile 키워드를 사용하거나 synchronized 블록을 통해 메모리 가시성을 확보할 수 있다고 한다.
51 | • 공유 자원과 동기화: JVM은 스레드가 공유하는 자원에 동시 접근할 때 synchronized 키워드를 통해 동기화하며, 이를 통해 데이터의 일관성을 유지한다.
52 |
53 | 5. 스레드 풀(Thread Pool)과 Executor
54 |
55 | • 스레드 풀: 자바에서는 Executor 프레임워크를 통해 스레드 풀을 관리할 수 있다. 스레드 풀은 미리 생성된 스레드를 재사용하여 스레드 생성 및 소멸에 드는 비용을 줄일 수 있다.
56 | • ExecutorService: ExecutorService는 다양한 유형의 스레드 풀을 제공하며, 여러 작업을 동시에 실행할 수 있도록 한다. 이를 통해 작업이 완료될 때까지 대기하거나, 특정 작업의 종료를 기다리는 등 다양한 기능을 제공한다.
57 |
58 | 6. JVM의 가비지 컬렉터와 스레드
59 |
60 | • 가비지 컬렉션의 동작 방식: JVM은 힙 메모리 관리를 위해 가비지 컬렉터(GC)를 실행하며, 이는 별도의 스레드로 작동한다. GC 스레드는 특정 주기에 따라 힙 메모리를 스캔하여 사용되지 않는 객체 인스턴스를 제거하며, 힙 메모리를 최적화한다.
61 | • GC 스레드와 멀티스레딩: GC 스레드도 JVM 내의 하나의 스레드로 작동하며, GC의 영향을 최소화하기 위해 다양한 GC 정책과 알고리즘이 사용되고 있다. 다중 스레드를 활용하여 가비지 컬렉션 작업을 빠르게 처리할 수도 있다한다.
62 |
63 | 7. JVM의 스레드 관리 최적화
64 |
65 | • 스레드 덤프: JVM은 특정 시점의 스레드 상태를 분석하기 위해 스레드 덤프(thread dump)를 제공한다. 스레드 덤프는 현재 실행 중인 각 스레드의 상태와 자원 잠금 정보를 포함하며, 이를 통해 교착 상태(deadlock)나 성능 병목을 분석할 수 있다한다. (이건 좀 흥미롭네요)
66 | • JVM 파라미터: -Xss 옵션을 통해 각 스레드 스택의 크기를 조절할 수 있으며, 이는 특정 애플리케이션에서 스택 오버플로우를 방지하거나 메모리 효율성을 높이기 위해 조정할 수 있다. (jib 컨테이너에서도 메모리에 대한 크기 조정이 가능한데 이것도 그러한지? 잘 모르겠습니다...)
67 |
68 | ---
69 |
70 | ## 컨텍스트 스위칭 비용이란?
71 |
72 | 우선 컨텍스트 스위칭이 뭘까?
73 | 컨텍스트라는 것은 프로세스/스레드의 상태를 의미할 수 있다.
74 | 그럼 스위칭이라는 워딩은 교체이라는 뜻으로 프로세스나 스레드의 교체에 대한 의미로 볼 수 있다.
75 |
76 | #### 왜 컨텍스트 스위칭을 할까?
77 |
78 | 간단하다. 여러 프로세스와 스레드를 동시에 실행시키게 하고 필요한 경우에 실행시키게 하기 위해서이다.
79 | 빠른 성능을 위해 과도하게 컨텍스트 스위칭을 하게 되면 스레드가 계속 생성될 수도 있고, 트래킹하기 어려워진다.
80 | 배보다 배꼽이 더 커질 수도 있다는 뜻이다.
81 |
82 | #### 컨텍스트 스위칭 과정
83 |
84 | 우선 프로세스, 스레드 별도의 스위칭 과정으로 분리할 것이다.
85 |
86 | **프로세스 컨텍스트 스위칭 과정**
87 |
88 | 1. 상태 저장: 현재 실행 중인 프로세스의 레지스터, 프로그램 카운터, 메모리 매핑 정보 등의 상태를 PCB(Process Control Block)에 저장한다.
89 | 2. 상태 변경: 스케줄러가 새롭게 실행할 프로세스를 선택하고, CPU 컨텍스트를 변경하여 해당 프로세스의 PCB에 저장된 정보를 불러오고
90 | 3. 메모리 매핑: 프로세스 간 주소 공간이 다르므로, 메모리 매핑 정보를 다시 설정한다.
91 | 4. 실행: 선택된 프로세스가 CPU에 의해 실행된다.
92 |
93 | **스레드 컨텍스트 스위칭 과정**
94 |
95 | 스레드의 경우 같은 프로세스 내에서 컨텍스트가 변경되기 때문에 주소 공간을 공유한다. 따라서 메모리 매핑 과정이 필요하지 않아 스레드 간 상태 정보만 변경하면 된다.
96 |
97 | ### 그래서 비용은?
98 |
99 | 컨텍스트 스위칭에는 다음과 같은 비용이 발생한다.
100 |
101 | • CPU 자원 소모: 상태 저장 및 복구 작업이 CPU 자원을 소모하여 실행 중인 프로세스나 스레드가 일시 중단된다.
102 | • 메모리 캐시 미스: 스위칭이 발생하면 이전 작업의 캐시 데이터가 무효화되고, 새로운 작업의 캐시를 다시 로드해야 하므로 캐시 미스가 증가할 수 있다.
103 | • 시간 지연: 상태 전환과 스케줄링 과정 자체가 시간이 걸리므로, 빈번한 스위칭은 시스템 성능을 저하시킬 수도 있다.
104 |
105 | 과도한 컨텍스트 스위칭은 이 비용이 누적되어 시스템 성능에 악영향을 미칠 수 있으므로, 필요할 때만 수행되도록 스케줄링 최적화가 중요하다.
106 |
107 | ---
108 |
109 | ## 병렬 프로그램 시 알아야 할 인프라 리소스
110 |
--------------------------------------------------------------------------------
/week_1/jaeyeong/res/image 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_1/jaeyeong/res/image 1.png
--------------------------------------------------------------------------------
/week_1/jaeyeong/res/image 2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_1/jaeyeong/res/image 2.png
--------------------------------------------------------------------------------
/week_1/jaeyeong/res/image 3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_1/jaeyeong/res/image 3.png
--------------------------------------------------------------------------------
/week_1/jaeyeong/res/image 4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_1/jaeyeong/res/image 4.png
--------------------------------------------------------------------------------
/week_1/jaeyeong/res/image 5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_1/jaeyeong/res/image 5.png
--------------------------------------------------------------------------------
/week_1/jaeyeong/res/image 6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_1/jaeyeong/res/image 6.png
--------------------------------------------------------------------------------
/week_1/jaeyeong/res/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_1/jaeyeong/res/image.png
--------------------------------------------------------------------------------
/week_1/jaeyeong/week 1.md:
--------------------------------------------------------------------------------
1 | # week 1
2 |
3 | ### What is thread?
4 |
5 | - 왜 스레드라는 개념이 만들어져야했을까?
6 | - 하나의 프로세스 안에서 여러 작업을 동시에 처리하고 싶기 때문
7 | - 그렇지만 스레드는 사실 본질적으로 프로세스와 같다.
8 | - 스레드는 각자의 program counter 와 register 값들을 갖고 있다. 때문에 여러 스레드가 하나의 core 에서 실행되어야 할 경우 context switching 이 일어난다.
9 | - 스레드 간의 context switching 은 프로세스 간의 그것과 아주 비슷하다. 코어를 양보할 스레드의 실행 상태는 저장되어야 하고 코어를 차지할 스레드의 실행 상태는 다시 복구되어야한다. 이는 프로세스의 context switching 과정과 동일하며 상태를 저장하는 구조체를 부르는 이름만 다르다. (PCB, TCB)
10 |
11 | 
12 |
13 | - 유일한 차이는 스레드는 프로세스와 다르게 여럿이서 **하나의 주소공간을 공유**하고 stack 이라는 각자의 공간을 가진다는 것
14 |
15 | 
16 |
17 | - 스레드끼리는 동일한 주소공간을 사용하기에 새롭게 생성하더라도 페이지 테이블 복사가 필요 없어서 생성 시간이 매우 짧고, 메모리를 공유하므로 이를 활용한 협력적인 병렬 코드를 작성할 수 있다.
18 |
19 | ### 프로세스와 스레드의 차이
20 |
21 | - 앞서 말했듯 프로세스와 스레드는 별반 다르지 않다.
22 | - 리눅스에서 새로운 프로세스를 생성하는 시스템 콜인 `fork()` 와 스레드를 생성하는 시스템 콜인 `pthread_create()` 는 결국 내부적으로 `clone()` 시스템 콜을 호출하게 되는데, `clone()` 시스템 콜은 파라미터를 통해 자식 프로세스가 부모와 주소공간을 공유할 수 있는지의 여부를 조절할 수 있다.
23 |
24 | 
25 |
26 | - POSIX `pthread_create()` 구현부를 보면 아래처럼 `clone()` 시스템 콜 호출 시 사용할 플래그들을 미리 정의해놓았는데, 이 플래그들을 사용해서 같은 `clone()` 이지만 프로세스가 아닌 스레드를 만들 수 있는걸로 보인다.
27 |
28 | 
29 |
30 | - https://github.com/torvalds/linux/blob/master/kernel/fork.c#L2694
31 | - 스레드를 생성한다고 하면 보통 커널 스레드를 말하는데, 커널 스레드는 각자 PID 를 가지고 있으며 PPID 는 부모 프로세스 PID 가 된다. 그래서 ps 명령어로 터미널에서 프로세스와 동일하게 조회 가능하다.
32 | - 우리가 애용하는 JVM 의 자바 스레드는 커널 스레드와 1:1 매핑된다. 자바에서 Thread 를 생성하면 커널 스레드가 새롭게 하나 생성되는 것이다.
33 | - 또 우리가 너무 좋아하는 스프링(tomcat) 의 경우 그냥 띄우기만 해도 수십개의 스레드가 생성될텐데 이게 다 터미널에서 조회가 될까?
34 |
35 | 
36 |
37 |
38 | ### JVM 스레드 구조
39 |
40 | - 자바도 병렬 처리를 위해 스레드를 기본적으로 제공한다.
41 | - 앞서 말했듯 대부분의 JVM 구현체는 1:1 스레드 모델을 채택했는데, 자바의 `java.lang.Thread` 인스턴스 하나가 OS 의 커널 스레드 하나에 직접 매핑된다는 의미이다.
42 | - JVM 의 스레드 구조는 간략히 나타내면 아래와 같다. (사진은 JVM 의 전체적인 메모리 구조)
43 |
44 | 
45 |
46 | - 모든 Thread는 자신만의 Stack 과 Register 값을 갖는다. (이 Register 값은 아마 JVM 명령어 주소를 저장할 듯)
47 | - JVM 은 Stack에 대해 pop 이나 push 동작만 수행할 수 있다.
48 | - Stack의 각 원소를 Stack Frame 이라고 부르며, 메소드 하나를 실행할 때 마다 새롭게 생성되며(push) 종료 시 사라진다(pop).
49 | - Stack Frame은 지역변수 Array, Operand Stack, 상수풀 레퍼런스 로 구성되어 있다.(위 사진에서 각각 LVA, OS, FD)
50 |
51 | ### JVM 에서 스레드를 효율적으로 다루기 위한 방법
52 |
53 | - 개인적으로 자바를 사용하면서 스레드를 직접 생성하고 다뤄야하는 상황은 잘 없었다.
54 | - 보통 기존에 존재하는 여러가지 도구를 활용해서 동시성 로직을 구현하는데, 자바의 모든 동시성 관련 도구는 [여기](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/package-summary.html)에 담겨있다.
55 |
56 | 
57 |
58 | - 이 중 가장 흔히 쓰이는 것 몇가지를 뽑자면 `ExecutorService`, `Future`, `ConcurrentHashMap`, `BlockingQueue` 정도가 되겠다.
59 | - 이들 각각에 대한 자세한 설명은 다음 주차에 계속
60 |
61 | ### JVM 에서 병렬 프로그래밍의 미래
62 |
63 | - WIP
--------------------------------------------------------------------------------
/week_1/junseo/1주차/res/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_1/junseo/1주차/res/image.png
--------------------------------------------------------------------------------
/week_1/junseo/1주차/res/image1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_1/junseo/1주차/res/image1.png
--------------------------------------------------------------------------------
/week_1/junseo/1주차/res/image2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_1/junseo/1주차/res/image2.png
--------------------------------------------------------------------------------
/week_1/junseo/1주차/res/image3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_1/junseo/1주차/res/image3.png
--------------------------------------------------------------------------------
/week_1/positivehun/1주차.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_1/positivehun/1주차.pdf
--------------------------------------------------------------------------------
/week_2/char-yb/images/week2_2_program.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_2/char-yb/images/week2_2_program.png
--------------------------------------------------------------------------------
/week_2/char-yb/images/week2_3_nio_connector.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_2/char-yb/images/week2_3_nio_connector.png
--------------------------------------------------------------------------------
/week_2/char-yb/week2/.gitattributes:
--------------------------------------------------------------------------------
1 | /gradlew text eol=lf
2 | *.bat text eol=crlf
3 | *.jar binary
4 |
--------------------------------------------------------------------------------
/week_2/char-yb/week2/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | .gradle
3 | build/
4 | !gradle/wrapper/gradle-wrapper.jar
5 | !**/src/main/**/build/
6 | !**/src/test/**/build/
7 |
8 | ### STS ###
9 | .apt_generated
10 | .classpath
11 | .factorypath
12 | .project
13 | .settings
14 | .springBeans
15 | .sts4-cache
16 | bin/
17 | !**/src/main/**/bin/
18 | !**/src/test/**/bin/
19 |
20 | ### IntelliJ IDEA ###
21 | .idea
22 | *.iws
23 | *.iml
24 | *.ipr
25 | out/
26 | !**/src/main/**/out/
27 | !**/src/test/**/out/
28 |
29 | ### NetBeans ###
30 | /nbproject/private/
31 | /nbbuild/
32 | /dist/
33 | /nbdist/
34 | /.nb-gradle/
35 |
36 | ### VS Code ###
37 | .vscode/
38 |
39 | ### Kotlin ###
40 | .kotlin
41 |
--------------------------------------------------------------------------------
/week_2/char-yb/week2/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("jvm") version "1.9.25"
3 | kotlin("plugin.spring") version "1.9.25"
4 | id("org.springframework.boot") version "3.3.5"
5 | id("io.spring.dependency-management") version "1.1.6"
6 | }
7 |
8 | group = "com.sipe"
9 | version = "0.0.1-SNAPSHOT"
10 |
11 | java {
12 | toolchain {
13 | languageVersion = JavaLanguageVersion.of(21)
14 | }
15 | }
16 |
17 | repositories {
18 | mavenCentral()
19 | }
20 |
21 | dependencies {
22 | implementation("org.springframework.boot:spring-boot-starter")
23 | implementation("org.jetbrains.kotlin:kotlin-reflect")
24 | testImplementation("org.springframework.boot:spring-boot-starter-test")
25 | testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
26 | testRuntimeOnly("org.junit.platform:junit-platform-launcher")
27 | }
28 |
29 | kotlin {
30 | compilerOptions {
31 | freeCompilerArgs.addAll("-Xjsr305=strict")
32 | }
33 | }
34 |
35 | tasks.withType {
36 | useJUnitPlatform()
37 | }
38 |
--------------------------------------------------------------------------------
/week_2/char-yb/week2/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_2/char-yb/week2/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/week_2/char-yb/week2/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/week_2/char-yb/week2/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/week_2/char-yb/week2/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "week2"
2 |
--------------------------------------------------------------------------------
/week_2/char-yb/week2/src/main/kotlin/com/sipe/week2/AtomicExample.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week2
2 |
3 | import java.util.concurrent.atomic.AtomicInteger
4 |
5 | class AtomicExample {
6 | private val count = AtomicInteger(0) // AtomicInteger로 원자성을 보장하는 카운트 변수
7 |
8 | // count를 증가시키는 메서드
9 | fun increment() {
10 | count.incrementAndGet() // CAS를 통해 값을 원자적으로 증가
11 | }
12 |
13 | // 현재 count 값을 반환하는 메서드
14 | fun getCount(): Int {
15 | return count.get()
16 | }
17 | }
18 |
19 | fun main() {
20 | val example = AtomicExample()
21 |
22 | // 두 개의 스레드 생성, 각각 10000번씩 count를 증가시킴
23 | val thread1 = Thread {
24 | for (i in 0 until 10000) {
25 | example.increment()
26 | }
27 | }
28 |
29 | val thread2 = Thread {
30 | for (i in 0 until 10000) {
31 | example.increment()
32 | }
33 | }
34 |
35 | // 스레드 실행
36 | thread1.start()
37 | thread2.start()
38 |
39 | // 두 스레드가 작업을 마칠 때까지 대기
40 | thread1.join()
41 | thread2.join()
42 |
43 | // 최종 count 값 출력
44 | println("최종 카운트 값: ${example.getCount()}")
45 | }
--------------------------------------------------------------------------------
/week_2/char-yb/week2/src/main/kotlin/com/sipe/week2/Problem1.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week2
2 |
3 | import java.util.*
4 |
5 | class Problem1 {
6 |
7 | companion object {
8 | private var count: Int = 0
9 | @JvmStatic
10 | fun main(args: Array) {
11 | for (i in 0..99) {
12 | Thread {
13 | for (j in 0..999) println(count++)
14 | }.start()
15 | }
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/week_2/char-yb/week2/src/main/kotlin/com/sipe/week2/SynchronizedExample.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week2
2 |
3 | class SynchronizedExample {
4 | private var count = 0 // 공유 자원
5 |
6 | // synchronized 블록을 사용하여 동기화
7 | @Synchronized
8 | fun incrementCount() {
9 | count++
10 | }
11 |
12 | fun getCount(): Int {
13 | return count
14 | }
15 | }
16 |
17 | fun main() {
18 | val example = SynchronizedExample()
19 | val thread1 = Thread {
20 | for (i in 0 until 10000) {
21 | example.incrementCount()
22 | }
23 | }
24 | val thread2 = Thread {
25 | for (i in 0 until 10000) {
26 | example.incrementCount()
27 | }
28 | }
29 |
30 | // 두 스레드를 시작하고 종료 대기
31 | thread1.start()
32 | thread2.start()
33 | thread1.join()
34 | thread2.join()
35 |
36 | // 최종 카운트 값을 출력
37 | println("최종 카운트 값: ${example.getCount()}")
38 | }
--------------------------------------------------------------------------------
/week_2/char-yb/week2/src/main/kotlin/com/sipe/week2/VolatileExample.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week2
2 |
3 | class VolatileExample {
4 | @Volatile
5 | var count: Int = 0
6 |
7 | private fun incrementCount() {
8 | for (i in 0..9999) {
9 | count++
10 | }
11 | }
12 |
13 | companion object {
14 | @JvmStatic
15 | fun main(args: Array) {
16 | val instance = VolatileExample()
17 |
18 | // Create threads
19 | val thread1 = Thread { instance.incrementCount() }
20 | val thread2 = Thread { instance.incrementCount() }
21 |
22 | // Start threads
23 | thread1.start()
24 | thread2.start()
25 |
26 | // Wait for threads to finish
27 | thread1.join()
28 | thread2.join()
29 |
30 | // Print final count value
31 | println("Final count value: ${instance.count}")
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/week_2/char-yb/week2/src/main/kotlin/com/sipe/week2/Week2Application.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week2
2 |
3 | import org.springframework.boot.autoconfigure.SpringBootApplication
4 | import org.springframework.boot.runApplication
5 |
6 | @SpringBootApplication
7 | class Week2Application
8 |
9 | fun main(args: Array) {
10 | runApplication(*args)
11 | }
12 |
--------------------------------------------------------------------------------
/week_2/char-yb/week2/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | spring.application.name=week2
2 |
--------------------------------------------------------------------------------
/week_2/char-yb/week2/src/test/kotlin/com/sipe/week2/Week2ApplicationTests.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week2
2 |
3 | import org.junit.jupiter.api.Test
4 | import org.springframework.boot.test.context.SpringBootTest
5 |
6 | @SpringBootTest
7 | class Week2ApplicationTests {
8 |
9 | @Test
10 | fun contextLoads() {
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/week_2/char-yb/week_2_1.md:
--------------------------------------------------------------------------------
1 | ## 컨텍스트 스위치 비용 (하드웨어적 관점)
2 |
3 | #### 멀티 프로세싱 및 멀티 스레딩
4 |
5 | **멀티 프로세싱**
6 | 멀티 프로세스는 여러 개의 독립적인 CPU를 사용해 여러 프로세스를 병렬적으로 수행하는 것입니다.
7 | 이는 각 프로세스가 서로 영향을 주지 않으므로 안정성이 높지만, 프로세스 간 통신(IPC)을 위한 추가적인 오버헤드가 발생합니다.
8 |
9 | 멀티 프로세스의 장점은 위와 같이 각 프로세스가 독립된 메모리 공간을 가지기 때문에 하나의 프로세스가 실패해도 다른 프로세스에 영향을 주지 않는다는 점입니다.
10 |
11 | 단점으로는 프로세스 간 통신 비용이 높고, 메모리 사용량이 많다는 점을 들 수 있습니다. 각 프로세스가 독립된 메모리 공간을 가지므로, 같은 데이터를 여러 프로세스에서 사용할 경우 중복 저장되어 메모리 사용량이 증가할 수도 있습니다.
12 |
13 | 예를 들어, 웹 서버와 데이터베이스 서버를 별도의 프로세스로 실행하여, 하나의 서버에 문제가 발생해도 다른 서버는 정상적으로 작동할 수 있습니다.
14 |
15 | **멀티 스레딩**
16 | 멀티 스레드는 하나의 프로세스 내에서 여러 스레드가 메모리를 공유하며 동시에 실행되는 방식입니다. 이는 프로세스 생성 비용보다 스레드 생성 비용이 훨씬 낮으며, 스레드 간 데이터 공유가 용이합니다.
17 |
18 | 멀티스레딩은 CPU의 사용률을 극대화하고, I/O 작업이 블로킹되는 동안 다른 스레드가 CPU를 사용할 수 있게 하기 때문입니다. 또한, 멀티스레딩은 프로그램의 구조를 단순화할 수 있으며, 사용자 인터페이스와 같은 비동기 작업을 용이하게 합니다. 이처럼 컴퓨터의 여러 코어 자원을 효율적으로 활용할 수 있어 대규모 연산이나 I/O 작업에서 큰 장점을 가질 수 있습니다
19 |
20 | 또 다른 장점으로는 메모리 공유로 인한 효율성입니다. 왜냐하면 스레드 간 데이터를 공유할 수 있으므로, 데이터 복사 비용이 줄어들고, 통신 비용이 낮아집니다. 각 요청을 별도의 스레드에서 처리하여 처리 속도를 높일 수 있습니다.
21 |
22 | 단점으로는 스레드 간의 동기화 문제가 발생할 수 있다는 점입니다. 공유된 메모리에 여러 스레드가 동시에 접근할 경우, 한없이 기다리는 데드락이나 레이스 컨디션에 빠지게됩니다. 이에 대한 적절한 동기화 기법으로 데이터의 일관성을 유지하기 위한 추가적인 작업이 필요합니다. 그외 디버깅과 오버헤드에 단점이 존재.
23 |
24 | ---
25 | ### 컨텍스트 스위치 이해와 비용
26 |
27 | CPU가 어떤 프로세스를 실행하고 있는 상태에서 인터럽트에 의해 다음 우선 순위를 가진 프로세스가 실행되어야 할 때 기존의 프로세스 정보들은 PCB에 저장하고 다음 프로세스의 정보를 PCB에서 가져와 교체하는 작업을 컨텍스트 스위칭이라 합니다. 이러한 컨텍스트 스위칭을 통해 우리는 멀티 프로세싱, 멀티 스레딩 운영이 가능합니다.
28 |
29 | **컨텍스트 스위치 발생 시점**
30 |
31 | - 주어진 time slice(=quantum)를 다 사용했을 때 (멀티태스킹 시스템),
32 | - I/O 작업을 해야할 때
33 | - 다른 리소스를 기다려야 할 때 (선점/비선점)
34 | - interrupt가 걸렸을 때
35 |
36 | **컨텍스트 스위치 동작 과정**
37 |
38 | 1. 현재 작업의 상태 저장: CPU가 현재 작업(프로세스 또는 스레드)을 중단하기 전에, 그 작업의 모든 상태(프로그램 카운터, 레지스터 값 등)를 저장합니다. 이 정보는 보통 PCB 또는 TCB라는 메모리 구조에 저장됩니다.
39 | 2. 다음 작업의 상태 복원: CPU는 대기 중인 다른 작업의 상태를 PCB나 TCB에서 불러와 복원합니다. 이때 CPU는 이전 작업이 어디에서 중단되었는지, 어떤 값을 가지고 있었는지를 알 수 있습니다.
40 | 3. 새로운 작업 실행: CPU는 복원된 상태에서 다음 작업을 이어서 실행합니다. 이 작업이 끝나거나 일정 시간 동안 실행된 후, 다시 다른 작업으로 전환됩니다.
41 |
42 | **컨텍스트 스위치 비용**
43 | 컨텍스트 스위칭에는 오버헤드(Overhead)가 발생합니다. 즉, CPU가 작업을 전환할 때 상태를 저장하고 복원하는 과정은 일정한 시간이 소요되며, 이는 CPU 자원을 소모하게 됩니다. 컨텍스트 스위칭이 빈번하게 일어나면 CPU가 실제 작업을 처리하는 시간보다 오히려 스위칭에 소요되는 시간이 많아질 수 있습니다.
44 |
45 | **비용이 발생하는 이유는?**
46 |
47 | 1. 상태 저장과 복원: 현재 작업의 모든 상태를 저장하고 다음 작업의 상태를 복원하는 데 시간이 소요됩니다.
48 | 2. 캐시 미스(Cache Miss): 새로운 작업으로 전환할 때, CPU 캐시가 새로운 작업에 맞게 다시 로드되어야 할 수 있습니다. 이는 캐시 미스를 발생시켜 성능 저하를 유발할 수 있습니다.
49 | 3. TLB 플러시(TLB Flush): 컨텍스트 스위칭 시에 페이지 테이블이 새롭게 설정되어야 하므로, TLB(Translation Lookaside Buffer)라는 캐시 메모리가 무효화될 수 있습니다. (스레드가 아닌 프로세스 한정)
50 |
51 | ---
52 | ### 컨텍스트 스위치 비용 절감 방법
53 |
54 | - **스케줄링**: 특정 스레드를 특정 코어에 고정시켜 캐시의 재사용률을 높이는 기법으로, 하드웨어적으로 캐시 및 TLB 히트율을 높일 수 있습니다.
55 |
56 | - **스레드 풀(Thread Pool) 사용**: 스레드 수 조정 및 효율적 관리로 새로운 스레드를 계속 생성하는 대신, 일정한 수의 스레드를 미리 만들어서 재사용하면 스위칭 빈도를 줄일 수 있습니다.
57 |
58 | - **락(lock) 사용 최소화**: 불필요한 락 사용을 줄여 스레드 간 경쟁을 줄이고, 컨텍스트 스위칭 빈도를 줄일 수 있습니다.
59 |
60 | - **비동기 프로그래밍**: I/O 작업을 기다리는 동안 CPU가 빈번한 컨텍스트 스위칭을 발생시키지 않도록 비동기 방식으로 작업을 처리합니다.
61 |
62 | - **코루틴 활용**: 스레드 단위의 동시성 대신 경량화된 코루틴을 활용하면, 스레드 수준에서 발생하는 컨텍스트 스위치를 줄일 수 있습니다.
63 |
64 | ---
65 | ### Kotlin 코루틴을 활용한 컨텍스트 스위치 비용 절감
66 |
67 | Kotlin의 코루틴은 스레드의 물리적 전환 없이 논리적인 작업 단위 전환을 지원하여 하드웨어적 관점에서도 효율적입니다.
68 |
69 | - **경량화된 실행**: 코루틴은 스레드와 달리 가벼운 작업 단위로, 메모리와 CPU 레지스터 값을 저장 및 복원하는 비용을 줄일 수 있습니다.
70 | - **스레드 간 전환 없이 작업 수행**: 코루틴은 단일 스레드에서 다수의 작업을 효율적으로 처리하므로, 캐시 및 TLB 플러시와 같은 하드웨어적 비용을 감소시킵니다.
71 | - **비동기적 작업 관리**: 코루틴은 동시성 작업을 논블로킹 방식으로 관리하므로, 블로킹으로 인한 스레드 스케줄링 및 컨텍스트 스위치를 줄이는 효과가 있습니다.
72 |
73 | ---
74 | ### Kotlin 코루틴 예제
75 |
76 | 아래는 코루틴을 사용하여 여러 작업을 효율적으로 실행하는 예제입니다. 이 예제는 두 개의 작업을 동시에 실행하며, 하드웨어 레벨의 스레드 전환 없이 동작하는 것을 보여줍니다.
77 |
78 | ```kotlin
79 | import kotlinx.coroutines.*
80 |
81 | fun main() = runBlocking {
82 | println("시작 - ${Thread.currentThread().name}")
83 |
84 | // launch를 통해 코루틴을 시작합니다.
85 | val job1 = launch {
86 | task("Task1", 1000)
87 | }
88 |
89 | val job2 = launch {
90 | task("Task2", 1500)
91 | }
92 |
93 | // 모든 코루틴이 끝날 때까지 기다립니다.
94 | job1.join()
95 | job2.join()
96 |
97 | println("완료 - ${Thread.currentThread().name}")
98 | }
99 |
100 | suspend fun task(name: String, timeMillis: Long) {
101 | println("$name 시작 - ${Thread.currentThread().name}")
102 | delay(timeMillis) // 비동기적으로 지연시킵니다.
103 | println("$name 완료 - ${Thread.currentThread().name}")
104 | }
105 | ```
106 |
107 | 이 코드에서는 `runBlocking`을 통해 메인 스레드에서 코루틴을 실행하고, `launch`와 `delay`를 통해 하드웨어적인 스레드 전환 없이 두 개의 작업을 비동기적으로 수행합니다. delay를 사용하는 동안 다른 코루틴이 실행될 수 있어 스레드 전환 없이 비동기 처리가 가능합니다.
108 |
109 | ---
110 | ### 하드웨어 관점에서 본 코루틴의 장점
111 |
112 | - **캐시 효율성**: 단일 스레드 내에서 여러 코루틴이 작동하므로, 캐시 데이터를 유지하며 작업을 처리할 수 있습니다.
113 | - **낮은 메모리 및 CPU 리소스 소모**: 레지스터나 메모리 복원이 필요하지 않으므로, 메모리 접근 횟수가 줄어들고, CPU 자원을 절약할 수 있습니다.
114 |
115 | Kotlin의 코루틴을 활용하여 스레드 단위의 전환 비용을 절감하고, 하드웨어 자원을 보다 효율적으로 사용할 수 있습니다. 이를 통해 고성능이 요구되는 애플리케이션에서도 보다 효율적인 동시성 처리를 기대할 수 있습니다.
116 |
--------------------------------------------------------------------------------
/week_2/char-yb/week_2_3.md:
--------------------------------------------------------------------------------
1 | ## Tomcat 네트워크 요청을 받아서, 스레드를 할당받고, 이게 스프링까지 넘어와서 어떤 식으로 스레드가 처리되는지?
2 |
3 | Tomcat은 5 버전 이후로 NIO (Non-blocking I/O) Connector를 지원하기 시작했습니다. NIO Connector는 네트워크 요청을 비동기적으로 처리하기 위한 구조를 제공하여, 동시에 다수의 클라이언트 요청을 효율적으로 처리할 수 있게 합니다. 이 과정에서 Selector를 사용하여 하나의 스레드가 여러 소켓 채널을 모니터링하고, 필요한 작업이 발생할 때만 스레드를 할당합니다.
4 |
5 | 
6 | 해당 과정은 Tomcat 5 버전 이후 Nio Connector 과정입니다.
7 |
8 | **1. 클라이언트 요청 수신 및 Tomcat에서의 처리**
9 | - 클라이언트가 HTTP 요청을 보내면, Tomcat 서버의 커넥터(NIO)가 요청을 수신합니다.
10 | - 이 커넥터는 요청을 수신할 때 스레드 풀(Thread Pool)을 통해 스레드를 할당받아 요청을 처리할 준비를 합니다.
11 | - 기본적으로 Tomcat은 Http11NioProtocol을 사용하여 NIO (Non-blocking I/O) 방식으로 요청을 수신하며, 네트워크 연결을 효율적으로 관리하기 위해 Connector와 Acceptor 스레드를 사용합니다. (Http11NioProtocol은 Non-blocking을 위해 HTTP/1.1을 지원하여 NioEndPoint, Acceptor, Poller를 초기화)
12 | - 요청이 수신되면, 스레드 풀에서 대기 중인 Worker 스레드 중에서 Executor라는 스레드 풀을 사용해 하나를 할당하여 요청을 처리합니다.
13 | - Executor의 기본 스레드 수, 최대 스레드 수, 대기 큐 사이즈 등의 설정에 따라 스레드가 관리됩니다.
14 |
15 | **2. Acceptor Thread가 요청 수락**
16 | - Acceptor Thread는 클라이언트로부터 들어오는 요청을 수락하는 역할을 합니다. 이 스레드는 서버 소켓을 모니터링하여 새로운 연결 요청이 들어오면 해당 요청을 처리할 준비를 합니다.
17 | - 새로운 연결이 감지되면, Acceptor는 이 연결을 Poller에 전달합니다.
18 |
19 | **3. Poller가 소켓 채널을 모니터링**
20 | - Poller는 I/O 이벤트가 발생한 소켓 채널을 모니터링하는 스레드입니다.
21 | - Poller는 여러 소켓 채널을 Selector를 사용하여 관리하며, I/O 이벤트가 발생할 때만 작업을 처리하여 효율성을 높입니다. Poller는 특정 이벤트가 발생하면 이를 Worker 스레드에 전달하여 실제 작업을 수행하게 합니다.
22 |
23 | **4. Worker 스레드 할당**
24 | - Worker 스레드는 실제로 클라이언트의 데이터를 읽어와 비동기 I/O 작업을 수행하여 Http11ConnectionHandler로 전달합니다.
25 | - 만약 다수의 요청이 동시에 들어와 기존의 Worker 스레드가 부족할 경우, Tomcat은 설정된 maxThreads 속성 값까지 Worker 스레드를 추가로 생성하여 요청을 처리할 수 있도록 합니다.
26 |
27 | **5. Http11ConnectionHandler와 요청 처리**
28 | - Http11ConnectionHandler는 HTTP 연결을 관리하며, 들어온 요청을 처리할 준비를 합니다.
29 | - 이를 통해 HTTP 헤더와 본문을 읽고 HTTP 요청이 적절하게 HTTPServletRequest와 HTTPServletResponse 객체로 래핑하여 Spring 컨텍스트로 전달합니다.
30 |
31 | ---
32 | Spring 프레임워크로 요청 전달
33 | **1. Spring으로 요청 전달 및 DispatcherServlet 처리**
34 | - Tomcat의 Worker 스레드는 요청을 서블릿 필터 체인과 DispatcherServlet을 통해 Spring으로 전달합니다.
35 | - Spring에서는 요청을 DispatcherServlet이 받아들이며, 이 DispatcherServlet은 중앙 제어 역할을 수행합니다.
36 | - DispatcherServlet은 요청의 URL 및 HTTP 메서드 (GET, POST 등)를 기반으로 적절한 컨트롤러를 찾아 실행합니다.
37 |
38 | **2. 컨트롤러와 서비스 계층에서의 스레드 처리**
39 |
40 | - Spring의 컨트롤러는 요청을 처리하는 진입점이며, 할당된 Worker 스레드가 그대로 컨트롤러 메서드까지 전달됩니다.
41 | - 컨트롤러는 요청을 처리하기 위해 서비스 계층을 호출할 수 있으며, 서비스 계층에서도 동일한 스레드에서 로직이 수행됩니다.
42 | - 만약 비동기 처리가 필요한 경우에는 @Async 어노테이션을 사용하거나 CompletableFuture 등 비동기 처리를 위한 별도 스레드를 사용할 수 있습니다. 이 경우, Spring의 @EnableAsync와 별도의 스레드 풀을 통해 추가적인 스레드를 생성하여 비동기 처리를 수행합니다.
43 |
44 | **3. 비동기 처리 및 스레드 풀 (선택 사항)**
45 |
46 | - 만약 비동기 처리가 필요한 로직이 있거나, @Async 어노테이션이 적용된 메서드를 호출하게 된다면, Spring은 TaskExecutor를 사용해 별도의 스레드 풀에서 해당 메서드를 실행합니다.
47 | - 기본적으로 SimpleAsyncTaskExecutor를 사용하며, 커스터마이징이 가능해 특정 스레드 풀을 정의할 수 있습니다.
48 | - 이 비동기 스레드는 기존 요청 스레드와 분리되어 백그라운드에서 로직을 수행합니다. 요청과 관계없는 스레드는 비동기 처리 후 결과를 반환하지 않고, 응답이 필요할 경우 DeferredResult나 CompletableFuture를 통해 클라이언트에 응답합니다.
49 |
50 | **4. 응답 및 스레드 반환**
51 |
52 | - 비동기 작업이 없는 경우, Spring은 컨트롤러에서 최종적으로 HTTP 응답 데이터를 생성하고 DispatcherServlet으로 응답을 반환합니다.
53 | - DispatcherServlet은 이 응답을 Tomcat으로 다시 전달하며, Tomcat의 Worker 스레드는 응답을 클라이언트로 전송하고, 작업이 완료되면 스레드를 스레드 풀로 반환합니다.
54 | - 이 스레드는 재사용되며, 다음 요청을 처리하는 데 사용될 수 있습니다.
55 |
56 | #### 요약된 흐름
57 | 1. Tomcat의 NIO Connector가 요청을 수신하고 Acceptor, Poller, Worker 스레드가 이를 순차적으로 처리.
58 | 2. Spring DispatcherServlet이 요청을 받아 컨트롤러로 전달.
59 | 3. 컨트롤러와 서비스 계층에서 동일한 스레드로 로직 처리.
60 | 4. 비동기 작업이 필요한 경우, Spring의 @Async를 통해 별도 스레드에서 비동기 로직 처리.
61 | 5. DispatcherServlet이 응답을 Tomcat으로 반환.
62 | 6. Tomcat의 Worker 스레드가 응답을 전송하고 스레드 풀로 반환.
63 |
--------------------------------------------------------------------------------
/week_2/jaeyeong/res/image 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_2/jaeyeong/res/image 1.png
--------------------------------------------------------------------------------
/week_2/jaeyeong/res/image 2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_2/jaeyeong/res/image 2.png
--------------------------------------------------------------------------------
/week_2/jaeyeong/res/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_2/jaeyeong/res/image.png
--------------------------------------------------------------------------------
/week_2/jaeyeong/week_2.md:
--------------------------------------------------------------------------------
1 | # week 2
2 |
3 | ## context switch 란?
4 |
5 | 1주차 문서에서 설명했던 것 처럼 thread 는 본질적으로 process 와 같고 OS 가 스케줄링하는 단위는 process 아니라 thread 다. 그래서 thread 도 process 와 동일하게 각자의 program counter 와 register 값들을 갖고 있고, 여러 스레드가 하나의 core 에서 실행되어야 할 경우 현재 실행중인 스레드에 해당하는 값들로 교체가 필요한데 이 과정을 context switch 라 부른다.
6 |
7 | 
8 |
9 | ### context switch 의 비용이란게 정확히 뭔가?
10 |
11 | - 하나의 cpu 코어는 하나의 thread 가 수행하는 동작을 다 끝내고 다른 thread 의 동작을 수행하는 식으로 정직하게 동작하지 않는다.
12 | - 한 thread 의 동작을 하던 중간에 갑자기 끊고 다른 thread 의 동작을 하는 식으로 동시성을 구현했다.
13 | - 그래서 어떤 thread 가 동작을 어디 까지했는지 기억해야하는데 이걸 thread context 라 부르고, 이 값중 cpu 가 당장 동작함에 필요한 값들을 cpu 내에 올렸다 나중에 다시 메모리로 내리는 과정이 필요한데 이걸 context switch 라 부른다.
14 |
15 | 
16 |
17 | - context switch 비용의 구성 요소들
18 | - 현재 실행 상태를 올리고 내리는 비용
19 | - 여러가지 register
20 | - Program Counter - 다음에 실행할 명령어의 주소
21 | - 커널 스택 포인터 - 커널 스택의 위치
22 | - 페이지 테이블 변경 (thread 간의 context 전환에서는 대부분 발생하지 않겠지만)
23 | - 캐시 초기화
24 | - thread 간의 전환이라면 일부러 cpu 캐시를 flush 하지는 않을 수 있으나
25 | - 캐시 미스는 확실히 많이 발생할 것 → 사실상 flush 된 꼴
26 | - 파이프라인 flush
27 | - 파이프라인은 확실히 flush 될 것
28 |
29 | ## java.util.concurrent 가족들
30 |
31 | java.util.concurrent 패키지에는 동시성과 관련한 정말 유용한 도구들이 많은데 이 중 가장 많이 쓰이는 몇가지만 살펴보자.
32 |
33 | ### ExecutorService
34 |
35 | - 스레드 풀을 통해 작업을 비동기적으로 실행할 수 있게 해주는 인터페이스
36 | - 직접 스레드를 생성하고 관리하는 복잡성을 줄여준다!
37 | - 앞서 말했듯 인터페이스이기 때문에 여러가지 구현체가 있다.
38 | - **ThreadPoolExecutor:** 스레드 풀을 구성할 수 있는 기본 구현체
39 | - **ScheduledThreadPoolExecutor:** 스케줄링 기능을 제공
40 | - **ForkJoinPool:** 대용량 작업을 작은 단위로 분할하여 병렬 처리에 하라고 만든 구현체
41 | - 기본적인 사용 예시
42 |
43 | ```java
44 | ExecutorService executor = Executors.newFixedThreadPool(5);
45 |
46 | Future future = executor.submit(new Callable() {
47 | public Integer call() {
48 | // 뭔가 계산
49 | return 작업결과;
50 | }
51 | });
52 |
53 | try {
54 | Integer result = future.get(); // 작업 완료까지 대기
55 | System.out.println("결과: " + result);
56 | } catch (InterruptedException | ExecutionException e) {
57 | e.printStackTrace();
58 | } finally {
59 | executor.shutdown();
60 | }
61 | ```
62 |
63 |
64 | ### Future & CompletableFuture
65 |
66 | - 비동기 작업의 결과를 나타내는 인터페이스인 Future
67 | - Future 를 확장하여 비동기 작업의 완료 후 동작(콜백)을 정의하거나 여러 비동기 작업을 연결할 수 있게 만든 CompletableFuture
68 | - Future 의 get() 메소드는 작업이 완료될 때 까지 기다려야하고 별도의 콜백 메커니즘이 없다.
69 | - CompletableFuture 의 사용 예시 몇가지
70 |
71 | ```java
72 | CompletableFuture future1 = CompletableFuture.supplyAsync(() -> "작업 1 결과");
73 | CompletableFuture future2 = CompletableFuture.supplyAsync(() -> "작업 2 결과");
74 |
75 | CompletableFuture combinedFuture = future1.thenCombine(future2, (result1, result2) -> {
76 | return result1 + " & " + result2;
77 | });
78 |
79 | combinedFuture.thenAccept(result -> System.out.println("조합된 결과: " + result));
80 | // 조합된 결과: 작업 1 결과 & 작업 2 결과
81 | ```
82 |
83 | - ExecutorService 를 명시적으로 지정하는 것도 가능하다!
84 | - 기본적으로 CompletableFuture는 ForkJoinPool 의 공용 스레드 풀을 사용
85 |
86 | 
87 |
88 | - 하지만 아래처럼 별도 ExecutorService 를 주입 가능
89 |
90 | ```java
91 | ExecutorService executor = Executors.newFixedThreadPool(3);
92 |
93 | CompletableFuture.supplyAsync(() -> {
94 | // 비동기 작업
95 | return "결과";
96 | }, executor).thenAccept(result -> System.out.println("결과: " + result));
97 |
98 | executor.shutdown();
99 | ```
100 |
101 | ### ConcurrentHashMap
102 |
103 | - 스레드 안전한 해시 맵 구현체
104 | - 스레드 안전하다는 말은 곧
105 | - 여러 스레드가 동시에 맵에 접근하여 데이터를 읽고 쓰는 상황에서도 데이터 무결성을 유지할 수 있다는 말
106 | - 스레드 안전하게 구현되었다면 성능 저하가 있지 않나?!
107 | - (문서에 따르면) 동기화로 인한 성능 저하를 최소화했다고 한다.
108 | - ConcurrentHashMap 은 스레드 안전을 어떻게 구현했길래 성능 저하를 최소화했다는걸까?
109 | - 버킷 수준에서 락을 건다 → 성능 향상
110 | - 필요한 버킷에만 락을 걸고, 다른 버킷은 독립적으로 내버려 둔다.
111 | - 락도 사실 정말 필요할 때만 걸고 웬만하면 CAS 연산으로 락 없이 진행한다.
112 | - 동작 방식
113 | - 키의 해시 값을 계산하여 버킷 인덱스를 결정
114 | - 버킷이 비어있으면 - CAS 연산을 사용하여 새로운 노드로 변경
115 | - 버킷이 안비어있으면
116 | - 락을 획득하고 연결 리스트 또는 트리를 탐색하여 키가 이미 존재하는지 확인
117 | - 키가 존재하면 값을 업데이트
118 | - 키가 없으면 새로운 노드를 연결
119 | - ConcurrentHashMap 에서 값을 집어넣는 메소드를 아주 간략히 요약하면 아래와 같다.
120 |
121 | ```java
122 | public V put(K key, V value) {
123 | return putVal(key, value, false);
124 | }
125 |
126 | final V putVal(K key, V value, boolean onlyIfAbsent) {
127 | Node[] tab = table;
128 | Node f = tabAt(tab, i); // volatile 읽기
129 |
130 | if (f == null) {
131 | // 비어있는 버킷에 새 노드 생성
132 | if (casTabAt(tab, i, null,
133 | new Node(hash, key, value, null)))
134 | break;
135 | } else {
136 | // 기존 노드가 있는 경우
137 | synchronized (f) {
138 | // 해당 노드만 잠금
139 | if (tabAt(tab, i) == f) {
140 | // 노드 갱신 작업
141 | }
142 | }
143 | }
144 | }
145 | ```
--------------------------------------------------------------------------------
/week_2/junseo/res/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_2/junseo/res/image.png
--------------------------------------------------------------------------------
/week_2/junseo/res/image1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_2/junseo/res/image1.png
--------------------------------------------------------------------------------
/week_2/junseo/res/image10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_2/junseo/res/image10.png
--------------------------------------------------------------------------------
/week_2/junseo/res/image11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_2/junseo/res/image11.png
--------------------------------------------------------------------------------
/week_2/junseo/res/image2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_2/junseo/res/image2.png
--------------------------------------------------------------------------------
/week_2/junseo/res/image3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_2/junseo/res/image3.png
--------------------------------------------------------------------------------
/week_2/junseo/res/image4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_2/junseo/res/image4.png
--------------------------------------------------------------------------------
/week_2/junseo/res/image5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_2/junseo/res/image5.png
--------------------------------------------------------------------------------
/week_2/junseo/res/image6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_2/junseo/res/image6.png
--------------------------------------------------------------------------------
/week_2/junseo/res/image7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_2/junseo/res/image7.png
--------------------------------------------------------------------------------
/week_2/junseo/res/image8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_2/junseo/res/image8.png
--------------------------------------------------------------------------------
/week_2/junseo/res/image9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_2/junseo/res/image9.png
--------------------------------------------------------------------------------
/week_2/positive/2주차.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_2/positive/2주차.pdf
--------------------------------------------------------------------------------
/week_3/char-yb/images/async_thread.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_3/char-yb/images/async_thread.png
--------------------------------------------------------------------------------
/week_3/char-yb/images/coroutine2-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_3/char-yb/images/coroutine2-1.png
--------------------------------------------------------------------------------
/week_3/char-yb/images/coroutine2-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_3/char-yb/images/coroutine2-2.png
--------------------------------------------------------------------------------
/week_3/char-yb/images/coroutine_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_3/char-yb/images/coroutine_example.png
--------------------------------------------------------------------------------
/week_3/char-yb/images/hierarchical_routing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_3/char-yb/images/hierarchical_routing.png
--------------------------------------------------------------------------------
/week_3/char-yb/week3/.gitattributes:
--------------------------------------------------------------------------------
1 | /gradlew text eol=lf
2 | *.bat text eol=crlf
3 | *.jar binary
4 |
--------------------------------------------------------------------------------
/week_3/char-yb/week3/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | .gradle
3 | build/
4 | !gradle/wrapper/gradle-wrapper.jar
5 | !**/src/main/**/build/
6 | !**/src/test/**/build/
7 |
8 | ### STS ###
9 | .apt_generated
10 | .classpath
11 | .factorypath
12 | .project
13 | .settings
14 | .springBeans
15 | .sts4-cache
16 | bin/
17 | !**/src/main/**/bin/
18 | !**/src/test/**/bin/
19 |
20 | ### IntelliJ IDEA ###
21 | .idea
22 | *.iws
23 | *.iml
24 | *.ipr
25 | out/
26 | !**/src/main/**/out/
27 | !**/src/test/**/out/
28 |
29 | ### NetBeans ###
30 | /nbproject/private/
31 | /nbbuild/
32 | /dist/
33 | /nbdist/
34 | /.nb-gradle/
35 |
36 | ### VS Code ###
37 | .vscode/
38 |
39 | ### Kotlin ###
40 | .kotlin
41 |
--------------------------------------------------------------------------------
/week_3/char-yb/week3/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("jvm") version "2.0.0"
3 | kotlin("plugin.spring") version "1.9.25"
4 | id("org.springframework.boot") version "3.3.5"
5 | id("io.spring.dependency-management") version "1.1.6"
6 | }
7 |
8 | group = "com.sipe"
9 | version = "0.0.1-SNAPSHOT"
10 |
11 | java {
12 | toolchain {
13 | languageVersion = JavaLanguageVersion.of(21)
14 | }
15 | }
16 |
17 | repositories {
18 | mavenCentral()
19 | }
20 |
21 | dependencies {
22 | implementation("org.springframework.boot:spring-boot-starter")
23 | implementation("org.springframework.boot:spring-boot-starter-web")
24 | implementation("org.jetbrains.kotlin:kotlin-reflect")
25 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
26 |
27 | testImplementation("org.springframework.boot:spring-boot-starter-test")
28 | testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
29 | testRuntimeOnly("org.junit.platform:junit-platform-launcher")
30 | }
31 |
32 | kotlin {
33 | compilerOptions {
34 | freeCompilerArgs.addAll("-Xjsr305=strict")
35 | }
36 | }
37 |
38 | tasks.withType {
39 | useJUnitPlatform()
40 | }
41 |
--------------------------------------------------------------------------------
/week_3/char-yb/week3/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_3/char-yb/week3/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/week_3/char-yb/week3/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/week_3/char-yb/week3/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/week_3/char-yb/week3/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "week3"
2 |
--------------------------------------------------------------------------------
/week_3/char-yb/week3/src/main/kotlin/com/sipe/week3/CoRoutineExample1.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week3
2 |
3 | import kotlinx.coroutines.coroutineScope
4 | import kotlinx.coroutines.delay
5 | import kotlinx.coroutines.launch
6 | import kotlinx.coroutines.runBlocking
7 |
8 | class CoRoutineExample1 {
9 | }
10 |
11 | fun main() = runBlocking { // this: CoroutineScope
12 | helloWorld()
13 | }
14 |
15 | suspend fun helloWorld() = coroutineScope {
16 | launch { // launch a new coroutine and continue
17 | delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
18 | println("World!") // print after delay
19 | }
20 | println("Hello") // main coroutine continues while a previous one is delayed
21 | }
--------------------------------------------------------------------------------
/week_3/char-yb/week3/src/main/kotlin/com/sipe/week3/CoRoutineExample2.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week3
2 |
3 | import kotlinx.coroutines.delay
4 | import kotlinx.coroutines.launch
5 | import kotlinx.coroutines.runBlocking
6 |
7 | class CoRoutineExample2 {
8 | }
9 |
10 | fun main() {
11 | runBlocking {
12 | val lineUp = launch {
13 | coroutineLinedUp()
14 | }
15 |
16 | val playMusicWithLinedUp = launch {
17 | coroutinePlayMusic()
18 | }
19 |
20 | lineUp.join()
21 | playMusicWithLinedUp.cancel()
22 | coroutineTicketing()
23 |
24 | val waitingBus = launch {
25 | coroutineWaitingTheBus()
26 | }
27 |
28 | val playMusicWithWaitingBus = launch {
29 | coroutinePlayMusic()
30 | }
31 |
32 | waitingBus.join()
33 | playMusicWithWaitingBus.cancel()
34 | coroutineTakeTheBus()
35 | }
36 | }
37 |
38 | suspend fun coroutineLinedUp() {
39 | println("lined up")
40 | delay(2000)
41 | }
42 |
43 | fun coroutineTicketing() {
44 | println("ticketing")
45 | }
46 |
47 | suspend fun coroutineWaitingTheBus() {
48 | println("waiting the bus")
49 | delay(2000)
50 | }
51 |
52 | fun coroutineTakeTheBus() {
53 | println("take the bus")
54 | }
55 |
56 | suspend fun coroutinePlayMusic() {
57 | println("play music")
58 | while(true) {
59 | println("listening..")
60 | delay(500)
61 | }
62 | }
--------------------------------------------------------------------------------
/week_3/char-yb/week3/src/main/kotlin/com/sipe/week3/ThreadExample.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week3
2 |
3 | class ThreadExample {
4 | }
5 |
6 | fun main() {
7 | asyncLinedUp {
8 | stopMusic()
9 | ticketing()
10 | asyncTakeTheBus {
11 | stopMusic()
12 | }
13 | asyncPlayMusic()
14 | }
15 |
16 | asyncPlayMusic()
17 |
18 | }
19 |
20 | fun asyncLinedUp(myTurn: () -> Unit) {
21 | Thread {
22 | println("lined up")
23 | Thread.sleep(2000)
24 | myTurn.invoke()
25 | }.start()
26 | }
27 |
28 | fun asyncTakeTheBus(onTime: () -> Unit) {
29 | Thread {
30 | println("waiting the bus")
31 | Thread.sleep(2000)
32 | onTime.invoke()
33 | println("take the bus")
34 | }.start()
35 | }
36 |
37 | var playingMusic = false
38 |
39 | fun asyncPlayMusic() {
40 | Thread {
41 | println("play music")
42 | playingMusic = true
43 | while(playingMusic) {
44 | println("listening..")
45 | Thread.sleep(500)
46 | }
47 | }.start()
48 | }
49 |
50 | fun stopMusic() {
51 | playingMusic = false
52 | println("stop music")
53 | }
--------------------------------------------------------------------------------
/week_3/char-yb/week3/src/main/kotlin/com/sipe/week3/TicketingExample.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week3
2 |
3 | class TicketingExample {
4 | }
5 |
6 | fun main() {
7 | linedUp()
8 | ticketing()
9 | takeTheBus()
10 | }
11 |
12 | fun linedUp() {
13 | println("lined up")
14 | Thread.sleep(2000)
15 | }
16 |
17 | fun ticketing() {
18 | println("ticketing")
19 | }
20 |
21 | fun takeTheBus() {
22 | println("waiting the bus")
23 | Thread.sleep(2000)
24 | println("take the bus")
25 | }
--------------------------------------------------------------------------------
/week_3/char-yb/week3/src/main/kotlin/com/sipe/week3/Week3Application.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week3
2 |
3 | import org.springframework.boot.autoconfigure.SpringBootApplication
4 | import org.springframework.boot.runApplication
5 |
6 | @SpringBootApplication
7 | class Week3Application
8 |
9 | fun main(args: Array) {
10 | runApplication(*args)
11 | }
12 |
--------------------------------------------------------------------------------
/week_3/char-yb/week3/src/main/kotlin/com/sipe/week3/application/AsyncService.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week3.application
2 |
3 | import kotlinx.coroutines.Dispatchers
4 | import kotlinx.coroutines.async
5 | import kotlinx.coroutines.delay
6 | import kotlinx.coroutines.runBlocking
7 | import org.springframework.stereotype.Service
8 | import java.time.LocalDateTime
9 |
10 | @Service
11 | class AsyncService {
12 |
13 | fun executeAsyncTasks(): String = runBlocking {
14 | // 각 작업의 시작 시간 기록
15 | println("Coroutine tasks started at: ${LocalDateTime.now()}")
16 |
17 | // 3개의 I/O 작업을 동시에 실행
18 | val task1 = async(Dispatchers.IO) { ioOperation("Task 1") }
19 | val task2 = async(Dispatchers.IO) { ioOperation("Task 2") }
20 | val task3 = async(Dispatchers.IO) { ioOperation("Task 3") }
21 |
22 | // 모든 작업이 완료된 후 결과를 반환
23 | val result = "${task1.await()}, ${task2.await()}, ${task3.await()}"
24 |
25 | // 모든 작업이 완료된 시간 기록
26 | println("Coroutine tasks completed at: ${LocalDateTime.now()}")
27 |
28 | result
29 | }
30 |
31 | // I/O 작업 시뮬레이션 함수 (비동기적으로 1초 지연)
32 | private suspend fun ioOperation(taskName: String): String {
33 | println("$taskName started at: ${LocalDateTime.now()}")
34 | delay(100) // 실제 I/O 작업을 비동기적으로 처리하는 부분 (예: DB 조회, 외부 API 호출)
35 | println("$taskName completed at: ${LocalDateTime.now()}")
36 | return "$taskName completed"
37 | }
38 | }
--------------------------------------------------------------------------------
/week_3/char-yb/week3/src/main/kotlin/com/sipe/week3/application/ThreadService.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week3.application
2 |
3 | import org.springframework.stereotype.Service
4 | import java.time.LocalDateTime
5 | import java.util.concurrent.Callable
6 | import java.util.concurrent.Executors
7 | import java.util.concurrent.Future
8 |
9 | @Service
10 | class ThreadService {
11 |
12 | private val executor = Executors.newFixedThreadPool(3) // 3개의 스레드를 사용
13 |
14 | fun executeThreadTasks(): String {
15 | // 각 작업의 시작 시간 기록
16 | println("Thread tasks started at: ${LocalDateTime.now()}")
17 |
18 | // 3개의 I/O 작업을 동시에 실행
19 | val task1: Future = executor.submit(Callable { ioOperation("Task 1") })
20 | val task2: Future = executor.submit(Callable { ioOperation("Task 2") })
21 | val task3: Future = executor.submit(Callable { ioOperation("Task 3") })
22 |
23 | // 모든 작업이 완료된 후 결과를 반환
24 | val result = "${task1.get()}, ${task2.get()}, ${task3.get()}"
25 |
26 | // 모든 작업이 완료된 시간 기록
27 | println("Thread tasks completed at: ${LocalDateTime.now()}")
28 |
29 | return result
30 | }
31 |
32 | // I/O 작업 시뮬레이션 함수 (1초 지연)
33 | private fun ioOperation(taskName: String): String {
34 | println("$taskName started at: ${LocalDateTime.now()}")
35 | Thread.sleep(100) // 실제 I/O 작업을 스레드 기반으로 처리하는 부분 (예: DB 조회, 외부 API 호출)
36 | println("$taskName completed at: ${LocalDateTime.now()}")
37 | return "$taskName completed"
38 | }
39 | }
--------------------------------------------------------------------------------
/week_3/char-yb/week3/src/main/kotlin/com/sipe/week3/presentation/ExampleController.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week3.presentation
2 |
3 | import com.sipe.week3.application.AsyncService
4 | import com.sipe.week3.application.ThreadService
5 | import kotlinx.coroutines.runBlocking
6 | import org.springframework.web.bind.annotation.RestController
7 | import org.springframework.web.bind.annotation.GetMapping
8 | import org.springframework.web.bind.annotation.RequestMapping
9 |
10 | @RestController
11 | @RequestMapping("/api")
12 | class ExampleController(
13 | private val asyncService: AsyncService,
14 | private val threadService: ThreadService,
15 | ) {
16 |
17 | @GetMapping("/async")
18 | fun getAsyncResult(): String = runBlocking {
19 | // 코루틴 기반으로 비동기 작업을 동시에 실행하고 결과 반환
20 | asyncService.executeAsyncTasks()
21 | }
22 |
23 | @GetMapping("/thread")
24 | fun getThreadResult(): String {
25 | // 스레드 기반으로 비동기 작업을 동시에 실행하고 결과 반환
26 | return threadService.executeThreadTasks()
27 | }
28 | }
--------------------------------------------------------------------------------
/week_3/char-yb/week3/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | spring.application.name=week3
2 |
--------------------------------------------------------------------------------
/week_3/char-yb/week3/src/test/kotlin/com/sipe/week3/Week3ApplicationTests.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week3
2 |
3 | import org.junit.jupiter.api.Test
4 | import org.springframework.boot.test.context.SpringBootTest
5 |
6 | @SpringBootTest
7 | class Week3ApplicationTests {
8 |
9 | @Test
10 | fun contextLoads() {
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/week_3/char-yb/week3_1.md:
--------------------------------------------------------------------------------
1 | ## 코루틴 개념, 코루틴을 왜 쓰는가?
2 | - [코루틴 개념](https://en.wikipedia.org/wiki/Coroutine)
3 | - [코루틴 docs](https://kotlinlang.org/docs/coroutines-overview.html#documentation)
4 |
5 | ### 코루틴 개념
6 | 코루틴, 영어로 하면 Co-Routine입니다.
7 | Kotlin이니까 Ko-인줄 아실테지만, 그러니 Kotlin이 아닌 다른 언어에서도 코루틴이 제공되고 있습니다. Co는 협력, Routine은 간단히 함수라고 표현할 수 있습니다.
8 | 협력하는 함수라는 뜻으로, 프로그래밍에서 함수도 서로의 호출과 반환을 주고 받으며 협력을 합니다. Coroutine은 일종의 가벼운 스레드(Light-weight thread)로 동시성 작업을 간편하게 처리할 수 있게 해주는 역할을 수행하고 있습니다.
9 |
10 | 제공해주신 위키피디아 내용의 일부를 발췌해보았습니다.
11 |
12 | > Coroutines are computer program components that allow execution to be suspended and resumed, generalizing subroutines for cooperative multitasking.
13 |
14 | 번역이 어렵죠? `코루틴은 실행을 일시 중지하고 재개할 수 있는 컴퓨터 프로그램 구성 요소로, 협력적 멀티태스킹(비선점적 멀티태스킹)을 위해 서브루틴을 일반화합니다.` 라는 의미로 Ok, 일시 중지하고 재개하고 그러한 내부의 협력을 위한 루틴을 활용한 프로그래밍 기법인 듯합니다?
15 | 하나씩 더 알아보겠습니다.
16 |
17 | #### 비선점적 프로그래밍 (Non-Preemptive Programming)
18 | 컴퓨터구조에서 많이 들었던 개념인 것 같은데 비선점형은 하나의 태스크가 다른 태스크가 실행 중이어도 프로세서(CPU)를 차지할 수 있다. 반대로 선점형은 하나의 태스크가 다른 태스크가 실행 중이라면 프로세서(CPU)를 차지할 수 없다.
19 | 코루틴은 비선점형 멀티태스킹, 스레드는 선점형 멀티태스킹이다. 그러므로 코루틴은 병행성(=동시성)은 제공하고 병렬성은 제공하지 않는다.
20 |
21 | 병렬성(Parallel) vs 병행성(Concurrency)
22 | 병렬성은 물리적, 병행성은 논리적 개념에서 다루며, 병행성은 동시에 실행되는 것처럼 보이는 것.
23 | 병렬성은 실제로 동시에 작업이 처리가 되는 것.
24 |
25 | #### 루틴 (Routine)
26 | Routine은 하나의 Task 혹은 Function이라고 이해해도 될 거 같습니다. 보통 프로그램은 다양한 Routine들을 조합시켜 개발하는데. Routine은 Main Routine과 Sub Routine으로 나뉩니다. Main Routine이 Sub Routine을 호출하는 방식이며 Coroutine 또한 Routine의 한 종류이지만 다음과 같은 특징이 있습니다.
27 |
28 | Main-Sub 개념을 구분하지 않습니다. 그러므로 모든 Routine들이 서로를 호출할 수 있고, 그러니 병행성을 가지고 있습니다. 진입과 탈출이 자유로우며 Sub Routine은 return을 만나야만 탈출할 수 있습니다. (고민점: return이 없으면 정말 Routine 탈출이 안될까?)
29 |
30 | ---
31 | ### 코루틴을 왜 사용하는가?
32 |
33 | 일반적인 CRUD 요구사항을 기반으로 성공 케이스를 따라가 개발하는 것은 사실 쉬운 개발입니다. 하지만 이제 우리가 회사라는 집단 속에서 "느리다", "빨리 좀..." 이런 VOC와 QA 사항을 전달받으면 개발을 하면서 가장 머리를 싸매야 하는 순간들입니다. 성능 개선을 시도하는 방법 중 한 가지 케이스로 어떤 코드를 동시에 처리를 해야 하는지, 반대로 그러면 안되는지를 결정하는 순간일 것입니다. 여기서 결정한다고 하더라도 코드를 작성했을 때 동시에 처리를 하는 경우 순서에 따라 결과가 달라져 문제를 해결할 수 없는 경우가 다반사일수도 있습니다. 특히 Android에서는 UI를 그리는 작업, 데이터를 받아오는 작업을 동시에 수행하는 경우가 많기 때문에 이를 제어하기 위해서는 비동기 처리는 필수적이며 코루틴을 사용하는 예시가 될 수 있습니다.
34 |
35 | 서버 개발자 관점에서도 당연히 그리는 작업도 작업이지만, DTO로 가공하거나 어떠한 데이터는 배제해야 하는 filter를 사용하고 distinct를 통해 중복도 제거를 할 수 있겠죠? 혹은 데이터베이스에서 필요한 데이터를 조회하기 위한 DB Connection을 맺어야 하는 순간이 다반사라고 생각합니다.
36 |
37 | 이를 빠르게 응답하기 위한 기술 중 하나가 코루틴이라고 볼 수 있습니다.
38 | 지금부터 왜 사용을 해야하는 지 장점을 나열해보겠습니다.
39 |
40 |
41 | #### 장점
42 |
43 | 1. Routine간 협력을 통한 비선점적 멀티태스킹
44 | Coroutine을 사용하면 비동기로 Routine을 실행하고 일반적인 Sub Routine과 다르게 진입과 탈출이 자유로워 Routine간 협력을 통해 비선점적 멀티태스킹을 가능하게 합니다.
45 |
46 | 2. 동시성 프로그래밍 지원
47 | 동시성 프로그래밍이란 2개 이상의 프로세스가 동시에 작업을 하는 상태를 말하는데, Coroutine은 단일 코어에서 실행되어야 하기 때문에 각 Routine을 교차 배치합니다. 다중 Thread를 이용하게 되면 각 Thread 간 교체 시 Context Switching 비용이 발생합니다.(우리 이거 공부 많이했죠?) 하지만 Coroutine은 하나의 Thread 내에서 스케줄링이 가능하기 때문에 경량 쓰레드(Light-weight thread)라고도 불립니다.
48 |
49 | 3. 쉬운 비동기 처리
50 | Multi Thread와 비교했을 때 Thread 간 통신과 콜백 구조로 코드가 흐르지 않기 때문에 코드 흐름 파악이 쉽습니다.(가독성) 또한, 개발자가 직접 작업을 스케줄링하기 때문에 코드 작성이 간단하고 예상하지 못한 상황을 줄일 수 있습니다.
51 |
52 |
53 | ```kotlin
54 | fun main() = runBlocking { // this: CoroutineScope
55 | launch { // launch a new coroutine and continue
56 | delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
57 | println("World!") // print after delay
58 | }
59 | println("Hello") // main coroutine continues while a previous one is delayed
60 | }
61 | ```
62 |
63 | 위 코드는 코틀린(kotlinlang) 공식 문서 페이지에 참조되어 있는 간단한 예제입니다.
64 |
65 | launch는 코루틴 빌더 입니다. 나머지 코드와 동시에 새로운 코루틴을 시작하는데, 나머지 코드는 독립적으로 계속 작동합니다. 그래서 Hello가 먼저 출력되었습니다. 왜냐하면?
66 |
67 | delay는 특별한 일시 중단 함수 입니다. 특정 시간 동안 코루틴을 일시 중단합니다 . 코루틴을 일시 중단해도 기본 스레드는 차단 되지 않지만 다른 코루틴이 실행되고 코드에 기본 스레드를 사용할 수 있습니다.
68 |
69 | runBlocking 함수는 일반 루틴 세계와 코루틴 세계를 연결하는 함수입니다.
70 | runBlocking의 이름은 이를 실행하는 스레드(이 경우 메인 스레드)가 runBlocking { ... } 내부의 모든 코루틴이 실행을 완료할 때까지 호출 기간 동안 차단된다는 것을 의미합니다. 스레드(일반 루틴 세계)는 비용이 많이 드는 리소스이고 이를 차단하는 것은 비효율적이며 종종 바람직하지 않기 때문입니다.
71 |
72 | 결국 정리하면 CoroutineScope 안에서 1초 뒤 World!를 출력하는 코루틴이 만들어졌고 Hello를 출력하는 코드는 해당 코루틴과 별도로 Main Coroutine에 존재하므로 Hello 다음에 World!가 출력되게 됩니다.
73 |
74 |
75 | ### 참고 링크
76 | - https://mochaive.medium.com/coroutine-5119fda3bc65
77 | - https://kotlinlang.org/docs/coroutines-basics.html#your-first-coroutine
--------------------------------------------------------------------------------
/week_3/char-yb/week3_2.md:
--------------------------------------------------------------------------------
1 | ## 코루틴의 동작 원리, 스레드와의 차이
2 |
3 | ### 스레드와의 차이
4 |
5 | 아시다시피 스레드는 프로세스보다 작은 개념으로, 프로세스 내에 있는 여러 코드들의 작은 실행단위입니다.
6 |
7 | 
8 | 위처럼 보통 멀티 스레드 환경에서 한 프로세스 내에 여러 스레드를 가지고 있습니다.
9 |
10 | 여기서 간단히 정리해보면, 스레드는 프로세스가 있어야 하고, 스레드는 코드를 실행하며 스레드가 프로세스보다 작은 개념입니다.
11 | 코루틴은 단지 우리가 작성한 루틴으로, 코드의 종류 중 하나이기 때문에 코루틴 코드가 실행되려면 스레드가 있어야만 합니다. 그런데 코루틴은 중단되었다가 재개될 수 있기 때문에, 코루틴 코드의 앞부분은 1번 스레드에 배정되고, 뒷부분은 2번 스레드에 배정될 수 있습니다.
12 |
13 | 여기서 코루틴은 스레드보다 작은 개념(경량 스레드)이기에 **프로세스 > 스레드 > 코루틴** 으로 크기가 비교됩니다.
14 |
15 | ---
16 |
17 | 프로세스 내에 스레드는 독립적으로 가져가기에 특정 스레드가 다른 특정 프로세스에 옮겨갈 수는 없습니다.
18 | 코루틴 같은 경우는 코루틴이 직접 코드를 실행시키는 개념이 아니라 코루틴이 가지고 있는 코드를 스레드에 넘겨 실행하는 방식입니다.
19 |
20 | 일단 코루틴이라는 존재는 중단과 재개가 가능한 루틴이며, 우선 특정 코루틴 내에 코드1,2가 존재한다고 가정해보겠습니다.
21 |
22 | 두 개의 스레드1, 2가 존재하면 코루틴 내에 코드1이 시작되고 스레드1에서 실행됩니다.
23 | 이후 코루틴 내에 코드1이 끝나고 중단되는 지점이 있다면, 코드2가 스레드2에서 실행할 수 있도록 스레드 할당이 가능합니다.
24 |
25 | ---
26 |
27 | 그리고 코루틴과 스레드는 **Context Switching(문맥 교환)** 에서도 차이가 발생됩니다.
28 |
29 | 우선 프로세스의 Context Switching이 일어난다면 각각의 프로세스는 완전히 독립된 메모리를 가지고 있기 때문에, CPU 코어에서 특정 프로세스를 가져가 실행시킬 때 프로세스1의 메모리를 사용합니다. 여기서 Context Switching이 발생된다면 두 번째 프로세스 메모리로 완전히 갈아끼워야 합니다.
30 |
31 | 그래서 메모리 비용이 크게 발생되는데, 스레드는 좀 다릅니다.
32 | 일단 스레드는 프로세스내에 독립적으로 움직이는 걸 알아봤었죠? 스레드는 스택 영역을 가지고 있다. 힙 내에서 스택 공유가 자유롭습니다. os로 인해 스레드1이 중단되고 스레드2로 갈아끼울 때 힙 영역은 그대로 두고 스택만 갈아끼우면 되는 것입니다.
33 | 즉, 스레드는 힙 메모리 영역을 공유하고, 스택만 교체되기에 Context Switching 비용이 적게 든다는 것을 알아봤었습니다.
34 |
35 | 코루틴은 아까 작성했던 내용처럼 중단되었다가 재개되는 게 포인트입니다.
36 | 멈췄다가 다른 코루틴이 실행되고 한 스레드에서 여러 코루틴이 실행됩니다.
37 | 한 스레드에서 코루틴1이 코드1,2를 가지고, 코루틴2가 코드3을 가지고 있다 가정해보겠습니다.
38 | 코루틴1의 코드1이 스레드에서 실행되었다가 중단 지점이후 코루틴2의 코드3을 실행하도록 양보(yield)하고, 코루틴 코드2를 스레드에 실행되도록 할 수 있습니다.
39 | 여기서 스레드 교환이 아닌 코루틴의 중단과 재개를 활용하여 메모리 전체를 공유하기에 Context Switching 비용이 스레드보다 적게 듭니다.
40 |
41 | - 이렇게 양보하는 행위를 온보딩했었던 비선점형이라고도 합니다.
42 | - 스레드처럼 어떤 다른 존재에 의해 직접적으로 개입되서 자리가 비켜지는 걸 선점형이라 합니다.
43 |
44 | 여기서 코루틴의 장점이 보일 수 있는 점으로 **동시성 확보**가 가능합니다.
45 |
46 | 
47 |
48 | ---
49 |
50 | ### 코루틴 동작 원리
51 |
52 | 우선 스레드와의 차이부터 알아보겠습니다.
53 | 스레드와의 차이를 이해하고 동작 과정에 대해 이야기 할 것인데, 예시 코드와 함께 보시는 것이 이해하는 데에 도움이 될 듯하여 스레드와의 차이부터 정의를 해보았습니다.
54 |
55 | 우선 [기본적인 티켓팅 예제 코드](./week3/src/main/kotlin/com/sipe/week3/TicketingExample.kt) 를 통해서 설명을 드리겠습니다.
56 |
57 | 위 코드에서 프로그램의 시작점인 main 함수가 메인 루틴이 되고 그 안에 호출되는 각각의 함수는 서브 루틴이라고 볼 수 있습니다. 서브루틴은 진입점과 종료시점을 갖게 되는데 일반적으로 사용하는 함수의 호출 시점과 return 시점이 진입과 종료 시점이 됩니다. 서브루틴은 진입점부터 종료시점까지 중단없이 실행이 되기 때문에 각각의 서브루틴들 사이의 관계는 계층적, 직렬적 관계가 됩니다.
58 |
59 | 
60 |
61 | 생각보다 코루틴이나 비동기 프로그래밍을 안했을 때 실행되는 서브 루틴이 많습니다.
62 | 만약 비즈니스 로직 내부 루틴들이 실행될 경우 많은 스레드와 로직이 왔다갔다 하는 것을 볼 수 있어요.
63 |
64 | ---
65 |
66 | 그렇다면 이번엔 비동기적으로 로직을 변경하여 [변경된 코드](./week3/src/main/kotlin/com/sipe/week3/ThreadExample.kt)를 리뷰해보겠습니다.
67 |
68 | 순서는 티켓팅 기다리면서 -> 음악을 듣는데 -> 내 차례가 되면 음악을 멈추고 티켓팅 한다. -> 버스를 기다린다. -> 버스가 도착한다. -> 음악을 멈추고 버스를 탄다.
69 |
70 | 
71 |
72 | 비동기 처리를 해주면서 코드가 조금 더 복잡해졌습니다. 이런 단순히 thread와 callback을 이용한 비동기 처리는 몇 가지 문제점을 가지고 있답니다.
73 |
74 | 첫번짼는 코드의 복잡성인데요, 각각의 루틴들은 독립적인 스레드 안에서 동작을 합니다. 따라서 각 루틴들이 서로에게 영향을 주기 위해서는 스레드 사이에 통신이 필요하게 됩니다.
75 |
76 | 이는 코드를 복잡하게 하고 관리하기 힘들게 합니다. 또한 thread/callback 구조는 코드 상으로 흐름을 파악하기가 쉽지 않습니다. (위 코드에서도 어떤 순서로 동작되는지 쉽게 파악하기가 어렵습니다.)
77 |
78 | 두번째는 비용입니다. 기본적으로 thread는 OS에서 할당하고 관리를 하게 됩니다. OS에서 스레드들의 작업을 적절하게 분배하기 위해 코어에 각각의 태스크들을 적절하게 할당 및 회수 작업을 하게 됩니다. 이처럼 OS에 의해서 작업이 할당되는 것을 preemptive multitasking이라고 합니다. OS가 각 thread의 작업을 스케줄링할 때 컨텍스트 스위칭이 필요하게 됩니다. 이때 스위칭 비용이 발생하게 됩니다. 무분별한 스레드 생성이 결국 많은 리소스를 소비하게 하여 전체적으로 프로그램의 성능을 저하시킬 수도 있습니다.
79 |
80 | 이런 thread/callback을 이용하여 비동기 프로그래밍을 하는 과정에서는 코드의 복잡도와 비용의 증가로 많은 문제를 야기할 수 있습니다. 그래서 이런 점들을 해결하기 위한 다양한 방법들이 나오기 시작합니다. 언어 자체에서 이런 문제들을 해결하기 위한 다양한 방법들이 등장하기도 하며 다양한 비동기 프로그래밍을 위한 라이브러리들도 등장하였습니다. 이런 언어적 지원과 라이브러리 지원들은 자체적으로 thread를 관리해주고 callback지옥에 빠지지 않고 단순하고 파악하기 쉬운 비동기 프로그래밍을 하도록 지원을 해줍니다.
81 |
82 | 코루틴 또한 이런 단점들을 해결해주는 언어적 기능 중 하나이며, 위에서 본 코루틴의 정의에서 non-preemptive multitasking(비선점)라는 것을 확인할 수 있는데요, 코루틴은 thread와 callback 을 통한 비동기 프로그래밍에서의 단점들을 non-preemptive multitasking 방식으로 해결하고 있습니다.
83 |
84 | #### non-preemptive multitasking란?
85 |
86 | 코루틴은 os가 스레드들의 작업을 스케줄링 하도록 하지 않고, 서브루틴 간의 상호작용을 통해서 언어적으로 또는 코드 작성자가 직접 작업을 스케줄링 할 수 있도록 합니다.
87 |
88 | ---
89 |
90 | 이번엔 코틀린을 사용하기에 코루틴을 사용한 티켓팅 로직을 [변경해본 코드](./week3/src/main/kotlin/com/sipe/week3/CoRoutineExample2.kt)를 리뷰해보겠습니다.
91 |
92 | runBlocking은 현재 스레드를 block하는 코루틴을 생성하는 함수입니다.
93 | runBlocking의 이름은 이를 실행하는 스레드(이 경우 메인 스레드)가 runBlocking { ... } 내부의 모든 코루틴이 실행을 완료할 때까지 호출 기간 동안 차단된다는 것을 의미합니다. 스레드(일반 루틴 세계)는 비용이 많이 드는 리소스이고 이를 차단하는 것은 비효율적이며 종종 바람직하지 않기 때문입니다.
94 |
95 | launch 함수는 현재 스레드에 대한 blocking 없이 실행되는 코루틴을 생성합니다. 즉 현재 스레드에 다른 작업을 할당할 수 있습니다.
96 |
97 | 위 코드에서는 크게 runBlocking, lineUp, playMusic 3개의 coroutine 이 상호작용을 하고 있습니다. runBlocking 은 메인 스레드를 잡고 있으며 작업이 완료될 때까지 프로그램이 종료되지 않도록 합니다. 즉 메인 루틴이 됩니다.
98 |
99 | 메인 루틴은 lineUp 과 playMusic 이라는 2개의 코루틴을 생성하고 실행합니다. lineUp과 playMusic 은 동시성을 보장하면서 각각의 서브 루틴을 수행합니다. 그 후 메인 루틴에서 lineUp.join() 이라는 함수를 호출합니다. 이는 lineUp 코루틴이 완료될 때까지 현재 루틴을 일시정지 시키고 lineUp 코루틴이 완료가 되면 그 때 루틴을 다시 재개하겠다라는 의미입니다. lineUp 코루틴이 완료가 되면 playMusic 코루틴을 cancel 시키고 ticketing과 takeTheBus를 호출하는 구조입니다. 이를 도식화 해보면 아래 그림과 같습니다.
100 |
101 | 
102 |
103 | 위 그림처럼 코루틴을 이용하여 루틴과 루틴간의 관계 정의만을 통해서 동시성이 보장되는 비동기 프로그래밍을 하였습니다. 일반적인 서브루틴과는 다르게 코루틴에서는 비동기적으로 routine을 실행할 수 있었으며 각 루틴에서 실행되는 작업들을 중간에 일시정지(lineUp.join())하고 임의의 시점에 재개할 수 있습니다. 이를 통해 코드는 더 읽기 쉬워졌으며 개발자가 정의한 모든 서브루틴을 같은 context 내에서 (해당 프로그램에서는 main thread) 쉽게 실행할 수 있게 합니다. 이처럼 코루틴은 routine 과 routine 간의 관계를 정의하고 정의된 관계에 따라 스케줄링을 코드 레벨에서 해줌으로서 코드를 좀 더 명확하게 하고 컨텍스트 스위칭 비용을 줄일 수 있게 합니다.
104 |
105 | #### GPT의 분석 (전체 동작 설명)
106 |
107 | 1. 코루틴 스코프 생성 및 코루틴 시작
108 |
109 | - runBlocking 스코프 내에서 여러 launch 코루틴이 실행됩니다.
110 | - lineUp 코루틴이 시작되며, coroutineLinedUp 함수가 호출되고 “lined up”을 출력한 후, 2초 동안 delay로 일시 중단됩니다.
111 |
112 | 2. 다른 코루틴 병렬 실행
113 |
114 | - playMusicWithLinedUp 코루틴이 시작되어, coroutinePlayMusic 함수가 호출됩니다.
115 | - “play music”을 출력한 후, 500ms마다 “listening..“을 출력하며 무한히 반복됩니다.
116 | - lineUp.join()을 호출하여 coroutineLinedUp이 완료될 때까지 대기합니다.
117 | - 이후 playMusicWithLinedUp.cancel()을 호출하여 coroutinePlayMusic의 무한 루프를 중단시킵니다.
118 |
119 | 3. 다음 단계의 작업
120 |
121 | - coroutineTicketing() 함수가 호출되어 “ticketing”을 출력합니다.
122 | - 이후, waitingBus와 playMusicWithWaitingBus 코루틴이 각각 시작됩니다.
123 | - waitingBus는 coroutineWaitingTheBus를 실행하여 “waiting the bus”를 출력하고 2초간 대기합니다.
124 | - playMusicWithWaitingBus는 coroutinePlayMusic을 실행하여 “play music”을 출력한 후, “listening..“을 무한히 출력합니다.
125 | - waitingBus.join()을 통해 coroutineWaitingTheBus가 완료될 때까지 대기합니다.
126 | - playMusicWithWaitingBus.cancel()을 호출하여 coroutinePlayMusic의 무한 루프를 중단합니다.
127 |
128 | 4. 마지막 작업
129 |
130 | - coroutineTakeTheBus()가 호출되어 “take the bus”를 출력합니다.
131 | - 모든 작업이 완료되면 runBlocking 스코프가 종료되고, 프로그램이 종료됩니다.
132 |
133 | 요약
134 |
135 | - Kotlin의 코루틴은 launch와 runBlocking을 통해 비동기적으로 작업을 실행하고 관리할 수 있습니다.
136 | - delay와 같은 일시 중단 함수는 스레드를 차단하지 않고, 코루틴을 일시 중단하여 효율적으로 시간을 대기합니다.
137 | - cancel과 join을 사용하여 코루틴의 생명 주기를 제어할 수 있으며, 무한 루프 코루틴에서 delay를 사용하여 취소가 가능하도록 처리할 수 있습니다.
138 |
139 | ### 참고 링크
140 |
141 | - https://tech.wonderwall.kr/articles/coroutinedeepdive/
142 |
--------------------------------------------------------------------------------
/week_3/donggeon/README.md:
--------------------------------------------------------------------------------
1 | # 3주차 진행
2 |
3 | ### 목차
4 | - 코루틴 개념, 코루틴을 왜 쓰는가? - 10분
5 | - 코루틴 개념
6 | - 코루틴 docs
7 |
8 | - 코루틴의 동작원리, 스레드와의 차이 - 10분
9 |
10 | - 간단 실습? - 10분
11 | - api 하나 만드는데, io작업 3개 이상이 있다.
12 | - 3개를 동시에 실행시키고, 동시에 완료된 이후에 return하도록 코루틴을 기반으로 구성.
13 | - 하나는 스레드 기반으로 해보기~
14 |
15 | ### 코루틴 개념, 코루틴을 왜 쓰는가? - 10분
16 |
17 | - 적은 리소스에서 다양한 IO 작업들을 처리하기 위해
18 | - 비용 최적화
19 | - Webflux와는 다른 가독성
20 | - 코드 유지보수에 매우 탁월
21 |
22 | ### 동작원리
23 | 1. **상태 머신 기반 실행**:
24 | - 코루틴은 상태 머신(State Machine)으로 동작합니다.
25 | - 각 코루틴은 **현재 실행 상태(중단점)**를 저장합니다.
26 | - 실행이 중단되면 상태를 기록하고 다른 코루틴을 실행.
27 | - 스택이나 OS 수준의 스케줄링이 필요하지 않음.
28 |
29 | 2. **비차단 방식**:
30 | - 스레드는 자원을 기다릴 때 차단(block) 상태로 전환되지만, 코루틴은 **일시 중단(suspend)** 상태가 되며 스레드 풀의 다른 작업을 실행 가능.
31 | - 예: 네트워크 요청 시, 해당 코루틴은 중단되지만 동일한 스레드는 다른 작업을 처리.
32 |
33 | 3. **디스패처를 통한 스케줄링**:
34 | - 디스패처(Dispatcher)가 코루틴 실행을 제어하며, 실행 가능한 코루틴을 스레드 풀에 배치.
35 | - 스케줄링이 협력적(cooperative) 방식으로 이루어지므로, 오버헤드가 적음.
36 |
37 | 4. **구조화된 동시성**:
38 | - 부모-자식 관계에서 모든 코루틴이 **단일 스코프**로 묶여 동작.
39 | - 부모 작업이 완료되거나 취소되면, 모든 자식 코루틴도 중단되어 리소스 누수를 방지.
40 |
41 | ### 코루틴의 동작원리, 스레드와의 차이 - 10분
42 |
43 | - 전통적인 스레드 방식의 병렬 처리 방식
44 | - 각 스레드는 독립적인 실행 단위로서, 자신만의 스택과 레지스터를 가지고 실행
45 | - 병렬 처리를 위해 CPU의 멀티코어를 활용하여 동시에 여러 작업을 실행하거나, 시간 분할 방식으로 컨텍스트 전환을 통해 여러 스레드를 번갈아 실행
46 |
47 | 1. **운영체제 스케줄링**:
48 | - OS는 CPU 코어에 여러 스레드를 할당하고 **시간 분할 스케줄링(Time Slicing)**을 통해 동시에 실행되는 것처럼 보이게 합니다.
49 | - 각 스레드는 컨텍스트(레지스터, 프로그램 카운터 등)를 저장하고 전환하며 실행.
50 |
51 | 2. **비용이 큰 이유**:
52 | - 컨텍스트 전환 시 스택, 레지스터 등 많은 상태를 저장하고 복원해야 하므로 오버헤드가 큼.
53 | - 많은 스레드가 생성되면 메모리 사용량이 급증.
54 |
55 | 
56 |
57 | - 코루틴의 처리 방식
58 | - 코루틴은 기본적으로 동시성, 병렬처리를 함께 진행
59 | - 스레드의 컨텍스트 스위칭을 별도의 çontinuation, coroutineContext로 Heap에 저장하여 사용
60 | - 그렇기 때문에 컨텍스트 스위칭 비용이 적음
61 |
62 | 
63 |
64 | ### 대규모 동시성 처리:
65 |
66 | 1. 스레드를 사용하면:
67 | - 1만 개의 비동기 요청을 처리하려면 1만 개의 스레드가 필요.
68 | - 스레드 생성 및 메모리 사용량 때문에 시스템이 과부하에 걸릴 가능성이 높음.
69 |
70 | 2. 코루틴을 사용하면:
71 | - 동일한 작업을 단일 스레드에서 처리 가능.
72 | - 각 코루틴이 가벼운 상태 머신으로 동작하므로, 메모리 소비가 최소화되고 전환 비용도 적음.
73 |
74 | ## **3. 주요 차이점**
75 |
76 | | 특징 | **스레드(Thread)** | **코루틴(Coroutine)** |
77 | | ---------------------- | ------------------------------------------------------- | ------------------------------------------------------- |
78 | | **관리 주체** | 운영체제(OS) | Kotlin 런타임 |
79 | | **메모리 소비** | 각 스레드가 독립적인 스택을 사용 (수백 KB~MB 단위) | 스택 없이 상태 머신 기반 (수십 바이트) |
80 | | **컨텍스트 전환 비용** | 스택, 레지스터 저장/복원 (높은 비용) | 상태만 저장 (낮은 비용) |
81 | | **비동기 처리** | 차단(blocking)이 기본 | 비차단(non-blocking) 방식 기본 |
82 | | **동작 단위** | 선점적(preemptive) - OS가 강제로 스레드 전환 | 협력적(cooperative) - 코루틴 스스로 양보 (suspend) |
83 | | **수량 제한** | 스레드 수는 OS/메모리에 따라 제한됨 (보통 수천 개 이하) | 코루틴은 제한 없이 생성 가능 (수십만 개 이상 실행 가능) |
84 |
85 | ---
86 |
87 | **정리**
88 | - **스레드**: 운영체제가 관리하는 무거운 실행 단위로, 높은 비용을 감수해야 함.
89 | - **코루틴**: 사용자 수준에서 관리되는 경량 실행 단위로, **협력적 스케줄링**과 **상태 머신 기반의 동작**을 통해 자원을 최적화.
90 | - 코루틴은 많은 동시 작업을 필요로 하는 현대 애플리케이션(네트워크 처리, UI 비동기 작업 등)에 적합한 동시성 솔루션을 제공.
91 |
92 |
93 | ### 간단 실습? - 10분
94 |
95 | - https://dong-geon.tistory.com/72
96 |
--------------------------------------------------------------------------------
/week_3/jaeyeong/res/Screenshot_2024-11-20_at_5.24.59_PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_3/jaeyeong/res/Screenshot_2024-11-20_at_5.24.59_PM.png
--------------------------------------------------------------------------------
/week_3/jaeyeong/res/image 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_3/jaeyeong/res/image 1.png
--------------------------------------------------------------------------------
/week_3/jaeyeong/res/image 2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_3/jaeyeong/res/image 2.png
--------------------------------------------------------------------------------
/week_3/jaeyeong/res/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_3/jaeyeong/res/image.png
--------------------------------------------------------------------------------
/week_3/junseo/3주차.md:
--------------------------------------------------------------------------------
1 | # 3주차
2 |
3 | # 코루틴 개념, 코루틴을 왜 쓰는가?
4 |
5 | ## 코루틴이란
6 |
7 | **코루틴은 Co(함께, 서로) + routine(규칙적 일의 순서, 작업의 집합) 2개가 합쳐진 단어로 함께 동작하며 규칙이 있는 일의 순서를 뜻함**.
8 |
9 | > 실행의 지연과 재개를 허용함으로써,
10 | >
11 | >
12 | > **비선점적 멀티태스킹**을 위한**서브 루틴**을 일반화한 컴퓨터 프로그램 구성요소
13 | >
14 |
15 | ### 루틴?
16 |
17 | 프로그램은 여러 **루틴의 조합**으로 진행되는데, 메인 루틴과 서브 루틴으로 나뉨.
18 |
19 | - **메인 루틴** : 프로그램 전체의 개괄적인 동작으로 **main 함수에 의해 수행되는 흐름**
20 | - **서브 루틴** : 반복적인 기능을 모은 동작으로 **main 함수 내에서 실행되는 개별 함수의 흐름**
21 |
22 | 
23 | 메인 루틴에서 함수가 실행되어 서브 루틴으로 진입한 후, 함수 동작이 완료될 때 다시 메인 루틴으로 돌아감을 표현하는 이미지
24 |
25 | 함수를 호출해서 새로운 서블 루틴으로 들어가게 되는 지점: **진입 지점**
26 |
27 | 서브 루틴에서 반환문을 통해 함수 호출부로 돌아가게 되는 지점: **반환 지점**
28 |
29 | 서브 루틴은 진입 지점, 반환 지점이 각각 한 개기 때문에
30 |
31 | **진입 후 동작을 완료하고 반환하기 전까지 다른 작업 수행 못 함.**
32 |
33 | ### 코루틴?
34 |
35 | **진입 지점과 반환 지점을 여러개 가질 수 있어서** 서브 루틴의 임의 지점에서 동작을 중단하고, 이후 다시 해당 지점에서부터 실행을 재개할 수 있음
36 |
37 | **→ 전체 루틴 동작을 끝내기 전에 동작을 중단하고 다른 작업 수행 가능**
38 |
39 | 스레드 전체를 블락킹하는게 아니라 하나의 스레드 위에서 여러 코루틴을 중단/재개 할 수 있는 것
40 |
41 | 
42 |
43 | 내부적으로 Continuation Passing Style(CPS, 연속 전달 방식)과 State machine을 이용하여 동작
44 |
45 | 코루틴에서 함수 호출 시 연산 결과 및 다음 수행 작업과 같은 제어 정보를 가진 **일종의 콜백 함수인 Continuation을 전달하며 각 함수의 작업이 완료되면 Continuation을 호출함**
46 | 이를 통해 상태를 연속적으로 전달하며 컨텍스트를 유지하고 코루틴 실행 관리를 위한 State machine에 따라 코드 블록을 구분해 실행함.
47 |
48 | → 자세한 얘기는 두 번째 주제에서!
49 |
50 | ### 비선점적 멀티태스킹
51 |
52 | 하나의 프로세스가 CPU를 할당받으면 종료되기 전까지 다른 프로세스가 CPU를 강제로 차지할 수 없음
53 |
54 | cf) 스레드: 선점적
55 |
56 | 따라서 코루틴은 병행성은 제공하지만, 병렬성(물리적으로 동시에)은 제공하지 않음. → 사실 코루틴은 하나의 스레드 위에서 중단/재개가 가능한 루틴을 말하기 때문에 당연한 말인 것 같다..
57 |
58 | ### 코루틴을 사용하는 이유
59 |
60 | **코루틴 도입시 장점**
61 |
62 | 스레드보다 가벼움
63 |
64 | 가장 기본적으로 스레드보다 가벼움 → 컨텍스트 스위칭 비용 절감
65 |
66 | 가독성이 좋음
67 |
68 | suspend 함수를 사용해서, 콜백 지옥에서 벗어남
69 |
70 | 구조화된 동시성
71 |
72 | 코루틴들 사이에서 부모-자식 관계와 같은 종속 개념이 있어, 동작 취소나 예외 전파 등을 관리하는 데 코드 줄이기 가능
73 |
74 | # 코루틴 동작 원리, 스레드와의 차이
75 |
76 | ## 코루틴 동작 원리
77 |
78 | Directive Style
79 |
80 | ```kotlin
81 | fun postItem(item: Item) {
82 | val token = requestToken() // -> acting
83 | val post = createPost(token, item) // -> continuation
84 | proessPost(post)
85 | }
86 | ```
87 |
88 | CPS(Continuation-Passing Style) == callback
89 |
90 | ```kotlin
91 | fun postImem(item: Item) {
92 | // acting의 파라미터로 continuation을 넘김
93 | requestToken { token ->
94 | createPost(token, item) { post ->
95 | processPost(post)
96 | }
97 | }
98 | }
99 | ```
100 |
101 | callback을 사용하면 가독성이 떨어지고, 많이 사용하면 콜백 지옥에 빠지므로..
102 |
103 | 코루틴의 suspend 키워드를 통해 CPS로 동작하되, Directive Style처럼 작성할 수 있게 함
104 |
105 | ### suspend 키워드
106 |
107 | kotlin에서 아래와 같이 suspend 키워드를 붙인 함수들은 말 그대로 중단 가능한 함수
108 |
109 | ```kotlin
110 | suspend fun createPost(token: Token, item: Item): Post {
111 | ...
112 | }
113 | ```
114 |
115 | 위 코드는 JVM이 다음과 같이 컴파일함.
116 |
117 | Continuation이 매개변수로 생기면서 CPS 스타일로 변환됨
118 |
119 | suspend 함수가 컴파일되면 각 중단 점에 label이 찍히게 됨
120 |
121 | 내부적으로는 switch-case처럼 바뀌게 됨 → 중간에 어떤 suspension point로도 갈 수 있게.
122 |
123 | 또, 현재까지의 연산 상태를 저장하는 객체를 생성하게 됨
124 |
125 | 함수 내에서 다른 함수를 호출할 때, 지금까지 연산했던 결과를 넘겨주고, 호출한 함수의 동작이 끝나면 재개할 지점을 넘겨줘야됨 == sm (state machine)
126 |
127 | ```kotlin
128 | suspend fun postItem(item: Item, cont: Continuation) {
129 | val sm = object : CoroutineImpl { ... }
130 | switch(sm.label) {
131 | case 0:
132 | sm.item = item // 연산 결과 저장
133 | sm.label = 1 // 다음에 동작할 지점**
134 | val token = requestToken(sm)
135 | case 1:
136 | val item = sm.item
137 | val token = sm.result as Token
138 | sm.label = 2
139 | val post = createPost(token, item, sm)
140 | ...
141 | }
142 | }
143 | ```
144 |
145 | requestToken()가 호출되고 동작이 완료되면 sm.resume()을 호출하게 됨 → postItem의 label1을 실행하게 됨
146 |
147 | label1에서는 지금까지 연산된 값을 불러와서 다음 동작인 createPost()를 호출하게 됨
148 |
149 | .. 반복
150 |
151 | ### Suspend 함수의 중단점은 어떤 기준으로 생기는 걸까?
152 |
153 | 1. suspend 함수를 호출할 때
154 | 2. 네트워크 요청, I/O 작업 등이 일어날 때
155 | 3. 코루틴 컨텍스트가 변경될 때
156 |
157 | 등
158 |
159 | suspend 키워드를 붙인다고 모두 중단하는 것이 아니라, 중단 “가능”할 뿐
160 |
161 | ex) 단순 연산만 반복하는 함수는 자동으로 중단하지 않음 → delay, yield 등을 이용해서 명시적으로 중단 지점을 사용해야됨
162 |
163 | ## 코루틴 vs 스레드
164 |
165 | ### 코루틴
166 |
167 | 
168 |
169 | cooperatively multitasked
170 | (협력적 멀티태스킹)
171 |
172 | 코루틴이 자발적으로 제어권을 넘김
173 |
174 | 동시성을 제공하지만 병렬성은 제공하지 않음.
175 |
176 | 코루틴 간 전환에는 system call이 불필요함 → 뮤텍스, 세마포어 등 동기화 작업 필요 X == 운영체제 지원 필요 X
177 |
178 | ### 스레드
179 |
180 | 
181 |
182 | preemptively multitasked
183 |
184 | (선점적 멀티태스킹)
185 |
186 | 스레드는 시분할이 끝나면 강제로 제어권이 넘어감
187 |
188 | # 실습
189 |
190 | - api 하나 만드는데, io작업 3개 이상이 있다.
191 | - 3개를 동시에 실행시키고, 동시에 완료된 이후에 return하도록 코루틴을 기반으로 구성.
192 | - 하나는 스레드 기반으로 해보기~
193 |
194 | ### 코루틴
195 |
196 | ```kotlin
197 | import kotlinx.coroutines.async
198 | import kotlinx.coroutines.delay
199 | import kotlinx.coroutines.runBlocking
200 | import kotlin.system.measureTimeMillis
201 |
202 | fun main() = runBlocking {
203 | val timeTaken = measureTimeMillis {
204 | val task1 = async { ioTask1() }
205 | val task2 = async { ioTask2() }
206 | val task3 = async { ioTask3() }
207 |
208 | val results = listOf(task1.await(), task2.await(), task3.await())
209 | println("Result: $results")
210 |
211 | }
212 |
213 | println("소요 시간: $timeTaken")
214 | }
215 |
216 | suspend fun ioTask1(): String {
217 | println("작업 1 시작")
218 | println("실행 중인 스레드: ${Thread.currentThread().name}")
219 | delay(1000)
220 | return "ioTask1 result"
221 | }
222 |
223 | suspend fun **ioTask2 * *(): String {
224 | println("작업 2 시작")
225 | println("실행 중인 스레드: ${Thread.currentThread().name}")
226 | delay(1500)
227 | return "ioTask2 result"
228 | }
229 |
230 | suspend fun **ioTask3 * *(): String {
231 | println("작업 3 시작")
232 | println("실행 중인 스레드: ${Thread.currentThread().name}")
233 | delay(2000)
234 | return "ioTask3 result"
235 | }
236 | ```
237 |
238 | 
239 |
240 | ### 스레드
241 |
242 | ```kotlin
243 | import java.util.concurrent.CompletableFuture
244 | import kotlin.system.measureTimeMillis
245 |
246 | fun main() {
247 | val timeMilies = measureTimeMillis {
248 | val task1 = CompletableFuture.supplyAsync { nonSuspendIoTask1() }
249 | val task2 = CompletableFuture.supplyAsync { nonSuspendIoTask2() }
250 | val task3 = CompletableFuture.supplyAsync { nonSuspendIoTask3() }
251 |
252 | val allTasks = CompletableFuture.allOf(task1, task2, task3)
253 |
254 | val result = allTasks.thenApply {
255 | listOf(task1.join(), task2.join(), task3.join())
256 | }
257 |
258 | println("Result: ${result.get()}")
259 | }
260 |
261 | println("소요 시간: $timeMilies")
262 |
263 | }
264 |
265 | fun nonSuspendIoTask1(): String {
266 | println("작업 1 시작")
267 | println("실행 중인 스레드: ${Thread.currentThread().name}")
268 | Thread.sleep(1000)
269 | return "ioTask1 result"
270 | }
271 |
272 | fun nonSuspendIoTask2(): String {
273 | println("작업 2 시작")
274 | println("실행 중인 스레드: ${Thread.currentThread().name}")
275 | Thread.sleep(1500)
276 | return "ioTask2 result"
277 | }
278 |
279 | fun nonSuspendIoTask3(): String {
280 | println("작업 3 시작")
281 | println("실행 중인 스레드: ${Thread.currentThread().name}")
282 | Thread.sleep(2000)
283 | return "ioTask3 result"
284 | }
285 |
286 | ```
287 |
288 | 
289 |
290 | 동작이 복잡하지 않아서, 걸리는 시간은 비슷하지만
291 |
292 | 코루틴은 스레드 생성 없이 동시 처리하는 것을 확인할 수 있음
--------------------------------------------------------------------------------
/week_3/junseo/res/625998e7-7582-4da9-b167-10d47628a923.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_3/junseo/res/625998e7-7582-4da9-b167-10d47628a923.png
--------------------------------------------------------------------------------
/week_3/junseo/res/9b28b523-7766-4e0d-80ef-4857806b982b.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_3/junseo/res/9b28b523-7766-4e0d-80ef-4857806b982b.png
--------------------------------------------------------------------------------
/week_3/junseo/res/image 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_3/junseo/res/image 1.png
--------------------------------------------------------------------------------
/week_3/junseo/res/image 2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_3/junseo/res/image 2.png
--------------------------------------------------------------------------------
/week_3/junseo/res/image 3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_3/junseo/res/image 3.png
--------------------------------------------------------------------------------
/week_3/junseo/res/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_3/junseo/res/image.png
--------------------------------------------------------------------------------
/week_3/positive/3주차.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_3/positive/3주차.pdf
--------------------------------------------------------------------------------
/week_4/char-yb/images/async_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_4/char-yb/images/async_1.png
--------------------------------------------------------------------------------
/week_4/char-yb/images/dispatcher_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_4/char-yb/images/dispatcher_1.png
--------------------------------------------------------------------------------
/week_4/char-yb/images/dispatcher_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_4/char-yb/images/dispatcher_2.png
--------------------------------------------------------------------------------
/week_4/char-yb/images/runblocking_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_4/char-yb/images/runblocking_1.png
--------------------------------------------------------------------------------
/week_4/char-yb/images/suspend_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_4/char-yb/images/suspend_1.png
--------------------------------------------------------------------------------
/week_4/char-yb/images/suspend_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_4/char-yb/images/suspend_2.png
--------------------------------------------------------------------------------
/week_4/char-yb/week4/.gitattributes:
--------------------------------------------------------------------------------
1 | /gradlew text eol=lf
2 | *.bat text eol=crlf
3 | *.jar binary
4 |
--------------------------------------------------------------------------------
/week_4/char-yb/week4/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | .gradle
3 | build/
4 | !gradle/wrapper/gradle-wrapper.jar
5 | !**/src/main/**/build/
6 | !**/src/test/**/build/
7 |
8 | ### STS ###
9 | .apt_generated
10 | .classpath
11 | .factorypath
12 | .project
13 | .settings
14 | .springBeans
15 | .sts4-cache
16 | bin/
17 | !**/src/main/**/bin/
18 | !**/src/test/**/bin/
19 |
20 | ### IntelliJ IDEA ###
21 | .idea
22 | *.iws
23 | *.iml
24 | *.ipr
25 | out/
26 | !**/src/main/**/out/
27 | !**/src/test/**/out/
28 |
29 | ### NetBeans ###
30 | /nbproject/private/
31 | /nbbuild/
32 | /dist/
33 | /nbdist/
34 | /.nb-gradle/
35 |
36 | ### VS Code ###
37 | .vscode/
38 |
39 | ### Kotlin ###
40 | .kotlin
41 |
--------------------------------------------------------------------------------
/week_4/char-yb/week4/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("jvm") version "1.9.25"
3 | kotlin("plugin.spring") version "1.9.25"
4 | id("org.springframework.boot") version "3.4.0"
5 | id("io.spring.dependency-management") version "1.1.6"
6 | }
7 |
8 | group = "com.sipe"
9 | version = "0.0.1-SNAPSHOT"
10 |
11 | java {
12 | toolchain {
13 | languageVersion = JavaLanguageVersion.of(21)
14 | }
15 | }
16 |
17 | repositories {
18 | mavenCentral()
19 | }
20 |
21 | dependencies {
22 | implementation("org.springframework.boot:spring-boot-starter")
23 | implementation("org.springframework.boot:spring-boot-starter-web")
24 | implementation("org.jetbrains.kotlin:kotlin-reflect")
25 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
26 | testImplementation("org.springframework.boot:spring-boot-starter-test")
27 | testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
28 | testRuntimeOnly("org.junit.platform:junit-platform-launcher")
29 | }
30 |
31 | kotlin {
32 | compilerOptions {
33 | freeCompilerArgs.addAll("-Xjsr305=strict")
34 | }
35 | }
36 |
37 | tasks.withType {
38 | useJUnitPlatform()
39 | }
40 |
--------------------------------------------------------------------------------
/week_4/char-yb/week4/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_4/char-yb/week4/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/week_4/char-yb/week4/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/week_4/char-yb/week4/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/week_4/char-yb/week4/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "week4"
2 |
--------------------------------------------------------------------------------
/week_4/char-yb/week4/src/main/kotlin/com/sipe/week4/AsyncExample.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week4
2 |
3 | import kotlinx.coroutines.*
4 |
5 | class AsyncExample {
6 | }
7 |
8 | fun main() = runBlocking {
9 | val deferredInt: Deferred = async {
10 | 1 // 마지막 줄 반환
11 | }
12 | val value = deferredInt.await()
13 | println(value) // 1 출력
14 | }
15 |
16 | //fun main() {
17 | // val deferred = CoroutineScope(Dispatchers.IO).async {
18 | // 1
19 | // }
20 | // deferred.await()
21 | //}
--------------------------------------------------------------------------------
/week_4/char-yb/week4/src/main/kotlin/com/sipe/week4/ContinuationExample.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week4
2 |
3 | import kotlinx.coroutines.delay
4 |
5 | /** 코드 흐름
6 | 1. UserService.findUser 호출:
7 | • findUser 메서드는 Continuation을 받아 코루틴의 중단 및 재개 상태를 관리합니다.
8 | • 초기 호출 시 Continuation이 null이므로 새로운 상태 머신 객체(FindUserContinuation)가 생성됩니다.
9 | 2. FindUserContinuation 객체 생성:
10 | • 내부적으로 resumeWith 메서드를 통해 재개 시 상태를 업데이트합니다.
11 | • label 값을 통해 진행 상태를 관리:
12 | • label == 0: 프로필 가져오기 단계
13 | • label == 1: 이미지 가져오기 단계
14 | 3. findUser 상태에 따라 작업 분기:
15 | • label == 0: UserProfileRepository.findProfile을 호출하여 프로필 데이터를 가져옴.
16 | • label == 1: UserImageRepository.findImage를 호출하여 이미지 데이터를 가져옴.
17 | • label == 2: 모든 데이터를 가져온 후 UserDto를 생성해 반환.
18 | 4. 재개 (Resume):
19 | • findProfile 및 findImage에서 데이터를 준비한 후 resumeWith을 호출하여 다음 단계로 넘어갑니다.
20 | • resumeWith 호출 시:
21 | • label 값을 업데이트.
22 | • findUser를 재귀적으로 호출하여 다음 단계를 처리.
23 | */
24 | class ContinuationExample {
25 | }
26 |
27 | suspend fun main() {
28 | val service = UserService()
29 | println(service.findUser(1L, null))
30 | }
31 |
32 | interface Continuation {
33 | // 라벨을 가지고 있을 것
34 |
35 | // suspend fun에서 불릴 애들. callback
36 | suspend fun resumeWith(data: Any?)
37 | }
38 |
39 | class UserService {
40 | private val userProfileRepository = UserProfileRepository()
41 | private val userImageRepository = UserImageRepository()
42 |
43 | private abstract class FindUserContinuation : Continuation {
44 | var label = 0
45 | var profile: Profile? = null
46 | var image: Image? = null
47 | }
48 |
49 | suspend fun findUser(userId: Long, continuation: Continuation?): UserDto {
50 | // state machine
51 | val sm = continuation as? FindUserContinuation ?: object : FindUserContinuation() {
52 | // 일종의 재귀함수
53 | override suspend fun resumeWith(data: Any?) {
54 | when (label) {
55 | 0 -> {
56 | profile = data as Profile
57 | label = 1
58 | }
59 | 1 -> {
60 | image = data as Image
61 | label = 2
62 | }
63 | }
64 | findUser(userId, this)
65 | }
66 | }
67 |
68 | when (sm.label) {
69 | 0 -> {
70 | // 0단계 - 초기 시작
71 | println("프로필을 가져오겠습니다")
72 | userProfileRepository.findProfile(userId, sm)
73 | }
74 | 1 -> {
75 | // 1단계 - 1차 중단 후 재시작
76 | println("이미지를 가져오겠습니다")
77 | userImageRepository.findImage(sm.profile!!, sm)
78 | }
79 | }
80 | // 2단계 - 2차 중단 후 재시작
81 | return UserDto(sm.profile!!, sm.image!!)
82 | }
83 | }
84 |
85 | class UserProfileRepository {
86 | suspend fun findProfile(userId: Long, continuation: Continuation){
87 | delay(100L)
88 | continuation.resumeWith(Profile())
89 | }
90 | }
91 |
92 | class UserImageRepository {
93 | suspend fun findImage(profile: Profile, continuation: Continuation){
94 | delay(100L)
95 | continuation.resumeWith(Image())
96 | }
97 | }
98 |
99 | data class Profile(
100 | val value: String = ""
101 | )
102 |
103 | data class Image(
104 | val value: String = ""
105 | )
106 |
107 | data class UserDto (
108 | val profile: Profile,
109 | val image: Image
110 | )
--------------------------------------------------------------------------------
/week_4/char-yb/week4/src/main/kotlin/com/sipe/week4/CoroutineContextExample.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week4
2 |
3 | import kotlinx.coroutines.*
4 | import kotlin.coroutines.CoroutineContext
5 |
6 | class CoroutineContextExample {
7 | private val customContext = CoroutineName("나만의 코루틴") + SupervisorJob() + Dispatchers.Default
8 |
9 | fun runExample() {
10 | runBlocking(customContext) {
11 | delayPrintCoroutineContext(customContext)
12 | }
13 | }
14 | }
15 |
16 | fun main() {
17 | CoroutineContextExample().runExample()
18 | }
19 |
20 | suspend fun delayPrintCoroutineContext(context: CoroutineContext) {
21 | val job = CoroutineScope(context).launch {
22 | delay(1000)
23 | println("Hello from ${coroutineContext[CoroutineName]}")
24 | }
25 | job.join()
26 | }
--------------------------------------------------------------------------------
/week_4/char-yb/week4/src/main/kotlin/com/sipe/week4/CoroutineScopeExample.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week4
2 | import kotlinx.coroutines.*
3 |
4 | class CoroutineScopeExample {}
5 |
6 | fun main() = runBlocking {
7 | launchAB()
8 | }
9 |
10 | private suspend fun launchAB() = coroutineScope {
11 | launch {
12 | println("launch A Start")
13 | delay(1000L)
14 | println("launch A End")
15 | }
16 |
17 | launch {
18 | println("launch B Start")
19 | delay(1000L)
20 | println("launch B End")
21 | }
22 | delay(500L)
23 | println("Hello World!")
24 | }
25 |
26 | //class CoroutineScopeExample {
27 | // // CoroutineScope를 정의
28 | // private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
29 | //
30 | // fun fetchData(onComplete: (String) -> Unit) {
31 | // // 스코프 내에서 네트워크 요청 실행
32 | // scope.launch {
33 | // try {
34 | // // 네트워크 호출 시뮬레이션
35 | // val result = simulateNetworkRequest()
36 | // // UI 업데이트는 Main 스레드에서
37 | // withContext(Dispatchers.Main) {
38 | // onComplete(result)
39 | // }
40 | // } catch (e: Exception) {
41 | // // 에러 처리
42 | // withContext(Dispatchers.Main) {
43 | // onComplete("Error: ${e.message}")
44 | // }
45 | // }
46 | // }
47 | // }
48 | //
49 | // // 네트워크 요청 시뮬레이션
50 | // private suspend fun simulateNetworkRequest(): String {
51 | // delay(2000) // 네트워크 대기 시간
52 | // return "Data from server"
53 | // }
54 | //
55 | // // 스코프 종료
56 | // fun clear() {
57 | // scope.cancel() // 모든 하위 코루틴 취소
58 | // }
59 | //}
60 | //
61 | //fun main() = runBlocking {
62 | // val example = CoroutineScopeExample()
63 | //
64 | // println("Fetching data...")
65 | //
66 | // example.fetchData { result ->
67 | // println("Result: $result")
68 | // }
69 | //
70 | // // 프로그램 종료 전 스코프 종료
71 | // delay(3000)
72 | // example.clear()
73 | //}
74 |
75 | //fun startGlobalTimer() {
76 | // GlobalScope.launch {
77 | // while (true) {
78 | // delay(1000)
79 | // println("Timer tick at: ${System.currentTimeMillis()}")
80 | // }
81 | // }
82 | //}
83 | //
84 | //fun main() {
85 | // println("Start Timer")
86 | // startGlobalTimer()
87 | //
88 | // // 프로그램을 강제로 5초 후 종료
89 | // Thread.sleep(5000)
90 | // println("Main function ends")
91 | //}
--------------------------------------------------------------------------------
/week_4/char-yb/week4/src/main/kotlin/com/sipe/week4/LaunchExample.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week4
2 |
3 | import kotlinx.coroutines.*
4 |
5 | class LaunchExample {
6 | }
7 |
8 | fun main() = runBlocking {
9 | println("Start")
10 |
11 | launch {
12 | delay(1000) // 1초 대기
13 | println("Task 1 completed")
14 | }
15 |
16 | launch {
17 | delay(500) // 0.5초 대기
18 | println("Task 2 completed")
19 | }
20 |
21 | println("End")
22 | val job: Job = launch { println(1) }
23 | job.join()
24 | println(2)
25 | }
--------------------------------------------------------------------------------
/week_4/char-yb/week4/src/main/kotlin/com/sipe/week4/RunBlockingExample.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week4
2 | import kotlinx.coroutines.*
3 |
4 | class RunBlockingExample {
5 | }
6 |
7 | fun main() = runBlocking {
8 | println("Start") // 즉시 실행
9 | delay(5000) // 5초 대기 (코루틴)
10 | println("End") // 5초 후 실행
11 | }
--------------------------------------------------------------------------------
/week_4/char-yb/week4/src/main/kotlin/com/sipe/week4/SuspendExample.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week4
2 |
3 | import kotlinx.coroutines.*
4 |
5 | class SuspendExample {
6 | }
7 |
8 | fun main() = runBlocking {
9 | val startTime = System.currentTimeMillis()
10 | val job1 = launch {
11 | delayAndPrintHelloCoroutines()
12 | }
13 | val job2 = launch {
14 | delayAndPrintHelloCoroutines()
15 | }
16 | job1.join()
17 | job2.join()
18 | println("${System.currentTimeMillis() - startTime}")
19 | }
20 |
21 | suspend fun delayAndPrintHelloCoroutines() {
22 | delay(100L)
23 | println("Hello Coroutines")
24 | }
--------------------------------------------------------------------------------
/week_4/char-yb/week4/src/main/kotlin/com/sipe/week4/Week4Application.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week4
2 |
3 | import org.springframework.boot.autoconfigure.SpringBootApplication
4 | import org.springframework.boot.runApplication
5 |
6 | @SpringBootApplication
7 | class Week4Application
8 |
9 | fun main(args: Array) {
10 | runApplication(*args)
11 | }
12 |
--------------------------------------------------------------------------------
/week_4/char-yb/week4/src/main/kotlin/com/sipe/week4/WithContextExample.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week4
2 |
3 |
4 | import kotlinx.coroutines.*
5 |
6 | class WithContextExample {
7 | }
8 |
9 | //// suspend 함수로 네트워크 작업 시뮬레이션
10 | //suspend fun fetchData(): String {
11 | // return withContext(Dispatchers.IO) { // I/O 스레드에서 실행
12 | // println("Fetching data on: ${Thread.currentThread().name}")
13 | // delay(3000) // 네트워크 지연 시뮬레이션
14 | // "Data from server"
15 | // }
16 | //}
17 | //
18 | //fun main() = runBlocking {
19 | // println("Main program starts: ${Thread.currentThread().name}")
20 | //
21 | // // suspend 함수 호출
22 | // val data = fetchData()
23 | // println("Received data: $data")
24 | //
25 | // println("Main program ends: ${Thread.currentThread().name}")
26 | //}
27 |
28 | // suspend 함수로 데이터를 가져오는 함수
29 | suspend fun fetchDataFromNetwork(): String {
30 | return withContext(Dispatchers.IO) {
31 | println("Fetching data on: ${Thread.currentThread().name}")
32 | delay(3000) // 네트워크 지연 시뮬레이션
33 | "Data from network"
34 | }
35 | }
36 |
37 | // suspend 함수로 데이터를 처리하는 함수
38 | suspend fun processData(data: String) {
39 | withContext(Dispatchers.Default) {
40 | println("Processing data on: ${Thread.currentThread().name}")
41 | delay(1000L) // 데이터 처리 시뮬레이션
42 | println("Data processed: $data")
43 | }
44 | }
45 |
46 | fun main() = runBlocking {
47 | println("Main program starts: ${Thread.currentThread().name}")
48 |
49 | // 네트워크에서 데이터를 가져오고 처리
50 | val data = fetchDataFromNetwork()
51 | processData(data)
52 |
53 | println("Main program ends: ${Thread.currentThread().name}")
54 | }
--------------------------------------------------------------------------------
/week_4/char-yb/week4/src/main/kotlin/com/sipe/week4/YieldExample.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week4
2 |
3 | import kotlinx.coroutines.*
4 |
5 | class YieldExample {
6 | }
7 |
8 | suspend fun exampleYield() {
9 | repeat(5) {
10 | println("Working...")
11 | yield() // 다른 코루틴에게 실행 권한 양보
12 | }
13 | }
14 |
15 | fun main(): Unit = runBlocking {
16 | launch {
17 | exampleYield()
18 | }
19 |
20 | launch {
21 | repeat(5) {
22 | println("Other task...")
23 | delay(200L)
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/week_4/char-yb/week4/src/main/kotlin/com/sipe/week4/config/SpringCoroutineDispatcher.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week4.config
2 |
3 | import kotlinx.coroutines.CoroutineDispatcher
4 | import kotlinx.coroutines.Runnable
5 | import org.springframework.core.task.TaskExecutor
6 | import kotlin.coroutines.CoroutineContext
7 |
8 | class SpringCoroutineDispatcher(
9 | private val taskExecutor: TaskExecutor
10 | ) : CoroutineDispatcher() {
11 | override fun dispatch(context: CoroutineContext, block: Runnable) {
12 | taskExecutor.execute(block) // TaskExecutor를 사용하여 코루틴 실행
13 | }
14 | }
--------------------------------------------------------------------------------
/week_4/char-yb/week4/src/main/kotlin/com/sipe/week4/config/TaskExecutorConfig.kt:
--------------------------------------------------------------------------------
1 | import org.springframework.context.annotation.Bean
2 | import org.springframework.context.annotation.Configuration
3 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
4 |
5 | @Configuration
6 | class TaskExecutorConfig {
7 |
8 | @Bean(name = ["taskExecutor1"])
9 | fun taskExecutor1(): ThreadPoolTaskExecutor {
10 | val executor = ThreadPoolTaskExecutor()
11 | executor.corePoolSize = 5
12 | executor.maxPoolSize = 10
13 | executor.queueCapacity = 25
14 | executor.initialize()
15 | return executor
16 | }
17 |
18 | @Bean(name = ["taskExecutor2"])
19 | fun taskExecutor2(): ThreadPoolTaskExecutor {
20 | val executor = ThreadPoolTaskExecutor()
21 | executor.corePoolSize = 10
22 | executor.maxPoolSize = 20
23 | executor.queueCapacity = 50
24 | executor.initialize()
25 | return executor
26 | }
27 | }
--------------------------------------------------------------------------------
/week_4/char-yb/week4/src/main/kotlin/com/sipe/week4/controller/ExampleController.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week4.controller
2 |
3 | import com.sipe.week4.service.ExampleService
4 | import org.springframework.web.bind.annotation.GetMapping
5 | import org.springframework.web.bind.annotation.RestController
6 |
7 | /** 요청 흐름
8 | * 1. 요청:
9 | * • /test-coroutine 엔드포인트로 요청을 보냅니다.
10 | * 2. 서비스에서 코루틴 실행:
11 | * • ExampleService에서 코루틴이 실행됩니다.
12 | * • Spring의 ThreadPoolTaskExecutor를 사용해 비동기적으로 작업을 실행.
13 | * 3. Dispatcher 동작:
14 | * • SpringCoroutineDispatcher를 통해 작업이 TaskExecutor의 스레드풀에서 처리됩니다.
15 | */
16 |
17 | @RestController
18 | class ExampleController(
19 | private val exampleService: ExampleService
20 | ) {
21 | @GetMapping("/test-coroutine")
22 | fun testCoroutine(): String {
23 | exampleService.executeCoroutines()
24 | return "Coroutine started!"
25 | }
26 | }
--------------------------------------------------------------------------------
/week_4/char-yb/week4/src/main/kotlin/com/sipe/week4/service/ExampleService.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week4.service
2 |
3 | import com.sipe.week4.config.SpringCoroutineDispatcher
4 | import kotlinx.coroutines.*
5 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
6 | import org.springframework.stereotype.Service
7 |
8 | @Service
9 | class ExampleService(
10 | private val taskExecutor: ThreadPoolTaskExecutor
11 | ) {
12 | private val springDispatcher = SpringCoroutineDispatcher(taskExecutor)
13 | private val scope = CoroutineScope(springDispatcher + SupervisorJob())
14 |
15 | fun executeCoroutines() {
16 | scope.launch {
17 | logCoroutineStart()
18 | delay(1000)
19 | logCoroutineEnd()
20 | }
21 | }
22 |
23 | private fun logCoroutineStart() {
24 | println("Coroutine running on: ${Thread.currentThread().name}")
25 | }
26 |
27 | private fun logCoroutineEnd() {
28 | println("Completed on: ${Thread.currentThread().name}")
29 | }
30 | }
--------------------------------------------------------------------------------
/week_4/char-yb/week4/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | spring.application.name=week4
2 |
--------------------------------------------------------------------------------
/week_4/char-yb/week4/src/test/kotlin/com/sipe/week4/Week4ApplicationTests.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week4
2 |
3 | import org.junit.jupiter.api.Test
4 | import org.springframework.boot.test.context.SpringBootTest
5 |
6 | @SpringBootTest
7 | class Week4ApplicationTests {
8 |
9 | @Test
10 | fun contextLoads() {
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/week_4/junseo/res/image (1).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_4/junseo/res/image (1).png
--------------------------------------------------------------------------------
/week_4/junseo/res/image (2).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_4/junseo/res/image (2).png
--------------------------------------------------------------------------------
/week_4/junseo/res/image (3).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_4/junseo/res/image (3).png
--------------------------------------------------------------------------------
/week_4/junseo/res/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_4/junseo/res/image.png
--------------------------------------------------------------------------------
/week_4/positive/4주차.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_4/positive/4주차.pdf
--------------------------------------------------------------------------------
/week_5/char-yb/week5/.editorconfig:
--------------------------------------------------------------------------------
1 | # top-most EditorConfig file
2 | root = true
3 |
4 | [*]
5 | # [encoding-utf8]
6 | charset = utf-8
7 |
8 | # [newline-lf]
9 | end_of_line = lf
10 |
11 | # [newline-eof]
12 | insert_final_newline = true
13 |
14 | [*.bat]
15 | end_of_line = crlf
16 |
17 | [*.{kt, kts}]
18 | charset = utf-8
19 | end_of_line = lf
20 | indent_size = 4
21 | indent_style = tab
22 | trim_trailing_whitespace = true
23 | insert_final_newline = true
24 | tab_width = 4
25 | max_line_length = 120
26 |
27 | # [no-wildcard-imports]
28 | ktlint_no-wildcard-imports = disabled
29 | ktlint_standard_no-wildcard-imports = disabled
30 | ktlint_standard_package-name = disabled
31 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/.gitattributes:
--------------------------------------------------------------------------------
1 | /gradlew text eol=lf
2 | *.bat text eol=crlf
3 | *.jar binary
4 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | .gradle
3 | build/
4 | !gradle/wrapper/gradle-wrapper.jar
5 | !**/src/main/**/build/
6 | !**/src/test/**/build/
7 |
8 | ### STS ###
9 | .apt_generated
10 | .classpath
11 | .factorypath
12 | .project
13 | .settings
14 | .springBeans
15 | .sts4-cache
16 | bin/
17 | !**/src/main/**/bin/
18 | !**/src/test/**/bin/
19 |
20 | ### IntelliJ IDEA ###
21 | .idea
22 | *.iws
23 | *.iml
24 | *.ipr
25 | out/
26 | !**/src/main/**/out/
27 | !**/src/test/**/out/
28 |
29 | ### NetBeans ###
30 | /nbproject/private/
31 | /nbbuild/
32 | /dist/
33 | /nbdist/
34 | /.nb-gradle/
35 |
36 | ### VS Code ###
37 | .vscode/
38 |
39 | ### Kotlin ###
40 | .kotlin
41 |
42 |
43 | ### Custom ###
44 | .env*
45 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/README.md:
--------------------------------------------------------------------------------
1 | ## 5주차
2 |
3 | - todo-list 구현해보기, CRUD
4 | - todo 생성하기
5 | - todo 단건 조회
6 | - todo 상태로 조회 (예정, 진행중, 완료), 상태는 index 없이.
7 | - equals 비교
8 | - test case를 만드는 api 구성. 5000만건 Todo 생성 후 진행
9 | - throughput -> jmeter
10 | - latency -> jmeter
11 | - webflux + coroutines 구현해보기!
12 | - mvc + virtual thread
13 | - throughput and memory, cpu resource 점검
14 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("jvm") version "1.9.25"
3 | kotlin("plugin.spring") version "1.9.25"
4 | id("org.springframework.boot") version "3.4.0"
5 | id("io.spring.dependency-management") version "1.1.6"
6 | id("org.jlleitschuh.gradle.ktlint") version "12.1.1"
7 | kotlin("plugin.jpa") version "1.9.25"
8 | }
9 |
10 | group = "com.sipe"
11 | version = "0.0.1-SNAPSHOT"
12 |
13 | java {
14 | toolchain {
15 | languageVersion = JavaLanguageVersion.of(21)
16 | }
17 | }
18 |
19 | repositories {
20 | mavenCentral()
21 | }
22 |
23 | dependencies {
24 | implementation("org.springframework.boot:spring-boot-starter-web")
25 | implementation("org.springframework.boot:spring-boot-starter-webflux")
26 | implementation("org.springframework.boot:spring-boot-starter-validation")
27 | implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
28 |
29 | // kotlin
30 | implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
31 | implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
32 | implementation("org.jetbrains.kotlin:kotlin-reflect")
33 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
34 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactive")
35 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
36 |
37 | // arrow-kt
38 | implementation("io.arrow-kt:arrow-core:1.2.4")
39 | implementation("io.arrow-kt:arrow-fx-coroutines:1.2.4")
40 | implementation("io.arrow-kt:arrow-fx-stm:1.2.4")
41 |
42 | // Database
43 | runtimeOnly("io.asyncer:r2dbc-mysql")
44 | runtimeOnly("com.mysql:mysql-connector-j")
45 |
46 | // JWT
47 | val jjwtVersion = "0.11.5"
48 | implementation("io.jsonwebtoken:jjwt-api:$jjwtVersion")
49 | runtimeOnly("io.jsonwebtoken:jjwt-impl:$jjwtVersion")
50 | runtimeOnly("io.jsonwebtoken:jjwt-jackson:$jjwtVersion")
51 |
52 | // Spring Security
53 | implementation("org.springframework.boot:spring-boot-starter-security")
54 | implementation("org.springframework.security:spring-security-test")
55 | implementation("org.springframework.security:spring-security-oauth2-client")
56 |
57 | testImplementation("org.springframework.boot:spring-boot-starter-test")
58 | testImplementation("io.projectreactor:reactor-test")
59 | testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
60 | testRuntimeOnly("org.junit.platform:junit-platform-launcher")
61 | /** etc **/
62 | runtimeOnly("io.netty:netty-resolver-dns-native-macos:4.1.104.Final:osx-aarch_64") // MacOS Silicon 라이브러리 누락 문제
63 | }
64 |
65 | kotlin {
66 | compilerOptions {
67 | freeCompilerArgs.addAll("-Xjsr305=strict")
68 | }
69 | }
70 |
71 | tasks.withType {
72 | useJUnitPlatform()
73 | }
74 |
75 | // scripts 경로의 pre-commit hook 등록
76 | tasks.register("addGitPreCommitHook", DefaultTask::class) {
77 | group = "setup"
78 | description = "Install git hooks"
79 | doLast {
80 | val hooksDir = project.file(".git/hooks")
81 | val scriptDir = project.file("scripts")
82 | val preCommit = scriptDir.resolve("pre-commit")
83 | preCommit.copyTo(hooksDir.resolve("pre-commit"), overwrite = true)
84 | hooksDir.resolve("pre-commit").setExecutable(true)
85 | }
86 | }
87 |
88 | // compileKotlin가 addGitPreCommitHook에 의존하도록 설정
89 | tasks.named("compileKotlin") {
90 | dependsOn("addGitPreCommitHook")
91 | }
92 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_5/char-yb/week5/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/week_5/char-yb/week5/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/scripts/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | targetFiles=$(git diff --staged --name-only)
4 |
5 | echo "Apply Ktlint.."
6 | ./gradlew ktlintFormat
7 | ./gradlew ktlintCheck
8 |
9 | # Add files to stage spotless applied
10 | for file in $targetFiles; do
11 | if test -f "$file"; then
12 | git add $file
13 | fi
14 | done
15 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "week5"
2 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/Week5Application.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5
2 |
3 | import org.springframework.boot.autoconfigure.SpringBootApplication
4 | import org.springframework.boot.runApplication
5 |
6 | @SpringBootApplication
7 | class Week5Application
8 |
9 | fun main(args: Array) {
10 | runApplication(*args)
11 | }
12 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/domain/auth/application/AuthService.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.domain.auth.application
2 |
3 | import com.sipe.week5.domain.auth.dto.TokenPairResponse
4 | import com.sipe.week5.domain.auth.dto.request.SignInRequest
5 | import com.sipe.week5.domain.auth.dto.request.SignUpRequest
6 | import com.sipe.week5.domain.member.domain.Member
7 | import com.sipe.week5.domain.member.infrastructure.ReactiveMemberRepository
8 | import com.sipe.week5.domain.member.infrastructure.SuspendableMemberRepository
9 | import com.sipe.week5.global.config.security.JwtTokenProvider
10 | import com.sipe.week5.global.exception.CustomException
11 | import com.sipe.week5.global.exception.ErrorCode
12 | import org.springframework.security.crypto.password.PasswordEncoder
13 | import org.springframework.stereotype.Service
14 | import org.springframework.transaction.annotation.Transactional
15 |
16 | @Service
17 | @Transactional
18 | class AuthService(
19 | private val suspendMemberRepository: SuspendableMemberRepository,
20 | private val reactiveMemberRepository: ReactiveMemberRepository,
21 | private val passwordEncoder: PasswordEncoder,
22 | private val jwtTokenProvider: JwtTokenProvider,
23 | ) {
24 | suspend fun signIn(signInRequest: SignInRequest): TokenPairResponse {
25 | val findMember =
26 | suspendMemberRepository.findByLoginId(signInRequest.loginId)
27 | ?: throw CustomException(ErrorCode.MEMBER_NOT_FOUND)
28 |
29 | // val findMemberReactive =
30 | // reactiveMemberRepository.findByLoginId(signInRequest.loginId)?.awaitSingle()
31 | // ?: throw CustomException(ErrorCode.MEMBER_NOT_FOUND)
32 |
33 | if (!passwordEncoder.matches(signInRequest.password, findMember.password)) {
34 | throw CustomException(ErrorCode.PASSWORD_NOT_MATCHES)
35 | }
36 |
37 | return getLoginResponse(findMember)
38 | }
39 |
40 | suspend fun signUp(signUpRequest: SignUpRequest): TokenPairResponse {
41 | suspendMemberRepository.findByLoginId(signUpRequest.loginId)?.let {
42 | throw CustomException(ErrorCode.MEMBER_ALREADY_REGISTERED)
43 | }
44 |
45 | // reactiveMemberRepository.findByLoginId(signUpRequest.loginId)?.awaitSingle()?.let {
46 | // throw CustomException(ErrorCode.MEMBER_ALREADY_REGISTERED)
47 | // }
48 |
49 | val saveMember =
50 | suspendMemberRepository.save(
51 | Member(
52 | loginId = signUpRequest.loginId,
53 | password = passwordEncoder.encode(signUpRequest.password),
54 | username = signUpRequest.username,
55 | ),
56 | )
57 |
58 | return getLoginResponse(saveMember)
59 | }
60 |
61 | private fun getLoginResponse(member: Member): TokenPairResponse {
62 | val accessToken: String = jwtTokenProvider.createAccessToken(member)
63 | val refreshToken: String = jwtTokenProvider.createRefreshToken(member)
64 |
65 | return TokenPairResponse.of(accessToken, refreshToken)
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/domain/auth/dto/AccessTokenDto.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.domain.auth.dto
2 |
3 | import com.sipe.week5.domain.member.domain.MemberRole
4 |
5 | data class AccessTokenDto(val memberId: Long, val memberRole: MemberRole, val tokenValue: String)
6 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/domain/auth/dto/RefreshTokenDto.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.domain.auth.dto
2 |
3 | data class RefreshTokenDto(val memberId: Long, val tokenValue: String, val ttl: Long)
4 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/domain/auth/dto/TokenPairResponse.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.domain.auth.dto
2 |
3 | data class TokenPairResponse(
4 | val accessToken: String,
5 | val refreshToken: String,
6 | ) {
7 | companion object {
8 | fun of(
9 | accessToken: String,
10 | refreshToken: String,
11 | ) = TokenPairResponse(accessToken, refreshToken)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/domain/auth/dto/TokenType.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.domain.auth.dto
2 | import java.util.*
3 |
4 | enum class TokenType(val value: String) {
5 | ACCESS("access"),
6 | REFRESH("refresh"),
7 | ;
8 |
9 | companion object {
10 | fun from(typeKey: String): TokenType =
11 | typeKey.takeIf { it.isNotBlank() }
12 | ?.uppercase(Locale.ENGLISH)
13 | ?.let { key ->
14 | when (key) {
15 | "ACCESS" -> ACCESS
16 | "REFRESH" -> REFRESH
17 | else -> throw IllegalArgumentException("Invalid token type: $typeKey")
18 | }
19 | } ?: throw IllegalArgumentException("Token type cannot be null or empty")
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/domain/auth/dto/request/SignInRequest.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.domain.auth.dto.request
2 |
3 | data class SignInRequest(
4 | val loginId: String,
5 | val password: String,
6 | )
7 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/domain/auth/dto/request/SignUpRequest.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.domain.auth.dto.request
2 |
3 | data class SignUpRequest(
4 | val loginId: String,
5 | val password: String,
6 | val username: String,
7 | )
8 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/domain/auth/exception/AuthenticationException.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.domain.auth.exception
2 |
3 | import com.sipe.week5.global.exception.CustomException
4 | import com.sipe.week5.global.exception.ErrorCode
5 |
6 | open class AuthenticationException : CustomException {
7 | constructor(errorCode: ErrorCode) : super(errorCode)
8 | constructor(errorCode: ErrorCode, data: Any? = null) : super(errorCode, data)
9 | }
10 |
11 | class AuthenticationInvalidTokenException : AuthenticationException(ErrorCode.INVALID_AUTH_TOKEN)
12 |
13 | class AuthenticationExpiredAccessTokenException : AuthenticationException(ErrorCode.EXPIRED_ACCESS_TOKEN)
14 |
15 | class AuthenticationExpiredRefreshTokenException : AuthenticationException(ErrorCode.EXPIRED_REFRESH_TOKEN)
16 |
17 | class AuthenticationTokenNotExistException : AuthenticationException(ErrorCode.NOT_EXIST_TOKEN)
18 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/domain/auth/presentation/AuthController.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.domain.auth.presentation
2 |
3 | import com.sipe.week5.domain.auth.application.AuthService
4 | import com.sipe.week5.domain.auth.dto.TokenPairResponse
5 | import com.sipe.week5.domain.auth.dto.request.SignInRequest
6 | import com.sipe.week5.domain.auth.dto.request.SignUpRequest
7 | import jakarta.validation.Valid
8 | import org.springframework.web.bind.annotation.PostMapping
9 | import org.springframework.web.bind.annotation.RequestBody
10 | import org.springframework.web.bind.annotation.RequestMapping
11 | import org.springframework.web.bind.annotation.RestController
12 |
13 | @RestController
14 | @RequestMapping("/auth")
15 | class AuthController(
16 | private val authService: AuthService,
17 | ) {
18 | @PostMapping("/signIn")
19 | suspend fun signIn(
20 | @RequestBody request: @Valid SignInRequest,
21 | ): TokenPairResponse = authService.signIn(request)
22 |
23 | @PostMapping("/signUp")
24 | suspend fun signUp(
25 | @RequestBody request: @Valid SignUpRequest,
26 | ): TokenPairResponse = authService.signUp(request)
27 | }
28 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/domain/common/BaseEntity.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.domain.common
2 |
3 | import com.fasterxml.jackson.annotation.JsonFormat
4 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties
5 | import org.springframework.data.annotation.CreatedDate
6 | import org.springframework.data.annotation.LastModifiedDate
7 | import org.springframework.data.relational.core.mapping.Column
8 | import java.time.LocalDateTime
9 |
10 | @JsonIgnoreProperties(value = ["createdAt, modifiedAt"], allowGetters = true)
11 | open class BaseEntity(
12 | /** 생성일 */
13 | @CreatedDate
14 | @Column("created_at")
15 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "Asia/Seoul")
16 | var createdAt: LocalDateTime = LocalDateTime.now(),
17 | /** 수정일 */
18 | @LastModifiedDate
19 | @Column("modified_at")
20 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "Asia/Seoul")
21 | var modifiedAt: LocalDateTime = LocalDateTime.now(),
22 | )
23 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/domain/member/application/MemberService.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.domain.member.application
2 |
3 | import com.sipe.week5.domain.member.dto.response.FindOneMemberResponse
4 | import com.sipe.week5.domain.member.infrastructure.SuspendableMemberRepository
5 | import com.sipe.week5.global.util.member.MemberUtil
6 | import org.springframework.stereotype.Service
7 | import org.springframework.transaction.annotation.Transactional
8 |
9 | @Service
10 | class MemberService(
11 | private val memberRepository: SuspendableMemberRepository,
12 | private val memberUtil: MemberUtil,
13 | ) {
14 | @Transactional(readOnly = true)
15 | suspend fun findMemberMe(): FindOneMemberResponse = FindOneMemberResponse.from(memberUtil.getCurrentMember())
16 | }
17 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/domain/member/domain/Member.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.domain.member.domain
2 |
3 | import com.sipe.week5.domain.common.BaseEntity
4 | import org.springframework.data.annotation.Id
5 | import org.springframework.data.relational.core.mapping.Column
6 | import org.springframework.data.relational.core.mapping.Table
7 | import kotlin.reflect.full.isSubclassOf
8 |
9 | @Table
10 | class Member(
11 | @Id
12 | @Column("member_id")
13 | val id: Long = 0L,
14 | @Column("login_id")
15 | val loginId: String,
16 | @Column("username")
17 | var username: String,
18 | @Column("password")
19 | var password: String,
20 | @Column("role")
21 | var role: MemberRole = MemberRole.USER,
22 | ) : BaseEntity() {
23 | // Proxy 객체 고려하여 equals Override, https://zins.tistory.com/19
24 | override fun equals(other: Any?): Boolean {
25 | if (this === other) return true
26 | if (other !is Member) return false
27 | if (!compareClassesIncludeProxy(other)) return false
28 | if (id != other.id) return false
29 | return true
30 | }
31 |
32 | private fun compareClassesIncludeProxy(other: Any) =
33 | this::class.isSubclassOf(other::class) ||
34 | other::class.isSubclassOf(this::class)
35 |
36 | override fun hashCode(): Int = id.hashCode()
37 | }
38 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/domain/member/domain/MemberRole.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.domain.member.domain
2 |
3 | enum class MemberRole(val value: String?) {
4 | USER("ROLE_USER"),
5 | ADMIN("ROLE_ADMIN"),
6 | }
7 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/domain/member/dto/response/FindOneMemberResponse.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.domain.member.dto.response
2 |
3 | import com.sipe.week5.domain.member.domain.Member
4 | import com.sipe.week5.domain.member.domain.MemberRole
5 |
6 | data class FindOneMemberResponse(
7 | val id: Long,
8 | val username: String,
9 | val loginId: String,
10 | val role: MemberRole,
11 | ) {
12 | companion object {
13 | fun from(member: Member) =
14 | FindOneMemberResponse(
15 | id = member.id,
16 | username = member.username,
17 | loginId = member.loginId,
18 | role = member.role,
19 | )
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/domain/member/infrastructure/MemberRepository.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.domain.member.infrastructure
2 |
3 | import com.sipe.week5.domain.member.domain.Member
4 | import org.springframework.data.r2dbc.repository.R2dbcRepository
5 | import org.springframework.data.repository.kotlin.CoroutineCrudRepository
6 | import org.springframework.stereotype.Repository
7 | import reactor.core.publisher.Mono
8 |
9 | @Repository
10 | interface SuspendableMemberRepository : CoroutineCrudRepository {
11 | suspend fun findByLoginId(loginId: String): Member?
12 | }
13 |
14 | @Repository
15 | interface ReactiveMemberRepository : R2dbcRepository {
16 | fun findByLoginId(loginId: String): Mono?
17 | }
18 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/domain/member/presentation/MemberController.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.domain.member.presentation
2 |
3 | import com.sipe.week5.domain.member.application.MemberService
4 | import com.sipe.week5.domain.member.dto.response.FindOneMemberResponse
5 | import org.springframework.web.bind.annotation.GetMapping
6 | import org.springframework.web.bind.annotation.RequestMapping
7 | import org.springframework.web.bind.annotation.RestController
8 |
9 | @RestController
10 | @RequestMapping("/members")
11 | class MemberController(
12 | private val memberService: MemberService,
13 | ) {
14 | @GetMapping("/me")
15 | suspend fun memberFindMe(): FindOneMemberResponse = memberService.findMemberMe()
16 | }
17 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/domain/todo/application/TodoService.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.domain.todo.application
2 |
3 | import com.sipe.week5.domain.todo.domain.TodoEntity
4 | import com.sipe.week5.domain.todo.domain.TodoStatus
5 | import com.sipe.week5.domain.todo.dto.request.CreateTodoRequest
6 | import com.sipe.week5.domain.todo.infrastructure.ReactiveTodoRepository
7 | import com.sipe.week5.domain.todo.infrastructure.SuspendableTodoRepository
8 | import com.sipe.week5.global.exception.CustomException
9 | import com.sipe.week5.global.exception.ErrorCode
10 | import com.sipe.week5.global.util.logging.logger
11 | import com.sipe.week5.global.util.member.MemberUtil
12 | import kotlinx.coroutines.Dispatchers
13 | import kotlinx.coroutines.flow.toList
14 | import kotlinx.coroutines.reactive.awaitFirst
15 | import kotlinx.coroutines.withContext
16 | import org.springframework.stereotype.Service
17 | import org.springframework.transaction.annotation.Transactional
18 |
19 | @Service
20 | @Transactional
21 | class TodoService(
22 | private val suspendTodoRepository: SuspendableTodoRepository,
23 | private val reactiveTodoRepository: ReactiveTodoRepository,
24 | private val memberUtil: MemberUtil,
25 | ) {
26 | private val log by logger()
27 |
28 | // 생성
29 | suspend fun createTodo(request: CreateTodoRequest): TodoEntity {
30 | val todo =
31 | TodoEntity(
32 | title = request.title,
33 | content = request.content,
34 | dueDate = request.dueDate,
35 | memberId = memberUtil.getCurrentMember().id,
36 | )
37 | return withContext(Dispatchers.IO) { suspendTodoRepository.save(todo) }
38 | }
39 |
40 | @Transactional(readOnly = true)
41 | suspend fun findOneTodo(todoId: Long): TodoEntity? =
42 | runCatching {
43 | withContext(Dispatchers.IO) {
44 | suspendTodoRepository.findById(todoId)
45 | }
46 | }.onFailure {
47 | log.error("findOneTodo error: ${it.message}")
48 | throw CustomException(ErrorCode.TODO_NOT_FOUND)
49 | }.getOrThrow()
50 |
51 | // 리스트 조회
52 | @Transactional(readOnly = true)
53 | suspend fun findListTodo(): List =
54 | withContext(Dispatchers.IO) {
55 | suspendTodoRepository.findAll().toList()
56 | }
57 |
58 | // 현재 사용자의 할 일 조회
59 | @Transactional(readOnly = true)
60 | suspend fun findByCurrentMemberTodo(): TodoEntity? {
61 | val currentMember = memberUtil.getCurrentMember()
62 | return runCatching {
63 | withContext(Dispatchers.IO) {
64 | suspendTodoRepository.findByMemberId(currentMember.id)
65 | }
66 | }.onFailure {
67 | log.error("findByCurrentMemberTodo error: ${it.message}")
68 | throw CustomException(ErrorCode.TODO_NOT_FOUND)
69 | }.getOrThrow()
70 | }
71 |
72 | @Transactional(readOnly = true)
73 | suspend fun findTodoByStatus(status: String): List =
74 | reactiveTodoRepository.findAllByStatus(TodoStatus.valueOf(status)).collectList().awaitFirst()
75 | }
76 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/domain/todo/domain/TodoEntity.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.domain.todo.domain
2 |
3 | import com.sipe.week5.domain.common.BaseEntity
4 | import org.springframework.data.annotation.Id
5 | import org.springframework.data.relational.core.mapping.Column
6 | import org.springframework.data.relational.core.mapping.Table
7 | import java.time.LocalDate
8 | import kotlin.reflect.full.isSubclassOf
9 |
10 | @Table("todo")
11 | class TodoEntity(
12 | @Id
13 | @Column("todo_id")
14 | val id: Long = 0L,
15 | val title: String,
16 | val content: String,
17 | val dueDate: LocalDate,
18 | @Column("status")
19 | val status: TodoStatus = TodoStatus.TODO,
20 | @Column("member_id")
21 | val memberId: Long,
22 | ) : BaseEntity() {
23 | override fun equals(other: Any?): Boolean {
24 | if (this === other) return true
25 | if (other !is TodoEntity) return false
26 | if (!compareClassesIncludeProxy(other)) return false
27 | if (id != other.id) return false
28 | return true
29 | }
30 |
31 | private fun compareClassesIncludeProxy(other: Any) =
32 | this::class.isSubclassOf(other::class) ||
33 | other::class.isSubclassOf(this::class)
34 |
35 | override fun hashCode(): Int = id.hashCode()
36 | }
37 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/domain/todo/domain/TodoStatus.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.domain.todo.domain
2 |
3 | enum class TodoStatus {
4 | TODO,
5 | IN_PROGRESS,
6 | DONE,
7 | }
8 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/domain/todo/dto/request/CreateTodoRequest.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.domain.todo.dto.request
2 |
3 | import java.time.LocalDate
4 |
5 | data class CreateTodoRequest(
6 | val title: String,
7 | val content: String,
8 | val dueDate: LocalDate,
9 | )
10 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/domain/todo/infrastructure/TodoRepository.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.domain.todo.infrastructure
2 |
3 | import com.sipe.week5.domain.todo.domain.TodoEntity
4 | import com.sipe.week5.domain.todo.domain.TodoStatus
5 | import org.springframework.data.r2dbc.repository.R2dbcRepository
6 | import org.springframework.data.repository.kotlin.CoroutineCrudRepository
7 | import org.springframework.stereotype.Repository
8 | import reactor.core.publisher.Flux
9 | import reactor.core.publisher.Mono
10 |
11 | @Repository
12 | interface SuspendableTodoRepository : CoroutineCrudRepository {
13 | suspend fun findByMemberId(memberId: Long): TodoEntity?
14 |
15 | suspend fun findAllByStatus(status: TodoStatus): List
16 | }
17 |
18 | @Repository
19 | interface ReactiveTodoRepository : R2dbcRepository {
20 | fun findByMemberId(memberId: Long): Mono?
21 |
22 | fun findAllByStatus(status: TodoStatus): Flux
23 | }
24 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/domain/todo/presentation/TodoController.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.domain.todo.presentation
2 |
3 | import com.sipe.week5.domain.todo.application.TodoService
4 | import com.sipe.week5.domain.todo.domain.TodoEntity
5 | import com.sipe.week5.domain.todo.dto.request.CreateTodoRequest
6 | import org.springframework.web.bind.annotation.*
7 |
8 | @RestController
9 | @RequestMapping("/todo")
10 | class TodoController(
11 | private val todoService: TodoService,
12 | ) {
13 | @PostMapping
14 | suspend fun createTodo(
15 | @RequestBody request: CreateTodoRequest,
16 | ): TodoEntity = todoService.createTodo(request)
17 |
18 | @GetMapping
19 | suspend fun findListTodo(): List = todoService.findListTodo()
20 |
21 | @GetMapping("/{todoId}")
22 | suspend fun findOneTodo(
23 | @PathVariable todoId: Long,
24 | ): TodoEntity? = todoService.findOneTodo(todoId)
25 |
26 | @GetMapping("/search")
27 | suspend fun findTodoByStatus(
28 | @RequestParam status: String,
29 | ): List = todoService.findTodoByStatus(status)
30 |
31 | @GetMapping("/me")
32 | suspend fun findByCurrentMemberTodo(): TodoEntity? = todoService.findByCurrentMemberTodo()
33 | }
34 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/global/common/constants/SecurityConstants.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.global.common.constants
2 |
3 | object SecurityConstants {
4 | const val TOKEN_ROLE_NAME: String = "role"
5 | const val TOKEN_PREFIX: String = "Bearer "
6 | const val ACCESS_TOKEN_COOKIE_NAME: String = "accessToken"
7 | const val REFRESH_TOKEN_COOKIE_NAME: String = "refreshToken"
8 | }
9 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/global/common/response/GlobalResponse.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.global.common.response
2 | import com.sipe.week5.global.error.ErrorResponse
3 | import java.time.LocalDateTime
4 |
5 | data class GlobalResponse(val success: Boolean, val status: Int, val data: Any, val timestamp: LocalDateTime) {
6 | companion object {
7 | @JvmStatic
8 | fun success(
9 | status: Int,
10 | data: Any,
11 | ) = GlobalResponse(true, status, data, LocalDateTime.now())
12 |
13 | fun fail(
14 | status: Int,
15 | errorResponse: ErrorResponse,
16 | ) = GlobalResponse(false, status, errorResponse, LocalDateTime.now())
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/global/common/response/GlobalResponseAdvice.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.global.common.response
2 |
3 | import org.springframework.core.MethodParameter
4 | import org.springframework.http.HttpStatus
5 | import org.springframework.http.MediaType
6 | import org.springframework.http.converter.HttpMessageConverter
7 | import org.springframework.http.server.ServerHttpRequest
8 | import org.springframework.http.server.ServerHttpResponse
9 | import org.springframework.http.server.ServletServerHttpResponse
10 | import org.springframework.web.bind.annotation.RestControllerAdvice
11 | import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice
12 |
13 | @RestControllerAdvice(basePackages = ["com.sipe"])
14 | class GlobalResponseAdvice : ResponseBodyAdvice {
15 | override fun supports(
16 | returnType: MethodParameter,
17 | converterType: Class>,
18 | ): Boolean {
19 | return true
20 | }
21 |
22 | override fun beforeBodyWrite(
23 | body: Any?,
24 | returnType: MethodParameter,
25 | selectedContentType: MediaType,
26 | selectedConverterType: Class>,
27 | request: ServerHttpRequest,
28 | response: ServerHttpResponse,
29 | ): Any? {
30 | val servletResponse = (response as? ServletServerHttpResponse)?.servletResponse ?: return body
31 | val status = servletResponse.status
32 | val resolve = HttpStatus.resolve(status)
33 |
34 | return when {
35 | resolve == null || body == null || body is String -> body
36 | resolve.is2xxSuccessful -> GlobalResponse.success(status, body)
37 | else -> body
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/global/config/database/R2dbcConfig.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.global.config.database
2 |
3 | import org.springframework.context.annotation.Configuration
4 | import org.springframework.data.r2dbc.config.EnableR2dbcAuditing
5 | import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories
6 |
7 | @Configuration
8 | @EnableR2dbcAuditing
9 | @EnableR2dbcRepositories
10 | class R2dbcConfig
11 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/global/config/properties/JwtProperties.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.global.config.properties
2 |
3 | import org.springframework.boot.context.properties.ConfigurationProperties
4 |
5 | @ConfigurationProperties(prefix = "jwt")
6 | data class JwtProperties(
7 | val accessTokenSecret: String,
8 | val refreshTokenSecret: String,
9 | val accessTokenExpirationTime: Long,
10 | val refreshTokenExpirationTime: Long,
11 | ) {
12 | fun accessTokenExpirationMilliTime(): Long {
13 | return accessTokenExpirationTime * 1000
14 | }
15 |
16 | fun refreshTokenExpirationMilliTime(): Long {
17 | return refreshTokenExpirationTime * 1000
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/global/config/properties/PropertiesConfig.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.global.config.properties
2 | import org.springframework.boot.context.properties.EnableConfigurationProperties
3 | import org.springframework.context.annotation.Configuration
4 |
5 | @EnableConfigurationProperties(
6 | JwtProperties::class,
7 | )
8 | @Configuration
9 | class PropertiesConfig
10 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/global/config/reactor/ReactorSchedulerConfig.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.global.config.reactor
2 |
3 | import org.springframework.context.annotation.Bean
4 | import org.springframework.context.annotation.Configuration
5 | import reactor.core.scheduler.Scheduler
6 | import reactor.core.scheduler.Schedulers
7 |
8 | @Configuration
9 | class ReactorSchedulerConfig {
10 | @Bean
11 | fun ioScheduler(): Scheduler {
12 | return Schedulers.boundedElastic()
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/global/config/security/JwtTokenProvider.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.global.config.security
2 |
3 | import com.sipe.week5.domain.auth.dto.AccessTokenDto
4 | import com.sipe.week5.domain.auth.dto.TokenType
5 | import com.sipe.week5.domain.auth.dto.TokenType.ACCESS
6 | import com.sipe.week5.domain.auth.dto.TokenType.REFRESH
7 | import com.sipe.week5.domain.auth.exception.AuthenticationExpiredAccessTokenException
8 | import com.sipe.week5.domain.auth.exception.AuthenticationExpiredRefreshTokenException
9 | import com.sipe.week5.domain.auth.exception.AuthenticationInvalidTokenException
10 | import com.sipe.week5.domain.member.domain.Member
11 | import com.sipe.week5.domain.member.domain.MemberRole
12 | import com.sipe.week5.global.common.constants.SecurityConstants.TOKEN_ROLE_NAME
13 | import com.sipe.week5.global.config.properties.JwtProperties
14 | import io.jsonwebtoken.*
15 | import io.jsonwebtoken.security.Keys
16 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
17 | import org.springframework.security.core.Authentication
18 | import org.springframework.stereotype.Component
19 | import java.security.Key
20 | import java.util.*
21 |
22 | @Component
23 | class JwtTokenProvider(
24 | private val jwtProperties: JwtProperties,
25 | ) {
26 | companion object {
27 | private const val TOKEN_TYPE_KEY_NAME = "type"
28 | private const val USER_ID_KEY_NAME = "memberId"
29 | }
30 |
31 | private val refreshTokenKey: Key
32 | get() = Keys.hmacShaKeyFor(jwtProperties.refreshTokenSecret.toByteArray())
33 |
34 | private val accessTokenKey: Key
35 | get() = Keys.hmacShaKeyFor(jwtProperties.accessTokenSecret.toByteArray())
36 |
37 | private fun createTokenHeader(tokenType: TokenType): Map {
38 | return mapOf(
39 | "typ" to "JWT" as Any,
40 | "alg" to "HS256" as Any,
41 | "regDate" to System.currentTimeMillis() as Any,
42 | TOKEN_TYPE_KEY_NAME to tokenType.value as Any,
43 | )
44 | }
45 |
46 | fun createAccessToken(member: Member): String =
47 | Jwts.builder()
48 | .setHeader(createTokenHeader(ACCESS))
49 | .setSubject(member.id.toString())
50 | .claim(TOKEN_ROLE_NAME, member.role.name)
51 | .claim(USER_ID_KEY_NAME, member.id)
52 | .setExpiration(Date(System.currentTimeMillis() + jwtProperties.accessTokenExpirationTime * 1000))
53 | .signWith(accessTokenKey)
54 | .compact()
55 |
56 | fun createRefreshToken(member: Member): String =
57 | Jwts.builder()
58 | .setHeader(createTokenHeader(REFRESH))
59 | .setSubject(member.id.toString())
60 | .claim(TOKEN_ROLE_NAME, member.role.name)
61 | .claim(USER_ID_KEY_NAME, member.id)
62 | .setExpiration(Date(System.currentTimeMillis() + jwtProperties.refreshTokenExpirationTime * 1000))
63 | .signWith(refreshTokenKey)
64 | .compact()
65 |
66 | @Throws(ExpiredJwtException::class)
67 | fun parseAccessToken(token: String): AccessTokenDto {
68 | // 토큰 파싱하여 성공하면 AccessTokenDto 반환, 실패하면 null 반환
69 | // 만료된 토큰인 경우에만 ExpiredJwtException 발생
70 | try {
71 | val claims: Jws = getClaims(token, accessTokenKey)
72 |
73 | return AccessTokenDto(
74 | claims.body.subject.toLong(),
75 | MemberRole.valueOf(claims.body.get(TOKEN_ROLE_NAME, String::class.java)),
76 | token,
77 | )
78 | } catch (e: ExpiredJwtException) {
79 | throw e
80 | }
81 | }
82 |
83 | private fun getClaims(
84 | token: String,
85 | key: Key,
86 | ): Jws {
87 | return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token)
88 | }
89 |
90 | fun parseToken(accessToken: String): Authentication {
91 | val claims: Jws = getClaims(accessToken, ACCESS.value, accessTokenKey)
92 |
93 | if (getTokenType(claims) != ACCESS.value) {
94 | throw AuthenticationInvalidTokenException()
95 | }
96 |
97 | return UsernamePasswordAuthenticationToken(getMemberId(claims), null, emptyList())
98 | }
99 |
100 | private fun getClaims(
101 | token: String,
102 | tokenType: String,
103 | key: Key,
104 | ) = runCatching { Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token) }
105 | .getOrElse {
106 | when (it) {
107 | is ExpiredJwtException ->
108 | when (tokenType) {
109 | ACCESS.name -> throw AuthenticationExpiredAccessTokenException()
110 | REFRESH.name -> throw AuthenticationExpiredRefreshTokenException()
111 | else -> throw AuthenticationInvalidTokenException()
112 | }
113 |
114 | else -> throw AuthenticationInvalidTokenException()
115 | }
116 | }
117 |
118 | private fun getTokenType(claims: Jws): String =
119 | runCatching { claims.header[TOKEN_TYPE_KEY_NAME] as? String? ?: throw AuthenticationInvalidTokenException() }
120 | .getOrElse { throw AuthenticationInvalidTokenException() }
121 |
122 | private fun getRole(claims: Jws): String =
123 | runCatching { claims.body[TOKEN_ROLE_NAME] as? String? ?: throw AuthenticationInvalidTokenException() }
124 | .getOrElse { throw AuthenticationInvalidTokenException() }
125 |
126 | private fun getMemberId(claims: Jws): String =
127 | runCatching { claims.body.subject }
128 | .getOrElse { throw AuthenticationInvalidTokenException() }
129 | }
130 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/global/config/security/PrincipalDetails.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.global.config.security
2 |
3 | import com.sipe.week5.domain.member.domain.MemberRole
4 | import org.springframework.security.core.GrantedAuthority
5 | import org.springframework.security.core.authority.SimpleGrantedAuthority
6 | import org.springframework.security.core.userdetails.UserDetails
7 |
8 | class PrincipalDetails(
9 | private val memberId: Long,
10 | private val role: MemberRole,
11 | ) : UserDetails {
12 | override fun getAuthorities(): MutableCollection {
13 | return mutableListOf(SimpleGrantedAuthority(role.value))
14 | }
15 |
16 | override fun getPassword(): String? {
17 | return null
18 | }
19 |
20 | override fun getUsername(): String {
21 | return memberId.toString()
22 | }
23 |
24 | override fun isAccountNonExpired(): Boolean {
25 | return true
26 | }
27 |
28 | override fun isAccountNonLocked(): Boolean {
29 | return true
30 | }
31 |
32 | override fun isCredentialsNonExpired(): Boolean {
33 | return true
34 | }
35 |
36 | override fun isEnabled(): Boolean {
37 | return true
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/global/config/security/WebSecurityConfig.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.global.config.security
2 | import com.sipe.week5.global.filter.JwtAuthenticationFilter
3 | import jakarta.servlet.http.HttpServletRequest
4 | import jakarta.servlet.http.HttpServletResponse
5 | import org.springframework.context.annotation.Bean
6 | import org.springframework.context.annotation.Configuration
7 | import org.springframework.core.annotation.Order
8 | import org.springframework.http.HttpHeaders
9 | import org.springframework.security.config.Customizer.withDefaults
10 | import org.springframework.security.config.annotation.web.builders.HttpSecurity
11 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
12 | import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer
13 | import org.springframework.security.config.http.SessionCreationPolicy
14 | import org.springframework.security.core.AuthenticationException
15 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
16 | import org.springframework.security.crypto.password.PasswordEncoder
17 | import org.springframework.security.web.SecurityFilterChain
18 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
19 | import org.springframework.web.cors.CorsConfiguration
20 | import org.springframework.web.cors.CorsConfigurationSource
21 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource
22 | import org.springframework.web.servlet.HandlerExceptionResolver
23 |
24 | @Configuration
25 | @EnableWebSecurity
26 | class WebSecurityConfig(
27 | private val jwtTokenProvider: JwtTokenProvider,
28 | private val handlerExceptionResolver: HandlerExceptionResolver,
29 | ) {
30 | @Bean
31 | fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()
32 |
33 | @Bean
34 | @Order(0)
35 | fun loginFilterChain(http: HttpSecurity): SecurityFilterChain =
36 | http
37 | .applyCommonConfigurations()
38 | .securityMatcher("/**", "/swagger-ui/**", "/v3/api-docs/**")
39 | .authorizeHttpRequests { it.anyRequest().permitAll() }
40 | .build()
41 |
42 | @Bean
43 | @Order(1)
44 | fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
45 | http
46 | .applyCommonConfigurations()
47 | .authorizeHttpRequests { authorize ->
48 | authorize
49 | .requestMatchers("/auth/signIn").permitAll()
50 | .requestMatchers("/auth/signUp").permitAll()
51 | .anyRequest().authenticated()
52 | }
53 | .exceptionHandling { exception: ExceptionHandlingConfigurer ->
54 | exception.authenticationEntryPoint {
55 | _: HttpServletRequest?,
56 | response: HttpServletResponse,
57 | _: AuthenticationException?,
58 | ->
59 | response.status = 401
60 | }
61 | }
62 |
63 | http.addFilterBefore(
64 | JwtAuthenticationFilter(jwtTokenProvider, handlerExceptionResolver),
65 | UsernamePasswordAuthenticationFilter::class.java,
66 | )
67 |
68 | return http.build()
69 | }
70 |
71 | @Bean
72 | fun corsConfigurationSource(): CorsConfigurationSource {
73 | val configuration =
74 | CorsConfiguration().apply {
75 | addAllowedOriginPattern("*") // TODO: CORS 임시 전체 허용
76 | addAllowedHeader("*")
77 | addAllowedMethod("*")
78 | allowCredentials = true
79 | addExposedHeader(HttpHeaders.SET_COOKIE)
80 | }
81 | return UrlBasedCorsConfigurationSource().apply {
82 | registerCorsConfiguration("/**", configuration)
83 | }
84 | }
85 |
86 | private fun HttpSecurity.applyCommonConfigurations(): HttpSecurity =
87 | this
88 | .httpBasic { it.disable() }
89 | .formLogin { it.disable() }
90 | .csrf { it.disable() }
91 | .cors(withDefaults())
92 | .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
93 | }
94 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/global/config/webflux/WebFluxConfig.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.global.config.webflux
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper
4 | import org.springframework.context.annotation.Configuration
5 | import org.springframework.core.ReactiveAdapterRegistry
6 | import org.springframework.data.web.ReactivePageableHandlerMethodArgumentResolver
7 | import org.springframework.data.web.ReactiveSortHandlerMethodArgumentResolver
8 | import org.springframework.format.FormatterRegistry
9 | import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar
10 | import org.springframework.http.codec.ServerCodecConfigurer
11 | import org.springframework.http.codec.json.Jackson2JsonDecoder
12 | import org.springframework.http.codec.json.Jackson2JsonEncoder
13 | import org.springframework.util.MimeType
14 | import org.springframework.web.cors.CorsConfiguration
15 | import org.springframework.web.reactive.config.CorsRegistry
16 | import org.springframework.web.reactive.config.WebFluxConfigurer
17 | import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer
18 | import java.nio.charset.Charset
19 |
20 | @Configuration
21 | class WebFluxConfig(
22 | private val objectMapper: ObjectMapper,
23 | ) : WebFluxConfigurer {
24 | override fun addCorsMappings(registry: CorsRegistry) {
25 | registry.addMapping("/**")
26 | .allowedOriginPatterns(CorsConfiguration.ALL)
27 | .allowedMethods(CorsConfiguration.ALL)
28 | .allowedHeaders(CorsConfiguration.ALL)
29 | .allowCredentials(true)
30 | .maxAge(3600)
31 | }
32 |
33 | override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
34 | val mimeTypes =
35 | arrayOf(
36 | MimeType("application", "json"),
37 | MimeType("application", "*+json"),
38 | MimeType("application", "json", Charset.forName("UTF-8")),
39 | )
40 |
41 | configurer.defaultCodecs().jackson2JsonEncoder(Jackson2JsonEncoder(objectMapper, *mimeTypes))
42 | configurer.defaultCodecs().jackson2JsonDecoder(Jackson2JsonDecoder(objectMapper))
43 | }
44 |
45 | override fun configureArgumentResolvers(configurer: ArgumentResolverConfigurer) {
46 | ReactiveAdapterRegistry()
47 | val serverCodecConfigurer = ServerCodecConfigurer.create()
48 | configureHttpMessageCodecs(serverCodecConfigurer)
49 |
50 | configurer.addCustomResolver(
51 | ReactiveSortHandlerMethodArgumentResolver(),
52 | ReactivePageableHandlerMethodArgumentResolver(),
53 | )
54 | }
55 |
56 | override fun addFormatters(registry: FormatterRegistry) {
57 | val registrar = DateTimeFormatterRegistrar()
58 | registrar.setUseIsoFormat(true)
59 | registrar.registerFormatters(registry)
60 | super.addFormatters(registry)
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/global/error/ErrorResponse.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.global.error
2 |
3 | data class ErrorResponse(val errorClassName: String, val message: String) {
4 | companion object {
5 | fun of(
6 | errorClassName: String,
7 | message: String,
8 | ): ErrorResponse {
9 | return ErrorResponse(errorClassName, message)
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/global/error/GlobalExceptionHandler.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.global.error
2 | import com.sipe.week5.global.common.response.GlobalResponse
3 | import com.sipe.week5.global.exception.CustomException
4 | import com.sipe.week5.global.exception.ErrorCode
5 | import com.sipe.week5.global.util.logging.logger
6 | import jakarta.validation.ConstraintViolation
7 | import jakarta.validation.ConstraintViolationException
8 | import org.springframework.http.HttpHeaders
9 | import org.springframework.http.HttpStatus
10 | import org.springframework.http.HttpStatusCode
11 | import org.springframework.http.ResponseEntity
12 | import org.springframework.web.HttpRequestMethodNotSupportedException
13 | import org.springframework.web.bind.MethodArgumentNotValidException
14 | import org.springframework.web.bind.annotation.ExceptionHandler
15 | import org.springframework.web.bind.annotation.RestControllerAdvice
16 | import org.springframework.web.context.request.WebRequest
17 | import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException
18 | import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler
19 | import java.util.function.Consumer
20 |
21 | @RestControllerAdvice
22 | class GlobalExceptionHandler : ResponseEntityExceptionHandler() {
23 | private val log by logger()
24 |
25 | override fun handleExceptionInternal(
26 | ex: Exception,
27 | body: Any?,
28 | headers: HttpHeaders,
29 | statusCode: HttpStatusCode,
30 | request: WebRequest,
31 | ): ResponseEntity? {
32 | val errorResponse =
33 | ErrorResponse.of(ex.javaClass.simpleName, ex.message!!)
34 | return super.handleExceptionInternal(ex, errorResponse, headers, statusCode, request)
35 | }
36 |
37 | /**
38 | * javax.validation.Valid or @Validated 으로 binding error 발생시 발생한다. HttpMessageConverter 에서 등록한
39 | * HttpMessageConverter binding 못할경우 발생 주로 @RequestBody, @RequestPart 어노테이션에서 발생
40 | */
41 | override fun handleMethodArgumentNotValid(
42 | e: MethodArgumentNotValidException,
43 | headers: HttpHeaders,
44 | status: HttpStatusCode,
45 | request: WebRequest,
46 | ): ResponseEntity? {
47 | log.error("MethodArgumentNotValidException : {}", e.message, e)
48 | val errorMessage: String? = e.bindingResult.allErrors[0].defaultMessage
49 | val errorResponse =
50 | ErrorResponse.of(e.javaClass.getSimpleName(), errorMessage!!)
51 | val response: GlobalResponse = GlobalResponse.fail(status.value(), errorResponse)
52 | return ResponseEntity.status(status).body(response)
53 | }
54 |
55 | /** Request Param Validation 예외 처리 */
56 | @ExceptionHandler(ConstraintViolationException::class)
57 | fun handleConstraintViolationException(e: ConstraintViolationException): ResponseEntity {
58 | log.error("ConstraintViolationException : {}", e.message, e)
59 |
60 | val bindingErrors: MutableMap = HashMap()
61 | e.constraintViolations
62 | .forEach(
63 | Consumer { constraintViolation: ConstraintViolation<*> ->
64 | val propertyPath =
65 | listOf(
66 | *constraintViolation
67 | .propertyPath
68 | .toString()
69 | .split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray(),
70 | )
71 | val path =
72 | propertyPath.stream()
73 | .skip(propertyPath.size - 1L)
74 | .findFirst()
75 | .orElse(null)
76 | bindingErrors[path] = constraintViolation.message
77 | },
78 | )
79 |
80 | val errorResponse =
81 | ErrorResponse.of(e.javaClass.simpleName, bindingErrors.toString())
82 | val response: GlobalResponse =
83 | GlobalResponse.fail(HttpStatus.BAD_REQUEST.value(), errorResponse)
84 | return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response)
85 | }
86 |
87 | /** PathVariable, RequestParam, RequestHeader, RequestBody 에서 타입이 일치하지 않을 경우 발생 */
88 | @ExceptionHandler(MethodArgumentTypeMismatchException::class)
89 | protected fun handleMethodArgumentTypeMismatchException(
90 | e: MethodArgumentTypeMismatchException,
91 | ): ResponseEntity {
92 | log.error("MethodArgumentTypeMismatchException : {}", e.message, e)
93 | val errorCode: ErrorCode = ErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH
94 | val errorResponse =
95 | ErrorResponse.of(e.javaClass.getSimpleName(), errorCode.message)
96 | val response: GlobalResponse =
97 | GlobalResponse.fail(errorCode.status.value(), errorResponse)
98 | return ResponseEntity.status(errorCode.status).body(response)
99 | }
100 |
101 | /** 지원하지 않은 HTTP method 호출 할 경우 발생 */
102 | override fun handleHttpRequestMethodNotSupported(
103 | e: HttpRequestMethodNotSupportedException,
104 | headers: HttpHeaders,
105 | status: HttpStatusCode,
106 | request: WebRequest,
107 | ): ResponseEntity? {
108 | log.error("HttpRequestMethodNotSupportedException : {}", e.message, e)
109 | val errorCode: ErrorCode = ErrorCode.METHOD_NOT_ALLOWED
110 | val errorResponse =
111 | ErrorResponse.of(e.javaClass.getSimpleName(), errorCode.message)
112 | val response: GlobalResponse =
113 | GlobalResponse.fail(errorCode.status.value(), errorResponse)
114 | return ResponseEntity.status(errorCode.status).body(response)
115 | }
116 |
117 | /** CustomException 예외 처리 */
118 | @ExceptionHandler(CustomException::class)
119 | fun handleCustomException(e: CustomException): ResponseEntity {
120 | log.error("CustomException : {}", e.message, e)
121 | val errorCode: ErrorCode = e.errorCode
122 | val errorResponse =
123 | ErrorResponse.of(errorCode.name, errorCode.message)
124 | val response: GlobalResponse =
125 | GlobalResponse.fail(errorCode.status.value(), errorResponse)
126 | return ResponseEntity.status(errorCode.status).body(response)
127 | }
128 |
129 | /** 500번대 에러 처리 */
130 | @ExceptionHandler(Exception::class)
131 | protected fun handleException(e: Exception): ResponseEntity {
132 | log.error("Internal Server Error : {}", e.message, e)
133 | val internalServerError: ErrorCode = ErrorCode.INTERNAL_SERVER_ERROR
134 | val errorResponse =
135 | ErrorResponse.of(e.javaClass.simpleName, internalServerError.message)
136 | val response: GlobalResponse =
137 | GlobalResponse.fail(internalServerError.status.value(), errorResponse)
138 | return ResponseEntity.status(internalServerError.status).body(response)
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/global/exception/CustomException.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.global.exception
2 |
3 | open class CustomException(
4 | val errorCode: ErrorCode,
5 | val data: Any? = null,
6 | ) : RuntimeException(errorCode.message)
7 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/global/exception/ErrorCode.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.global.exception
2 | import org.springframework.http.HttpStatus
3 |
4 | enum class ErrorCode(
5 | val status: HttpStatus,
6 | val message: String,
7 | ) {
8 | // Common
9 | METHOD_ARGUMENT_TYPE_MISMATCH(HttpStatus.BAD_REQUEST, "요청 한 값 타입이 잘못되어 binding에 실패하였습니다."),
10 | METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "지원하지 않는 HTTP method 입니다."),
11 | INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 오류입니다."),
12 |
13 | // Member
14 | MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 회원을 찾을 수 없습니다."),
15 |
16 | // Security
17 | AUTH_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, "시큐리티 인증 정보를 찾을 수 없습니다."),
18 | EXPIRED_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "만료된 JWT Access 토큰입니다."),
19 | EXPIRED_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "만료된 JWT Refresh 토큰입니다."),
20 | NOT_EXIST_TOKEN(HttpStatus.UNAUTHORIZED, "토큰이 존재하지 않습니다."),
21 | MEMBER_ALREADY_REGISTERED(HttpStatus.CONFLICT, "이미 가입된 회원입니다."),
22 | PASSWORD_NOT_MATCHES(HttpStatus.UNAUTHORIZED, "비밀번호가 일치하지 않습니다."),
23 | INVALID_AUTH_TOKEN(HttpStatus.UNAUTHORIZED, "토큰 검증에 실패했습니다."),
24 |
25 | // TodoEntity
26 | TODO_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 Todo를 찾을 수 없습니다."),
27 | TODO_ALREADY_COMPLETED(HttpStatus.BAD_REQUEST, "이미 완료된 Todo입니다."),
28 | TODO_ALREADY_DELETED(HttpStatus.BAD_REQUEST, "이미 삭제된 Todo입니다."),
29 | TODO_ALREADY_EXIST(HttpStatus.CONFLICT, "이미 존재하는 Todo입니다."),
30 | }
31 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/global/filter/JwtAuthenticationFilter.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.global.filter
2 | import com.sipe.week5.domain.auth.exception.AuthenticationInvalidTokenException
3 | import com.sipe.week5.domain.auth.exception.AuthenticationTokenNotExistException
4 | import com.sipe.week5.domain.member.domain.MemberRole
5 | import com.sipe.week5.global.config.security.JwtTokenProvider
6 | import com.sipe.week5.global.config.security.PrincipalDetails
7 | import jakarta.servlet.FilterChain
8 | import jakarta.servlet.http.HttpServletRequest
9 | import jakarta.servlet.http.HttpServletResponse
10 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
11 | import org.springframework.security.core.Authentication
12 | import org.springframework.security.core.context.SecurityContextHolder
13 | import org.springframework.security.core.userdetails.UserDetails
14 | import org.springframework.web.filter.OncePerRequestFilter
15 | import org.springframework.web.servlet.HandlerExceptionResolver
16 |
17 | class JwtAuthenticationFilter(
18 | private val jwtTokenProvider: JwtTokenProvider,
19 | private val handlerExceptionResolver: HandlerExceptionResolver,
20 | ) : OncePerRequestFilter() {
21 | override fun doFilterInternal(
22 | request: HttpServletRequest,
23 | response: HttpServletResponse,
24 | filterChain: FilterChain,
25 | ) {
26 | try {
27 | val authenticationHeader = request.getHeader("Authorization") ?: throw AuthenticationTokenNotExistException()
28 | val accessToken =
29 | if (authenticationHeader.startsWith("Bearer ")) {
30 | authenticationHeader.substring(7)
31 | } else {
32 | throw AuthenticationInvalidTokenException()
33 | }
34 |
35 | val accessTokenDto = jwtTokenProvider.parseAccessToken(accessToken)
36 | println("memberId, memberRole: ${accessTokenDto.memberId}, ${accessTokenDto.memberRole}")
37 | setAuthenticationToContext(accessTokenDto.memberId, accessTokenDto.memberRole)
38 | return filterChain.doFilter(request, response)
39 | } catch (e: Exception) {
40 | handlerExceptionResolver.resolveException(request, response, null, e)
41 | }
42 | }
43 |
44 | private fun setAuthenticationToContext(
45 | memberId: Long,
46 | memberRole: MemberRole,
47 | ) {
48 | val userDetails: UserDetails = PrincipalDetails(memberId, memberRole)
49 |
50 | val authentication: Authentication =
51 | UsernamePasswordAuthenticationToken(
52 | userDetails,
53 | null,
54 | userDetails.authorities,
55 | )
56 | SecurityContextHolder.getContext().authentication = authentication
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/global/util/logging/LoggingUtils.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.global.util.logging
2 |
3 | import org.slf4j.Logger
4 | import org.slf4j.LoggerFactory
5 |
6 | /* Logger 인스턴스
7 | * Logger 인스턴스를 생성하는 함수
8 | * inline 함수로 선언하여 Logger 인스턴스를 생성하는 코드를 호출하는 위치에 삽입
9 | * reified는 코틀린에서 제네릭 타입 파라미터를 실체화(reify)하는 키워드
10 | * lazy를 사용하여 로그 인스턴스가 필요할 때까지 로그 객체의 생성을 지연
11 | * 일반적으로, 제네릭 타입은 런타임에 지워지는데, 이를 타입 소거(type erasure)라고 한다. 즉, 런타임에는 제네릭 타입 정보를 알 수 없다.
12 |
13 | * 참고 링크
14 | * https://cheese10yun.github.io/kotlin-pattern/#null
15 | * 사용 예시
16 | * private val log by logger()
17 | * log.info("Hello, World!")
18 | * 포인트 정리
19 | 효율적인 자원 사용: lazy를 사용함으로써 로거의 초기화를 실제 로깅이 필요한 시점까지 지연. 이는 자원을 효율적으로 사용
20 | 코드 중복 감소: logger() 확장 함수를 사용하면 모든 클래스에서 동일한 로깅 구성을 쉽게 재사용성 용이
21 | 유지보수의 용이성: 로그 인스턴스 생성 코드를 한 곳에 집중시키므로, 로거 설정을 변경할 때 다수의 클래스를 수정할 필요가 없다.
22 | */
23 | inline fun T.logger(): Lazy = lazy { LoggerFactory.getLogger(T::class.java) }
24 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/global/util/member/MemberUtil.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.global.util.member
2 |
3 | import com.sipe.week5.domain.member.domain.Member
4 | import com.sipe.week5.domain.member.infrastructure.SuspendableMemberRepository
5 | import com.sipe.week5.global.exception.CustomException
6 | import com.sipe.week5.global.exception.ErrorCode
7 | import com.sipe.week5.global.util.security.SecurityUtil
8 | import org.springframework.stereotype.Component
9 |
10 | @Component
11 | class MemberUtil(
12 | private val securityUtil: SecurityUtil,
13 | private val suspendableMemberRepository: SuspendableMemberRepository,
14 | ) {
15 | suspend fun getCurrentMember(): Member =
16 | suspendableMemberRepository
17 | .findById(securityUtil.currentMemberId) ?: throw CustomException(ErrorCode.MEMBER_NOT_FOUND)
18 |
19 | suspend fun getMemberByMemberId(memberId: Long): Member =
20 | suspendableMemberRepository
21 | .findById(memberId) ?: throw CustomException(ErrorCode.MEMBER_NOT_FOUND)
22 | }
23 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/kotlin/com/sipe/week5/global/util/security/SecurityUtil.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.global.util.security
2 |
3 | import com.sipe.week5.global.exception.CustomException
4 | import com.sipe.week5.global.exception.ErrorCode
5 | import org.springframework.security.core.Authentication
6 | import org.springframework.security.core.context.SecurityContextHolder
7 | import org.springframework.stereotype.Component
8 |
9 | @Component
10 | class SecurityUtil {
11 | val currentMemberId: Long
12 | get() {
13 | val authentication: Authentication = SecurityContextHolder.getContext().authentication
14 |
15 | try {
16 | return authentication.name.toLong()
17 | } catch (e: Exception) {
18 | throw CustomException(ErrorCode.AUTH_NOT_FOUND)
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/main/resources/application.yaml:
--------------------------------------------------------------------------------
1 | spring:
2 | application:
3 | name: week5
4 | # R2DBC 설정
5 | r2dbc:
6 | url: r2dbc:mysql://${R2DBC_HOST}:${R2DBC_PORT}/${DB_NAME}?useUnicode=true&charset=utf8mb4&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull
7 | username: ${R2DBC_USER}
8 | password: ${R2DBC_PASSWORD}
9 | # R2DBC 연결 풀 설정
10 | r2dbc.pool:
11 | enabled: true
12 | initial-size: 5
13 | max-size: 20
14 | validation-query: SELECT 1
15 | sql:
16 | init:
17 | mode: never # for production
18 |
19 |
20 | jwt:
21 | access-token-secret: ${JWT_ACCESS_TOKEN_SECRET:}
22 | refresh-token-secret: ${JWT_REFRESH_TOKEN_SECRET:}
23 | access-token-expiration-time: ${JWT_ACCESS_TOKEN_EXPIRATION_TIME:7200}
24 | refresh-token-expiration-time: ${JWT_REFRESH_TOKEN_EXPIRATION_TIME:604800}
25 |
26 | # LOGGING
27 | logging:
28 | level:
29 | root: INFO
30 | com.sipe: DEBUG
31 | org.springframework.r2dbc: DEBUG
32 | reactor.netty.http.client: DEBUG
33 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/test/kotlin/com/sipe/week5/Week5ApplicationTests.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5
2 |
3 | import org.junit.jupiter.api.Test
4 | import org.springframework.boot.test.context.SpringBootTest
5 |
6 | @SpringBootTest
7 | class Week5ApplicationTests {
8 | @Test
9 | fun contextLoads() {
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/test/kotlin/com/sipe/week5/domain/todo/application/TodoServiceTest.kt:
--------------------------------------------------------------------------------
1 | package com.sipe.week5.domain.todo.application
2 |
3 | import com.sipe.week5.domain.member.domain.Member
4 | import com.sipe.week5.domain.member.domain.MemberRole
5 | import com.sipe.week5.domain.member.infrastructure.ReactiveMemberRepository
6 | import com.sipe.week5.domain.member.infrastructure.SuspendableMemberRepository
7 | import com.sipe.week5.domain.todo.domain.TodoEntity
8 | import com.sipe.week5.domain.todo.domain.TodoStatus
9 | import com.sipe.week5.domain.todo.infrastructure.ReactiveTodoRepository
10 | import com.sipe.week5.domain.todo.infrastructure.SuspendableTodoRepository
11 | import com.sipe.week5.global.config.security.PrincipalDetails
12 | import kotlinx.coroutines.flow.toList
13 | import kotlinx.coroutines.runBlocking
14 | import org.assertj.core.api.Assertions.assertThat
15 | import org.junit.jupiter.api.BeforeEach
16 | import org.junit.jupiter.api.Test
17 | import org.springframework.beans.factory.annotation.Autowired
18 | import org.springframework.boot.test.context.SpringBootTest
19 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
20 | import org.springframework.security.core.Authentication
21 | import org.springframework.security.core.context.SecurityContextHolder
22 | import org.springframework.security.crypto.password.PasswordEncoder
23 | import org.springframework.test.context.ActiveProfiles
24 | import org.springframework.transaction.annotation.Transactional
25 | import java.time.LocalDate
26 | import kotlin.system.measureTimeMillis
27 |
28 | @ActiveProfiles("test")
29 | @Transactional
30 | @SpringBootTest
31 | class TodoServiceTest
32 | @Autowired
33 | constructor(
34 | private val todoService: TodoService,
35 | private val suspendableMemberRepository: SuspendableMemberRepository,
36 | private val reactiveMemberRepository: ReactiveMemberRepository,
37 | private val suspendableTodoRepository: SuspendableTodoRepository,
38 | private val reactiveTodoRepository: ReactiveTodoRepository,
39 | private val passwordEncoder: PasswordEncoder,
40 | ) {
41 | private lateinit var testMember: Member
42 | private var todos = mutableListOf()
43 |
44 | @BeforeEach
45 | fun setUp() {
46 | runBlocking {
47 | testMember =
48 | suspendableMemberRepository.save(
49 | Member(
50 | loginId = "test",
51 | password = passwordEncoder.encode("test"),
52 | username = "test",
53 | role = MemberRole.USER,
54 | ),
55 | )
56 |
57 | for (i in 1..50_000_000) {
58 | todos.add(
59 | TodoEntity(
60 | title = "test $i",
61 | content = "test $i",
62 | dueDate = LocalDate.now(),
63 | memberId = testMember.id,
64 | ),
65 | )
66 | }
67 | suspendableTodoRepository.saveAll(todos)
68 |
69 | val principalDetails = PrincipalDetails(testMember.id, testMember.role)
70 | val authentication: Authentication =
71 | UsernamePasswordAuthenticationToken(principalDetails, null, principalDetails.authorities)
72 | SecurityContextHolder.getContext().authentication = authentication
73 | }
74 | }
75 |
76 | @Test
77 | fun `test performance of suspendableTodoRepository for findTodoByStatus`() =
78 | runBlocking {
79 | val fetchTime =
80 | measureTimeMillis {
81 | suspendableTodoRepository.findAllByStatus(TodoStatus.TODO).toList()
82 | }
83 |
84 | println("Time taken to fetch records using suspendableTodoRepository: $fetchTime ms")
85 | }
86 |
87 | @Test
88 | fun `test performance of reactiveTodoRepository for findTodoByStatus`() {
89 | val acceptableTimeMs = 1000L
90 | val fetchTime =
91 | measureTimeMillis {
92 | reactiveTodoRepository.findAllByStatus(TodoStatus.TODO).collectList().block()
93 | }
94 |
95 | // Assert performance meets requirements
96 | assertThat(fetchTime).isLessThan(acceptableTimeMs)
97 |
98 | // Cleanup
99 | reactiveMemberRepository.deleteAll().block()
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/week_5/char-yb/week5/src/test/resources/application-test.yaml:
--------------------------------------------------------------------------------
1 | spring:
2 | config:
3 | activate:
4 | on-profile: "test"
5 |
6 | r2dbc:
7 | url: r2dbc:h2:mem:///test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false;MODE=MYSQL
8 | username: sa
9 | password:
10 |
11 | r2dbc.pool:
12 | enabled: true
13 | initial-size: 5
14 | max-size: 20
15 | validation-query: SELECT 1
16 |
--------------------------------------------------------------------------------
/week_5/positive/.gitattributes:
--------------------------------------------------------------------------------
1 | /gradlew text eol=lf
2 | *.bat text eol=crlf
3 | *.jar binary
4 |
--------------------------------------------------------------------------------
/week_5/positive/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | .gradle
3 | build/
4 | !gradle/wrapper/gradle-wrapper.jar
5 | !**/src/main/**/build/
6 | !**/src/test/**/build/
7 |
8 | ### STS ###
9 | .apt_generated
10 | .classpath
11 | .factorypath
12 | .project
13 | .settings
14 | .springBeans
15 | .sts4-cache
16 | bin/
17 | !**/src/main/**/bin/
18 | !**/src/test/**/bin/
19 |
20 | ### IntelliJ IDEA ###
21 | .idea
22 | *.iws
23 | *.iml
24 | *.ipr
25 | out/
26 | !**/src/main/**/out/
27 | !**/src/test/**/out/
28 |
29 | ### NetBeans ###
30 | /nbproject/private/
31 | /nbbuild/
32 | /dist/
33 | /nbdist/
34 | /.nb-gradle/
35 |
36 | ### VS Code ###
37 | .vscode/
38 |
39 | ### Kotlin ###
40 | .kotlin
41 |
--------------------------------------------------------------------------------
/week_5/positive/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("jvm") version "1.9.25"
3 | kotlin("plugin.spring") version "1.9.25"
4 | id("org.springframework.boot") version "3.4.0"
5 | id("io.spring.dependency-management") version "1.1.6"
6 | }
7 |
8 | group = "com.todolist"
9 | version = "0.0.1-SNAPSHOT"
10 |
11 | java {
12 | toolchain {
13 | languageVersion.set(JavaLanguageVersion.of(17))
14 | }
15 | }
16 |
17 |
18 | repositories {
19 | mavenCentral()
20 | }
21 |
22 | dependencies {
23 | implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
24 | implementation("org.springframework.boot:spring-boot-starter-webflux")
25 | implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
26 | implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
27 | implementation("org.jetbrains.kotlin:kotlin-reflect")
28 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
29 | developmentOnly("org.springframework.boot:spring-boot-devtools")
30 | runtimeOnly("com.h2database:h2")
31 | runtimeOnly("io.r2dbc:r2dbc-h2")
32 | testImplementation("org.springframework.boot:spring-boot-starter-test")
33 | testImplementation("io.projectreactor:reactor-test")
34 | testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
35 | testRuntimeOnly("org.junit.platform:junit-platform-launcher")
36 | }
37 |
38 | kotlin {
39 | compilerOptions {
40 | freeCompilerArgs.addAll("-Xjsr305=strict")
41 | }
42 | }
43 |
44 | tasks.withType {
45 | useJUnitPlatform()
46 | }
47 |
--------------------------------------------------------------------------------
/week_5/positive/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipe-team/3_1_spring_webflux_coroutines/09703b390539c683ef866080a6a9ddc0b8a028df/week_5/positive/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/week_5/positive/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/week_5/positive/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/week_5/positive/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "positive"
2 |
--------------------------------------------------------------------------------
/week_5/positive/src/main/kotlin/com/todolist/positive/PositiveApplication.kt:
--------------------------------------------------------------------------------
1 | package com.todolist.positive
2 |
3 | import org.springframework.boot.autoconfigure.SpringBootApplication
4 | import org.springframework.boot.runApplication
5 |
6 | @SpringBootApplication
7 | class PositiveApplication
8 |
9 | fun main(args: Array) {
10 | runApplication(*args)
11 | }
12 |
--------------------------------------------------------------------------------
/week_5/positive/src/main/kotlin/com/todolist/positive/controller/TodoController.kt:
--------------------------------------------------------------------------------
1 | package com.todolist.positive.controller
2 |
3 | import Todo
4 | import TodoStatus
5 | import com.todolist.positive.service.TodoService
6 | import kotlinx.coroutines.flow.Flow
7 | import org.springframework.web.bind.annotation.*
8 | import org.springframework.http.ResponseEntity
9 | import kotlinx.coroutines.flow.*
10 |
11 | @RestController
12 | @RequestMapping("/todos")
13 | class TodoController(private val todoService: TodoService) {
14 |
15 | // Todo 생성 API
16 | @PostMapping
17 | suspend fun createTodo(@RequestBody todo: Todo): Todo? {
18 | return todoService.createTodo(todo)
19 | }
20 |
21 | // Todo 단건 조회 API
22 | @GetMapping("/{id}")
23 | suspend fun getTodoById(@PathVariable id: Long): Todo? {
24 | return todoService.getTodoById(id)
25 | }
26 |
27 | // 상태별 Todo 조회 API
28 | @GetMapping
29 | suspend fun getTodosByStatus(@RequestParam status: TodoStatus): ResponseEntity> {
30 | val todos = todoService.getTodosByStatus(status)
31 | return ResponseEntity.ok(todos)
32 | }
33 |
34 | // 전체 Todo 조회 API
35 | @GetMapping("/all")
36 | suspend fun getAllTodos(): ResponseEntity> {
37 | val todos = todoService.getAllTodos()
38 | return ResponseEntity.ok(todos)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/week_5/positive/src/main/kotlin/com/todolist/positive/model/Todo.kt:
--------------------------------------------------------------------------------
1 | import org.springframework.data.annotation.Id
2 | import org.springframework.data.relational.core.mapping.Table
3 |
4 |
5 | @Table("todo")
6 | data class Todo(
7 | @Id
8 | val id: Long? = null,
9 | val title: String,
10 | val description: String,
11 | val status: TodoStatus
12 | )
13 |
14 | enum class TodoStatus {
15 | PENDING, IN_PROGRESS, COMPLETED
16 | }
--------------------------------------------------------------------------------
/week_5/positive/src/main/kotlin/com/todolist/positive/repository/TodoRepository.kt:
--------------------------------------------------------------------------------
1 | package com.todolist.positive.repository
2 |
3 | import Todo
4 | import TodoStatus
5 | import org.springframework.data.r2dbc.repository.Query
6 | import org.springframework.data.repository.reactive.ReactiveCrudRepository
7 | import org.springframework.stereotype.Repository
8 | import reactor.core.publisher.Flux
9 |
10 | @Repository
11 | interface TodoRepository : ReactiveCrudRepository {
12 |
13 | // 상태로 조회하는 기본 메서드
14 | fun findByStatus(status: TodoStatus): Flux
15 |
16 | // Custom 쿼리 예시 (Optional)
17 | @Query("SELECT * FROM todo WHERE status = :status")
18 | fun findTodosByStatusCustomQuery(status: String): Flux
19 | }
20 |
--------------------------------------------------------------------------------
/week_5/positive/src/main/kotlin/com/todolist/positive/service/TodoService.kt:
--------------------------------------------------------------------------------
1 | package com.todolist.positive.service
2 |
3 | import Todo
4 | import TodoStatus
5 | import com.todolist.positive.repository.TodoRepository
6 |
7 | import kotlinx.coroutines.flow.Flow
8 | import kotlinx.coroutines.flow.map
9 | import kotlinx.coroutines.reactive.asFlow
10 | import kotlinx.coroutines.reactive.awaitFirst
11 | import kotlinx.coroutines.reactive.awaitFirstOrNull
12 | import org.springframework.stereotype.Service
13 | @Service
14 | class TodoService(private val todoRepository: TodoRepository) {
15 |
16 | suspend fun createTodo(todo: Todo): Todo {
17 | // ID가 null이어야 새로 생성
18 | if (todo.id != null) {
19 | throw IllegalArgumentException("ID should not be provided for new Todo creation.")
20 | }
21 | return todoRepository.save(todo).awaitFirst() // save 결과 반환
22 | }
23 |
24 | suspend fun getTodoById(id: Long): Todo? {
25 | return todoRepository.findById(id).awaitFirstOrNull()
26 | }
27 |
28 | fun getTodosByStatus(status: TodoStatus): Flow {
29 | return todoRepository.findByStatus(status).asFlow()
30 | }
31 |
32 | suspend fun getAllTodos(): Flow {
33 | return todoRepository.findAll().asFlow()
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/week_5/positive/src/main/resources/application.yml:
--------------------------------------------------------------------------------
1 | spring:
2 | reactor:
3 | netty:
4 | worker-count: 4 # WebFlux? ?? ?? ? ??
5 |
6 | r2dbc:
7 | url: r2dbc:h2:mem:///testdb;DB_CLOSE_DELAY=-1;MODE=PostgreSQL
8 | h2:
9 | console:
10 | enabled: true
11 |
12 |
13 | server:
14 | port: 8080
15 | management:
16 | endpoints:
17 | web:
18 | exposure:
19 | include: "*"
20 |
--------------------------------------------------------------------------------
/week_5/positive/src/main/resources/schema.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE todo (
2 | id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
3 | title VARCHAR(255) NOT NULL,
4 | description TEXT NOT NULL,
5 | status VARCHAR(50) NOT NULL
6 | );
7 |
--------------------------------------------------------------------------------
/week_5/positive/src/test/kotlin/com/todolist/positive/PositiveApplicationTests.kt:
--------------------------------------------------------------------------------
1 | package com.todolist.positive
2 |
3 | import org.junit.jupiter.api.Test
4 | import org.springframework.boot.test.context.SpringBootTest
5 |
6 | @SpringBootTest
7 | class PositiveApplicationTests {
8 |
9 | @Test
10 | fun contextLoads() {
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------